diff --git a/.env.example b/.env.example
index 78d1bd5acf..20c43643a2 100644
--- a/.env.example
+++ b/.env.example
@@ -417,6 +417,15 @@ ALLOW_API_KEY_REVEAL=false
# by OMNIROUTE_CHAT_MAX_HEAVY_IN_FLIGHT and the heap-pressure shed instead. Set a positive
# value only on memory-constrained deployments that need a hard ceiling.
# OMNIROUTE_CHAT_HARD_MAX_MESSAGES=0
+
+# Skip OmniRoute's local context-window and max-input-token check for direct
+# single-model requests. Default: false (dangerous opt-in).
+# The upstream provider still enforces its real limits, so enabling this can
+# replace an early OmniRoute 400 with an upstream context-length error.
+# Prompt compression and the model's own output-token cap remain active.
+# Also configurable from Dashboard > Settings > Feature Flags; no restart is
+# required. Used by: src/shared/utils/featureFlags.ts and open-sse/handlers/chatCore.ts.
+# DISABLE_CONTEXT_WINDOW_CHECKS=false
# How long a heavy request waits for heavyweight capacity before a retryable 503.
# A short bounded wait serializes agent bursts instead of an instant 503; 0 = instant.
# Default 2000 (2s).
@@ -705,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.
@@ -885,6 +899,14 @@ NEXT_PUBLIC_ENABLE_SOCKS5_PROXY=true
# Set to 0/false/off to skip compression entirely. Default: rtk
# OMNIROUTE_MCP_DESCRIPTION_COMPRESSION=rtk
+# Abort budget (ms) for MCP-server internal management reads (health, resilience,
+# combos, quota, usage). Default: 10000. Used by: open-sse/mcp-server/fetchTimeout.ts
+# OMNIROUTE_MCP_FETCH_TIMEOUT_MS=10000
+
+# Abort budget (ms) for MCP hops that wait on a provider (route_request, web_search,
+# web_fetch). Default: 60000. Used by: open-sse/mcp-server/fetchTimeout.ts
+# OMNIROUTE_MCP_UPSTREAM_TIMEOUT_MS=60000
+
# Model catalog sync interval in hours.
# Used by: src/shared/services/modelSyncScheduler.ts — periodic model refresh.
# Default: 24
@@ -1361,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).
@@ -1415,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
@@ -2140,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 8db8504007..3dfad903a7 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -50,13 +50,13 @@ updates:
# bumps; majors here need their own PR and a deliberate migration review.
- dependency-name: "ioredis"
update-types: ["version-update:semver-major"]
- # @huggingface/transformers is HARD-PINNED at 3.5.2 (exact, no caret) — FROZEN.
- # It is load-bearing for the LLMLingua ONNX compression engine (open-sse/services/
- # compression/engines/llmlingua/ — worker.ts pins @huggingface/transformers@3.5.2)
- # and for local memory embeddings (src/lib/memory/embedding/transformersLocal.ts),
- # and was VPS-validated at 3.5.2 (#4014). 4.x breaks both, and even 3.x minors must
- # be re-validated on the VPS — so freeze ALL auto-bumps (no update-types = ignore
- # every version). Migrate it intentionally, not via dependabot (#4050).
+ # @huggingface/transformers is VPS-validated at ^4.2.0 (migrated intentionally in
+ # #9962). It is load-bearing for the LLMLingua ONNX compression engine (open-sse/
+ # services/compression/engines/llmlingua/ — @atjsh/llmlingua-2@2.0.5 peers on
+ # "@huggingface/transformers": "^3.5.2 || ^4.0.0") and for local memory embeddings
+ # (src/lib/memory/embedding/transformersLocal.ts). Further majors must be re-validated
+ # on the VPS — so keep auto-bumps frozen (no update-types = ignore every version).
+ # Migrate it intentionally, not via dependabot (#4050).
- dependency-name: "@huggingface/transformers"
- package-ecosystem: "github-actions"
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
index 36f8254977..d12585363a 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 6e4adc0192..d8a65576dc 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/@omniroute/opencode-plugin/src/index.ts b/@omniroute/opencode-plugin/src/index.ts
index 7c196aeccb..be985361c9 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,
@@ -1676,7 +1692,8 @@ export interface OmniRouteRawAutoCombo {
export type OmniRouteAutoCombosFetcher = (
baseURL: string,
apiKey: string,
- timeoutMs?: number
+ timeoutMs?: number,
+ logger?: OmniRouteLoggerSink
) => Promise;
/**
@@ -1688,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+$/, "");
@@ -1709,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;
@@ -1735,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 {
@@ -2935,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;
@@ -3130,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
@@ -3206,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.`
);
@@ -3238,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.
@@ -3248,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);
}
}
@@ -3261,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.
@@ -3275,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);
}
}
@@ -3293,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);
}
}
@@ -3307,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
);
}
@@ -3327,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 + ` +
@@ -3608,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.`);
}
}
}
@@ -3628,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.`
);
}
@@ -4273,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();
@@ -4652,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.`
);
}
@@ -4674,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;
@@ -5347,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})`
@@ -5399,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
}
@@ -5420,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",
@@ -5533,7 +5554,8 @@ export function createOmniRouteConfigHook(
localRawEnrichment,
localRawCompressionCombos,
localRawConnections,
- localRawAutoCombos
+ localRawAutoCombos,
+ logger
);
const inputWithProvider2 = input as { provider?: Record };
if (inputWithProvider2.provider) {
@@ -5623,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/log-level.test.ts b/@omniroute/opencode-plugin/tests/log-level.test.ts
index 7e73cb4eb8..58566aba6d 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 2168ae70b9..d4a7e7eb80 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -46,7 +46,7 @@ Repository map and Reference Documentation sections below.
## Project at a Glance
-**OmniRoute** — unified AI proxy/router. One endpoint, 343 LLM providers, auto-fallback.
+**OmniRoute** — unified AI proxy/router. One endpoint, 346 LLM providers, auto-fallback.
| Layer | Location | Purpose |
| ------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
@@ -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 94438a0922..9c01b39a7a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -169,6 +169,7 @@ _Living section — regenerated 2026-08-12 from all cycle commits (cycle open `e
- **security(search)**: block SSRF via `/v1/search` `provider_options.baseUrl` for the Firecrawl search provider — the client-controlled override is now validated as a public URL before it is used to build the server-side fetch target, so a caller with a valid API key can no longer redirect search requests at loopback, RFC1918, or cloud-metadata hosts — thanks @zmf963
- **providers**: honor `PATCH /api/providers/[id]` so `omniroute providers rotate` stops 405ing (the OpenAPI spec and CLI already use PATCH) (PR #10366)
+- **cli**: route provider test commands through configured connection test endpoints (#10570)
- **executors**: fix internal timeout misclassified as client disconnect (499) for 7 niche executors — pass TimeoutError reason to controller.abort() (#8197 side-finding)
- test(combo): guard auto/best-free never leaks the combo name as a model (#7754)
- fix(vision-bridge): describe-model no longer returns unreachable "openai/gpt-4o-mini" when every vision-capable provider is unreachable on the instance — returns null instead and surfaces a clear error (#8430)
diff --git a/Dockerfile b/Dockerfile
index de9b5a1499..8eca2c3bd2 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -173,7 +173,7 @@ COPY . ./
RUN --mount=type=cache,id=s/92ca8a61-c1ba-421f-a389-d48ac7258c2d-next-cache,target=/app/.build/next/cache \
mkdir -p /app/data \
&& npm run build \
- && node --input-type=module -e "import { createRequire } from 'node:module'; import { pathToFileURL } from 'node:url'; const standaloneRoot = '/app/.build/next/standalone/node_modules/'; const require = createRequire('/app/.build/next/standalone/package.json'); for (const pkg of ['@atjsh/llmlingua-2', '@huggingface/transformers', '@tensorflow/tfjs', 'js-tiktoken']) { const resolved = require.resolve(pkg); if (!resolved.startsWith(standaloneRoot)) throw new Error(pkg + ' resolved outside standalone: ' + resolved); await import(pathToFileURL(resolved).href); } const onnxRuntime = require.resolve('onnxruntime-node'); if (!onnxRuntime.startsWith(standaloneRoot)) throw new Error('onnxruntime-node resolved outside standalone: ' + onnxRuntime); await import(pathToFileURL(onnxRuntime).href);"
+ && node --input-type=module -e "import { createRequire } from 'node:module'; import { pathToFileURL } from 'node:url'; const standaloneRoot = '/app/.build/next/standalone/node_modules/'; const require = createRequire('/app/.build/next/standalone/package.json'); for (const pkg of ['@atjsh/llmlingua-2', '@huggingface/transformers', 'js-tiktoken']) { const resolved = require.resolve(pkg); if (!resolved.startsWith(standaloneRoot)) throw new Error(pkg + ' resolved outside standalone: ' + resolved); await import(pathToFileURL(resolved).href); } const onnxRuntime = require.resolve('onnxruntime-node'); if (!onnxRuntime.startsWith(standaloneRoot)) throw new Error('onnxruntime-node resolved outside standalone: ' + onnxRuntime); await import(pathToFileURL(onnxRuntime).href);"
# ── Runner base ────────────────────────────────────────────────────────────
FROM base AS runner-base
diff --git a/README.md b/README.md
index fc66a61a71..390acbe472 100644
--- a/README.md
+++ b/README.md
@@ -7,7 +7,7 @@
# 🚀 OmniRoute — The Free AI Gateway
-
+
@@ -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 **343-provider catalog**. → [Providers](docs/reference/PROVIDER_REFERENCE.md)
+- **🤝 More providers & agents** — Cursor Cloud Agent, Grok Build (xAI) with browser + OAuth login, Ollama first-class card, Claude Opus 5 & Sonnet 5, Kimi official partnership (Code/Web/Moonshot), Zed, Requesty, SenseNova, Yuanbao, Agnes AI… and a refreshed **346-provider catalog**. → [Providers](docs/reference/PROVIDER_REFERENCE.md)
- **📡 Routing transparency** — every response carries an `X-OmniRoute-Decision` header naming the strategy/provider/latency that served it, a new `cache-optimized` combo strategy + Auto-Combo `cacheAffinity` factor route repeat requests back to the connection holding the cached prefix, and a read-only `/v1/auto-combo/{channel}/candidates` endpoint exposes an `auto/*` channel's live candidate pool. → [Auto-Combo](docs/routing/AUTO-COMBO.md)
- **⚡ Local performance & infra** — one-click local Redis, Cloudflare Workers / Deno Deploy relay deployers, Bifrost & Mux as supervised embedded services. → [Embedded Services](docs/frameworks/EMBEDDED-SERVICES.md)
@@ -642,11 +642,11 @@ of your shell history. → [CLI Integrations](docs/guides/CLI-INTEGRATIONS.md)
-## 🌐 343 AI Providers — 90+ Free
+## 🌐 346 AI Providers — 90+ Free
-> The most complete catalog of any open-source router: **343 providers**, **90+ with a free tier**, **56 free forever**.
+> The most complete catalog of any open-source router: **346 providers**, **90+ with a free tier**, **57 free forever**.
@@ -1174,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)
@@ -1497,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/commands/providers.mjs b/bin/cli/commands/providers.mjs
index 91d60cead8..eb872241bf 100644
--- a/bin/cli/commands/providers.mjs
+++ b/bin/cli/commands/providers.mjs
@@ -129,7 +129,34 @@ function buildTestInput(connection, apiKey) {
};
}
-async function runProviderTest(db, connection) {
+async function testProviderConnectionThroughServer(connection) {
+ try {
+ const res = await apiFetch(`/api/providers/${encodeURIComponent(connection.id)}/test`, {
+ method: "POST",
+ body: {},
+ retry: false,
+ timeout: 30000,
+ acceptNotOk: true,
+ });
+ const data = res.ok ? await res.json() : { valid: false, error: `HTTP ${res.status}` };
+ return {
+ connection: publicConnection(connection),
+ ...data,
+ valid: data.valid === true,
+ skipped: false,
+ };
+ } catch (error) {
+ return {
+ connection: publicConnection(connection),
+ valid: false,
+ skipped: false,
+ error: error instanceof Error ? error.message : String(error),
+ statusCode: null,
+ };
+ }
+}
+
+async function runProviderTest(db, connection, { serverUp = false } = {}) {
// Only API-key connections can be probed with a stored credential. OAuth /
// no-auth connections have nothing for testProviderApiKey() to send, and
// getProviderApiKey() throws for them by design — reporting that as a FAILED
@@ -151,6 +178,9 @@ async function runProviderTest(db, connection) {
// means the CLI has no probe recipe, not that the provider is unhealthy.
// Persisting it would overwrite a good test_status with a failure.
if (result.unsupported) {
+ if (serverUp) {
+ return testProviderConnectionThroughServer(connection);
+ }
return {
connection: publicConnection(connection),
...result,
@@ -266,6 +296,7 @@ export async function runTestCommand(selector, opts = {}) {
}
export async function runTestAllCommand(opts = {}) {
+ const serverUp = await isServerUp();
const { db } = await openOmniRouteDb();
try {
const connections = listProviderConnections(db);
@@ -280,7 +311,7 @@ export async function runTestAllCommand(opts = {}) {
});
continue;
}
- results.push(await runProviderTest(db, connection));
+ results.push(await runProviderTest(db, connection, { serverUp }));
}
if (opts.json) {
diff --git a/bin/cli/commands/test-provider.mjs b/bin/cli/commands/test-provider.mjs
index 8802f75cd1..ec10c24649 100644
--- a/bin/cli/commands/test-provider.mjs
+++ b/bin/cli/commands/test-provider.mjs
@@ -38,12 +38,19 @@ export async function runTestProviderCommand(provider, model, opts = {}) {
}
const targetProvider = provider || "anthropic";
- const targetModel = model || "claude-haiku-4-5-20251001";
+ const connections = await _loadConnections();
+ if (!connections) return 1;
+ const connection = _resolveConnection(connections, targetProvider, model);
+ if (!connection) {
+ console.error(`Provider connection not found: ${targetProvider}`);
+ return 1;
+ }
+ const targetModel = model || connection.defaultModel;
const repeat = opts.repeat && opts.repeat > 0 ? opts.repeat : 1;
const results = [];
for (let i = 0; i < repeat; i++) {
- const result = await _runSingleTest(targetProvider, targetModel);
+ const result = await _runSingleTest(connection, targetModel);
results.push(result);
}
@@ -70,18 +77,10 @@ export async function runTestProviderCommand(provider, model, opts = {}) {
}
async function _runAllProviders(opts) {
- const res = await apiFetch("/api/providers?limit=200", {
- retry: false,
- timeout: 5000,
- acceptNotOk: true,
- });
- if (!res.ok) {
- console.error(t("test.noServer"));
- return 1;
- }
- const data = await res.json();
- const connections = (data.connections ?? data.providers ?? data.items ?? data).filter(
- (c) => c.authType === "apikey" || c.testStatus !== "unavailable"
+ const loaded = await _loadConnections();
+ if (!loaded) return 1;
+ const connections = loaded.filter(
+ (c) => c.isActive !== false && (c.authType === "apikey" || c.testStatus !== "unavailable")
);
if (connections.length === 0) {
console.log(t("test.noProviders"));
@@ -89,6 +88,7 @@ async function _runAllProviders(opts) {
}
const providers = connections.map((c) => ({
+ connectionId: c.id,
provider: c.provider ?? c.id,
model: c.defaultModel ?? c.model,
}));
@@ -102,8 +102,8 @@ async function _runAllProviders(opts) {
}
const results = await Promise.all(
- providers.map(async ({ provider, model }) => {
- const r = await _runSingleTest(provider, model);
+ providers.map(async ({ connectionId, provider, model }) => {
+ const r = await _runSingleTest({ id: connectionId }, model);
return { provider, model, ...r };
})
);
@@ -123,6 +123,13 @@ async function _runAllProviders(opts) {
async function _runCompare(provider, opts) {
const targetProvider = provider || "anthropic";
+ const connections = await _loadConnections();
+ if (!connections) return 1;
+ const connection = _resolveConnection(connections, targetProvider);
+ if (!connection) {
+ console.error(`Provider connection not found: ${targetProvider}`);
+ return 1;
+ }
const models = opts.compare
.split(",")
.map((m) => m.trim())
@@ -138,7 +145,7 @@ async function _runCompare(provider, opts) {
for (const model of models) {
const results = [];
for (let i = 0; i < repeat; i++) {
- const result = await _runSingleTest(targetProvider, model);
+ const result = await _runSingleTest(connection, model);
results.push(result);
}
rows.push({ model, ..._aggregate(results, true) });
@@ -180,19 +187,55 @@ async function _runCompare(provider, opts) {
return rows.every((r) => r.success) ? 0 : 1;
}
-async function _runSingleTest(provider, model) {
+async function _loadConnections() {
+ const res = await apiFetch("/api/providers?limit=200", {
+ retry: false,
+ timeout: 5000,
+ acceptNotOk: true,
+ });
+ if (!res.ok) {
+ console.error(t("test.noServer"));
+ return null;
+ }
+ const data = await res.json();
+ const connections = data.connections ?? data.providers ?? data.items ?? data;
+ if (!Array.isArray(connections)) {
+ console.error(t("test.noServer"));
+ return null;
+ }
+ return connections;
+}
+
+function _resolveConnection(connections, selector, model) {
+ const normalized = String(selector || "")
+ .trim()
+ .toLowerCase();
+ const active = connections.filter((connection) => connection.isActive !== false);
+ return (
+ active.find((connection) => String(connection.id || "").toLowerCase() === normalized) ??
+ active.find((connection) => String(connection.name || "").toLowerCase() === normalized) ??
+ active.find(
+ (connection) =>
+ String(connection.provider || "").toLowerCase() === normalized &&
+ (!model || connection.defaultModel === model || connection.model === model)
+ ) ??
+ active.find((connection) => String(connection.provider || "").toLowerCase() === normalized)
+ );
+}
+
+async function _runSingleTest(connection, model) {
const startMs = Date.now();
try {
- const res = await apiFetch("/api/v1/providers/test", {
+ const res = await apiFetch(`/api/providers/${encodeURIComponent(connection.id)}/test`, {
method: "POST",
- body: { provider, model },
+ body: model ? { validationModelId: model } : {},
retry: false,
timeout: 30000,
acceptNotOk: true,
});
const durationMs = Date.now() - startMs;
- const data = res.ok ? await res.json() : { success: false, error: `HTTP ${res.status}` };
- return { ...data, durationMs };
+ const data = res.ok ? await res.json() : { valid: false, error: `HTTP ${res.status}` };
+ return { ...data, success: data.valid === true, durationMs };
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
return {
diff --git a/bin/cli/tui/ProvidersTestAll.jsx b/bin/cli/tui/ProvidersTestAll.jsx
index 73fc78614a..c1911888ac 100644
--- a/bin/cli/tui/ProvidersTestAll.jsx
+++ b/bin/cli/tui/ProvidersTestAll.jsx
@@ -1,6 +1,7 @@
import React, { useState, useEffect, useCallback } from "react";
import { render, Box, Text, useInput } from "ink";
import Spinner from "ink-spinner";
+import { apiFetch } from "../api.mjs";
import { DataTable } from "../tui-components/DataTable.jsx";
import { ProgressBar } from "../tui-components/ProgressBar.jsx";
@@ -31,22 +32,20 @@ const TABLE_SCHEMA = [
{ key: "error", header: "Error", width: 28, formatter: (v) => (v ? v.slice(0, 26) : "") },
];
-async function testOne(provider, model, baseUrl, apiKey) {
- const headers = {
- "Content-Type": "application/json",
- ...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}),
- };
+async function testOne(connectionId, model, baseUrl, apiKey) {
const start = Date.now();
try {
- const res = await fetch(`${baseUrl}/api/v1/providers/test`, {
+ const res = await apiFetch(`/api/providers/${encodeURIComponent(connectionId)}/test`, {
method: "POST",
- headers,
- body: JSON.stringify({ provider, model }),
- signal: AbortSignal.timeout(30000),
+ body: model ? { validationModelId: model } : {},
+ baseUrl,
+ token: apiKey,
+ timeout: 30000,
+ acceptNotOk: true,
});
const latencyMs = Date.now() - start;
- const data = res.ok ? await res.json() : { success: false, error: `HTTP ${res.status}` };
- return { status: data.success ? STATUS.PASS : STATUS.FAIL, latencyMs, error: data.error };
+ const data = res.ok ? await res.json() : { valid: false, error: `HTTP ${res.status}` };
+ return { status: data.valid ? STATUS.PASS : STATUS.FAIL, latencyMs, error: data.error };
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
return {
@@ -63,6 +62,7 @@ function ProvidersTestAllApp({ providers, baseUrl, apiKey, concurrency = 4, onEx
const [rows, setRows] = useState(() =>
providers.map((p, i) => ({
id: i,
+ connectionId: p.connectionId ?? p.id,
provider: p.provider ?? p.id ?? String(p),
model: p.model ?? p.defaultModel ?? "",
status: STATUS.PENDING,
@@ -91,7 +91,7 @@ function ProvidersTestAllApp({ providers, baseUrl, apiKey, concurrency = 4, onEx
const row = queue[cursor++];
running++;
update(row.id, { status: STATUS.RUNNING });
- testOne(row.provider, row.model, resolved, apiKey).then((result) => {
+ testOne(row.connectionId, row.model, resolved, apiKey).then((result) => {
update(row.id, result);
running--;
nextSlot();
diff --git a/changelog.d/features/10668-newapi-gateway-protocols.md b/changelog.d/features/10668-newapi-gateway-protocols.md
new file mode 100644
index 0000000000..1ec6e5f0b7
--- /dev/null
+++ b/changelog.d/features/10668-newapi-gateway-protocols.md
@@ -0,0 +1,2 @@
+- **feat(providers):** add the TabiToken NewAPI gateway (`tabitoken`) and teach the existing HCNSec entry (`hcnsec`) the three further protocols it actually serves. TabiToken leaves the NewAPI pricing endpoint public, so its catalog is read from the host rather than guessed: four Claude models, each reporting the Anthropic and OpenAI protocols. HCNSec shipped OpenAI-only; probing the host showed `/v1/messages`, `/v1/responses` and the Gemini `/v1beta` path all reach its token layer, so each is now declared as an alternate format — with its default format, base URL, auth scheme and regional catalog classification untouched. ([#10668](https://github.com/diegosouzapw/OmniRoute/pull/10668)) — thanks @yawar-aquil
+- **feat(sse):** allow an alternate protocol to build its own upstream URL. `AlternateFormat` gained an optional `urlBuilder`, because the Gemini protocol carries the model inside the path (`{base}/{model}:generateContent`) and the existing `chatPath`/`urlSuffix` fields are constants that cannot express it. The route builder is extracted as `buildGeminiGenerateContentUrl` and shared with the native `gemini` provider so the two consumers cannot drift on the `?alt=sse` streaming suffix. ([#10668](https://github.com/diegosouzapw/OmniRoute/pull/10668)) — thanks @yawar-aquil
diff --git a/changelog.d/features/10896-glm-5.3.md b/changelog.d/features/10896-glm-5.3.md
new file mode 100644
index 0000000000..0edfc4a55b
--- /dev/null
+++ b/changelog.d/features/10896-glm-5.3.md
@@ -0,0 +1 @@
+- **feat(sse):** add GLM-5.3 support (`glm-5.3`, `glm-5.3-high`, `glm-5.3-low`) across the z.ai first-party providers, mapping the upstream `reasoning_effort` request parameter to the existing 5.2 tier UX ([#10896](https://github.com/diegosouzapw/OmniRoute/pull/10896)) — thanks @phuongddx
diff --git a/changelog.d/features/10897-home-recent-requests.md b/changelog.d/features/10897-home-recent-requests.md
new file mode 100644
index 0000000000..fd6bcc9abe
--- /dev/null
+++ b/changelog.d/features/10897-home-recent-requests.md
@@ -0,0 +1 @@
+- **feat(home):** add a live **Recent Requests** panel beside the home Provider Topology (polls `GET /api/usage/call-logs?excludeTests=1` every ~3s, gated by the topology appearance toggle + page visibility). `excludeTests` is now an allowlist of real provider inference (`/v1/%` or `/api/v1/%`), applied before `LIMIT`, so connection-test/model-sync/management rows can never leak into the feed ([#10897](https://github.com/diegosouzapw/OmniRoute/pull/10897), extracted from [#8450](https://github.com/diegosouzapw/OmniRoute/pull/8450)) — thanks @nguyenha935
diff --git a/changelog.d/features/10909-free-provider-rankings-reliability.md b/changelog.d/features/10909-free-provider-rankings-reliability.md
new file mode 100644
index 0000000000..e5377885ef
--- /dev/null
+++ b/changelog.d/features/10909-free-provider-rankings-reliability.md
@@ -0,0 +1 @@
+- **feat(rankings):** free provider rankings now expose a `reliability` field (raw `testStatus`/`rateLimitedUntil` per connection plus a `healthy`/`degraded`/`down` state, reusing the `ProviderHealthState` vocabulary of the provider health matrix) when the configured/available filters are active — derived from already-loaded data, without touching the ranking order ([#10909](https://github.com/diegosouzapw/OmniRoute/pull/10909))
diff --git a/changelog.d/features/10920-egress-ip-lock.md b/changelog.d/features/10920-egress-ip-lock.md
new file mode 100644
index 0000000000..af7308b62f
--- /dev/null
+++ b/changelog.d/features/10920-egress-ip-lock.md
@@ -0,0 +1,8 @@
+- `feat(resilience)`: when an allowlisted provider (opencode family) answers
+ 429 classified `quota_exhausted` or `rate_limit_exceeded` and its free-tier
+ quota is bucketed by egress IP (#9611), every connection of that family
+ sharing the IP is cooled down together before the rotation tries them — one
+ guaranteed-failed upstream call per episode instead of N, on the combo path
+ as well. For the allowlisted family a 429 now cools the connection instead
+ of locking a single model. Exclusive allowlist, never terminal, best-effort
+ when the egress IP is unknown (#10920).
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 0000000000..3e9b172204
--- /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/disable-context-window-checks.md b/changelog.d/features/disable-context-window-checks.md
new file mode 100644
index 0000000000..1cdd3cc0a8
--- /dev/null
+++ b/changelog.d/features/disable-context-window-checks.md
@@ -0,0 +1 @@
+- feat(routing): add the default-off `DISABLE_CONTEXT_WINDOW_CHECKS` feature flag to let operators bypass OmniRoute's local context-window and max-input-token check for direct single-model requests, leaving upstream limits, prompt compression, and output-token caps intact.
diff --git a/changelog.d/features/opencode-go-muse-spark-efforts.md b/changelog.d/features/opencode-go-muse-spark-efforts.md
new file mode 100644
index 0000000000..25da8482a9
--- /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/features/per-connection-upstream-timeout.md b/changelog.d/features/per-connection-upstream-timeout.md
new file mode 100644
index 0000000000..a5987ed485
--- /dev/null
+++ b/changelog.d/features/per-connection-upstream-timeout.md
@@ -0,0 +1 @@
+- **feat(providers):** restore the operator-owned upstream timeout tier per connection via `providerSpecificData.timeoutMs` (preempts the maintainer-only model/provider registry tiers and the global `FETCH_TIMEOUT_MS`), and make the combo per-target timeout ceiling follow the selected connection
\ No newline at end of file
diff --git a/changelog.d/fixes/10095-antigravity-multiaccount-quota-false-exhaustion.md b/changelog.d/fixes/10095-antigravity-multiaccount-quota-false-exhaustion.md
new file mode 100644
index 0000000000..579005e943
--- /dev/null
+++ b/changelog.d/fixes/10095-antigravity-multiaccount-quota-false-exhaustion.md
@@ -0,0 +1 @@
+- fix(domain): stop treating an unreported Antigravity quota fraction (`fractionReported:false`) as 0% remaining in `quotaCache.ts`, which was falsely marking every fresh/newly-connected account as exhausted and blocking multi-account rotation (#10095)
diff --git a/changelog.d/fixes/10156-responses-commentary-completed-snapshot.md b/changelog.d/fixes/10156-responses-commentary-completed-snapshot.md
new file mode 100644
index 0000000000..7a976ab85d
--- /dev/null
+++ b/changelog.d/fixes/10156-responses-commentary-completed-snapshot.md
@@ -0,0 +1 @@
+- **fix(sse):** Responses-passthrough `response.completed` snapshots now drop `phase:"commentary"` items the same way live SSE frames already do, so the terminal `response.output` array no longer echoes internal commentary text that was already suppressed from the stream (#10156).
diff --git a/changelog.d/fixes/10528-direct-dispatcher-response-start-timeout.md b/changelog.d/fixes/10528-direct-dispatcher-response-start-timeout.md
new file mode 100644
index 0000000000..9354b02822
--- /dev/null
+++ b/changelog.d/fixes/10528-direct-dispatcher-response-start-timeout.md
@@ -0,0 +1 @@
+- **fix(network):** direct (no-proxy) egress now bounds each attempt's response-start window (default 30s, `OMNIROUTE_DIRECT_HEADERS_TIMEOUT_MS`) and retries once on a fresh no-keep-alive socket, so a silently-dropped pooled keep-alive connection can no longer stall direct providers (opencode-go, command-code) until a service restart ([#10214](https://github.com/diegosouzapw/OmniRoute/issues/10214))
diff --git a/changelog.d/fixes/10536-llmlingua-2-2.0.5-drop-tfjs.md b/changelog.d/fixes/10536-llmlingua-2-2.0.5-drop-tfjs.md
new file mode 100644
index 0000000000..11a1845715
--- /dev/null
+++ b/changelog.d/fixes/10536-llmlingua-2-2.0.5-drop-tfjs.md
@@ -0,0 +1 @@
+- **fix(deps):** upgrade `@atjsh/llmlingua-2` from 2.0.3 to 2.0.5 and remove `@tensorflow/tfjs` from the LLMLingua SLM stack — 2.0.5 adds official Transformers.js v4 support (peers `@huggingface/transformers` at `^3.5.2 || ^4.0.0`) and 2.0.4+ no longer requires TensorFlow.js, restoring compatibility with OmniRoute's Transformers.js v4 while dropping the largest single contributor to the optional runtime footprint ([#10536](https://github.com/diegosouzapw/OmniRoute/issues/10536))
diff --git a/changelog.d/fixes/10550-responses-reasoning-transport.md b/changelog.d/fixes/10550-responses-reasoning-transport.md
new file mode 100644
index 0000000000..d34c433deb
--- /dev/null
+++ b/changelog.d/fixes/10550-responses-reasoning-transport.md
@@ -0,0 +1 @@
+- Preserve portable plaintext reasoning by default across streaming and non-streaming Chat Completions and Responses routes while keeping provider-bound opaque state target-compatible. Combos now drop incompatible continuation reasoning by default and can explicitly skip incompatible targets, while known providers no longer show redundant encrypted-reasoning controls. (#10550)
diff --git a/changelog.d/fixes/10592-playground-chattab-endpoint-routing.md b/changelog.d/fixes/10592-playground-chattab-endpoint-routing.md
new file mode 100644
index 0000000000..ca602b9122
--- /dev/null
+++ b/changelog.d/fixes/10592-playground-chattab-endpoint-routing.md
@@ -0,0 +1 @@
+- fix(dashboard): route the Playground's ChatTab "Send" through the endpoint actually selected in StudioConfigPane (`search`, `web.fetch`, etc.) instead of always POSTing to `/api/v1/chat/completions`, fixing the false "No active credentials for provider" 404 when testing search-only providers (#10592)
diff --git a/changelog.d/fixes/10594-freepik-magnific-api.md b/changelog.d/fixes/10594-freepik-magnific-api.md
new file mode 100644
index 0000000000..4c1c59a701
--- /dev/null
+++ b/changelog.d/fixes/10594-freepik-magnific-api.md
@@ -0,0 +1 @@
+- **fix(providers):** Magnific Mystic is now the canonical provider (`/dashboard/providers/magnific`, `magnific/`). It uses the Magnific API (`api.magnific.com` + `x-magnific-api-key`), dashboard Test Connection validates keys without starting a paid generation, and the old `freepik` slug remains a legacy alias ([#10594](https://github.com/diegosouzapw/OmniRoute/pull/10594))
diff --git a/changelog.d/fixes/10597-combo-log-error-body.md b/changelog.d/fixes/10597-combo-log-error-body.md
new file mode 100644
index 0000000000..ff6608947c
--- /dev/null
+++ b/changelog.d/fixes/10597-combo-log-error-body.md
@@ -0,0 +1 @@
+- **fix(sse):** Include the redacted upstream error body in the per-target COMBO failure log (`Model X failed, trying next`) so operators can triage a 400/500 without reproducing the request ([#10597](https://github.com/diegosouzapw/OmniRoute/issues/10597))
diff --git a/changelog.d/fixes/10765-rtk-unconditional-stats-cpu.md b/changelog.d/fixes/10765-rtk-unconditional-stats-cpu.md
new file mode 100644
index 0000000000..ff36462d47
--- /dev/null
+++ b/changelog.d/fixes/10765-rtk-unconditional-stats-cpu.md
@@ -0,0 +1 @@
+- fix(compression): skip the expensive `createCompressionStats()` pass in RTK when no message was actually compressed, matching every sibling stacked engine (#10765)
diff --git a/changelog.d/fixes/10788-ollama-cloud-effort-tiers.md b/changelog.d/fixes/10788-ollama-cloud-effort-tiers.md
new file mode 100644
index 0000000000..0437576d38
--- /dev/null
+++ b/changelog.d/fixes/10788-ollama-cloud-effort-tiers.md
@@ -0,0 +1 @@
+- **fix(open-sse):** declare `supportedThinkingEfforts` (`low`/`medium`/`high`/`max`) on Ollama Cloud's `glm-5.1`, `glm-5.2`, `deepseek-v4-pro` and `deepseek-v4-flash` registry entries so the catalog's `appendSyncedEffortVariants()` pass — which only synthesizes selectable `-low`/`-high`/`-max` model ids from an already-populated `capabilities.effort_tiers` — can expose an effort selector for these reasoning-capable models, matching what `gpt-oss:20b`/`gpt-oss:120b` already had (#10788)
diff --git a/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 0000000000..3a11aab909
--- /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/10815-kiro-oauth-profilearn-dedup.md b/changelog.d/fixes/10815-kiro-oauth-profilearn-dedup.md
new file mode 100644
index 0000000000..51768aab4c
--- /dev/null
+++ b/changelog.d/fixes/10815-kiro-oauth-profilearn-dedup.md
@@ -0,0 +1 @@
+- fix(db): disambiguate `createProviderConnection()`'s OAuth email dedup by `providerSpecificData.profileArn` in addition to `username`, so adding a second Kiro/AWS profile with the same email creates a new connection instead of silently merging into the first (#10815)
diff --git a/changelog.d/fixes/10848-image-scan-cookie-bridge.md b/changelog.d/fixes/10848-image-scan-cookie-bridge.md
new file mode 100644
index 0000000000..07e0f20291
--- /dev/null
+++ b/changelog.d/fixes/10848-image-scan-cookie-bridge.md
@@ -0,0 +1 @@
+- fix(config): exclude cookie-auth image bridges (chatgpt-web, gemini-web) from the unprefixed model scan so a bare id never silently binds to an unofficial web bridge (#10848)
diff --git a/changelog.d/fixes/10849-search-provider-opaque-400.md b/changelog.d/fixes/10849-search-provider-opaque-400.md
new file mode 100644
index 0000000000..a8982fb194
--- /dev/null
+++ b/changelog.d/fixes/10849-search-provider-opaque-400.md
@@ -0,0 +1 @@
+- fix(api): POST /v1/search now replies with a named `Unknown search provider: ` error (and field-named validation messages) instead of an opaque `Invalid request` for unrecognized or short-alias provider ids like `brave`/`serper` (#10849)
diff --git a/changelog.d/fixes/10854-skills-marketplace-owner.md b/changelog.d/fixes/10854-skills-marketplace-owner.md
new file mode 100644
index 0000000000..e80a109f77
--- /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/10877-quota-alias-fetcher-lookup-gap.md b/changelog.d/fixes/10877-quota-alias-fetcher-lookup-gap.md
new file mode 100644
index 0000000000..9501c6dad2
--- /dev/null
+++ b/changelog.d/fixes/10877-quota-alias-fetcher-lookup-gap.md
@@ -0,0 +1 @@
+- **fix(sse):** `getResetAwareProvider()` and the auto-combo quota lookup in `combo.ts` now canonicalize the provider id via `resolveProviderId()` before calling `getQuotaFetcher()`, so a fetcher registered under a provider's canonical id (e.g. `ollama-cloud`, `codex`) is found for combo targets stored under an alias spelling (e.g. `ollamacloud`, `cx`) instead of silently degrading reset-aware/reset-window/auto quota-aware routing to plain priority ordering (#10877)
diff --git a/changelog.d/fixes/10887-memory-mcp-tools.md b/changelog.d/fixes/10887-memory-mcp-tools.md
new file mode 100644
index 0000000000..8dc02db1d9
--- /dev/null
+++ b/changelog.d/fixes/10887-memory-mcp-tools.md
@@ -0,0 +1 @@
+- **fix(memory):** enable agent memory save/update via MCP tools (`memory_save`/`update`/`search`/`delete` builtins with per-provider schemas, `apiKeyId` optional with caller-principal fallback) and gate server-side memory builtin injection to non-stream requests only ([#10887](https://github.com/diegosouzapw/OmniRoute/pull/10887)) — thanks @Egorich-print
diff --git a/changelog.d/fixes/10902-pplx-search-hint-optin.md b/changelog.d/fixes/10902-pplx-search-hint-optin.md
new file mode 100644
index 0000000000..233fb61f19
--- /dev/null
+++ b/changelog.d/fixes/10902-pplx-search-hint-optin.md
@@ -0,0 +1 @@
+- **fix(perplexity-web):** make the built-in-search hint appended to every system message opt-in via `OMNIROUTE_PPLX_SEARCH_HINT` (off by default) — Perplexity's answer engine searches anyway, and the hint leaked into replies as meta-commentary for coding clients ([#10902](https://github.com/diegosouzapw/OmniRoute/pull/10902), extracted from [#8634](https://github.com/diegosouzapw/OmniRoute/pull/8634)) — thanks @danscMax
diff --git a/changelog.d/fixes/10903-loopback-gate-memory-success.md b/changelog.d/fixes/10903-loopback-gate-memory-success.md
new file mode 100644
index 0000000000..25720ba1a8
--- /dev/null
+++ b/changelog.d/fixes/10903-loopback-gate-memory-success.md
@@ -0,0 +1 @@
+- **fix(providers):** the loopback readiness gate no longer memorizes a failed probe — the next caller after 30s starts a fresh probe, and a readiness failure is logged once per probe instead of once per caller ([#10903](https://github.com/diegosouzapw/OmniRoute/pull/10903))
diff --git a/changelog.d/fixes/10935-cloudflare-relay-path-guard.md b/changelog.d/fixes/10935-cloudflare-relay-path-guard.md
new file mode 100644
index 0000000000..0799cdc52d
--- /dev/null
+++ b/changelog.d/fixes/10935-cloudflare-relay-path-guard.md
@@ -0,0 +1 @@
+- **fix(relay):** the Cloudflare proxy-relay worker now resolves `x-relay-path` through the shared `resolveRelayTarget()` guard instead of concatenating it onto the validated target. PR #4643 and its follow-up applied that guard to the Deno and Vercel workers; the Cloudflare generator, ported separately from upstream `decolua/9router` PR #1360, kept `fetch(targetBase + relayPath)`. Validating `x-relay-target` and then concatenating is not sufficient — the path re-points the request past the host that was just checked, through userinfo (`/x@evil.com`), a backslash (`\evil.com`), or a protocol-relative path (`//evil.com/x`). The guard is embedded verbatim under a literal `const resolveRelayTarget =` binding so the hardcoded call site still resolves when the SWC-minified standalone build mangles the source function's own name (#6149), and the new regression test pins that property for this worker by renaming the embedded function and re-evaluating the emitted source. The auth check and the private/loopback target guard are unchanged
diff --git a/changelog.d/fixes/10936-standalone-server-cjs-esm-scope.md b/changelog.d/fixes/10936-standalone-server-cjs-esm-scope.md
new file mode 100644
index 0000000000..824f7df647
--- /dev/null
+++ b/changelog.d/fixes/10936-standalone-server-cjs-esm-scope.md
@@ -0,0 +1 @@
+- **fix(build):** the `next` Docker image no longer crashes on boot with `ReferenceError: require is not defined in ES module scope`. The standalone `server.js` is CommonJS, but the `postbuild` colocate step was re-adding `"type":"module"` to the standalone root `package.json` (undoing `assembleStandalone`'s strip) to make its ESM worker bundles load. The `type:module` scope is now written per-worker-directory instead of on the root, so `server.js` stays CommonJS while the workers stay ESM ([#10936](https://github.com/diegosouzapw/OmniRoute/pull/10936), fixes [#10933](https://github.com/diegosouzapw/OmniRoute/issues/10933)) — thanks @arminanton
diff --git a/changelog.d/fixes/10941-relay-private-host-guard.md b/changelog.d/fixes/10941-relay-private-host-guard.md
new file mode 100644
index 0000000000..53d3aedeec
--- /dev/null
+++ b/changelog.d/fixes/10941-relay-private-host-guard.md
@@ -0,0 +1 @@
+- **fix(relay):** the private/loopback guard the three proxy-relay workers embed no longer misses four host spellings, and now lives in one place instead of three byte-identical inline copies. Driving `new URL(target).hostname` the way the workers do, the previous guard allowed `::` (the unspecified address, which reaches a service bound to the IPv6 loopback), `localhost.` (the FQDN root dot defeated the exact match and every `.localhost`/`.local`/`.internal` suffix rule, so `svc.internal.` slipped too), `::127.0.0.1` (the deprecated IPv4-compatible form — only `::ffff:` was checked), and `feb0::1` (link-local is `fe80::/10`, spanning `fe80`–`febf`, but only the literal `fe80:` spelling matched). The policy moved to `src/lib/proxyRelay/privateHostname.ts` and is embedded verbatim via `Function#toString` under a literal const name, the same mechanism `resolveRelayTarget` already uses for these workers, so a minified standalone build cannot break the call site (#6149). Nothing previously blocked is now allowed. Severity is low — reaching a worker needs the `x-relay-auth` secret and these are edge runtimes where loopback has nothing listening — but the suffix-rule bypass held regardless of runtime
diff --git a/changelog.d/fixes/claude-to-gemini-consecutive-roles.md b/changelog.d/fixes/claude-to-gemini-consecutive-roles.md
new file mode 100644
index 0000000000..17483dce52
--- /dev/null
+++ b/changelog.d/fixes/claude-to-gemini-consecutive-roles.md
@@ -0,0 +1 @@
+- **fix(translator):** merge consecutive same-role contents in direct Claude to Gemini request translation to prevent upstream HTTP 400 errors
diff --git a/changelog.d/fixes/cline-task-id-passthrough.md b/changelog.d/fixes/cline-task-id-passthrough.md
new file mode 100644
index 0000000000..a6d2ecec57
--- /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-sticky-pin-clear-on-disable.md b/changelog.d/fixes/combo-sticky-pin-clear-on-disable.md
new file mode 100644
index 0000000000..17abffb7f7
--- /dev/null
+++ b/changelog.d/fixes/combo-sticky-pin-clear-on-disable.md
@@ -0,0 +1 @@
+- fix(combo): evict in-memory session-stickiness bindings when a combo disables stickiness, so stale pins stop overriding the declared priority order until TTL/restart
diff --git a/changelog.d/fixes/minimax-music-generation-dispatch.md b/changelog.d/fixes/minimax-music-generation-dispatch.md
new file mode 100644
index 0000000000..e0cf2114ca
--- /dev/null
+++ b/changelog.d/fixes/minimax-music-generation-dispatch.md
@@ -0,0 +1 @@
+- **fix(sse):** MiniMax music models now generate audio instead of failing with `Unsupported music format: minimax-music` — the provider entry was registered in the music registry (and advertised by `/v1/models`), but `handleMusicGeneration` had no branch for its format, so every `minimax/*` music request fell through the dispatch chain to a 400. Adds the missing dispatch: a single synchronous POST with the `base_resp` envelope check (a non-zero `status_code` arrives on HTTP 200 too), `data.status` handling (an unfinished generation is reported instead of polled — the operation has no task id and no query endpoint), `url` and `hex` output formats (hex normalized to base64), `mp3`/`wav`/`pcm` containers via `audio_setting`, and the regional endpoint through the per-connection base-URL override, which is also the only host that accepts `aigc_watermark`. The registry entry gains the generation and cover model ids it was missing and drops a query URL that does not exist for this operation. Regression guard: `tests/unit/minimax-music-generation.test.ts` (9 tests).
diff --git a/changelog.d/maintenance/10859-filesize-baseline-fix.md b/changelog.d/maintenance/10859-filesize-baseline-fix.md
new file mode 100644
index 0000000000..aed0a3fab5
--- /dev/null
+++ b/changelog.d/maintenance/10859-filesize-baseline-fix.md
@@ -0,0 +1 @@
+- fix(quality): rebaseline file-size for #10859's own modelCapabilities.ts/commandCode.ts growth (missed at merge time)
diff --git a/changelog.d/maintenance/10889-feature-flag-count-fix.md b/changelog.d/maintenance/10889-feature-flag-count-fix.md
new file mode 100644
index 0000000000..fd93221987
--- /dev/null
+++ b/changelog.d/maintenance/10889-feature-flag-count-fix.md
@@ -0,0 +1 @@
+- fix(quality): bump EXPECTED_FEATURE_FLAG_COUNT to 52 for #10889's own new flag (missed at merge time)
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 0000000000..7822df2283
--- /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/config/quality/dependency-allowlist.json b/config/quality/dependency-allowlist.json
index f276fc45d2..c4476f95d1 100644
--- a/config/quality/dependency-allowlist.json
+++ b/config/quality/dependency-allowlist.json
@@ -20,7 +20,6 @@
"@stryker-mutator/tap-runner",
"@swc/helpers",
"@tailwindcss/postcss",
- "@tensorflow/tfjs",
"@testing-library/jest-dom",
"@testing-library/react",
"@toon-format/toon",
diff --git a/config/quality/eslint-suppressions.json b/config/quality/eslint-suppressions.json
index 40f74e61f9..9abe7bbf1d 100644
--- a/config/quality/eslint-suppressions.json
+++ b/config/quality/eslint-suppressions.json
@@ -348,11 +348,6 @@
"count": 1
}
},
- "src/app/api/auth/login/route.ts": {
- "no-restricted-imports": {
- "count": 1
- }
- },
"src/app/api/auth/oidc/callback/route.ts": {
"no-restricted-imports": {
"count": 1
@@ -813,14 +808,10 @@
"count": 1
}
},
- "src/app/api/settings/require-login/route.ts": {
- "no-restricted-imports": {
- "count": 1
- }
- },
+
"src/app/api/settings/route.ts": {
"no-restricted-imports": {
- "count": 2
+ "count": 1
}
},
"src/app/api/settings/system-prompt/route.ts": {
@@ -1616,11 +1607,7 @@
"count": 11
}
},
- "tests/unit/auth-login-route.test.ts": {
- "@typescript-eslint/no-explicit-any": {
- "count": 1
- }
- },
+
"tests/unit/auth-ollama-cloud-per-model-403-3027.test.ts": {
"@typescript-eslint/no-explicit-any": {
"count": 11
@@ -2499,11 +2486,7 @@
"count": 12
}
},
- "tests/unit/login-bootstrap-route.test.ts": {
- "@typescript-eslint/no-explicit-any": {
- "count": 10
- }
- },
+
"tests/unit/management-password.test.ts": {
"@typescript-eslint/no-explicit-any": {
"count": 4
diff --git a/config/quality/file-size-baseline.json b/config/quality/file-size-baseline.json
index 900f6220f8..2e7fa58dac 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.)",
@@ -441,17 +443,21 @@
"src/shared/components/ModelSelectModal.tsx": 1138,
"src/shared/constants/providers/apikey/gateways.ts": 1250
},
- "src/app/(dashboard)/dashboard/providers/[id]/components/modals/AddApiKeyModal.tsx": 1062,
+ "src/app/(dashboard)/dashboard/providers/[id]/components/modals/AddApiKeyModal.tsx": 1067,
"src/app/(dashboard)/dashboard/providers/[id]/hooks/useProviderConnections.ts": 1051,
"src/shared/components/ModelSelectModal.tsx": 1138,
- "src/shared/constants/providers/apikey/gateways.ts": 1268,
+ "src/shared/constants/providers/apikey/gateways.ts": 1298,
"open-sse/vendor/codex-chatgpt-web/bridge.ts": 1387,
"_rebaseline_2026_08_11_v3850_merge_storm_provider_registry": "DRIFT do merge-storm 2026-08-11 (99 PRs mergeados no release/v3.8.50). AddApiKeyModal.tsx (PR #8949 ChatGPT Web provider) e useProviderConnections.ts/ModelSelectModal.tsx (PRs #9011 combo test-all, #9499 image combos) = UI nova legitima acima do cap; gateways.ts = god-file de catalogo de providers que cresceu com PRs #9009/#9421/#9468/#9594 (qualquer split arriscaria corromper o merge de novo — o proprio PR #9421 quebrou o arquivo); bridge.ts (PR #8949) = ponte Chromium vendored; proxyFetch.ts 1207->1220 = drift herdado de merges. Owner autorizou rebaseline com anotacao (2026-08-11).",
- "src/lib/modelCapabilities.ts": 1006,
+ "src/lib/modelCapabilities.ts": 1016,
"src/app/(dashboard)/dashboard/providers/[id]/providerPageHelpers.ts": 1014,
"open-sse/config/imageRegistry.ts": 1034,
- "src/sse/handlers/chatHelpers.ts": 1017,
- "src/shared/middleware/chatBodyAdmission.ts": 1005
+ "src/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).",
+ "open-sse/executors/commandCode.ts": 1038,
+ "_rebaseline_2026_08_21_10859_vision_bridge_catalog": "#10859 own growth (Vision Bridge fixes #10808/#10809): src/lib/modelCapabilities.ts 1006->1016 (+10, cmd/gpt-5.3-codex* text-only capability resolution) and open-sse/executors/commandCode.ts 988->1023 (+35, Command Code wire-model normalization for bare ids + reasoning field fallback for opencode-routed gateways). Cohesive bug fixes at the existing capability-resolution / executor chokepoints; not extractable mid-fix. Covered by tests/unit/model-capabilities-command-code-codex-textonly-10703.test.ts, tests/unit/command-code-vision.test.ts, tests/unit/opencode-mimo-reasoning-details-nonstream.test.ts. Pushed directly to release (own-session miss: the original rebaseline was made in a throwaway validation worktree and never landed on the PR branch or the release before merge).",
+ "_rebaseline_2026_08_21_10907_sticky_pin_clear": "#10907 own growth: open-sse/executors/commandCode.ts 1023->1038 (+15, effort-suffix sanitization threading for the sticky-pin-clear fix). Cohesive change at the existing executor chokepoint. Covered by tests/unit/command-code-executor.test.ts."
},
"_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).",
@@ -620,4 +626,4 @@
"_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 9b900ce2bd..c6de98b418 100644
--- a/config/quality/open-sse-typecheck-baseline.json
+++ b/config/quality/open-sse-typecheck-baseline.json
@@ -2,28 +2,10 @@
"open-sse/handlers/chatCore/clientUsageBuffer.ts": {
"TS2345": 2
},
- "open-sse/services/browserBackedChat.ts": {
- "TS2353": 2
- },
- "open-sse/services/compression/engines/omniglyphAdapter.ts": {
- "TS2307": 1
- },
- "open-sse/services/compression/stats.ts": {
- "TS2307": 1
- },
- "open-sse/utils/cursorImages.ts": {
- "TS2339": 1
- },
- "open-sse/utils/imageNormalize.ts": {
- "TS2339": 1
- },
"open-sse/utils/stream.ts": {
"TS2345": 2,
"TS2322": 2
},
- "open-sse/vendor/codex-chatgpt-web/adapters/chatgpt-web/markdown.ts": {
- "TS2307": 2
- },
"src/lib/guardrails/videoBridgeHelpers.ts": {
"TS2488": 1,
"TS2365": 2,
diff --git a/config/quality/quality-baseline.json b/config/quality/quality-baseline.json
index eae9067b4b..3bfef6d099 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,
diff --git a/docs/architecture/ADAPTIVE_ROUTING.md b/docs/architecture/ADAPTIVE_ROUTING.md
new file mode 100644
index 0000000000..73bacfec3a
--- /dev/null
+++ b/docs/architecture/ADAPTIVE_ROUTING.md
@@ -0,0 +1,350 @@
+---
+title: "Adaptive Routing: Routing Events, Quality Feedback & Explainability"
+version: 3.8.50
+lastUpdated: 2026-08-20
+---
+
+# Adaptive Routing: Routing Events, Quality Feedback & Explainability
+
+This document describes the feedback-driven adaptive routing foundation added to
+OmniRoute. It is deliberately small: it introduces a typed routing-outcome
+channel, an online quality signal that feeds the existing auto-combo scorer, an
+optional OpenTelemetry exporter, and an explainability endpoint. It does **not**
+replace the existing resilience stack (circuit breaker, connection cooldown,
+model lockout, health matrix, autopilot) — it complements it.
+
+## 1. Architectural context
+
+OmniRoute is a data plane with a **request hot path** and a **control/intelligence
+plane**. The hot path must stay fast, memory-efficient, asynchronous, resilient and
+predictable. Evaluation, quality scoring, experiments and historical analysis belong
+to the control plane.
+
+```
+AI Agent / IDE
+ │
+ ▼
+┌─────────────────────┐
+│ OmniRoute │ data plane (fast, sync, in-memory)
+│ routing / failover │
+│ health / guardrail │
+│ cache / streaming │
+└──────────┬──────────┘
+ │ RoutingEvent (fire-and-forget, ~0.2µs)
+ ▼
+┌─────────────────────┐
+│ Feedback sinks │ control plane (async, best-effort)
+│ quality tracker │
+│ OTel exporter │
+│ explain store │
+└──────────┬──────────┘
+ ▼ quality score
+ auto-combo scorer
+```
+
+### What was already there (audited, not duplicated)
+
+| Concept | Existing implementation |
+| ----------------------------------- | -------------------------------------------------------------------------------------------------- |
+| Availability (can we send traffic?) | Circuit breaker (CLOSED/DEGRADED/OPEN/HALF_OPEN, DB-persisted), connection cooldown, model lockout |
+| Health reporting | `providerHealthMatrix.ts`, `providerHealthAutopilot.ts` |
+| Shadow traffic | `open-sse/services/combo/shadowRouting.ts` |
+| Guardrails | `src/lib/guardrails/` (pre/post hooks) |
+| Exact cache | `src/lib/semanticCache.ts` (signature-based) |
+| Evaluators / eval-driven routing | `src/lib/evals/`, `open-sse/services/evalRouting.ts` |
+| Combo decision explainability | `open-sse/services/combo/decisionTrace.ts` |
+| Dashboard real-time events | `src/lib/events/eventBus.ts` (UI notification channel, `unknown` payloads, 100-entry history) |
+
+The routing-event layer is **not** a re-implementation of `eventBus`: that bus is
+the dashboard's real-time notification channel (typed _event names_, opaque
+payloads, UI consumers). `RoutingEvent` is a typed _outcome_ struct
+(latency/tokens/cost/outcome/finish-reason) consumed by the control plane's
+feedback sinks (quality tracker, OTel exporter, explain store).
+
+### What was missing (added here)
+
+1. A **typed routing-outcome event + sink abstraction** (`RoutingEvent` /
+ `RoutingEventSink`). `decisionTrace` is combo-scoped and in-memory-only;
+ `comboMetrics` are cumulative counters; `call_logs` is raw async persistence.
+ None is a typed, sink-based outcome channel that a quality tracker, an OTel
+ exporter, or a Future-AGI-style evaluator can subscribe to.
+2. An **online quality signal** (EWMA) for output quality — the scorer previously
+ proxied "quality" only through static task fitness and opt-in eval pass-rates.
+3. An **optional, dependency-free OTel exporter** using GenAI semantic conventions.
+4. An **explainability endpoint** returning the real routing decisions + quality state.
+
+## 2. Routing Events (feedback foundation)
+
+Files: `open-sse/services/routing/events.ts`, `.../index.ts`
+
+A `RoutingEvent` carries only routing metadata:
+
+```ts
+interface RoutingEvent {
+ requestId: string;
+ provider: string;
+ model: string;
+ strategy: string; // "auto" | "priority" | "direct" | ...
+ latencyMs: number;
+ ttftMs: number | null;
+ inputTokens: number | null;
+ outputTokens: number | null;
+ cost: number | null;
+ retries: number;
+ fallbackUsed: boolean;
+ outcome: RoutingOutcome; // allowlisted union
+ status: number | null;
+ finishReason: string | null;
+ connectionId: string | null;
+ ts: number;
+}
+```
+
+`RoutingEventSink` is a `Send+Sync`-style trait in TypeScript:
+
+```ts
+interface RoutingEventSink {
+ readonly name: string;
+ record(event: RoutingEvent): void; // must be O(1), no sync I/O
+}
+```
+
+The hot path calls `emitRoutingEvent(event)` once per completed request
+(the streaming-completion callback, the non-streaming success path, and the
+malformed-200 failure path in `handleChatCore`). Dispatch is synchronous fan-out
+to registered sinks, but each sink only enqueues/updates in-memory state. **No
+synchronous database writes, no network I/O on the hot path.**
+
+Default sinks:
+
+- `MemoryRoutingEventStore` — bounded (500) ring buffer, newest-first, for the
+ explain endpoint.
+- `QualityTracker` consumer — updates the EWMA quality estimate.
+- `OtlpHttpsEventSink` — optional, enabled only when `OMNIROUTE_OTEL_ENDPOINT`
+ (or `OTEL_EXPORTER_OTLP_ENDPOINT`) is set.
+
+### Measured overhead (honest comparison)
+
+`npm run bench:routing-events` on this workstation (100k iterations; sub-µs ops
+measured as aggregate µs/op because per-op percentiles are below
+`performance.now()` timer resolution):
+
+| Scenario | µs/op | ops/s |
+| --------------------------------- | ------ | ------ |
+| baseline (scoring only) | ~0.045 | ~22 M |
+| baseline + RoutingEvent (2 sinks) | ~0.168 | ~5.9 M |
+| baseline + event + OTel enqueue | ~0.163 | ~6.1 M |
+| concurrent (8 interleaved bursts) | ~0.18 | — |
+
+The event-dispatch delta over baseline scoring is ~0.12 µs/request; the OTel sink
+only enqueues (O(1) buffer push), adding nothing measurable. These numbers are
+machine-specific and relative — not a production guarantee. The v1 "~0.2 µs"
+figure was an aggregate estimate; this methodology separates the scoring baseline
+from the event-dispatch cost.
+
+## 3. Quality Signal (feedback-driven provider state)
+
+Files: `open-sse/services/routing/quality.ts`
+
+v2 separates **operational** from **semantic** quality:
+
+- **Operational** — derived from the routing hot path (HTTP 4xx/5xx, connection
+ failures, 429s, malformed responses, stream interruptions, `finish_reason=length`,
+ zero-output successes, latency/TTFT EWMA). A 200 is NOT treated as semantic
+ quality.
+- **Semantic** — the actual value of the generated output. ONLY ever produced by
+ an evaluator via `setSemanticQuality()`. It is `null` until one provides it and
+ never leaks into the operational score.
+
+Per-(provider, model) state (EWMA + bounded counters):
+
+- `successEwma` — EWMA (α=0.2) of outcome success.
+- `latencyEwma` / `ttftEwma` — EWMA of latency (α=0.1).
+- `samples`, `anomalies`, `rateLimited`, `semantic`, `semanticConfidence`.
+- `recencyMs` — how recently the model was last observed.
+
+### Confidence / sample awareness
+
+`confidence = clamp01(samples / 50)`, and the score returned to the scorer is
+blended toward the neutral midpoint:
+
+```
+score = 0.5 + confidence * (operational - 0.5)
+```
+
+Consequences (verified by tests):
+
+- A cold provider (0 samples) scores **0.5** — not unfairly penalized, but
+ unable to dominate a provider with thousands of solid observations.
+- A provider with 7 lucky successes is pulled toward 0.5 (never dominates from
+ optimistic initialization).
+- A provider with 50+ samples converges to its true operational score.
+- Degradation and recovery are gradual (EWMA), and one isolated failure does
+ not destroy a healthy provider.
+
+`ProviderQuality` exposes `{ operational, semantic, confidence, samples, anomalies,
+rateLimited, successEwma, latencyEwmaMs, ttftEwmaMs, recencyMs }`.
+
+This feeds the auto-combo scorer as the `quality` scoring factor:
+
+- `ScoringFactors.quality` / `ScoringWeights.quality` in
+ `open-sse/services/autoCombo/scoring.ts`.
+- `DEFAULT_WEIGHTS`: `health` 0.1905 → 0.1605, `quality` 0.03. Sum stays 1.0.
+- `buildAutoCandidates` populates `candidate.quality` from the tracker; candidates
+ without data default to neutral **0.5** (a cold candidate is neither boosted nor
+ penalized).
+
+The closed loop:
+
+```
+RoutingEvent → QualityTracker → getQualityScore → auto-combo quality factor
+ ↑ │
+ └────── request outcome (handleChatCore) ←────────────┘
+```
+
+### Hard exclusion vs soft penalty
+
+The quality signal is a **soft adaptive preference** only. Hard exclusion stays
+with the existing resilience stack: circuit breaker OPEN, quota exhausted,
+auth failure, model lockout — none of these are affected by the quality score.
+A provider whose quality score dips temporarily is de-preferenced, never
+hard-disabled.
+
+## 3b. Canonical stream timing (TTFT / ITL)
+
+Files: `open-sse/utils/streamTiming.ts`
+
+`createStreamTiming()` is the single instrumentation seam for the streaming path,
+wired into `createSSEStream` (open-sse/utils/stream.ts):
+
+- `markByte()` — first upstream chunk received.
+- `markForward()` — first chunk forwarded to the client (used for TTFT).
+- `markInterrupted()` — stream timeout/abort/error before a clean finish.
+- `ttft()` = first-forwarded-SSE-chunk latency. **This is NOT token-level TTFT** —
+ a single SSE chunk may carry zero/one/many tokens. Documented precisely.
+- `avgItlMs()` = mean inter-chunk gap (a chunk-latency proxy for ITL).
+
+TTFT/ITL/interrupted flow into the `RoutingEvent` (`ttftMs`, `itlMs`) and are
+exported as GenAI/OmniRoute span attributes by the OTel sink.
+
+## 4. OpenTelemetry / GenAI observability
+
+Files: `open-sse/services/routing/otel.ts`
+
+- Dependency-free OTLP/HTTP JSON exporter (uses global `fetch`, no
+ `@opentelemetry/*` SDK).
+- Spans follow GenAI semantic conventions (`gen_ai.provider.name`,
+ `gen_ai.request.model`, `gen_ai.usage.input_tokens/output_tokens`,
+ `gen_ai.completion.finish_reason`, `gen_ai.system`) plus OmniRoute routing
+ attributes (outcome, status, ttft, retries, fallback).
+- `record()` only enqueues into a bounded buffer (O(1)); a background timer
+ flushes via `POST {endpoint}/v1/traces` asynchronously. Under overload the
+ oldest events are dropped (`dropped` counter) — never backpressure the data
+ plane.
+- **Disabled unless configured.** `OMNIROUTE_OTEL_ENDPOINT` (or
+ `OTEL_EXPORTER_OTLP_ENDPOINT`) must be set; otherwise the sink is not
+ registered and zero OTel code runs.
+
+## 5. Explainability
+
+- `GET /v1/explain/routing` returns the recent `RoutingEvent`s (the real
+ decisions, newest first) and the per-provider/model quality snapshot.
+- Auth mirrors `/v1/combos` (Bearer API key or dashboard session; anonymous on
+ single-user local deployments with `REQUIRE_API_KEY=false`).
+- Combo-level per-invocation traces remain available via the existing
+ `decisionTrace.ts` (header `X-OmniRoute-Combo-Trace`).
+- Safety: events carry only routing metadata, never prompts/bodies/credentials.
+
+## 6. Evaluation-plane integration (Future AGI readiness)
+
+OmniRoute treats Future AGI (or any evaluator) as a **potential
+intelligence/evaluation backend, not a dependency**. The seams:
+
+- A `RoutingEventSink` can forward events to an evaluator asynchronously.
+- The `MemoryRoutingEventStore` + quality snapshot give an evaluator the raw
+ decision stream.
+- A future `Evaluator` (deterministic, local judge, HTTP, WASM) would consume
+ events/traces and return a `QualityScore` that feeds the same
+ `getQualityScore`/quality-factor path.
+- Existing eval-driven routing (`open-sse/services/evalRouting.ts`) already
+ re-orders combo targets by `eval_runs` pass-rates when enabled.
+
+No evaluation runs synchronously on the request path, and the gateway operates
+fully with the evaluator absent.
+
+## 7. Final architectural review
+
+1. **What remains on the synchronous hot path?** Routing/scoring, guardrail
+ pre-checks, cache lookup, and one `emitRoutingEvent` fan-out (~0.12 µs over
+ baseline scoring) to in-memory sinks.
+2. **What moved to asynchronous processing?** OTel export (timer + fetch),
+ `call_logs`/usage persistence, semantic-cache writes, quality is in-memory
+ and O(1) (no async needed).
+3. **How does a routing outcome become feedback?** `handleChatCore` emits a
+ `RoutingEvent` → `QualityTracker` updates EWMA state → `getQualityScore`
+ feeds the auto-combo `quality` factor.
+4. **How does quality influence future routing?** A low quality score reduces
+ the weighted score of that provider/model in `scoreAutoTargets`, so degraded
+ models are gradually de-preferenced and recover as their EWMA improves.
+5. **How can Future AGI integrate without becoming a dependency?** Via the
+ `RoutingEventSink` interface / a future `Evaluator` adapter — no hardcoded
+ dependency.
+6. **What happens when the evaluator is unavailable?** Routing is unaffected;
+ quality falls back to neutral (1.0) for models with no observed signal.
+7. **What happens when telemetry is unavailable?** The OTel sink simply isn't
+ registered; the rest of the routing layer runs unchanged.
+8. **What happens under overload?** The OTel buffer drops oldest events; quality
+ and the ring buffer are bounded by construction; no backpressure.
+9. **How does provider state recover after degradation?** EWMA re-converges as
+ successes accumulate; warmup keeps cold models neutral; the circuit breaker
+ independently recovers via HALF_OPEN probes.
+10. **Which proposed features were intentionally NOT implemented, and why?**
+ - Shadow traffic / experiments — already implemented
+ (`combo/shadowRouting.ts`); not re-built.
+ - Guardrails — already implemented (`src/lib/guardrails/`); not duplicated.
+ - Semantic cache — already implemented (`src/lib/semanticCache.ts`); not
+ duplicated.
+ - A full experiment-management platform, dataset tooling, prompt-optimization
+ platform, vector DB, or mandatory external OTel infrastructure — out of
+ scope for a lean data plane.
+ - A Rust `RoutingEvent` struct — the data plane is TypeScript; the TS type
+ is the adapted equivalent.
+
+## 8. Configuration reference
+
+| Variable | Default | Effect |
+| ----------------------------- | ----------- | ------------------------------------------------------------------------------- |
+| `OMNIROUTE_OTEL_ENDPOINT` | unset | When set, enables the OTLP/HTTP traces exporter (e.g. `http://collector:4318`). |
+| `OTEL_EXPORTER_OTLP_ENDPOINT` | unset | Fallback alias for the OTLP endpoint. |
+| `OTEL_SERVICE_NAME` | `omniroute` | `service.name` resource attribute. |
+
+## 9. Tests
+
+- `tests/unit/routing-events.test.ts` — event normalization, status
+ classification, bounded ring buffer, sink fan-out + isolation.
+- `tests/unit/routing-quality.test.ts` — EWMA warmup, failure/success recovery,
+ anomaly penalties, 429 transient handling, snapshot, reset.
+- `tests/unit/routing-scoring-quality.test.ts` — weight integrity, neutral
+ default, quality factor ranking.
+- `tests/unit/routing-otel.test.ts` — enable gating, GenAI span payload, async
+ flush, drop-under-overload.
+- `tests/unit/routing-events-concurrency.test.ts` — thousands of events, ring
+ buffer boundedness, throwing-sink isolation, interleaved async bursts,
+ reset-during-inserts.
+- `tests/unit/routing-adaptive-e2e.test.ts` — deterministic end-to-end loop via
+ the real `scoreAutoTargets` scorer: healthy → degrade → recover → blip, plus
+ cold-start and lucky-cold-provider scenarios.
+- `tests/unit/stream-timing.test.ts` — TTFT (first-forwarded-chunk), ITL,
+ first-byte vs first-forward, interruption, malformed/empty chunk safety.
+
+## 10. Pre-existing issues status (Phase 18)
+
+| Issue | Status | Notes |
+| ----------------------------------------------------- | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `omniglyph` export mismatch | **FIXED (environmental)** | `node_modules` was out of sync with `package-lock.json` (installed 1.3.1 vs locked 1.4.0). Running `npm install omniglyph@1.4.0` restored the locked version; type errors dropped to 0. Manifests unchanged. |
+| Stale `getKnownContextOverflow` tests | **KNOWN — not fixed** | `combo-context-overflow-compression-probe.test.ts` imports a function that no longer exists in `open-sse/services/combo.ts` (only comments reference it). Fixing requires re-implementing or re-writing those tests — unrelated architectural churn. |
+| `combo-runtime-unit-concurrency.test.ts` DB isolation | **KNOWN — not fixed** | Test-harness SQLite-isolation assertion fails when run directly; fails identically on the base branch. |
+| i18n `llm.txt` drift | **KNOWN — not fixed** | `docs/i18n/*/llm.txt` differ from root; pre-existing, blocks the docs-sync pre-commit gate. |
+
+Environmental vs code issues are kept distinct; no unrelated failures are hidden
+behind changed test filters.
diff --git a/docs/architecture/RESILIENCE_GUIDE.md b/docs/architecture/RESILIENCE_GUIDE.md
index d92aebb512..030f65dd41 100644
--- a/docs/architecture/RESILIENCE_GUIDE.md
+++ b/docs/architecture/RESILIENCE_GUIDE.md
@@ -369,7 +369,7 @@ matching provider classification rule
(`agentrouter-model-access-denied` in `open-sse/config/providerErrorRules.ts`:
`reason: "auth_error"`, `scope: "model"`, a `6h` declared base cooldown) is
consulted by `checkFallbackError` (`open-sse/services/accountFallback.ts`)
-*before* the generic apikey-category `FORBIDDEN` early-return, gated on
+_before_ the generic apikey-category `FORBIDDEN` early-return, gated on
`honorsRuleLockScope(provider)` (#10334 — currently agentrouter-exclusive via
the `HONORS_RULE_LOCK_SCOPE_PROVIDERS` allowlist in
`providerErrorRules.ts`). The rule's declared 6h cooldown flows through as
@@ -378,7 +378,7 @@ per-model-quota lockout path (`lockModelIfPerModelQuota()` /
`recordModelLockoutFailure()`, unchanged by #10334 except for the cooldown
source): it is clamped down to the operator's `mlSettings.maxCooldownMs`
(default `1_800_000ms` / 30min), like every other model lockout, and the
-*persisted lockout reason* stays the pre-existing hardcoded `"forbidden"`,
+_persisted lockout reason_ stays the pre-existing hardcoded `"forbidden"`,
not the rule's `"auth_error"` — only the cooldown duration is honored
end-to-end, not the reason string. The connection itself stays active;
sibling models on the same connection are unaffected.
@@ -413,7 +413,7 @@ never `creditsExhausted` — a defense against a future rule pairing scope
`open-sse/services/combo/targetExhaustion.ts`): the same guard marks the
connection into the in-memory `exhaustedConnections` set, keyed
`${provider}:${connectionId}`. This only skips a remaining SAME-REQUEST
- target that *itself already carries that exact `connectionId`* on its own
+ target that _itself already carries that exact `connectionId`_ on its own
target object (`getExhaustedTargetSkipReason()`,
`open-sse/services/combo/comboPredicates.ts`, `if (provider &&
connectionId)` before the `exhaustedConnections` lookup) — a plain
@@ -498,6 +498,68 @@ provider is on that allowlist.
No changes to `chatCore.ts`, `classifyError`, or combo are needed.
+#### Egress-bucketed lock (#10880)
+
+Providers in `EGRESS_BUCKETED_LOCK_PROVIDERS` (opencode family) are treated
+as IP-bucketed upstream (the opencode free tier is IP-bucketed, not
+account-bucketed — see #9611): a status-429 classified `quota_exhausted`
+**or** `rate_limit_exceeded` cools down every allowlisted-family connection
+whose last known egress IP matches the failing connection's, before the
+rotation can try them
+— avoiding N-1 guaranteed-failed upstream calls (same shape as #10460/#10525).
+`rate_limit_exceeded` is included deliberately: on the `markAccountUnavailable`
+path the opencode-specific rules never match (no headers/body handed to
+`checkFallbackError`, opencode not in `FULL_TEXT_RULE_PROVIDERS`), so a 429
+whose body carries the subscription-quota text ("monthly usage limit
+reached") is classified `quota_exhausted` by the quota-text fallback
+(`buildSubscriptionQuotaFallback`, `accountFallback.ts`; 1h cooldown) before
+the `status_429` rule is ever reached — while a quota-text-free 429 (plain
+rate limiting) classifies via the `status_429` rule as `rate_limit_exceeded`
+and still cools the IP family down. For an allowlisted provider an IP-bucketed
+rate limit is the same signal as an exhausted quota. Honest limits:
+
+- **Best-effort**: the lock resolves the connection's last known `egress_ip`
+ from `proxy_logs` (24h window, synchronous, no cache). Cold cache (egress
+ IP never probed) or no row → the failing connection is still cooled by the
+ branch (recorded like today), only no sibling is locked.
+- **Never terminal**: the cooldown is a renewing quota window
+ (`testStatus: "unavailable"`); a permanent state is never derived from an
+ IP-level signal. `disableCooling` connections skip the branch entirely.
+- **Lock granularity changes for the allowlisted family**: this is a scope
+ change, not only a sibling optimization. opencode is a `passthroughModels`
+ provider, so before this branch a 429 produced a per-MODEL lockout; it now
+ produces a connection cooldown — including for an operator running a single
+ connection with no sibling at all. That is the granularity the opencode rule
+ table already declares correct (`scope: "connection"`,
+ `providerErrorRules.ts`), never honored so far because opencode is not in
+ `HONORS_RULE_LOCK_SCOPE_PROVIDERS`. The branch writes the failing
+ connection's cooldown + `backoffLevel` itself, mirroring the
+ connection-scoped agentrouter branch, and returns — the per-model block and
+ the generic path below are never reached.
+- **Combo included**: like the agentrouter branch, the scope deliberately
+ ignores the `persistUnavailableState`/`isCombo` downgrade a combo caller
+ applies to a 429. A per-model lockout is not a weaker form of this scope, it
+ is the wrong unit: it says nothing about the exhausted IP, so the combo
+ rotation would keep burning one guaranteed-failed call per sibling.
+- **Sibling safety**: a sibling already terminal (banned/credits_exhausted)
+ or already in a longer cooldown is never overwritten.
+- **Exclusive allowlist**: widening `EGRESS_BUCKETED_LOCK_PROVIDERS` is an
+ explicit owner decision; no generic wiring (pattern #10334/#10419). The
+ sibling query binds that same allowlist rather than repeating it as a SQL
+ literal, so widening it stays a one-line change.
+- **Egress IP rotation, both directions**: the lookup window (24h) is far
+ wider than the egress-IP cache TTL (5 min), so "last known IP" is history,
+ not current state. If a connection's proxy rotated within the window the
+ lock may **miss** a genuinely shared IP (the recorded IP is the new,
+ unexhausted one) — and symmetrically it may **cool a sibling that has since
+ rotated away** from the exhausted IP. The second case costs that sibling one
+ cooldown window; both are accepted best-effort limits of a history-based
+ lookup.
+- **Cost**: two bounded scans of `proxy_logs` (window-filtered via
+ `idx_pl_timestamp`), only at 429 frequency. No new index (migration 134
+ YAGNI). Measured on a real-traffic DB copy of moderate size; a
+ high-throughput instance holds proportionally more rows in the same window.
+
---
## Other Resilience Features
diff --git a/docs/architecture/meta.json b/docs/architecture/meta.json
index dba5872324..c5b10b7923 100644
--- a/docs/architecture/meta.json
+++ b/docs/architecture/meta.json
@@ -12,6 +12,7 @@
"ROUTER_BACKENDS",
"admission-lanes",
"cluster-decisions",
- "persistence-backend-boundary"
+ "persistence-backend-boundary",
+ "ADAPTIVE_ROUTING"
]
}
diff --git a/docs/compression/COMPRESSION_ENGINES.md b/docs/compression/COMPRESSION_ENGINES.md
index 22cffe7c42..4a232a0e5d 100644
--- a/docs/compression/COMPRESSION_ENGINES.md
+++ b/docs/compression/COMPRESSION_ENGINES.md
@@ -28,12 +28,12 @@ The `omniglyph` engine (package `omniglyph`, 1.4.0+) accepts a named semantic pr
globally through `omniglyph.profile` in the compression settings or per step through the
stacked pipeline's step config:
-| Profile | Boundary |
-| -------------- | --------------------------------------------------------------------------- |
-| `aggressive` | Default. The policy the published receipts measured — images system, tool docs and dense history |
-| `balanced` | Keeps live state native, protects the last 8 turns, collapses older closed history |
-| `coding-safe` | Keeps authority, tool schemas and live tool output native, protects the last 12 turns |
-| `passthrough` | Routes without transforming; the engine is skipped |
+| Profile | Boundary |
+| ------------- | ------------------------------------------------------------------------------------------------ |
+| `aggressive` | Default. The policy the published receipts measured — images system, tool docs and dense history |
+| `balanced` | Keeps live state native, protects the last 8 turns, collapses older closed history |
+| `coding-safe` | Keeps authority, tool schemas and live tool output native, protects the last 12 turns |
+| `passthrough` | Routes without transforming; the engine is skipped |
The profile is a **ceiling, not a floor**: `mergeCompressionProfileOptions` in the package
refuses to let a caller override reopen a lossy lane the profile closed, so a per-step
@@ -170,22 +170,22 @@ override points it at a local copy instead (offline / air-gapped installs).
### Optional dependencies & on-demand install
-The prunable LLMLingua runtime peer stack is **optional**. Three packages are declared as
+The prunable LLMLingua runtime peer stack is **optional**. Two packages are declared as
`optionalDependencies` in `package.json` and kept **external** by the production build
(`scripts/build/prepublish.ts` does not bundle them):
-| Package | Version (pin) | Notes |
-| -------------------- | ------------- | ---------------------------------------------- |
-| `@atjsh/llmlingua-2` | `2.0.3` | Entry package; declares the others as peers |
-| `@tensorflow/tfjs` | `4.22.0` | Heaviest dep — dominates the ~800 MB footprint |
-| `js-tiktoken` | `^1.0.20` | Tokenizer |
+| Package | Version (pin) | Notes |
+| -------------------- | ------------- | ------------------------------------------- |
+| `@atjsh/llmlingua-2` | `2.0.5` | Entry package; declares the others as peers |
+| `js-tiktoken` | `^1.0.20` | Tokenizer |
-`@huggingface/transformers` is pinned at `3.5.2` as an **optional** dependency (shared with
-the local embeddings path and also traced into the standalone bundle). Keeping it optional prevents
-`onnxruntime-node` CUDA provider postinstall failures on CUDA 11 hosts from aborting the whole
-OmniRoute install; when the optional stack is absent, LLMLingua still fail-opens. Only the three
-packages above are prunable SLM peers. A standard `npm install` (dev) installs the optional stack
-automatically unless optional dependencies are omitted.
+`@huggingface/transformers` is pinned at `^4.2.0` (shared with the local embeddings path and
+also traced into the standalone bundle); `@atjsh/llmlingua-2@2.0.5` peers on it with
+`"^3.5.2 || ^4.0.0"`, so both Transformers.js v3 and v4 are supported. Since 2.0.4,
+`@atjsh/llmlingua-2` no longer requires `@tensorflow/tfjs`, which removed the largest single
+contributor (TensorFlow.js) from the SLM stack. Only the two packages above are prunable SLM
+peers. A standard `npm install` (dev) installs the optional stack automatically unless optional
+dependencies are omitted.
**Why on-demand:** the npm-published package, the standalone bundle, and the Docker image
ship **without** these deps to stay slim. When they are absent, the worker's dependency
@@ -195,11 +195,12 @@ error logged). To activate it in a pruned environment, install the optional stac
```bash
# pin to the versions declared in package.json optionalDependencies
-npm install @atjsh/llmlingua-2@2.0.3 @tensorflow/tfjs@4.22.0 js-tiktoken
+npm install @atjsh/llmlingua-2@2.0.5 js-tiktoken
```
-Roughly **~800 MB** total: the TensorFlow.js + transformers runtimes dominate; the
-TinyBERT model adds ~57 MB downloaded at first use (not via npm).
+The `@tensorflow/tfjs` removal (2.0.4+) eliminates the previously dominant ~800 MB
+contributor — the remaining footprint is the transformers.js + onnxruntime-node runtimes,
+plus the TinyBERT model (~57 MB) downloaded at first use (not via npm).
Per environment:
diff --git a/docs/diagrams/cli-terminal.svg b/docs/diagrams/cli-terminal.svg
index 507e8ac54c..4fd6887859 100644
--- a/docs/diagrams/cli-terminal.svg
+++ b/docs/diagrams/cli-terminal.svg
@@ -1,6 +1,6 @@
-
+
Compact animated terminal cycling three real OmniRoute CLI commands with a typewriter effect and a scrolling subcommand ticker; the first frame shows the completed providers-list screen.
-
+
@@ -16,12 +16,12 @@
OmniRoute 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 e69c2d0c2c..d73a1fb255 100644
--- a/docs/diagrams/comparison-table.svg
+++ b/docs/diagrams/comparison-table.svg
@@ -1,4 +1,4 @@
-
+
Static-header comparison table where each capability row fades in top to bottom; the OmniRoute column is highlighted and shows a check or a leading value in every row, while competitors show a mix of checks, partials and crosses.
diff --git a/docs/diagrams/promise-pillars.svg b/docs/diagrams/promise-pillars.svg
index c73ef5e0d7..1c3f0a6cc9 100644
--- a/docs/diagrams/promise-pillars.svg
+++ b/docs/diagrams/promise-pillars.svg
@@ -1,4 +1,4 @@
-
+
Animated promise card: six pillar tiles fade in in reading order, then a soft colored border highlight sweeps from tile to tile in a continuous cycle.
@@ -21,7 +21,7 @@
- One endpoint. 343 providers. Never stop building — OmniRoute picks the cheapest one that works .
+ One endpoint. 346 providers. Never stop building — OmniRoute picks the cheapest one that works .
@@ -38,7 +38,7 @@
Never hit limits
- Auto-fallback across 343 providers in
+ Auto-fallback across 346 providers in
milliseconds. Quota out? The next provider
takes over — zero downtime.
diff --git a/docs/diagrams/readme-hero.svg b/docs/diagrams/readme-hero.svg
index e5ad1f9971..fb332a4574 100644
--- a/docs/diagrams/readme-hero.svg
+++ b/docs/diagrams/readme-hero.svg
@@ -1,4 +1,4 @@
-
+
Animated hero card: a pulse travels the divider line and a compression bar demo repeatedly shrinks a prompt by up to 95 percent; all headline content is static and readable on the first frame.
@@ -28,7 +28,7 @@
Never stop coding.
- Every AI tool → 343 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/getting-started/PROVIDERS-GUIDE.md b/docs/getting-started/PROVIDERS-GUIDE.md
index 65de3c63ea..d6d20906c5 100644
--- a/docs/getting-started/PROVIDERS-GUIDE.md
+++ b/docs/getting-started/PROVIDERS-GUIDE.md
@@ -52,6 +52,8 @@ safely retry only the failures after a partial result.
- **Pollinations** — Free GPT-5, Claude, Gemini (no key needed)
- **LongCat** — 10M tokens free (one-time grant, requires account + KYC)
- **Cloudflare AI** — 50+ models, 10K neurons/day
+ - **MLX Gemma 26B** — Local Apple Silicon model (~38.5 tok/s, ~15.9GB RAM)
+ - **MLX Qwen 3.8 27B** — Local Apple Silicon model (~9.1 tok/s, ~13.1GB RAM)
4. Click **Connect**
5. Done! You now have free AI access.
@@ -79,6 +81,94 @@ safely retry only the failures after a partial result.
5. Login with your account
6. Done! You now have access to your subscription models.
+### Option D: Local MLX Models (Apple Silicon)
+
+For Apple Silicon Macs with unified memory, OmniRoute supports connecting to local MLX models running via `mlx-lm.server` as regular OpenAI-compatible local providers.
+
+#### Prerequisites
+
+- **Apple Silicon Mac** (M1/M2/M3/M4) with 24GB+ unified memory recommended
+- **uv** package manager: `curl -LsSf https://astral.sh/uv/install.sh | sh`
+- **mlx-lm**: `uv pip install mlx-lm`
+
+#### Quick Start
+
+1. **Install dependencies**:
+
+ ```bash
+ # Install uv if not already installed
+ curl -LsSf https://astral.sh/uv/install.sh | sh
+
+ # Install mlx-lm
+ uv pip install mlx-lm
+ ```
+
+2. **Start MLX servers manually** (in separate terminals):
+
+ ```bash
+ # Terminal 1: Gemma 4 26B A4B IT-QAT (port 11435)
+ uv run mlx_lm.server --model mlx-community/gemma-4-26B-A4B-it-qat-q4_0-mlx-aligned --port 11435 --host 127.0.0.1
+
+ # Terminal 2: Qwen 3.8 27B MLX Mixed (port 11436)
+ uv run mlx_lm.server --model maglun/Qwen3.8-27B-MLX-Mixed-3.80bpw --port 11436 --host 127.0.0.1
+ ```
+
+3. **Connect in OmniRoute Dashboard**:
+ - Go to **Providers** → **Add Provider**
+ - Select **MLX Gemma 26B** or **MLX Qwen 3.8 27B**
+ - Click **Connect** (no API key needed)
+
+4. **Use with OpenCode**:
+ ```bash
+ # Configure OpenCode to use OmniRoute
+ opencode config set api.base_url http://localhost:20128/v1
+ opencode config set api.key
+
+ # Use MLX models
+ opencode run --model mlx-gemma/gemma-4-26b
+ opencode run --model mlx-qwen/qwen3.8-27b
+ ```
+
+#### Memory Management
+
+**Important**: With 24GB unified memory, only **one large MLX model can run at a time**.
+
+- Gemma 26B: ~15.9GB peak memory
+- Qwen 3.8 27B: ~13.1GB peak memory
+
+You must manage this manually:
+
+- Run only one MLX server at a time, or
+- Run both on separate machines, or
+- Stop one before starting the other
+
+OmniRoute does not automatically manage MLX server processes — it only routes requests to the OpenAI-compatible endpoints you configure.
+
+#### Tool Calling Support
+
+Both models support OpenAI-compatible tool calling. Test with:
+
+```bash
+curl -X POST http://localhost:20128/v1/chat/completions \
+ -H "Authorization: Bearer " \
+ -H "Content-Type: application/json" \
+ -d '{
+ "model": "mlx-gemma/gemma-4-26b",
+ "messages": [{"role": "user", "content": "What is 2+2? Use the calculator tool."}],
+ "tools": [{"type": "function", "function": {"name": "calculator", "description": "Calculate", "parameters": {"type": "object", "properties": {"expression": {"type": "string"}}, "required": ["expression"]}}}]
+ }'
+```
+
+#### Troubleshooting
+
+| Issue | Solution |
+| ------------------ | ----------------------------------------------------------------------------- |
+| Server won't start | Check `uv run mlx_lm.server --help` and verify model IDs |
+| Out of memory | Ensure only one model runs; close other apps; check Activity Monitor |
+| Connection refused | Verify server is running on correct port (11435/11436) |
+| Slow responses | First request loads model into memory (~30-60s); subsequent requests are fast |
+| Tool calling fails | Ensure model supports tools; check OmniRoute logs for translation errors |
+
---
## Best Free Providers
diff --git a/docs/guides/MANAGEMENT-AUTH.md b/docs/guides/MANAGEMENT-AUTH.md
index 31391e4b7f..5e0d8d6eeb 100644
--- a/docs/guides/MANAGEMENT-AUTH.md
+++ b/docs/guides/MANAGEMENT-AUTH.md
@@ -14,8 +14,8 @@ Canonical implementation: `src/lib/api/requireManagementAuth.ts`.
| Credential | Typical form | Created where | Intended use | Management capability |
|---|---|---|---|---|
-| Dashboard session | `auth_token` cookie | Dashboard login | Browser UI | Full dashboard management, subject to CSRF, locality, and always-protected-route rules |
-| Local CLI machine token | internal / local | CLI bootstrap (`omniroute` on the same machine) | Local CLI | Local management only |
+| Dashboard JWT session | `auth_token` cookie | Dashboard login | Browser UI | Full dashboard management, subject to CSRF, locality, and always-protected-route rules |
+| CLI machine-id token | internal / local | CLI bootstrap (`omniroute` on the same machine) | Local CLI | Local management only |
| Scoped Access Token | `oma_live_…` | **Settings → Access Tokens** or `omniroute connect` | Remote CLI and management API | Must satisfy the route's required `read`, `write`, or `admin` scope |
| Inference API key | `sk-…` (and other API-key prefixes) | **API Manager / API Keys** | `/v1/*` inference | **None** unless the key metadata includes `manage` or `admin` |
@@ -61,13 +61,13 @@ chat client key for automation unless you deliberately granted that scope.
## How to create and revoke
-### Dashboard session
+### Dashboard JWT session
1. Open `/login`, sign in with the management password (`INITIAL_PASSWORD` on first boot).
2. Cookie `auth_token` is HttpOnly. Browser dashboard uses it automatically.
3. Log out via `/api/auth/logout`. There is no long-lived secret to copy.
-### Local CLI machine token
+### CLI machine-id token
1. Run `omniroute` on the **same host** as the server (loopback).
2. The CLI bootstraps a machine-id token under `~/.omniroute/` (chmod 600).
diff --git a/docs/i18n/ar/llm.txt b/docs/i18n/ar/llm.txt
index 732d79189c..53dfd1c0d6 100644
--- a/docs/i18n/ar/llm.txt
+++ b/docs/i18n/ar/llm.txt
@@ -4,7 +4,7 @@
---
-> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
+> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 346 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 157 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ └── manager.ts # MITM proxy manager
│ ├── shared/ # Shared utilities, components, and constants
│ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.)
-│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes
+│ │ ├── constants/ # Provider definitions (346), model lists, pricing, routing strategies, MCP scopes
│ │ ├── contracts/ # Shared API contracts
│ │ ├── hooks/ # React hooks
│ │ ├── middleware/ # Shared middleware utilities
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
-- **342 AI providers** with automatic format translation
+- **346 AI providers** with automatic format translation
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
- **4-tier fallback**: Subscription → API Key → Cheap → Free
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 157 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
## v3.8.x Highlights
-- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
+- **346-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
- **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
diff --git a/docs/i18n/az/llm.txt b/docs/i18n/az/llm.txt
index e930a1f05a..cf0018a415 100644
--- a/docs/i18n/az/llm.txt
+++ b/docs/i18n/az/llm.txt
@@ -4,7 +4,7 @@
---
-> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
+> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 346 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 157 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ └── manager.ts # MITM proxy manager
│ ├── shared/ # Shared utilities, components, and constants
│ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.)
-│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes
+│ │ ├── constants/ # Provider definitions (346), model lists, pricing, routing strategies, MCP scopes
│ │ ├── contracts/ # Shared API contracts
│ │ ├── hooks/ # React hooks
│ │ ├── middleware/ # Shared middleware utilities
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
-- **342 AI providers** with automatic format translation
+- **346 AI providers** with automatic format translation
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
- **4-tier fallback**: Subscription → API Key → Cheap → Free
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 157 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
## v3.8.x Highlights
-- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
+- **346-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
- **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
diff --git a/docs/i18n/bg/llm.txt b/docs/i18n/bg/llm.txt
index e930a1f05a..cf0018a415 100644
--- a/docs/i18n/bg/llm.txt
+++ b/docs/i18n/bg/llm.txt
@@ -4,7 +4,7 @@
---
-> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
+> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 346 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 157 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ └── manager.ts # MITM proxy manager
│ ├── shared/ # Shared utilities, components, and constants
│ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.)
-│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes
+│ │ ├── constants/ # Provider definitions (346), model lists, pricing, routing strategies, MCP scopes
│ │ ├── contracts/ # Shared API contracts
│ │ ├── hooks/ # React hooks
│ │ ├── middleware/ # Shared middleware utilities
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
-- **342 AI providers** with automatic format translation
+- **346 AI providers** with automatic format translation
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
- **4-tier fallback**: Subscription → API Key → Cheap → Free
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 157 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
## v3.8.x Highlights
-- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
+- **346-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
- **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
diff --git a/docs/i18n/bn/llm.txt b/docs/i18n/bn/llm.txt
index baa6656839..2749b7b108 100644
--- a/docs/i18n/bn/llm.txt
+++ b/docs/i18n/bn/llm.txt
@@ -4,7 +4,7 @@
---
-> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
+> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 346 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 157 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ └── manager.ts # MITM proxy manager
│ ├── shared/ # Shared utilities, components, and constants
│ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.)
-│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes
+│ │ ├── constants/ # Provider definitions (346), model lists, pricing, routing strategies, MCP scopes
│ │ ├── contracts/ # Shared API contracts
│ │ ├── hooks/ # React hooks
│ │ ├── middleware/ # Shared middleware utilities
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
-- **342 AI providers** with automatic format translation
+- **346 AI providers** with automatic format translation
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
- **4-tier fallback**: Subscription → API Key → Cheap → Free
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 157 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
## v3.8.x Highlights
-- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
+- **346-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
- **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
diff --git a/docs/i18n/cs/llm.txt b/docs/i18n/cs/llm.txt
index dfb5f9b2b8..76a283564b 100644
--- a/docs/i18n/cs/llm.txt
+++ b/docs/i18n/cs/llm.txt
@@ -4,7 +4,7 @@
---
-> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
+> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 346 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 157 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ └── manager.ts # MITM proxy manager
│ ├── shared/ # Shared utilities, components, and constants
│ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.)
-│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes
+│ │ ├── constants/ # Provider definitions (346), model lists, pricing, routing strategies, MCP scopes
│ │ ├── contracts/ # Shared API contracts
│ │ ├── hooks/ # React hooks
│ │ ├── middleware/ # Shared middleware utilities
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
-- **342 AI providers** with automatic format translation
+- **346 AI providers** with automatic format translation
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
- **4-tier fallback**: Subscription → API Key → Cheap → Free
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 157 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
## v3.8.x Highlights
-- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
+- **346-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
- **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
diff --git a/docs/i18n/da/llm.txt b/docs/i18n/da/llm.txt
index 10ce4811ac..5bcb53ef10 100644
--- a/docs/i18n/da/llm.txt
+++ b/docs/i18n/da/llm.txt
@@ -4,7 +4,7 @@
---
-> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
+> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 346 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 157 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ └── manager.ts # MITM proxy manager
│ ├── shared/ # Shared utilities, components, and constants
│ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.)
-│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes
+│ │ ├── constants/ # Provider definitions (346), model lists, pricing, routing strategies, MCP scopes
│ │ ├── contracts/ # Shared API contracts
│ │ ├── hooks/ # React hooks
│ │ ├── middleware/ # Shared middleware utilities
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
-- **342 AI providers** with automatic format translation
+- **346 AI providers** with automatic format translation
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
- **4-tier fallback**: Subscription → API Key → Cheap → Free
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 157 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
## v3.8.x Highlights
-- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
+- **346-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
- **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
diff --git a/docs/i18n/de/llm.txt b/docs/i18n/de/llm.txt
index b5ebeb9c86..17cb4a70bf 100644
--- a/docs/i18n/de/llm.txt
+++ b/docs/i18n/de/llm.txt
@@ -4,7 +4,7 @@
---
-> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
+> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 346 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 157 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ └── manager.ts # MITM proxy manager
│ ├── shared/ # Shared utilities, components, and constants
│ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.)
-│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes
+│ │ ├── constants/ # Provider definitions (346), model lists, pricing, routing strategies, MCP scopes
│ │ ├── contracts/ # Shared API contracts
│ │ ├── hooks/ # React hooks
│ │ ├── middleware/ # Shared middleware utilities
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
-- **342 AI providers** with automatic format translation
+- **346 AI providers** with automatic format translation
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
- **4-tier fallback**: Subscription → API Key → Cheap → Free
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 157 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
## v3.8.x Highlights
-- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
+- **346-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
- **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
diff --git a/docs/i18n/es/llm.txt b/docs/i18n/es/llm.txt
index 72d4fa4a05..0441be519d 100644
--- a/docs/i18n/es/llm.txt
+++ b/docs/i18n/es/llm.txt
@@ -4,7 +4,7 @@
---
-> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
+> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 346 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 157 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ └── manager.ts # MITM proxy manager
│ ├── shared/ # Shared utilities, components, and constants
│ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.)
-│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes
+│ │ ├── constants/ # Provider definitions (346), model lists, pricing, routing strategies, MCP scopes
│ │ ├── contracts/ # Shared API contracts
│ │ ├── hooks/ # React hooks
│ │ ├── middleware/ # Shared middleware utilities
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
-- **342 AI providers** with automatic format translation
+- **346 AI providers** with automatic format translation
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
- **4-tier fallback**: Subscription → API Key → Cheap → Free
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 157 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
## v3.8.x Highlights
-- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
+- **346-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
- **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
diff --git a/docs/i18n/fa/llm.txt b/docs/i18n/fa/llm.txt
index 4b324a992d..c81ec3a853 100644
--- a/docs/i18n/fa/llm.txt
+++ b/docs/i18n/fa/llm.txt
@@ -4,7 +4,7 @@
---
-> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
+> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 346 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 157 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ └── manager.ts # MITM proxy manager
│ ├── shared/ # Shared utilities, components, and constants
│ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.)
-│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes
+│ │ ├── constants/ # Provider definitions (346), model lists, pricing, routing strategies, MCP scopes
│ │ ├── contracts/ # Shared API contracts
│ │ ├── hooks/ # React hooks
│ │ ├── middleware/ # Shared middleware utilities
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
-- **342 AI providers** with automatic format translation
+- **346 AI providers** with automatic format translation
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
- **4-tier fallback**: Subscription → API Key → Cheap → Free
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 157 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
## v3.8.x Highlights
-- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
+- **346-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
- **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
diff --git a/docs/i18n/fi/llm.txt b/docs/i18n/fi/llm.txt
index 92d30e036c..974084ef14 100644
--- a/docs/i18n/fi/llm.txt
+++ b/docs/i18n/fi/llm.txt
@@ -4,7 +4,7 @@
---
-> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
+> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 346 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 157 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ └── manager.ts # MITM proxy manager
│ ├── shared/ # Shared utilities, components, and constants
│ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.)
-│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes
+│ │ ├── constants/ # Provider definitions (346), model lists, pricing, routing strategies, MCP scopes
│ │ ├── contracts/ # Shared API contracts
│ │ ├── hooks/ # React hooks
│ │ ├── middleware/ # Shared middleware utilities
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
-- **342 AI providers** with automatic format translation
+- **346 AI providers** with automatic format translation
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
- **4-tier fallback**: Subscription → API Key → Cheap → Free
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 157 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
## v3.8.x Highlights
-- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
+- **346-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
- **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
diff --git a/docs/i18n/fr/llm.txt b/docs/i18n/fr/llm.txt
index 2b5cdefa71..6db495b3d9 100644
--- a/docs/i18n/fr/llm.txt
+++ b/docs/i18n/fr/llm.txt
@@ -4,7 +4,7 @@
---
-> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
+> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 346 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 157 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ └── manager.ts # MITM proxy manager
│ ├── shared/ # Shared utilities, components, and constants
│ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.)
-│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes
+│ │ ├── constants/ # Provider definitions (346), model lists, pricing, routing strategies, MCP scopes
│ │ ├── contracts/ # Shared API contracts
│ │ ├── hooks/ # React hooks
│ │ ├── middleware/ # Shared middleware utilities
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
-- **342 AI providers** with automatic format translation
+- **346 AI providers** with automatic format translation
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
- **4-tier fallback**: Subscription → API Key → Cheap → Free
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 157 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
## v3.8.x Highlights
-- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
+- **346-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
- **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
diff --git a/docs/i18n/gu/llm.txt b/docs/i18n/gu/llm.txt
index d0f0f42f59..885f78df63 100644
--- a/docs/i18n/gu/llm.txt
+++ b/docs/i18n/gu/llm.txt
@@ -4,7 +4,7 @@
---
-> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
+> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 346 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 157 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ └── manager.ts # MITM proxy manager
│ ├── shared/ # Shared utilities, components, and constants
│ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.)
-│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes
+│ │ ├── constants/ # Provider definitions (346), model lists, pricing, routing strategies, MCP scopes
│ │ ├── contracts/ # Shared API contracts
│ │ ├── hooks/ # React hooks
│ │ ├── middleware/ # Shared middleware utilities
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
-- **342 AI providers** with automatic format translation
+- **346 AI providers** with automatic format translation
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
- **4-tier fallback**: Subscription → API Key → Cheap → Free
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 157 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
## v3.8.x Highlights
-- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
+- **346-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
- **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
diff --git a/docs/i18n/he/llm.txt b/docs/i18n/he/llm.txt
index f8f0b3f644..617ce45e8c 100644
--- a/docs/i18n/he/llm.txt
+++ b/docs/i18n/he/llm.txt
@@ -4,7 +4,7 @@
---
-> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
+> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 346 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 157 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ └── manager.ts # MITM proxy manager
│ ├── shared/ # Shared utilities, components, and constants
│ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.)
-│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes
+│ │ ├── constants/ # Provider definitions (346), model lists, pricing, routing strategies, MCP scopes
│ │ ├── contracts/ # Shared API contracts
│ │ ├── hooks/ # React hooks
│ │ ├── middleware/ # Shared middleware utilities
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
-- **342 AI providers** with automatic format translation
+- **346 AI providers** with automatic format translation
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
- **4-tier fallback**: Subscription → API Key → Cheap → Free
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 157 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
## v3.8.x Highlights
-- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
+- **346-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
- **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
diff --git a/docs/i18n/hi/llm.txt b/docs/i18n/hi/llm.txt
index d79717b61e..67b0cfeba3 100644
--- a/docs/i18n/hi/llm.txt
+++ b/docs/i18n/hi/llm.txt
@@ -4,7 +4,7 @@
---
-> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
+> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 346 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 157 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ └── manager.ts # MITM proxy manager
│ ├── shared/ # Shared utilities, components, and constants
│ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.)
-│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes
+│ │ ├── constants/ # Provider definitions (346), model lists, pricing, routing strategies, MCP scopes
│ │ ├── contracts/ # Shared API contracts
│ │ ├── hooks/ # React hooks
│ │ ├── middleware/ # Shared middleware utilities
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
-- **342 AI providers** with automatic format translation
+- **346 AI providers** with automatic format translation
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
- **4-tier fallback**: Subscription → API Key → Cheap → Free
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 157 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
## v3.8.x Highlights
-- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
+- **346-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
- **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
diff --git a/docs/i18n/hu/llm.txt b/docs/i18n/hu/llm.txt
index 1b941f1e5a..fa74ab5997 100644
--- a/docs/i18n/hu/llm.txt
+++ b/docs/i18n/hu/llm.txt
@@ -4,7 +4,7 @@
---
-> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
+> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 346 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 157 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ └── manager.ts # MITM proxy manager
│ ├── shared/ # Shared utilities, components, and constants
│ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.)
-│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes
+│ │ ├── constants/ # Provider definitions (346), model lists, pricing, routing strategies, MCP scopes
│ │ ├── contracts/ # Shared API contracts
│ │ ├── hooks/ # React hooks
│ │ ├── middleware/ # Shared middleware utilities
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
-- **342 AI providers** with automatic format translation
+- **346 AI providers** with automatic format translation
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
- **4-tier fallback**: Subscription → API Key → Cheap → Free
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 157 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
## v3.8.x Highlights
-- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
+- **346-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
- **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
diff --git a/docs/i18n/id/llm.txt b/docs/i18n/id/llm.txt
index e1b23c5e3d..71572bd5a4 100644
--- a/docs/i18n/id/llm.txt
+++ b/docs/i18n/id/llm.txt
@@ -4,7 +4,7 @@
---
-> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
+> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 346 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 157 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ └── manager.ts # MITM proxy manager
│ ├── shared/ # Shared utilities, components, and constants
│ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.)
-│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes
+│ │ ├── constants/ # Provider definitions (346), model lists, pricing, routing strategies, MCP scopes
│ │ ├── contracts/ # Shared API contracts
│ │ ├── hooks/ # React hooks
│ │ ├── middleware/ # Shared middleware utilities
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
-- **342 AI providers** with automatic format translation
+- **346 AI providers** with automatic format translation
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
- **4-tier fallback**: Subscription → API Key → Cheap → Free
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 157 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
## v3.8.x Highlights
-- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
+- **346-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
- **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
diff --git a/docs/i18n/in/llm.txt b/docs/i18n/in/llm.txt
index 7713dacb68..c6f22ffc46 100644
--- a/docs/i18n/in/llm.txt
+++ b/docs/i18n/in/llm.txt
@@ -4,7 +4,7 @@
---
-> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
+> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 346 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 157 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ └── manager.ts # MITM proxy manager
│ ├── shared/ # Shared utilities, components, and constants
│ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.)
-│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes
+│ │ ├── constants/ # Provider definitions (346), model lists, pricing, routing strategies, MCP scopes
│ │ ├── contracts/ # Shared API contracts
│ │ ├── hooks/ # React hooks
│ │ ├── middleware/ # Shared middleware utilities
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
-- **342 AI providers** with automatic format translation
+- **346 AI providers** with automatic format translation
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
- **4-tier fallback**: Subscription → API Key → Cheap → Free
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 157 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
## v3.8.x Highlights
-- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
+- **346-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
- **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
diff --git a/docs/i18n/it/llm.txt b/docs/i18n/it/llm.txt
index e0823ba499..db4f6a9cfd 100644
--- a/docs/i18n/it/llm.txt
+++ b/docs/i18n/it/llm.txt
@@ -4,7 +4,7 @@
---
-> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
+> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 346 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 157 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ └── manager.ts # MITM proxy manager
│ ├── shared/ # Shared utilities, components, and constants
│ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.)
-│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes
+│ │ ├── constants/ # Provider definitions (346), model lists, pricing, routing strategies, MCP scopes
│ │ ├── contracts/ # Shared API contracts
│ │ ├── hooks/ # React hooks
│ │ ├── middleware/ # Shared middleware utilities
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
-- **342 AI providers** with automatic format translation
+- **346 AI providers** with automatic format translation
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
- **4-tier fallback**: Subscription → API Key → Cheap → Free
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 157 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
## v3.8.x Highlights
-- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
+- **346-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
- **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
diff --git a/docs/i18n/ja/llm.txt b/docs/i18n/ja/llm.txt
index 5c1963195f..f021f83d5a 100644
--- a/docs/i18n/ja/llm.txt
+++ b/docs/i18n/ja/llm.txt
@@ -4,7 +4,7 @@
---
-> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
+> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 346 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 157 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ └── manager.ts # MITM proxy manager
│ ├── shared/ # Shared utilities, components, and constants
│ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.)
-│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes
+│ │ ├── constants/ # Provider definitions (346), model lists, pricing, routing strategies, MCP scopes
│ │ ├── contracts/ # Shared API contracts
│ │ ├── hooks/ # React hooks
│ │ ├── middleware/ # Shared middleware utilities
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
-- **342 AI providers** with automatic format translation
+- **346 AI providers** with automatic format translation
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
- **4-tier fallback**: Subscription → API Key → Cheap → Free
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 157 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
## v3.8.x Highlights
-- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
+- **346-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
- **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
diff --git a/docs/i18n/ko/llm.txt b/docs/i18n/ko/llm.txt
index 2ae8fe4ec4..52cf209a74 100644
--- a/docs/i18n/ko/llm.txt
+++ b/docs/i18n/ko/llm.txt
@@ -4,7 +4,7 @@
---
-> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
+> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 346 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 157 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ └── manager.ts # MITM proxy manager
│ ├── shared/ # Shared utilities, components, and constants
│ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.)
-│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes
+│ │ ├── constants/ # Provider definitions (346), model lists, pricing, routing strategies, MCP scopes
│ │ ├── contracts/ # Shared API contracts
│ │ ├── hooks/ # React hooks
│ │ ├── middleware/ # Shared middleware utilities
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
-- **342 AI providers** with automatic format translation
+- **346 AI providers** with automatic format translation
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
- **4-tier fallback**: Subscription → API Key → Cheap → Free
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 157 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
## v3.8.x Highlights
-- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
+- **346-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
- **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
diff --git a/docs/i18n/mr/llm.txt b/docs/i18n/mr/llm.txt
index 157eec643a..cc3125f845 100644
--- a/docs/i18n/mr/llm.txt
+++ b/docs/i18n/mr/llm.txt
@@ -4,7 +4,7 @@
---
-> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
+> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 346 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 157 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ └── manager.ts # MITM proxy manager
│ ├── shared/ # Shared utilities, components, and constants
│ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.)
-│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes
+│ │ ├── constants/ # Provider definitions (346), model lists, pricing, routing strategies, MCP scopes
│ │ ├── contracts/ # Shared API contracts
│ │ ├── hooks/ # React hooks
│ │ ├── middleware/ # Shared middleware utilities
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
-- **342 AI providers** with automatic format translation
+- **346 AI providers** with automatic format translation
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
- **4-tier fallback**: Subscription → API Key → Cheap → Free
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 157 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
## v3.8.x Highlights
-- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
+- **346-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
- **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
diff --git a/docs/i18n/ms/llm.txt b/docs/i18n/ms/llm.txt
index e26b195208..650ac31922 100644
--- a/docs/i18n/ms/llm.txt
+++ b/docs/i18n/ms/llm.txt
@@ -4,7 +4,7 @@
---
-> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
+> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 346 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 157 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ └── manager.ts # MITM proxy manager
│ ├── shared/ # Shared utilities, components, and constants
│ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.)
-│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes
+│ │ ├── constants/ # Provider definitions (346), model lists, pricing, routing strategies, MCP scopes
│ │ ├── contracts/ # Shared API contracts
│ │ ├── hooks/ # React hooks
│ │ ├── middleware/ # Shared middleware utilities
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
-- **342 AI providers** with automatic format translation
+- **346 AI providers** with automatic format translation
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
- **4-tier fallback**: Subscription → API Key → Cheap → Free
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 157 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
## v3.8.x Highlights
-- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
+- **346-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
- **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
diff --git a/docs/i18n/nl/llm.txt b/docs/i18n/nl/llm.txt
index 67f935d590..bdd29b2d01 100644
--- a/docs/i18n/nl/llm.txt
+++ b/docs/i18n/nl/llm.txt
@@ -4,7 +4,7 @@
---
-> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
+> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 346 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 157 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ └── manager.ts # MITM proxy manager
│ ├── shared/ # Shared utilities, components, and constants
│ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.)
-│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes
+│ │ ├── constants/ # Provider definitions (346), model lists, pricing, routing strategies, MCP scopes
│ │ ├── contracts/ # Shared API contracts
│ │ ├── hooks/ # React hooks
│ │ ├── middleware/ # Shared middleware utilities
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
-- **342 AI providers** with automatic format translation
+- **346 AI providers** with automatic format translation
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
- **4-tier fallback**: Subscription → API Key → Cheap → Free
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 157 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
## v3.8.x Highlights
-- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
+- **346-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
- **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
diff --git a/docs/i18n/no/llm.txt b/docs/i18n/no/llm.txt
index e28dda1508..7cb57aa53c 100644
--- a/docs/i18n/no/llm.txt
+++ b/docs/i18n/no/llm.txt
@@ -4,7 +4,7 @@
---
-> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
+> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 346 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 157 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ └── manager.ts # MITM proxy manager
│ ├── shared/ # Shared utilities, components, and constants
│ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.)
-│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes
+│ │ ├── constants/ # Provider definitions (346), model lists, pricing, routing strategies, MCP scopes
│ │ ├── contracts/ # Shared API contracts
│ │ ├── hooks/ # React hooks
│ │ ├── middleware/ # Shared middleware utilities
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
-- **342 AI providers** with automatic format translation
+- **346 AI providers** with automatic format translation
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
- **4-tier fallback**: Subscription → API Key → Cheap → Free
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 157 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
## v3.8.x Highlights
-- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
+- **346-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
- **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
diff --git a/docs/i18n/phi/llm.txt b/docs/i18n/phi/llm.txt
index ed539c84be..73ea9e49ba 100644
--- a/docs/i18n/phi/llm.txt
+++ b/docs/i18n/phi/llm.txt
@@ -4,7 +4,7 @@
---
-> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
+> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 346 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 157 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ └── manager.ts # MITM proxy manager
│ ├── shared/ # Shared utilities, components, and constants
│ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.)
-│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes
+│ │ ├── constants/ # Provider definitions (346), model lists, pricing, routing strategies, MCP scopes
│ │ ├── contracts/ # Shared API contracts
│ │ ├── hooks/ # React hooks
│ │ ├── middleware/ # Shared middleware utilities
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
-- **342 AI providers** with automatic format translation
+- **346 AI providers** with automatic format translation
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
- **4-tier fallback**: Subscription → API Key → Cheap → Free
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 157 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
## v3.8.x Highlights
-- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
+- **346-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
- **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
diff --git a/docs/i18n/pl/docs/compression/COMPRESSION_ENGINES.md b/docs/i18n/pl/docs/compression/COMPRESSION_ENGINES.md
index b8488d6872..ea266af05b 100644
--- a/docs/i18n/pl/docs/compression/COMPRESSION_ENGINES.md
+++ b/docs/i18n/pl/docs/compression/COMPRESSION_ENGINES.md
@@ -142,22 +142,22 @@ wskazuje zamiast tego lokalną kopię (instalacje offline / air-gapped).
### Opcjonalne zależności i instalacja on-demand
-Przycinany stos peerów runtime LLMLingua jest **opcjonalny**. Trzy pakiety są zadeklarowane jako
+Przycinany stos peerów runtime LLMLingua jest **opcjonalny**. Dwa pakiety są zadeklarowane jako
`optionalDependencies` w `package.json` i utrzymywane jako **external** przez build produkcyjny
(`scripts/build/prepublish.ts` ich nie bundluje):
-| Package | Version (pin) | Notes |
-| -------------------- | ------------- | ------------------------------------------------- |
-| `@atjsh/llmlingua-2` | `2.0.3` | Pakiet wejściowy; deklaruje pozostałe jako peery |
-| `@tensorflow/tfjs` | `4.22.0` | Najcięższa zależność — dominuje footprint ~800 MB |
-| `js-tiktoken` | `^1.0.20` | Tokenizer |
+| Package | Version (pin) | Notes |
+| -------------------- | ------------- | ------------------------------------------- |
+| `@atjsh/llmlingua-2` | `2.0.5` | Pakiet wejściowy; deklaruje pozostałe jako peery |
+| `js-tiktoken` | `^1.0.20` | Tokenizer |
-`@huggingface/transformers` jest pinowany na `3.5.2` jako **opcjonalna** zależność (współdzielona ze
-ścieżką lokalnych embeddings i również śledzona do standalone bundle). Utrzymanie jej jako optional
-zapobiega awariom postinstall providera CUDA `onnxruntime-node` na hostach CUDA 11, które przerywałyby
-całą instalację OmniRoute; gdy opcjonalny stos jest nieobecny, LLMLingua nadal fail-openuje. Tylko trzy
-powyższe pakiety to przycinane peery SLM. Standardowe `npm install` (dev) instaluje opcjonalny stos
-automatycznie, o ile opcjonalne zależności nie zostaną pominięte.
+`@huggingface/transformers` jest pinowany na `^4.2.0` (współdzielony ze ścieżką lokalnych embeddings
+i również śledzony do standalone bundle); `@atjsh/llmlingua-2@2.0.5` peeruje na nim przez
+`"^3.5.2 || ^4.0.0"`, więc obsługiwane są zarówno Transformers.js v3, jak i v4. Od 2.0.4
+`@atjsh/llmlingua-2` nie wymaga już `@tensorflow/tfjs`, co usunęło największy pojedynczy wkład
+(TensorFlow.js) ze stosu SLM. Tylko dwa powyższe pakiety to przycinane peery SLM. Standardowe
+`npm install` (dev) instaluje opcjonalny stos automatycznie, o ile opcjonalne zależności nie zostaną
+pominięte.
**Dlaczego on-demand:** pakiet publikowany w npm, standalone bundle i obraz Docker
dostarczane są **bez** tych zależności, aby pozostać lekkie. Gdy ich brakuje, bramka zależności
@@ -167,11 +167,12 @@ logowanego błędu). Aby aktywować go w przyciętym środowisku, zainstaluj opc
```bash
# pin to the versions declared in package.json optionalDependencies
-npm install @atjsh/llmlingua-2@2.0.3 @tensorflow/tfjs@4.22.0 js-tiktoken
+npm install @atjsh/llmlingua-2@2.0.5 js-tiktoken
```
-Łącznie mniej więcej **~800 MB**: dominują runtime’y TensorFlow.js + transformers; model
-TinyBERT dodaje ~57 MB pobierane przy pierwszym użyciu (nie przez npm).
+Usunięcie `@tensorflow/tfjs` (2.0.4+) eliminuje wcześniej dominujący wkład ~800 MB — pozostały
+footprint to runtime’y transformers.js + onnxruntime-node oraz model TinyBERT (~57 MB) pobierany
+przy pierwszym użyciu (nie przez npm).
Per środowisko:
diff --git a/docs/i18n/pl/docs/ops/RELEASE_CHECKLIST.md b/docs/i18n/pl/docs/ops/RELEASE_CHECKLIST.md
index 1d9cdb29f6..535dadd53c 100644
--- a/docs/i18n/pl/docs/ops/RELEASE_CHECKLIST.md
+++ b/docs/i18n/pl/docs/ops/RELEASE_CHECKLIST.md
@@ -326,13 +326,11 @@ Przed wypuszczeniem dowolnego wydania v3.8.x zweryfikuj te dodatkowe pozycje:
- [ ] `npm install -g omniroute@` uruchamia postinstall bez fatalnego wyjścia
- [ ] Ścieżka update zachowuje optional deps: `omniroute update --apply` i auto-updater
uruchamiają `npm install -g … --include=optional`, żeby `optionalDependencies` (better-sqlite3,
- keytar, tls-client oraz stack SLM llmlingua: `@atjsh/llmlingua-2`,
- `@huggingface/transformers@3.5.2`, `@tensorflow/tfjs`, `js-tiktoken`) przeżyły update.
- `@huggingface/transformers` zostaje optional, żeby jego postinstall providera CUDA `onnxruntime-node`
- nie mógł przerwać instalacji na hostach CUDA 11. Tier ultra `modelPath` SLM potrzebuje też
+ keytar, tls-client oraz stack SLM llmlingua: `@atjsh/llmlingua-2@2.0.5`,
+ `js-tiktoken`) przeżyły update. Tier ultra `modelPath` SLM potrzebuje też
modelu tinybert, auto-pobieranego do `${DATA_DIR}/models/llmlingua` przy pierwszym użyciu. Postinstall
(`scripts/build/colocateOptionals.mjs`) następnie ko-lokuje opcjonalne zamknięcie SLM do
- `dist/node_modules`, żeby worker rozwiązywał JEDNĄ opcjonalną instancję `@huggingface/transformers` 3.5.2
+ `dist/node_modules`, żeby worker rozwiązywał JEDNĄ instancję `@huggingface/transformers` ^4.2.0
— standalone trace bundluje tylko transformers, nie dynamicznie importowane
optionals, więc bez tego worker załadowałby llmlingua-2 przeciw transformers z roota
i tier SLM cicho fail-openowałby.
diff --git a/docs/i18n/pl/llm.txt b/docs/i18n/pl/llm.txt
index f8665fa410..7d1d8be0f2 100644
--- a/docs/i18n/pl/llm.txt
+++ b/docs/i18n/pl/llm.txt
@@ -4,7 +4,7 @@
---
-> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
+> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 346 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 157 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ └── manager.ts # MITM proxy manager
│ ├── shared/ # Shared utilities, components, and constants
│ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.)
-│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes
+│ │ ├── constants/ # Provider definitions (346), model lists, pricing, routing strategies, MCP scopes
│ │ ├── contracts/ # Shared API contracts
│ │ ├── hooks/ # React hooks
│ │ ├── middleware/ # Shared middleware utilities
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
-- **342 AI providers** with automatic format translation
+- **346 AI providers** with automatic format translation
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
- **4-tier fallback**: Subscription → API Key → Cheap → Free
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 157 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
## v3.8.x Highlights
-- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
+- **346-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
- **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
diff --git a/docs/i18n/pt-BR/llm.txt b/docs/i18n/pt-BR/llm.txt
index c56d966794..59be9758e4 100644
--- a/docs/i18n/pt-BR/llm.txt
+++ b/docs/i18n/pt-BR/llm.txt
@@ -4,7 +4,7 @@
---
-> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
+> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 346 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 157 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ └── manager.ts # MITM proxy manager
│ ├── shared/ # Shared utilities, components, and constants
│ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.)
-│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes
+│ │ ├── constants/ # Provider definitions (346), model lists, pricing, routing strategies, MCP scopes
│ │ ├── contracts/ # Shared API contracts
│ │ ├── hooks/ # React hooks
│ │ ├── middleware/ # Shared middleware utilities
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
-- **342 AI providers** with automatic format translation
+- **346 AI providers** with automatic format translation
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
- **4-tier fallback**: Subscription → API Key → Cheap → Free
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 157 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
## v3.8.x Highlights
-- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
+- **346-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
- **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
diff --git a/docs/i18n/pt/llm.txt b/docs/i18n/pt/llm.txt
index 6e2aa2bb20..1f4f205d44 100644
--- a/docs/i18n/pt/llm.txt
+++ b/docs/i18n/pt/llm.txt
@@ -4,7 +4,7 @@
---
-> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
+> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 346 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 157 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ └── manager.ts # MITM proxy manager
│ ├── shared/ # Shared utilities, components, and constants
│ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.)
-│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes
+│ │ ├── constants/ # Provider definitions (346), model lists, pricing, routing strategies, MCP scopes
│ │ ├── contracts/ # Shared API contracts
│ │ ├── hooks/ # React hooks
│ │ ├── middleware/ # Shared middleware utilities
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
-- **342 AI providers** with automatic format translation
+- **346 AI providers** with automatic format translation
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
- **4-tier fallback**: Subscription → API Key → Cheap → Free
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 157 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
## v3.8.x Highlights
-- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
+- **346-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
- **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
diff --git a/docs/i18n/ro/llm.txt b/docs/i18n/ro/llm.txt
index 4fac614baf..485f971d1f 100644
--- a/docs/i18n/ro/llm.txt
+++ b/docs/i18n/ro/llm.txt
@@ -4,7 +4,7 @@
---
-> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
+> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 346 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 157 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ └── manager.ts # MITM proxy manager
│ ├── shared/ # Shared utilities, components, and constants
│ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.)
-│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes
+│ │ ├── constants/ # Provider definitions (346), model lists, pricing, routing strategies, MCP scopes
│ │ ├── contracts/ # Shared API contracts
│ │ ├── hooks/ # React hooks
│ │ ├── middleware/ # Shared middleware utilities
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
-- **342 AI providers** with automatic format translation
+- **346 AI providers** with automatic format translation
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
- **4-tier fallback**: Subscription → API Key → Cheap → Free
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 157 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
## v3.8.x Highlights
-- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
+- **346-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
- **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
diff --git a/docs/i18n/ru/llm.txt b/docs/i18n/ru/llm.txt
index 9d522135b2..319111231a 100644
--- a/docs/i18n/ru/llm.txt
+++ b/docs/i18n/ru/llm.txt
@@ -4,7 +4,7 @@
---
-> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
+> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 346 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 157 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ └── manager.ts # MITM proxy manager
│ ├── shared/ # Shared utilities, components, and constants
│ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.)
-│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes
+│ │ ├── constants/ # Provider definitions (346), model lists, pricing, routing strategies, MCP scopes
│ │ ├── contracts/ # Shared API contracts
│ │ ├── hooks/ # React hooks
│ │ ├── middleware/ # Shared middleware utilities
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
-- **342 AI providers** with automatic format translation
+- **346 AI providers** with automatic format translation
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
- **4-tier fallback**: Subscription → API Key → Cheap → Free
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 157 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
## v3.8.x Highlights
-- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
+- **346-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
- **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
diff --git a/docs/i18n/sk/llm.txt b/docs/i18n/sk/llm.txt
index df0dbd95a7..edb3477ef2 100644
--- a/docs/i18n/sk/llm.txt
+++ b/docs/i18n/sk/llm.txt
@@ -4,7 +4,7 @@
---
-> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
+> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 346 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 157 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ └── manager.ts # MITM proxy manager
│ ├── shared/ # Shared utilities, components, and constants
│ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.)
-│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes
+│ │ ├── constants/ # Provider definitions (346), model lists, pricing, routing strategies, MCP scopes
│ │ ├── contracts/ # Shared API contracts
│ │ ├── hooks/ # React hooks
│ │ ├── middleware/ # Shared middleware utilities
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
-- **342 AI providers** with automatic format translation
+- **346 AI providers** with automatic format translation
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
- **4-tier fallback**: Subscription → API Key → Cheap → Free
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 157 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
## v3.8.x Highlights
-- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
+- **346-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
- **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
diff --git a/docs/i18n/sv/llm.txt b/docs/i18n/sv/llm.txt
index 59246e38bf..48847120cb 100644
--- a/docs/i18n/sv/llm.txt
+++ b/docs/i18n/sv/llm.txt
@@ -4,7 +4,7 @@
---
-> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
+> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 346 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 157 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ └── manager.ts # MITM proxy manager
│ ├── shared/ # Shared utilities, components, and constants
│ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.)
-│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes
+│ │ ├── constants/ # Provider definitions (346), model lists, pricing, routing strategies, MCP scopes
│ │ ├── contracts/ # Shared API contracts
│ │ ├── hooks/ # React hooks
│ │ ├── middleware/ # Shared middleware utilities
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
-- **342 AI providers** with automatic format translation
+- **346 AI providers** with automatic format translation
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
- **4-tier fallback**: Subscription → API Key → Cheap → Free
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 157 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
## v3.8.x Highlights
-- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
+- **346-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
- **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
diff --git a/docs/i18n/sw/llm.txt b/docs/i18n/sw/llm.txt
index dbbf236c5d..f90e0ec031 100644
--- a/docs/i18n/sw/llm.txt
+++ b/docs/i18n/sw/llm.txt
@@ -4,7 +4,7 @@
---
-> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
+> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 346 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 157 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ └── manager.ts # MITM proxy manager
│ ├── shared/ # Shared utilities, components, and constants
│ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.)
-│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes
+│ │ ├── constants/ # Provider definitions (346), model lists, pricing, routing strategies, MCP scopes
│ │ ├── contracts/ # Shared API contracts
│ │ ├── hooks/ # React hooks
│ │ ├── middleware/ # Shared middleware utilities
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
-- **342 AI providers** with automatic format translation
+- **346 AI providers** with automatic format translation
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
- **4-tier fallback**: Subscription → API Key → Cheap → Free
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 157 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
## v3.8.x Highlights
-- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
+- **346-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
- **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
diff --git a/docs/i18n/ta/llm.txt b/docs/i18n/ta/llm.txt
index 842ca8c7ee..8f64c7aab8 100644
--- a/docs/i18n/ta/llm.txt
+++ b/docs/i18n/ta/llm.txt
@@ -4,7 +4,7 @@
---
-> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
+> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 346 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 157 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ └── manager.ts # MITM proxy manager
│ ├── shared/ # Shared utilities, components, and constants
│ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.)
-│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes
+│ │ ├── constants/ # Provider definitions (346), model lists, pricing, routing strategies, MCP scopes
│ │ ├── contracts/ # Shared API contracts
│ │ ├── hooks/ # React hooks
│ │ ├── middleware/ # Shared middleware utilities
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
-- **342 AI providers** with automatic format translation
+- **346 AI providers** with automatic format translation
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
- **4-tier fallback**: Subscription → API Key → Cheap → Free
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 157 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
## v3.8.x Highlights
-- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
+- **346-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
- **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
diff --git a/docs/i18n/te/llm.txt b/docs/i18n/te/llm.txt
index 5098dbc0ae..483d2f8a41 100644
--- a/docs/i18n/te/llm.txt
+++ b/docs/i18n/te/llm.txt
@@ -4,7 +4,7 @@
---
-> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
+> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 346 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 157 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ └── manager.ts # MITM proxy manager
│ ├── shared/ # Shared utilities, components, and constants
│ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.)
-│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes
+│ │ ├── constants/ # Provider definitions (346), model lists, pricing, routing strategies, MCP scopes
│ │ ├── contracts/ # Shared API contracts
│ │ ├── hooks/ # React hooks
│ │ ├── middleware/ # Shared middleware utilities
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
-- **342 AI providers** with automatic format translation
+- **346 AI providers** with automatic format translation
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
- **4-tier fallback**: Subscription → API Key → Cheap → Free
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 157 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
## v3.8.x Highlights
-- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
+- **346-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
- **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
diff --git a/docs/i18n/th/llm.txt b/docs/i18n/th/llm.txt
index f1251db7be..ac3a14103a 100644
--- a/docs/i18n/th/llm.txt
+++ b/docs/i18n/th/llm.txt
@@ -4,7 +4,7 @@
---
-> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
+> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 346 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 157 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ └── manager.ts # MITM proxy manager
│ ├── shared/ # Shared utilities, components, and constants
│ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.)
-│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes
+│ │ ├── constants/ # Provider definitions (346), model lists, pricing, routing strategies, MCP scopes
│ │ ├── contracts/ # Shared API contracts
│ │ ├── hooks/ # React hooks
│ │ ├── middleware/ # Shared middleware utilities
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
-- **342 AI providers** with automatic format translation
+- **346 AI providers** with automatic format translation
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
- **4-tier fallback**: Subscription → API Key → Cheap → Free
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 157 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
## v3.8.x Highlights
-- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
+- **346-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
- **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
diff --git a/docs/i18n/tr/llm.txt b/docs/i18n/tr/llm.txt
index ba729555c2..d6e2fbd674 100644
--- a/docs/i18n/tr/llm.txt
+++ b/docs/i18n/tr/llm.txt
@@ -4,7 +4,7 @@
---
-> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
+> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 346 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 157 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ └── manager.ts # MITM proxy manager
│ ├── shared/ # Shared utilities, components, and constants
│ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.)
-│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes
+│ │ ├── constants/ # Provider definitions (346), model lists, pricing, routing strategies, MCP scopes
│ │ ├── contracts/ # Shared API contracts
│ │ ├── hooks/ # React hooks
│ │ ├── middleware/ # Shared middleware utilities
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
-- **342 AI providers** with automatic format translation
+- **346 AI providers** with automatic format translation
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
- **4-tier fallback**: Subscription → API Key → Cheap → Free
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 157 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
## v3.8.x Highlights
-- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
+- **346-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
- **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
diff --git a/docs/i18n/uk-UA/llm.txt b/docs/i18n/uk-UA/llm.txt
index 33b12b870b..17e04f3ce1 100644
--- a/docs/i18n/uk-UA/llm.txt
+++ b/docs/i18n/uk-UA/llm.txt
@@ -4,7 +4,7 @@
---
-> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
+> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 346 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 157 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ └── manager.ts # MITM proxy manager
│ ├── shared/ # Shared utilities, components, and constants
│ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.)
-│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes
+│ │ ├── constants/ # Provider definitions (346), model lists, pricing, routing strategies, MCP scopes
│ │ ├── contracts/ # Shared API contracts
│ │ ├── hooks/ # React hooks
│ │ ├── middleware/ # Shared middleware utilities
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
-- **342 AI providers** with automatic format translation
+- **346 AI providers** with automatic format translation
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
- **4-tier fallback**: Subscription → API Key → Cheap → Free
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 157 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
## v3.8.x Highlights
-- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
+- **346-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
- **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
diff --git a/docs/i18n/ur/llm.txt b/docs/i18n/ur/llm.txt
index bd5d5fdc83..9291f52642 100644
--- a/docs/i18n/ur/llm.txt
+++ b/docs/i18n/ur/llm.txt
@@ -4,7 +4,7 @@
---
-> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
+> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 346 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 157 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ └── manager.ts # MITM proxy manager
│ ├── shared/ # Shared utilities, components, and constants
│ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.)
-│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes
+│ │ ├── constants/ # Provider definitions (346), model lists, pricing, routing strategies, MCP scopes
│ │ ├── contracts/ # Shared API contracts
│ │ ├── hooks/ # React hooks
│ │ ├── middleware/ # Shared middleware utilities
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
-- **342 AI providers** with automatic format translation
+- **346 AI providers** with automatic format translation
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
- **4-tier fallback**: Subscription → API Key → Cheap → Free
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 157 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
## v3.8.x Highlights
-- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
+- **346-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
- **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
diff --git a/docs/i18n/vi/llm.txt b/docs/i18n/vi/llm.txt
index 4b59fd5d3d..0cc3e4e22e 100644
--- a/docs/i18n/vi/llm.txt
+++ b/docs/i18n/vi/llm.txt
@@ -4,7 +4,7 @@
---
-> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
+> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 346 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 157 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ └── manager.ts # MITM proxy manager
│ ├── shared/ # Shared utilities, components, and constants
│ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.)
-│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes
+│ │ ├── constants/ # Provider definitions (346), model lists, pricing, routing strategies, MCP scopes
│ │ ├── contracts/ # Shared API contracts
│ │ ├── hooks/ # React hooks
│ │ ├── middleware/ # Shared middleware utilities
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
-- **342 AI providers** with automatic format translation
+- **346 AI providers** with automatic format translation
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
- **4-tier fallback**: Subscription → API Key → Cheap → Free
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 157 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
## v3.8.x Highlights
-- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
+- **346-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
- **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
diff --git a/docs/i18n/zh-CN/docs/ops/RELEASE_CHECKLIST.md b/docs/i18n/zh-CN/docs/ops/RELEASE_CHECKLIST.md
index d8cefd6497..afdbf398af 100644
--- a/docs/i18n/zh-CN/docs/ops/RELEASE_CHECKLIST.md
+++ b/docs/i18n/zh-CN/docs/ops/RELEASE_CHECKLIST.md
@@ -275,14 +275,12 @@ npm run build:release
- [ ] `npm install -g omniroute@` 运行 postinstall 无致命退出
- [ ] 更新路径保留可选依赖:`omniroute update --apply` 以及自动更新器
运行 `npm install -g … --include=optional` 以确保 `optionalDependencies`(better-sqlite3、
- keytar、tls-client 以及 llmlingua SLM 栈:`@atjsh/llmlingua-2`、
- `@huggingface/transformers@3.5.2`、`@tensorflow/tfjs`、`js-tiktoken`)在更新后仍然存在。
- `@huggingface/transformers` 保持为可选依赖,这样其 `onnxruntime-node` CUDA provider postinstall
- 不会在 CUDA 11 主机上中断安装。Ultra 模式的 `modelPath` SLM 层还需要
+ keytar、tls-client 以及 llmlingua SLM 栈:`@atjsh/llmlingua-2@2.0.5`、
+ `js-tiktoken`)在更新后仍然存在。Ultra 模式的 `modelPath` SLM 层还需要
tinybert 模型,首次使用时自动下载到 `${DATA_DIR}/models/llmlingua`。postinstall
(`scripts/build/colocateOptionals.mjs`)随后将 SLM 可选依赖闭包共置到
- `dist/node_modules`,使 Worker 解析单一的 `@huggingface/transformers` 3.5.2
- 可选实例 — standalone trace 仅打包 transformers,不包含动态导入的
+ `dist/node_modules`,使 Worker 解析单一的 `@huggingface/transformers` ^4.2.0
+ 实例 — standalone trace 仅打包 transformers,不包含动态导入的
可选依赖,否则 Worker 会基于根目录的 transformers 加载 llmlingua-2,
SLM 层将静默失效。
- [ ] `omniroute status` 在无 `.env` 的情况下正常工作(CLI Token 路径,仅 loopback)
diff --git a/docs/i18n/zh-CN/llm.txt b/docs/i18n/zh-CN/llm.txt
index 122734bec7..28d5e6fe23 100644
--- a/docs/i18n/zh-CN/llm.txt
+++ b/docs/i18n/zh-CN/llm.txt
@@ -4,7 +4,7 @@
---
-> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
+> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 346 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 157 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ └── manager.ts # MITM proxy manager
│ ├── shared/ # Shared utilities, components, and constants
│ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.)
-│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes
+│ │ ├── constants/ # Provider definitions (346), model lists, pricing, routing strategies, MCP scopes
│ │ ├── contracts/ # Shared API contracts
│ │ ├── hooks/ # React hooks
│ │ ├── middleware/ # Shared middleware utilities
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
-- **342 AI providers** with automatic format translation
+- **346 AI providers** with automatic format translation
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
- **4-tier fallback**: Subscription → API Key → Cheap → Free
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 157 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
## v3.8.x Highlights
-- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
+- **346-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
- **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
diff --git a/docs/i18n/zh-TW/docs/ops/RELEASE_CHECKLIST.md b/docs/i18n/zh-TW/docs/ops/RELEASE_CHECKLIST.md
index bbadf03423..c604668d19 100644
--- a/docs/i18n/zh-TW/docs/ops/RELEASE_CHECKLIST.md
+++ b/docs/i18n/zh-TW/docs/ops/RELEASE_CHECKLIST.md
@@ -322,14 +322,12 @@ npm run build:release
- [ ] `npm install -g omniroute@<此版本>` 執行 postinstall 而不會致命退出
- [ ] 更新路徑保留選擇性依賴:`omniroute update --apply` 和自動更新器
執行 `npm install -g … --include=optional`,因此 `optionalDependencies`(better-sqlite3、
- keytar、tls-client,以及 llmlingua SLM 堆疊:`@atjsh/llmlingua-2`、
- `@huggingface/transformers@3.5.2`、`@tensorflow/tfjs`、`js-tiktoken`)在更新後仍會保留。
- `@huggingface/transformers` 維持選擇性,因此其 `onnxruntime-node` CUDA 提供者的 postinstall
- 不會在 CUDA 11 主機上中斷安裝。Ultra `modelPath` SLM 層還需要
+ keytar、tls-client,以及 llmlingua SLM 堆疊:`@atjsh/llmlingua-2@2.0.5`、
+ `js-tiktoken`)在更新後仍會保留。Ultra `modelPath` SLM 層還需要
tinybert 模型,會在首次使用時自動下載到 `${DATA_DIR}/models/llmlingua`。Postinstall
(`scripts/build/colocateOptionals.mjs`)接著將 SLM 選擇性閉包複製到
- `dist/node_modules`,使工作者解析到**單一** `@huggingface/transformers` 3.5.2
- 選擇性實例——獨立追蹤僅捆綁 transformers,而非動態匯入的
+ `dist/node_modules`,使工作者解析到**單一** `@huggingface/transformers` ^4.2.0
+ 實例——獨立追蹤僅捆綁 transformers,而非動態匯入的
選擇性套件,因此若無此步驟,工作者會載入 llmlingua-2 並使用根目錄的 transformers,
導致 SLM 層靜默地失敗但仍保持運作。
- [ ] `omniroute status` 在無 `.env` 的情況下正常運作(僅限 CLI 權杖路徑,迴環介面)
diff --git a/docs/i18n/zh-TW/llm.txt b/docs/i18n/zh-TW/llm.txt
index b1f841f8e9..ae8fd155b5 100644
--- a/docs/i18n/zh-TW/llm.txt
+++ b/docs/i18n/zh-TW/llm.txt
@@ -4,7 +4,7 @@
---
-> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
+> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 346 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 157 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ └── manager.ts # MITM proxy manager
│ ├── shared/ # Shared utilities, components, and constants
│ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.)
-│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes
+│ │ ├── constants/ # Provider definitions (346), model lists, pricing, routing strategies, MCP scopes
│ │ ├── contracts/ # Shared API contracts
│ │ ├── hooks/ # React hooks
│ │ ├── middleware/ # Shared middleware utilities
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
-- **342 AI providers** with automatic format translation
+- **346 AI providers** with automatic format translation
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
- **4-tier fallback**: Subscription → API Key → Cheap → Free
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 157 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
## v3.8.x Highlights
-- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
+- **346-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
- **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
diff --git a/docs/ops/RELEASE_CHECKLIST.md b/docs/ops/RELEASE_CHECKLIST.md
index 310bd5cf84..dfa96d53ee 100644
--- a/docs/ops/RELEASE_CHECKLIST.md
+++ b/docs/ops/RELEASE_CHECKLIST.md
@@ -351,14 +351,12 @@ Before shipping any v3.8.x release, verify these additional items:
- [ ] `npm install -g omniroute@` runs postinstall without fatal exit
- [ ] Update path keeps optional deps: `omniroute update --apply` and the auto-updater
run `npm install -g … --include=optional` so `optionalDependencies` (better-sqlite3,
- keytar, tls-client, and the llmlingua SLM stack: `@atjsh/llmlingua-2`,
- `@huggingface/transformers@3.5.2`, `@tensorflow/tfjs`, `js-tiktoken`) survive an update.
- `@huggingface/transformers` stays optional so its `onnxruntime-node` CUDA provider postinstall
- cannot abort installation on CUDA 11 hosts. The ultra `modelPath` SLM tier also needs the
+ keytar, tls-client, and the llmlingua SLM stack: `@atjsh/llmlingua-2@2.0.5`,
+ `js-tiktoken`) survive an update. The ultra `modelPath` SLM tier also needs the
tinybert model, auto-downloaded to `${DATA_DIR}/models/llmlingua` on first use. Postinstall
(`scripts/build/colocateOptionals.mjs`) then co-locates the SLM optional closure into
- `dist/node_modules` so the worker resolves a SINGLE `@huggingface/transformers` 3.5.2
- optional instance — the standalone trace bundles only transformers, not the dynamically-imported
+ `dist/node_modules` so the worker resolves a SINGLE `@huggingface/transformers` ^4.2.0
+ instance — the standalone trace bundles only transformers, not the dynamically-imported
optionals, so without this the worker would load llmlingua-2 against the root's transformers
and the SLM tier would silently fail-open.
- [ ] `omniroute status` works with no `.env` (CLI token path, loopback only)
diff --git a/docs/reference/ENVIRONMENT.md b/docs/reference/ENVIRONMENT.md
index bf3fbd68d2..401d7f6a30 100644
--- a/docs/reference/ENVIRONMENT.md
+++ b/docs/reference/ENVIRONMENT.md
@@ -282,6 +282,7 @@ OmniRoute provides a two-layer defense: request-side injection scanning and resp
| `OMNIROUTE_PAYLOAD_RULES_RELOAD_MS` | `5000` | `open-sse/services/payloadRules.ts` | Reload interval (ms) for hot-reloading the payload rules file. Minimum `1000`. |
| `OMNIROUTE_PREFER_CLAUDE_CODE_FOR_UNPREFIXED_CLAUDE_MODELS` | `false` | `open-sse/services/model.ts` | Opt-in: route bare `claude-*` model IDs from Claude Code clients through the Claude Code OAuth account instead of requiring a provider prefix. Explicit provider prefixes still win. Also configurable via a dashboard toggle on the Claude provider page. |
| `COMBO_CONCURRENCY_PER_MODEL` | `3` | `open-sse/services/comboConfig.ts` | Per-model concurrency cap for round-robin combos (#9100). The round-robin combo semaphore was hard-capped at 3 concurrent requests per model with no override, serializing higher-concurrency traffic behind that cap. Validated to `>= 1`, clamped to `<= 32`. |
+| `DISABLE_CONTEXT_WINDOW_CHECKS` | `false` | `open-sse/handlers/chatCore.ts` | Dangerous opt-in that skips OmniRoute's local context-window / max-input-token check for direct single-model requests. Upstream providers still enforce their actual limits; prompt compression and the model's own output-token cap remain active. Effective precedence is Feature Flags DB override > environment variable > default; no restart is required. |
---
@@ -504,6 +505,8 @@ detection above).
| `OMNIROUTE_MCP_SCOPES` | _(all)_ | `open-sse/mcp-server/server.ts` | Comma-separated scopes: `admin`, `combos`, `health`, `models`, `routing`, `budget`, `metrics`, `pricing`, `memory`, `skills`. |
| `OMNIROUTE_MCP_COMPRESS_DESCRIPTIONS` | `false` | `open-sse/mcp-server/descriptionCompressor.ts` | Compress MCP tool descriptions before serializing the manifest. Enable values: `1`, `true`, `on`. |
| `OMNIROUTE_MCP_DESCRIPTION_COMPRESSION` | `rtk` | `open-sse/mcp-server/descriptionCompressor.ts` | Compression algorithm/profile. Disable values: `0`, `false`, `off`. |
+| `OMNIROUTE_MCP_FETCH_TIMEOUT_MS` | `10000` | `open-sse/mcp-server/fetchTimeout.ts` | Abort budget (ms) for MCP-server internal management reads (health, resilience, combos, quota, usage). |
+| `OMNIROUTE_MCP_UPSTREAM_TIMEOUT_MS` | `60000` | `open-sse/mcp-server/fetchTimeout.ts` | Abort budget (ms) for MCP hops that wait on a provider (`route_request`, `web_search`, `web_fetch`). |
| `MODEL_SYNC_INTERVAL_HOURS` | `24` | `src/shared/services/modelSyncScheduler.ts` | Model catalog sync interval in hours. |
| `PROVIDER_LIMITS_SYNC_INTERVAL_MINUTES` | `70` | `src/lib/usage/providerLimits.ts` | Provider rate-limit and quota polling interval. |
| `PROVIDER_LIMITS_SYNC_SPACING_MS` | `1500` | `src/lib/usage/providerLimits.ts` | Gap (ms) between consecutive OAuth quota fetches in a bulk sync; OAuth connections are fetched one at a time to avoid bursting an upstream. `0` opts out (concurrent). |
@@ -732,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. |
@@ -755,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. |
@@ -1114,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 45eededb28..8b46db649b 100644
--- a/docs/reference/FEATURE_FLAGS.md
+++ b/docs/reference/FEATURE_FLAGS.md
@@ -76,13 +76,14 @@ used when neither a DB override nor an environment variable is present.
| `OMNIROUTE_ALLOW_LOCAL_PROVIDER_URLS` | boolean | `true` | | Allow adding/validating providers on local/private addresses (127.0.0.1, localhost, LAN). On by default (local-first); disable for strict public-only blocking. Cloud-metadata stays blocked. |
| `ENABLE_CC_COMPATIBLE_PROVIDER` | boolean | `false` | ✓ | Enable Claude Code compatible provider mode. |
-### Policies (3)
+### Policies (4)
| Key | Type | Default | Restart | Description |
| ----------------------------------------- | ------- | ---------- | ------- | ---------------------------------------------------------------------- |
| `TOOL_POLICY_MODE` | enum | `disabled` | | Tool-use policy enforcement mode. Values: `disabled`, `warn`, `block`. |
| `RATE_LIMIT_AUTO_ENABLE` | boolean | `false` | | Automatically enable rate limiting based on usage patterns. |
| `ALLOW_MULTI_CONNECTIONS_PER_COMPAT_NODE` | boolean | `false` | ✓ | Allow multiple connections per compatibility node. |
+| `DISABLE_CONTEXT_WINDOW_CHECKS` | boolean | `false` | | Skip OmniRoute's local context-window / max-input-token check for direct single-model requests. Upstream limits still apply. |
### Runtime (11)
diff --git a/docs/reference/PROVIDER_REFERENCE.md b/docs/reference/PROVIDER_REFERENCE.md
index f6b2568af5..30eac06ad5 100644
--- a/docs/reference/PROVIDER_REFERENCE.md
+++ b/docs/reference/PROVIDER_REFERENCE.md
@@ -10,7 +10,7 @@ lastUpdated: 2026-08-20
> Regenerate with: `npm run gen:provider-reference`
> **Last generated:** 2026-08-20
-Total providers: **343**. See category breakdown below.
+Total providers: **346**. See category breakdown below.
## Categories
@@ -120,7 +120,7 @@ Use the dashboard at `/dashboard/providers` to enable, configure, and test each
| `zai-web` | `zw` | Z.ai Web | Web cookie | [link](https://chat.z.ai) | Copy the "token" value from chat.z.ai → DevTools → Application → Local Storage. Do not copy cookies; OmniRoute handles the per-request CAPTCHA through its browser transport. | — |
| `zenmux-free` | `zmf` | ZenMux Free (Web) | Web cookie | [link](https://zenmux.ai) | Login at zenmux.ai, then export all cookies using EditThisCookie or Cookie-Editor and paste the full Cookie header string here. Refresh every ~30 days. | — |
-## API Key Providers (paid / paid-with-free-credits) (230)
+## API Key Providers (paid / paid-with-free-credits) (231)
| ID | Alias | Name | Tags | Website | Notes |
|----|-------|------|------|---------|-------|
@@ -319,6 +319,7 @@ Use the dashboard at `/dashboard/providers` to enable, configure, and test each
| `sumopod` | `sumopod` | SumoPod | API key | [link](https://ai.sumopod.com) | Use your SumoPod API key (sk-...) in Authorization: Bearer . Fully OpenAI-compatible. API base URL: https://ai.sumopod.com/v1. |
| `suno` | `suno` | Suno | API key | [link](https://suno.ai) | Paste session cookie from suno.ai (Clerk auth) |
| `synthetic` | `synthetic` | Synthetic | API key, aggregator | [link](https://synthetic.new) | — |
+| `tabitoken` | `tabitoken` | TabiToken | API key, aggregator | [link](https://tabitoken.com) | — |
| `tencent` | `tencent` | Tencent Hunyuan | API key | [link](https://hunyuan.tencent.com) | Get API key at console.cloud.tencent.com |
| `thebai` | `thebai` | TheB.AI | API key, aggregator | [link](https://theb.ai) | Bearer API key for the TheB.AI OpenAI-compatible gateway. |
| `tinyfish` | `tf` | TinyFish Fetch | API key | [link](https://docs.tinyfish.ai/fetch-api) | X-API-Key from agent.tinyfish.ai/api-keys |
@@ -355,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 |
|----|-------|------|------|---------|-------|
@@ -365,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/llm.txt b/llm.txt
index c80df65ca7..da0da5334e 100644
--- a/llm.txt
+++ b/llm.txt
@@ -1,6 +1,6 @@
# OmniRoute
-> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 343 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
+> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 346 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -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 (343), model lists, pricing, routing strategies, MCP scopes
+│ │ ├── constants/ # Provider definitions (346), model lists, pricing, routing strategies, MCP scopes
│ │ ├── contracts/ # Shared API contracts
│ │ ├── hooks/ # React hooks
│ │ ├── middleware/ # Shared middleware utilities
@@ -277,7 +277,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
-- **343 AI providers** with automatic format translation
+- **346 AI providers** with automatic format translation
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
- **4-tier fallback**: Subscription → API Key → Cheap → Free
@@ -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
-- **343-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
+- **346-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
- **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
diff --git a/next.config.mjs b/next.config.mjs
index 2f22b8aebd..df3f6e32c4 100644
--- a/next.config.mjs
+++ b/next.config.mjs
@@ -438,6 +438,11 @@ const nextConfig = {
destination: "/dashboard/omni-skills",
permanent: true,
},
+ {
+ source: "/dashboard/providers/freepik",
+ destination: "/dashboard/providers/magnific",
+ permanent: true,
+ },
// Architecture
{
source: "/docs/architecture",
diff --git a/open-sse/config/freeModelCatalog.data.ts b/open-sse/config/freeModelCatalog.data.ts
index fe30a37325..8236a4d5d8 100644
--- a/open-sse/config/freeModelCatalog.data.ts
+++ b/open-sse/config/freeModelCatalog.data.ts
@@ -439,7 +439,8 @@ export const FREE_MODEL_BUDGETS: FreeModelBudget[] = [
{ provider: "ovhcloud", modelId: "Qwen3.6-27B", displayName: "Qwen3.6 27B (OVH anonymous)", monthlyTokens: 0, creditTokens: 0, freeType: "keyless", poolKey: "ovhcloud-anon", tos: "ok" },
{ provider: "ovhcloud", modelId: "Mistral-Small-3.2-24B-Instruct-2506", displayName: "Mistral Small 3.2 24B (OVH anonymous)", monthlyTokens: 0, creditTokens: 0, freeType: "keyless", poolKey: "ovhcloud-anon", tos: "ok" },
{ provider: "ovhcloud", modelId: "Qwen2.5-VL-72B-Instruct", displayName: "Qwen2.5 VL 72B (OVH anonymous)", monthlyTokens: 0, creditTokens: 0, freeType: "keyless", poolKey: "ovhcloud-anon", tos: "ok" },
- { provider: "agnes", modelId: "agnes-2.5-pro", displayName: "Agnes 2.5 Pro", monthlyTokens: 0, creditTokens: 0, freeType: "recurring-uncapped", poolKey: "agnes-free", tos: "ok" },
+ { provider: "agnes", modelId: "agnes-1.5-flash", displayName: "Agnes 1.5 Flash", monthlyTokens: 0, creditTokens: 0, freeType: "recurring-uncapped", poolKey: "agnes-free", tos: "ok" },
+ { provider: "agnes", modelId: "agnes-2.0-flash", displayName: "Agnes 2.0 Flash", monthlyTokens: 0, creditTokens: 0, freeType: "recurring-uncapped", poolKey: "agnes-free", tos: "ok" },
{ provider: "agnes", modelId: "agnes-2.5-flash", displayName: "Agnes 2.5 Flash", monthlyTokens: 0, creditTokens: 0, freeType: "recurring-uncapped", poolKey: "agnes-free", tos: "ok" },
{ provider: "glm", modelId: "glm-4.7-flash", displayName: "GLM-4.7-Flash", monthlyTokens: 0, creditTokens: 0, freeType: "recurring-uncapped", poolKey: "zhipu-flash-free", tos: "ok" },
{ provider: "glm", modelId: "glm-4.5-flash", displayName: "GLM-4.5-Flash", monthlyTokens: 0, creditTokens: 0, freeType: "recurring-uncapped", poolKey: "zhipu-flash-free", tos: "ok" },
diff --git a/open-sse/config/glmProvider.ts b/open-sse/config/glmProvider.ts
index f8acfd4031..9c1580e7ae 100644
--- a/open-sse/config/glmProvider.ts
+++ b/open-sse/config/glmProvider.ts
@@ -18,6 +18,35 @@ export const GLM_ANTHROPIC_DEFAULT_BASE_URLS = Object.freeze({
});
export const GLM_SHARED_MODELS = Object.freeze([
+ {
+ // GLM-5.3 (2026-08-14): one upstream id; effort is the reasoning_effort
+ // param (low|high|max, default max) — the -high/-low entries below are
+ // OmniRoute aliases resolved by GlmExecutor::parseGlmEffortTier.
+ // Default context window not yet published by Z.ai; 1M mirrored from
+ // GLM-5.2 (same base model). https://z.ai/blog/glm-5.3
+ id: "glm-5.3",
+ name: "GLM 5.3",
+ contextLength: 1000000,
+ maxOutputTokens: 131072,
+ toolCalling: true,
+ supportsReasoning: true,
+ },
+ {
+ id: "glm-5.3-high",
+ name: "GLM 5.3 High",
+ contextLength: 1000000,
+ maxOutputTokens: 131072,
+ toolCalling: true,
+ supportsReasoning: true,
+ },
+ {
+ id: "glm-5.3-low",
+ name: "GLM 5.3 Low",
+ contextLength: 1000000,
+ maxOutputTokens: 131072,
+ toolCalling: true,
+ supportsReasoning: true,
+ },
{
id: "glm-5.2",
name: "GLM 5.2",
diff --git a/open-sse/config/imageRegistry.ts b/open-sse/config/imageRegistry.ts
index 8defd6d8a8..02019dc4a0 100644
--- a/open-sse/config/imageRegistry.ts
+++ b/open-sse/config/imageRegistry.ts
@@ -8,7 +8,7 @@
import { LMARENA_DIRECT_IMAGE_MODELS } from "./providers/registry/lmarena/directModels.ts";
import { SEGMIND_IMAGE_PROVIDER } from "./providers/registry/segmind/imageModels.ts";
import { KIE_IMAGE_MODELS } from "./providers/registry/kie/imageModels.ts";
-import { FREEPIK_IMAGE_PROVIDER } from "./providers/registry/freepik/index.ts";
+import { MAGNIFIC_IMAGE_PROVIDER } from "./providers/registry/magnific/index.ts";
import { STABILITY_AI_IMAGE_MODELS } from "./providers/registry/stability-ai/imageModels.ts";
import { CHEAPERINFERENCE_IMAGE_PROVIDER } from "./providers/registry/cheaperinference/imageModels.ts";
import {
@@ -495,7 +495,7 @@ export const IMAGE_PROVIDERS: Record = {
],
supportedSizes: ["1024x1024", "1024x1792", "1792x1024"],
},
- freepik: FREEPIK_IMAGE_PROVIDER,
+ magnific: MAGNIFIC_IMAGE_PROVIDER,
sdwebui: {
id: "sdwebui",
baseUrl: "http://localhost:7860/sdapi/v1/txt2img",
@@ -884,7 +884,12 @@ export const IMAGE_PROVIDERS: Record = {
* Get image provider config by ID
*/
export function getImageProvider(providerId) {
- return IMAGE_PROVIDERS[providerId] || null;
+ if (IMAGE_PROVIDERS[providerId]) return IMAGE_PROVIDERS[providerId];
+ if (!providerId) return null;
+ for (const config of Object.values(IMAGE_PROVIDERS)) {
+ if (config.alias === providerId) return config;
+ }
+ return null;
}
/**
@@ -918,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 };
}
}
@@ -1021,12 +1026,7 @@ export function getImageModelEntry(modelStr) {
};
}
-/**
- * An image input is only MANDATORY for edit-only models — those whose modalities
- * are `["image"]` with no `"text"`. Models listing both `["text", "image"]` accept
- * an image but can also run pure text-to-image, so they must NOT be gated on an
- * image input (that gate previously blocked 41 dual-modality t2i models).
- */
+/** Image input is mandatory only for edit-only models (`["image"]`, no `"text"`). Dual-modality models also accept pure t2i. */
export function modalitiesRequireImageInput(inputModalities) {
const list = Array.isArray(inputModalities) ? inputModalities : ["text"];
return list.includes("image") && !list.includes("text");
diff --git a/open-sse/config/musicRegistry.ts b/open-sse/config/musicRegistry.ts
index 06aa8a159b..fd1f88fd88 100644
--- a/open-sse/config/musicRegistry.ts
+++ b/open-sse/config/musicRegistry.ts
@@ -17,6 +17,8 @@ interface MusicProvider {
id: string;
baseUrl: string;
statusUrl?: string;
+ /** Regional deployment of the same contract, reachable via a base-URL override. */
+ regionalBaseUrl?: string;
authType: string;
authHeader: string;
format: string;
@@ -79,14 +81,21 @@ export const MUSIC_PROVIDERS: Record = {
minimax: {
id: "minimax",
baseUrl: "https://api.minimax.io/v1/music_generation",
- statusUrl: "https://api.minimax.io/v1/query/music_generation",
+ // The music operation answers with the finished audio in the POST response —
+ // there is no task id and no query endpoint, hence no statusUrl. The regional
+ // deployment serves the same contract and is the only host that accepts the
+ // `aigc_watermark` request field.
+ regionalBaseUrl: "https://api.minimaxi.com/v1/music_generation",
authType: "apikey",
authHeader: "bearer",
format: "minimax-music",
models: [
+ { id: "music-3.0", name: "Music 3.0" },
{ id: "music-2.6", name: "Music 2.6" },
+ { id: "music-3.0-free", name: "Music 3.0 Free" },
{ id: "music-2.6-free", name: "Music 2.6 Free" },
{ id: "music-cover", name: "Music Cover" },
+ { id: "music-cover-free", name: "Music Cover Free" },
],
},
comfyui: {
diff --git a/open-sse/config/providerErrorRules.ts b/open-sse/config/providerErrorRules.ts
index 17d72be598..c910e3fb4d 100644
--- a/open-sse/config/providerErrorRules.ts
+++ b/open-sse/config/providerErrorRules.ts
@@ -53,11 +53,13 @@ export type ProviderErrorRuleMatch = {
// every model on the same provider until the 5h window resets.
//
// Scope note: `scope: "connection"` (not "provider") is correct because the
-// upstream quota is per-account, and a single OmniRoute provider entry maps to
-// one user account. Multiple OmniRoute connections under the same provider
-// name mean the user has multiple upstream accounts — locking at the provider
-// level would disable every one of them when only one is exhausted. See
-// Issue #2 (Monthly quota exhausted treated as transient 429).
+// upstream quota is per egress IP for the free tier (the opencode free tier
+// is IP-bucketed, not account-bucketed — see #9611) and per account for paid
+// plans; a single OmniRoute provider entry maps to one user account. Multiple
+// OmniRoute connections under the same provider name mean the user has
+// multiple upstream accounts — locking at the provider level would disable
+// every one of them when only one is exhausted. See Issue #2 (Monthly quota
+// exhausted treated as transient 429) and #10880 (egress-bucketed cooldown).
function buildOpencodeRules(): ProviderErrorRule[] {
return [
{
@@ -277,6 +279,36 @@ export function honorsRuleLockScope(provider: string | null | undefined): boolea
return !!provider && HONORS_RULE_LOCK_SCOPE_PROVIDERS.has(provider.toLowerCase());
}
+/**
+ * Providers whose upstream quota is bucketed by EGRESS IP, not by account —
+ * the opencode free tier is IP-bucketed, not account-bucketed (see #9611).
+ * When such a provider answers 429 quota_exhausted or
+ * rate_limit_exceeded (see the markAccountUnavailable branch comment — the
+ * real opencode 429 arrives as rate_limit_exceeded on that path), every
+ * connection egressing through that IP shares the exhausted budget, so the
+ * lock is applied at egress-IP scope (see markAccountUnavailable /
+ * applyEgressIpLockout). EXCLUSIVE allowlist by design — same pattern as
+ * HONORS_RULE_LOCK_SCOPE_PROVIDERS (#10334): a provider must opt in, and any
+ * widening is an explicit owner decision.
+ */
+const EGRESS_BUCKETED_LOCK_PROVIDERS = new Set(["opencode", "opencode-go", "opencode-cli"]);
+
+export function isEgressBucketedLockScope(provider: string | null | undefined): boolean {
+ return !!provider && EGRESS_BUCKETED_LOCK_PROVIDERS.has(provider.toLowerCase());
+}
+
+/**
+ * The same allowlist as a sorted array, for callers that must express it as
+ * data rather than a predicate (the sibling lookup in `applyEgressIpLockout`
+ * binds it into a SQL `IN (...)`). Single source of truth on purpose: a
+ * literal provider list duplicated in a query would silently NOT follow a
+ * widening of `EGRESS_BUCKETED_LOCK_PROVIDERS`, leaving the opt-in half
+ * applied.
+ */
+export function egressBucketedLockProviders(): string[] {
+ return [...EGRESS_BUCKETED_LOCK_PROVIDERS].sort();
+}
+
/**
* Providers whose rules match on the FULL upstream error text.
* checkFallbackError's rule lookup normally passes only the structured
diff --git a/open-sse/config/providers/alternateFormats.ts b/open-sse/config/providers/alternateFormats.ts
index b223a26448..8a7deb782c 100644
--- a/open-sse/config/providers/alternateFormats.ts
+++ b/open-sse/config/providers/alternateFormats.ts
@@ -19,6 +19,19 @@ export interface AlternateFormat {
authHeader?: string;
headers?: Record;
urlSuffix?: string;
+ /**
+ * Monta a URL final quando o protocolo alternativo embute o modelo no path, e
+ * nao apenas um sufixo fixo. O caso concreto e o protocolo Gemini, cuja rota e
+ * `{base}/{model}:generateContent` (ou `:streamGenerateContent?alt=sse`) — algo
+ * que `chatPath`/`urlSuffix` nao expressam, porque ambos sao constantes.
+ *
+ * Mesma assinatura do `urlBuilder` de RegistryEntry (base ja sem "/" final,
+ * modelo e stream), de proposito: um gateway que fala Gemini como alternativa
+ * reaproveita `buildGeminiGenerateContentUrl` de shared.ts — o mesmo builder que
+ * o provedor Gemini nativo usa — em vez de reimplementar a rota.
+ * Quando ausente, a URL continua sendo `baseUrl + chatPath + urlSuffix`.
+ */
+ urlBuilder?: (base: string, model: string, stream: boolean) => string;
label: string;
}
diff --git a/open-sse/config/providers/index.ts b/open-sse/config/providers/index.ts
index 34189e0d00..00861b2422 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";
@@ -261,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,
@@ -277,6 +283,7 @@ export const REGISTRY: Record = {
deepai: deepaiProvider,
nebius: nebiusProvider,
fireworks: fireworksProvider,
+ freebuff: freebuffProvider,
llamagate: llamagateProvider,
glm: glmProvider,
glmt: glmtProvider,
@@ -526,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/agnes/index.ts b/open-sse/config/providers/registry/agnes/index.ts
index 848ad0242c..2843328f00 100644
--- a/open-sse/config/providers/registry/agnes/index.ts
+++ b/open-sse/config/providers/registry/agnes/index.ts
@@ -2,21 +2,28 @@ import type { RegistryEntry } from "../../shared.ts";
export const agnesProvider: RegistryEntry = {
id: "agnes",
- format: "openai-responses",
+ format: "openai",
executor: "default",
- baseUrl: "https://apihub.agnes-ai.com/v1/responses",
+ baseUrl: "https://apihub.agnes-ai.com/v1/chat/completions",
authType: "apikey",
authHeader: "bearer",
models: [
{
- id: "agnes-2.5-pro",
- name: "Agnes 2.5 Pro",
- contextLength: 1048576,
+ id: "agnes-1.5-flash",
+ name: "Agnes 1.5 Flash",
+ contextLength: 262144,
+ maxOutputTokens: 65536,
+ supportsVision: true,
+ toolCalling: true,
+ },
+ {
+ id: "agnes-2.0-flash",
+ name: "Agnes 2.0 Flash",
+ contextLength: 262144,
maxOutputTokens: 65536,
supportsReasoning: true,
supportsVision: true,
toolCalling: true,
- interleavedField: "reasoning_content",
},
{
id: "agnes-2.5-flash",
diff --git a/open-sse/config/providers/registry/chatgpt-web-codex/index.ts b/open-sse/config/providers/registry/chatgpt-web-codex/index.ts
index a1ccb6b13c..1c290668f9 100644
--- a/open-sse/config/providers/registry/chatgpt-web-codex/index.ts
+++ b/open-sse/config/providers/registry/chatgpt-web-codex/index.ts
@@ -14,6 +14,7 @@ export const chatgpt_web_codexProvider: RegistryEntry = {
format: "openai-responses",
executor: "chatgpt-web-codex",
baseUrl: "https://chatgpt.com",
+ reasoningTransport: "opaque",
authType: "apikey",
authHeader: "cookie",
forceStream: true,
diff --git a/open-sse/config/providers/registry/codex/index.ts b/open-sse/config/providers/registry/codex/index.ts
index 7d6fee4557..6b67797fa3 100644
--- a/open-sse/config/providers/registry/codex/index.ts
+++ b/open-sse/config/providers/registry/codex/index.ts
@@ -12,6 +12,7 @@ export const codexProvider: RegistryEntry = {
format: "openai-responses",
executor: "codex",
baseUrl: "https://chatgpt.com/backend-api/codex/responses",
+ reasoningTransport: "opaque",
authType: "oauth",
authHeader: "bearer",
defaultContextLength: 400000,
diff --git a/open-sse/config/providers/registry/command-code/index.ts b/open-sse/config/providers/registry/command-code/index.ts
index 23a73efe46..77b08ab8ce 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/freebuff/index.ts b/open-sse/config/providers/registry/freebuff/index.ts
new file mode 100644
index 0000000000..713f469548
--- /dev/null
+++ b/open-sse/config/providers/registry/freebuff/index.ts
@@ -0,0 +1,70 @@
+import type { RegistryEntry } from "../../shared.ts";
+
+export const freebuffProvider: RegistryEntry = {
+ id: "freebuff",
+ alias: "fb",
+ format: "openai",
+ executor: "freebuff",
+ baseUrl: "https://www.codebuff.com/api/v1",
+ authType: "apikey",
+ authHeader: "bearer",
+ models: [
+ {
+ id: "deepseek/deepseek-v4-flash",
+ name: "DeepSeek V4 Flash",
+ supportsReasoning: true,
+ contextLength: 131_072,
+ },
+ {
+ id: "deepseek/deepseek-v4-pro",
+ name: "DeepSeek V4 Pro",
+ supportsReasoning: true,
+ contextLength: 131_072,
+ },
+ {
+ id: "openai/gpt-5.6-luna",
+ name: "GPT-5.6 Luna",
+ supportsReasoning: true,
+ contextLength: 131_072,
+ },
+ {
+ id: "minimax/minimax-m3",
+ name: "MiniMax M3",
+ supportsVision: true,
+ supportsReasoning: true,
+ contextLength: 131_072,
+ },
+ {
+ id: "mimo/mimo-v2.5",
+ name: "MiMo v2.5",
+ supportsReasoning: true,
+ contextLength: 131_072,
+ },
+ {
+ id: "z-ai/glm-5.2",
+ name: "GLM 5.2",
+ supportsReasoning: true,
+ contextLength: 131_072,
+ },
+ {
+ id: "crof/kimi-k3-eco",
+ name: "Kimi K3 Eco",
+ supportsVision: true,
+ supportsReasoning: true,
+ contextLength: 131_072,
+ },
+ {
+ id: "anthropic/claude-fable-5",
+ name: "Claude Fable 5",
+ supportsVision: true,
+ supportsReasoning: true,
+ contextLength: 131_072,
+ },
+ {
+ id: "meta/muse-spark-1.2-contributor",
+ name: "Meta Muse Spark 1.2 Contributor",
+ supportsReasoning: true,
+ contextLength: 131_072,
+ },
+ ],
+};
diff --git a/open-sse/config/providers/registry/freepik/index.ts b/open-sse/config/providers/registry/freepik/index.ts
deleted file mode 100644
index 7b99c71168..0000000000
--- a/open-sse/config/providers/registry/freepik/index.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-/**
- * Freepik (Magnific Mystic) image provider registry entry.
- * Extracted into its own module to keep open-sse/config/imageRegistry.ts
- * under the file-size cap (god-file decomposition; semantic split).
- */
-export const FREEPIK_IMAGE_PROVIDER = {
- id: "freepik",
- // Freepik rebranded its API docs to Magnific in April 2026; the Mystic
- // endpoint itself still lives under api.freepik.com as of this writing
- // (docs.freepik.com redirects to docs.magnific.com, but the API host
- // has not moved). Re-verify against live docs if this ever 404s.
- baseUrl: "https://api.freepik.com/v1/ai/mystic",
- statusUrl: "https://api.freepik.com/v1/ai/mystic",
- authType: "apikey",
- authHeader: "x-freepik-api-key",
- format: "freepik-image", // custom: async submit task_id, then poll GET /{task_id}
- models: [
- { id: "realism", name: "Mystic Realism" },
- { id: "fluid", name: "Mystic Fluid (Imagen 3)" },
- { id: "zen", name: "Mystic Zen" },
- { id: "flexible", name: "Mystic Flexible" },
- { id: "super_real", name: "Mystic Super Real" },
- { id: "editorial_portraits", name: "Mystic Editorial Portraits" },
- ],
- supportedSizes: ["1024x1024", "1024x1792", "1792x1024"],
-};
diff --git a/open-sse/config/providers/registry/gemini/index.ts b/open-sse/config/providers/registry/gemini/index.ts
index 8119ed9c41..468fcd8889 100644
--- a/open-sse/config/providers/registry/gemini/index.ts
+++ b/open-sse/config/providers/registry/gemini/index.ts
@@ -1,5 +1,5 @@
import type { RegistryEntry } from "../../shared.ts";
-import { resolvePublicCred } from "../../shared.ts";
+import { buildGeminiGenerateContentUrl, resolvePublicCred } from "../../shared.ts";
export const geminiProvider: RegistryEntry = {
id: "gemini",
@@ -7,10 +7,7 @@ export const geminiProvider: RegistryEntry = {
format: "gemini",
executor: "default",
baseUrl: "https://generativelanguage.googleapis.com/v1beta/models",
- urlBuilder: (base, model, stream) => {
- const action = stream ? "streamGenerateContent?alt=sse" : "generateContent";
- return `${base}/${model}:${action}`;
- },
+ urlBuilder: buildGeminiGenerateContentUrl,
authType: "apikey",
authHeader: "x-goog-api-key",
defaultContextLength: 1048576,
diff --git a/open-sse/config/providers/registry/grok-cli/index.ts b/open-sse/config/providers/registry/grok-cli/index.ts
index f257f8d60a..e65ced0e76 100644
--- a/open-sse/config/providers/registry/grok-cli/index.ts
+++ b/open-sse/config/providers/registry/grok-cli/index.ts
@@ -14,6 +14,7 @@ export const grok_cliProvider: RegistryEntry = {
// Keep the generic translate-path contract stable. GrokCliExecutor owns the
// official Grok Build upstream URL and always dispatches to /v1/responses.
baseUrl: "https://cli-chat-proxy.grok.com/v1/chat/completions",
+ reasoningTransport: "opaque",
modelsUrl: GROK_BUILD_MODELS_URL,
clientVersion: getGrokBuildClientVersion(),
authType: "oauth",
diff --git a/open-sse/config/providers/registry/hcnsec/index.ts b/open-sse/config/providers/registry/hcnsec/index.ts
index acce62c2bc..dba2b690da 100644
--- a/open-sse/config/providers/registry/hcnsec/index.ts
+++ b/open-sse/config/providers/registry/hcnsec/index.ts
@@ -1,11 +1,62 @@
import type { RegistryEntry } from "../../shared.ts";
-import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts";
+import {
+ buildGeminiGenerateContentUrl,
+ buildOpenAiCompatibleRegistryEntry,
+ getAnthropicCompatHeaders,
+} from "../../shared.ts";
+/**
+ * HCNSec — NewAPI-based host (https://api.hcnsec.cn), announced by its own `/api/status` as
+ * 新疆幻城网安科技公益大模型安全网关. Catalogued as an API-key **regional** provider
+ * (`APIKEY_PROVIDERS_REGIONAL.hcnsec`); this entry only describes how to reach it.
+ *
+ * It shipped OpenAI-only. The three alternates below were added after probing the host live:
+ * every one of them reaches the NewAPI token layer (`{"error":{"type":"new_api_error"}}` on an
+ * invalid key) rather than a router 404, so each is a route this host actually serves —
+ * including the Gemini path in both its unary and `:streamGenerateContent?alt=sse` forms.
+ * The default format, base URL and auth scheme are deliberately untouched.
+ *
+ * `models: []` is unchanged and deliberate. Unlike TabiToken, this host gates every discovery
+ * endpoint behind auth (`/api/status` reports `pricing.requireAuth: true`; `/api/pricing`,
+ * `/api/models`, `/api/models/display` and `/api/user/models` all answer "Unauthorized, not
+ * logged in and no access token provided"). Rather than ship a guessed catalog, the model list
+ * is left to live discovery through `modelsUrl` with the operator's own key — the same
+ * arrangement `anyapi` and `helixmind` use.
+ */
export const hcnsecProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({
id: "hcnsec",
alias: "hcnsec",
baseUrl: "https://api.hcnsec.cn/v1/chat/completions",
modelsUrl: "https://api.hcnsec.cn/v1/models",
+ responsesBaseUrl: "https://api.hcnsec.cn/v1/responses",
models: [],
passthroughModels: true,
+ alternateFormats: [
+ {
+ // `Anthropic-Version` is scoped to this alternate (deepseek's arrangement) because
+ // it is only meaningful on `/v1/messages`, and because `default.ts` supplies that
+ // default solely for `anthropic-compatible-*` provider ids — not for a gateway that
+ // reaches the Claude protocol through an alternate.
+ format: "claude",
+ baseUrl: "https://api.hcnsec.cn/v1/messages",
+ authHeader: "x-api-key",
+ headers: getAnthropicCompatHeaders(),
+ label: "Anthropic-compatible",
+ },
+ {
+ format: "openai-responses",
+ baseUrl: "https://api.hcnsec.cn/v1/responses",
+ authHeader: "bearer",
+ label: "OpenAI Responses",
+ },
+ {
+ // The Gemini protocol carries the model in the path, so this alternate needs the
+ // same builder the native `gemini` provider uses instead of a constant chatPath.
+ format: "gemini",
+ baseUrl: "https://api.hcnsec.cn/v1beta/models",
+ authHeader: "x-goog-api-key",
+ urlBuilder: buildGeminiGenerateContentUrl,
+ label: "Gemini-compatible",
+ },
+ ],
});
diff --git a/open-sse/config/providers/registry/magnific/index.ts b/open-sse/config/providers/registry/magnific/index.ts
new file mode 100644
index 0000000000..63c83d5b1c
--- /dev/null
+++ b/open-sse/config/providers/registry/magnific/index.ts
@@ -0,0 +1,26 @@
+/**
+ * Magnific Mystic image provider registry entry.
+ * Extracted into its own module to keep open-sse/config/imageRegistry.ts
+ * under the file-size cap (god-file decomposition; semantic split).
+ */
+export const MAGNIFIC_IMAGE_PROVIDER = {
+ id: "magnific",
+ // Official Magnific API (docs.magnific.com). The previous OmniRoute slug
+ // was `freepik` because Magnific started as Freepik's developer API; keep
+ // that id as a legacy alias so old URLs and `freepik/` still resolve.
+ alias: "freepik",
+ baseUrl: "https://api.magnific.com/v1/ai/mystic",
+ statusUrl: "https://api.magnific.com/v1/ai/mystic",
+ authType: "apikey",
+ authHeader: "x-magnific-api-key",
+ format: "magnific-image", // custom: async submit task_id, then poll GET /{task_id}
+ models: [
+ { id: "realism", name: "Mystic Realism" },
+ { id: "fluid", name: "Mystic Fluid (Imagen 3)" },
+ { id: "zen", name: "Mystic Zen" },
+ { id: "flexible", name: "Mystic Flexible" },
+ { id: "super_real", name: "Mystic Super Real" },
+ { id: "editorial_portraits", name: "Mystic Editorial Portraits" },
+ ],
+ supportedSizes: ["1024x1024", "1024x1792", "1792x1024"],
+};
diff --git a/open-sse/config/providers/registry/mlx/index.ts b/open-sse/config/providers/registry/mlx/index.ts
new file mode 100644
index 0000000000..24d3e8b232
--- /dev/null
+++ b/open-sse/config/providers/registry/mlx/index.ts
@@ -0,0 +1,66 @@
+import type { RegistryEntry } from "../../shared.ts";
+import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts";
+
+// MLX ports (deterministic, documented)
+const MLX_GEMMA_PORT = 11435;
+const MLX_QWEN_PORT = 11436;
+
+// ─────────────────────────────────────────────────────────────────────────────
+// Memory-aware context windows for MLX models on 24GB unified memory.
+// Based on verified peak memory: Gemma 26B ~15.9GB, Qwen 27B ~13.1GB.
+// KV cache estimate: 2 * 2 * layers * kv_heads * head_dim * num_ctx bytes.
+// Conservative context windows to leave headroom for OS/other processes.
+export const MLX_DEFAULT_CONTEXT_LIMIT = 32768;
+
+const CONTEXT_GEMMA_26B = 8192; // 15.9GB weights + ~3.5GB KV @ 8k = ~19.4GB (safe for 24GB)
+const CONTEXT_QWEN_27B = 8192; // 13.1GB weights + ~3.5GB KV @ 8k = ~16.6GB (safe for 24GB)
+
+// ─────────────────────────────────────────────────────────────────────────────
+// MLX Gemma 26B Provider
+// Model: mlx-community/gemma-4-26B-A4B-it-qat-q4_0-mlx-aligned
+// Verified speed: ~38.5 tok/s, peak memory: ~15.9 GB
+export const mlxGemmaProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({
+ id: "mlx-gemma",
+ alias: "mlx-gemma",
+ baseUrl: `http://localhost:${MLX_GEMMA_PORT}/v1`,
+ modelsUrl: `http://localhost:${MLX_GEMMA_PORT}/v1/models`,
+ passthroughModels: false,
+ defaultContextLength: MLX_DEFAULT_CONTEXT_LIMIT,
+ models: [
+ {
+ id: "mlx-community/gemma-4-26B-A4B-it-qat-q4_0-mlx-aligned",
+ name: "Gemma 4 26B A4B IT-QAT (MLX)",
+ toolCalling: true,
+ supportsVision: false,
+ supportsReasoning: false,
+ contextLength: CONTEXT_GEMMA_26B,
+ maxOutputTokens: 8192,
+ },
+ ],
+ timeoutMs: 120000, // Longer timeout for model loading
+});
+
+// ─────────────────────────────────────────────────────────────────────────────
+// MLX Qwen3.8 27B Provider
+// Model: maglun/Qwen3.8-27B-MLX-Mixed-3.80bpw
+// Verified speed: ~9.1 tok/s, peak memory: ~13.1 GB
+export const mlxQwenProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({
+ id: "mlx-qwen",
+ alias: "mlx-qwen",
+ baseUrl: `http://localhost:${MLX_QWEN_PORT}/v1`,
+ modelsUrl: `http://localhost:${MLX_QWEN_PORT}/v1/models`,
+ passthroughModels: false,
+ defaultContextLength: MLX_DEFAULT_CONTEXT_LIMIT,
+ models: [
+ {
+ id: "maglun/Qwen3.8-27B-MLX-Mixed-3.80bpw",
+ name: "Qwen 3.8 27B MLX Mixed 3.80bpw",
+ toolCalling: true,
+ supportsVision: false,
+ supportsReasoning: false,
+ contextLength: CONTEXT_QWEN_27B,
+ maxOutputTokens: 8192,
+ },
+ ],
+ timeoutMs: 120000, // Longer timeout for model loading
+});
diff --git a/open-sse/config/providers/registry/muse-code/index.ts b/open-sse/config/providers/registry/muse-code/index.ts
index 66f59f9f42..37db1e988d 100644
--- a/open-sse/config/providers/registry/muse-code/index.ts
+++ b/open-sse/config/providers/registry/muse-code/index.ts
@@ -14,6 +14,7 @@ export const muse_codeProvider: RegistryEntry = buildOpenAiCompatibleRegistryEnt
id: "muse-code",
alias: "mc",
passthroughModels: true,
+ reasoningTransport: "opaque",
defaultContextLength: 200000,
models: [
{
diff --git a/open-sse/config/providers/registry/ollama-cloud/index.ts b/open-sse/config/providers/registry/ollama-cloud/index.ts
index 05b28df65f..4cf020263a 100644
--- a/open-sse/config/providers/registry/ollama-cloud/index.ts
+++ b/open-sse/config/providers/registry/ollama-cloud/index.ts
@@ -24,8 +24,24 @@ export const ollama_cloudProvider: RegistryEntry = {
supportsReasoning: true,
supportedThinkingEfforts: ["low", "medium", "high"],
},
- { id: "deepseek-v4-pro", name: "DeepSeek V4 Pro", supportsReasoning: true },
- { id: "deepseek-v4-flash", name: "DeepSeek V4 Flash", supportsReasoning: true },
+ // #10788: Ollama Cloud accepts low|medium|high|max|none uniformly across
+ // its reasoning-capable models (see supportsMaxEffortForProvider's
+ // isOllamaCloud comment in open-sse/executors/base/reasoningEffort.ts) —
+ // declare supportedThinkingEfforts so appendSyncedEffortVariants() (which
+ // runs before static-model capability enrichment) can synthesize the
+ // catalog's selectable -low/-high/-max variant ids for these models.
+ {
+ id: "deepseek-v4-pro",
+ name: "DeepSeek V4 Pro",
+ supportsReasoning: true,
+ supportedThinkingEfforts: ["low", "medium", "high", "max"],
+ },
+ {
+ id: "deepseek-v4-flash",
+ name: "DeepSeek V4 Flash",
+ supportsReasoning: true,
+ supportedThinkingEfforts: ["low", "medium", "high", "max"],
+ },
{ id: "kimi-k2.6", name: "Kimi K2.6" },
// Ollama Cloud accepts low|medium|high|max|none and rejects xhigh, so the
// explicit supportsXHighEffort:false makes the sanitizer map xhigh → max.
@@ -34,12 +50,14 @@ export const ollama_cloudProvider: RegistryEntry = {
name: "GLM 5.1",
supportsReasoning: true,
supportsXHighEffort: false,
+ supportedThinkingEfforts: ["low", "medium", "high", "max"],
},
{
id: "glm-5.2",
name: "GLM 5.2",
supportsReasoning: true,
supportsXHighEffort: false,
+ supportedThinkingEfforts: ["low", "medium", "high", "max"],
},
// #3110: MiniMax M3 via Ollama
{ id: "minimax-m3", name: "MiniMax M3", contextLength: 1048576, supportsVision: true },
diff --git a/open-sse/config/providers/registry/openai/index.ts b/open-sse/config/providers/registry/openai/index.ts
index 63b6a30fa3..60a276a948 100644
--- a/open-sse/config/providers/registry/openai/index.ts
+++ b/open-sse/config/providers/registry/openai/index.ts
@@ -7,6 +7,7 @@ export const openaiProvider: RegistryEntry = {
format: "openai",
executor: "default",
baseUrl: "https://api.openai.com/v1/chat/completions",
+ reasoningTransport: "opaque",
authType: "apikey",
authHeader: "bearer",
defaultContextLength: 128000,
diff --git a/open-sse/config/providers/registry/opencode/go/index.ts b/open-sse/config/providers/registry/opencode/go/index.ts
index 54e30e5738..bf0c94ed90 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 4aaa6ea046..07f228b77e 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/tabitoken/index.ts b/open-sse/config/providers/registry/tabitoken/index.ts
new file mode 100644
index 0000000000..f95d188c20
--- /dev/null
+++ b/open-sse/config/providers/registry/tabitoken/index.ts
@@ -0,0 +1,59 @@
+import type { RegistryEntry } from "../../shared.ts";
+import { getAnthropicCompatHeaders } from "../../shared.ts";
+
+/**
+ * TabiToken — NewAPI-based Claude gateway (https://tabitoken.com).
+ *
+ * The catalog below is not hand-written: TabiToken leaves the NewAPI pricing endpoint
+ * public (`/api/status` reports `pricing.requireAuth: false`), so `GET /api/pricing`
+ * lists every model together with the protocols it accepts. All four entries report
+ * `supported_endpoint_types: ["anthropic","openai"]`, which is why only those two
+ * protocols are declared here — the host also routes `/v1/responses` and the Gemini
+ * `/v1beta` path, but no model on this gateway is reachable through them.
+ *
+ * Claude-first (`/v1/messages` + `x-api-key`) because the whole catalog is Claude and
+ * that avoids a translation hop for Claude-native clients; `passthroughModels` keeps
+ * models added upstream usable before this list catches up.
+ *
+ * No static fingerprint headers. TabiToken fronts Cloudflare, and the only User-Agent
+ * it rejects is the literal `curl/*` default — a browser UA is answered with
+ * "Access denied: abusive or non-compliant use is prohibited", while sending no UA
+ * (the fetch default) reaches the token layer normally. So, unlike agentrouter, this
+ * entry needs neither a static nor a dynamic wire image.
+ *
+ * `headers` carries only `Anthropic-Version`, and it has to live on the entry rather
+ * than come from the executor: `default.ts` defaults that header solely for provider
+ * ids prefixed `anthropic-compatible-` (buildHeaders, the `startsWith` branch), so a
+ * plain `format: "claude"` entry would POST `/v1/messages` without it. Six sibling
+ * third-party Claude entries (wafer, zai, xiaomi-mimo, xiaomi-mimo-token-plan,
+ * bailian-coding-plan, deepseek) set it for exactly this reason. Entry-level headers
+ * are merged for every format (base.ts::buildHeadersPreamble), so the OpenAI alternate
+ * below also sends it — a documented no-op on `/chat/completions` (see the same note in
+ * executors/github.ts).
+ */
+export const tabitokenProvider: RegistryEntry = {
+ id: "tabitoken",
+ alias: "tabitoken",
+ format: "claude",
+ executor: "default",
+ baseUrl: "https://tabitoken.com/v1/messages",
+ modelsUrl: "https://tabitoken.com/v1/models",
+ authType: "apikey",
+ authHeader: "x-api-key",
+ headers: getAnthropicCompatHeaders(),
+ alternateFormats: [
+ {
+ format: "openai",
+ baseUrl: "https://tabitoken.com/v1/chat/completions",
+ authHeader: "bearer",
+ label: "OpenAI-compatible",
+ },
+ ],
+ models: [
+ { id: "claude-opus-5", name: "Claude Opus 5" },
+ { id: "claude-opus-5-thinking", name: "Claude Opus 5 (Thinking)" },
+ { id: "claude-opus-4-8", name: "Claude Opus 4.8" },
+ { id: "claude-opus-4-8-thinking", name: "Claude Opus 4.8 (Thinking)" },
+ ],
+ passthroughModels: true,
+};
diff --git a/open-sse/config/providers/registry/xai/index.ts b/open-sse/config/providers/registry/xai/index.ts
index efd0a72e3a..dcf9124441 100644
--- a/open-sse/config/providers/registry/xai/index.ts
+++ b/open-sse/config/providers/registry/xai/index.ts
@@ -12,6 +12,7 @@ export const xaiProvider: RegistryEntry = {
// XaiExecutor.buildUrl (open-sse/executors/xai.ts) for models tagged
// targetFormat: "openai-responses" below.
responsesBaseUrl: "https://api.x.ai/v1/responses",
+ reasoningTransport: "opaque",
authType: "apikey",
authHeader: "bearer",
models: [
@@ -54,6 +55,7 @@ export const xai_oauthProvider: RegistryEntry = {
executor: "xai-oauth",
baseUrl: xaiProvider.baseUrl,
responsesBaseUrl: xaiProvider.responsesBaseUrl,
+ reasoningTransport: "opaque",
authType: "oauth",
authHeader: xaiProvider.authHeader,
passthroughModels: true,
diff --git a/open-sse/config/providers/registry/zai/index.ts b/open-sse/config/providers/registry/zai/index.ts
index e126aee21f..1141ea8dc3 100644
--- a/open-sse/config/providers/registry/zai/index.ts
+++ b/open-sse/config/providers/registry/zai/index.ts
@@ -11,13 +11,15 @@ export const zaiProvider: RegistryEntry = {
authType: "apikey",
authHeader: "x-api-key",
headers: getAnthropicCompatHeaders(),
- // Real upstream model IDs only. The effort tiers (glm-5.2-high / glm-5.2-max)
- // are intentionally NOT listed here: they are OmniRoute aliases resolved by the
- // GlmExecutor (parseGlm52Effort → base "glm-5.2" + effort field). This provider
- // uses the DefaultExecutor, which sends the model ID verbatim, so the aliases
- // would reach z.ai's Anthropic endpoint as unknown IDs. Use the `glm` provider
- // for effort tiers. Vision models are likewise omitted (handled elsewhere).
+ // Real upstream model IDs only. The effort tiers (glm-5.2-high/-max,
+ // glm-5.3-high/-low) are intentionally NOT listed here: they are OmniRoute
+ // aliases resolved by the GlmExecutor (parseGlmEffortTier → base model +
+ // effort selector). This provider uses the DefaultExecutor, which sends the
+ // model ID verbatim, so the aliases would reach z.ai's Anthropic endpoint as
+ // unknown IDs. Use the `glm` provider for effort tiers. Vision models are
+ // likewise omitted (handled elsewhere).
models: [
+ { id: "glm-5.3", name: "GLM 5.3" },
{ id: "glm-5.2", name: "GLM 5.2" },
{ id: "glm-5.1", name: "GLM 5.1" },
{ id: "glm-5", name: "GLM 5" },
diff --git a/open-sse/config/providers/shared.ts b/open-sse/config/providers/shared.ts
index 94e39b29a0..d87250644d 100644
--- a/open-sse/config/providers/shared.ts
+++ b/open-sse/config/providers/shared.ts
@@ -108,6 +108,8 @@ export interface RegistryOAuth {
pollUrlBase?: string;
}
+export type ReasoningTransport = "plaintext" | "opaque" | "none";
+
export interface RegistryEntry {
id: string;
alias?: string;
@@ -120,6 +122,8 @@ export interface RegistryEntry {
/** Override models URL used only for API key validation, not catalog discovery. */
testKeyModelsUrl?: string;
responsesBaseUrl?: string;
+ /** Provider-bound replay format; omitted providers accept portable plaintext reasoning. */
+ reasoningTransport?: ReasoningTransport;
/** Anthropic-native /v1/messages endpoint (e.g. GitHub Copilot's shim) used
* for models tagged `targetFormat: "claude"` on an otherwise openai-format
* provider — see registry/github/index.ts. */
@@ -758,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 ce20777cb0..9230fa1b0e 100644
--- a/open-sse/config/searchRegistry.ts
+++ b/open-sse/config/searchRegistry.ts
@@ -303,6 +303,19 @@ export const SEARCH_CREDENTIAL_FALLBACKS: Record = {
export const SEARCH_PROVIDER_ALIASES: Record = {
"jina-ai": "jina-search",
jina: "jina-search",
+ brave: "brave-search",
+ serper: "serper-search",
+ perplexity: "perplexity-search",
+ exa: "exa-search",
+ tavily: "tavily-search",
+ "google-pse": "google-pse-search",
+ linkup: "linkup-search",
+ ollama: "ollama-search",
+ searchapi: "searchapi-search",
+ youcom: "youcom-search",
+ searxng: "searxng-search",
+ zai: "zai-search",
+ duckduckgo: "duckduckgo-free",
};
export function resolveSearchProviderId(providerId: string): string {
diff --git a/open-sse/executors/base/reasoningEffort.ts b/open-sse/executors/base/reasoningEffort.ts
index 9b8ffb1da0..fa416dbc1a 100644
--- a/open-sse/executors/base/reasoningEffort.ts
+++ b/open-sse/executors/base/reasoningEffort.ts
@@ -275,6 +275,21 @@ export function sanitizeReasoningEffortForProvider(
return stripEffortValue(b, c);
}
+ // `minimal` is a sub-`low` reasoning tier some catalogs advertise (e.g.
+ // Muse Spark via models.dev) and the Codex provider accepts natively — but
+ // Command Code rejects it outright:
+ // Validation error: Invalid option: expected one of
+ // "low"|"medium"|"high"|"xhigh"|"max" at "params.reasoning_effort"
+ // Map it to the closest supported value (`low`) for command-code only;
+ // other providers (codex etc.) keep their native `minimal` handling.
+ if (provider === "command-code" && effortStr === "minimal") {
+ log?.info?.(
+ "REASONING_SANITIZE",
+ `${provider}/${modelStr}: mapped reasoning_effort minimal → low`
+ );
+ return writeEffortValue(b, "low", c);
+ }
+
// Command Code accepts the literal top-tier value `max`, while the shared
// standardization stage may have already represented the client's `max` as
// OmniRoute's internal `xhigh`. Convert it back before the upstream request.
diff --git a/open-sse/executors/codex.ts b/open-sse/executors/codex.ts
index b7bc008ec3..ab595c3cbb 100644
--- a/open-sse/executors/codex.ts
+++ b/open-sse/executors/codex.ts
@@ -34,7 +34,7 @@ import {
} from "../config/codexIdentity.ts";
import { getAccessToken } from "../services/tokenRefresh.ts";
import { sanitizeResponsesInputItems } from "../services/responsesInputSanitizer.ts";
-import { applyResponsesInputPolicy } from "../services/responsesInputPolicy.ts";
+import { applyReasoningInputPolicy } from "../services/reasoningInputPolicy.ts";
import { normalizeCodexVerbosity } from "../services/codexVerbosity.ts";
import { getThinkingBudgetConfig, ThinkingMode } from "../services/thinkingBudget.ts";
import { CORS_HEADERS } from "../utils/cors.ts";
@@ -1389,10 +1389,12 @@ export class CodexExecutor extends BaseExecutor {
delete body.session_id;
delete body.conversation_id;
- applyResponsesInputPolicy(
- body,
- credentials?.providerSpecificData?.preserveEncryptedReasoning === true
- );
+ applyReasoningInputPolicy(body, "responses", {
+ provider: "codex",
+ preserveEncryptedReasoning:
+ credentials?.providerSpecificData?.preserveEncryptedReasoning === true,
+ onIncompatibleReasoning: "drop",
+ });
if (nativeCodexPassthrough) {
return body;
diff --git a/open-sse/executors/commandCode.ts b/open-sse/executors/commandCode.ts
index 6f7fe08940..b736e6cdf1 100644
--- a/open-sse/executors/commandCode.ts
+++ b/open-sse/executors/commandCode.ts
@@ -2,7 +2,12 @@ import { randomUUID } from "node:crypto";
import { isVisionModelId } from "@/shared/constants/visionModels";
import { REGISTRY } from "../config/providerRegistry.ts";
-import { BaseExecutor, mergeUpstreamExtraHeaders, type ExecuteInput } from "./base.ts";
+import {
+ BaseExecutor,
+ mergeUpstreamExtraHeaders,
+ sanitizeReasoningEffortForProvider,
+ type ExecuteInput,
+} from "./base.ts";
type JsonRecord = Record;
@@ -387,6 +392,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 +435,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 : "";
@@ -953,7 +992,17 @@ export class CommandCodeExecutor extends BaseExecutor {
};
mergeUpstreamExtraHeaders(headers, upstreamExtraHeaders);
- const { body: transformedBody, toolNameMap } = buildCommandCodeBody(model, body, stream);
+ // The combo/single-model dispatch boundary does not always run
+ // sanitizeRequestForResolvedTarget before reaching this executor (combo
+ // path), and Command Code rejects unsupported reasoning_effort values
+ // outright (e.g. "minimal" → 400 "expected one of low|medium|high|xhigh|max").
+ // Sanitize here — the executor is the last line of defense for the wire body.
+ const sanitizedBody = sanitizeReasoningEffortForProvider(body, this.provider, model);
+ const { body: transformedBody, toolNameMap } = buildCommandCodeBody(
+ model,
+ sanitizedBody,
+ stream
+ );
const url = this.buildUrl();
const upstream = await fetch(url, {
method: "POST",
diff --git a/open-sse/executors/copilot-m365-connection.ts b/open-sse/executors/copilot-m365-connection.ts
index d5f6d80c6c..c8251af5f4 100644
--- a/open-sse/executors/copilot-m365-connection.ts
+++ b/open-sse/executors/copilot-m365-connection.ts
@@ -8,6 +8,7 @@
* the URL MUST go through redactWsUrl().
*/
+import { resolvePublicCred } from "../utils/publicCreds.ts";
import { randomUUID, randomBytes } from "node:crypto";
import type { ProviderCredentials } from "./base.ts";
@@ -272,7 +273,7 @@ export function redactWsUrl(wsUrl: string): string {
// of requiring a fresh DevTools capture after every expiry.
/** Public client id observed in both the browser token and M365-Copilot2API. */
-export const M365_OAUTH_CLIENT_ID = "c0ab8ce9-e9a0-42e7-b064-33d422df41f1";
+export const M365_OAUTH_CLIENT_ID = resolvePublicCred("m365_oauth_client_id");
export const M365_OAUTH_SCOPE =
"openid profile offline_access https://substrate.office.com/sydney/M365Chat.Read " +
diff --git a/open-sse/executors/copilot-m365-web.ts b/open-sse/executors/copilot-m365-web.ts
index 57f854ef86..5bdaf9ae9b 100644
--- a/open-sse/executors/copilot-m365-web.ts
+++ b/open-sse/executors/copilot-m365-web.ts
@@ -320,15 +320,16 @@ export class CopilotM365WebExecutor extends BaseExecutor {
const rotated = result.refreshToken || refreshToken;
const chathubPath = currentM365ChathubPath(credentials);
+ const assembledApiKey = chathubPath
+ ? ["access_token=", result.accessToken, "; chathubPath=", chathubPath].join("")
+ : "";
const next = {
...credentials,
accessToken: result.accessToken,
refreshToken: rotated,
// Keep the pasted-format apiKey self-consistent so every resolution path
// (fresh column, stale column, dashboard re-read) sees the same token.
- ...(chathubPath
- ? { apiKey: `access_token=${result.accessToken}; chathubPath=${chathubPath}` }
- : {}),
+ ...(assembledApiKey ? { apiKey: assembledApiKey } : {}),
...(result.expiresIn
? { expiresAt: new Date(Date.now() + result.expiresIn * 1000).toISOString() }
: {}),
diff --git a/open-sse/executors/cursor.ts b/open-sse/executors/cursor.ts
index bc9ade9d27..ebcfe053bd 100644
--- a/open-sse/executors/cursor.ts
+++ b/open-sse/executors/cursor.ts
@@ -82,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 {
@@ -193,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;
@@ -718,7 +715,7 @@ export class CursorExecutor extends BaseExecutor {
}
buildUrl() {
- return CURSOR_AGENT_URL;
+ return PROVIDERS.cursor.baseUrl;
}
/**
@@ -1211,10 +1208,40 @@ export class CursorExecutor extends BaseExecutor {
}
async execute({ model, body, stream, credentials, signal, log, upstreamExtraHeaders }) {
- const url = this.buildUrl();
+ const fallbackUrl = this.buildUrl();
const executionCredentials = await this.resolveExecutionCredentials(credentials);
if (executionCredentials instanceof Response) {
- return { response: executionCredentials, url, headers: {}, transformedBody: body };
+ 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);
diff --git a/open-sse/executors/cursor/agentEndpoint.ts b/open-sse/executors/cursor/agentEndpoint.ts
new file mode 100644
index 0000000000..e1168df5b7
--- /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 056772040a..0d2c9182bd 100644
--- a/open-sse/executors/default.ts
+++ b/open-sse/executors/default.ts
@@ -191,6 +191,9 @@ export class DefaultExecutor extends BaseExecutor {
// Operator's manual override (#6147) keeps its own semantics and falls
// through to the provider-specific handling below.
const normalized = alternate.baseUrl.replace(/\/$/, "");
+ // A model-scoped alternate (the Gemini protocol: `{base}/{model}:generateContent`)
+ // builds its own URL — chatPath/urlSuffix are constants and cannot carry the model.
+ if (alternate.urlBuilder) return alternate.urlBuilder(normalized, model, stream);
return `${normalized}${alternate.chatPath || ""}${alternate.urlSuffix || ""}`;
}
}
diff --git a/open-sse/executors/freebuff.ts b/open-sse/executors/freebuff.ts
new file mode 100644
index 0000000000..bc2de30f63
--- /dev/null
+++ b/open-sse/executors/freebuff.ts
@@ -0,0 +1,172 @@
+import {
+ BaseExecutor,
+ type ExecuteInput,
+} from "./base.ts";
+import { PROVIDERS } from "../config/constants.ts";
+
+const MODEL_TO_AGENT: Record = {
+ "deepseek/deepseek-v4-flash": "base2-free-deepseek-flash",
+ "deepseek/deepseek-v4-pro": "base2-free-deepseek",
+ "openai/gpt-5.6-luna": "base2-free-luna",
+ "minimax/minimax-m3": "base2-free-minimax-m3",
+ "mimo/mimo-v2.5": "base2-free-mimo",
+ "z-ai/glm-5.2": "base2-free-glm",
+ "crof/kimi-k3-eco": "base2-free-kimi-k3-eco",
+ "anthropic/claude-fable-5": "base2-free-fable",
+ "meta/muse-spark-1.2-contributor": "base2-free-muse-spark",
+};
+
+function generateClientSessionId(): string {
+ const alphabet = "0123456789abcdefghijklmnopqrstuvwxyz";
+ let out = "";
+ for (let i = 0; i < 13; i++) {
+ out += alphabet[Math.floor(Math.random() * alphabet.length)];
+ }
+ return out;
+}
+
+export class FreebuffExecutor extends BaseExecutor {
+ constructor() {
+ super("freebuff", (PROVIDERS as Record).freebuff as string || "freebuff");
+ }
+
+ override async execute(input: ExecuteInput) {
+ const { model, body, stream, credentials, signal } = input;
+ const token = credentials?.apiKey || credentials?.accessToken || "";
+
+ if (!token) {
+ return {
+ response: new Response(
+ JSON.stringify({ error: { message: "Freebuff Auth Token required", type: "authentication_error" } }),
+ { status: 401, headers: { "Content-Type": "application/json" } }
+ ),
+ };
+ }
+
+ const requestedModel = typeof model === "string" ? model.replace(/^freebuff\//, "") : (model || "deepseek/deepseek-v4-flash");
+ const agentId = MODEL_TO_AGENT[requestedModel] || "base2-free";
+
+ const authHeaders = {
+ Authorization: `Bearer ${token}`,
+ "Content-Type": "application/json",
+ "User-Agent": "codebuff/0.1.0 (darwin-arm64)",
+ };
+
+ let instanceId = "";
+ let runId = "";
+
+ // 1. Session acquisition
+ try {
+ const sessionRes = await fetch("https://www.codebuff.com/api/v1/freebuff/session", {
+ method: "POST",
+ headers: {
+ ...authHeaders,
+ "x-freebuff-model": requestedModel,
+ },
+ body: JSON.stringify({}),
+ signal,
+ });
+ if (sessionRes.ok) {
+ const data = (await sessionRes.json()) as { instanceId?: string };
+ instanceId = data.instanceId || "";
+ } else {
+ const errText = await sessionRes.text();
+ return {
+ response: new Response(
+ JSON.stringify({ error: { message: `Freebuff session failed (${sessionRes.status}): ${errText}`, type: "upstream_error" } }),
+ { status: sessionRes.status, headers: { "Content-Type": "application/json" } }
+ ),
+ };
+ }
+ } catch (e: unknown) {
+ const msg = e instanceof Error ? e.message : String(e);
+ return {
+ response: new Response(
+ JSON.stringify({ error: { message: `Freebuff session network error: ${msg}`, type: "upstream_error" } }),
+ { status: 502, headers: { "Content-Type": "application/json" } }
+ ),
+ };
+ }
+
+ // 2. Start agent run
+ try {
+ const runRes = await fetch("https://www.codebuff.com/api/v1/agent-runs", {
+ method: "POST",
+ headers: authHeaders,
+ body: JSON.stringify({ action: "START", agentId }),
+ signal,
+ });
+ if (runRes.ok) {
+ const runData = (await runRes.json()) as { runId?: string };
+ runId = runData.runId || "";
+ }
+ } catch {}
+
+ // 3. Prepare Chat Payload & Buffy System Prompt
+ const incomingMessages = Array.isArray(body?.messages) ? [...body.messages] : [];
+ const hasBuffyPrompt =
+ incomingMessages.length > 0 &&
+ incomingMessages[0].role === "system" &&
+ typeof incomingMessages[0].content === "string" &&
+ incomingMessages[0].content.trim().startsWith("You are Buffy");
+
+ if (!hasBuffyPrompt) {
+ incomingMessages.unshift({
+ role: "system",
+ content: "You are Buffy, the strategic coding assistant.",
+ });
+ }
+
+ const clientSessionId = generateClientSessionId();
+ const upstreamBody = {
+ ...(body || {}),
+ model: requestedModel,
+ messages: incomingMessages,
+ stream: stream !== false,
+ codebuff_metadata: {
+ run_id: runId,
+ cost_mode: "free",
+ client_id: clientSessionId,
+ freebuff_instance_id: instanceId,
+ ...((body as Record)?.codebuff_metadata as Record || {}),
+ },
+ };
+
+ const completionHeaders = {
+ Authorization: `Bearer ${token}`,
+ "Content-Type": "application/json",
+ "User-Agent": "ai-sdk/openai-compatible/1.0.25/codebuff",
+ Accept: "application/json, text/event-stream",
+ "x-freebuff-instance-id": instanceId,
+ ...(runId ? { "x-codebuff-run-id": runId } : {}),
+ "x-codebuff-agent-id": agentId,
+ };
+
+ // 4. Chat Completion
+ const completionUrl = "https://www.codebuff.com/api/v1/chat/completions";
+ const response = await fetch(completionUrl, {
+ method: "POST",
+ headers: completionHeaders,
+ body: JSON.stringify(upstreamBody),
+ signal,
+ });
+
+ // 5. Finish agent run (background)
+ if (runId) {
+ void fetch("https://www.codebuff.com/api/v1/agent-runs", {
+ method: "POST",
+ headers: authHeaders,
+ body: JSON.stringify({
+ action: "FINISH",
+ runId,
+ status: "completed",
+ totalSteps: 1,
+ directCredits: 0,
+ totalCredits: 0,
+ }),
+ }).catch(() => {});
+ }
+
+ return { response };
+ }
+}
diff --git a/open-sse/executors/glm.ts b/open-sse/executors/glm.ts
index 945dbe82cb..6318aaab2e 100644
--- a/open-sse/executors/glm.ts
+++ b/open-sse/executors/glm.ts
@@ -1,4 +1,5 @@
import { randomUUID } from "node:crypto";
+import type { KeyHealth } from "../services/apiKeyRotator.ts";
import { DefaultExecutor } from "./default.ts";
import {
@@ -52,17 +53,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 +269,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 +305,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 +340,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 +399,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 +475,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 +509,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/index.ts b/open-sse/executors/index.ts
index d82452bb7e..34f3bafe73 100644
--- a/open-sse/executors/index.ts
+++ b/open-sse/executors/index.ts
@@ -1,5 +1,6 @@
import { SEARCH_PROVIDERS } from "../config/searchRegistry.ts";
import { registerExecutor, getRegisteredExecutor, hasRegisteredExecutor } from "./registry.ts";
+import type { BaseExecutor } from "./base.ts";
import { AntigravityExecutor } from "./antigravity.ts";
import { GithubExecutor } from "./github.ts";
import { GheCopilotExecutor } from "./ghe-copilot.ts";
@@ -13,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";
@@ -116,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
@@ -233,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/kimi-web.ts b/open-sse/executors/kimi-web.ts
index aaf2b32c69..8f9c13c332 100644
--- a/open-sse/executors/kimi-web.ts
+++ b/open-sse/executors/kimi-web.ts
@@ -29,6 +29,7 @@ import {
sanitizeErrorMessage,
} from "../utils/error.ts";
import { extractKimiAccessToken } from "@/lib/providers/webCookieAuth";
+import { exchangeKimiRefreshToken } from "@/lib/kimi/tokenRefresh";
import {
type KimiWebModelConfig,
resolveKimiWebContextLength,
@@ -38,7 +39,23 @@ import {
export { extractKimiAccessToken };
-const BASE_URL = "https://www.kimi.com";
+export function getKimiWebBaseUrl(): string {
+ const envUrl = process.env.KIMI_WEB_BASE_URL?.trim();
+ if (envUrl) {
+ return envUrl.replace(/\/+$/, "");
+ }
+ return "https://www.kimi.ai";
+}
+
+export function getKimiWebChatUrl(): string {
+ const envChat = process.env.KIMI_WEB_CHAT_URL?.trim();
+ if (envChat) {
+ return envChat;
+ }
+ return `${getKimiWebBaseUrl()}/apiv2/kimi.gateway.chat.v1.ChatService/Chat`;
+}
+
+const BASE_URL = "https://www.kimi.ai";
const CHAT_URL = `${BASE_URL}/apiv2/kimi.gateway.chat.v1.ChatService/Chat`;
const USER_AGENT =
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36";
@@ -305,7 +322,7 @@ export class KimiWebExecutor extends BaseExecutor {
const bodyObj = (body || {}) as Record;
const rawCredential = String(credentials?.accessToken || credentials?.apiKey || "").trim();
- const accessToken = extractKimiAccessToken(rawCredential);
+ let accessToken = extractKimiAccessToken(rawCredential);
if (!accessToken) {
return makeErrorResult(
400,
@@ -389,6 +406,29 @@ export class KimiWebExecutor extends BaseExecutor {
);
}
+ if (upstream.status === 401) {
+ const refreshToken =
+ credentials?.refreshToken || credentials?.providerSpecificData?.refreshToken;
+ if (refreshToken && typeof refreshToken === "string") {
+ const refreshRes = await exchangeKimiRefreshToken(
+ refreshToken,
+ getKimiWebBaseUrl()
+ );
+ if (refreshRes.success && refreshRes.accessToken) {
+ accessToken = refreshRes.accessToken;
+ const retryHeaders = this.buildKimiHeaders(accessToken);
+ try {
+ upstream = await fetch(CHAT_URL, {
+ method: "POST",
+ headers: retryHeaders,
+ body: new Uint8Array(framedBody),
+ signal,
+ });
+ } catch {}
+ }
+ }
+ }
+
if (!upstream.ok) {
const errText = await upstream.text().catch(() => "");
return makeErrorResult(
diff --git a/open-sse/executors/opencode.ts b/open-sse/executors/opencode.ts
index 6f7a08fbaf..51419ead9c 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 f98680c4ff..12e98ccdc4 100644
--- a/open-sse/executors/perplexity-web/protocol.ts
+++ b/open-sse/executors/perplexity-web/protocol.ts
@@ -370,15 +370,29 @@ export function buildPplxRequestBody(
};
}
+const SEARCH_HINT = "You have built-in web search. Answer questions directly using search results.";
+
+/**
+ * Whether to append {@link SEARCH_HINT} to the caller's system message.
+ *
+ * It used to be unconditional. Perplexity's answer engine is search-first anyway, and
+ * for coding clients the sentence leaks into replies as meta-commentary ("I need to
+ * search before responding per my instructions"), so it is now opt-in via
+ * `OMNIROUTE_PPLX_SEARCH_HINT`. Read per call rather than at module load so the flag
+ * can be flipped without restarting the server (and so tests can toggle it).
+ */
+function searchHintEnabled(): boolean {
+ return /^(1|true|yes|on)$/i.test(process.env.OMNIROUTE_PPLX_SEARCH_HINT ?? "");
+}
+
export function buildQuery(parsed: ParsedMessages, followUpUuid: string | null): string {
if (followUpUuid) return parsed.currentMsg;
const obj: Record = {};
if (parsed.systemMsg.trim()) {
- obj.instructions = [
- parsed.systemMsg.trim(),
- "You have built-in web search. Answer questions directly using search results.",
- ];
+ obj.instructions = searchHintEnabled()
+ ? [parsed.systemMsg.trim(), SEARCH_HINT]
+ : [parsed.systemMsg.trim()];
}
if (parsed.history.length > 0) {
obj.history = parsed.history;
diff --git a/open-sse/handlers/chatCore.ts b/open-sse/handlers/chatCore.ts
index e955ffd5d3..9f530d4605 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,
@@ -170,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,
@@ -312,6 +347,7 @@ import {
computeBillableTokens,
normalizeExecutorResult,
executeWithUpstreamStartTimeout,
+ resolveConnectionTimeoutMs,
} from "./chatCore/upstreamTimeouts.ts";
import { getModelNormalizeToolCallId, getModelPreserveOpenAIDeveloperRole } from "@/lib/db/models";
import { getProviderCredentials, extractSessionAffinityKey } from "@/sse/services/auth";
@@ -400,6 +436,7 @@ import { isLocalStreamLifecycleError } from "@/shared/utils/circuitBreaker";
import { shouldIsolateProbeFailures } from "@/shared/utils/probeOrigin";
import { extractFacts } from "@/lib/memory/extraction";
import { handleToolCallExecution } from "@/lib/skills/interception";
+import { MEMORY_BUILTIN_TOOL_NAMES } from "@/lib/skills/memoryBuiltins";
import { OMNIROUTE_RESPONSE_HEADERS } from "@/shared/constants/headers";
import { getClaudeCodeCompatibleRequestDefaults } from "@/lib/providers/requestDefaults";
import {
@@ -475,6 +512,7 @@ export async function handleChatCore({
conversationId = null,
modelPinned = false,
skipResourcePressureGuard = false,
+ reasoningTransportFallback = "skip",
managedLease = null,
}) {
let { provider, model, extendedContext } = modelInfo;
@@ -1157,11 +1195,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);
@@ -2008,13 +2065,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;
@@ -3004,6 +3066,7 @@ export async function handleChatCore({
executor,
provider,
model: modelToCall,
+ connectionTimeoutMs: resolveConnectionTimeoutMs(execCreds?.providerSpecificData),
signal: streamController.signal,
log,
execute: (signal) =>
@@ -3307,6 +3370,7 @@ export async function handleChatCore({
executor,
provider,
model: modelToCall,
+ connectionTimeoutMs: resolveConnectionTimeoutMs(execCreds?.providerSpecificData),
signal: streamController.signal,
log,
execute: (signal) =>
@@ -4920,9 +4984,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;
@@ -5054,6 +5120,27 @@ export async function handleChatCore({
});
persistFailureUsage(HTTP_STATUS.BAD_GATEWAY, "malformed_translated_response");
trackPendingRequest(model, provider, pendingConnId, false);
+ // Routing event (feedback foundation) — record the malformed outcome so
+ // the quality tracker de-prioritizes this model over time.
+ void emitRoutingEvent(
+ createRoutingEvent({
+ requestId: traceId || pendingRequestId || "unknown",
+ provider: provider || "unknown",
+ model: model || "unknown",
+ strategy: isCombo ? (comboStrategy ?? "combo") : "direct",
+ latencyMs: Date.now() - startTime,
+ ttftMs: null,
+ inputTokens: null,
+ outputTokens: null,
+ cost: null,
+ retries: 0,
+ fallbackUsed: false, // combo-level fallback tracked by decisionTrace
+ outcome: "malformed",
+ status: HTTP_STATUS.BAD_GATEWAY,
+ finishReason: routingFinishReason(translatedResponse),
+ connectionId: credentials?.connectionId ?? null,
+ })
+ );
return createErrorResult(
HTTP_STATUS.BAD_GATEWAY,
malformedMessage,
@@ -5154,6 +5241,43 @@ export async function handleChatCore({
response: { status: 200, data: translatedResponse },
});
+ // Routing event (feedback foundation) — fire-and-forget, cheap.
+ void emitRoutingEvent(
+ createRoutingEvent({
+ requestId: traceId || pendingRequestId || "unknown",
+ provider: provider || "unknown",
+ model: model || "unknown",
+ strategy: isCombo ? (comboStrategy ?? "combo") : "direct",
+ latencyMs: Date.now() - startTime,
+ ttftMs: null,
+ inputTokens:
+ usage && typeof usage === "object"
+ ? (() => {
+ const promptTokens = (usage as Record).prompt_tokens;
+ return typeof promptTokens === "number" && Number.isFinite(promptTokens)
+ ? promptTokens
+ : null;
+ })()
+ : null,
+ outputTokens:
+ usage && typeof usage === "object"
+ ? (() => {
+ const completionTokens = (usage as Record).completion_tokens;
+ return typeof completionTokens === "number" && Number.isFinite(completionTokens)
+ ? completionTokens
+ : null;
+ })()
+ : null,
+ cost: Number.isFinite(estimatedCost) ? estimatedCost : null,
+ retries: 0,
+ fallbackUsed: false, // combo-level fallback tracked by decisionTrace
+ outcome: "success",
+ status: 200,
+ finishReason: routingFinishReason(translatedResponse),
+ connectionId: credentials?.connectionId ?? null,
+ })
+ );
+
return {
success: true,
response: buildNonStreamingJsonResponse(translatedResponse, responseHeaders),
@@ -5274,6 +5398,8 @@ export async function handleChatCore({
error: streamError,
errorCode: streamErrorCode,
ttft,
+ itlMs: streamItlMs,
+ interrupted: streamInterrupted,
}) => {
const normalizedStreamStatus = streamStatus || 200;
if (streamCompletionRecorded) return;
@@ -5377,6 +5503,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/clientUsageBuffer.ts b/open-sse/handlers/chatCore/clientUsageBuffer.ts
index 4c7b1e48dc..b34abba1f6 100644
--- a/open-sse/handlers/chatCore/clientUsageBuffer.ts
+++ b/open-sse/handlers/chatCore/clientUsageBuffer.ts
@@ -23,6 +23,7 @@ import {
filterUsageForFormat as defaultFilterUsage,
estimateUsage as defaultEstimateUsage,
sanitizeProviderUsageForRequest,
+ type UsageLike,
} from "../../utils/usageTracking.ts";
type ResponseLike =
@@ -106,15 +107,18 @@ export function applyClientUsageBuffer(
const { preserveContextBudgetInVisibleUsage = false } = options;
if (translatedResponse?.usage) {
translatedResponse.usage = sanitizeProviderUsageForRequest(
- translatedResponse.usage,
+ translatedResponse.usage as UsageLike,
body,
clientResponseFormat
);
}
// Add buffer and filter usage for client (to prevent CLI context errors)
- if (translatedResponse?.usage && !isEmptyUsage(translatedResponse.usage)) {
- const buffered = deps.addBufferToUsage(translatedResponse.usage) as Record;
+ if (translatedResponse?.usage && !isEmptyUsage(translatedResponse.usage as UsageLike)) {
+ const buffered = deps.addBufferToUsage(translatedResponse.usage as UsageLike) as Record<
+ string,
+ unknown
+ >;
if (preserveContextBudgetInVisibleUsage) {
foldContextBudgetIntoVisibleUsage(buffered);
}
diff --git a/open-sse/handlers/chatCore/memorySkillsInjection.ts b/open-sse/handlers/chatCore/memorySkillsInjection.ts
index 2a43c17cbf..14fbdc9575 100644
--- a/open-sse/handlers/chatCore/memorySkillsInjection.ts
+++ b/open-sse/handlers/chatCore/memorySkillsInjection.ts
@@ -2,6 +2,7 @@ import { retrieveMemories } from "@/lib/memory/retrieval";
import { getMemorySettings, DEFAULT_MEMORY_SETTINGS, toMemoryRetrievalConfig } from "@/lib/memory/settings";
import { injectMemory, shouldInjectMemory } from "@/lib/memory/injection";
import { injectSkills } from "@/lib/skills/injection";
+import { buildMemoryToolsForProvider } from "@/lib/skills/memoryBuiltins";
import { skillRegistry } from "@/lib/skills/registry";
import { FORMATS } from "../../translator/formats.ts";
import { detectCachingContext } from "../../services/compression/cachingAware.ts";
@@ -138,6 +139,43 @@ export async function injectMemoryAndSkills({
}
}
+ if (memoryOwnerId && memorySettings?.enabled && body.stream !== true) {
+ // Server-side builtin memory tools (memory_save/update/search/delete) are
+ // executed by the gateway's tool-call interception, which runs only on the
+ // non-stream path. Stream clients (opencode etc.) execute tools client-side,
+ // so for them these tools would be announced but never executed; they should
+ // use the MCP memory tools (omniroute_memory_*) instead.
+ const existingTools = Array.isArray(body.tools) ? body.tools : [];
+ const existingToolNames = new Set(
+ existingTools.flatMap((tool) => {
+ const record = tool as Record | null;
+ if (!record || typeof record !== "object") return [];
+ const fn = record.function as Record | undefined;
+ if (typeof fn?.name === "string") return [fn.name];
+ if (typeof record.name === "string") return [record.name];
+ return [];
+ })
+ );
+ const memoryTools = buildMemoryToolsForProvider(
+ getSkillsProviderForFormat(sourceFormat)
+ ).filter((tool) => {
+ const record = tool as Record;
+ const name =
+ (record.function as Record | undefined)?.name ?? record.name;
+ return typeof name === "string" && !existingToolNames.has(name);
+ });
+ if (memoryTools.length > 0) {
+ body = {
+ ...body,
+ tools: [...existingTools, ...memoryTools],
+ };
+ log?.debug?.(
+ "MEMORY",
+ `Injected ${memoryTools.length} memory tool(s) for key=${memoryOwnerId}`
+ );
+ }
+ }
+
if (memoryOwnerId && memorySettings?.skillsEnabled) {
// Ensure the registry cache is warm before listing: on a cold/fresh
// process skills that exist only in the DB would be missed (false
diff --git a/open-sse/handlers/chatCore/upstreamTimeouts.ts b/open-sse/handlers/chatCore/upstreamTimeouts.ts
index 9f0ace2b0a..b1a8da548d 100644
--- a/open-sse/handlers/chatCore/upstreamTimeouts.ts
+++ b/open-sse/handlers/chatCore/upstreamTimeouts.ts
@@ -9,6 +9,7 @@ import {
getLoggedOutputTokens,
getReasoningTokens,
} from "@/lib/usage/tokenAccounting";
+import { MAX_PROVIDER_SPECIFIC_TIMEOUT_MS } from "@/shared/validation/providerSpecificData";
export function createBodyTimeoutError(timeoutMs: number): Error {
const err = new Error(`Response body read timeout after ${timeoutMs}ms`);
@@ -89,14 +90,44 @@ function resolveProviderTimeoutMs(executor: unknown): number {
}
}
+/** Per-connection operator timeout tier: reads
+ * `providerSpecificData.timeoutMs`, bounded to 1..86_400_000 ms.
+ * Returns undefined when absent or invalid so the chain falls through. */
+export function resolveConnectionTimeoutMs(psd: unknown): number | undefined {
+ const timeoutMs = (psd as Record | null | undefined)?.timeoutMs;
+ if (typeof timeoutMs !== "number" || !Number.isFinite(timeoutMs)) return undefined;
+ const floored = Math.floor(timeoutMs);
+ if (floored < 1 || floored > MAX_PROVIDER_SPECIFIC_TIMEOUT_MS) return undefined;
+ return floored;
+}
+
/**
* Resolves the upstream header-response timeout in precedence order:
+ * connection-level override (`providerSpecificData.timeoutMs`) →
* model-level override (registry `RegistryModel.timeoutMs`) → provider-level
* override (`executor.getTimeoutMs()`) → global `FETCH_TIMEOUT_MS` default.
* `provider`/`model` are optional so existing single-argument call sites
* keep resolving to the provider/global chain unchanged (#6354).
*/
-export function getExecutorTimeoutMs(executor: unknown, provider?: string, model?: string): number {
+export function getExecutorTimeoutMs(
+ executor: unknown,
+ provider?: string,
+ model?: string,
+ connectionTimeoutMs?: number
+): number {
+ if (
+ typeof connectionTimeoutMs === "number" &&
+ Number.isFinite(connectionTimeoutMs) &&
+ connectionTimeoutMs > 0
+ ) {
+ // Defensive backstop for direct callers: resolveConnectionTimeoutMs is the
+ // gate (it rejects out-of-range values so the chain falls through); this
+ // clamp only caps values a future caller could pass unvetted.
+ return Math.min(
+ Math.max(0, Math.floor(connectionTimeoutMs)),
+ MAX_PROVIDER_SPECIFIC_TIMEOUT_MS
+ );
+ }
const modelOverride = resolveModelTimeoutOverride(provider, model);
if (modelOverride !== undefined) return modelOverride;
return resolveProviderTimeoutMs(executor);
@@ -196,6 +227,7 @@ export async function executeWithUpstreamStartTimeout({
executor,
provider,
model,
+ connectionTimeoutMs,
signal,
log,
execute,
@@ -203,11 +235,12 @@ export async function executeWithUpstreamStartTimeout({
executor: unknown;
provider: string;
model: string;
+ connectionTimeoutMs?: number;
signal: AbortSignal;
log?: { warn?: (tag: string, message: string) => void } | null;
execute: (signal: AbortSignal) => Promise;
}): Promise {
- const timeoutMs = getExecutorTimeoutMs(executor, provider, model);
+ const timeoutMs = getExecutorTimeoutMs(executor, provider, model, connectionTimeoutMs);
if (timeoutMs <= 0) return execute(signal);
if (signal.aborted) throw createAbortError(signal);
diff --git a/open-sse/handlers/imageGeneration.ts b/open-sse/handlers/imageGeneration.ts
index 5d50f125f9..c8312ab974 100644
--- a/open-sse/handlers/imageGeneration.ts
+++ b/open-sse/handlers/imageGeneration.ts
@@ -44,7 +44,7 @@ import { handleImagen3ImageGeneration } from "./imageGeneration/providers/imagen
import { handleIdeogramImageGeneration } from "./imageGeneration/providers/ideogram.ts";
import { handleHaiperImageGeneration } from "./imageGeneration/providers/haiper.ts";
import { handleLeonardoImageGeneration } from "./imageGeneration/providers/leonardo.ts";
-import { handleFreepikImageGeneration } from "./imageGeneration/providers/freepik.ts";
+import { handleMagnificImageGeneration } from "./imageGeneration/providers/magnific.ts";
import {
handleChatGptWebImageGeneration,
extractMarkdownImageUrls,
@@ -631,8 +631,8 @@ export async function handleImageGeneration({
log,
});
}
- if (providerConfig.format === "freepik-image") {
- return handleFreepikImageGeneration({
+ if (providerConfig.format === "magnific-image" || providerConfig.format === "freepik-image") {
+ return handleMagnificImageGeneration({
model,
provider,
providerConfig,
@@ -1346,6 +1346,107 @@ export async function handleOpenAIImageEdit({
return result;
}
+/**
+ * Handle OpenRouter's unified Image API reference-image flow.
+ *
+ * OpenRouter does not expose `/images/edits`; image-to-image requests use
+ * `POST /api/v1/images` with `input_references` containing data-URL images.
+ * Keep this separate from the generic multipart `/images/edits` forwarder,
+ * whose contract is used by custom OpenAI-compatible nodes (#10197).
+ */
+export async function handleOpenRouterImageEdit({
+ model,
+ provider,
+ baseUrl,
+ credentials,
+ prompt,
+ imageBytes,
+ imageMime,
+ size,
+ n = 1,
+ log,
+}: {
+ model: string;
+ provider: string;
+ baseUrl: string;
+ credentials:
+ | {
+ apiKey?: string;
+ accessToken?: string;
+ }
+ | null
+ | undefined;
+ prompt: string;
+ imageBytes: Buffer;
+ imageMime?: string | null;
+ size?: string | null;
+ n?: number;
+ log?: { info: (tag: string, message: string) => void } | null;
+}) {
+ const startTime = Date.now();
+ let url = baseUrl.trim();
+ while (url.endsWith("/")) url = url.slice(0, -1);
+ if (url.endsWith("/images/generations")) {
+ url = url.slice(0, -"/images/generations".length) + "/images";
+ } else if (!url.endsWith("/images")) {
+ url += "/images";
+ }
+
+ const mime = imageMime || "image/png";
+ const upstreamBody: Record = {
+ model,
+ prompt,
+ input_references: [
+ {
+ type: "image_url",
+ image_url: {
+ url: `data:${mime};base64,${imageBytes.toString("base64")}`,
+ },
+ },
+ ],
+ n: n || 1,
+ };
+ if (size) upstreamBody.size = size;
+
+ const headers: Record = {
+ "Content-Type": "application/json",
+ };
+ const token = credentials?.apiKey || credentials?.accessToken;
+ if (token) headers.Authorization = `Bearer ${token}`;
+
+ log?.info(
+ "IMAGE",
+ `${provider}/${model} (reference edit) | prompt: "${prompt.slice(0, 60)}..." -> ${url}`
+ );
+
+ const result = await fetchImageEndpoint(
+ url,
+ headers,
+ JSON.stringify(upstreamBody),
+ provider,
+ log
+ );
+
+ saveCallLog({
+ method: "POST",
+ path: "/v1/images/edits",
+ status: result.status || (result.success ? 200 : 502),
+ model: `${provider}/${model}`,
+ provider,
+ duration: Date.now() - startTime,
+ tokens: { prompt_tokens: 0, completion_tokens: 0 },
+ error: result.success
+ ? null
+ : typeof result.error === "string"
+ ? result.error.slice(0, 500)
+ : null,
+ requestBody: { model, prompt: prompt.slice(0, 200), size: size || "default", n: n || 1 },
+ responseBody: result.success ? { images_count: result.data?.data?.length || 0 } : null,
+ }).catch(() => {});
+
+ return result;
+}
+
export async function handleImageEdit({
provider,
model,
diff --git a/open-sse/handlers/imageGeneration/providers/freepik.ts b/open-sse/handlers/imageGeneration/providers/magnific.ts
similarity index 74%
rename from open-sse/handlers/imageGeneration/providers/freepik.ts
rename to open-sse/handlers/imageGeneration/providers/magnific.ts
index 2f3320cf06..31ea686327 100644
--- a/open-sse/handlers/imageGeneration/providers/freepik.ts
+++ b/open-sse/handlers/imageGeneration/providers/magnific.ts
@@ -1,12 +1,11 @@
-// Freepik (Magnific Mystic) image generation adapter.
+// Magnific Mystic image generation adapter.
// Async submit->poll flow modeled on leonardo.ts's generationId pattern:
// POST /v1/ai/mystic returns { data: { task_id, status } }, then
// GET /v1/ai/mystic/{task_id} is polled until status is COMPLETED/FAILED.
-// Docs: https://docs.magnific.com/api-reference/mystic (Freepik rebranded to
-// Magnific in April 2026; both `api.freepik.com` and the newer
-// `api.magnific.com` domain/header pair are in circulation during the
-// transition, so the base URL and auth header both come from providerConfig
-// rather than being hardcoded here).
+// Docs: https://docs.magnific.com/api-reference/mystic
+// Official host/header: api.magnific.com + x-magnific-api-key.
+// Both come from providerConfig so a local override can still use the
+// legacy api.freepik.com / x-freepik-api-key pair if needed.
import { saveCallLog } from "@/lib/usageDb";
import { sleep } from "../../../utils/sleep.ts";
@@ -21,34 +20,34 @@ function normalizePositiveNumber(value: unknown, fallback: number): number {
return Math.floor(n);
}
-interface FreepikProviderConfig {
+interface MagnificProviderConfig {
baseUrl: string;
statusUrl?: string;
authHeader?: string;
}
-interface FreepikCredentials {
+interface MagnificCredentials {
apiKey?: string;
}
-interface FreepikGenerationParams {
+interface MagnificGenerationParams {
model: string;
provider: string;
- providerConfig: FreepikProviderConfig;
+ providerConfig: MagnificProviderConfig;
body: Record;
- credentials: FreepikCredentials;
+ credentials: MagnificCredentials;
log?: { info: (tag: string, msg: string) => void; error: (tag: string, msg: string) => void };
}
-interface FreepikImageResult {
+interface MagnificImageResult {
success: boolean;
status?: number;
error?: string;
data?: { created: number; data: Array<{ b64_json: string }> };
}
-function freepikAuthHeader(providerConfig: FreepikProviderConfig, token: string) {
- const headerName = providerConfig.authHeader || "x-freepik-api-key";
+function magnificAuthHeader(providerConfig: MagnificProviderConfig, token: string) {
+ const headerName = providerConfig.authHeader || "x-magnific-api-key";
return { [headerName]: token };
}
@@ -58,7 +57,7 @@ async function logAndFail(params: {
startTime: number;
status: number;
error: string;
-}): Promise {
+}): Promise {
const { provider, model, startTime, status, error } = params;
const sanitized = sanitizeErrorMessage(error);
saveCallLog({
@@ -74,7 +73,7 @@ async function logAndFail(params: {
}
async function submitMysticTask(params: {
- providerConfig: FreepikProviderConfig;
+ providerConfig: MagnificProviderConfig;
token: string;
model: string;
prompt: string;
@@ -85,7 +84,7 @@ async function submitMysticTask(params: {
method: "POST",
headers: {
"Content-Type": "application/json",
- ...freepikAuthHeader(providerConfig, token),
+ ...magnificAuthHeader(providerConfig, token),
},
body: JSON.stringify({
prompt,
@@ -97,14 +96,14 @@ async function submitMysticTask(params: {
}
async function pollMysticTask(params: {
- providerConfig: FreepikProviderConfig;
+ providerConfig: MagnificProviderConfig;
token: string;
taskId: string;
}): Promise<{ status: string; imageUrl?: string }> {
const { providerConfig, token, taskId } = params;
const statusBase = providerConfig.statusUrl || providerConfig.baseUrl;
const res = await fetch(`${statusBase}/${taskId}`, {
- headers: { ...freepikAuthHeader(providerConfig, token) },
+ headers: { ...magnificAuthHeader(providerConfig, token) },
});
const json = await res.json();
const task = json?.data || json;
@@ -113,9 +112,9 @@ async function pollMysticTask(params: {
return { status, imageUrl: typeof generated[0] === "string" ? generated[0] : undefined };
}
-async function downloadGeneratedImage(imageUrl: string): Promise<
- { state: "ok"; b64: string } | { state: "failed"; status: number; error: string }
-> {
+async function downloadGeneratedImage(
+ imageUrl: string
+): Promise<{ state: "ok"; b64: string } | { state: "failed"; status: number; error: string }> {
const imgRes = await fetch(imageUrl);
if (!imgRes.ok) {
return {
@@ -133,7 +132,7 @@ async function resolveCompletedResult(params: {
model: string;
startTime: number;
imageUrl?: string;
-}): Promise {
+}): Promise {
const { provider, model, startTime, imageUrl } = params;
if (!imageUrl) {
return logAndFail({
@@ -141,7 +140,7 @@ async function resolveCompletedResult(params: {
model,
startTime,
status: 502,
- error: "Freepik Mystic completed without a generated image URL",
+ error: "Magnific Mystic completed without a generated image URL",
});
}
const downloaded = await downloadGeneratedImage(imageUrl);
@@ -163,7 +162,7 @@ async function resolveCompletedResult(params: {
}
async function pollUntilDone(params: {
- providerConfig: FreepikProviderConfig;
+ providerConfig: MagnificProviderConfig;
token: string;
taskId: string;
provider: string;
@@ -171,9 +170,17 @@ async function pollUntilDone(params: {
startTime: number;
pollIntervalMs: number;
pollTimeoutMs: number;
-}): Promise {
- const { providerConfig, token, taskId, provider, model, startTime, pollIntervalMs, pollTimeoutMs } =
- params;
+}): Promise {
+ const {
+ providerConfig,
+ token,
+ taskId,
+ provider,
+ model,
+ startTime,
+ pollIntervalMs,
+ pollTimeoutMs,
+ } = params;
const deadline = Date.now() + pollTimeoutMs;
while (Date.now() < deadline) {
@@ -189,7 +196,7 @@ async function pollUntilDone(params: {
model,
startTime,
status: 502,
- error: "Freepik Mystic image generation failed",
+ error: "Magnific Mystic image generation failed",
});
}
}
@@ -199,24 +206,32 @@ async function pollUntilDone(params: {
model,
startTime,
status: 504,
- error: "Freepik Mystic image generation timed out",
+ error: "Magnific Mystic image generation timed out",
});
}
async function submitAndGetTaskId(params: {
- providerConfig: FreepikProviderConfig;
+ providerConfig: MagnificProviderConfig;
token: string;
model: string;
prompt: string;
body: Record;
provider: string;
startTime: number;
-}): Promise<{ taskId: string } | { failed: FreepikImageResult }> {
+}): Promise<{ taskId: string } | { failed: MagnificImageResult }> {
const { providerConfig, token, model, prompt, body, provider, startTime } = params;
const res = await submitMysticTask({ providerConfig, token, model, prompt, body });
if (!res.ok) {
const errorText = await res.text();
- return { failed: await logAndFail({ provider, model, startTime, status: res.status, error: errorText }) };
+ return {
+ failed: await logAndFail({
+ provider,
+ model,
+ startTime,
+ status: res.status,
+ error: errorText,
+ }),
+ };
}
const submitJson = await res.json();
@@ -228,28 +243,31 @@ async function submitAndGetTaskId(params: {
model,
startTime,
status: 502,
- error: "Freepik Mystic did not return a task_id",
+ error: "Magnific Mystic did not return a task_id",
}),
};
}
return { taskId };
}
-export async function handleFreepikImageGeneration({
+export async function handleMagnificImageGeneration({
model,
provider,
providerConfig,
body,
credentials,
log,
-}: FreepikGenerationParams): Promise {
+}: MagnificGenerationParams): Promise {
const startTime = Date.now();
const token = credentials?.apiKey || "";
const prompt = typeof body.prompt === "string" ? body.prompt : String(body.prompt ?? "");
const pollIntervalMs = normalizePositiveNumber(body.poll_interval_ms, DEFAULT_POLL_INTERVAL_MS);
const pollTimeoutMs = normalizePositiveNumber(body.poll_timeout_ms, DEFAULT_POLL_TIMEOUT_MS);
if (log) {
- log.info("IMAGE", `${provider}/${model} (freepik-mystic) | prompt: "${prompt.slice(0, 60)}..."`);
+ log.info(
+ "IMAGE",
+ `${provider}/${model} (magnific-mystic) | prompt: "${prompt.slice(0, 60)}..."`
+ );
}
try {
@@ -276,7 +294,7 @@ export async function handleFreepikImageGeneration({
});
} catch (err) {
const message = (err as Error)?.message || String(err);
- if (log) log.error("IMAGE", `${provider} freepik error: ${sanitizeErrorMessage(message)}`);
+ if (log) log.error("IMAGE", `${provider} magnific error: ${sanitizeErrorMessage(message)}`);
return logAndFail({
provider,
model,
diff --git a/open-sse/handlers/mediaGeneration/minimaxMusic.ts b/open-sse/handlers/mediaGeneration/minimaxMusic.ts
new file mode 100644
index 0000000000..3486676058
--- /dev/null
+++ b/open-sse/handlers/mediaGeneration/minimaxMusic.ts
@@ -0,0 +1,358 @@
+/**
+ * MiniMax music generation handler (format: "minimax-music").
+ *
+ * The provider entry has been in musicRegistry since the media registries were
+ * introduced, but handleMusicGeneration never grew a branch for its format — so
+ * every registered `minimax/*` music model fell through the dispatch chain to
+ * `Unsupported music format: minimax-music` (400) and the models were
+ * advertised by /v1/models while being impossible to call.
+ *
+ * The upstream contract is a single synchronous POST — unlike the vendor's
+ * task-based media endpoints there is no task id and no query endpoint, so a
+ * request is either finished (`data.status` 2, audio in `data.audio`) or still
+ * generating (`data.status` 1), which can only be reported back, never awaited.
+ * Failures are carried in the `base_resp` envelope (`status_code` 0 = success)
+ * even on HTTP 200.
+ *
+ * `output_format` selects how the audio comes back: `url` (a short-lived link,
+ * valid for 24h — callers must download it before it expires) or `hex` (the raw
+ * container inline, normalized here to base64 so the response matches the
+ * OpenAI-shaped payload the other music branches return).
+ */
+
+import { saveCallLog } from "@/lib/usageDb";
+import { sanitizeErrorMessage } from "../../utils/error.ts";
+
+type MinimaxMusicBody = Record;
+
+interface MinimaxMusicProviderConfig {
+ baseUrl: string;
+ /** Regional deployment of the same contract — see resolveEndpoint below. */
+ regionalBaseUrl?: string;
+}
+
+interface MinimaxMusicCredentials {
+ apiKey?: unknown;
+ accessToken?: unknown;
+ providerSpecificData?: { baseUrl?: unknown } | null;
+}
+
+interface MinimaxMusicLog {
+ info?: (scope: string, message: string) => void;
+ error?: (scope: string, message: string) => void;
+}
+
+interface MinimaxMusicArgs {
+ model: string;
+ provider: string;
+ providerConfig: MinimaxMusicProviderConfig;
+ body: MinimaxMusicBody;
+ credentials?: MinimaxMusicCredentials | null;
+ log?: MinimaxMusicLog | null;
+}
+
+/** Containers accepted by `audio_setting.format`. */
+const AUDIO_FORMATS = new Set(["mp3", "wav", "pcm"]);
+/** Accepted `output_format` values. */
+const OUTPUT_FORMATS = new Set(["url", "hex"]);
+/** Container assumed when the request does not pin `audio_setting.format`. */
+const DEFAULT_AUDIO_FORMAT = "mp3";
+/** `data.status`: 1 = still generating, 2 = finished. */
+const STATUS_IN_PROGRESS = 1;
+/** String request fields forwarded verbatim when the caller provides them. */
+const STRING_REQUEST_FIELDS = [
+ "prompt",
+ "lyrics",
+ "audio_url",
+ "audio_base64",
+ "cover_feature_id",
+] as const;
+
+function isRecord(value: unknown): value is Record {
+ return typeof value === "object" && value !== null;
+}
+
+function stringValue(value: unknown): string | undefined {
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
+}
+
+function numberValue(value: unknown): number | undefined {
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
+}
+
+function booleanValue(value: unknown): boolean | undefined {
+ return typeof value === "boolean" ? value : undefined;
+}
+
+/** Fire-and-forget usage log for a MiniMax music-generation call. */
+function logMinimaxMusicCall(params: {
+ status: number;
+ model: string;
+ provider: string;
+ duration: number;
+ error?: string;
+ requestBody?: unknown;
+ responseBody?: unknown;
+}): void {
+ saveCallLog({
+ method: "POST",
+ path: "/v1/music/generations",
+ ...params,
+ }).catch(() => {});
+}
+
+/**
+ * Endpoint for this call: the per-connection `providerSpecificData.baseUrl`
+ * override (the same storage every configurable-base-URL provider uses) wins
+ * over the registry default. That override is how a connection targets the
+ * regional deployment declared as `regionalBaseUrl`.
+ */
+function resolveEndpoint(
+ providerConfig: MinimaxMusicProviderConfig,
+ credentials?: MinimaxMusicCredentials | null
+): string {
+ const psd = credentials?.providerSpecificData;
+ const override = isRecord(psd) ? stringValue(psd.baseUrl) : undefined;
+ return override || providerConfig.baseUrl;
+}
+
+/** True when `endpoint` is the regional deployment declared by the registry. */
+function isRegionalEndpoint(endpoint: string, regionalBaseUrl?: string): boolean {
+ if (!regionalBaseUrl) return false;
+ try {
+ return new URL(endpoint).host === new URL(regionalBaseUrl).host;
+ } catch {
+ return false;
+ }
+}
+
+/** Forwards only the recognized `audio_setting` members, dropping unknown containers. */
+function buildAudioSetting(body: MinimaxMusicBody): Record | undefined {
+ const provided: Record = isRecord(body.audio_setting) ? body.audio_setting : {};
+ const setting: Record = {};
+
+ const sampleRate = numberValue(provided.sample_rate);
+ if (sampleRate !== undefined) setting.sample_rate = sampleRate;
+
+ const bitrate = numberValue(provided.bitrate);
+ if (bitrate !== undefined) setting.bitrate = bitrate;
+
+ const format = stringValue(provided.format)?.toLowerCase();
+ if (format && AUDIO_FORMATS.has(format)) setting.format = format;
+
+ return Object.keys(setting).length > 0 ? setting : undefined;
+}
+
+/** Container reported back to the caller — mirrors what was asked upstream. */
+function resolveAudioFormat(body: MinimaxMusicBody): string {
+ const provided: Record = isRecord(body.audio_setting) ? body.audio_setting : {};
+ const format = stringValue(provided.format)?.toLowerCase();
+ return format && AUDIO_FORMATS.has(format) ? format : DEFAULT_AUDIO_FORMAT;
+}
+
+function resolveOutputFormat(body: MinimaxMusicBody): string {
+ const requested = stringValue(body.output_format)?.toLowerCase();
+ return requested && OUTPUT_FORMATS.has(requested) ? requested : "url";
+}
+
+/**
+ * Upstream request body. `stream` is pinned false: this route answers with a
+ * single JSON payload, and streaming responses would also be restricted to the
+ * hex output format.
+ */
+function buildUpstreamBody(
+ model: string,
+ body: MinimaxMusicBody,
+ regional: boolean
+): Record {
+ const request: Record = {
+ model,
+ stream: false,
+ output_format: resolveOutputFormat(body),
+ };
+
+ for (const field of STRING_REQUEST_FIELDS) {
+ const value = stringValue(body[field]);
+ if (value !== undefined) request[field] = value;
+ }
+
+ const audioSetting = buildAudioSetting(body);
+ if (audioSetting) request.audio_setting = audioSetting;
+
+ const lyricsOptimizer = booleanValue(body.lyrics_optimizer);
+ if (lyricsOptimizer !== undefined) request.lyrics_optimizer = lyricsOptimizer;
+
+ // `instrumental` is the spelling the other music branches already accept.
+ const isInstrumental = booleanValue(body.is_instrumental) ?? booleanValue(body.instrumental);
+ if (isInstrumental !== undefined) request.is_instrumental = isInstrumental;
+
+ // Only the regional endpoint accepts a watermark flag.
+ if (regional) {
+ const watermark = booleanValue(body.aigc_watermark);
+ if (watermark !== undefined) request.aigc_watermark = watermark;
+ }
+
+ return request;
+}
+
+async function readPayload(response: Response): Promise> {
+ const rawText = await response.text();
+ if (!rawText) return {};
+ try {
+ const parsed: unknown = JSON.parse(rawText);
+ return isRecord(parsed) ? parsed : {};
+ } catch {
+ return {};
+ }
+}
+
+/** Hex payloads are normalized to base64; Buffer would silently drop bad nibbles. */
+function hexAudioToBase64(audioHex: string): string {
+ if (audioHex.length % 2 !== 0 || !/^[0-9a-f]+$/i.test(audioHex)) {
+ throw new Error("MiniMax music generation returned invalid hex audio");
+ }
+ return Buffer.from(audioHex, "hex").toString("base64");
+}
+
+/** `base_resp.status_code` is non-zero on failures that still answer HTTP 200. */
+function readEnvelopeError(payload: Record): string | undefined {
+ const baseResp: Record = isRecord(payload.base_resp) ? payload.base_resp : {};
+ const statusCode = numberValue(baseResp.status_code);
+ if (statusCode === undefined || statusCode === 0) return undefined;
+ return stringValue(baseResp.status_msg) || `upstream status code ${statusCode}`;
+}
+
+export async function handleMinimaxMusicGeneration({
+ model,
+ provider,
+ providerConfig,
+ body,
+ credentials,
+ log,
+}: MinimaxMusicArgs) {
+ const startTime = Date.now();
+ const token = stringValue(credentials?.apiKey) || stringValue(credentials?.accessToken);
+ if (!token) {
+ return { success: false as const, status: 401, error: "MiniMax API key is required" };
+ }
+
+ const modelId = stringValue(model);
+ if (!modelId) {
+ return { success: false as const, status: 400, error: "MiniMax music model is required" };
+ }
+
+ const endpoint = resolveEndpoint(providerConfig, credentials);
+ const upstreamBody = buildUpstreamBody(
+ modelId,
+ body,
+ isRegionalEndpoint(endpoint, providerConfig.regionalBaseUrl)
+ );
+ const audioFormat = resolveAudioFormat(body);
+ const modelLabel = `${provider}/${modelId}`;
+
+ log?.info?.(
+ "MUSIC",
+ `${modelLabel} (minimax-music) | prompt: "${String(body.prompt ?? "").slice(0, 60)}..." | ` +
+ `output_format: ${upstreamBody.output_format} | audio_format: ${audioFormat}`
+ );
+
+ try {
+ const response = await fetch(endpoint, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ Authorization: `Bearer ${token}`,
+ },
+ body: JSON.stringify(upstreamBody),
+ });
+
+ const payload = await readPayload(response);
+
+ if (!response.ok) {
+ const errorMessage =
+ readEnvelopeError(payload) || `MiniMax music generation failed (${response.status})`;
+ log?.error?.("MUSIC", `${provider} minimax-music error ${response.status}: ${errorMessage}`);
+ logMinimaxMusicCall({
+ status: response.status,
+ model: modelLabel,
+ provider,
+ duration: Date.now() - startTime,
+ error: errorMessage,
+ requestBody: upstreamBody,
+ });
+ return { success: false as const, status: response.status, error: errorMessage };
+ }
+
+ const envelopeError = readEnvelopeError(payload);
+ if (envelopeError) {
+ log?.error?.("MUSIC", `${provider} minimax-music rejected the request: ${envelopeError}`);
+ logMinimaxMusicCall({
+ status: 502,
+ model: modelLabel,
+ provider,
+ duration: Date.now() - startTime,
+ error: envelopeError,
+ requestBody: upstreamBody,
+ });
+ return { success: false as const, status: 502, error: envelopeError };
+ }
+
+ const data: Record = isRecord(payload.data) ? payload.data : {};
+
+ // No task id and no query endpoint exist for this operation, so an
+ // unfinished generation cannot be polled — surface it instead of hanging.
+ if (numberValue(data.status) === STATUS_IN_PROGRESS) {
+ const pending = "MiniMax music generation is still in progress; retry the request";
+ logMinimaxMusicCall({
+ status: 502,
+ model: modelLabel,
+ provider,
+ duration: Date.now() - startTime,
+ error: pending,
+ });
+ return { success: false as const, status: 502, error: pending };
+ }
+
+ const audio = stringValue(data.audio);
+ if (!audio) {
+ const errorMessage = "MiniMax music generation returned no audio";
+ logMinimaxMusicCall({
+ status: 502,
+ model: modelLabel,
+ provider,
+ duration: Date.now() - startTime,
+ error: errorMessage,
+ });
+ return { success: false as const, status: 502, error: errorMessage };
+ }
+
+ const track =
+ upstreamBody.output_format === "hex"
+ ? { b64_json: hexAudioToBase64(audio), format: audioFormat }
+ : { url: audio, format: audioFormat };
+
+ logMinimaxMusicCall({
+ status: 200,
+ model: modelLabel,
+ provider,
+ duration: Date.now() - startTime,
+ responseBody: { audio_count: 1 },
+ });
+
+ return {
+ success: true as const,
+ data: { created: Math.floor(Date.now() / 1000), data: [track] },
+ };
+ } catch (err: unknown) {
+ const errorMessage = sanitizeErrorMessage(err) || "Music provider error";
+ log?.error?.("MUSIC", `${provider} minimax-music error: ${errorMessage}`);
+ logMinimaxMusicCall({
+ status: 502,
+ model: modelLabel,
+ provider,
+ duration: Date.now() - startTime,
+ error: errorMessage,
+ });
+ return { success: false as const, status: 502, error: errorMessage };
+ }
+}
diff --git a/open-sse/handlers/musicGeneration.ts b/open-sse/handlers/musicGeneration.ts
index 92ee333c2e..89052b3765 100644
--- a/open-sse/handlers/musicGeneration.ts
+++ b/open-sse/handlers/musicGeneration.ts
@@ -33,6 +33,7 @@ import {
} from "../utils/kieTask.ts";
import { sanitizeErrorMessage } from "../utils/error.ts";
import { handleFalMusicGeneration } from "./mediaGeneration/fal.ts";
+import { handleMinimaxMusicGeneration } from "./mediaGeneration/minimaxMusic.ts";
function normalizeKieSunoModel(model: string): string {
const map: Record = {
@@ -153,6 +154,17 @@ export async function handleMusicGeneration({ body, credentials, log }) {
return handleUdioMusicGeneration({ model, provider, providerConfig, body, credentials, log });
}
+ if (providerConfig.format === "minimax-music") {
+ return handleMinimaxMusicGeneration({
+ model,
+ provider,
+ providerConfig,
+ body,
+ credentials,
+ log,
+ });
+ }
+
return {
success: false,
status: 400,
diff --git a/open-sse/handlers/responseSanitizer.ts b/open-sse/handlers/responseSanitizer.ts
index 06d3202f5f..a2210681d7 100644
--- a/open-sse/handlers/responseSanitizer.ts
+++ b/open-sse/handlers/responseSanitizer.ts
@@ -8,7 +8,10 @@ import {
collapseExcessiveNewlines,
extractThinkingFromContent,
} from "./responseSanitizer/reasoning.ts";
-import { applyCacheHitTokensToUsage, applyCacheHitTokensToResponsesUsage } from "./responseSanitizer/cacheHitTokens.ts";
+import {
+ applyCacheHitTokensToUsage,
+ applyCacheHitTokensToResponsesUsage,
+} from "./responseSanitizer/cacheHitTokens.ts";
export {
extractThinkingFromContent,
shouldParseTextualReasoningTags,
@@ -31,7 +34,9 @@ const ALLOWED_USAGE_FIELDS = new Set([
"total_tokens",
"cached_tokens",
"prompt_tokens_details",
- "completion_tokens_details", "cache_read_input_tokens", "cache_creation_input_tokens",
+ "completion_tokens_details",
+ "cache_read_input_tokens",
+ "cache_creation_input_tokens",
// Keep through sanitize → applyClientUsageBuffer so heuristic web usage is
// not inflated by the default USAGE_TOKEN_BUFFER (2000).
"estimated",
@@ -550,7 +555,7 @@ function sanitizeResponsesUsage(usage: unknown): unknown {
!(toRecord(normalized.input_tokens_details) ?? {}).cached_tokens
) {
normalized.input_tokens_details = {
- ...(normalized.input_tokens_details as Record || {}),
+ ...((normalized.input_tokens_details as Record) || {}),
cached_tokens: normalized.prompt_cache_hit_tokens,
};
}
@@ -562,7 +567,7 @@ function sanitizeResponsesUsage(usage: unknown): unknown {
!(toRecord(normalized.input_tokens_details) ?? {}).cached_tokens
) {
normalized.input_tokens_details = {
- ...(normalized.input_tokens_details as Record || {}),
+ ...((normalized.input_tokens_details as Record) || {}),
cached_tokens: normalized.cache_read_input_tokens,
};
}
@@ -863,6 +868,7 @@ function sanitizeResponsesOutputItem(item: unknown, index: number): JsonRecord |
: [];
return {
+ ...itemRecord,
id: toString(itemRecord.id) || `rs_${index}`,
type: "reasoning",
summary,
diff --git a/open-sse/handlers/responseTranslator.ts b/open-sse/handlers/responseTranslator.ts
index 407393966d..01bdd4c14a 100644
--- a/open-sse/handlers/responseTranslator.ts
+++ b/open-sse/handlers/responseTranslator.ts
@@ -10,6 +10,7 @@ import {
caseInsensitiveToolNameLookup,
restoreOpenAIToolNames,
} from "../translator/helpers/toolCallHelper.ts";
+import { extractReplayableResponsesReasoningText } from "../services/reasoningInputPolicy.ts";
import { sanitizeToolId } from "../translator/helpers/schemaCoercion.ts";
type JsonRecord = Record;
@@ -178,7 +179,8 @@ export function translateNonStreamingResponse(
const messageSelection = findBestMessageText(output);
let textContent = messageSelection.text;
- let reasoningContent = "";
+ let replayableReasoningContent = "";
+ let reasoningSummary = "";
const toolCalls: JsonRecord[] = [];
for (const item of output) {
@@ -192,16 +194,22 @@ export function translateNonStreamingResponse(
if (partObj.type === "summary_text" && typeof partObj.text === "string") {
// #9500 — reasoning summary parts are discrete segments; join with "\n\n"
// (matches extractThinkingFromContent convention) so they don't glue back-to-back.
- reasoningContent += reasoningContent ? `\n\n${partObj.text}` : partObj.text;
+ reasoningSummary += reasoningSummary ? `\n\n${partObj.text}` : partObj.text;
}
}
- } else if (itemObj.type === "reasoning" && Array.isArray(itemObj.summary)) {
- for (const part of itemObj.summary) {
- const partObj = toRecord(part);
- if (partObj.type === "summary_text" && typeof partObj.text === "string") {
- // #9500 — reasoning summary parts are discrete segments; join with "\n\n"
- // (matches extractThinkingFromContent convention) so they don't glue back-to-back.
- reasoningContent += reasoningContent ? `\n\n${partObj.text}` : partObj.text;
+ } else if (itemObj.type === "reasoning") {
+ const replayable = extractReplayableResponsesReasoningText(itemObj);
+ if (replayable) {
+ replayableReasoningContent += replayableReasoningContent
+ ? `\n\n${replayable}`
+ : replayable;
+ }
+ if (Array.isArray(itemObj.summary)) {
+ for (const part of itemObj.summary) {
+ const partObj = toRecord(part);
+ if (partObj.type === "summary_text" && typeof partObj.text === "string") {
+ reasoningSummary += reasoningSummary ? `\n\n${partObj.text}` : partObj.text;
+ }
}
}
} else if (itemObj.type === "function_call") {
@@ -238,8 +246,11 @@ export function translateNonStreamingResponse(
if (textContent) {
message.content = textContent;
}
- if (reasoningContent) {
- message.reasoning_content = reasoningContent;
+ if (replayableReasoningContent) {
+ message.reasoning_content = replayableReasoningContent;
+ }
+ if (reasoningSummary) {
+ message.reasoning_summary = [{ type: "summary_text", text: reasoningSummary }];
}
if (toolCalls.length > 0) {
message.tool_calls = toolCalls;
diff --git a/open-sse/mcp-server/__tests__/glmCodingProviderConfig.test.ts b/open-sse/mcp-server/__tests__/glmCodingProviderConfig.test.ts
index 52ce967e1f..8b7f3cee32 100644
--- a/open-sse/mcp-server/__tests__/glmCodingProviderConfig.test.ts
+++ b/open-sse/mcp-server/__tests__/glmCodingProviderConfig.test.ts
@@ -87,6 +87,9 @@ describe("GLM Coding provider registry surfaces", () => {
expect(PROVIDER_ID_TO_ALIAS.glm).toBe("glm");
expect(byProviderId).toEqual(byAlias);
expect(byProviderId.map((model) => model.id)).toEqual([
+ "glm-5.3",
+ "glm-5.3-high",
+ "glm-5.3-low",
"glm-5.2",
"glm-5.2-high",
"glm-5.2-max",
diff --git a/open-sse/mcp-server/fetchTimeout.ts b/open-sse/mcp-server/fetchTimeout.ts
index a9389b0c24..e3cea5bcce 100644
--- a/open-sse/mcp-server/fetchTimeout.ts
+++ b/open-sse/mcp-server/fetchTimeout.ts
@@ -34,6 +34,21 @@ function readPositiveIntEnv(raw: string | undefined): number | null {
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : null;
}
+function readMcpTimeoutOverride(
+ kind: McpFetchTimeoutKind,
+ env: Record
+): string | undefined {
+ // Direct process.env member access so fabricated-docs / env-doc-sync see
+ // the operator knobs. Tests inject a fake env object and keep using the
+ // exported constant keys.
+ if (env === process.env) {
+ return kind === "upstream"
+ ? process.env.OMNIROUTE_MCP_UPSTREAM_TIMEOUT_MS
+ : process.env.OMNIROUTE_MCP_FETCH_TIMEOUT_MS;
+ }
+ return env[kind === "upstream" ? MCP_UPSTREAM_FETCH_TIMEOUT_ENV : MCP_FETCH_TIMEOUT_ENV];
+}
+
/**
* Resolve the timeout for one internal fetch class. An unset, malformed or
* non-positive override falls back to the built-in default rather than
@@ -44,11 +59,8 @@ export function resolveMcpFetchTimeoutMs(
kind: McpFetchTimeoutKind,
env: Record = process.env
): number {
- const upstream = kind === "upstream";
- const override = readPositiveIntEnv(
- env[upstream ? MCP_UPSTREAM_FETCH_TIMEOUT_ENV : MCP_FETCH_TIMEOUT_ENV]
- );
- return override ?? (upstream ? MCP_UPSTREAM_FETCH_TIMEOUT_MS : MCP_FETCH_TIMEOUT_MS);
+ const override = readPositiveIntEnv(readMcpTimeoutOverride(kind, env));
+ return override ?? (kind === "upstream" ? MCP_UPSTREAM_FETCH_TIMEOUT_MS : MCP_FETCH_TIMEOUT_MS);
}
/** `AbortSignal` for one internal fetch of the given class. */
diff --git a/open-sse/mcp-server/tools/memoryTools.ts b/open-sse/mcp-server/tools/memoryTools.ts
index 908970f2e1..16c835fd8e 100644
--- a/open-sse/mcp-server/tools/memoryTools.ts
+++ b/open-sse/mcp-server/tools/memoryTools.ts
@@ -7,9 +7,23 @@ import {
toMemoryRetrievalConfig,
DEFAULT_MEMORY_SETTINGS,
} from "@/lib/memory/settings";
+import { resolveMcpCallerApiKeyId } from "../mcpCallerIdentity.ts";
+
+/**
+ * Resolve the memory owner id for an MCP tool call:
+ * explicit arg wins, otherwise fall back to the authenticated caller's
+ * principal id (HTTP auth headers on SSE/Streamable HTTP transports,
+ * OMNIROUTE_API_KEY env var on stdio). Keeps MCP-stored memories under
+ * the same owner id that chat-context memory uses, so retrieval in the
+ * chat pipeline finds entries written via MCP.
+ */
+async function resolveMemoryOwnerId(explicit?: string): Promise {
+ if (explicit && explicit.trim() !== "") return explicit.trim();
+ return (await resolveMcpCallerApiKeyId().catch(() => undefined)) || "mcp";
+}
export const MemorySearchSchema = z.object({
- apiKeyId: z.string(),
+ apiKeyId: z.string().optional(),
query: z.string().optional(),
type: z.enum(["factual", "episodic", "procedural", "semantic"]).optional(),
maxTokens: z.number().int().positive().max(8000).optional(),
@@ -17,7 +31,7 @@ export const MemorySearchSchema = z.object({
});
export const MemoryAddSchema = z.object({
- apiKeyId: z.string(),
+ apiKeyId: z.string().optional(),
sessionId: z.string().optional(),
type: z.enum(["factual", "episodic", "procedural", "semantic"]),
key: z.string().min(1),
@@ -26,7 +40,7 @@ export const MemoryAddSchema = z.object({
});
export const MemoryClearSchema = z.object({
- apiKeyId: z.string(),
+ apiKeyId: z.string().optional(),
type: z.enum(["factual", "episodic", "procedural", "semantic"]).optional(),
olderThan: z.string().optional(),
});
@@ -38,6 +52,7 @@ export const memoryTools = {
scopes: ["read:memory"],
inputSchema: MemorySearchSchema,
handler: async (args: z.infer) => {
+ const apiKeyId = await resolveMemoryOwnerId(args.apiKeyId);
// Plan 21 D16/Bug#7 fix: even on the error path the fallback must
// respect DEFAULT_MEMORY_SETTINGS.strategy instead of hardcoding "exact".
const memorySettings =
@@ -54,7 +69,7 @@ export const memoryTools = {
(memorySettings.enabled ? memorySettings.maxTokens : DEFAULT_MEMORY_SETTINGS.maxTokens),
};
- const memories = await retrieveMemories(args.apiKeyId, config);
+ const memories = await retrieveMemories(apiKeyId, config);
const filtered = args.type ? memories.filter((m) => m.type === args.type) : memories;
@@ -77,8 +92,9 @@ export const memoryTools = {
scopes: ["write:memory"],
inputSchema: MemoryAddSchema,
handler: async (args: z.infer) => {
+ const apiKeyId = await resolveMemoryOwnerId(args.apiKeyId);
const memory = await createMemory({
- apiKeyId: args.apiKeyId,
+ apiKeyId,
sessionId: args.sessionId || "",
type: args.type as MemoryType,
key: args.key,
@@ -103,8 +119,9 @@ export const memoryTools = {
scopes: ["write:memory"],
inputSchema: MemoryClearSchema,
handler: async (args: z.infer) => {
+ const apiKeyId = await resolveMemoryOwnerId(args.apiKeyId);
const result = await listMemories({
- apiKeyId: args.apiKeyId,
+ apiKeyId,
type: args.type as MemoryType | undefined,
});
const existingMemories = Array.isArray(result)
diff --git a/open-sse/services/autoCombo/chaosEngine.ts b/open-sse/services/autoCombo/chaosEngine.ts
index 89813fe48f..32f5b09f48 100644
--- a/open-sse/services/autoCombo/chaosEngine.ts
+++ b/open-sse/services/autoCombo/chaosEngine.ts
@@ -180,7 +180,22 @@ function dispatchOnePanelModel(opts: {
log?.info?.(
`CHAOS panel ${index} (${model}) ok=${res.ok} status=${res.status} textLen=${text.length}`
);
- const part: ChaosPart = { model, index, ok: true, text };
+ // G5b: honor the upstream response status — a 4xx/5xx is a panel FAILURE,
+ // not a success (previously ok:true was hardcoded, so an all-error panel
+ // never reached the all-failed branch and the error text was streamed as
+ // if it were a successful answer).
+ if (res.ok) {
+ const part: ChaosPart = { model, index, ok: true, text };
+ await onResult?.(part);
+ return part;
+ }
+ const part: ChaosPart = {
+ model,
+ index,
+ ok: false,
+ text: "",
+ error: `upstream ${res.status}: ${text.slice(0, 200) || res.statusText || "error"}`,
+ };
await onResult?.(part);
return part;
} catch (err) {
@@ -466,8 +481,17 @@ export async function handleChaosChat(opts: {
}
if (successes.length === 0) {
- const errText = "All chaos panel models failed";
- await safeEnqueue(chatChunk(chunkId, panelToDispatch[0] ?? "", errText));
+ // G5 (silent-stop fix): make an all-panel failure visible server-side.
+ // The status stays 200 (SSE envelope must stay well-formed), but the
+ // failure is now logged with the per-model errors so operators can see
+ // why the chaos panel produced nothing.
+ const modelErrors = allParts.map((p) => `${p.model}: ${p.error ?? "unknown"}`).join(" | ");
+ log?.warn?.(
+ "CHAOS",
+ `All chaos panel models failed for ${comboName ?? "panel"}: ${modelErrors}`
+ );
+ const errText = `All chaos panel models failed — ${modelErrors}`;
+ await safeEnqueue(chatChunk(chunkId, panelToDispatch[0] ?? panel[0] ?? "", errText));
await safeEnqueue(SSE_DONE);
await enqueueChain;
closed = true;
diff --git a/open-sse/services/autoCombo/pipelineRouter.ts b/open-sse/services/autoCombo/pipelineRouter.ts
index 5fbc9eea02..bc2ce3c5d5 100644
--- a/open-sse/services/autoCombo/pipelineRouter.ts
+++ b/open-sse/services/autoCombo/pipelineRouter.ts
@@ -343,6 +343,17 @@ export async function handlePipelineCombo({
}
}
+ // G6 (silent-stop fix): if the reflection loop burned its retry budget and the
+ // verdict is still "fail", the fall-through below returns a FAILED result
+ // indistinguishable from a first-attempt failure. Surface it loudly so the
+ // caller (and operator logs) can tell "retries exhausted" apart.
+ if (result.reflectVerdict === "fail" && reflectionCount > 0) {
+ log.warn(
+ "PIPELINE",
+ `Reflection retries exhausted (${reflectionCount}/${maxReflectionLoops}) — pipeline verdict still "fail", returning the original failed result`
+ );
+ }
+
// ── Return result ─────────────────────────────────────────────────────────
// Check if the last stage has a streaming Response
const lastStage = result.stages[result.stages.length - 1];
diff --git a/open-sse/services/autoCombo/scoring.ts b/open-sse/services/autoCombo/scoring.ts
index b8f66a797e..4c939501c7 100644
--- a/open-sse/services/autoCombo/scoring.ts
+++ b/open-sse/services/autoCombo/scoring.ts
@@ -23,6 +23,12 @@ export interface ScoringFactors {
sessionAvailability?: number;
resetWindowAffinity: number;
connectionDensity: number;
+ /**
+ * Feedback-driven quality signal [0,1] from the routing-event quality tracker
+ * (open-sse/services/routing/quality.ts). Optional so cold candidates with no
+ * observed events default to neutral (1.0) and are never penalized.
+ */
+ quality?: number;
}
export interface ScoringWeights {
@@ -40,11 +46,13 @@ export interface ScoringWeights {
sessionAvailability?: number;
resetWindowAffinity: number;
connectionDensity: number;
+ /** Weight for the feedback-driven quality factor (#feedback-foundation). */
+ quality?: number;
}
export const DEFAULT_WEIGHTS: ScoringWeights = {
quota: 0.1429,
- health: 0.1905,
+ health: 0.1605,
costInv: 0.1429,
latencyInv: 0.1143,
taskFit: 0.0762,
@@ -57,6 +65,10 @@ export const DEFAULT_WEIGHTS: ScoringWeights = {
sessionAvailability: 0.0476,
resetWindowAffinity: 0,
connectionDensity: 0.0476,
+ // Shifted from `health` (0.1905 → 0.1605): availability stays dominant, and
+ // the new quality signal (observed output quality over time) gets a real,
+ // if smaller, vote. Sum remains exactly 1.0.
+ quality: 0.03,
};
/** Normalize independently configured UI weights into a scoring distribution. */
@@ -107,6 +119,12 @@ export interface ProviderCandidate {
sessionAvailability?: number;
/** Score [0..1] for quota reset-window preference; sooner selected reset windows score higher. */
resetWindowAffinity?: number;
+ /**
+ * Feedback-driven quality score [0..1] for this provider/model from the
+ * routing-event quality tracker (open-sse/services/routing). Omitted/undefined
+ * candidates default to a neutral 1.0 in calculateFactors.
+ */
+ quality?: number;
connectionPoolSize?: number;
connectionId?: string;
}
@@ -141,7 +159,10 @@ export function calculateScore(factors: ScoringFactors, weights: ScoringWeights)
(weights.cacheAffinity ?? 0) * (factors.cacheAffinity ?? 0) +
(weights.sessionAvailability ?? 0) * (factors.sessionAvailability ?? 1) +
(weights.resetWindowAffinity ?? 0) * factors.resetWindowAffinity +
- (weights.connectionDensity ?? 0) * factors.connectionDensity
+ (weights.connectionDensity ?? 0) * factors.connectionDensity +
+ // Missing quality factor → neutral 0.5: a cold candidate is neither boosted
+ // (which would let optimistic initialization dominate) nor penalized.
+ (weights.quality ?? 0) * (factors.quality ?? 0.5)
);
}
@@ -268,6 +289,9 @@ export function calculateFactors(
sessionAvailability: clamp01(candidate.sessionAvailability ?? 1),
resetWindowAffinity: clamp01(candidate.resetWindowAffinity ?? 0.5),
connectionDensity: clamp01(((candidate.connectionPoolSize ?? 1) - 1) / 10),
+ // Feedback quality signal; neutral 0.5 when the tracker has no data yet
+ // (cold providers are neither boosted nor unfairly penalized).
+ quality: clamp01(candidate.quality ?? 0.5),
};
}
diff --git a/open-sse/services/autoCombo/virtualFactory.ts b/open-sse/services/autoCombo/virtualFactory.ts
index f468f23005..2eff8257ea 100644
--- a/open-sse/services/autoCombo/virtualFactory.ts
+++ b/open-sse/services/autoCombo/virtualFactory.ts
@@ -20,6 +20,7 @@ import {
type AutoCategory,
type AutoTier,
} from "./suffixComposition";
+import { classifyTier } from "../tierResolver";
import type { AutoVariant } from "./autoPrefix";
import { buildFamilyCandidateFilter, type ModelFamily } from "./modelFamily";
import { getHiddenModelsByProvider } from "@/models";
@@ -602,6 +603,61 @@ export async function prepareVirtualAutoComboInputs(
};
}
+/**
+ * Score candidates at snapshot time using available data (capabilities, tier)
+ * and the mode-pack's dominant factors. Runtime telemetry (p95 latency, quota
+ * remaining) is not available during combo creation — this uses static signals only.
+ *
+ * Returns a map from modelStr → normalized weight score [0, 1].
+ */
+export function computeSnapshotWeights(
+ candidates: readonly VirtualAutoComboCandidate[],
+ weights: ScoringWeights
+): Map {
+ const scores = new Map();
+ for (const c of candidates) {
+ let score = 0;
+
+ // taskFit: reasoning + vision capable models score higher when taskFit is weighted
+ if (weights.taskFit > 0) {
+ if (c.resolvedReasoning || c.resolvedSupportsThinking) score += weights.taskFit * 0.6;
+ if (c.resolvedSupportsVision) score += weights.taskFit * 0.3;
+ }
+
+ // stability: models with richer capabilities are assumed more stable
+ if (weights.stability > 0) {
+ const capabilityCount =
+ Number(c.resolvedReasoning ?? false) +
+ Number(c.resolvedSupportsThinking ?? false) +
+ Number(c.resolvedSupportsVision ?? false);
+ score += weights.stability * Math.min(capabilityCount / 2, 1);
+ }
+
+ // Tier-based scoring (single classifyTier call covers both checks)
+ let tierInfo: { tier: string } | null = null;
+ if (weights.tierPriority > 0 || weights.costInv > 0) {
+ try {
+ tierInfo = classifyTier(c.provider, c.model);
+ } catch {
+ // fall through with zero
+ }
+ }
+ if (tierInfo && weights.tierPriority > 0 && tierInfo.tier === "premium")
+ score += weights.tierPriority;
+ if (tierInfo && weights.costInv > 0 && tierInfo.tier === "free") score += weights.costInv;
+
+ // latencyInv: all candidates get a base score when latency matters
+ // (no runtime data at snapshot time, so equal baseline)
+ if (weights.latencyInv > 0) score += weights.latencyInv * 0.5;
+
+ // health + quota: no runtime telemetry at snapshot time → neutral baseline
+ score += (weights.health + weights.quota) * 0.5;
+
+ scores.set(c.modelStr, Math.min(score, 1));
+ }
+ return scores;
+}
+
function clonePreparedCandidates(
candidates: readonly VirtualAutoComboCandidate[]
): VirtualAutoComboCandidate[] {
@@ -770,6 +826,7 @@ export async function createVirtualAutoComboFromPrepared(
}
const providerPool = [...new Set(effectivePool.map((c) => c.provider))];
+ const snapshotScores = computeSnapshotWeights(effectivePool, weights);
const models = effectivePool.map((candidate, index) => ({
id: `virtual-auto-${variant || "default"}-${index + 1}-${candidate.provider}`,
kind: "model" as const,
@@ -779,7 +836,7 @@ export async function createVirtualAutoComboFromPrepared(
...(candidate.allowedConnectionIds
? { allowedConnectionIds: candidate.allowedConnectionIds }
: {}),
- weight: 1,
+ weight: snapshotScores.get(candidate.modelStr) ?? 1,
label: candidate.provider,
}));
const autoConfig = {
diff --git a/open-sse/services/autoRefreshDaemon.ts b/open-sse/services/autoRefreshDaemon.ts
index 120b081545..3a177a87ae 100644
--- a/open-sse/services/autoRefreshDaemon.ts
+++ b/open-sse/services/autoRefreshDaemon.ts
@@ -125,8 +125,13 @@ class AutoRefreshDaemon {
`[AutoRefreshDaemon] Credential expired for "${providerId}" (${config.displayName})`
);
}
- } catch {
- // Network errors are non-fatal — retry next cycle
+ } catch (err) {
+ // Network errors are non-fatal — retry next cycle. G8: log which
+ // provider failed so credential problems are not silently masked.
+ console.warn(
+ `[AutoRefreshDaemon] Network error validating credential for "${providerId}" — retry next cycle`,
+ err instanceof Error ? err.message : err
+ );
}
}
@@ -165,8 +170,16 @@ class AutoRefreshDaemon {
}
return true;
- } catch {
- // Network errors (timeout, DNS failure) don't mean the credential is bad
+ } catch (err) {
+ // Network errors (timeout, DNS failure) don't mean the credential is bad.
+ // G8 (silent-stop fix): the previous bare `catch { return true; }` swallowed
+ // the error entirely — operators could never tell a credential was failing
+ // to validate due to network trouble. Log it (provider + reason) before
+ // returning the fail-open result.
+ console.warn(
+ `[AutoRefreshDaemon] Network error validating credential for "${providerId}" — treated as valid (fail-open), will retry next cycle`,
+ err instanceof Error ? err.message : err
+ );
return true;
} finally {
clearTimeout(timeout);
diff --git a/open-sse/services/batchProcessor.ts b/open-sse/services/batchProcessor.ts
index 578814427a..e9fe915afd 100644
--- a/open-sse/services/batchProcessor.ts
+++ b/open-sse/services/batchProcessor.ts
@@ -506,14 +506,46 @@ async function processSingleItemWithRetry(item: BatchRequestItem, apiKey: string
}
}
+// G10 (silent-stop fix): individual batch-item dispatches can hang indefinitely
+// if the upstream route stalls (no signal/timeout plumbed through). Bound each
+// item with a wall-clock timeout so a stuck item fails fast (recorded as an item
+// error) instead of freezing the whole batch loop. The orphaned dispatch keeps
+// running in the background but can no longer block the batch.
+export const BATCH_ITEM_DISPATCH_TIMEOUT_MS = 120_000;
+
+/**
+ * G10: race a promise against a wall-clock deadline. Exported for unit testing
+ * (batch dispatch is a module-internal import, so the timeout mechanism itself
+ * is verified directly here).
+ */
+export function withItemDispatchTimeout(
+ promise: Promise,
+ timeoutMs: number,
+ label: string
+): Promise {
+ let timer: ReturnType | undefined;
+ const timeoutPromise = new Promise((_, reject) => {
+ timer = setTimeout(
+ () => reject(new Error(`${label} timed out after ${timeoutMs}ms`)),
+ timeoutMs
+ );
+ });
+ return Promise.race([promise, timeoutPromise]).finally(() => {
+ if (timer) clearTimeout(timer);
+ });
+}
+
async function processSingleItem(item: BatchRequestItem, apiKey: string) {
const body = buildRequestBody(item);
-
- return await dispatch.dispatchBatchApiRequest({
- endpoint: item.url,
- body,
- apiKey,
- });
+ return withItemDispatchTimeout(
+ dispatch.dispatchBatchApiRequest({
+ endpoint: item.url,
+ body,
+ apiKey,
+ }),
+ BATCH_ITEM_DISPATCH_TIMEOUT_MS,
+ `Batch item dispatch (${item.url})`
+ );
}
export function buildRequestBody(item: BatchRequestItem) {
diff --git a/open-sse/services/chatgptTlsClient.ts b/open-sse/services/chatgptTlsClient.ts
index 57e5365726..fd7b3f4550 100644
--- a/open-sse/services/chatgptTlsClient.ts
+++ b/open-sse/services/chatgptTlsClient.ts
@@ -1,629 +1,48 @@
/**
* Browser-TLS-impersonating HTTP client for chatgpt.com.
*
- * Why this exists: ChatGPT's Cloudflare config pins `cf_clearance` to the
- * client's TLS fingerprint (JA3/JA4) + HTTP/2 SETTINGS frame ordering.
- * Node's Undici fetch presents an obvious "not a browser" handshake and
- * gets challenged with `cf-mitigated: challenge` — even with all the right
- * cookies. This module wraps `tls-client-node` (native shared library
- * built from bogdanfinn/tls-client) to send a Firefox handshake instead.
- *
- * The first call lazily starts the managed sidecar; subsequent calls reuse
- * a singleton TLSClient. Process exit hooks stop the sidecar cleanly.
+ * Thin re-export over the shared `tlsClientBase.ts` factory
+ * (`createTlsClientModule`). All provider-agnostic logic (sidecar lifecycle,
+ * streaming tail-file, proxy resolution, error classes, SSE detection) lives
+ * in the base module; this file supplies only ChatGPT-specific config and
+ * preserves the original public export surface.
*/
-import { tmpdir } from "node:os";
-import { join } from "node:path";
-import { mkdtemp, open, unlink, rmdir, stat, readFile } from "node:fs/promises";
-import { randomUUID } from "node:crypto";
-import { buildNativeTlsClientOptions } from "./tlsClientDownloadDir.ts";
+import {
+ createTlsClientModule,
+ type TlsFetchOptions,
+ type TlsFetchResult,
+} from "./tlsClientBase.ts";
-let clientPromise: Promise | null = null;
-let exitHookInstalled = false;
-
-const CHATGPT_PROFILE = "firefox_148"; // matches the Firefox 148 UA we send
const DEFAULT_TIMEOUT_MS =
Number.parseInt(process.env.OMNIROUTE_CHATGPT_TLS_TIMEOUT_MS || "", 10) || 60_000;
-// Grace period added to the binding's wire-level timeout before our JS-level
-// hard timeout fires. Under healthy operation `tls-client-node` honors
-// `timeoutMilliseconds` and rejects on its own; the JS-level race only wins
-// when the koffi-loaded native library is wedged (which the binding's own
-// timer can't escape). Keep the grace small so users don't wait noticeably
-// longer than the configured timeout when the binding is dead.
const HARD_TIMEOUT_GRACE_MS =
Number.parseInt(process.env.OMNIROUTE_CHATGPT_TLS_GRACE_MS || "", 10) || 10_000;
const STREAM_FIRST_BYTE_TIMEOUT_MS =
Number.parseInt(process.env.OMNIROUTE_CHATGPT_STREAM_FIRST_BYTE_TIMEOUT_MS || "", 10) || 30_000;
-function installExitHook(): void {
- if (exitHookInstalled) return;
- exitHookInstalled = true;
- const stop = async () => {
- if (!clientPromise) return;
- try {
- const c = (await clientPromise) as { stop?: () => Promise };
- await c.stop?.();
- } catch {
- // ignore
- }
- };
- process.once("beforeExit", stop);
- process.once("SIGINT", () => {
- void stop();
- });
- process.once("SIGTERM", () => {
- void stop();
- });
-}
+export const tlsClientModule = createTlsClientModule({
+ providerName: "ChatGPT",
+ tlsProfile: "firefox_148",
+ domain: "https://chatgpt.com",
+ tempDirPrefix: "cgpt-stream-",
+ tailFileVariant: "A",
+ responseValidation: "sse",
+ exportCloudflareCheck: false,
+ exposeStreamingForTesting: true,
+ defaultTimeoutMs: DEFAULT_TIMEOUT_MS,
+ hardTimeoutGraceMs: HARD_TIMEOUT_GRACE_MS,
+ firstByteTimeoutMs: STREAM_FIRST_BYTE_TIMEOUT_MS,
+});
-/**
- * Drop the cached client so the next `getClient()` call respawns it. Called
- * when a request observes the native binding has wedged — releasing the
- * reference lets a fresh TLSClient (and a fresh koffi load) take over without
- * a process restart.
- */
-function resetClientCache(): void {
- clientPromise = null;
-}
-
-export class TlsClientHangError extends Error {
- constructor(message: string) {
- super(message);
- this.name = "TlsClientHangError";
- }
-}
-
-/**
- * Race a `client.request()` promise against (a) a JS-level hard timeout and
- * (b) the caller's abort signal. The native binding's `timeoutMilliseconds`
- * already covers the wire path; this guards the case where the koffi binding
- * itself deadlocks (observed after sustained load), where neither the
- * binding's own timer nor a post-call `signal.aborted` re-check can recover.
- */
-async function raceWithTimeout(
- promise: Promise,
- timeoutMs: number,
- signal: AbortSignal | null | undefined
-): Promise {
- let timer: ReturnType | null = null;
- let abortListener: (() => void) | null = null;
- try {
- const racers: Promise[] = [
- promise,
- new Promise((_, reject) => {
- timer = setTimeout(() => {
- reject(
- new TlsClientHangError(
- `tls-client-node call exceeded ${timeoutMs}ms — native binding likely deadlocked`
- )
- );
- }, timeoutMs);
- }),
- ];
- if (signal) {
- racers.push(
- new Promise((_, reject) => {
- if (signal.aborted) {
- reject(makeAbortError(signal));
- return;
- }
- abortListener = () => reject(makeAbortError(signal));
- signal.addEventListener("abort", abortListener, { once: true });
- })
- );
- }
- return await Promise.race(racers);
- } finally {
- if (timer) clearTimeout(timer);
- if (signal && abortListener) signal.removeEventListener("abort", abortListener);
- }
-}
-
-async function getClient(): Promise<{
- request: (url: string, opts: Record) => Promise;
-}> {
- if (!clientPromise) {
- clientPromise = (async () => {
- try {
- const mod = await import("tls-client-node");
- const TLSClient = (mod as { TLSClient: new (opts?: Record) => unknown })
- .TLSClient;
- // Native mode loads the shared library directly via koffi, avoiding the
- // managed sidecar's localhost HTTP calls that OmniRoute's global fetch
- // proxy patch interferes with.
- const client = new TLSClient(buildNativeTlsClientOptions()) as {
- start: () => Promise;
- request: (url: string, opts: Record) => Promise;
- };
- await client.start();
-
- installExitHook();
- return client;
- } catch (err) {
- clientPromise = null;
- const msg = err instanceof Error ? err.message : String(err);
- throw new TlsClientUnavailableError(
- `TLS impersonation client failed to start: ${msg}. ` +
- `Verify tls-client-node is installed and its native binary downloaded.`
- );
- }
- })();
- }
- return clientPromise as Promise<{
- request: (url: string, opts: Record) => Promise;
- }>;
-}
-
-interface TlsResponseLike {
- status: number;
- headers: Record;
- body: string; // for non-streaming requests, the full response body
- cookies?: Record;
- text: () => Promise;
- bytes: () => Promise;
- json: () => Promise;
-}
-
-export class TlsClientUnavailableError extends Error {
- constructor(message: string) {
- super(message);
- this.name = "TlsClientUnavailableError";
- }
-}
-
-export interface TlsFetchOptions {
- method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
- headers?: Record;
- body?: string;
- timeoutMs?: number;
- signal?: AbortSignal | null;
- /**
- * If true, the response body is streamed to a temp file and exposed as a
- * ReadableStream. Use for SSE responses (the conversation
- * endpoint). Otherwise, the full body is read into memory.
- */
- stream?: boolean;
- /** EOF marker the upstream sends to signal end of stream (default: "[DONE]"). */
- streamEofSymbol?: string;
- /**
- * If true, instructs the underlying tls-client to return the response body
- * as a base64 `data:;base64,...` string (so binary payloads survive
- * the JSON marshalling step). Required for image / binary downloads —
- * without it, raw bytes get UTF-8-decoded and any non-ASCII byte is
- * mangled. Default false (text mode).
- */
- byteResponse?: boolean;
- /**
- * Optional upstream proxy URL (`http://user:pass@host:port` or
- * `socks5://...`). When set, the request is tunneled through this proxy
- * before reaching chatgpt.com. Required for hosts whose bare IP is
- * flagged by ChatGPT/Cloudflare (Russia, datacenter ranges, etc.) —
- * without it, every call leaks the host IP and gets edge-rejected with
- * a templated 401 / `Invalid session cookie`.
- *
- * Resolution order:
- * 1. `options.proxyUrl` (per-call override from caller)
- * 2. `process.env.OMNIROUTE_TLS_PROXY_URL` (single-flag opt-in)
- * 3. `process.env.HTTPS_PROXY` / `HTTP_PROXY` / `ALL_PROXY` (POSIX-standard fallback)
- *
- * The native `tls-client-node` binding does **not** consult Go's
- * `http.ProxyFromEnvironment`, so the env vars need to be plumbed in
- * here at the JS layer. The dashboard's global-fetch monkey-patch only
- * reaches Node's undici, not the koffi-loaded shared library used here.
- */
- proxyUrl?: string;
-}
-
-import { resolveProxyForRequest } from "../utils/proxyFetch.ts";
-import { resolveTlsClientProxyUrl } from "./tlsClientProxy.ts";
-
-/**
- * Resolve the proxy URL for a tls-client request. Per-call value wins;
- * otherwise we use the standard proxy fetch resolution which reads from
- * the dashboard AsyncLocalStorage context or falls back to env vars.
- *
- * Fail-closed: if resolution throws (e.g. a configured socks5 proxy with
- * ENABLE_SOCKS5_PROXY=false), this rethrows rather than returning undefined —
- * undefined would let the native binding connect directly and leak the real IP.
- */
-function resolveProxyUrl(perCall: string | undefined): string | undefined {
- return resolveTlsClientProxyUrl("https://chatgpt.com", perCall, resolveProxyForRequest);
-}
-
-export interface TlsFetchResult {
- status: number;
- headers: Headers;
- /** Full response body as text — only populated for non-streaming requests. */
- text: string | null;
- /** Streaming body — only populated when options.stream === true. */
- body: ReadableStream | null;
-}
-
-// Test-only injection point. Tests call __setTlsFetchOverrideForTesting()
-// to replace the real TLS client with a mock; production never touches this.
-let testOverride: ((url: string, options: TlsFetchOptions) => Promise) | null =
- null;
-
-export function __setTlsFetchOverrideForTesting(fn: typeof testOverride): void {
- testOverride = fn;
-}
-
-/**
- * Make a single HTTP request to chatgpt.com with a Firefox-like TLS fingerprint.
- *
- * Throws TlsClientUnavailableError if the native binary failed to load.
- */
-export async function tlsFetchChatGpt(
+export const tlsFetchChatGpt = (
url: string,
options: TlsFetchOptions = {}
-): Promise {
- if (testOverride) return testOverride(url, options);
- // Honor abort signals up-front. tls-client-node's koffi binding doesn't
- // accept an AbortSignal mid-flight (the binary call is opaque), so the best
- // we can do is bail before issuing the call. We also re-check after — if
- // the caller aborted while the upstream was running, throw rather than
- // returning a stale response so the caller doesn't try to use it.
- if (options.signal?.aborted) {
- throw makeAbortError(options.signal);
- }
- const client = await getClient();
- if (options.signal?.aborted) {
- throw makeAbortError(options.signal);
- }
+): Promise => tlsClientModule.tlsFetch(url, options);
+export const __tlsFetchStreamingForTesting = tlsClientModule.__tlsFetchStreamingForTesting;
- const requestOptions: Record = {
- method: options.method || "GET",
- headers: options.headers || {},
- body: options.body,
- tlsClientIdentifier: CHATGPT_PROFILE,
- timeoutMilliseconds: options.timeoutMs ?? DEFAULT_TIMEOUT_MS,
- followRedirects: true,
- withRandomTLSExtensionOrder: true,
- isByteResponse: options.byteResponse === true,
- // Plumb the configured proxy through to the native binding. tls-client-node
- // consults `proxyUrl` in the per-call options (it does NOT auto-pick up
- // HTTP_PROXY / HTTPS_PROXY env), so callers / env have to be threaded in
- // explicitly. See `resolveProxyUrl()` for the lookup order. Without this
- // line, every chatgpt-web call egresses with the bare host IP regardless
- // of dashboard proxy config — see #2022.
- proxyUrl: resolveProxyUrl(options.proxyUrl),
- };
+export const __setTlsFetchOverrideForTesting = tlsClientModule.__setTlsFetchOverrideForTesting;
- if (options.stream) {
- return await tlsFetchStreaming(
- client,
- url,
- requestOptions,
- options.streamEofSymbol,
- options.signal ?? null,
- (options.timeoutMs ?? DEFAULT_TIMEOUT_MS) + HARD_TIMEOUT_GRACE_MS,
- STREAM_FIRST_BYTE_TIMEOUT_MS
- );
- }
-
- let tlsResponse: TlsResponseLike;
- try {
- tlsResponse = await raceWithTimeout(
- client.request(url, requestOptions),
- (options.timeoutMs ?? DEFAULT_TIMEOUT_MS) + HARD_TIMEOUT_GRACE_MS,
- options.signal ?? null
- );
- } catch (err) {
- if (err instanceof TlsClientHangError) {
- // The native binding is wedged — drop the singleton so the next
- // request respawns a fresh client (and a fresh koffi load).
- resetClientCache();
- }
- throw err;
- }
- if (options.signal?.aborted) {
- throw makeAbortError(options.signal);
- }
- return {
- status: tlsResponse.status,
- headers: toHeaders(tlsResponse.headers),
- text: tlsResponse.body,
- body: null,
- };
-}
-
-function makeAbortError(signal: AbortSignal): Error {
- const reason = signal.reason;
- if (reason instanceof Error) return reason;
- const err = new Error(typeof reason === "string" ? reason : "The operation was aborted");
- err.name = "AbortError";
- return err;
-}
-
-function toHeaders(raw: Record): Headers {
- const h = new Headers();
- for (const [k, vs] of Object.entries(raw || {})) {
- for (const v of vs) h.append(k, v);
- }
- return h;
-}
-
-// ─── Streaming via temp file ────────────────────────────────────────────────
-// tls-client-node's streaming primitive writes the response body chunk-by-chunk
-// to a file path, terminating when the upstream sends `streamOutputEOFSymbol`.
-// We tail the file from a worker and surface the bytes as a ReadableStream.
-
-async function tlsFetchStreaming(
- client: { request: (url: string, opts: Record) => Promise },
- url: string,
- requestOptions: Record,
- eofSymbol = "[DONE]",
- signal: AbortSignal | null = null,
- hardTimeoutMs: number = DEFAULT_TIMEOUT_MS + HARD_TIMEOUT_GRACE_MS,
- firstByteTimeoutMs: number = STREAM_FIRST_BYTE_TIMEOUT_MS
-): Promise {
- const dir = await mkdtemp(join(tmpdir(), "cgpt-stream-"));
- const path = join(dir, `${randomUUID()}.sse`);
-
- const streamOpts = {
- ...requestOptions,
- streamOutputPath: path,
- streamOutputBlockSize: 1024,
- streamOutputEOFSymbol: eofSymbol,
- };
-
- // Kick off the request without awaiting — tls-client writes the body to
- // `path` chunk-by-chunk while the call runs. The Promise resolves when the
- // request fully completes (full body written). Wrapping in raceWithTimeout
- // guarantees this promise eventually settles even if the koffi binding
- // wedges; on hang we reset the singleton so the next request respawns.
- let resetOnHang = true;
- const requestPromise = raceWithTimeout(
- client.request(url, streamOpts),
- hardTimeoutMs,
- signal
- ).catch((err: unknown) => {
- if (resetOnHang && err instanceof TlsClientHangError) {
- resetClientCache();
- resetOnHang = false;
- }
- // Re-throw so downstream consumers (waitForContent, tailFile) observe
- // the rejection and surface it instead of treating the stream as having
- // ended cleanly.
- throw err;
- });
-
- // Wait for the file to exist AND have at least one byte. tls-client-node
- // creates the output file when the request starts, but the file can be
- // empty for a brief window before the first body chunk lands — peeking
- // during that window would return "" and misclassify the response as
- // non-SSE, dropping us into the buffered-wait branch and silently turning
- // a streaming request into a buffered one. Waiting for content avoids
- // that race; if the request actually fails before producing any bytes,
- // the timeout falls through to the requestPromise drain below (returning
- // the real upstream status).
- const ready = await waitForContent(path, firstByteTimeoutMs, requestPromise);
- if (!ready) {
- const r = await requestPromise.catch(
- (e) => ({ status: 502, headers: {}, body: String(e) }) as TlsResponseLike
- );
- // If the first byte arrived after our first-byte wait but before the
- // request settled, tls-client-node may have written the full SSE body to
- // streamOutputPath while leaving r.body empty. Prefer those captured bytes
- // over misclassifying a successful delayed stream as "empty response body".
- const fileText = await readTextFileIfExists(path);
- await cleanupTempPath(path);
- return {
- status: r.status,
- headers: toHeaders(r.headers),
- text: fileText || r.body,
- body: null,
- };
- }
-
- // Peek the first bytes to decide whether this looks like SSE. Anything
- // that doesn't positively look like SSE (JSON `{...}`, HTML `<...>`, plain
- // text rate-limit messages, Cloudflare challenge pages, etc.) gets surfaced
- // as a non-streaming response so the executor sees the real upstream status
- // and body — otherwise non-2xx error pages get silently treated as 200 OK
- // and the SSE parser produces an empty completion.
- const peek = await readFirstBytes(path, 256);
- if (!looksLikeSse(peek)) {
- const r = await requestPromise.catch(
- (e) => ({ status: 502, headers: {}, body: String(e) }) as TlsResponseLike
- );
- const fileText = await readTextFileIfExists(path);
- await cleanupTempPath(path);
- return {
- status: r.status,
- headers: toHeaders(r.headers),
- text: r.body || fileText,
- body: null,
- };
- }
-
- // Looks like SSE — start tailing. SSE bodies in practice are always 2xx;
- // tls-client-node doesn't expose response status separately from full-body
- // completion, so we report 200 and let the SSE parser consume the stream.
- const stream = tailFile(path, eofSymbol, requestPromise, signal);
- const headers = new Headers({
- "Content-Type": "text/event-stream",
- "Cache-Control": "no-cache",
- });
- return { status: 200, headers, text: null, body: stream };
-}
-
-/**
- * Returns true if the peeked response body looks like an SSE stream — i.e.,
- * begins (after any leading whitespace) with one of the SSE field markers
- * (`data:`, `event:`, `id:`, `retry:`) or a comment line (`:`).
- *
- * Exported for tests.
- */
-export function looksLikeSse(text: string): boolean {
- const trimmed = text.replace(/^[\s\r\n]+/, "");
- if (!trimmed) return false;
- if (trimmed.startsWith(":")) return true;
- return /^(data|event|id|retry):/i.test(trimmed);
-}
-
-async function cleanupTempPath(path: string): Promise {
- await unlink(path).catch(() => {});
- const dir = path.substring(0, path.lastIndexOf("/"));
- await rmdir(dir).catch(() => {});
-}
-
-async function readTextFileIfExists(path: string): Promise {
- try {
- return await readFile(path, "utf8");
- } catch {
- return "";
- }
-}
-
-export async function __tlsFetchStreamingForTesting(
- client: { request: (url: string, opts: Record) => Promise },
- url: string,
- requestOptions: Record,
- eofSymbol = "[DONE]",
- signal: AbortSignal | null = null,
- hardTimeoutMs: number = DEFAULT_TIMEOUT_MS + HARD_TIMEOUT_GRACE_MS,
- firstByteTimeoutMs: number = STREAM_FIRST_BYTE_TIMEOUT_MS
-): Promise {
- return tlsFetchStreaming(
- client as { request: (url: string, opts: Record) => Promise },
- url,
- requestOptions,
- eofSymbol,
- signal,
- hardTimeoutMs,
- firstByteTimeoutMs
- );
-}
-
-async function readFirstBytes(path: string, n: number): Promise {
- const fd = await open(path, "r");
- try {
- const buf = Buffer.alloc(n);
- const { bytesRead } = await fd.read(buf, 0, n, 0);
- return buf.subarray(0, bytesRead).toString("utf8");
- } finally {
- await fd.close().catch(() => {});
- }
-}
-
-/**
- * Wait for the streaming output file to exist AND contain at least one byte.
- * Returns false if the request settles before any bytes arrive (so the caller
- * can drain `requestPromise` and surface the real upstream status). Returns
- * true as soon as the file has data — even one byte is enough for the SSE
- * heuristic to give a useful answer.
- */
-async function waitForContent(
- path: string,
- timeoutMs: number,
- requestPromise: Promise
-): Promise {
- let requestSettled = false;
- requestPromise.then(
- () => {
- requestSettled = true;
- },
- () => {
- requestSettled = true;
- }
- );
- const start = Date.now();
- while (Date.now() - start < timeoutMs) {
- try {
- const s = await stat(path);
- if (s.size > 0) return true;
- } catch {
- // file doesn't exist yet
- }
- // If the request finished without producing any bytes, no point waiting
- // out the rest of the timeout — let the caller drain it.
- if (requestSettled) return false;
- await sleep(25);
- }
- return false;
-}
-
-function tailFile(
- path: string,
- eofSymbol: string,
- done: Promise,
- signal: AbortSignal | null = null
-): ReadableStream {
- return new ReadableStream({
- async start(controller) {
- const fd = await open(path, "r");
- const buf = Buffer.alloc(64 * 1024);
- let offset = 0;
- let finished = false;
- let aborted = false;
- let upstreamError: Error | null = null;
-
- // Track request settlement, capturing both fulfillment and rejection.
- // Without the rejection branch, a mid-stream tls-client-node error
- // becomes an unhandledRejection — the stream cleans up silently and
- // the consumer sees what looks like a successful truncated response.
- done.then(
- () => {
- finished = true;
- },
- (err) => {
- upstreamError = err instanceof Error ? err : new Error(String(err));
- finished = true;
- }
- );
-
- // If the caller aborts, stop tailing immediately.
- const onAbort = () => {
- aborted = true;
- };
- if (signal) {
- if (signal.aborted) aborted = true;
- else signal.addEventListener("abort", onAbort, { once: true });
- }
-
- let errored = false;
- try {
- while (!aborted) {
- const { bytesRead } = await fd.read(buf, 0, buf.length, offset);
- if (bytesRead > 0) {
- const chunk = buf.subarray(0, bytesRead);
- offset += bytesRead;
- const text = chunk.toString("utf8");
- if (text.includes(eofSymbol)) {
- const cutAt = text.indexOf(eofSymbol) + eofSymbol.length;
- controller.enqueue(new Uint8Array(chunk.subarray(0, cutAt)));
- break;
- }
- controller.enqueue(new Uint8Array(chunk));
- } else if (finished) {
- // No more data and request completed. If the request rejected,
- // surface the error so the consumer doesn't think the stream
- // ended cleanly.
- if (upstreamError) {
- controller.error(upstreamError);
- errored = true;
- }
- break;
- } else {
- await sleep(25);
- }
- }
- } catch (err) {
- controller.error(err);
- errored = true;
- } finally {
- if (signal) signal.removeEventListener("abort", onAbort);
- await fd.close().catch(() => {});
- await unlink(path).catch(() => {});
- const dir = path.substring(0, path.lastIndexOf("/"));
- await rmdir(dir).catch(() => {});
- if (!errored) controller.close();
- }
- },
- });
-}
-
-function sleep(ms: number): Promise {
- return new Promise((r) => setTimeout(r, ms));
-}
+export { TlsClientHangError, TlsClientUnavailableError } from "./tlsClientBase.ts";
+export type { TlsFetchOptions, TlsFetchResult } from "./tlsClientBase.ts";
+export { looksLikeSse } from "./tlsClientBase.ts";
diff --git a/open-sse/services/claudeTlsClient.ts b/open-sse/services/claudeTlsClient.ts
index eb57220da8..4ab4746195 100644
--- a/open-sse/services/claudeTlsClient.ts
+++ b/open-sse/services/claudeTlsClient.ts
@@ -1,617 +1,49 @@
/**
* Browser-TLS-impersonating HTTP client for claude.ai.
*
- * Why this exists: Claude's Cloudflare config pins `cf_clearance` to the
- * client's TLS fingerprint (JA3/JA4) + HTTP/2 SETTINGS frame ordering.
- * Node's Undici fetch presents an obvious "not a browser" handshake and
- * gets challenged with `cf-mitigated: challenge` — even with all the right
- * cookies. This module wraps `tls-client-node` (native shared library
- * built from bogdanfinn/tls-client) to send a Chrome handshake instead.
- *
- * The first call lazily starts the managed sidecar; subsequent calls reuse
- * a singleton TLSClient. Process exit hooks stop the sidecar cleanly.
+ * Thin re-export over the shared `tlsClientBase.ts` factory
+ * (`createTlsClientModule`). All provider-agnostic logic (sidecar lifecycle,
+ * streaming tail-file, proxy resolution, error classes, SSE detection) lives
+ * in the base module; this file supplies only Claude-specific config and
+ * preserves the original public export surface.
*/
-import { tmpdir } from "node:os";
-import { join } from "node:path";
-import { mkdtemp, open, unlink, rmdir, stat } from "node:fs/promises";
-import { randomUUID } from "node:crypto";
-import { buildNativeTlsClientOptions } from "./tlsClientDownloadDir.ts";
-
-let clientPromise: Promise | null = null;
-let exitHookInstalled = false;
+import {
+ createTlsClientModule,
+ type TlsFetchOptions,
+ type TlsFetchResult,
+} from "./tlsClientBase.ts";
export const CLAUDE_TLS_BROWSER_MAJOR_VERSION = "146";
-const CLAUDE_PROFILE = `chrome_${CLAUDE_TLS_BROWSER_MAJOR_VERSION}`;
+
const DEFAULT_TIMEOUT_MS =
Number.parseInt(process.env.OMNIROUTE_CLAUDE_TLS_TIMEOUT_MS || "", 10) || 60_000;
-// Grace period added to the binding's wire-level timeout before our JS-level
-// hard timeout fires. Under healthy operation `tls-client-node` honors
-// `timeoutMilliseconds` and rejects on its own; the JS-level race only wins
-// when the koffi-loaded native library is wedged (which the binding's own
-// timer can't escape). Keep the grace small so users don't wait noticeably
-// longer than the configured timeout when the binding is dead.
const HARD_TIMEOUT_GRACE_MS =
Number.parseInt(process.env.OMNIROUTE_CLAUDE_TLS_GRACE_MS || "", 10) || 10_000;
-function installExitHook(): void {
- if (exitHookInstalled) return;
- exitHookInstalled = true;
- const stop = async () => {
- if (!clientPromise) return;
- try {
- const c = (await clientPromise) as { stop?: () => Promise };
- await c.stop?.();
- } catch {
- // ignore
- }
- };
- process.once("beforeExit", stop);
- process.once("SIGINT", () => {
- void stop();
- });
- process.once("SIGTERM", () => {
- void stop();
- });
-}
+export const tlsClientModule = createTlsClientModule({
+ providerName: "Claude",
+ tlsProfile: `chrome_${CLAUDE_TLS_BROWSER_MAJOR_VERSION}`,
+ domain: "https://claude.ai",
+ tempDirPrefix: "cgpt-stream-",
+ tailFileVariant: "A",
+ responseValidation: "sse",
+ exportCloudflareCheck: false,
+ exposeStreamingForTesting: true,
+ // Claude waits indefinitely for the first SSE byte (original 2-arg waitForContent).
+ defaultTimeoutMs: DEFAULT_TIMEOUT_MS,
+ hardTimeoutGraceMs: HARD_TIMEOUT_GRACE_MS,
+ firstByteTimeoutMs: Number.POSITIVE_INFINITY,
+});
-/**
- * Drop the cached client so the next `getClient()` call respawns it. Called
- * when a request observes the native binding has wedged — releasing the
- * reference lets a fresh TLSClient (and a fresh koffi load) take over without
- * a process restart.
- */
-function resetClientCache(): void {
- clientPromise = null;
-}
-
-export class TlsClientHangError extends Error {
- constructor(message: string) {
- super(message);
- this.name = "TlsClientHangError";
- }
-}
-
-/**
- * Race a `client.request()` promise against (a) a JS-level hard timeout and
- * (b) the caller's abort signal. The native binding's `timeoutMilliseconds`
- * already covers the wire path; this guards the case where the koffi binding
- * itself deadlocks (observed after sustained load), where neither the
- * binding's own timer nor a post-call `signal.aborted` re-check can recover.
- */
-async function raceWithTimeout(
- promise: Promise,
- timeoutMs: number,
- signal: AbortSignal | null | undefined
-): Promise {
- let timer: ReturnType | null = null;
- let abortListener: (() => void) | null = null;
- try {
- const racers: Promise[] = [
- promise,
- new Promise((_, reject) => {
- timer = setTimeout(() => {
- reject(
- new TlsClientHangError(
- `tls-client-node call exceeded ${timeoutMs}ms — native binding likely deadlocked`
- )
- );
- }, timeoutMs);
- }),
- ];
- if (signal) {
- racers.push(
- new Promise((_, reject) => {
- if (signal.aborted) {
- reject(makeAbortError(signal));
- return;
- }
- abortListener = () => reject(makeAbortError(signal));
- signal.addEventListener("abort", abortListener, { once: true });
- })
- );
- }
- return await Promise.race(racers);
- } finally {
- if (timer) clearTimeout(timer);
- if (signal && abortListener) signal.removeEventListener("abort", abortListener);
- }
-}
-
-async function getClient(): Promise<{
- request: (url: string, opts: Record) => Promise;
-}> {
- if (!clientPromise) {
- clientPromise = (async () => {
- try {
- const mod = await import("tls-client-node");
- const TLSClient = (mod as { TLSClient: new (opts?: Record) => unknown })
- .TLSClient;
- // Native mode loads the shared library directly via koffi, avoiding the
- // managed sidecar's localhost HTTP calls that OmniRoute's global fetch
- // proxy patch interferes with.
- const client = new TLSClient(buildNativeTlsClientOptions()) as {
- start: () => Promise;
- request: (url: string, opts: Record) => Promise;
- };
- await client.start();
-
- installExitHook();
- return client;
- } catch (err) {
- clientPromise = null;
- const msg = err instanceof Error ? err.message : String(err);
- throw new TlsClientUnavailableError(
- `TLS impersonation client failed to start: ${msg}. ` +
- `Verify tls-client-node is installed and its native binary downloaded.`
- );
- }
- })();
- }
- return clientPromise as Promise<{
- request: (url: string, opts: Record) => Promise;
- }>;
-}
-
-interface TlsResponseLike {
- status: number;
- headers: Record;
- body: string; // for non-streaming requests, the full response body
- cookies?: Record;
- text: () => Promise;
- bytes: () => Promise;
- json: () => Promise;
-}
-
-export class TlsClientUnavailableError extends Error {
- constructor(message: string) {
- super(message);
- this.name = "TlsClientUnavailableError";
- }
-}
-
-export interface TlsFetchOptions {
- method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
- headers?: Record;
- body?: string;
- timeoutMs?: number;
- signal?: AbortSignal | null;
- /**
- * If true, the response body is streamed to a temp file and exposed as a
- * ReadableStream. Use for SSE responses (the conversation
- * endpoint). Otherwise, the full body is read into memory.
- */
- stream?: boolean;
- /** EOF marker the upstream sends to signal end of stream (default: "[DONE]"). */
- streamEofSymbol?: string;
- /**
- * If true, instructs the underlying tls-client to return the response body
- * as a base64 `data:;base64,...` string (so binary payloads survive
- * the JSON marshalling step). Required for image / binary downloads —
- * without it, raw bytes get UTF-8-decoded and any non-ASCII byte is
- * mangled. Default false (text mode).
- */
- byteResponse?: boolean;
- /**
- * Optional upstream proxy URL (`http://user:pass@host:port` or
- * `socks5://...`). When set, the request is tunneled through this proxy
- * before reaching claude.ai. Required for hosts whose bare IP is
- * flagged by Claude/Cloudflare (Russia, datacenter ranges, etc.) —
- * without it, every call leaks the host IP and gets edge-rejected with
- * a templated 401 / `Invalid session cookie`.
- *
- * Resolution order:
- * 1. `options.proxyUrl` (per-call override from caller)
- * 2. `process.env.OMNIROUTE_TLS_PROXY_URL` (single-flag opt-in)
- * 3. `process.env.HTTPS_PROXY` / `HTTP_PROXY` / `ALL_PROXY` (POSIX-standard fallback)
- *
- * The native `tls-client-node` binding does **not** consult Go's
- * `http.ProxyFromEnvironment`, so the env vars need to be plumbed in
- * here at the JS layer. The dashboard's global-fetch monkey-patch only
- * reaches Node's undici, not the koffi-loaded shared library used here.
- */
- proxyUrl?: string;
-}
-
-import { resolveProxyForRequest } from "../utils/proxyFetch.ts";
-import { resolveTlsClientProxyUrl } from "./tlsClientProxy.ts";
-
-/**
- * Resolve the proxy URL for a tls-client request. Per-call value wins;
- * otherwise we use the standard proxy fetch resolution which reads from
- * the dashboard AsyncLocalStorage context or falls back to env vars.
- *
- * Fail-closed: if resolution throws (e.g. a configured socks5 proxy with
- * ENABLE_SOCKS5_PROXY=false), this rethrows rather than returning undefined —
- * undefined would let the native binding connect directly and leak the real IP.
- */
-function resolveProxyUrl(perCall: string | undefined): string | undefined {
- return resolveTlsClientProxyUrl("https://claude.ai", perCall, resolveProxyForRequest);
-}
-
-export interface TlsFetchResult {
- status: number;
- headers: Headers;
- /** Full response body as text — only populated for non-streaming requests. */
- text: string | null;
- /** Streaming body — only populated when options.stream === true. */
- body: ReadableStream | null;
-}
-
-// Test-only injection point. Tests call __setTlsFetchOverrideForTesting()
-// to replace the real TLS client with a mock; production never touches this.
-let testOverride: ((url: string, options: TlsFetchOptions) => Promise) | null =
- null;
-
-export function __setTlsFetchOverrideForTesting(fn: typeof testOverride): void {
- testOverride = fn;
-}
-
-/**
- * Make a single HTTP request to claude.ai with the configured Chrome TLS profile.
- *
- * Throws TlsClientUnavailableError if the native binary failed to load.
- */
-export async function tlsFetchClaude(
+export const tlsFetchClaude = (
url: string,
options: TlsFetchOptions = {}
-): Promise {
- if (testOverride) return testOverride(url, options);
- // Honor abort signals up-front. tls-client-node's koffi binding doesn't
- // accept an AbortSignal mid-flight (the binary call is opaque), so the best
- // we can do is bail before issuing the call. We also re-check after — if
- // the caller aborted while the upstream was running, throw rather than
- // returning a stale response so the caller doesn't try to use it.
- if (options.signal?.aborted) {
- throw makeAbortError(options.signal);
- }
- const client = await getClient();
- if (options.signal?.aborted) {
- throw makeAbortError(options.signal);
- }
+): Promise => tlsClientModule.tlsFetch(url, options);
+export const tlsFetchStreaming = tlsClientModule.__tlsFetchStreamingForTesting;
- const requestOptions: Record = {
- method: options.method || "GET",
- headers: options.headers || {},
- body: options.body,
- tlsClientIdentifier: CLAUDE_PROFILE,
- timeoutMilliseconds: options.timeoutMs ?? DEFAULT_TIMEOUT_MS,
- followRedirects: true,
- withRandomTLSExtensionOrder: true,
- isByteResponse: options.byteResponse === true,
- // Plumb the configured proxy through to the native binding. tls-client-node
- // consults `proxyUrl` in the per-call options (it does NOT auto-pick up
- // HTTP_PROXY / HTTPS_PROXY env), so callers / env have to be threaded in
- // explicitly. See `resolveProxyUrl()` for the lookup order. Without this
- // line, every chatgpt-web call egresses with the bare host IP regardless
- // of dashboard proxy config — see #2022.
- proxyUrl: resolveProxyUrl(options.proxyUrl),
- };
+export const __setTlsFetchOverrideForTesting = tlsClientModule.__setTlsFetchOverrideForTesting;
- if (options.stream) {
- return await tlsFetchStreaming(
- client,
- url,
- requestOptions,
- options.streamEofSymbol,
- options.signal ?? null,
- (options.timeoutMs ?? DEFAULT_TIMEOUT_MS) + HARD_TIMEOUT_GRACE_MS
- );
- }
-
- let tlsResponse: TlsResponseLike;
- try {
- tlsResponse = await raceWithTimeout(
- client.request(url, requestOptions),
- (options.timeoutMs ?? DEFAULT_TIMEOUT_MS) + HARD_TIMEOUT_GRACE_MS,
- options.signal ?? null
- );
- } catch (err) {
- if (err instanceof TlsClientHangError) {
- // The native binding is wedged — drop the singleton so the next
- // request respawns a fresh client (and a fresh koffi load).
- resetClientCache();
- }
- throw err;
- }
- if (options.signal?.aborted) {
- throw makeAbortError(options.signal);
- }
- return {
- status: tlsResponse.status,
- headers: toHeaders(tlsResponse.headers),
- text: tlsResponse.body,
- body: null,
- };
-}
-
-function makeAbortError(signal: AbortSignal): Error {
- const reason = signal.reason;
- if (reason instanceof Error) return reason;
- const err = new Error(typeof reason === "string" ? reason : "The operation was aborted");
- err.name = "AbortError";
- return err;
-}
-
-function toHeaders(raw: Record): Headers {
- const h = new Headers();
- for (const [k, vs] of Object.entries(raw || {})) {
- for (const v of vs) h.append(k, v);
- }
- return h;
-}
-
-// ─── Streaming via temp file ────────────────────────────────────────────────
-// tls-client-node's streaming primitive writes the response body chunk-by-chunk
-// to a file path, terminating when the upstream sends `streamOutputEOFSymbol`.
-// We tail the file from a worker and surface the bytes as a ReadableStream.
-
-// Cap for the bounded fallback read of a non-SSE error body straight from the
-// streaming temp file (mirrors the 2048-byte cap executors/claude-web.ts
-// already applies when reading error bodies) — avoids buffering an unbounded
-// error page into memory. See #7134.
-const MAX_ERROR_BODY_BYTES = 16 * 1024;
-
-/**
- * Exported for tests (issue #7134): allows injecting a fake `client` so the
- * non-SSE error-body fallback path can be exercised without
- * `--experimental-test-module-mocks`, matching the DI pattern already used
- * by `__setTlsFetchOverrideForTesting` for the outer `tlsFetchClaude`.
- */
-export async function tlsFetchStreaming(
- client: { request: (url: string, opts: Record) => Promise },
- url: string,
- requestOptions: Record,
- eofSymbol = "[DONE]",
- signal: AbortSignal | null = null,
- hardTimeoutMs: number = DEFAULT_TIMEOUT_MS + HARD_TIMEOUT_GRACE_MS
-): Promise {
- const dir = await mkdtemp(join(tmpdir(), "cgpt-stream-"));
- const path = join(dir, `${randomUUID()}.sse`);
-
- const streamOpts = {
- ...requestOptions,
- streamOutputPath: path,
- streamOutputBlockSize: 1024,
- streamOutputEOFSymbol: eofSymbol,
- };
-
- // Kick off the request without awaiting — tls-client writes the body to
- // `path` chunk-by-chunk while the call runs. The Promise resolves when the
- // request fully completes (full body written). Wrapping in raceWithTimeout
- // guarantees this promise eventually settles even if the koffi binding
- // wedges; on hang we reset the singleton so the next request respawns.
- let resetOnHang = true;
- const requestPromise = raceWithTimeout(
- client.request(url, streamOpts),
- hardTimeoutMs,
- signal
- ).catch((err: unknown) => {
- if (resetOnHang && err instanceof TlsClientHangError) {
- resetClientCache();
- resetOnHang = false;
- }
- // Re-throw so downstream consumers (waitForContent, tailFile) observe
- // the rejection and surface it instead of treating the stream as having
- // ended cleanly.
- throw err;
- });
-
- // Wait for the file to exist AND have at least one byte. tls-client-node
- // creates the output file when the request starts, but the file can be
- // empty for a brief window before the first body chunk lands — peeking
- // during that window would return "" and misclassify the response as
- // non-SSE, dropping us into the buffered-wait branch and silently turning
- // a streaming request into a buffered one. Waiting for content avoids
- // that race; if the request actually fails before producing any bytes,
- // the timeout falls through to the requestPromise drain below (returning
- // the real upstream status).
- // Do not impose a second, shorter first-byte timeout here. Opus-class
- // models can legitimately take more than five seconds before emitting the
- // first SSE event. `requestPromise` is already guarded by the configured
- // wire timeout plus the JS hard-timeout grace, so waiting until either the
- // file has data or that promise settles remains bounded.
- const ready = await waitForContent(path, requestPromise);
- if (!ready) {
- const r = await requestPromise.catch(
- (e) => ({ status: 502, headers: {}, body: String(e) }) as TlsResponseLike
- );
- await cleanupTempPath(path);
- return {
- status: r.status,
- headers: toHeaders(r.headers),
- text: r.body,
- body: null,
- };
- }
-
- // Peek the first bytes to decide whether this looks like SSE. Anything
- // that doesn't positively look like SSE (JSON `{...}`, HTML `<...>`, plain
- // text rate-limit messages, Cloudflare challenge pages, etc.) gets surfaced
- // as a non-streaming response so the executor sees the real upstream status
- // and body — otherwise non-2xx error pages get silently treated as 200 OK
- // and the SSE parser produces an empty completion.
- const peek = await readFirstBytes(path, 256);
- if (!looksLikeSse(peek)) {
- const r = await requestPromise.catch(
- (e) => ({ status: 502, headers: {}, body: String(e) }) as TlsResponseLike
- );
- // tls-client-node's `streamOutputPath` mode writes the response body to
- // the temp file chunk-by-chunk and does NOT also populate the resolved
- // response's in-memory `body` field (confirmed against
- // node_modules/tls-client-node/dist/response.js) — so for every non-SSE,
- // non-2xx claude-web response (400/403/429/500 with a real JSON/HTML
- // error), `r.body` is empty even though the real bytes are sitting in
- // `path` (we just peeked them above). Prefer `r.body` when it IS
- // populated (some native-client modes do fill it in); otherwise fall
- // back to a bounded read of the temp file so the real upstream error
- // detail reaches the caller instead of being silently discarded. #7134
- const text = r.body || (await readFirstBytes(path, MAX_ERROR_BODY_BYTES).catch(() => ""));
- await cleanupTempPath(path);
- return {
- status: r.status,
- headers: toHeaders(r.headers),
- text,
- body: null,
- };
- }
-
- // Looks like SSE — start tailing. SSE bodies in practice are always 2xx;
- // tls-client-node doesn't expose response status separately from full-body
- // completion, so we report 200 and let the SSE parser consume the stream.
- const stream = tailFile(path, eofSymbol, requestPromise, signal);
- const headers = new Headers({
- "Content-Type": "text/event-stream",
- "Cache-Control": "no-cache",
- });
- return { status: 200, headers, text: null, body: stream };
-}
-
-/**
- * Returns true if the peeked response body looks like an SSE stream — i.e.,
- * begins (after any leading whitespace) with one of the SSE field markers
- * (`data:`, `event:`, `id:`, `retry:`) or a comment line (`:`).
- *
- * Exported for tests.
- */
-export function looksLikeSse(text: string): boolean {
- const trimmed = text.replace(/^[\s\r\n]+/, "");
- if (!trimmed) return false;
- if (trimmed.startsWith(":")) return true;
- return /^(data|event|id|retry):/i.test(trimmed);
-}
-
-async function cleanupTempPath(path: string): Promise {
- await unlink(path).catch(() => {});
- const dir = path.substring(0, path.lastIndexOf("/"));
- await rmdir(dir).catch(() => {});
-}
-
-async function readFirstBytes(path: string, n: number): Promise {
- const fd = await open(path, "r");
- try {
- const buf = Buffer.alloc(n);
- const { bytesRead } = await fd.read(buf, 0, n, 0);
- return buf.subarray(0, bytesRead).toString("utf8");
- } finally {
- await fd.close().catch(() => {});
- }
-}
-
-/**
- * Wait for the streaming output file to exist AND contain at least one byte.
- * Returns false if the request settles before any bytes arrive (so the caller
- * can drain `requestPromise` and surface the real upstream status). Returns
- * true as soon as the file has data — even one byte is enough for the SSE
- * heuristic to give a useful answer.
- */
-async function waitForContent(
- path: string,
- requestPromise: Promise
-): Promise {
- let requestSettled = false;
- requestPromise.then(
- () => {
- requestSettled = true;
- },
- () => {
- requestSettled = true;
- }
- );
- while (true) {
- try {
- const s = await stat(path);
- if (s.size > 0) return true;
- } catch {
- // file doesn't exist yet
- }
- // If the request finished without producing any bytes, no point waiting
- // out the rest of the timeout — let the caller drain it.
- if (requestSettled) return false;
- await sleep(25);
- }
-}
-
-function tailFile(
- path: string,
- eofSymbol: string,
- done: Promise,
- signal: AbortSignal | null = null
-): ReadableStream {
- return new ReadableStream({
- async start(controller) {
- const fd = await open(path, "r");
- const buf = Buffer.alloc(64 * 1024);
- let offset = 0;
- let finished = false;
- let aborted = false;
- let upstreamError: Error | null = null;
-
- // Track request settlement, capturing both fulfillment and rejection.
- // Without the rejection branch, a mid-stream tls-client-node error
- // becomes an unhandledRejection — the stream cleans up silently and
- // the consumer sees what looks like a successful truncated response.
- done.then(
- () => {
- finished = true;
- },
- (err) => {
- upstreamError = err instanceof Error ? err : new Error(String(err));
- finished = true;
- }
- );
-
- // If the caller aborts, stop tailing immediately.
- const onAbort = () => {
- aborted = true;
- };
- if (signal) {
- if (signal.aborted) aborted = true;
- else signal.addEventListener("abort", onAbort, { once: true });
- }
-
- let errored = false;
- try {
- while (!aborted) {
- const { bytesRead } = await fd.read(buf, 0, buf.length, offset);
- if (bytesRead > 0) {
- const chunk = buf.subarray(0, bytesRead);
- offset += bytesRead;
- const text = chunk.toString("utf8");
- if (text.includes(eofSymbol)) {
- const cutAt = text.indexOf(eofSymbol) + eofSymbol.length;
- controller.enqueue(new Uint8Array(chunk.subarray(0, cutAt)));
- break;
- }
- controller.enqueue(new Uint8Array(chunk));
- } else if (finished) {
- // No more data and request completed. If the request rejected,
- // surface the error so the consumer doesn't think the stream
- // ended cleanly.
- if (upstreamError) {
- controller.error(upstreamError);
- errored = true;
- }
- break;
- } else {
- await sleep(25);
- }
- }
- } catch (err) {
- controller.error(err);
- errored = true;
- } finally {
- if (signal) signal.removeEventListener("abort", onAbort);
- await fd.close().catch(() => {});
- await unlink(path).catch(() => {});
- const dir = path.substring(0, path.lastIndexOf("/"));
- await rmdir(dir).catch(() => {});
- if (!errored) controller.close();
- }
- },
- });
-}
-
-function sleep(ms: number): Promise {
- return new Promise((r) => setTimeout(r, ms));
-}
+export { TlsClientHangError, TlsClientUnavailableError } from "./tlsClientBase.ts";
+export type { TlsFetchOptions, TlsFetchResult } from "./tlsClientBase.ts";
+export { looksLikeSse } from "./tlsClientBase.ts";
diff --git a/open-sse/services/combo.ts b/open-sse/services/combo.ts
index b6ae5d534a..f01d206eca 100644
--- a/open-sse/services/combo.ts
+++ b/open-sse/services/combo.ts
@@ -36,6 +36,7 @@ import {
import { buildNoUpstreamResponseDiagnostics, buildRecoveryHint } from "./combo/pinRecovery.ts";
import { buildTargetTimeoutRunner } from "./combo/targetTimeoutRunner.ts";
import { recordComboRequest, recordComboShadowRequest, getComboMetrics } from "./comboMetrics.ts";
+import { qualityScoreFor } from "./routing/index.ts";
import {
expandComboSystemPromptIfPresent,
resolveTargetFingerprint,
@@ -45,6 +46,7 @@ import {
getDefaultComboConfig,
resolveComboQueueDepth,
isComboCooldownWaitEligible,
+ resolveComboTargetTimeoutMsForCombo,
} from "./comboConfig.ts";
import {
maybeGenerateHandoff,
@@ -63,6 +65,7 @@ import { getHiddenModelsByProvider } from "@/models";
import { resolveModelLockoutSettings } from "../../src/lib/resilience/modelLockoutSettings";
import { fetchCodexQuota } from "./codexQuotaFetcher.ts";
import { evaluateQuotaCutoff, getQuotaFetcher, type QuotaInfo } from "./quotaPreflight.ts";
+import { resolveProviderId } from "../../src/shared/constants/providers.ts";
import * as semaphore from "./rateLimitSemaphore.ts";
import { getCircuitBreaker } from "../../src/shared/utils/circuitBreaker";
import { parseModel } from "./model.ts";
@@ -80,6 +83,7 @@ import {
normalizeStickinessMessages,
recordStickyBinding,
clearStickyBinding,
+ clearStickyBindingsForCombo,
peekStickyConnectionId,
resolveDisableSessionStickiness,
} from "./combo/sessionStickiness.ts";
@@ -87,6 +91,7 @@ import { selectQuotaShareTarget } from "./combo/quotaShareStrategy.ts";
import { makeConnectionConcurrencyResolver, lookupPositiveCap } from "./combo/concurrencyCaps.ts";
import { acquireQuotaShareConcurrencySlot } from "./combo/quotaShareConcurrency.ts";
import { canAffordRequest } from "../../src/lib/quota/quotaScheduler.ts";
+import { resolveConnectionTimeoutMs } from "../handlers/chatCore/upstreamTimeouts.ts";
import { getCachedProviderConnectionById } from "../../src/lib/db/readCache.ts";
import { orderTargetsByEvalScores } from "./evalRouting.ts";
@@ -131,6 +136,7 @@ import { isProviderInCooldown, recordProviderCooldown } from "./providerCooldown
import {
resolveResilienceSettings,
type ResilienceSettings,
+ type ComboCooldownWaitSettings,
} from "../../src/lib/resilience/settings";
import { resolveReasoningBufferedMaxTokens, toPositiveInteger } from "./reasoningTokenBuffer.ts";
import { RESET_WINDOW_NAMES } from "./combo/types.ts";
@@ -139,6 +145,7 @@ import type {
ComboRetryAfter,
ComboErrorBody,
SingleModelTarget,
+ ComboLogger,
HandleComboChatOptions,
HandleRoundRobinOptions,
ResolvedComboTarget,
@@ -177,6 +184,8 @@ import {
TRANSIENT_FOR_SEMAPHORE,
MAX_FALLBACK_WAIT_MS,
MAX_GLOBAL_ATTEMPTS,
+ COMBO_LOOP_SAFETY_TIMEOUT_MS,
+ COMBO_SAFETY_DRAIN_MS,
isAllAccountsRateLimitedResponse,
clampComboDepth,
shouldSkipForPredictedTtft,
@@ -490,7 +499,10 @@ export async function buildAutoCandidates(
let quotaRemaining = 100;
let quotaCutoffBlocked = false;
let quotaCutoffReason: string | undefined;
- const fetcher = getQuotaFetcher(provider);
+ // #10877: `provider` here may be a legacy/user-facing alias spelling
+ // (target.provider/parseModel output); canonicalize before the fetcher
+ // registry lookup so aliased combo members still hit quota-aware scoring.
+ const fetcher = getQuotaFetcher(resolveProviderId(provider));
const connection = target.connectionId ? connectionById.get(target.connectionId) : undefined;
const authType = typeof connection?.authType === "string" ? connection.authType : null;
const sessionAvailability =
@@ -578,6 +590,9 @@ export async function buildAutoCandidates(
connectionPoolSize: connectionPoolCounts.get(provider) ?? 1,
connectionId: target.connectionId ?? undefined,
authType,
+ // Feedback-driven quality signal (routing quality tracker). Neutral 1.0
+ // before enough samples accumulate — a cold model is never penalized.
+ quality: qualityScoreFor(provider, model),
};
})
);
@@ -611,6 +626,40 @@ export { pinIsDurablyUnhealthy };
/** @param {string} errorText */
/** @param {object} options */
+/**
+ * Resolves the per-target timeout ceiling for a combo target: when the target's
+ * connection carries `providerSpecificData.timeoutMs`, re-runs
+ * resolveComboTargetTimeoutMsForCombo with that timeout as the ceiling so the
+ * combo's per-target timer follows the selected connection.
+ * Returns undefined when the connection or its timeout is absent — the runner
+ * then falls back to the setup-time comboTargetTimeoutMs.
+ */
+export async function resolveTargetTimeoutMsForTarget(
+ config: Record | null | undefined,
+ strategy: string,
+ comboCooldownWait: Pick,
+ target?: SingleModelTarget,
+ log?: Pick | null
+): Promise {
+ const connectionId = target && "connectionId" in target ? target.connectionId : null;
+ if (!connectionId) return undefined;
+ try {
+ const connection = await getCachedProviderConnectionById(connectionId);
+ if (!connection) return undefined;
+ const timeoutMs = resolveConnectionTimeoutMs(connection.providerSpecificData);
+ if (timeoutMs === undefined) return undefined;
+ return resolveComboTargetTimeoutMsForCombo(config, timeoutMs, strategy, comboCooldownWait);
+ } catch (err) {
+ log?.debug?.(
+ "COMBO",
+ `resolveTargetTimeoutMsForTarget connection lookup failed: ${
+ err instanceof Error ? err.message : String(err)
+ }`
+ );
+ return undefined;
+ }
+}
+
/**
* #10681 egress: every combo response carries the opaque trace id in an
* `X-OmniRoute-Combo-Trace` header so a post-incident lookup of the ordered
@@ -673,6 +722,14 @@ async function handleComboChatInner({
const handleSingleModelWithTimeout = buildTargetTimeoutRunner({
handleSingleModel,
comboTargetTimeoutMs,
+ resolveTargetTimeoutMs: (target) =>
+ resolveTargetTimeoutMsForTarget(
+ config,
+ strategy,
+ resilienceSettings.comboCooldownWait,
+ target,
+ log
+ ),
log,
});
@@ -1038,11 +1095,54 @@ async function handleComboChatInner({
const globalPromise = new Promise((res) => {
globalResolve = res;
});
+
+ // G1 (silent-stop fix): the speculative loop's `Promise.race` waits on
+ // `globalPromise`, which is ONLY resolved from inside a task (success or
+ // fatal error). If a target hangs — e.g. the operator disabled the per-model
+ // timeout (`targetTimeoutMs: 0`) and the upstream never settles — the race
+ // never resolves and the request hangs forever with no response. This safety
+ // promise force-resolves after the combo budget (comboTimeoutMs when set,
+ // otherwise a hard ceiling) so the request ALWAYS terminates with an
+ // actionable 504 instead of dying silently. `comboExpired` is flipped so the
+ // target loop stops launching new work; the existing comboExpired branch
+ // returns the aggregated 504.
+ const loopSafetyMs =
+ comboTimeoutMs > 0 ? comboTimeoutMs : COMBO_LOOP_SAFETY_TIMEOUT_MS;
+ let loopSafetyFired = false;
+ let loopSafetyTimer: ReturnType | null = null;
+ const loopSafetyPromise = new Promise((resolve) => {
+ loopSafetyTimer = setTimeout(() => {
+ loopSafetyFired = true;
+ log.warn(
+ "COMBO",
+ `Combo loop safety timeout (${loopSafetyMs}ms) reached without a terminal response — force-terminating`
+ );
+ resolve(
+ errorResponseWithComboDiagnostics(
+ 504,
+ `Combo global timeout (${loopSafetyMs}ms) without a terminal response`,
+ buildComboDiag("combo_timeout"),
+ { code: "COMBO_TIMEOUT", type: "server_error" }
+ )
+ );
+ }, loopSafetyMs);
+ loopSafetyTimer.unref?.();
+ });
const runningTasks = new Set>();
let anySuccess = false;
// #10681: steps already recorded as dispatched (so per-target retries do not
// duplicate the decision).
const dispatchedTargets = new Set();
+ // G1: flip comboExpired as soon as the safety timer fires so the next loop
+ // iteration breaks instead of launching more targets after the budget, and
+ // abort every in-flight target so a hung upstream actually gets cancelled
+ // (not just "response stops").
+ const markLoopExpiredIfSafetyFired = () => {
+ if (loopSafetyFired) {
+ comboExpired = true;
+ for (const [, ac] of abortControllers.entries()) ac.abort();
+ }
+ };
const abortControllers = new Map();
const zeroLatencyOptimizationsEnabled = config.zeroLatencyOptimizationsEnabled === true;
const hasProtectedPriorityTarget =
@@ -2271,7 +2371,10 @@ async function handleComboChatInner({
);
}
}
- log.warn("COMBO", `Model ${modelStr} failed, trying next`, { status: result.status });
+ log.warn("COMBO", `Model ${modelStr} failed, trying next`, {
+ status: result.status,
+ errorBody: redactConnectionLabel(errorText),
+ });
// #5976: per-model-quota providers (Gemini, GitHub, etc.) multiplex models
// behind one connection. A model-level 500 or 429 (RPM) must NOT cool down
@@ -2352,6 +2455,17 @@ async function handleComboChatInner({
})().catch((err) => {
const logError = log.error ?? log.warn;
logError("COMBO", `Speculative task error for target ${i}`, err);
+ // G2 (silent-stop fix): never leave the speculative loop waiting on an
+ // unresolved globalPromise. If a task throws unexpectedly (outside
+ // executeTarget's error handling) and no other task succeeds, the post-loop
+ // `Promise.race([globalPromise, ...])` would hang forever. Resolve with a
+ // 502 so the request terminates with an actionable error.
+ if (!anySuccess && globalResolve) {
+ anySuccess = true;
+ globalResolve(
+ errorResponse(502, `Combo target ${i} failed with an unexpected error`)
+ );
+ }
});
runningTasks.add(task);
@@ -2369,10 +2483,11 @@ async function handleComboChatInner({
timeoutResolve = r;
setTimeout(r, hedgeDelay);
});
- await Promise.race([task, globalPromise, timeoutPromise]);
+ await Promise.race([task, globalPromise, timeoutPromise, loopSafetyPromise]);
} else {
- await Promise.race([task, globalPromise]);
+ await Promise.race([task, globalPromise, loopSafetyPromise]);
}
+ markLoopExpiredIfSafetyFired();
// Global combo timeout check: after each target completes, stop trying
// further targets if the total elapsed time exceeds comboTimeoutMs.
@@ -2387,13 +2502,51 @@ async function handleComboChatInner({
}
if (!anySuccess && runningTasks.size > 0) {
- await Promise.race([globalPromise, Promise.all([...runningTasks])]);
+ // G1: include loopSafetyPromise so a hung last task (per-model timeout
+ // disabled) cannot freeze this post-loop race forever.
+ await Promise.race([globalPromise, Promise.all([...runningTasks]), loopSafetyPromise]);
+ markLoopExpiredIfSafetyFired();
+ }
+
+ // G1: if the safety timer won the race (request would otherwise hang), give
+ // in-flight tasks a short drain window to land their per-model errors into
+ // comboErrors so the 504 carries the same "tried: a (500)" summary the
+ // regular comboExpired branch produces — then return the safety 504.
+ if (loopSafetyFired && !anySuccess) {
+ if (runningTasks.size > 0) {
+ await Promise.race([
+ Promise.allSettled([...runningTasks]),
+ new Promise((resolve) => setTimeout(resolve, COMBO_SAFETY_DRAIN_MS)),
+ ]);
+ }
+ const summary = comboErrors
+ .slice(0, 5)
+ .map((e) => `${e.model} (${e.status})`)
+ .join(", ");
+ const msg =
+ `Combo global timeout (${loopSafetyMs}ms) after ${recordedAttempts}/${orderedTargets.length} targets` +
+ (comboErrors.length > 0
+ ? ` | tried: ${summary}${comboErrors.length > 5 ? `... (+${comboErrors.length - 5})` : ""}`
+ : "") +
+ " without a terminal response";
+ return errorResponseWithComboDiagnostics(
+ 504,
+ msg,
+ buildComboDiag("combo_timeout"),
+ { code: "COMBO_TIMEOUT", type: "server_error" }
+ );
}
// #10681: finalize the decision trace (success).
finalizeComboTrace(traceInvocationId, orderedTargets);
finishComboTrace(traceInvocationId, { status: 200 });
if (anySuccess) {
+ // G1: clear the safety timer on the happy path so a successful combo does
+ // not leave a 10-minute timer alive per request.
+ if (loopSafetyTimer) {
+ clearTimeout(loopSafetyTimer);
+ loopSafetyTimer = null;
+ }
return await globalPromise;
}
@@ -2861,6 +3014,9 @@ async function handleRoundRobinCombo({
filteredTargets = await expandPromptCacheAffinityTargets(filteredTargets);
modelCount = filteredTargets.length;
}
+ if (disableSessionStickiness) {
+ clearStickyBindingsForCombo(combo.name);
+ }
const _rrSessionSticky = disableSessionStickiness
? ({ targets: filteredTargets, messageHash: null, stuck: false } as const)
: await applySessionStickiness(
@@ -2912,6 +3068,33 @@ async function handleRoundRobinCombo({
// and the "Done with this model" path below), mirroring handleComboChat.
const rrOutcomes: Array = [];
+ // G4 (silent-stop fix): round-robin has NO global timeout — a hung model
+ // (per-model timeout disabled via targetTimeoutMs: 0) would freeze the request
+ // forever with no response. Safety promise + timer bound the whole loop; when
+ // it fires, rrExpired flips and every subsequent model attempt short-circuits
+ // to the 504. Cleaned up in the loop's finally.
+ const rrConfiguredTimeoutMs =
+ (config as { comboTimeoutMs?: number }).comboTimeoutMs ?? 0;
+ const rrLoopSafetyMs =
+ rrConfiguredTimeoutMs > 0 ? rrConfiguredTimeoutMs : COMBO_LOOP_SAFETY_TIMEOUT_MS;
+ let rrExpired = false;
+ let rrLoopSafetyTimer: ReturnType | null = null;
+ let rrResolveSafety: ((res: Response) => void) | null = null;
+ const rrSafetyPromise = new Promise((resolve) => {
+ rrResolveSafety = resolve;
+ });
+ rrLoopSafetyTimer = setTimeout(() => {
+ rrExpired = true;
+ log.warn(
+ "COMBO-RR",
+ `Round-robin loop exceeded ${rrLoopSafetyMs}ms without a terminal response — force-terminating`
+ );
+ rrResolveSafety?.(
+ errorResponse(504, `Round-robin combo exceeded ${rrLoopSafetyMs}ms without a terminal response`)
+ );
+ }, rrLoopSafetyMs);
+ rrLoopSafetyTimer.unref?.();
+
// #1731: Per-request in-memory set of providers whose quota is fully exhausted.
// When a target returns a quota-exhausted 429, remaining targets from the same
// provider are skipped to avoid the cascade through N same-provider targets.
@@ -2920,8 +3103,11 @@ async function handleRoundRobinCombo({
const transientRateLimitedProviders = new Set();
// Try each model starting from the round-robin target
- for (let offset = 0; offset < modelCount; offset++) {
- const modelIndex = (rrStartIndex + offset) % modelCount;
+ try {
+ for (let offset = 0; offset < modelCount; offset++) {
+ // G4: stop launching new work once the safety timer fired.
+ if (rrExpired) break;
+ const modelIndex = (rrStartIndex + offset) % modelCount;
const target = filteredTargets[modelIndex];
const modelStr = target.modelStr;
const provider = target.provider;
@@ -3066,11 +3252,15 @@ async function handleRoundRobinCombo({
fingerprint: resolveTargetFingerprint(target) ?? "",
});
- const result = await handleSingleModel(attemptBody, modelStr, {
- ...targetForAttempt,
- effectiveComboStrategy: "round-robin",
- failoverBeforeRetry: config.failoverBeforeRetry,
- });
+ const result = await Promise.race([
+ handleSingleModel(attemptBody, modelStr, {
+ ...targetForAttempt,
+ effectiveComboStrategy: "round-robin",
+ failoverBeforeRetry: config.failoverBeforeRetry,
+ }),
+ rrSafetyPromise,
+ ]);
+ if (rrExpired) return result; // G4: safety timer won — stop everything
// Quota-aware scheduling: reserve the estimated budget for this
// dispatch (opt-in, same env gate as the pre-request check). Best-effort
@@ -3456,7 +3646,10 @@ async function handleRoundRobinCombo({
kind: classifyComboOutcome(result.status, errorText),
});
if (offset > 0) fallbackCount++;
- log.warn("COMBO-RR", `${modelStr} failed, trying next model`, { status: result.status });
+ log.warn("COMBO-RR", `${modelStr} failed, trying next model`, {
+ status: result.status,
+ errorBody: redactConnectionLabel(errorText),
+ });
if (
resilienceSettings.providerCooldown.enabled &&
@@ -3505,6 +3698,26 @@ async function handleRoundRobinCombo({
release();
}
}
+ } catch (err) {
+ // G4: unexpected exception in the round-robin loop must never crash the
+ // request silently — surface a 500 instead of hanging the client.
+ log.error?.("COMBO-RR", "Unexpected error in round-robin loop", err);
+ return errorResponse(500, "Unexpected error in round-robin combo");
+ } finally {
+ if (rrLoopSafetyTimer) {
+ clearTimeout(rrLoopSafetyTimer);
+ rrLoopSafetyTimer = null;
+ }
+ }
+
+ // G4: if the safety timer fired between iterations (no race captured it),
+ // terminate with the actionable 504 instead of the generic exhaustion path.
+ if (rrExpired) {
+ return errorResponse(
+ 504,
+ `Round-robin combo exceeded ${rrLoopSafetyMs}ms without a terminal response`
+ );
+ }
// All models exhausted
const latencyMs = Date.now() - startTime;
diff --git a/open-sse/services/combo/comboPredicates.ts b/open-sse/services/combo/comboPredicates.ts
index dc87509236..f7cfa3322d 100644
--- a/open-sse/services/combo/comboPredicates.ts
+++ b/open-sse/services/combo/comboPredicates.ts
@@ -18,6 +18,15 @@ import type { ResolvedComboTarget } from "./types.ts";
// Status codes that should mark round-robin target semaphores as cooling down.
export const TRANSIENT_FOR_SEMAPHORE = [429, 502, 503, 504];
+// G1 (silent-stop fix): hard ceiling for the combo target loop when the operator
+// left comboTimeoutMs at 0 ("unlimited"). Without this, a hung upstream (per-model
+// timeout disabled) would freeze the request forever with no response. 10 minutes
+// is a generous bound for legitimate long-running fallback cascades.
+export const COMBO_LOOP_SAFETY_TIMEOUT_MS = 10 * 60 * 1000;
+// G1: after the safety timer fires, wait this long for in-flight targets to land
+// their per-model errors into comboErrors (so the 504 carries the same "tried:"
+// summary as the regular timeout path) before returning the safety response.
+export const COMBO_SAFETY_DRAIN_MS = 2000;
// Patterns that signal all accounts for a provider are rate-limited / exhausted.
// Used to detect 503 responses from handleNoCredentials so combo can fallback.
export const ALL_ACCOUNTS_RATE_LIMITED_PATTERNS = [
diff --git a/open-sse/services/combo/quotaScoring.ts b/open-sse/services/combo/quotaScoring.ts
index 4a853b1455..a107768278 100644
--- a/open-sse/services/combo/quotaScoring.ts
+++ b/open-sse/services/combo/quotaScoring.ts
@@ -14,6 +14,7 @@ import { isRecord } from "./comboData.ts";
import type { SlaRoutingPolicy } from "../autoCombo/routerStrategy.ts";
import { RESET_WINDOW_NAMES } from "./types.ts";
import type { ResolvedComboTarget } from "./types.ts";
+import { resolveProviderId } from "../../../src/shared/constants/providers.ts";
const RESET_AWARE_SESSION_WINDOW_MS = 5 * 60 * 60 * 1000;
const RESET_AWARE_WEEKLY_WINDOW_MS = 7 * 24 * 60 * 60 * 1000;
@@ -138,7 +139,11 @@ export function resolveSlaRoutingPolicy(
export function getResetAwareProvider(target: ResolvedComboTarget): string | null {
const provider = (target.providerId || target.provider || "").toLowerCase();
- return provider || null;
+ // #10877: combo targets can carry a legacy/user-facing alias spelling
+ // (e.g. "ollamacloud", "cx") while quota fetchers register under the
+ // canonical provider id (e.g. "ollama-cloud", "codex"). Canonicalize here
+ // so getQuotaFetcher() lookups downstream (quotaStrategies.ts) find them.
+ return provider ? resolveProviderId(provider) : null;
}
function normalizeResetAt(value: unknown): string | null {
diff --git a/open-sse/services/combo/quotaShareStrategy.ts b/open-sse/services/combo/quotaShareStrategy.ts
index e9e4293543..e12958042c 100644
--- a/open-sse/services/combo/quotaShareStrategy.ts
+++ b/open-sse/services/combo/quotaShareStrategy.ts
@@ -181,6 +181,7 @@ function applyDrr(targets: ResolvedComboTarget[], comboName: string): ResolvedCo
const deficits = getDrrDeficits(comboName);
const totalWeight = targets.reduce((sum, t) => sum + normalizeWeight(t.weight), 0);
+ if (totalWeight <= 0) return targets.slice();
// Add each target's quantum (weight share) to its deficit.
for (const target of targets) {
@@ -206,8 +207,9 @@ function applyDrr(targets: ResolvedComboTarget[], comboName: string): ResolvedCo
return [winner, ...rest];
}
-/** Weights default to 1 and are floored at 1 to keep quantum math well-defined. */
+/** Weights default to 1. Explicit 0 stays 0 so the operator can disable a target. */
function normalizeWeight(weight: number | undefined): number {
+ if (weight === 0) return 0;
return Number.isFinite(weight) && (weight as number) > 0 ? (weight as number) : 1;
}
diff --git a/open-sse/services/combo/sessionStickiness.ts b/open-sse/services/combo/sessionStickiness.ts
index 6f51f69df0..7347730a60 100644
--- a/open-sse/services/combo/sessionStickiness.ts
+++ b/open-sse/services/combo/sessionStickiness.ts
@@ -77,6 +77,8 @@ interface StickyEntry {
connectionId: string;
createdAt: number;
lastUsedAt: number;
+ /** Combo identity that owns this binding (matches `scopeMessageHash` namespace). */
+ namespace?: string;
}
/**
@@ -357,17 +359,23 @@ function evict(): void {
}
/** Record (or refresh) a sticky binding after a successful request. */
-export function recordStickyBinding(messageHash: string, connectionId: string): void {
+export function recordStickyBinding(
+ messageHash: string,
+ connectionId: string,
+ namespace?: string
+): void {
const existing = stickyMap.get(messageHash);
if (existing) {
existing.connectionId = connectionId;
existing.lastUsedAt = Date.now();
+ if (namespace) existing.namespace = namespace;
} else {
evict();
stickyMap.set(messageHash, {
connectionId,
createdAt: Date.now(),
lastUsedAt: Date.now(),
+ ...(namespace ? { namespace } : {}),
});
}
}
@@ -377,6 +385,24 @@ export function clearStickyBinding(messageHash: string): void {
stickyMap.delete(messageHash);
}
+/**
+ * Evict every in-memory sticky binding owned by a combo.
+ *
+ * Stale pins survive combo edits: `updateCombo` clears the persisted
+ * `session_model_history` rows, but the process-global sticky map is only
+ * bounded by TTL (15 min) — a binding recorded before the operator disabled
+ * stickiness or reordered models keeps promoting the old connection to
+ * position 0 for the remainder of the TTL window, silently defeating the
+ * combo's declared priority order (#XXXX). Combo writes call this so a
+ * config/model change takes effect immediately instead of after TTL expiry.
+ */
+export function clearStickyBindingsForCombo(namespace: string): void {
+ if (!namespace) return;
+ for (const [key, entry] of stickyMap) {
+ if (entry.namespace === namespace) stickyMap.delete(key);
+ }
+}
+
/**
* Read-only peek at the connectionId currently bound to `messageHash`, without
* mutating the store or checking TTL/health. Lets combo.ts's failure paths
@@ -462,6 +488,10 @@ export async function applySessionStickiness(
const existing = stickyMap.get(messageHash);
if (!existing) return { targets: orderedTargets, messageHash, stuck: false };
+ // Backfill the owning namespace so combo-scoped eviction (combo edit /
+ // stickiness disable) can find bindings recorded before this field existed.
+ if (namespace && existing.namespace !== namespace) existing.namespace = namespace;
+
// Check TTL
if (Date.now() - existing.lastUsedAt > TTL_MS) {
stickyMap.delete(messageHash);
diff --git a/open-sse/services/combo/targetResolution.ts b/open-sse/services/combo/targetResolution.ts
index ff3b0362f7..eeec177c86 100644
--- a/open-sse/services/combo/targetResolution.ts
+++ b/open-sse/services/combo/targetResolution.ts
@@ -72,6 +72,7 @@ import {
} from "./rrState.ts";
import {
applySessionStickiness,
+ clearStickyBindingsForCombo,
normalizeStickinessMessages,
resolveDisableSessionStickiness,
type ApplyStickinessResult,
@@ -458,6 +459,15 @@ async function applyContinuityFilters(
config as Record | null | undefined,
settings as Record | null | undefined
);
+ // Evict any in-memory sticky bindings this combo still owns when stickiness is
+ // disabled. Disabling stops NEW bindings, but a binding recorded while it was
+ // enabled would otherwise keep re-promoting the old connection for the rest of
+ // the 15-minute TTL — silently defeating the combo's priority order until the
+ // binding ages out or the process restarts (user report: disabling stickiness
+ // on orchestrator still pinned opencode-go/mimo-v2.5-max first).
+ if (disableSessionStickiness) {
+ clearStickyBindingsForCombo(combo.name);
+ }
const sticky: ApplyStickinessResult = disableSessionStickiness
? { targets: initialOrderedTargets, messageHash: null, stuck: false }
: await applySessionStickiness(
diff --git a/open-sse/services/combo/targetTimeoutRunner.ts b/open-sse/services/combo/targetTimeoutRunner.ts
index 4eb6cb8b68..402d093f35 100644
--- a/open-sse/services/combo/targetTimeoutRunner.ts
+++ b/open-sse/services/combo/targetTimeoutRunner.ts
@@ -93,19 +93,34 @@ export function buildTargetTimeoutRunner(deps: {
handleSingleModel: HandleSingleModel;
comboTargetTimeoutMs: number;
log: ComboLogger;
+ resolveTargetTimeoutMs?: (
+ target?: SingleModelTarget
+ ) => Promise | number | undefined;
}): (
b: Record,
modelStr: string,
target?: SingleModelTarget
) => Promise {
- const { handleSingleModel, comboTargetTimeoutMs, log } = deps;
+ const { handleSingleModel, comboTargetTimeoutMs, log, resolveTargetTimeoutMs } = deps;
ensureDiagnosticListener();
return async (
b: Record,
modelStr: string,
target?: SingleModelTarget
): Promise => {
- if (comboTargetTimeoutMs <= 0) {
+ const resolvedTimeoutMs = await resolveTargetTimeoutMs?.(target);
+ const effectiveTimeoutMs =
+ typeof resolvedTimeoutMs === "number" && Number.isFinite(resolvedTimeoutMs)
+ ? resolvedTimeoutMs
+ : comboTargetTimeoutMs;
+ if (effectiveTimeoutMs <= 0) {
+ // G3 (silent-stop fix): a disabled per-model timeout means a hung upstream
+ // stalls the target until the combo loop safety timer (COMBO_LOOP_SAFETY_TIMEOUT_MS)
+ // force-terminates — surface that dependency instead of silently running bare.
+ log.warn(
+ "COMBO",
+ `Per-model combo timeout is DISABLED (effectiveTimeoutMs=${effectiveTimeoutMs}) for ${modelStr} — a hung upstream will hang this target until the combo loop safety timeout`
+ );
return handleSingleModel(b, modelStr, target).catch((err) =>
errorResponse(502, err?.message ?? "Upstream model error")
);
@@ -120,13 +135,13 @@ export function buildTargetTimeoutRunner(deps: {
const abortErr = new Error(COMBO_PER_MODEL_TIMEOUT_REASON);
recordTimeoutContext({
modelStr,
- timeoutMs: comboTargetTimeoutMs,
+ timeoutMs: effectiveTimeoutMs,
abortError: abortErr,
timestamp: Date.now(),
});
log.warn(
"COMBO",
- `Model ${modelStr} exceeded ${comboTargetTimeoutMs}ms timeout — falling back`
+ `Model ${modelStr} exceeded ${effectiveTimeoutMs}ms timeout — falling back`
);
timeoutController.abort(abortErr);
// HTTP 504 (not proprietary 524): this is OmniRoute's own per-target timer.
@@ -147,7 +162,7 @@ export function buildTargetTimeoutRunner(deps: {
}
)
);
- }, comboTargetTimeoutMs);
+ }, effectiveTimeoutMs);
});
const targetWithSignal = {
...(target ?? {}),
diff --git a/open-sse/services/compression/engines/cavemanAdapter.ts b/open-sse/services/compression/engines/cavemanAdapter.ts
index 0fa8e5bb9e..d07e0b0c91 100644
--- a/open-sse/services/compression/engines/cavemanAdapter.ts
+++ b/open-sse/services/compression/engines/cavemanAdapter.ts
@@ -262,6 +262,10 @@ export const liteEngine: CompressionEngine = {
},
apply(body, options) {
const adapter = adaptBodyForCompression(body);
+ // stepConfig is Record, so its compressToolResults is `unknown`.
+ // Only an explicit boolean counts as a step override — anything else falls through
+ // to global config.lite, then the default (keeps the type `boolean`, and a malformed
+ // step value can no longer leak through the `??` chain as `{}`).
const stepCompressToolResults = options?.stepConfig?.compressToolResults;
const result = applyLiteCompression(adapter.body, {
...options,
diff --git a/open-sse/services/compression/engines/headroom/gcf/decode_generic.ts b/open-sse/services/compression/engines/headroom/gcf/decode_generic.ts
index 9c9a7774d9..f9597e7b2e 100644
--- a/open-sse/services/compression/engines/headroom/gcf/decode_generic.ts
+++ b/open-sse/services/compression/engines/headroom/gcf/decode_generic.ts
@@ -1,7 +1,8 @@
/**
* GCF generic-profile decoder (decodeGeneric).
* Vendored from gcf-typescript — generic profile only. Current with GCF spec v3.2
- * (nested object flattening) and the [N]: inline-array quoting fix.
+ * (nested object flattening), the [N]: inline-array quoting fix, the int64/2^53 numeric-
+ * domain rendering (SPEC 2.3.1), and the root-array surplus count check (SPEC 13).
* https://github.com/blackwell-systems/gcf-typescript
*
* SPDX-License-Identifier: MIT
@@ -78,7 +79,14 @@ export function decodeGeneric(input: string): any {
// Root array.
if (first.startsWith("## [")) {
- const [arr] = parseArrayFromHeader(contentLines, 0, 0, first.slice(3));
+ const [arr, consumed] = parseArrayFromHeader(contentLines, 0, 0, first.slice(3));
+ // A root array spans the whole document, so any structural line past the consumed
+ // rows is a surplus item, not sibling content. The row loop stops at the declared
+ // count, so the count assert only catches the deficit; surplus is caught here (SPEC
+ // Section 13: a mismatch, fewer OR more items than declared, is an error).
+ if (consumed < contentLines.length) {
+ throw new Error("count_mismatch: declared count is fewer than the rows present");
+ }
return arr;
}
diff --git a/open-sse/services/compression/engines/headroom/gcf/index.ts b/open-sse/services/compression/engines/headroom/gcf/index.ts
index 5671ced952..6be512f61e 100644
--- a/open-sse/services/compression/engines/headroom/gcf/index.ts
+++ b/open-sse/services/compression/engines/headroom/gcf/index.ts
@@ -1,7 +1,8 @@
/**
* GCF (Graph Compact Format) — generic profile encoder/decoder.
* Vendored from gcf-typescript for zero-dependency integration. Current with
- * GCF spec v3.2 (nested object flattening) + [N]: inline-array quoting fix.
+ * GCF spec v3.2 (nested object flattening) + [N]: inline-array quoting fix + int64/2^53
+ * numeric-domain rendering (SPEC 2.3.1) + root-array surplus count check (SPEC 13).
* https://github.com/blackwell-systems/gcf-typescript
*
* SPDX-License-Identifier: MIT
diff --git a/open-sse/services/compression/engines/headroom/gcf/scalar.ts b/open-sse/services/compression/engines/headroom/gcf/scalar.ts
index f7e82d4419..f3b5082362 100644
--- a/open-sse/services/compression/engines/headroom/gcf/scalar.ts
+++ b/open-sse/services/compression/engines/headroom/gcf/scalar.ts
@@ -1,7 +1,8 @@
/**
* Common scalar grammar for GCF (Graph Compact Format).
* Vendored from gcf-typescript — generic profile only. Current with GCF spec v3.2
- * (nested object flattening) and the [N]: inline-array quoting fix.
+ * (nested object flattening), the [N]: inline-array quoting fix, the int64/2^53 numeric-
+ * domain rendering (SPEC 2.3.1), and the root-array surplus count check (SPEC 13).
* https://github.com/blackwell-systems/gcf-typescript
*
* SPDX-License-Identifier: MIT
@@ -107,7 +108,12 @@ export function formatNumber(f: number): string {
if (Object.is(f, -0)) return "-0";
if (f === 0) return "0";
const abs = Math.abs(f);
- if (abs >= 1e-6 && abs < 1e21) {
+ // Plain decimal only below 2^53. Every double at or above 2^53 is integer-valued, so a
+ // plain rendering emits a bare-integer token: indistinguishable from an int64 on the wire
+ // and beyond a JavaScript decoder's safe-integer range (2^53-1), so it is rejected/misread
+ // on decode. Exponent shape keeps bare tokens int64 and decimal/exponent tokens doubles
+ // (SPEC 2.3.1). 2^53 = 9007199254740992.
+ if (abs >= 1e-6 && abs < 9007199254740992) {
return toPreciseDecimal(f);
}
// Exponent notation.
diff --git a/open-sse/services/compression/engines/llmlingua/worker.ts b/open-sse/services/compression/engines/llmlingua/worker.ts
index bf6fba4963..00ba3e1255 100644
--- a/open-sse/services/compression/engines/llmlingua/worker.ts
+++ b/open-sse/services/compression/engines/llmlingua/worker.ts
@@ -8,7 +8,7 @@
*
* ## Fail-open paths
* 1. Optional-deps gate: if any of `@atjsh/llmlingua-2`, `@huggingface/transformers`,
- * `@tensorflow/tfjs`, `js-tiktoken` does not resolve, return `text` immediately —
+ * `js-tiktoken` does not resolve, return `text` immediately —
* NO worker spawn. This is the default in CI / most installs (deps are OPTIONAL).
* 2. Per-call timeout: first call for a model gets `FIRST_CALL_TIMEOUT_MS` (one-time
* model load); warm calls get `LLMLINGUA_WORKER_TIMEOUT_MS`. On timeout → original
@@ -16,7 +16,7 @@
* 3. Worker error/exit → resolve all pending with their original text + respawn next.
*
* ## Serialization
- * ONNX/tfjs are not reentrant — calls are queued FIFO and only one message is
+ * ONNX inference is not reentrant — calls are queued FIFO and only one message is
* in-flight at a time (the next is posted after the previous reply or its timeout).
*
* ## Idle eviction
@@ -45,7 +45,7 @@ const FIRST_CALL_TIMEOUT_MS = 60000;
/**
* Gate probe: `@atjsh/llmlingua-2` is the entry package that declares the others
- * (`@huggingface/transformers`, `@tensorflow/tfjs`, `js-tiktoken`) as peers. We probe
+ * (`@huggingface/transformers`, `js-tiktoken`) as peers. We probe
* ONLY it (by manifest existence) because the peers are ESM-only and `require.resolve`
* throws for them even when installed; the worker still fail-opens if a peer is
* genuinely missing at `import()` time.
diff --git a/open-sse/services/compression/engines/rtk/index.ts b/open-sse/services/compression/engines/rtk/index.ts
index 33b6c90b59..b3791bfc54 100644
--- a/open-sse/services/compression/engines/rtk/index.ts
+++ b/open-sse/services/compression/engines/rtk/index.ts
@@ -656,6 +656,18 @@ export function applyRtkCompression(
};
});
+ // Mirror the sibling stacked engines (headroom, session-dedup, ccr, relevance,
+ // ionizer, readLifecycle): skip the expensive createCompressionStats() pass
+ // (full JSON.stringify + tokenizer over the whole body, twice) when nothing
+ // actually changed. Untouched messages keep their original reference above,
+ // so a reference-identity scan is enough to detect the no-op case (#10765).
+ const anyMessageChanged = compressedMessages.some(
+ (message, index) => message !== messages[index]
+ );
+ if (!anyMessageChanged) {
+ return { body, compressed: false, stats: null };
+ }
+
const compressedBody = { ...adapter.body, messages: compressedMessages };
const stats = createCompressionStats(
adapter.body,
diff --git a/open-sse/services/errorClassifier.ts b/open-sse/services/errorClassifier.ts
index 735f44eb08..2776644de1 100644
--- a/open-sse/services/errorClassifier.ts
+++ b/open-sse/services/errorClassifier.ts
@@ -28,13 +28,17 @@ export function isEmptyContentResponse(responseBody: unknown): boolean {
const content = message?.content ?? delta?.content;
const reasoningContent = message?.reasoning_content ?? delta?.reasoning_content;
+ // opencode-routed gateways (e.g. opencode/mimo-v2.5-free) name the reasoning
+ // field `reasoning` instead of `reasoning_content` (#6623).
+ const reasoningAlt = message?.reasoning ?? delta?.reasoning;
const hasToolCalls =
(Array.isArray(message?.tool_calls) && (message.tool_calls as unknown[]).length > 0) ||
(Array.isArray(delta?.tool_calls) && (delta.tool_calls as unknown[]).length > 0);
const hasContent = content !== null && content !== undefined && content !== "";
const hasReasoning =
- reasoningContent !== null && reasoningContent !== undefined && reasoningContent !== "";
+ (reasoningContent !== null && reasoningContent !== undefined && reasoningContent !== "") ||
+ (reasoningAlt !== null && reasoningAlt !== undefined && reasoningAlt !== "");
// A response truncated at the token limit (finish_reason "length") is a valid,
// successful completion even with empty text — do not flag it as a fake success.
diff --git a/open-sse/services/grokTlsClient.ts b/open-sse/services/grokTlsClient.ts
index 685f371080..00a952dd70 100644
--- a/open-sse/services/grokTlsClient.ts
+++ b/open-sse/services/grokTlsClient.ts
@@ -1,608 +1,41 @@
/**
* Browser-TLS-impersonating HTTP client for grok.com.
*
- * Why this exists: Grok sits behind Cloudflare Enterprise which pins
- * `cf_clearance` to the client's TLS fingerprint (JA3/JA4) + HTTP/2 SETTINGS
- * frame ordering. Node's Undici fetch presents an obvious "not a browser"
- * handshake and gets challenged with a 403 "Request rejected by anti-bot
- * rules." — even with a valid `sso` + `sso-rw` session cookie. This module
- * wraps `tls-client-node` (native shared library built from
- * bogdanfinn/tls-client) to send a Chrome handshake instead.
- *
- * Mirrors `perplexityTlsClient.ts`; kept as an independent module so changes
- * here cannot regress the production chatgpt-web / perplexity-web paths.
- * The first call lazily starts the managed sidecar; subsequent calls reuse
- * a singleton TLSClient. Process exit hooks stop the sidecar cleanly.
- *
- * Issue: #3180
+ * Thin re-export over the shared `tlsClientBase.ts` factory
+ * (`createTlsClientModule`). All provider-agnostic logic (sidecar lifecycle,
+ * streaming tail-file, proxy resolution, error classes, Cloudflare challenge
+ * detection) lives in the base module; this file supplies only Grok-specific
+ * config and preserves the original public export surface.
*/
-import { tmpdir } from "node:os";
-import { join, dirname } from "node:path";
-import { mkdtemp, open, unlink, rmdir, stat } from "node:fs/promises";
-import { randomUUID } from "node:crypto";
-import { buildNativeTlsClientOptions } from "./tlsClientDownloadDir.ts";
+import {
+ createTlsClientModule,
+ type TlsFetchOptions,
+ type TlsFetchResult,
+} from "./tlsClientBase.ts";
-let clientPromise: Promise | null = null;
-let exitHookInstalled = false;
-
-const GROK_PROFILE = "chrome_146"; // closest supported wreq-js profile (chrome_149 absent in 2.3.1, #5591)
const DEFAULT_TIMEOUT_MS =
Number.parseInt(process.env.OMNIROUTE_GROK_TLS_TIMEOUT_MS || "", 10) || 60_000;
-// Grace period added to the binding's wire-level timeout before our JS-level
-// hard timeout fires. Under healthy operation `tls-client-node` honors
-// `timeoutMilliseconds` and rejects on its own; the JS-level race only wins
-// when the koffi-loaded native library is wedged (which the binding's own
-// timer can't escape). Keep the grace small so users don't wait noticeably
-// longer than the configured timeout when the binding is dead.
const HARD_TIMEOUT_GRACE_MS =
Number.parseInt(process.env.OMNIROUTE_GROK_TLS_GRACE_MS || "", 10) || 10_000;
-function installExitHook(): void {
- if (exitHookInstalled) return;
- exitHookInstalled = true;
- const stop = async () => {
- if (clientPromise === null) return;
- try {
- const c = (await clientPromise) as { stop?: () => Promise };
- await c.stop?.();
- } catch {
- // ignore
- }
- };
- process.once("beforeExit", stop);
- process.once("SIGINT", () => {
- void stop();
- });
- process.once("SIGTERM", () => {
- void stop();
- });
-}
+export const tlsClientModule = createTlsClientModule({
+ providerName: "Grok",
+ tlsProfile: "chrome_146",
+ domain: "https://grok.com",
+ tempDirPrefix: "grok-stream-",
+ tailFileVariant: "B1",
+ responseValidation: "cf",
+ exportCloudflareCheck: true,
+ defaultTimeoutMs: DEFAULT_TIMEOUT_MS,
+ hardTimeoutGraceMs: HARD_TIMEOUT_GRACE_MS,
+});
-/**
- * Drop the cached client so the next `getClient()` call respawns it. Called
- * when a request observes the native binding has wedged — releasing the
- * reference lets a fresh TLSClient (and a fresh koffi load) take over without
- * a process restart.
- */
-function resetClientCache(): void {
- clientPromise = null;
-}
+export const tlsFetchGrok = (url: string, options: TlsFetchOptions = {}): Promise =>
+ tlsClientModule.tlsFetch(url, options);
-export class TlsClientHangError extends Error {
- constructor(message: string) {
- super(message);
- this.name = "TlsClientHangError";
- }
-}
+export const __setTlsFetchOverrideForTesting = tlsClientModule.__setTlsFetchOverrideForTesting;
-/**
- * Race a `client.request()` promise against (a) a JS-level hard timeout and
- * (b) the caller's abort signal. The native binding's `timeoutMilliseconds`
- * already covers the wire path; this guards the case where the koffi binding
- * itself deadlocks (observed after sustained load), where neither the
- * binding's own timer nor a post-call `signal.aborted` re-check can recover.
- */
-async function raceWithTimeout(
- promise: Promise,
- timeoutMs: number,
- signal: AbortSignal | null | undefined
-): Promise {
- let timer: ReturnType | null = null;
- let abortListener: (() => void) | null = null;
- try {
- const racers: Promise[] = [
- promise,
- new Promise((_, reject) => {
- timer = setTimeout(() => {
- reject(
- new TlsClientHangError(
- `tls-client-node call exceeded ${timeoutMs}ms — native binding likely deadlocked`
- )
- );
- }, timeoutMs);
- }),
- ];
- if (signal) {
- racers.push(
- new Promise((_, reject) => {
- if (signal.aborted) {
- reject(makeAbortError(signal));
- return;
- }
- abortListener = () => reject(makeAbortError(signal));
- signal.addEventListener("abort", abortListener, { once: true });
- })
- );
- }
- return await Promise.race(racers);
- } finally {
- if (timer) clearTimeout(timer);
- if (signal && abortListener) signal.removeEventListener("abort", abortListener);
- }
-}
-
-async function getClient(): Promise<{
- request: (url: string, opts: Record) => Promise;
-}> {
- if (!clientPromise) {
- clientPromise = (async () => {
- try {
- const mod = await import("tls-client-node");
- const TLSClient = (mod as { TLSClient: new (opts?: Record) => unknown })
- .TLSClient;
- // Native mode loads the shared library directly via koffi, avoiding the
- // managed sidecar's localhost HTTP calls that OmniRoute's global fetch
- // proxy patch interferes with.
- const client = new TLSClient(buildNativeTlsClientOptions()) as {
- start: () => Promise;
- request: (url: string, opts: Record) => Promise;
- };
- await client.start();
-
- installExitHook();
- return client;
- } catch (err) {
- clientPromise = null;
- const msg = err instanceof Error ? err.message : String(err);
- throw new TlsClientUnavailableError(
- `TLS impersonation client failed to start: ${msg}. ` +
- `Verify tls-client-node is installed and its native binary downloaded.`
- );
- }
- })();
- }
- return clientPromise as Promise<{
- request: (url: string, opts: Record