Compare commits

..

4 Commits

Author SHA1 Message Date
Xiangzhe
ac206e9375 feat(cli): boot exposure warning for unauthenticated LAN bind (GHSA-wmgv-ph3p-rv57)
The shipped default (bind 0.0.0.0 + no API-key requirement) is a deliberate,
documented local-first posture — but an operator on an untrusted network
should learn that at startup, not after a surprise quota bill. serve now
prints a loud warning naming both escape hatches (REQUIRE_API_KEY=true or
OMNIROUTE_SERVER_HOST=127.0.0.1) whenever the resolved bind is non-loopback
and no key is required. Silent on loopback binds and when REQUIRE_API_KEY is
enabled. The default posture itself is unchanged (operator decision).
2026-08-23 12:50:42 -03:00
Xiangzhe
0fb4eb6878 fix(security): A2A REST auth + task owner scoping (GHSA-jcm5-6wpp-wjj8)
The REST task routes (/api/a2a/tasks, /api/a2a/tasks/[id], /[id]/cancel) had
NO authentication call at all — open regardless of configuration — and the
task manager stored tasks in an owner-less Map, so any caller could read or
cancel any task by id over either the JSON-RPC or the REST surface.

- New shared src/lib/a2a/authenticate.ts (the v54m JSON-RPC posture, lifted
  so both surfaces cannot drift) + src/app/api/a2a/_auth.ts implementing the
  full posture matrix: REQUIRE_API_KEY=true demands a valid key (management
  session also passes via alwaysRequireAuth); requireLogin=true accepts
  management or a valid key; the keyless local-first default stays open by
  design.
- Tasks bind to an owner (hashed API key) at creation; get/cancel/list are
  owner-scoped. Another principal's task answers with the same not-found a
  missing one would (no existence oracle). Ownerless tasks (keyless posture)
  stay visible to everyone; management/operator view sees all tasks.
- Callers discriminate the auth failure with instanceof Response, never
  instanceof NextResponse — createErrorResponse() returns a plain Response,
  which silently fell through to the handler (caught by the 401-vs-404 test).
2026-08-23 12:50:04 -03:00
Xiangzhe
81cc000cf5 fix(security): SSRF guard on client-controlled search baseUrl (GHSA-j7j4-g9qc-q69c)
/v1/search accepted provider_options.baseUrl / providerSpecificData.baseUrl
verbatim and flowed it through resolveSearchBaseUrl() into every builder's
server-side fetch target, while the sink (searchProxy.ts) is a plain fetch().
The Firecrawl sibling was fixed in #10738; this shared resolver was missed —
full-read SSRF to cloud metadata (IMDS credential theft) and JSON-speaking
internal services, reachable with no credentials on the default posture.

resolveSearchBaseUrl() now validates any request-supplied override with
parseAndValidateNonMetadataUrl (block-metadata): self-hosted searxng on
loopback/LAN — the provider's primary use case — keeps working, while
cloud-metadata endpoints are rejected. The catalog's operator-configured
baseUrl stays untouched.
2026-08-23 12:48:02 -03:00
Xiangzhe
e1fdfc40a5 fix(security): harden authz tiers — legacy export/import + MITM loopback
GHSA-v7g9-7f55-5g46 (follow-up to mghq): /api/settings/export-json dumps every
stored credential and /api/settings/import-json irreversibly replaces settings,
yet both were left out of the mghq ALWAYS_PROTECTED fix and their handlers only
gate on isAuthRequired() — false under requireLogin=false. Added to
ALWAYS_PROTECTED_API_PATHS alongside /api/settings/database and /api/db-backups.

GHSA-x7vm-hp44-9p79: the MITM management routes (/api/settings/mitm,
/api/cli-tools/antigravity-mitm) install a system-wide trusted root CA and
write /etc/hosts DNS overrides, but were MANAGEMENT-only — remotely reachable
under requireLogin=false, violating the documented loopback contract for
privileged surfaces (Hard Rules #15/#17). Added to LOCAL_ONLY_API_PREFIXES and
SPAWN_CAPABLE_PREFIXES (never manage-scope bypassable), same tier as
/api/tools/agent-bridge/.
2026-08-23 12:46:50 -03:00
223 changed files with 898 additions and 5979 deletions

View File

@@ -1303,30 +1303,6 @@ CURSOR_USER_AGENT="Cursor/3.4"
# set to true/1/yes to enable. Used by: open-sse/executors/codex.ts.
# OMNIROUTE_CODEX_DROP_NONSTANDARD_EVENTS=true
# Codex app-server WebSocket transport (opt-in). When a WebSocket URL and a
# capability token are both provided, Codex requests are routed through a local
# `codex app-server` sidecar over JSON-RPC instead of the HTTP Responses API.
# Each var is also settable per-connection via providerSpecificData; the env var
# is the process-wide fallback. Used by:
# open-sse/executors/codex/appServerConfig.ts.
#
# WebSocket endpoint of the codex app-server (ws:// or wss://). Required to
# enable the transport; leaving it unset keeps Codex on its HTTP transports.
# OMNIROUTE_CODEX_APPSERVER_WS=ws://127.0.0.1:8081
# Inline capability/bearer token presented to the app-server.
# OMNIROUTE_CODEX_APPSERVER_WS_TOKEN=deadbeef...
# Path to a file holding the capability token (produced by
# `codex app-server --ws-token-file <path>`). Used when the inline token above
# is not set.
# OMNIROUTE_CODEX_APPSERVER_WS_TOKEN_FILE=/run/codex-ws-token
# Working directory the app-server turn runs in (defaults to /tmp).
# OMNIROUTE_CODEX_APPSERVER_CWD=/tmp
# Approval policy passed to the app-server turn (e.g. never, on-request).
# OMNIROUTE_CODEX_APPSERVER_APPROVAL=never
# Sandbox policy passed to the app-server turn (e.g. read-only,
# workspace-write, danger-full-access).
# OMNIROUTE_CODEX_APPSERVER_SANDBOX=read-only
# ═══════════════════════════════════════════════════════════════════════════════
# 13. CLI FINGERPRINT COMPATIBILITY (Anti-Detection)
# ═══════════════════════════════════════════════════════════════════════════════

View File

@@ -92,9 +92,5 @@
# - x-api-key PUBLICO do Firefly web (documentado em open-sse/utils/publicCreds.ts:207);
# as duas ocorrencias sinalizadas estao em COMENTARIOS JSDoc, o runtime le de resolvePublicCred().
'''omniroute-kimi-sponsor-banner-dismissed-v\d+''',
# CheaperInference sponsor banner localStorage key (upstream #11196 /
# eb5797370). Same UI-identifier pattern as the kimi banner above, not a
# credential; the generic-api-key rule flags the long hyphenated string.
'''omniroute-cheaperinference-sponsor-banner-dismissed-v\d+''',
'''SunbreakWebUI1''',
]

View File

@@ -46,7 +46,7 @@ Repository map and Reference Documentation sections below.
## Project at a Glance
**OmniRoute** — unified AI proxy/router. One endpoint, 351 LLM providers, auto-fallback.
**OmniRoute** — unified AI proxy/router. One endpoint, 350 LLM providers, auto-fallback.
| Layer | Location | Purpose |
| ------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |

View File

@@ -7,7 +7,7 @@
# 🚀 OmniRoute — The Free AI Gateway
<img src="./docs/diagrams/readme-hero.svg" width="100%" alt="OmniRoute — Never stop coding. Every AI tool → 351 providers — 90+ free — through one endpoint. Claude Code, Codex, Cursor, Cline, Copilot & Antigravity into FREE Claude / GPT / Gemini with auto-fallback. RTK + Caveman stacked compression saves 1595% tokens (~89% avg) — never hit limits. 351 AI providers · 90+ free tiers · ~1.51B free tokens/mo · 19 routing strategies · $0 to start."/>
<img src="./docs/diagrams/readme-hero.svg" width="100%" alt="OmniRoute — Never stop coding. Every AI tool → 349 providers — 90+ free — through one endpoint. Claude Code, Codex, Cursor, Cline, Copilot & Antigravity into FREE Claude / GPT / Gemini with auto-fallback. RTK + Caveman stacked compression saves 1595% tokens (~89% avg) — never hit limits. 350 AI providers · 90+ free tiers · ~1.51B free tokens/mo · 19 routing strategies · $0 to start."/>
</div>
@@ -210,7 +210,7 @@ curl http://localhost:20128/v1/chat/completions \
</div>
<img src="./docs/diagrams/promise-pillars.svg" width="100%" alt="The Promise — One endpoint. 351 providers. Never stop building — OmniRoute picks the cheapest one that works. Six pillars: Never hit limits (auto-fallback across 351 providers in milliseconds, zero downtime) · Save up to 95% tokens (RTK + Caveman stacked compression cuts 1595%, ~89% avg on tool-heavy sessions) · $0 to start (90+ free tiers, 56 free forever — no card needed) · Every tool works (33 coding agents through one config) · One endpoint (OpenAI ↔ Claude ↔ Gemini ↔ Responses API at /v1) · Production-grade (circuit breakers, TLS stealth, MCP 110 tools, A2A, memory, guardrails, evals — 25,000+ tests)."/>
<img src="./docs/diagrams/promise-pillars.svg" width="100%" alt="The Promise — One endpoint. 349 providers. Never stop building — OmniRoute picks the cheapest one that works. Six pillars: Never hit limits (auto-fallback across 349 providers in milliseconds, zero downtime) · Save up to 95% tokens (RTK + Caveman stacked compression cuts 1595%, ~89% avg on tool-heavy sessions) · $0 to start (90+ free tiers, 56 free forever — no card needed) · Every tool works (33 coding agents through one config) · One endpoint (OpenAI ↔ Claude ↔ Gemini ↔ Responses API at /v1) · Production-grade (circuit breakers, TLS stealth, MCP 110 tools, A2A, memory, guardrails, evals — 25,000+ tests)."/>
<br/>
<br/>
@@ -461,7 +461,7 @@ All **19** strategies — mix & match per combo step:
</div>
<img src="./docs/diagrams/comparison-table.svg" width="100%" alt="What sets OmniRoute apart — comparison table vs 9router, OpenRouter, CLIProxyAPI and LiteLLM across 13 capabilities. OmniRoute: 351 providers, 90+ free providers built-in, 19 routing strategies, 12-engine token compression, built-in MCP server with 110 tools, A2A agent protocol, persistent memory, guardrails, cloud agents, TLS fingerprint stealth, Desktop/Termux/PWA, 43 i18n UI locales, 100% MIT self-hosted. OmniRoute is the only one with the full set; competitors show a mix of checks, partials and crosses. Verified from each project&apos;s docs."/>
<img src="./docs/diagrams/comparison-table.svg" width="100%" alt="What sets OmniRoute apart — comparison table vs 9router, OpenRouter, CLIProxyAPI and LiteLLM across 13 capabilities. OmniRoute: 349 providers, 90+ free providers built-in, 19 routing strategies, 12-engine token compression, built-in MCP server with 110 tools, A2A agent protocol, persistent memory, guardrails, cloud agents, TLS fingerprint stealth, Desktop/Termux/PWA, 43 i18n UI locales, 100% MIT self-hosted. OmniRoute is the only one with the full set; competitors show a mix of checks, partials and crosses. Verified from each project&apos;s docs."/>
<sub>📊 Full methodology &amp; per-feature detail vs 9router, OpenRouter, CLIProxyAPI &amp; LiteLLM → [`docs/comparison/OMNIROUTE_VS_ALTERNATIVES.md`](docs/comparison/OMNIROUTE_VS_ALTERNATIVES.md)</sub>
@@ -612,7 +612,7 @@ the current catalog at **[radar.omniroute.online/planos](https://radar.omniroute
<b> also works with</b> · Kiro · Command Code · Antigravity · Windsurf · AMP · <b>any OpenAI-compatible tool</b>
</div>
<sub>📖 Per-tool setup for all 35 tools (26 CLI Code's + 9 CLI Agents) → [`docs/reference/CLI-TOOLS.md`](docs/reference/CLI-TOOLS.md) · 🧩 OpenCode plugin → [`@omniroute/opencode-provider`](https://www.npmjs.com/package/@omniroute/opencode-provider)</sub>
<sub>📖 Per-tool setup for all 34 tools (26 CLI Code's + 8 CLI Agents) → [`docs/reference/CLI-TOOLS.md`](docs/reference/CLI-TOOLS.md) · 🧩 OpenCode plugin → [`@omniroute/opencode-provider`](https://www.npmjs.com/package/@omniroute/opencode-provider)</sub>
</div>
@@ -646,7 +646,7 @@ of your shell history. → [CLI Integrations](docs/guides/CLI-INTEGRATIONS.md)
</div>
> The most complete catalog of any open-source router: **351 providers**, **90+ with a free tier**, **56 free forever**.
> The most complete catalog of any open-source router: **349 providers**, **90+ with a free tier**, **56 free forever**.
<div align="center">

View File

@@ -12,7 +12,7 @@ import {
isFatalInstrumentationHookFailure,
formatAndroidInstrumentationFailureHint,
} from "../utils/ensureAndroidCacheDir.mjs";
import { resolveServerHost } from "../utils/serverHost.mjs";
import { resolveServerHost, resolveExposureWarning } from "../utils/serverHost.mjs";
import {
resolveMaxOldSpaceMb,
calibrateHeapFallbackMb,
@@ -20,7 +20,6 @@ import {
buildNodeHeapArgs,
} from "../../../scripts/build/runtime-env.mjs";
import { resolveTlsOptions } from "../../../scripts/dev/tls-options.mjs";
import { startDetachedTray, validateTrayOptions } from "../tray/detachedTray.mjs";
const __dirname = dirname(fileURLToPath(import.meta.url));
const _pkg = JSON.parse(readFileSync(join(__dirname, "..", "..", "..", "package.json"), "utf8"));
@@ -43,7 +42,7 @@ function parsePort(value, fallback) {
}
export function registerServe(program) {
const command = program
program
.command("serve", { isDefault: true })
.description(t("serve.description"))
.option("--port <port>", t("serve.port"))
@@ -52,7 +51,7 @@ export function registerServe(program) {
.option("--log", t("serve.log"))
.option("--no-recovery", t("serve.no_recovery"))
.option("--max-restarts <n>", t("serve.max_restarts"), parseInt, 2)
.option("--tray", t("serve.tray") || "Start in the system tray (desktop only)")
.option("--tray", t("serve.tray") || "Show system tray icon (desktop only)")
.option("--no-tray", t("serve.no_tray") || "Disable system tray icon")
.option(
"--tls-cert <path>",
@@ -67,9 +66,6 @@ export function registerServe(program) {
.action(async (opts) => {
await runServe(opts);
});
command.addOption(command.createOption("--tray-worker").hideHelp());
command.addOption(command.createOption("--tray-ready-port <port>").hideHelp());
command.addOption(command.createOption("--tray-ready-token <token>").hideHelp());
}
/** Once-per-process guard so the Android/Termux cache hint is not spammed. */
@@ -99,32 +95,6 @@ export function resetInstrumentationFailureHintForTests() {
export async function runServe(opts = {}) {
const startedAt = performance.now();
const trayOptionError = validateTrayOptions(opts);
if (trayOptionError) throw new Error(trayOptionError);
if (opts.tray === true && opts.trayWorker !== true) {
const port = parsePort(opts.port ?? process.env.PORT ?? "20128", 20128);
const tlsCert = opts.tlsCert ?? process.env.OMNIROUTE_TLS_CERT;
const tlsKey = opts.tlsKey ?? process.env.OMNIROUTE_TLS_KEY;
urlScheme = resolveTlsOptions({
...process.env,
...(tlsCert ? { OMNIROUTE_TLS_CERT: tlsCert } : {}),
...(tlsKey ? { OMNIROUTE_TLS_KEY: tlsKey } : {}),
})
? "https"
: "http";
const result = await startDetachedTray({
cliPath: join(ROOT, "bin", "omniroute.mjs"),
port,
maxRestarts: opts.maxRestarts ?? 2,
tlsCert,
tlsKey,
});
console.log(`\x1b[32m✔ OmniRoute tray started in background\x1b[0m`);
console.log(` \x1b[1mDashboard:\x1b[0m ${urlScheme}://localhost:${port}`);
return result;
}
// Same prep as bin/omniroute.mjs — keep it here so a direct `runServe()` call
// (tests / programmatic) still gets a writable Next.js cache dir before spawn.
ensureAndroidCacheDir({ env: process.env });
@@ -162,6 +132,15 @@ export async function runServe(opts = {}) {
`);
}
// GHSA-wmgv-ph3p-rv57: the default posture (all interfaces + no API key) is a
// deliberate local-first choice, but it must be loud at startup — an operator
// on an untrusted network learns the two escape hatches here, not after a
// surprise quota bill.
const exposureWarning = resolveExposureWarning();
if (exposureWarning) {
console.warn(`\x1b[33m ⚠ ${exposureWarning}\x1b[0m\n`);
}
const serverWsJs = join(APP_DIR, "server-ws.mjs");
const serverJs = existsSync(serverWsJs) ? serverWsJs : join(APP_DIR, "server.js");
@@ -285,8 +264,7 @@ export async function runServe(opts = {}) {
opts.log === true,
opts.maxRestarts ?? 2,
startedAt,
useTray,
{ trayReadyPort: opts.trayReadyPort, trayReadyToken: opts.trayReadyToken }
useTray
);
}
@@ -399,11 +377,9 @@ async function runWithSupervisor(
showLog,
maxRestarts,
startedAt,
useTray = false,
{ trayReadyPort, trayReadyToken } = {}
useTray = false
) {
if (showLog) process.env.OMNIROUTE_SHOW_LOG = "1";
writePidFile("supervisor", process.pid);
const supervisor = new ServerSupervisor({
serverPath: serverJs,
@@ -427,38 +403,17 @@ async function runWithSupervisor(
process.on("SIGINT", () => {
killTrayIfActive();
cleanupPidFile("supervisor");
supervisor.stop();
});
process.on("SIGTERM", () => {
killTrayIfActive();
cleanupPidFile("supervisor");
supervisor.stop();
});
if (!showLog) {
waitForServer(dashboardPort, 60000).then(async (up) => {
if (up) {
if (useTray) {
const trayReady = await maybeStartTray(dashboardPort, apiPort, supervisor);
if (!trayReady) {
cleanupPidFile("supervisor");
supervisor.stop();
process.exitCode = 1;
return;
}
if (trayReadyPort && trayReadyToken) {
const { notifyTrayReady } = await import("../tray/detachedTray.mjs");
try {
await notifyTrayReady(parsePort(trayReadyPort, 0), trayReadyToken);
} catch {
cleanupPidFile("supervisor");
supervisor.stop();
process.exitCode = 1;
return;
}
}
}
if (useTray) await maybeStartTray(dashboardPort, apiPort, supervisor);
onReady(dashboardPort, apiPort, noOpen, startedAt);
} else {
reportReadinessTimeout(dashboardPort, supervisor);
@@ -505,30 +460,29 @@ function killTrayIfActive() {
async function maybeStartTray(port, apiPort, supervisor) {
try {
const { initTray, isTraySupported } = await import("../tray/index.mjs");
if (!isTraySupported()) return false;
if (!isTraySupported()) return;
const { default: open } = await import("open").catch(() => ({ default: null }));
const dashboardUrl = `${urlScheme}://localhost:${port}`;
const tray = await initTray({
port,
onQuit: () => {
killTrayIfActive();
cleanupPidFile("supervisor");
supervisor.stop();
},
onOpenDashboard: () => open?.(dashboardUrl),
onShowLogs: () => open?.(`${dashboardUrl}/dashboard/logs`),
onShowLogs: () => {
// In-place: open logs stream (best-effort)
process.stdout.write(`[omniroute][tray] Logs at: ${dashboardUrl}/logs\n`);
},
});
if (tray) {
const { killTray } = await import("../tray/index.mjs");
_killTray = killTray;
return true;
}
return false;
} catch (err) {
// tray is optional — do not fail the server, but surface why it failed so
// "--tray shows nothing" is diagnosable instead of silent (#4605).
process.stderr.write(`[omniroute][tray] failed to start: ${err?.message ?? String(err)}\n`);
return false;
}
}

View File

@@ -254,7 +254,7 @@
"log": "Show server logs inline",
"no_recovery": "Disable auto-restart on crash (debugging mode)",
"max_restarts": "Max crash restarts within 30s before giving up (default: 2)",
"tray": "Start in the system tray (desktop only, opt-in)",
"tray": "Show system tray icon (desktop only, opt-in)",
"no_tray": "Disable system tray icon",
"tls_cert": "Path to a TLS certificate (PEM) to serve HTTPS (also OMNIROUTE_TLS_CERT)",
"tls_key": "Path to the TLS private key (PEM) to serve HTTPS (also OMNIROUTE_TLS_KEY)"

View File

@@ -6,9 +6,7 @@ import { pathToFileURL } from "node:url";
import { validateBinaryMagic, platformBinaryLabel } from "./magicBytes.mjs";
const RUNTIME_DIR = join(homedir(), ".omniroute", "runtime");
// Exported so the packaging coherence guard (tests/unit/pack-boot-runtime-paths.test.ts)
// can assert this stays on the same major as optionalDependencies.better-sqlite3 (#11242).
export const BETTER_SQLITE3_VERSION = "better-sqlite3@^13.0.2";
const BETTER_SQLITE3_VERSION = "better-sqlite3@^12.10.1";
let resolvedCached = null;

View File

@@ -276,10 +276,6 @@ function isAgentSelfMac() {
}
}
function isDetachedTrayWorker() {
return process.argv.includes("--tray-worker");
}
function enableMac() {
const plistDir = join(homedir(), "Library", "LaunchAgents");
mkdirSync(plistDir, { recursive: true });
@@ -304,7 +300,7 @@ function enableMac() {
// If we're already the running agent, launchctl load/unload would SIGTERM us.
// The plist is updated on disk and launchd already has us loaded under our own
// PID — nothing more to do for the current session.
if (isAgentSelfMac() || isDetachedTrayWorker()) return existsSync(plistPath);
if (isAgentSelfMac()) return existsSync(plistPath);
try {
execSync("launchctl load -w " + JSON.stringify(plistPath), { stdio: "ignore" });
} catch {}
@@ -317,7 +313,7 @@ function disableMac() {
// `launchctl unload` sends SIGTERM and a user clicking "Disable Autostart"
// from the tray would lose the tray icon instead of just flipping the label.
// Removing the plist file is enough to stop the agent at the next login.
if (!isAgentSelfMac() && !isDetachedTrayWorker()) {
if (!isAgentSelfMac()) {
try {
execSync("launchctl unload -w " + JSON.stringify(plistPath), { stdio: "ignore" });
} catch {}

View File

@@ -1,176 +0,0 @@
import { execFileSync, spawn } from "node:child_process";
import { randomBytes, timingSafeEqual } from "node:crypto";
import { createServer, connect } from "node:net";
/** Builds arguments for the hidden process that owns the server and tray. */
export function buildTrayWorkerArgs({ port, maxRestarts, readyPort, readyToken, tlsCert, tlsKey }) {
const args = [
"serve",
"--tray",
"--tray-worker",
"--no-open",
"--port",
String(port),
"--max-restarts",
String(maxRestarts),
"--tray-ready-port",
String(readyPort),
"--tray-ready-token",
readyToken,
];
if (tlsCert) args.push("--tls-cert", tlsCert);
if (tlsKey) args.push("--tls-key", tlsKey);
return args;
}
/** Builds the platform command that starts the hidden tray worker. */
export function buildTrayLaunch({ platform, execPath, cliPath, workerArgs, label }) {
if (platform === "darwin") {
return {
command: "launchctl",
args: ["submit", "-l", label, "--", execPath, cliPath, ...workerArgs],
options: { stdio: "ignore" },
};
}
return {
command: execPath,
args: [cliPath, ...workerArgs],
options: { detached: true, stdio: "ignore", windowsHide: true },
};
}
/** Returns an error for command modes that conflict with detached tray mode. */
export function validateTrayOptions(opts) {
if (opts.trayWorker && (!opts.trayReadyPort || !opts.trayReadyToken)) {
return "tray worker requires readiness credentials";
}
if (!opts.tray || opts.trayWorker) return null;
if (opts.daemon) return "--tray cannot use --daemon";
if (opts.log) return "--tray cannot use --log";
if (opts.noRecovery || opts.recovery === false) return "--tray cannot use --no-recovery";
return null;
}
/** Creates a token-protected loopback server for tray worker readiness. */
export async function createTrayReadinessServer(token) {
let markReady;
const ready = new Promise((resolve) => {
markReady = resolve;
});
const expected = Buffer.from(token);
const server = createServer((socket) => {
let data = "";
socket.setEncoding("utf8");
socket.on("data", (chunk) => {
data += chunk;
if (data.length > 256) socket.destroy();
});
socket.on("end", () => {
const received = Buffer.from(data);
if (received.length !== expected.length || !timingSafeEqual(received, expected)) {
socket.end("ERROR");
return;
}
socket.end("READY");
markReady();
});
});
await new Promise((resolve, reject) => {
server.once("error", reject);
server.listen(0, "127.0.0.1", resolve);
});
const address = server.address();
return {
port: address.port,
wait(timeoutMs) {
return new Promise((resolve, reject) => {
const timer = setTimeout(
() => reject(new Error("Tray worker did not become ready")),
timeoutMs
);
ready.then(() => {
clearTimeout(timer);
resolve();
});
});
},
close() {
server.close();
},
};
}
/** Notifies the parent process that the server and tray are ready. */
export async function notifyTrayReady(port, token) {
await new Promise((resolve, reject) => {
const socket = connect({ host: "127.0.0.1", port }, () => socket.end(token));
let reply = "";
socket.setEncoding("utf8");
socket.on("data", (chunk) => {
reply += chunk;
});
socket.on("end", () => {
if (reply === "READY") resolve();
else reject(new Error("Tray readiness token was rejected"));
});
socket.on("error", reject);
});
}
/** Starts a detached tray worker and waits until its server and tray are ready. */
export async function startDetachedTray(
{ cliPath, port, maxRestarts, tlsCert, tlsKey, timeoutMs = 60000 },
{ platform = process.platform, spawnProcess = spawn } = {}
) {
const token = randomBytes(32).toString("hex");
const readiness = await createTrayReadinessServer(token);
const label = `com.omniroute.tray.${process.pid}.${Date.now()}`;
const workerArgs = buildTrayWorkerArgs({
port,
maxRestarts,
readyPort: readiness.port,
readyToken: token,
tlsCert,
tlsKey,
});
const launch = buildTrayLaunch({
platform,
execPath: process.execPath,
cliPath,
workerArgs,
label,
});
const child = spawnProcess(launch.command, launch.args, launch.options);
const spawnFailure = new Promise((_, reject) => {
child.once("error", reject);
child.once("exit", (code) => {
if (platform !== "darwin" || code !== 0) {
reject(new Error(`Tray worker exited before readiness with code ${code ?? "unknown"}`));
}
});
});
if (platform !== "darwin") child.unref?.();
try {
await Promise.race([readiness.wait(timeoutMs), spawnFailure]);
return { platform, pid: child.pid, label: platform === "darwin" ? label : null };
} catch (err) {
if (platform === "darwin") {
try {
execFileSync("launchctl", ["bootout", `gui/${process.getuid()}/${label}`], {
stdio: "ignore",
});
} catch {}
} else if (platform === "win32" && child.pid) {
try {
execFileSync("taskkill", ["/PID", String(child.pid), "/T", "/F"], { stdio: "ignore" });
} catch {}
} else if (child.pid) {
try {
process.kill(child.pid, "SIGTERM");
} catch {}
}
throw err;
} finally {
readiness.close();
}
}

View File

@@ -97,7 +97,9 @@ export async function initSystrayUnix(
}
});
await tray.ready();
tray.ready().catch((err) => {
process.stderr.write(`[omniroute][tray] systray2 failed: ${err?.message ?? String(err)}\n`);
});
return tray;
}

View File

@@ -24,3 +24,34 @@ export function resolveServerHost(
}
return "0.0.0.0";
}
const LOOPBACK_HOSTS = new Set(["127.0.0.1", "localhost", "::1", "[::1]"]);
/**
* Boot-time exposure warning (GHSA-wmgv-ph3p-rv57): the shipped default binds
* all interfaces while the inference plane requires no credentials, so any
* LAN peer can spend the operator's quota. That local-first posture is a
* deliberate, documented default — but it must be LOUD at startup so an
* operator who never read the docs still learns the two escape hatches.
*
* Returns the warning text when the server will listen on a non-loopback
* interface with no API-key requirement, or null when the exposure is closed.
*
* @param {NodeJS.ProcessEnv} [env]
* @param {string} [host]
* @returns {string | null}
*/
export function resolveExposureWarning(env = process.env, host = resolveServerHost(env)) {
if (LOOPBACK_HOSTS.has(host)) return null;
const requireKey = String(env.REQUIRE_API_KEY || "")
.trim()
.toLowerCase();
if (requireKey === "true" || requireKey === "1" || requireKey === "yes") return null;
return (
`SECURITY: listening on ${host} with NO API-key requirement — the inference ` +
`plane (/v1/*) is reachable by ANY device that can route to this host, and ` +
`requests are billed to your configured providers. This local-first default ` +
`is intentional, but on an untrusted network either set REQUIRE_API_KEY=true ` +
`or bind loopback with OMNIROUTE_SERVER_HOST=127.0.0.1.`
);
}

View File

@@ -1 +0,0 @@
- **feat(cli):** run `omniroute serve --tray` as a detached desktop process after server and tray readiness, with graphical login auto-start support.

View File

@@ -1 +0,0 @@
- fix(sse): default single-target incompatible reasoning to drop for agentic replay — single-target requests to opaque reasoning targets now gracefully strip incompatible plaintext reasoning history instead of returning HTTP 400, matching combo default behavior while preserving operator and per-request overrides ([#10959](https://github.com/diegosouzapw/OmniRoute/issues/10959))

View File

@@ -1 +0,0 @@
- fix(codex): prefer `max_context_window` over the `context_window` pricing tier as the usable input limit in discovery, and raise the static Codex OAuth catalog to the same usable window so the conservative discovery merge no longer caps live values at the 272K pricing tier

View File

@@ -451,8 +451,7 @@
"src/shared/components/ModelSelectModal.tsx": 1138,
"src/shared/constants/providers/apikey/gateways.ts": 1250
},
"src/app/(dashboard)/dashboard/providers/[id]/components/modals/AddApiKeyModal.tsx": 1173,
"_rebaseline_2026_08_23_11207_aws_polly_fields": "PR #11207 (rafacpti23, draft) own growth: AddApiKeyModal.tsx 1082->1173 (+91, AWS SigV4 credential fields for aws-polly — Access Key ID / Region / optional Session Token blocks with providerText i18n labels, at the existing per-provider form-section chokepoint; the file is the known god-modal with repeated dated rebaselines). Covered by tests/unit/dashboard/aws-polly-connection-modal-fields.test.ts. Owner pre-authorized baseline bumps 2026-08-22.",
"src/app/(dashboard)/dashboard/providers/[id]/components/modals/AddApiKeyModal.tsx": 1082,
"_rebaseline_2026_08_22_11156_enter_check_disabled": "PR #11156 (rqzbeh) own growth: AddApiKeyModal.tsx 1080->1082 (+2, Enter keydown handler now mirrors the isCheckDisabled condition — owner-requested post-merge polish from #11056; the rest of the diff is Prettier reflow). Covered by tests/unit/ui/add-api-key-modal-enter-key.test.tsx (jsdom render test, Enter dispatch assertions).",
"src/app/(dashboard)/dashboard/providers/[id]/hooks/useProviderConnections.ts": 1051,
"src/shared/components/ModelSelectModal.tsx": 1138,

View File

@@ -43,21 +43,8 @@ x-common: &common
- LIVE_WS_ALLOWED_ORIGINS=${LIVE_WS_ALLOWED_ORIGINS:-http://localhost:20128,http://127.0.0.1:20128}
- REDIS_URL=${REDIS_URL:-redis://redis:6379}
- NODE_OPTIONS=--max-old-space-size=2048
# Codex App-Server transport (provider: codex-app-server). Inert unless the
# `codex-app-server` compose profile is up (the sidecar below). Points the app
# at the internal sidecar; the capability token is shared via the mounted file.
- OMNIROUTE_CODEX_APPSERVER_WS=${OMNIROUTE_CODEX_APPSERVER_WS:-ws://codex-app-server:1456}
- OMNIROUTE_CODEX_APPSERVER_WS_TOKEN_FILE=${OMNIROUTE_CODEX_APPSERVER_WS_TOKEN_FILE:-/run/codex-appserver/token}
volumes:
- ./data:/app/data
# Shared capability token + codex auth for the app-server WS. Only meaningful
# when the codex-app-server profile is active. The token dir carries the WS
# capability token; the codex home is where the dashboard "Apply auth" writes
# ~/.codex/auth.json (getCliConfigPaths("codex") = <home>/.codex; the base
# image runs as `node`, so /home/node/.codex) and the SAME volume is mounted
# into the sidecar so its `codex app-server` reads the same auth.
- codex-appserver-token:/run/codex-appserver
- codex-appserver-home:/home/node/.codex
healthcheck:
test: ["CMD", "node", "healthcheck.mjs"]
interval: 30s
@@ -303,59 +290,6 @@ services:
profiles:
- cliproxyapi
# ── Profile: codex-app-server (Codex CLI app-server sidecar) ──────────
# A PLAIN Codex app-server for the `codex-app-server` provider: OmniRoute drives
# the Codex CLI's own `codex app-server` over JSON-RPC/WebSocket instead of
# replaying a session token to the API. It listens ONLY on the internal compose
# network (ws://codex-app-server:1456), guarded by a capability token — it is
# NEVER published to the host / internet. The Codex CLI (baked into
# omniroute:base) self-manages its OpenAI OAuth via the shared ~/.codex volume,
# which the dashboard "Apply auth" (device-OAuth) writes and this sidecar reads.
#
# NOTE: this is the GENERIC public sidecar. An operator wanting residential /
# UDP egress (via a TUN sidecar) runs that separately as an override; it is
# intentionally not shipped here.
codex-app-server:
image: omniroute:base
container_name: omniroute-codex-app-server
restart: unless-stopped
# Generate the WS capability token on first boot if absent, then run the
# app-server. entrypoint is overridden because the base image's default is the
# Next.js server.
entrypoint: ["/bin/sh", "-c"]
command:
- |
set -e
TOKEN_FILE=/run/codex-appserver/token
mkdir -p /run/codex-appserver
if [ ! -s "$$TOKEN_FILE" ]; then
# 32-byte hex capability token; shared with the app via the token volume.
TF="$$TOKEN_FILE" node -e 'require("fs").writeFileSync(process.env.TF, require("crypto").randomBytes(32).toString("hex"))' 2>/dev/null || \
{ head -c 32 /dev/urandom | od -An -tx1 | tr -d ' \n' > "$$TOKEN_FILE"; }
chmod 600 "$$TOKEN_FILE"
fi
exec codex app-server \
--listen ws://0.0.0.0:1456 \
--ws-auth capability-token \
--ws-token-file "$$TOKEN_FILE"
environment:
- CODEX_HOME=/home/node/.codex
- RUST_LOG=${CODEX_APPSERVER_RUST_LOG:-warn}
volumes:
- codex-appserver-token:/run/codex-appserver
- codex-appserver-home:/home/node/.codex
# No `ports:` — internal-only. Reached at ws://codex-app-server:1456 over the
# compose network by the omniroute app.
healthcheck:
test:
["CMD", "node", "-e", "require('http').get('http://127.0.0.1:1456/readyz',r=>process.exit(r.statusCode===200?0:1)).on('error',()=>process.exit(1))"]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
profiles:
- codex-app-server
volumes:
chatgpt-web-codex-browser-data:
name: omniroute-chatgpt-web-codex-browser-data
@@ -367,7 +301,3 @@ volumes:
name: omniroute-qdrant-data
bifrost-data:
name: omniroute-bifrost-data
codex-appserver-token:
name: omniroute-codex-appserver-token
codex-appserver-home:
name: omniroute-codex-appserver-home

View File

@@ -17,7 +17,7 @@ It provides a single OpenAI-compatible endpoint (`/v1/*`) and routes traffic acr
Core capabilities:
- OpenAI-compatible API surface for CLI/tools (349 providers, 107 executors)
- OpenAI-compatible API surface for CLI/tools (338 providers, 100 executors)
- Request/response translation across provider formats
- Model combo fallback (multi-model sequence)
- Structured combo steps (`provider + model + connection`) with runtime ordering by `compositeTiers`

View File

@@ -451,7 +451,7 @@ open-sse/
├── types.d.ts
├── config/ Provider registries, header profiles, identity, …
├── handlers/ Request handlers (chat, embeddings, audio, image, …)
├── executors/ 107 provider-specific HTTP executors
├── executors/ 101 provider-specific HTTP executors
├── translator/ Format conversion (OpenAI ↔ Claude ↔ Gemini ↔ Cursor ↔ Kiro)
├── transformer/ Responses API ↔ Chat Completions stream transformer
├── services/ 80+ service modules (combos, fallback, quotas, identity, …)

View File

@@ -1,4 +1,4 @@
<svg viewBox="0 0 1200 350" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Animated terminal demoing the OmniRoute CLI: omniroute providers list (351 providers registered, anthropic, codex, glm, kimi shown active), omniroute combo list (always-on priority, cost-saver, fusion-panel, context-relay) and omniroute health (healthy, 18412 requests in 24h, p95 412ms, circuit breakers 24 closed, 1 half-open, 0 open), cycling over the 80+ command surface: providers, oauth, keys, combo, nodes, models, cache, compression, cost, usage, quota, health, resilience, telemetry, logs, audit, mcp, a2a, cloud, memory, skills, eval, doctor, repl, tunnel, backup, sync, webhooks, policy, pricing, translator, simulate and more.">
<svg viewBox="0 0 1200 350" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Animated terminal demoing the OmniRoute CLI: omniroute providers list (350 providers registered, anthropic, codex, glm, kimi shown active), omniroute combo list (always-on priority, cost-saver, fusion-panel, context-relay) and omniroute health (healthy, 18412 requests in 24h, p95 412ms, circuit breakers 24 closed, 1 half-open, 0 open), cycling over the 80+ command surface: providers, oauth, keys, combo, nodes, models, cache, compression, cost, usage, quota, health, resilience, telemetry, logs, audit, mcp, a2a, cloud, memory, skills, eval, doctor, repl, tunnel, backup, sync, webhooks, policy, pricing, translator, simulate and more.">
<desc>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.</desc>
<defs><clipPath id="tickerClip"><rect x="12" y="304" width="1176" height="40"/></clipPath><clipPath id="tw0"><rect x="64" y="46" height="26" width="0"><animate attributeName="width" calcMode="discrete" values="0;31;61;92;122;153;184;214;245;245" keyTimes="0;0.012;0.018;0.024;0.030;0.036;0.042;0.048;0.054;1" dur="18s" repeatCount="indefinite"/></rect></clipPath><clipPath id="tw1"><rect x="64" y="46" height="26" width="0"><animate attributeName="width" calcMode="discrete" values="0;26;51;76;102;128;153;178;204;204" keyTimes="0;0.348;0.351;0.357;0.363;0.369;0.375;0.381;0.387;1" dur="18s" repeatCount="indefinite"/></rect></clipPath><clipPath id="tw2"><rect x="64" y="46" height="26" width="0"><animate attributeName="width" calcMode="discrete" values="0;20;41;61;82;102;122;143;163;163" keyTimes="0;0.678;0.684;0.690;0.696;0.702;0.708;0.714;0.720;1" dur="18s" repeatCount="indefinite"/></rect></clipPath></defs>
<rect width="1200" height="350" fill="#0d1117"/>

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -1,4 +1,4 @@
<svg viewBox="0 0 1200 780" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Comparison table: OmniRoute versus 9router, OpenRouter, CLIProxyAPI and LiteLLM across 13 capabilities. OmniRoute is the only one with the full set: 351 providers, 90+ free providers built-in, 19 routing strategies, 12-engine token compression, a built-in MCP server with 110 tools, A2A protocol, persistent memory, guardrails, cloud agents, TLS fingerprint stealth, desktop/Termux/PWA, 43 UI locales and 100% MIT self-hosted. 9router has free providers, RTK compression and translation but no MCP, A2A, memory, guardrails, cloud agents or stealth. OpenRouter is a hosted SaaS with 400+ models, guardrails and a hosted MCP but is not self-hosted and lacks A2A, memory, cloud agents and stealth. CLIProxyAPI is a light OAuth proxy with two routing strategies. LiteLLM has 100+ providers, A2A and extensive guardrails but no memory, compression, free tier, stealth or cloud agents.">
<svg viewBox="0 0 1200 780" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Comparison table: OmniRoute versus 9router, OpenRouter, CLIProxyAPI and LiteLLM across 13 capabilities. OmniRoute is the only one with the full set: 350 providers, 90+ free providers built-in, 19 routing strategies, 12-engine token compression, a built-in MCP server with 110 tools, A2A protocol, persistent memory, guardrails, cloud agents, TLS fingerprint stealth, desktop/Termux/PWA, 43 UI locales and 100% MIT self-hosted. 9router has free providers, RTK compression and translation but no MCP, A2A, memory, guardrails, cloud agents or stealth. OpenRouter is a hosted SaaS with 400+ models, guardrails and a hosted MCP but is not self-hosted and lacks A2A, memory, cloud agents and stealth. CLIProxyAPI is a light OAuth proxy with two routing strategies. LiteLLM has 100+ providers, A2A and extensive guardrails but no memory, compression, free tier, stealth or cloud agents.">
<desc>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.</desc>
<defs>
<pattern id="gC" width="32" height="32" patternUnits="userSpaceOnUse"><path d="M 32 0 L 0 0 0 32" fill="none" stroke="#ffffff" stroke-opacity="0.05" stroke-width="1"/></pattern>

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -1,4 +1,4 @@
<svg viewBox="0 0 1200 540" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="The OmniRoute promise: one endpoint, 351 providers — never stop building, OmniRoute picks the cheapest one that works. Six pillars. Never hit limits: auto-fallback across 351 providers in milliseconds, quota out means the next provider takes over with zero downtime. Save up to 95 percent of tokens: RTK plus Caveman stacked compression cuts 15 to 95 percent of eligible tokens, about 89 percent average on tool-heavy sessions. Zero dollars to start: 90+ providers with a free tier, 56 free forever — Qoder, Pollinations, Cloudflare, SiliconFlow — no card needed. Every tool works: 33 coding agents including Claude Code, Codex, Cursor, Cline, Copilot and Antigravity through one config. One endpoint: OpenAI, Claude, Gemini and Responses API translation — point any tool at /v1 and it just works. Production-grade: circuit breakers, TLS stealth, MCP with 110 tools, A2A, memory, guardrails, evals — 25,000+ tests.">
<svg viewBox="0 0 1200 540" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="The OmniRoute promise: one endpoint, 350 providers — never stop building, OmniRoute picks the cheapest one that works. Six pillars. Never hit limits: auto-fallback across 350 providers in milliseconds, quota out means the next provider takes over with zero downtime. Save up to 95 percent of tokens: RTK plus Caveman stacked compression cuts 15 to 95 percent of eligible tokens, about 89 percent average on tool-heavy sessions. Zero dollars to start: 90+ providers with a free tier, 56 free forever — Qoder, Pollinations, Cloudflare, SiliconFlow — no card needed. Every tool works: 33 coding agents including Claude Code, Codex, Cursor, Cline, Copilot and Antigravity through one config. One endpoint: OpenAI, Claude, Gemini and Responses API translation — point any tool at /v1 and it just works. Production-grade: circuit breakers, TLS stealth, MCP with 110 tools, A2A, memory, guardrails, evals — 25,000+ tests.">
<desc>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.</desc>
<defs>
<pattern id="gridPaperP" width="32" height="32" patternUnits="userSpaceOnUse">
@@ -21,7 +21,7 @@
<line x1="150" y1="53" x2="1160" y2="53" stroke="#232b38" stroke-width="1.5"/>
</g>
<g>
<text x="40" y="100" font-family="Inter, 'Segoe UI', Arial, Helvetica, system-ui, sans-serif" font-size="23" font-weight="600" fill="#c9d1d9">One endpoint. <tspan fill="#a78bfa" font-weight="800">351 providers.</tspan> Never stop building — OmniRoute picks <tspan fill="#7ee787" font-weight="700">the cheapest one that works</tspan>.</text>
<text x="40" y="100" font-family="Inter, 'Segoe UI', Arial, Helvetica, system-ui, sans-serif" font-size="23" font-weight="600" fill="#c9d1d9">One endpoint. <tspan fill="#a78bfa" font-weight="800">350 providers.</tspan> Never stop building — OmniRoute picks <tspan fill="#7ee787" font-weight="700">the cheapest one that works</tspan>.</text>
</g>
<g font-family="Inter, 'Segoe UI', Arial, Helvetica, system-ui, sans-serif">
@@ -38,7 +38,7 @@
<line x1="3.9" y1="3.9" x2="18.1" y2="18.1"/>
</g>
<text x="102" y="170" font-size="18" font-weight="800" fill="#74b9ff">Never hit limits</text>
<text x="66" y="204" font-size="13.5" fill="#a1a1aa">Auto-fallback across 351 providers in</text>
<text x="66" y="204" font-size="13.5" fill="#a1a1aa">Auto-fallback across 350 providers in</text>
<text x="66" y="226" font-size="13.5" fill="#a1a1aa">milliseconds. Quota out? The next provider</text>
<text x="66" y="248" font-size="13.5" fill="#a1a1aa">takes over — zero downtime.</text>
</g>

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -1,4 +1,4 @@
<svg viewBox="0 0 1200 548" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="OmniRoute hero: Never stop coding. Every AI tool to 351 providers — 90+ free — through one endpoint. Claude Code, Codex, Cursor, Cline, Copilot and Antigravity into free Claude, GPT and Gemini with auto-fallback. RTK + Caveman stacked compression saves 15 to 95 percent of tokens — about 89 percent average on tool-heavy sessions — so you never hit limits. Stats: 351 AI providers, 90+ free tiers, about 1.51B free tokens per month, 15 to 95 percent token savings, 19 routing strategies, zero dollars to start.">
<svg viewBox="0 0 1200 548" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="OmniRoute hero: Never stop coding. Every AI tool to 350 providers — 90+ free — through one endpoint. Claude Code, Codex, Cursor, Cline, Copilot and Antigravity into free Claude, GPT and Gemini with auto-fallback. RTK + Caveman stacked compression saves 15 to 95 percent of tokens — about 89 percent average on tool-heavy sessions — so you never hit limits. Stats: 350 AI providers, 90+ free tiers, about 1.51B free tokens per month, 15 to 95 percent token savings, 19 routing strategies, zero dollars to start.">
<desc>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.</desc>
<defs>
<pattern id="gridPaperH" width="32" height="32" patternUnits="userSpaceOnUse">
@@ -28,7 +28,7 @@
<text x="48" y="138" font-family="Inter, 'Segoe UI', Arial, Helvetica, system-ui, sans-serif" font-size="60" font-weight="800" fill="#e9edf3">Never stop coding<tspan fill="#a855f7">.</tspan></text>
<!-- subheadline -->
<text x="48" y="184" font-family="Inter, 'Segoe UI', Arial, Helvetica, system-ui, sans-serif" font-size="25" font-weight="600" fill="#c9d1d9">Every AI tool → <tspan fill="#a78bfa" font-weight="800">351 providers</tspan><tspan fill="#7ee787" font-weight="800">90+ free</tspan> — through one endpoint.</text>
<text x="48" y="184" font-family="Inter, 'Segoe UI', Arial, Helvetica, system-ui, sans-serif" font-size="25" font-weight="600" fill="#c9d1d9">Every AI tool → <tspan fill="#a78bfa" font-weight="800">350 providers</tspan><tspan fill="#7ee787" font-weight="800">90+ free</tspan> — through one endpoint.</text>
<!-- plug line -->
<text x="48" y="222" font-family="Inter, 'Segoe UI', Arial, Helvetica, system-ui, sans-serif" font-size="16.5" fill="#a1a1aa">Claude Code · Codex · Cursor · Cline · Copilot · Antigravity&#160;&#160;&#160;&#160;<tspan fill="#7ee787" font-weight="700">FREE</tspan> Claude / GPT / Gemini · auto-fallback</text>

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 7.3 KiB

View File

@@ -1,89 +0,0 @@
---
title: "OpenAI Codex (App-Server) provider"
version: 3.8.50
lastUpdated: 2026-08-22
---
# OpenAI Codex — App-Server provider (`codex-app-server`)
OmniRoute exposes **two** ways to use OpenAI Codex:
| Provider | How it talks to OpenAI | Usage caveat |
|---|---|---|
| **`codex`** | Replays your ChatGPT/OpenAI OAuth token directly to the Responses API | **Yes** — the official session is not authorized for proxy/router use |
| **`codex-app-server`** | Drives the **Codex CLI's own `codex app-server`** over JSON-RPC/WebSocket; the CLI owns and self-refreshes its OAuth (`~/.codex/auth.json`) exactly like an interactive `codex` session | **No** — OmniRoute never replays a token to the API |
Because `codex-app-server` never replays a token, it does not carry the
session-replay usage caveat. It does require a **Codex CLI reachable at the
configured app-server URL**, and that CLI must be **signed in**.
---
## 1. Architecture
```
┌─ OmniRoute app ─────────────────┐ ┌─ codex-app-server sidecar ─────────┐
│ CodexAppServerExecutor │ WS │ codex app-server │
│ ws://codex-app-server:1456 ─────┼───────▶│ --listen ws://0.0.0.0:1456 │
│ (+ capability token) │ JSON │ --ws-auth capability-token │
│ │ RPC │ self-manages OpenAI OAuth │
└──────────────────────────────────┘ │ (~/.codex/auth.json, auto-refresh) │
│ shares (compose volumes) └─────────────────────────────────────┘
codex-appserver-token → the WS capability token (both mount it)
codex-appserver-home → ~/.codex (auth.json written by the dashboard,
read by the sidecar's codex app-server)
```
- The sidecar listens **only** on the internal compose network
(`ws://codex-app-server:1456`) behind a capability token. It is **never**
published to the host or internet.
- The Codex CLI is baked into `omniroute:base`, so no codex install is needed on
the host or the user's machine when you run the sidecar.
## 2. Bring it up
```bash
# Start the stack WITH the codex app-server sidecar profile:
docker compose --profile base --profile codex-app-server up -d
# (podman: podman compose --profile base --profile codex-app-server up -d)
```
The sidecar mints its WS capability token on first boot (into the shared
`codex-appserver-token` volume) and the app reads the same token via
`OMNIROUTE_CODEX_APPSERVER_WS_TOKEN_FILE`. No manual token wiring needed.
## 3. Connect + sign in
1. In the dashboard, add a connection for **OpenAI Codex (App-Server)**. No API
key or token is required (it's a no-auth provider — the sidecar owns auth).
2. If the sidecar's Codex CLI is **not yet signed in**, the connection health
check reports *"running but not signed in"* (not a red auth error). Use
**Sign in with ChatGPT**: this runs the standard Codex device-OAuth in your
browser and then writes `~/.codex/auth.json` into the shared volume via
**Apply auth** (the same one login serves both the `codex` and
`codex-app-server` providers).
3. Once signed in, the health check goes green (it verifies both `/readyz` **and**
`account/read` — i.e. up *and* authenticated) and turns work.
The dashboard never clobbers a healthy existing `~/.codex/auth.json` — it writes
only when the file is absent or its token is stale (a backup is always taken).
## 4. Deployment scenarios
- **Operator with an already-authenticated Codex CLI** — mount your host
`~/.codex` into the sidecar (`codex-appserver-home`) and skip the sign-in step.
- **Public user, no codex installed locally** — irrelevant: the sidecar has the
CLI. The user only authenticates through the dashboard.
- **Bare-metal OmniRoute (no sidecar, host codex)** — point
`OMNIROUTE_CODEX_APPSERVER_WS` at your own `codex app-server` and ensure the
host codex is signed in; the "codex not installed" hint appears if the binary
is missing.
## 5. Residential / UDP egress (operator extra, not shipped)
The generic sidecar above egresses over the container's normal network. An
operator who needs Codex traffic to egress via a **residential exit** (e.g. a TUN
tailscale sidecar carrying TCP + UDP/QUIC) runs that as a separate compose
override; it is intentionally **not** part of the shipped `codex-app-server`
profile. See the internal operations runbook for that setup.

View File

@@ -357,49 +357,6 @@ omniroute --port 3000
The CLI automatically loads `.env` from `~/.omniroute/.env` or `./.env`.
### Tray mode
Start OmniRoute in the system tray:
```bash
omniroute serve --tray
```
The command returns after the server and tray are ready.
The server continues without the terminal.
Tray mode supports macOS, Windows, and graphical Linux sessions. Tray mode does not open the dashboard automatically.
Use the tray menu for these actions:
- Open the dashboard.
- Open `/dashboard/logs`.
- Change auto-start.
- Stop OmniRoute.
Do not combine `--tray` with these options:
- `--daemon`
- `--log`
- `--no-recovery`
These modes require different process ownership.
Enable startup at the next machine login:
```bash
omniroute autostart enable
```
Auto-start uses tray mode on macOS, Windows, and graphical Linux sessions. Headless Linux uses the existing systemd user service.
Disable startup at login:
```bash
omniroute autostart disable
```
### Uninstalling
When you no longer need OmniRoute, we provide two quick scripts for a clean removal:

View File

@@ -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 351 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 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 350 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
- **351 AI providers** with automatic format translation
- **350 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

View File

@@ -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 351 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 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 350 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
- **351 AI providers** with automatic format translation
- **350 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

View File

@@ -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 351 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 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 350 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
- **351 AI providers** with automatic format translation
- **350 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

View File

@@ -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 351 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 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 350 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
- **351 AI providers** with automatic format translation
- **350 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

View File

@@ -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 351 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 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 350 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
- **351 AI providers** with automatic format translation
- **350 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

View File

@@ -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 351 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 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 350 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
- **351 AI providers** with automatic format translation
- **350 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

View File

@@ -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 351 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 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 350 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
- **351 AI providers** with automatic format translation
- **350 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

View File

@@ -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 351 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 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 350 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
- **351 AI providers** with automatic format translation
- **350 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

View File

@@ -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 351 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 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 350 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
- **351 AI providers** with automatic format translation
- **350 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

View File

@@ -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 351 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 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 350 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
- **351 AI providers** with automatic format translation
- **350 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

View File

@@ -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 351 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 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 350 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
- **351 AI providers** with automatic format translation
- **350 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

View File

@@ -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 351 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 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 350 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
- **351 AI providers** with automatic format translation
- **350 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

View File

@@ -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 351 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 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 350 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
- **351 AI providers** with automatic format translation
- **350 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

View File

@@ -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 351 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 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 350 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
- **351 AI providers** with automatic format translation
- **350 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

View File

@@ -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 351 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 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 350 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
- **351 AI providers** with automatic format translation
- **350 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

View File

@@ -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 351 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 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 350 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
- **351 AI providers** with automatic format translation
- **350 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

View File

@@ -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 351 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 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 350 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
- **351 AI providers** with automatic format translation
- **350 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

View File

@@ -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 351 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 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 350 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
- **351 AI providers** with automatic format translation
- **350 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

View File

@@ -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 351 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 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 350 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
- **351 AI providers** with automatic format translation
- **350 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

View File

@@ -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 351 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 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 350 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
- **351 AI providers** with automatic format translation
- **350 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

View File

@@ -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 351 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 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 350 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
- **351 AI providers** with automatic format translation
- **350 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

View File

@@ -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 351 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 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 350 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
- **351 AI providers** with automatic format translation
- **350 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

View File

@@ -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 351 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 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 350 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
- **351 AI providers** with automatic format translation
- **350 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

View File

@@ -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 351 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 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 350 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
- **351 AI providers** with automatic format translation
- **350 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

View File

@@ -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 351 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 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 350 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
- **351 AI providers** with automatic format translation
- **350 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

View File

@@ -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 351 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 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 350 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
- **351 AI providers** with automatic format translation
- **350 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

View File

@@ -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 351 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 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 350 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
- **351 AI providers** with automatic format translation
- **350 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

View File

@@ -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 351 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 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 350 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
- **351 AI providers** with automatic format translation
- **350 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

View File

@@ -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 351 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 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 350 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
- **351 AI providers** with automatic format translation
- **350 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

View File

@@ -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 351 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 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 350 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
- **351 AI providers** with automatic format translation
- **350 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

View File

@@ -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 351 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 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 350 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
- **351 AI providers** with automatic format translation
- **350 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

View File

@@ -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 351 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 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 350 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
- **351 AI providers** with automatic format translation
- **350 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

View File

@@ -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 351 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 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 350 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
- **351 AI providers** with automatic format translation
- **350 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

View File

@@ -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 351 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 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 350 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
- **351 AI providers** with automatic format translation
- **350 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

View File

@@ -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 351 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 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 350 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
- **351 AI providers** with automatic format translation
- **350 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

View File

@@ -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 351 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 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 350 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
- **351 AI providers** with automatic format translation
- **350 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

View File

@@ -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 351 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 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 350 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
- **351 AI providers** with automatic format translation
- **350 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

View File

@@ -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 351 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 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 350 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
- **351 AI providers** with automatic format translation
- **350 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

View File

@@ -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 351 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 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 350 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
- **351 AI providers** with automatic format translation
- **350 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

View File

@@ -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 351 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 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 350 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
- **351 AI providers** with automatic format translation
- **350 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

View File

@@ -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 351 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 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 350 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
- **351 AI providers** with automatic format translation
- **350 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

View File

@@ -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 351 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 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 350 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
- **351 AI providers** with automatic format translation
- **350 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

View File

@@ -176,7 +176,7 @@ All tools that appear in `/dashboard/cli-code`. Those with `baseUrlSupport: none
Tools with `baseUrlSupport: "partial"` show a badge "⚠ Base URL parcial" in the dashboard card.
---
## 2. CLI Agents Catalog (9 tools)
## 2. CLI Agents Catalog (8 tools)
Autonomous agents that appear in `/dashboard/cli-agents`:
@@ -190,7 +190,6 @@ Autonomous agents that appear in `/dashboard/cli-agents`:
| agent-deck | Agent Deck | asheshgoplani (OSS) | full | false |
| omp | Oh My Pi | OSS | full | true |
| letta | Letta CLI | Letta | full | false |
| prime-agent | Prime Agent | Prime Intellect (OSS) | full | false |
---

View File

@@ -737,12 +737,6 @@ REQUEST_TIMEOUT_MS (global override)
| `OMNIROUTE_AGENT_GOAL_READINESS_MAX_TIMEOUT_MS` | `600000` | Maximum first-event readiness window for detected `/goal` agent runs or requests forced with `x-omniroute-agent-goal`. |
| `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` | `true` | Strip non-standard `codex.*` SSE events (e.g. `codex.rate_limits`) that break the OpenAI SDK's `responses.stream()` with a 502. Default ON (#11014). Set `0`/`false`/`no`/`off` to forward them. |
| `OMNIROUTE_CODEX_APPSERVER_WS` | _(unset)_ | Opt-in Codex app-server transport. WebSocket endpoint (`ws://`/`wss://`) of a local `codex app-server` sidecar. When set together with a token, Codex requests are routed over JSON-RPC to the sidecar instead of the HTTP Responses API. Also settable per-connection via `providerSpecificData.codexAppServerUrl`. Used by `open-sse/executors/codex/appServerConfig.ts`. |
| `OMNIROUTE_CODEX_APPSERVER_WS_TOKEN` | _(unset)_ | Inline capability/bearer token presented to the app-server. Per-connection override: `providerSpecificData.codexAppServerToken`. |
| `OMNIROUTE_CODEX_APPSERVER_WS_TOKEN_FILE` | _(unset)_ | Path to a file holding the app-server capability token (from `codex app-server --ws-token-file`). Used when `OMNIROUTE_CODEX_APPSERVER_WS_TOKEN` is unset. Per-connection override: `providerSpecificData.codexAppServerTokenFile`. |
| `OMNIROUTE_CODEX_APPSERVER_CWD` | `/tmp` | Working directory the app-server turn runs in. Per-connection override: `providerSpecificData.codexAppServerCwd`. |
| `OMNIROUTE_CODEX_APPSERVER_APPROVAL` | _(unset)_ | Approval policy passed to the app-server turn (e.g. `never`, `on-request`). Per-connection override: `providerSpecificData.codexAppServerApprovalPolicy`. |
| `OMNIROUTE_CODEX_APPSERVER_SANDBOX` | _(unset)_ | Sandbox policy passed to the app-server turn (e.g. `read-only`, `workspace-write`, `danger-full-access`). Per-connection override: `providerSpecificData.codexAppServerSandbox`. |
| `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. |

View File

@@ -10,7 +10,7 @@ lastUpdated: 2026-08-23
> Regenerate with: `npm run gen:provider-reference`
> **Last generated:** 2026-08-23
Total providers: **351**. See category breakdown below.
Total providers: **350**. See category breakdown below.
## Categories
@@ -34,7 +34,7 @@ Use the dashboard at `/dashboard/providers` to enable, configure, and test each
---
## No-auth Providers (no key required) (13)
## No-auth Providers (no key required) (12)
| ID | Alias | Name | Tags | Website | Notes | Tool calling |
|----|-------|------|------|---------|-------|--------------|
@@ -42,7 +42,6 @@ Use the dashboard at `/dashboard/providers` to enable, configure, and test each
| `auggie` | `aug` | Augment (Auggie CLI) | No-auth | [link](https://augmentcode.com) | No API key stored by OmniRoute. Install the Auggie CLI and run `auggie login` on this machine, then OmniRoute spawns it locally for each request. | — |
| `chipotle` | `pepper` | Chipotle Pepper AI (Free) | No-auth | [link](https://amelia.chipotle.com) | No credentials required. Uses Chipotle's public support chatbot via reverse-engineered SockJS/STOMP protocol. | — |
| `cloudflare-playground` | `cfp` | Cloudflare AI Playground | No-auth | [link](https://playground.ai.cloudflare.com) | No credentials required — anonymous browser sessions over a reverse-engineered cf_agent WebSocket protocol (Playwright transport). | — |
| `codex-app-server` | `cxa` | OpenAI Codex (App-Server) | No-auth | [link](https://developers.openai.com/codex/cli) | No token stored by OmniRoute. The Codex CLI app-server manages its own ChatGPT sign-in (~/.codex/auth.json, auto-refreshed). Use “Sign in with ChatGPT” if the CLI is not yet authenticated. | — |
| `devin-cli-agentic` | `dva` | Devin CLI Agentic Bridge | No-auth | [link](https://docs.devin.ai/work-with-devin/devin-cli) | Authentication is owned by the official Devin CLI in its isolated bridge volume. | emulated |
| `duckduckgo-web` | `ddgw` | DuckDuckGo AI Chat | No-auth | [link](https://duckduckgo.com/duckchat) | No credentials required — DuckDuckGo AI Chat is anonymous and free. | emulated |
| `felo-web` | `felo` | Felo | No-auth | [link](https://felo.ai) | No credentials required — Felo is a free, no-signup chat/search aggregator. | — |
@@ -439,7 +438,7 @@ Use the dashboard at `/dashboard/providers` to enable, configure, and test each
- Catalog: [`src/shared/constants/providers.ts`](../../src/shared/constants/providers.ts)
- Registry (per-model details): [`open-sse/config/providerRegistry.ts`](../../open-sse/config/providerRegistry.ts)
- Executors: [`open-sse/executors/`](../../open-sse/executors/) (108 implementations)
- Executors: [`open-sse/executors/`](../../open-sse/executors/) (107 implementations)
- Translators: [`open-sse/translator/`](../../open-sse/translator/)
## See Also

View File

@@ -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 351 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 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 350 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -277,7 +277,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
- **351 AI providers** with automatic format translation
- **350 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

View File

@@ -210,7 +210,6 @@ import { baiduProvider } from "./registry/baidu/index.ts";
import { pollinationsProvider } from "./registry/pollinations/index.ts";
import { veoaifree_webProvider } from "./registry/veoaifree-web/index.ts";
import { codexProvider } from "./registry/codex/index.ts";
import { codexAppServerProvider } from "./registry/codex-app-server/index.ts";
import { veniceProvider } from "./registry/venice/index.ts";
import { kiroProvider } from "./registry/kiro/index.ts";
import { openadapterProvider } from "./registry/openadapter/index.ts";
@@ -477,7 +476,6 @@ export const REGISTRY: Record<string, RegistryEntry> = {
pollinations: pollinationsProvider,
"veoaifree-web": veoaifree_webProvider,
codex: codexProvider,
"codex-app-server": codexAppServerProvider,
venice: veniceProvider,
kiro: kiroProvider,
byteplus: byteplusProvider,

View File

@@ -1,36 +0,0 @@
import type { RegistryEntry } from "../../shared.ts";
import { codexProvider } from "../codex/index.ts";
/**
* OpenAI Codex — App-Server transport (sibling of the `codex` provider).
*
* This provider drives the Codex CLI's own `codex app-server` over JSON-RPC/
* WebSocket (executor: "codex-app-server"). Unlike the `codex` provider — which
* replays the user's ChatGPT/OpenAI OAuth token directly to the Responses API —
* the app-server process OWNS and self-refreshes its OpenAI auth
* (~/.codex/auth.json), exactly like an interactive `codex` session. OmniRoute
* never receives or replays a token, so there is no `authType: "oauth"` and no
* usage-caveat: `authType: "none"`.
*
* The connection target (ws:// URL + capability token) is supplied per-connection
* via providerSpecificData (codexAppServerUrl / codexAppServerToken[File]) and
* resolved by resolveAppServerConfig — NOT from `baseUrl` below, which is a
* documentation sentinel only.
*
* Models are shared with the `codex` provider (same underlying ChatGPT Codex
* backend), imported from codexProvider so the two stay in lockstep.
*/
export const codexAppServerProvider: RegistryEntry = {
id: "codex-app-server",
alias: "cxa",
format: "openai-responses",
executor: "codex-app-server",
// Sentinel: the executor dials the WebSocket app-server URL from
// providerSpecificData, not this baseUrl. Kept for catalog/debug display.
baseUrl: "codex-app-server://cli/websocket",
reasoningTransport: "opaque",
authType: "none",
authHeader: "none",
defaultContextLength: 400000,
models: [...codexProvider.models],
};

View File

@@ -284,21 +284,14 @@ export const GPT_5_6_API_CAPABILITIES = {
maxOutputTokens: 128000,
} as const;
// Codex OAuth catalog limits. The live OAuth `/codex/models` endpoint reports
// `context_window` (~272K, the first pricing tier) alongside
// `max_context_window` (~872K, the real usable window); requests past the
// pricing tier succeed upstream (verified: gpt-5.6-luna-xhigh served 380-390K
// input tokens with HTTP 200). The static catalog must advertise the usable
// window so the conservative discovery merge (`Math.min`) does not cap the
// live value at the pricing tier.
export const GPT_5_6_CODEX_CAPABILITIES = {
targetFormat: "openai-responses",
toolCalling: true,
supportsReasoning: true,
supportsVision: true,
supportsXHighEffort: true,
contextLength: 872000,
maxInputTokens: 872000,
contextLength: 272000,
maxInputTokens: 272000,
maxOutputTokens: 128000,
} as const;

View File

@@ -1,448 +0,0 @@
import {
bridgeToResponsesSSE,
buildResponseJSON,
} from "../vendor/codex-chatgpt-web/bridge.ts";
import { AsyncEventQueue } from "../vendor/codex-chatgpt-web/event-queue.ts";
import type { AdapterEvent } from "../vendor/codex-chatgpt-web/types.ts";
import { sanitizeErrorMessage } from "../utils/error.ts";
import { PROVIDERS } from "../config/constants.ts";
import { BaseExecutor, type ExecuteInput, type ExecutorExecuteResult } from "./base.ts";
import {
CodexAppServerClient,
type CodexAppServerClientOptions,
} from "./codex/appServerClient.ts";
import { resolveAppServerConfig, type CodexAppServerConfig } from "./codex/appServerConfig.ts";
import {
translateNotification,
translateToolCall,
type DynamicToolCallLike,
} from "./codex/appServerEvents.ts";
const JSON_HEADERS = { "Content-Type": "application/json" };
const SSE_HEADERS = {
"Cache-Control": "no-cache, no-transform",
Connection: "keep-alive",
"Content-Type": "text/event-stream; charset=utf-8",
};
/** A single text UserInput as accepted by turn/start (text_elements is required). */
interface CodexTextUserInput {
type: "text";
text: string;
text_elements: [];
}
/**
* Flatten an OpenAI Responses request body into the plain prompt text the
* app-server turn expects. The body's `input` is a string, a single message item,
* or an array of message items with `content` parts; we concatenate the user-facing
* text. This is intentionally lossless-enough for a text turn (images/tool parts are
* out of scope for the initial app-server transport).
*/
export function extractPromptText(body: unknown): string {
if (!body || typeof body !== "object") return "";
const input = (body as Record<string, unknown>).input;
if (typeof input === "string") return input;
if (input == null) return "";
const items = Array.isArray(input) ? input : [input];
const chunks: string[] = [];
for (const item of items) {
collectText(item, chunks);
}
return chunks.join("\n").trim();
}
function collectText(item: unknown, out: string[]): void {
if (typeof item === "string") {
if (item.length > 0) out.push(item);
return;
}
if (!item || typeof item !== "object") return;
const rec = item as Record<string, unknown>;
if (typeof rec.text === "string" && rec.text.length > 0) {
out.push(rec.text);
return;
}
const content = rec.content;
if (typeof content === "string") {
if (content.length > 0) out.push(content);
return;
}
if (Array.isArray(content)) {
for (const part of content) {
if (part && typeof part === "object") {
const text = (part as Record<string, unknown>).text;
if (typeof text === "string" && text.length > 0) out.push(text);
} else if (typeof part === "string" && part.length > 0) {
out.push(part);
}
}
}
}
/** Optional reasoning effort carried on the Responses body (`reasoning.effort`). */
function extractEffort(body: unknown): string | undefined {
if (!body || typeof body !== "object") return undefined;
const reasoning = (body as Record<string, unknown>).reasoning;
if (reasoning && typeof reasoning === "object") {
const effort = (reasoning as Record<string, unknown>).effort;
if (typeof effort === "string" && effort.length > 0) return effort;
}
return undefined;
}
/** A codex app-server DynamicToolSpec (experimental-api) advertised on thread/start. */
interface DynamicToolFunctionSpec {
type: "function";
name: string;
description: string;
inputSchema: Record<string, unknown>;
}
interface AppServerToolMaps {
/** wireName -> {namespace, name} for restoring MCP namespaced calls in the bridge. */
namespace: Map<string, { namespace: string; name: string }>;
/** wireNames the bridge must relay as custom_tool_call (freeform, e.g. apply_patch). */
freeform: Set<string>;
/** wireNames the bridge must relay as tool_search_call. */
toolSearch: Set<string>;
/** DynamicToolSpecs to advertise to codex on thread/start (experimental-api). */
specs: DynamicToolFunctionSpec[];
}
const EMPTY_OBJECT_SCHEMA: Record<string, unknown> = { type: "object", properties: {} };
const FREEFORM_INPUT_SCHEMA: Record<string, unknown> = {
type: "object",
properties: { input: { type: "string", description: "Raw tool input." } },
required: ["input"],
};
function asRecord(v: unknown): Record<string, unknown> | null {
return v && typeof v === "object" && !Array.isArray(v) ? (v as Record<string, unknown>) : null;
}
/**
* Build the bridge tool maps + the codex dynamicTools specs from the harness's
* Responses `tools` array. This mirrors chatgpt-web-codex.ts:toolMaps() /
* parser.ts:buildTools(): every harness tool is exposed to codex FLAT under its
* wire name ("<namespace>__<name>" for MCP tools) so the round-trip is
* namespace-preserving (codex echoes the call via item/tool/call; the bridge
* restores {namespace, name} from `toolNsMap`). Custom (freeform) and tool_search
* tools are tracked so the bridge relays them as custom_tool_call / tool_search_call.
*/
function buildAppServerToolMaps(body: unknown): AppServerToolMaps {
const namespace = new Map<string, { namespace: string; name: string }>();
const freeform = new Set<string>();
const toolSearch = new Set<string>();
const specs: DynamicToolFunctionSpec[] = [];
const rec = asRecord(body);
const tools = rec && Array.isArray(rec.tools) ? (rec.tools as unknown[]) : [];
const pushFn = (name: string, description: string, inputSchema: Record<string, unknown>) => {
specs.push({ type: "function", name, description, inputSchema });
};
for (const raw of tools) {
const t = asRecord(raw);
if (!t) continue;
const type = t.type;
const desc = typeof t.description === "string" ? t.description : "";
if (type === "function" && typeof t.name === "string") {
const wireName = t.name;
pushFn(wireName, desc, asRecord(t.parameters) ?? EMPTY_OBJECT_SCHEMA);
} else if (type === "namespace" && Array.isArray(t.tools) && typeof t.name === "string") {
const ns = t.name;
for (const innerRaw of t.tools as unknown[]) {
const inner = asRecord(innerRaw);
if (inner && inner.type === "function" && typeof inner.name === "string") {
const wireName = `${ns}__${inner.name}`;
namespace.set(wireName, { namespace: ns, name: inner.name });
const innerDesc = typeof inner.description === "string" ? inner.description : "";
pushFn(wireName, innerDesc, asRecord(inner.parameters) ?? EMPTY_OBJECT_SCHEMA);
}
}
} else if (type === "custom" && typeof t.name === "string") {
const wireName = t.name;
freeform.add(wireName);
pushFn(wireName, desc, FREEFORM_INPUT_SCHEMA);
} else if (type === "tool_search") {
const wireName = "tool_search";
toolSearch.add(wireName);
pushFn(
wireName,
desc || "Search for additional tools to load for the next turn.",
asRecord(t.parameters) ?? {
type: "object",
properties: { query: { type: "string" }, limit: { type: "number" } },
required: ["query"],
}
);
} else if (
typeof t.name === "string" &&
type !== "web_search" &&
type !== "image_generation" &&
type !== "web_search_preview"
) {
// Any other named, client-executed tool → pass through as a function so the
// routed model can call it; the bridge relays its call as a function_call.
pushFn(t.name, desc, asRecord(t.parameters) ?? EMPTY_OBJECT_SCHEMA);
}
// web_search / image_generation are OpenAI-hosted server-side tools — not relayable.
}
return { namespace, freeform, toolSearch, specs };
}
/**
* Executor for the Codex app-server WS transport. Drives one turn against a local
* `codex app-server` over JSON-RPC and re-emits its notifications as OpenAI
* Responses SSE via the shared bridge.
*
* Errors are delivered IN-BAND (an `error` AdapterEvent → `response.failed` SSE
* frame for streaming, or an error field in the JSON body for non-streaming),
* never thrown out of execute().
*/
export class CodexAppServerExecutor extends BaseExecutor {
private readonly clientOptions: CodexAppServerClientOptions;
/**
* @param clientOptions transport options (websocketFn, timeouts).
* @param providerId which provider identity this executor reports as. Defaults
* to "codex" so the existing per-connection `codexTransport==="app-server"`
* flag path (routed through CodexExecutor for the `codex` provider) keeps its
* original identity. The first-class `codex-app-server` sibling passes
* "codex-app-server" so logs/quota scoping and the golden executor map reflect
* the real provider. Falls back to PROVIDERS.codex when the sibling registry
* entry is not present (defensive; both share the codex backend).
*/
constructor(clientOptions: CodexAppServerClientOptions = {}, providerId = "codex") {
super(providerId, PROVIDERS[providerId] ?? PROVIDERS.codex);
this.clientOptions = clientOptions;
}
override async execute(input: ExecuteInput): Promise<ExecutorExecuteResult> {
const psd = input.credentials?.providerSpecificData;
const config = resolveAppServerConfig(psd);
if (!config) {
return errorResponse(
503,
"Codex app-server transport is not configured (missing url or token)",
"codex_app_server_unconfigured"
);
}
const promptText = extractPromptText(input.body);
const effort = extractEffort(input.body);
const toolMaps = buildAppServerToolMaps(input.body);
const hasTools = toolMaps.specs.length > 0;
const events = new AsyncEventQueue<AdapterEvent>();
const client = new CodexAppServerClient(this.clientOptions);
const run = async () => {
let terminated = false;
// Resolves when the turn reaches a terminal state (turn/completed, error,
// or an item/tool/call passthrough). `turn/start` resolving only means the
// turn was ACCEPTED (status: inProgress) — the model's output arrives later
// as notifications. run() MUST await this before the finally-block closes
// the client, otherwise the socket is torn down mid-turn and the event
// queue never receives its terminal event (the request then hangs until the
// caller's timeout). See translateNotification: it returns true on the
// terminal notification, which is where we settle this.
let settleTurn!: () => void;
const turnDone = new Promise<void>((resolve) => {
settleTurn = resolve;
});
const markTerminated = () => {
if (terminated) return;
terminated = true;
settleTurn();
};
const finishTurn = () => {
if (terminated) return;
events.push({ type: "done", endTurn: true });
events.close();
markTerminated();
};
try {
await client.connect(config.url, config.token);
await client.request("initialize", {
clientInfo: {
name: "omniroute-codex-app-server",
title: null,
version: "1.0",
},
// Harness function tools are advertised via thread/start's `dynamicTools`,
// which is an EXPERIMENTAL app-server field: opt into experimental API so
// codex accepts it (and can emit the item/tool/call ServerRequest).
capabilities: hasTools
? { experimentalApi: true, requestAttestation: false }
: null,
});
const threadResult = (await client.request("thread/start", {
cwd: config.cwd,
// OmniRoute is a router: the HARNESS that consumes OmniRoute owns tool
// execution and policy. codex must therefore NEVER block a turn waiting
// on its own interactive approval, and its own sandbox must not gate the
// model — the harness decides what actually runs. So we pair
// approvalPolicy:"never" (non-interactive; codex never prompts) with
// sandbox:"danger-full-access" (codex's own sandbox imposes no
// restriction), mirroring codexInstructions.ts:50 ("never +
// danger-full-access = take advantage of it"). Any server→client
// approval request that still arrives is auto-APPROVED by the client
// (see CodexAppServerClient), never denied — denial would sabotage the
// harness's tool calls. Callers can override both via providerSpecificData.
approvalPolicy: config.approvalPolicy ?? "never",
sandbox: config.sandbox ?? "danger-full-access",
// INBOUND harness tools → codex. The client tells the app-server which
// function tools are available for the thread via the `dynamicTools`
// field on thread/start (a DynamicToolSpec[] under the experimental API,
// verified from the real codex binary; see appServerEvents.ts). codex
// then invokes them by sending the `item/tool/call` ServerRequest back
// to the client (DynamicToolCallParams), which we PASS THROUGH.
...(hasTools ? { dynamicTools: toolMaps.specs } : {}),
})) as { thread?: { id?: unknown }; threadId?: unknown };
// The live app-server (codex 0.149.0) returns the thread under
// result.thread.id — NOT a top-level threadId (verified against the real
// binary 2026-08-22). Keep the top-level fallback for forward/back compat.
const threadId =
threadResult && typeof threadResult.thread?.id === "string"
? threadResult.thread.id
: threadResult && typeof threadResult.threadId === "string"
? threadResult.threadId
: "";
client.onNotification((method, params) => {
if (terminated) return;
const isTerminal = translateNotification(method, params, (event) => events.push(event));
if (isTerminal) {
events.close();
markTerminated();
}
});
// OUTBOUND codex tool call → harness. codex asks us to execute a harness
// tool via the `item/tool/call` ServerRequest. OmniRoute is a STATELESS
// ROUTER and CANNOT execute the harness's tool (the tool body lives in the
// harness downstream). So we PASS IT THROUGH: emit tool_call_* AdapterEvents
// (the bridge renders a Responses function_call / custom_tool_call /
// tool_search_call), settle the app-server request with a benign
// DynamicToolCallResponse so codex does not hang, and COMPLETE the turn.
// The harness runs the tool and replays the result in a fresh /v1/responses
// request (the stateless-full-history contract every OmniRoute provider uses).
client.onToolCall((_id, params, api) => {
if (terminated) return;
const toolParams = (params && typeof params === "object" ? params : {}) as DynamicToolCallLike;
translateToolCall(toolParams, (event) => events.push(event));
// Settle the app-server request so the socket does not stall. The router
// does not have the tool output (the harness will produce it next turn),
// so we report the passthrough as an unsuccessful in-line result and end
// the turn — the function_call has already been surfaced to the harness.
api.respond({
contentItems: [
{
type: "inputText",
text: "router: tool executed by harness; call surfaced as function_call",
},
],
success: false,
});
finishTurn();
});
const onAbort = () => {
try {
client.notify("turn/interrupt", { threadId, turnId: "" });
} catch {
/* interrupt best-effort */
}
// Unblock run() so the finally-block can tear down the client. Without
// this, an aborted request would wait on turnDone until the terminal
// notification that will never come.
if (!terminated) {
events.close();
markTerminated();
}
};
input.signal?.addEventListener("abort", onAbort, { once: true });
const turnInput: CodexTextUserInput[] = [
{ type: "text", text: promptText, text_elements: [] },
];
await client.request("turn/start", {
threadId,
input: turnInput,
model: input.model,
...(effort ? { effort } : {}),
});
// `turn/start` resolving only ACCEPTS the turn (status: inProgress). The
// model's output (agentMessage deltas) and the terminal turn/completed
// arrive AFTER, as notifications. Wait for the terminal signal before
// falling through to the finally-block — otherwise client.close() tears
// down the socket mid-turn and the queue never closes (request hangs).
await turnDone;
} catch (err) {
if (!terminated) {
events.push({
type: "error",
message: sanitizeErrorMessage(err instanceof Error ? err.message : err),
status: 502,
errorType: "provider_error",
code: "codex_app_server_turn_failed",
});
events.close();
markTerminated();
}
} finally {
client.close();
}
};
if (!input.stream) {
const running = run();
const collected = await events.collect();
await running;
const response = buildResponseJSON(collected, input.model, {
toolNsMap: toolMaps.namespace,
freeformToolNames: toolMaps.freeform,
toolSearchToolNames: toolMaps.toolSearch,
});
return {
response: new Response(JSON.stringify(response), { status: 200, headers: JSON_HEADERS }),
url: config.url,
};
}
void run();
const stream = bridgeToResponsesSSE(
events,
input.model,
toolMaps.namespace,
toolMaps.freeform,
toolMaps.toolSearch,
() => client.close(),
2_000
);
return {
response: new Response(stream, { status: 200, headers: SSE_HEADERS }),
url: config.url,
};
}
}
function errorResponse(status: number, message: string, code: string): Response {
return new Response(
JSON.stringify({
error: {
code,
message: sanitizeErrorMessage(message),
type: status >= 500 ? "provider_error" : "invalid_request_error",
},
}),
{ status, headers: JSON_HEADERS }
);
}
// re-export config type for consumers/tests
export type { CodexAppServerConfig };

View File

@@ -58,8 +58,6 @@ import {
type CodexEffortLevel as EffortLevel,
} from "./codex/reasoningSuffix.ts";
import { repairMissingCodexToolCallOutputs } from "./codex/toolCallRepair.ts";
import { resolveAppServerConfig } from "./codex/appServerConfig.ts";
import { CodexAppServerExecutor } from "./codex-app-server.ts";
// Re-exported for external importers (tests + provider services).
export { isCodexFreePlan, normalizeCodexTools } from "./codex/tools.ts";
@@ -104,12 +102,6 @@ export function __setCodexWebSocketTransportForTesting(
_websocketOverride = websocket;
}
// Exposed for the app-server transport, which needs the same wreq-js websocket
// factory (with the testing override honored) to open its JSON-RPC socket.
export function getCodexAppServerWebsocketTransport(): WebsocketFn | null {
return getCodexWebSocketTransport();
}
function codexWebSocketUnavailableResponse(): Response {
return new Response(
JSON.stringify({
@@ -403,34 +395,6 @@ function isCodexWsGloballyEnabled(): boolean {
}
}
/**
* Global Codex app-server kill-switch (feature flag OMNIROUTE_CODEX_APP_SERVER_ENABLED,
* default ON). Fail-open, mirroring isCodexWsGloballyEnabled.
*/
function isCodexAppServerGloballyEnabled(): boolean {
try {
return isFeatureFlagEnabled("OMNIROUTE_CODEX_APP_SERVER_ENABLED");
} catch {
return true;
}
}
/**
* True when the connection opted into the app-server transport
* (providerSpecificData.codexTransport === "app-server") AND the app-server is
* configured (URL + token resolvable) AND the global flag is on. Selected BEFORE
* the websocket check so it wins when configured.
*/
export function isCodexAppServerRequired(credentials: unknown): boolean {
if (!isCodexAppServerGloballyEnabled()) return false;
const providerSpecificData =
credentials && typeof credentials === "object"
? (credentials as { providerSpecificData?: Record<string, unknown> }).providerSpecificData
: null;
if (providerSpecificData?.codexTransport !== "app-server") return false;
return !!resolveAppServerConfig(providerSpecificData);
}
export function isCodexResponsesWebSocketRequired(_model: string, credentials: unknown): boolean {
// Global kill-switch (default ON). When disabled, Codex never uses the WS
// transport — even per-connection codexTransport=websocket falls back to the
@@ -796,8 +760,6 @@ function normalizeCodexWsHeaders(headers: Record<string, string>): Record<string
* IMPORTANT: Includes chatgpt-account-id header for workspace binding.
*/
export class CodexExecutor extends BaseExecutor {
private appServer: CodexAppServerExecutor | null = null;
constructor() {
super("codex", PROVIDERS.codex);
}
@@ -816,15 +778,6 @@ export class CodexExecutor extends BaseExecutor {
);
const nextInput = { ...requestInput, credentials };
if (isCodexAppServerRequired(nextInput.credentials)) {
if (!this.appServer) {
this.appServer = new CodexAppServerExecutor({
websocketFn: getCodexAppServerWebsocketTransport(),
});
}
return this.appServer.execute(nextInput);
}
if (!isCodexResponsesWebSocketRequired(nextInput.model, nextInput.credentials)) {
const httpResult = await super.execute(nextInput);
if (codexDropNonstandardEvents()) {

View File

@@ -1,102 +0,0 @@
/**
* Layer-2 auth-status probe for the Codex app-server transport.
*
* The HTTP `/readyz` endpoint only proves the app-server PROCESS is up — not that
* its Codex CLI is signed in. A public user whose CLI is not yet authenticated
* would otherwise see a green "ready" badge and then fail on the first turn with
* an upstream auth error. This probe opens the same JSON-RPC/WebSocket the
* executor uses and calls `account/read` (verified against codex 0.149.0): an
* authenticated server returns `{ account: { type, email, planType }, ... }`;
* a logged-out server returns no account (or an error). So the presence of
* `result.account` is the "authenticated" signal.
*
* Kept separate from the executor turn path so the health check pulls in only the
* lightweight client + transport, and so it is independently unit-testable with a
* fake websocketFn.
*/
import {
CodexAppServerClient,
type CodexAppServerWebsocketFn,
} from "./appServerClient.ts";
import type { CodexAppServerConfig } from "./appServerConfig.ts";
export type CodexAppServerAuthStatus =
| { state: "authenticated"; account: { type?: string; email?: string; planType?: string } }
| { state: "logged_out"; reason: string }
| { state: "unknown"; reason: string };
interface AccountReadResult {
account?: { type?: unknown; email?: unknown; planType?: unknown } | null;
requiresOpenaiAuth?: unknown;
}
function str(v: unknown): string | undefined {
return typeof v === "string" && v.length > 0 ? v : undefined;
}
/**
* Open a short-lived WS to the app-server, initialize, and read the account.
* Returns an auth status; never throws (maps failures to state "unknown").
*
* @param config resolved app-server config (url + capability token).
* @param websocketFn the wreq-js websocket factory
* (getCodexAppServerWebsocketTransport()); when null, returns "unknown".
* @param timeoutMs overall budget for connect + account/read.
*/
export async function probeCodexAppServerAuth(
config: CodexAppServerConfig,
websocketFn: CodexAppServerWebsocketFn | null,
timeoutMs = 8000
): Promise<CodexAppServerAuthStatus> {
if (!websocketFn) {
return { state: "unknown", reason: "websocket transport unavailable" };
}
const client = new CodexAppServerClient({ websocketFn, defaultTimeoutMs: timeoutMs });
const deadline = new Promise<CodexAppServerAuthStatus>((resolve) =>
setTimeout(() => resolve({ state: "unknown", reason: "auth probe timed out" }), timeoutMs)
);
const run = (async (): Promise<CodexAppServerAuthStatus> => {
try {
await client.connect(config.url, config.token);
await client.request(
"initialize",
{
clientInfo: { name: "omniroute-codex-app-server-health", title: null, version: "1.0" },
capabilities: null,
},
timeoutMs
);
// account/read: authenticated → { account: {...} }; logged out → no account.
const result = (await client.request("account/read", {}, timeoutMs)) as AccountReadResult;
const account = result?.account;
if (account && typeof account === "object") {
return {
state: "authenticated",
account: {
type: str(account.type),
email: str(account.email),
planType: str(account.planType),
},
};
}
return {
state: "logged_out",
reason: "app-server reachable but its Codex CLI is not signed in",
};
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
// A JSON-RPC error on account/read (e.g. AuthRequiredError) also means
// "up but not authenticated" — surface it as logged_out, not unknown, so
// the dashboard offers "Sign in with ChatGPT" rather than a scary error.
if (/auth|login|sign|unauthor|401/i.test(message)) {
return { state: "logged_out", reason: message };
}
return { state: "unknown", reason: message };
} finally {
client.close();
}
})();
return Promise.race([run, deadline]);
}

View File

@@ -1,289 +0,0 @@
/**
* Id-correlated JSON-RPC 2.0 client over a single WebSocket, for the Codex
* app-server transport.
*
* Ported from the stdio JSON-RPC pattern in `devin-cli-agentic.ts` (monotonic id,
* pending-request map settled on responses, notification vs response
* discrimination, settle-once) onto the wreq-js WebSocket transport used by the
* existing Codex WS path.
*
* The critical addition over the other transports is a catch-all handler for
* server -> client ServerRequests: the app-server can ask the client to approve a
* command / patch / permission. OmniRoute is a ROUTER — the harness that consumes
* it owns tool execution and policy — so codex must never stall a turn on its own
* interactive approval. Every inbound ServerRequest is always answered: approval
* prompts are auto-APPROVED (so the model's agentic tool calls proceed; the harness
* decides what really runs), and anything else we can't service gets a JSON-RPC
* error so the id is always settled and the turn never hangs.
*/
// wreq-js WebSocket surface (mirrors the private type in codex.ts:71-77).
export type CodexWreqWebSocket = {
send: (data: string) => void;
close: (code?: number, reason?: string) => void;
onmessage: ((event: { data: unknown }) => void) | null;
onerror: ((event: { message?: string }) => void) | null;
onclose: (() => void) | null;
};
export type CodexAppServerWebsocketFn = (
url: string,
opts?: Record<string, unknown>
) => Promise<CodexWreqWebSocket>;
interface PendingReq {
resolve: (result: unknown) => void;
reject: (err: Error) => void;
}
// The set of ServerRequest methods that are approval prompts (see PROTOCOL-DIGEST
// "Server -> client REQUESTS"). All of these get an auto-denial decision.
const APPROVAL_REQUEST_METHODS = new Set<string>([
"item/commandExecution/requestApproval",
"item/fileChange/requestApproval",
"item/permissions/requestApproval",
"applyPatchApproval",
"execCommandApproval",
]);
const ROUTER_APPROVAL_NOTE = "router: harness-controlled execution";
export interface CodexAppServerClientOptions {
/** Transport factory. Defaults to the shared wreq-js websocket() when omitted. */
websocketFn?: CodexAppServerWebsocketFn | null;
/** Default per-request timeout (ms). */
defaultTimeoutMs?: number;
}
/**
* The app-server → client REQUEST method by which codex invokes a harness-defined
* (dynamic) function tool. See appServerEvents.ts:CODEX_APPSERVER_TOOL_CALL_METHOD.
* A stateless router cannot execute the harness's tool, so this is handled by a
* PASSTHROUGH handler (surface it as a Responses function_call and complete the
* turn) rather than by the default -32601 rejection.
*/
const TOOL_CALL_REQUEST_METHOD = "item/tool/call";
/**
* Handler for a server → client `item/tool/call` ServerRequest. It receives the
* JSON-RPC id and raw params (DynamicToolCallParams). It OWNS settling the id
* (call `respond`/`respondError`) so the socket never hangs. Returning lets the
* executor emit tool_call_* AdapterEvents + complete the turn.
*/
export type CodexAppServerToolCallHandler = (
id: number,
params: unknown,
api: {
/** Settle the request id with a JSON-RPC result (a DynamicToolCallResponse). */
respond: (result: unknown) => void;
/** Settle the request id with a JSON-RPC error. */
respondError: (code: number, message: string) => void;
}
) => void;
export class CodexAppServerClient {
private ws: CodexWreqWebSocket | null = null;
private nextId = 1;
private readonly pending = new Map<number, PendingReq>();
private notificationHandler: (method: string, params: unknown) => void = () => {};
private toolCallHandler: CodexAppServerToolCallHandler | null = null;
private readonly websocketFn: CodexAppServerWebsocketFn | null;
private readonly defaultTimeoutMs: number;
private closed = false;
constructor(options: CodexAppServerClientOptions = {}) {
this.websocketFn = options.websocketFn ?? null;
this.defaultTimeoutMs = options.defaultTimeoutMs ?? 120_000;
}
/**
* Open the WebSocket and attach the capability token as `Authorization: Bearer`.
* Do NOT add any chatgpt.com Origin/WS header normalization here — the local
* app-server wants only the Authorization header.
*/
async connect(url: string, token: string): Promise<void> {
if (!this.websocketFn) {
throw new Error("Codex app-server websocket transport unavailable");
}
// wreq-js's websocket() REQUIRES a browser/os impersonation profile alongside
// headers — the same shape the existing Codex WS path uses (codex.ts:980).
// Omitting browser/os makes the native call hang/throw, so the app-server
// turn never connects. The local app-server ignores the impersonation
// fingerprint; only the Authorization bearer matters for its ws-auth.
this.ws = await this.websocketFn(url, {
browser: "chrome_142",
os: "windows",
headers: { Authorization: `Bearer ${token}` },
});
this.ws.onmessage = (event) => this.onFrame(event.data);
this.ws.onerror = (event) => this.failAll(event?.message ?? "app-server socket error");
this.ws.onclose = () => this.failAll("app-server connection closed");
}
/** Send a ClientRequest and resolve when its id-matched response arrives. */
request<T = unknown>(method: string, params: unknown, timeoutMs = this.defaultTimeoutMs): Promise<T> {
const id = this.nextId++;
return new Promise<T>((resolve, reject) => {
if (!this.ws || this.closed) {
reject(new Error(`Cannot send ${method}: app-server connection is not open`));
return;
}
const timer = setTimeout(() => {
this.pending.delete(id);
reject(new Error(`Codex app-server request "${method}" timed out after ${timeoutMs}ms`));
}, timeoutMs);
this.pending.set(id, {
resolve: (result) => {
clearTimeout(timer);
resolve(result as T);
},
reject: (err) => {
clearTimeout(timer);
reject(err);
},
});
this.ws.send(JSON.stringify({ jsonrpc: "2.0", id, method, params }));
});
}
/** Send a ClientNotification (no id, no reply expected — e.g. turn/interrupt). */
notify(method: string, params: unknown): void {
if (!this.ws || this.closed) return;
this.ws.send(JSON.stringify({ jsonrpc: "2.0", method, params }));
}
/** Register the handler that receives server -> client NOTIFICATIONS (no id). */
onNotification(fn: (method: string, params: unknown) => void): void {
this.notificationHandler = fn;
}
/**
* Register the handler for the `item/tool/call` server → client ServerRequest
* (a harness function-tool invocation). When set, `item/tool/call` is routed to
* this handler INSTEAD of the default -32601 rejection; the handler must settle
* the id via the provided `respond`/`respondError`. When unset, `item/tool/call`
* falls through to the default rejection (keeps the turn unstuck).
*/
onToolCall(fn: CodexAppServerToolCallHandler): void {
this.toolCallHandler = fn;
}
close(): void {
if (this.closed) return;
this.closed = true;
try {
this.ws?.close(1000, "done");
} catch {
/* socket close race — ignore */
}
}
/** Parse one inbound frame and dispatch by JSON-RPC shape. */
private onFrame(raw: unknown): void {
let msg: Record<string, unknown>;
try {
const line = typeof raw === "string" ? raw : Buffer.from(raw as Uint8Array).toString("utf8");
msg = JSON.parse(line) as Record<string, unknown>;
} catch {
// A non-JSON frame is unusable; drop it rather than crash the socket.
return;
}
const hasId = msg.id !== undefined && msg.id !== null;
const hasMethod = typeof msg.method === "string";
if (hasId && !hasMethod) {
// A RESPONSE to one of our ClientRequests → settle the pending map.
const id = msg.id as number;
const pending = this.pending.get(id);
if (!pending) return;
this.pending.delete(id);
if (msg.error) {
const err = msg.error as { code?: unknown; message?: unknown };
pending.reject(new Error(`${String(err.code ?? "error")}: ${String(err.message ?? "unknown")}`));
} else {
pending.resolve(msg.result);
}
return;
}
if (hasMethod && hasId) {
// A server -> client REQUEST → we MUST reply with the matching id or the turn stalls.
const id = msg.id as number;
const method = msg.method as string;
// A harness function-tool invocation is routed to the passthrough handler
// (if registered) so the executor can surface it as a Responses function_call
// and complete the turn. The handler owns settling the id.
if (method === TOOL_CALL_REQUEST_METHOD && this.toolCallHandler) {
this.toolCallHandler(id, msg.params, {
respond: (result) => this.respondToRequest(id, result),
respondError: (code, message) => this.respondErrorToRequest(id, code, message),
});
return;
}
this.answerServerRequest(id, method);
return;
}
if (hasMethod) {
// A server -> client NOTIFICATION → hand to the stream.
this.notificationHandler(msg.method as string, msg.params);
}
}
/**
* Always answer an inbound ServerRequest so its id is settled. Approval prompts
* are auto-APPROVED (OmniRoute is a router; the harness that consumes it owns
* execution policy, so codex's own approval must not block the turn). Anything
* we cannot service gets a JSON-RPC error so the id is still settled.
*/
private answerServerRequest(id: number, method: string): void {
if (!this.ws || this.closed) return;
if (APPROVAL_REQUEST_METHODS.has(method)) {
// ReviewDecision "approved" — let the model's agentic action proceed. The
// harness downstream of OmniRoute is the real gate. Note the note field is
// advisory; the decision string is what codex acts on.
this.ws.send(
JSON.stringify({
jsonrpc: "2.0",
id,
result: { decision: "approved", note: ROUTER_APPROVAL_NOTE },
})
);
return;
}
// Non-approval server request we do not service here: reject the id so the
// app-server does not wait on us (belt-and-suspenders; keeps turns unstuck).
this.ws.send(
JSON.stringify({
jsonrpc: "2.0",
id,
error: {
code: -32601,
message: `router: unsupported server request "${method}"`,
},
})
);
}
/** Settle an inbound ServerRequest id with a JSON-RPC result. */
private respondToRequest(id: number, result: unknown): void {
if (!this.ws || this.closed) return;
this.ws.send(JSON.stringify({ jsonrpc: "2.0", id, result }));
}
/** Settle an inbound ServerRequest id with a JSON-RPC error. */
private respondErrorToRequest(id: number, code: number, message: string): void {
if (!this.ws || this.closed) return;
this.ws.send(JSON.stringify({ jsonrpc: "2.0", id, error: { code, message } }));
}
private failAll(reason: string): void {
const err = new Error(reason);
for (const [id, pending] of this.pending.entries()) {
this.pending.delete(id);
pending.reject(err);
}
this.notificationHandler("__transport_closed__", { reason });
}
}

View File

@@ -1,94 +0,0 @@
import { readFileSync } from "node:fs";
/**
* Resolved connection config for the Codex app-server WS transport.
*
* The app-server is a locally-running `codex app-server` process reachable over a
* single WebSocket speaking JSON-RPC 2.0. It self-manages OpenAI auth + model
* routing; the ONLY credential OmniRoute presents is the capability token, sent as
* `Authorization: Bearer <hex>` on the WS handshake.
*/
export interface CodexAppServerConfig {
/** ws:// or wss:// URL of the app-server (e.g. "ws://ts-egress:1456"). */
url: string;
/** Capability token (hex string) sent as `Authorization: Bearer <token>`. */
token: string;
/** Working directory passed to `thread/start { cwd }` inside the codex container. */
cwd: string;
/**
* Optional codex approval policy override (AskForApproval). Defaults to "never"
* in the executor so codex runs non-interactively and never blocks the turn on
* its own approval — the harness that consumes OmniRoute owns execution policy.
*/
approvalPolicy?: string;
/**
* Optional codex sandbox override (SandboxMode). Defaults to "danger-full-access"
* in the executor so codex's own sandbox does not gate the model; the harness is
* the real gate. Callers may tighten this per request via providerSpecificData.
*/
sandbox?: string;
}
type ProviderSpecificData = Record<string, unknown> | null | undefined;
function firstString(...values: unknown[]): string | null {
for (const value of values) {
if (typeof value === "string" && value.trim().length > 0) return value.trim();
}
return null;
}
/**
* Read the capability token, preferring an inline token, then a token FILE path.
* The token file (produced by `codex app-server --ws-token-file <path>`) holds the
* same hex string that is presented as the bearer token.
*/
function resolveToken(psd: ProviderSpecificData): string | null {
const inline = firstString(
psd?.codexAppServerToken,
process.env.OMNIROUTE_CODEX_APPSERVER_WS_TOKEN
);
if (inline) return inline;
const tokenFile = firstString(
psd?.codexAppServerTokenFile,
process.env.OMNIROUTE_CODEX_APPSERVER_WS_TOKEN_FILE
);
if (!tokenFile) return null;
try {
const contents = readFileSync(tokenFile, "utf8").trim();
return contents.length > 0 ? contents : null;
} catch {
return null;
}
}
function isWebSocketUrl(url: string): boolean {
return url.startsWith("ws://") || url.startsWith("wss://");
}
/**
* Resolve the app-server connection config from providerSpecificData with env
* fallbacks. Returns `null` when not fully configured (URL + token both required)
* so the gating predicate `isCodexAppServerRequired` stays false and Codex falls
* back to its other transports.
*/
export function resolveAppServerConfig(psd: ProviderSpecificData): CodexAppServerConfig | null {
const url = firstString(psd?.codexAppServerUrl, process.env.OMNIROUTE_CODEX_APPSERVER_WS);
if (!url || !isWebSocketUrl(url)) return null;
const token = resolveToken(psd);
if (!token) return null;
const cwd =
firstString(psd?.codexAppServerCwd, process.env.OMNIROUTE_CODEX_APPSERVER_CWD) ?? "/tmp";
const approvalPolicy =
firstString(psd?.codexAppServerApprovalPolicy, process.env.OMNIROUTE_CODEX_APPSERVER_APPROVAL) ??
undefined;
const sandbox =
firstString(psd?.codexAppServerSandbox, process.env.OMNIROUTE_CODEX_APPSERVER_SANDBOX) ??
undefined;
return { url, token, cwd, ...(approvalPolicy ? { approvalPolicy } : {}), ...(sandbox ? { sandbox } : {}) };
}

View File

@@ -1,208 +0,0 @@
import type { AdapterEvent, CodexUsage } from "../../vendor/codex-chatgpt-web/types.ts";
/**
* Map Codex app-server JSON-RPC notifications onto the AdapterEvent stream that
* `bridgeToResponsesSSE` / `buildResponseJSON` consume.
*
* Wire method names are the slash-notation ServerNotification variants verified
* from the real codex binary (see PROTOCOL-DIGEST.md). Only the handful needed for
* a plain text turn are mapped; everything else is ignored.
*
* The `*Notification` param TYPES referenced below (adapted from the ts-rs bindings):
* AgentMessageDeltaNotification { threadId, turnId, itemId, delta }
* ReasoningTextDeltaNotification { threadId, turnId, itemId, delta, contentIndex }
* TurnCompletedNotification { threadId, turn } (turn carries usage)
* ErrorNotification { error, willRetry, threadId, turnId }
*/
// Wire method names (slash-notation) → intent. Kept as named constants so a typo
// can't silently break the mapping.
export const CODEX_APPSERVER_METHODS = {
agentMessageDelta: "item/agentMessage/delta",
reasoningTextDelta: "item/reasoning/textDelta",
reasoningSummaryTextDelta: "item/reasoning/summaryTextDelta",
turnCompleted: "turn/completed",
error: "error",
} as const;
/**
* The app-server → client REQUEST method by which codex invokes a harness-defined
* (dynamic) function tool. It is NOT a notification: it is a server→client
* ServerRequest that BLOCKS the codex turn waiting for a `DynamicToolCallResponse`
* with the tool's output.
*
* `params` shape = `DynamicToolCallParams` (ts-rs binding):
* { threadId, turnId, callId, namespace: string | null, tool: string, arguments: JsonValue }
*
* OmniRoute is a STATELESS ROUTER: it cannot execute the harness's tool (the tool
* body lives in the harness downstream, not here). So instead of "executing" the
* call, we PASS IT THROUGH: emit tool_call_* AdapterEvents so the bridge renders a
* Responses `function_call` output item, then complete the turn. The harness runs
* the tool and replays the result in a fresh /v1/responses request (the same
* stateless-full-history contract every other OmniRoute provider uses).
*/
export const CODEX_APPSERVER_TOOL_CALL_METHOD = "item/tool/call";
/** Minimal shape of the DynamicToolCallParams we consume for the passthrough. */
export interface DynamicToolCallLike {
callId?: unknown;
namespace?: unknown;
tool?: unknown;
arguments?: unknown;
}
/**
* The wire name the bridge's `toolNsMap` is keyed by: namespaced (MCP) tools are
* flattened to "<namespace>__<name>". codex sends the namespace + tool separately
* on DynamicToolCallParams, so we reconstruct the flat name for the round-trip.
*/
export function dynamicToolWireName(namespace: unknown, tool: unknown): string {
const name = typeof tool === "string" ? tool : "";
return typeof namespace === "string" && namespace.length > 0
? `${namespace}__${name}`
: name;
}
/**
* Translate ONE codex `item/tool/call` ServerRequest into the tool_call_* AdapterEvent
* triple the bridge already knows how to turn into a Responses function_call /
* custom_tool_call / tool_search_call (see bridge.ts:700-784). The `arguments` are
* serialized to a JSON string (the bridge accumulates `tool_call_delta.arguments`
* as a string and JSON.parses it at close).
*
* This emits the COMPLETE call in one shot (start → delta → end) because the
* server-request carries the fully-formed arguments (codex does not stream dynamic
* tool-call arguments to the client the way the chatgpt-web adapter streams native
* ones). The caller is responsible for then completing the turn.
*/
export function translateToolCall(
params: DynamicToolCallLike,
push: (event: AdapterEvent) => void
): void {
const callId =
typeof params.callId === "string" && params.callId.length > 0
? params.callId
: `call_${Math.random().toString(36).slice(2)}`;
const name = dynamicToolWireName(params.namespace, params.tool);
let argsStr = "{}";
const rawArgs = params.arguments;
if (typeof rawArgs === "string") {
argsStr = rawArgs.length > 0 ? rawArgs : "{}";
} else if (rawArgs !== undefined && rawArgs !== null) {
try {
argsStr = JSON.stringify(rawArgs);
} catch {
argsStr = "{}";
}
}
push({ type: "tool_call_start", id: callId, name });
if (argsStr.length > 0) push({ type: "tool_call_delta", arguments: argsStr });
push({ type: "tool_call_end" });
}
interface RawUsage {
input_tokens?: number;
cached_input_tokens?: number;
output_tokens?: number;
reasoning_output_tokens?: number;
total_tokens?: number;
}
/** Extract a numeric field defensively (the wire may omit or null it). */
function num(value: unknown): number | undefined {
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
}
/**
* Convert the app-server usage shape (snake_case token counts) into the canonical
* CodexUsage the bridge expects. Returns undefined when nothing usable is present.
*/
export function mapUsage(raw: unknown): CodexUsage | undefined {
if (!raw || typeof raw !== "object") return undefined;
const u = raw as RawUsage;
const inputTokens = num(u.input_tokens) ?? 0;
const outputTokens = num(u.output_tokens) ?? 0;
const usage: CodexUsage = { inputTokens, outputTokens };
const cached = num(u.cached_input_tokens);
if (cached !== undefined) {
usage.cachedInputTokens = cached;
usage.cacheReadInputTokens = cached;
}
const reasoning = num(u.reasoning_output_tokens);
if (reasoning !== undefined) usage.reasoningOutputTokens = reasoning;
const total = num(u.total_tokens);
if (total !== undefined) usage.totalTokens = total;
return usage;
}
/**
* Pull a usage object out of a `turn/completed` param. The Turn payload carries
* token counts; different app-server builds nest it under `usage` or `tokenUsage`,
* so probe both before giving up.
*/
function extractTurnUsage(params: Record<string, unknown>): CodexUsage | undefined {
const turn = params.turn;
if (turn && typeof turn === "object") {
const t = turn as Record<string, unknown>;
return mapUsage(t.usage) ?? mapUsage(t.tokenUsage) ?? mapUsage(t.token_usage);
}
return mapUsage(params.usage);
}
function errorMessage(params: Record<string, unknown>): string {
const err = params.error;
if (err && typeof err === "object") {
const m = (err as Record<string, unknown>).message;
if (typeof m === "string" && m.length > 0) return m;
}
if (typeof params.message === "string" && params.message.length > 0) return params.message;
return "Codex app-server reported an error";
}
/**
* Translate one notification into AdapterEvent(s) and push them into the queue.
*
* Returns `true` when the notification is terminal (turn/completed or error), so
* the caller can close the event queue after draining.
*/
export function translateNotification(
method: string,
params: unknown,
push: (event: AdapterEvent) => void
): boolean {
const p = (params && typeof params === "object" ? params : {}) as Record<string, unknown>;
switch (method) {
case CODEX_APPSERVER_METHODS.agentMessageDelta: {
const delta = p.delta;
if (typeof delta === "string" && delta.length > 0) {
push({ type: "text_delta", text: delta });
}
return false;
}
case CODEX_APPSERVER_METHODS.reasoningTextDelta:
case CODEX_APPSERVER_METHODS.reasoningSummaryTextDelta: {
const delta = p.delta;
if (typeof delta === "string" && delta.length > 0) {
push({ type: "thinking_delta", thinking: delta });
}
return false;
}
case CODEX_APPSERVER_METHODS.turnCompleted: {
push({ type: "done", usage: extractTurnUsage(p), endTurn: true });
return true;
}
case CODEX_APPSERVER_METHODS.error: {
push({
type: "error",
message: errorMessage(p),
status: 502,
errorType: "provider_error",
code: "codex_app_server_turn_failed",
});
return true;
}
default:
return false;
}
}

View File

@@ -7,7 +7,6 @@ import { GheCopilotExecutor } from "./ghe-copilot.ts";
import { QoderExecutor } from "./qoder.ts";
import { KiroExecutor } from "./kiro.ts";
import { CodexExecutor } from "./codex.ts";
import { CodexAppServerExecutor } from "./codex-app-server.ts";
import { CursorExecutor } from "./cursor.ts";
import { TraeExecutor } from "./trae.ts";
import { DefaultExecutor } from "./default.ts";
@@ -98,7 +97,6 @@ const executors = {
"amazon-q": new KiroExecutor("amazon-q"),
bedrock: new BedrockExecutor(),
codex: new CodexExecutor(),
"codex-app-server": new CodexAppServerExecutor({}, "codex-app-server"),
"chatgpt-web-codex": new ChatGptWebCodexExecutor(),
"cgpt-codex": new ChatGptWebCodexExecutor(),
cursor: new CursorExecutor(),

View File

@@ -1,9 +1,4 @@
import {
BaseExecutor,
type ExecuteInput,
type ExecutorExecuteResult,
type ProviderCredentials,
} from "./base.ts";
import { BaseExecutor, type ExecuteInput, type ProviderCredentials } from "./base.ts";
import { PROVIDERS } from "../config/constants.ts";
import { getModelTargetFormat, PROVIDER_ID_TO_ALIAS } from "../config/providerModels.ts";
import {
@@ -150,101 +145,6 @@ export function resolveOpencodeTargetFormat(provider: string, model: string): st
return getModelTargetFormat(alias, model) || "openai";
}
/**
* muse-spark (opencode-go) burns its entire output budget on invisible
* server-side reasoning before emitting any content. With small caller-set
* budgets the upstream answers HTTP 200 with an empty message
* (`{"message":{"role":"assistant"},"finish_reason":null}` and
* `completion_tokens == max_tokens`) — chatCore then flags the fake success as
* "Provider returned empty content" / 502 and burns a fallback attempt.
*
* Verified live 2026-08-23: max_tokens=64/100 → empty content;
* 256/512/1024 → content present (hidden reasoning consumed 196253 of it).
*
* Floor raised budgets only — explicit large budgets and non-muse-spark models
* are untouched, and no budget is synthesized when the caller set none.
*/
export const MUSE_SPARK_MIN_OUTPUT_TOKENS = 512;
export function applyMuseSparkMinOutputTokens(model: string, body: Record<string, unknown>): void {
if (!model.startsWith("muse-spark")) return;
const current = body.max_tokens;
if (typeof current !== "number" || !Number.isFinite(current)) return;
if (current >= MUSE_SPARK_MIN_OUTPUT_TOKENS) return;
body.max_tokens = MUSE_SPARK_MIN_OUTPUT_TOKENS;
}
/**
* muse-spark's gateway reports `finish_reason:"length"` whenever its hidden
* reasoning consumed part of the output budget — even when the visible
* completion is tiny relative to the requested budget (observed: ~270
* completion tokens on a 128000-token request). OpenAI-protocol clients map a
* "length" stop onto the caller's own max-tokens cap, so Claude Code aborts a
* fully-delivered answer with "response exceeded the 128000 output token
* maximum".
*
* Rewrite `length` → `stop` when the reported completion count proves the real
* token limit was never reached (<90% of the caller's budget). Genuine
* truncations at the budget are preserved. Streaming frames carry usage before
* the terminal finish frame, so the completion count is known in time.
*/
export function normalizeMuseSparkFinishReason(
payload: Record<string, unknown>,
requestedBudget: number | null,
/** Streaming: usage arrives in an earlier frame than the finish frame — caller passes the tracked count here. */
completionOverride?: number | null
): void {
const choices = Array.isArray(payload.choices) ? payload.choices : [];
for (const choice of choices) {
if (!choice || typeof choice !== "object") continue;
const record = choice as Record<string, unknown>;
if (record.finish_reason !== "length") continue;
if (requestedBudget === null || requestedBudget === undefined) continue;
const usage = payload.usage as Record<string, unknown> | undefined;
const completion =
typeof completionOverride === "number"
? completionOverride
: typeof usage?.completion_tokens === "number"
? usage.completion_tokens
: null;
if (completion === null) continue;
if (completion < Math.floor(requestedBudget * 0.9)) {
record.finish_reason = "stop";
}
}
}
/** SSE line normalizer for muse-spark streams: tracks usage, rewrites finish frames. */
export function createMuseSparkStreamFinishNormalizer(
requestedBudget: number | null
): (dataLine: string) => string {
let completionTokens: number | null = null;
return (line: string): string => {
const trimmed = line.trim();
if (!trimmed.startsWith("data:") || trimmed.includes("[DONE]")) return line;
let parsed: unknown;
try {
parsed = JSON.parse(trimmed.slice(5).trim());
} catch {
return line;
}
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return line;
const payload = parsed as Record<string, unknown>;
const usage = payload.usage as Record<string, unknown> | undefined;
if (usage && typeof usage.completion_tokens === "number") {
completionTokens = usage.completion_tokens;
}
const hadFinish = Array.isArray(payload.choices)
? (payload.choices as Array<Record<string, unknown>>).some(
(c) => c && c.finish_reason === "length"
)
: false;
if (!hadFinish) return line;
normalizeMuseSparkFinishReason(payload, requestedBudget, completionTokens);
return `data: ${JSON.stringify(payload)}`;
};
}
export class OpencodeExecutor extends BaseExecutor {
/** Delegates to `isPremiumOpencodeModel`. Exported for testability. */
static isPremiumModel(model: string, provider: string): boolean {
@@ -324,97 +224,6 @@ export class OpencodeExecutor extends BaseExecutor {
markAccountSuccess(account);
}
/**
* Rewrite muse-spark's bogus `finish_reason:"length"` (see the
* normalizeMuseSparkFinishReason note) to `"stop"` on both streaming and
* non-streaming success responses. Non-muse-spark models pass through
* untouched.
*/
private normalizeMuseSparkResponse(
input: ExecuteInput,
result: ExecutorExecuteResult
): ExecutorExecuteResult {
const model = String(input.model ?? "");
if (!model.startsWith("muse-spark")) return result;
if (!("response" in result) || !result.response?.ok || !result.response.body) return result;
const bodyObj =
input.body && typeof input.body === "object" && !Array.isArray(input.body)
? (input.body as Record<string, unknown>)
: null;
const rawBudget = bodyObj?.max_tokens;
const budget = typeof rawBudget === "number" && Number.isFinite(rawBudget) ? rawBudget : null;
const response = result.response;
const isSse = response.headers.get("content-type")?.includes("event-stream") ?? false;
if (!isSse) {
// Non-streaming JSON: rewrite in a buffered pass.
const stream = new ReadableStream<Uint8Array>({
async start(controller) {
try {
const text = await response.clone().text();
let out = text;
try {
const parsed = JSON.parse(text) as Record<string, unknown>;
normalizeMuseSparkFinishReason(parsed, budget);
out = JSON.stringify(parsed);
} catch {
/* not JSON — forward verbatim */
}
controller.enqueue(new TextEncoder().encode(out));
} catch (err) {
controller.error(err);
return;
}
controller.close();
},
});
return {
...result,
response: new Response(stream, {
status: response.status,
statusText: response.statusText,
headers: response.headers,
}),
};
}
// Streaming SSE: line-buffered passthrough with finish_reason rewriting.
const normalizer = createMuseSparkStreamFinishNormalizer(budget);
const decoder = new TextDecoder();
const encoder = new TextEncoder();
let buffer = "";
const reader = response.body.getReader();
const stream = new ReadableStream<Uint8Array>({
async pull(controller) {
try {
const { done, value } = await reader.read();
if (done) {
if (buffer.length > 0) controller.enqueue(encoder.encode(normalizer(buffer)));
controller.close();
return;
}
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n");
buffer = lines.pop() ?? "";
for (const line of lines) controller.enqueue(encoder.encode(normalizer(line) + "\n"));
} catch (err) {
controller.error(err);
}
},
cancel(reason) {
reader.cancel(reason).catch(() => undefined);
},
});
return {
...result,
response: new Response(stream, {
status: response.status,
statusText: response.statusText,
headers: response.headers,
}),
};
}
async execute(input: ExecuteInput) {
this._requestFormat = resolveOpencodeTargetFormat(this.provider, input.model);
@@ -445,14 +254,6 @@ export class OpencodeExecutor extends BaseExecutor {
}
try {
// muse-spark reasoning models consume the entire output budget on hidden
// server-side reasoning; small caller budgets come back as empty-message
// 200s ("Provider returned empty content"). Raise tiny budgets to the
// floor before dispatch (see MUSE_SPARK_MIN_OUTPUT_TOKENS).
if (input.body && typeof input.body === "object" && !Array.isArray(input.body)) {
applyMuseSparkMinOutputTokens(String(input.model ?? ""), input.body as Record<string, unknown>);
}
this.syncAccountsFromCredentials(input.credentials);
const { log } = input;
@@ -478,7 +279,7 @@ export class OpencodeExecutor extends BaseExecutor {
"OPENCODE",
`upstream empty rejection on direct account (${chatcmplId}), retrying once…`
);
return this.normalizeMuseSparkResponse(input, await super.execute(input));
return await super.execute(input);
}
log?.debug?.(
"OPENCODE",
@@ -486,7 +287,7 @@ export class OpencodeExecutor extends BaseExecutor {
);
}
}
return this.normalizeMuseSparkResponse(input, single);
return single;
}
// This loop only ever dispatches through super.execute() (the HTTP request
@@ -616,7 +417,7 @@ export class OpencodeExecutor extends BaseExecutor {
}
this.markSuccess(account);
return this.normalizeMuseSparkResponse(input, result);
return result;
}
// The loop exhausted without a result. If it's because every remaining
@@ -630,10 +431,7 @@ export class OpencodeExecutor extends BaseExecutor {
}
// All accounts returned 429 (or errored) — surface the last response.
return this.normalizeMuseSparkResponse(
input,
lastResult ?? (await super.execute(input))
);
return lastResult ?? (await super.execute(input));
} finally {
this._requestFormat = null;
}

View File

@@ -33,10 +33,7 @@ import { assembleStreamingResponseHeaders } from "./chatCore/streamingResponseHe
import { storeStreamingSemanticCacheResponse } from "./chatCore/streamingSemanticCacheStore.ts";
import { assembleStreamingPipeline } from "./chatCore/streamingPipeline.ts";
import { sanitizeChatRequestBody } from "./chatCore/sanitization.ts";
import {
applyReasoningInputPolicy,
resolveIncompatibleReasoningAction,
} from "../services/reasoningInputPolicy.ts";
import { applyReasoningInputPolicy } from "../services/reasoningInputPolicy.ts";
import {
createRoutingEvent,
emitRoutingEvent,
@@ -1216,11 +1213,7 @@ export async function handleChatCore({
provider,
preserveEncryptedReasoning:
credentials?.providerSpecificData?.preserveEncryptedReasoning === true,
onIncompatibleReasoning: resolveIncompatibleReasoningAction({
reasoningTransportFallback,
isComboStep: Boolean(comboStepId || comboExecutionKey),
headers: clientRawRequest?.headers ?? null,
}),
onIncompatibleReasoning: reasoningTransportFallback === "skip" ? "reject" : "drop",
}
);
if (policy.incompatibleReasoning) {

View File

@@ -91,14 +91,6 @@ interface KieImageOptions {
} | null;
}
export const KIE_MARKET_UPSTREAM_MODEL_IDS: ReadonlyMap<string, string> = new Map([
["google-imagen/nano-banana-2", "nano-banana-2"],
]);
export function resolveKieMarketUpstreamModelId(publicModelId: string): string {
return KIE_MARKET_UPSTREAM_MODEL_IDS.get(publicModelId) ?? publicModelId;
}
const OPENAI_IMAGE_TO_IMAGE_MODELS = new Set([
"black-forest-labs/FLUX.2-max",
"black-forest-labs/FLUX.2-pro",
@@ -213,9 +205,7 @@ function isCodexChatGptModelAccessError(status: number, errorText: string, model
if (typeof nested === "string") detail = nested;
}
}
return (
detail === `The '${model}' model is not supported when using Codex with a ChatGPT account.`
);
return detail === `The '${model}' model is not supported when using Codex with a ChatGPT account.`;
}
const BFL_MODEL_ENDPOINTS = {
@@ -783,7 +773,7 @@ async function handleKieImageGeneration({
input.image_url = imageUrl;
}
payload = {
model: resolveKieMarketUpstreamModelId(model),
model,
input,
};
} else {

View File

@@ -31,6 +31,7 @@ import * as xSearch from "./search/xSearch.ts";
import { freeWebSearch } from "../services/freeWebSearch.ts";
import { saveCallLog } from "@/lib/usageDb";
import { safeOutboundFetch } from "@/shared/network/safeOutboundFetch";
import { parseAndValidateNonMetadataUrl } from "@/shared/network/outboundUrlGuard";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
import { z } from "zod";
@@ -313,9 +314,23 @@ function getProviderSettingString(
return undefined;
}
function resolveSearchBaseUrl(config: SearchProviderConfig, params: SearchRequestParams): string {
export function resolveSearchBaseUrl(
config: SearchProviderConfig,
params: SearchRequestParams
): string {
const override = getProviderSettingString(params, "baseUrl");
return (override || config.baseUrl).replace(/\/+$/, "");
if (override) {
// GHSA-j7j4-g9qc-q69c: the override is client-controlled (provider_options /
// providerSpecificData) and flows into a plain fetch() sink — validate it
// before any builder uses it as the server-side fetch target. Mode is
// block-metadata (NOT public-only): the primary searxng use case is a
// self-hosted instance on loopback/LAN, so private hosts keep working,
// while cloud-metadata endpoints (IMDS credential theft) are rejected.
// The catalog's own config.baseUrl is operator config and stays untouched.
parseAndValidateNonMetadataUrl(override);
return override.replace(/\/+$/, "");
}
return config.baseUrl.replace(/\/+$/, "");
}
function toSearchPageNumber(offset: number | undefined, maxResults: number): number | undefined {

View File

@@ -187,12 +187,6 @@ export function engineToCompressFn(engineId: string): CompressFn {
return async (text: string): Promise<string> => {
const body: Record<string, unknown> = {
messages: [{ role: "user", content: text }],
// #7746 follow-up: CCR only compresses for callers that advertise the
// omniroute_ccr_retrieve tool (otherwise its content-addressed marker is
// unresolvable). Real CCR traffic always carries this tool, so the
// benchmark must too, or CCR measures as a no-op. Other engines ignore
// the `tools` field, so this is inert for them.
tools: [{ type: "function", function: { name: "omniroute_ccr_retrieve" } }],
};
try {
@@ -205,16 +199,6 @@ export function engineToCompressFn(engineId: string): CompressFn {
const messages = result.body["messages"];
if (Array.isArray(messages) && messages.length > 0) {
// CCR may inject a leading [CCR protocol] system instruction, so the
// compressed user text is not necessarily messages[0]. Prefer the LAST
// message with string content (the user turn we fed in); fall back to
// the first string content otherwise.
for (let i = messages.length - 1; i >= 0; i--) {
const c = (messages[i] as Record<string, unknown>)["content"];
if (typeof c === "string" && (messages[i] as Record<string, unknown>)["role"] !== "system") {
return c;
}
}
const content = (messages[0] as Record<string, unknown>)["content"];
if (typeof content === "string") return content;
}

View File

@@ -344,65 +344,3 @@ export function applyReasoningInputPolicy(
}
return { incompatibleReasoning: false };
}
export function createReasoningTransportIncompatibleError(): Error & {
statusCode: number;
errorType: string;
} {
const error = new Error(
"Reasoning continuation is not compatible with the selected target"
) as Error & { statusCode: number; errorType: string };
error.statusCode = 400;
error.errorType = "reasoning_transport_incompatible";
return error;
}
export const REASONING_FALLBACK_HEADER = "x-omniroute-reasoning-fallback";
function readFallbackHeader(
headers: Headers | Record<string, unknown> | null | undefined
): string | null {
if (!headers) return null;
if (headers instanceof Headers) {
const value = headers.get(REASONING_FALLBACK_HEADER);
return typeof value === "string" ? value : null;
}
if (typeof headers !== "object") return null;
for (const [key, value] of Object.entries(headers)) {
if (key.toLowerCase() === REASONING_FALLBACK_HEADER && typeof value === "string") {
return value;
}
}
return null;
}
/**
* Resolves the action taken when inbound continuation reasoning is incompatible with the selected
* target's reasoning transport. Combo steps keep their explicit configuration. Single-target
* requests default to "drop" so replayed summary-only reasoning from agentic clients does not
* hard-fail every continuation turn; an operator (OMNIROUTE_SINGLE_TARGET_REASONING_FALLBACK=reject)
* or caller (x-omniroute-reasoning-fallback: reject) may explicitly enforce "reject".
*/
export function resolveIncompatibleReasoningAction(options: {
reasoningTransportFallback?: string | null;
isComboStep?: boolean;
headers?: Headers | Record<string, unknown> | null;
env?: Record<string, string | undefined>;
}): "drop" | "reject" {
if (options.reasoningTransportFallback === "drop") return "drop";
if (options.isComboStep && options.reasoningTransportFallback === "skip") return "reject";
const headerRaw = readFallbackHeader(options.headers)?.trim().toLowerCase();
if (headerRaw === "reject") return "reject";
if (headerRaw === "drop") return "drop";
const envRaw = (
options.env ?? process.env
).OMNIROUTE_SINGLE_TARGET_REASONING_FALLBACK?.trim().toLowerCase();
if (envRaw === "reject") return "reject";
if (envRaw === "drop") return "drop";
// Default to "drop" for single-target requests so multi-turn agentic loops on direct
// Codex / OpenAI targets work seamlessly out of the box.
return "drop";
}

View File

@@ -334,15 +334,6 @@ export function translateRequest(
const isKimiCoding =
normalizedProvider === "kimi-coding" || normalizedProvider === "kimi-coding-apikey";
// GLM-family upstreams (Z.AI / Zhipu console gateways) reject messages arrays
// with no role:"user" turn (400 [1214] "The messages parameter is illegal").
// Pure tool-loop continuations from coding agents produce exactly that shape
// after Claude→OpenAI conversion, so flag those providers to have the source→
// openai translator append a synthetic user turn when none survives.
const isGlmFamilyUpstream =
["opencode-go", "opencode-zen"].includes(normalizedProvider) ||
/glm|zhipu|z-ai/i.test(normalizedModel);
// Phase 2: Apply thinking budget control before normalization
result = applyThinkingBudget(result);
// Explicit reasoning-routing policies are final. The marker is internal and is
@@ -472,15 +463,13 @@ export function translateRequest(
options?.copilotClient ||
hasTargetHint ||
preserveCacheControl ||
preserveResponsesReasoning ||
isGlmFamilyUpstream
preserveResponsesReasoning
? {
...(credentials && typeof credentials === "object" ? credentials : {}),
...(options?.copilotClient ? { _copilotClient: true } : {}),
...(hasTargetHint ? { _targetFormat: targetFormat } : {}),
...(preserveCacheControl ? { _preserveCacheControl: true } : {}),
...(preserveResponsesReasoning ? { _preserveReasoningContent: true } : {}),
...(isGlmFamilyUpstream ? { _ensureUserTurn: true } : {}),
}
: credentials;
result = toOpenAI(model, result, stream, step1Credentials);

View File

@@ -191,24 +191,6 @@ export function claudeToOpenAIRequest(model, body, stream, credentials: unknown
// unanswered tool_call receives a "[No response received]" placeholder.
fixMissingToolResponses(result.messages);
// GLM-family gateways (Z.AI / Zhipu — fronted by opencode-go / opencode-zen /
// glm-* targets) reject any payload whose messages array has NO role:"user"
// turn with `400 [1214] The messages parameter is illegal`. Claude Code agent
// loops legitimately produce such payloads: every inbound user turn carries
// only tool_result blocks (translated to role:"tool") and context compression
// can evict the original prompt. When the caller flags a GLM-family upstream
// (_ensureUserTurn), append a minimal synthetic user turn so the request
// satisfies the validator. Appending at the end keeps every earlier byte
// identical for upstream prompt caches.
const ensureUserTurn =
credentials !== null &&
typeof credentials === "object" &&
!Array.isArray(credentials) &&
(credentials as JsonRecord)._ensureUserTurn === true;
if (ensureUserTurn && !result.messages.some((m) => m && m.role === "user")) {
result.messages.push({ role: "user", content: "(continue)" });
}
const useNativeResponsesWebSearch = shouldUseNativeResponsesWebSearch(credentials);
// Tools

View File

@@ -1,7 +1,7 @@
{
"name": "omniroute",
"version": "3.8.50",
"description": "Unified AI router with 351 providers, RTK+Caveman compression, auto fallback, MCP/A2A, desktop, PWA, and OpenAI-compatible APIs.",
"description": "Unified AI router with 350 providers, RTK+Caveman compression, auto fallback, MCP/A2A, desktop, PWA, and OpenAI-compatible APIs.",
"type": "module",
"bin": {
"omniroute": "bin/omniroute.mjs",

View File

@@ -1,162 +0,0 @@
/**
* OmniRoute — cross-platform spawning of locally installed build tools.
*
* WHY: `node_modules/.bin/<tool>` (no extension) is a POSIX shell script. On
* Windows the executable shim is `<tool>.cmd`, so `execFileSync(join(ROOT,
* "node_modules", ".bin", "esbuild"), …)` dies with
*
* Error: spawnSync C:\…\node_modules\.bin\esbuild ENOENT
*
* and — because the `postbuild` hook runs after a SUCCESSFUL `next build` — the
* operator sees "✓ Compiled successfully" immediately followed by a failed
* `npm run build`, with a complete `.build/next/standalone` tree on disk.
*
* Switching to `<tool>.cmd` alone is not enough: since the CVE-2024-27980
* hardening, Node >= 20 refuses to spawn a `.cmd`/`.bat` without a shell
* (EINVAL), and `shell: true` in turn disables argument escaping (DEP0190).
*
* So the preferred path avoids the shim entirely: read the tool's own `bin`
* entry from its package.json and run THAT with this Node binary — no shim, no
* shell, nothing to escape, identical behaviour on every platform. The `.bin`
* shim stays only as a last resort for a tool that is not resolvable inside the
* local dependency tree.
*
* These helpers were private to `scripts/build/prepublish.ts`, where the same
* Windows failure was already fixed; they live here so plain-`node` build
* scripts (`postbuild` → colocate-standalone.mjs) can share one implementation
* instead of re-learning the same lesson. `planBuildToolSpawn()` takes the
* platform as a parameter — like `resolveNextBuildEnv()` in
* build-next-isolated.mjs — so the Windows behaviour is unit-testable from CI's
* Linux runners.
*/
import { execFileSync } from "node:child_process";
import { closeSync, existsSync, openSync, readFileSync, readSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
const ROOT = dirname(dirname(dirname(fileURLToPath(import.meta.url))));
/**
* Absolute path of a tool's own `bin` entry inside the local dependency tree,
* or `null` when the package (or the entry it advertises) is not there.
*
* @param {string} packageName Package that ships the tool, e.g. `"esbuild"`.
* @param {string} binName Key in that package's `bin` map, e.g. `"esbuild"`.
* @param {string} [root] Directory holding `node_modules` (defaults to repo root).
* @returns {string | null}
*/
export function resolveLocalBinEntry(packageName, binName, root = ROOT) {
try {
const packageJsonPath = join(root, "node_modules", packageName, "package.json");
if (!existsSync(packageJsonPath)) return null;
const meta = JSON.parse(readFileSync(packageJsonPath, "utf8"));
const relative = typeof meta.bin === "string" ? meta.bin : meta.bin?.[binName];
if (!relative) return null;
const absolute = join(root, "node_modules", packageName, relative);
return existsSync(absolute) ? absolute : null;
} catch {
return null;
}
}
/**
* Does this file start with an executable image's magic bytes?
*
* esbuild >= 0.25 ships `bin/esbuild` as the NATIVE platform executable on
* Linux/macOS (ELF / Mach-O) instead of a JS shim — handing that to
* `process.execPath` makes Node parse machine code as JavaScript and die with
* "SyntaxError: Invalid or unexpected token". Native entries must be executed
* directly; JS entries go through this Node binary.
*
* @param {string} entryPath
* @returns {boolean}
*/
export function isNativeExecutable(entryPath) {
try {
const fd = openSync(entryPath, "r");
const head = Buffer.alloc(4);
readSync(fd, head, 0, 4, 0);
closeSync(fd);
return (
(head[0] === 0x7f && head[1] === 0x45 && head[2] === 0x4c && head[3] === 0x46) || // ELF
head.readUInt32BE(0) === 0xfeedfacf || // Mach-O 64
head.readUInt32BE(0) === 0xcffaedfe || // Mach-O 64 (LE on disk)
(head[0] === 0x4d && head[1] === 0x5a) // PE (Windows MZ)
);
} catch {
return false;
}
}
/**
* `cmd.exe` receives one flat command line, and Node does NOT escape arguments
* when `shell` is set, so anything holding whitespace has to be quoted here.
* Build arguments carry absolute paths, and `C:\Users\First Last\…` is an
* ordinary Windows home directory.
*
* @param {string} value
* @returns {string}
*/
function quoteForShell(value) {
if (!/\s/.test(value) || value.startsWith('"')) return value;
return `"${value}"`;
}
/**
* Decide HOW to spawn a build tool. Pure: no filesystem access, no `process`
* inspection beyond `execPath`, platform injected — so a Linux test can assert
* the Windows plan.
*
* @param {object} input
* @param {string} input.binName Tool name as it appears in `node_modules/.bin`.
* @param {readonly string[]} input.args Arguments for the tool.
* @param {string | null} [input.entryPath] Result of {@link resolveLocalBinEntry}.
* @param {boolean} [input.entryIsNative] Result of {@link isNativeExecutable}.
* @param {string} [input.root] Directory holding `node_modules`.
* @param {string} [input.platform] `process.platform` value to plan for.
* @returns {{ file: string, args: string[], shell: boolean }} `file`/`args` are
* already shell-quoted when `shell` is true, and must be passed together.
*/
export function planBuildToolSpawn({
binName,
args,
entryPath = null,
entryIsNative = false,
root = ROOT,
platform = process.platform,
}) {
// Preferred: the tool's own entry point, spawned with no shim and no shell.
if (entryPath) {
return entryIsNative
? { file: entryPath, args: [...args], shell: false }
: { file: process.execPath, args: [entryPath, ...args], shell: false };
}
// Last resort: the `node_modules/.bin` shim. On Windows that means the `.cmd`
// variant, which Node only spawns through a shell (see the module header).
const isWindows = platform === "win32";
const shim = join(root, "node_modules", ".bin", isWindows ? `${binName}.cmd` : binName);
return isWindows
? { file: quoteForShell(shim), args: args.map(quoteForShell), shell: true }
: { file: shim, args: [...args], shell: false };
}
/**
* Run a locally installed build tool, synchronously, on any platform.
*
* @param {string} packageName Package that ships the tool, e.g. `"esbuild"`.
* @param {string} binName Key in that package's `bin` map, e.g. `"esbuild"`.
* @param {readonly string[]} args Arguments for the tool.
* @param {import("node:child_process").ExecFileSyncOptions} [options] Passed to `execFileSync`.
* @returns {void}
*/
export function runBuildTool(packageName, binName, args, options = {}) {
const entryPath = resolveLocalBinEntry(packageName, binName);
const plan = planBuildToolSpawn({
binName,
args,
entryPath,
entryIsNative: entryPath ? isNativeExecutable(entryPath) : false,
});
execFileSync(plan.file, plan.args, plan.shell ? { ...options, shell: true } : options);
}

View File

@@ -18,8 +18,8 @@
*/
import { cpSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { execFileSync } from "node:child_process";
import { fileURLToPath, pathToFileURL } from "node:url";
import { runBuildTool } from "./buildToolRunner.mjs";
import { computeDependencyClosure } from "./colocateOptionals.mjs";
const ROOT = dirname(dirname(dirname(fileURLToPath(import.meta.url))));
@@ -89,12 +89,8 @@ function main() {
const callLogWorkerDest = join(STANDALONE, CALL_LOG_WORKER_REL);
mkdirSync(dirname(callLogWorkerDest), { recursive: true });
// Never spawn `node_modules/.bin/esbuild` directly: that extensionless path is
// a POSIX shell script and does not exist on Windows (ENOENT), which failed
// `npm run build` right after a successful `next build`. See buildToolRunner.mjs.
runBuildTool(
"esbuild",
"esbuild",
execFileSync(
join(ROOT, "node_modules", ".bin", "esbuild"),
[
CALL_LOG_WORKER_SRC,
"--bundle",
@@ -124,9 +120,8 @@ function main() {
if (!existsSync(workerDest)) {
mkdirSync(dirname(workerDest), { recursive: true });
try {
runBuildTool(
"esbuild",
"esbuild",
execFileSync(
join(ROOT, "node_modules", ".bin", "esbuild"),
[
join(
ROOT,

View File

@@ -22,12 +22,14 @@ import {
readdirSync,
statSync,
chmodSync,
openSync,
readSync,
closeSync,
} from "node:fs";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { assembleStandalone } from "./assembleStandalone.mjs";
import { isNativeExecutable, resolveLocalBinEntry } from "./buildToolRunner.mjs";
import { resolveBundledNpmEntry } from "./resolveNpmEntry.ts";
import {
APP_STAGING_ALLOWED_EXACT_PATHS,
@@ -49,15 +51,52 @@ const NPX_BIN = process.platform === "win32" ? "npx.cmd" : "npx";
//
// `shell: true` would fix the spawn but disables argument escaping (DEP0190), so it
// is only the last resort. Preferred order: run the tool's own JS entry point with
// this Node binary — no shim, no shell, nothing to escape. `resolveLocalBinEntry()`
// and `isNativeExecutable()` implement that resolution and now live in
// buildToolRunner.mjs, shared with the plain-`node` build scripts.
// this Node binary — no shim, no shell, nothing to escape.
function resolveLocalBinEntry(packageName: string, binName: string): string | null {
try {
const packageJsonPath = join(ROOT, "node_modules", packageName, "package.json");
if (!existsSync(packageJsonPath)) return null;
const meta = JSON.parse(readFileSync(packageJsonPath, "utf8")) as {
bin?: string | Record<string, string>;
};
const relative = typeof meta.bin === "string" ? meta.bin : meta.bin?.[binName];
if (!relative) return null;
const absolute = join(ROOT, "node_modules", packageName, relative);
return existsSync(absolute) ? absolute : null;
} catch {
return null;
}
}
/**
* Runs a build tool without ever touching a `.cmd` shim. `packageName` is where the
* tool lives in the local dependency tree; when it is not installed there the call
* falls back to the Node-resolved `npx` entry point, and only then to the shim.
*/
/**
* esbuild ≥0.25 ships its `bin/esbuild` as the NATIVE platform executable on
* Linux/macOS (ELF / Mach-O) instead of a JS shim — running it through
* `process.execPath` makes Node parse machine code as JavaScript and crash with
* "SyntaxError: Invalid or unexpected token". Sniff the magic bytes and exec
* native entries directly; JS entries keep going through this Node binary.
*/
function isNativeExecutable(entryPath: string): boolean {
try {
const fd = openSync(entryPath, "r");
const head = Buffer.alloc(4);
readSync(fd, head, 0, 4, 0);
closeSync(fd);
return (
(head[0] === 0x7f && head[1] === 0x45 && head[2] === 0x4c && head[3] === 0x46) || // ELF
head.readUInt32BE(0) === 0xfeedfacf || // Mach-O 64
head.readUInt32BE(0) === 0xcffaedfe || // Mach-O 64 (LE on disk)
(head[0] === 0x4d && head[1] === 0x5a) // PE (Windows MZ)
);
} catch {
return false;
}
}
function runBuildTool(
packageName: string,
binName: string,

View File

@@ -26,16 +26,10 @@ const MAX_SERVER_OUTPUT_CHARS = 1_000_000;
const SQLJS_STARTUP_MARKER = "Pre-initializing sql.js WASM";
const DEFAULT_CLI_SALT = "omniroute-cli-auth-v1";
// Dependency-based packaging (#11242): the tarball can never contain a node_modules
// path (files[] has "!**/node_modules/**" and check:pack-artifact fails on the
// segment), so sql.js must be required where a clean `npm install` of the declared
// `dependencies` places it — <packageRoot>/node_modules/sql.js — NOT under the old
// vendored dist/node_modules location. The runtime resolves the WASM the same way
// (src/lib/db/adapters/sqljsAdapter.ts → <cwd>/node_modules/sql.js/dist/sql-wasm.wasm).
export const REQUIRED_SQLJS_RUNTIME_FILES = Object.freeze([
"node_modules/sql.js/package.json",
"node_modules/sql.js/dist/sql-wasm.js",
"node_modules/sql.js/dist/sql-wasm.wasm",
"dist/node_modules/sql.js/package.json",
"dist/node_modules/sql.js/dist/sql-wasm.js",
"dist/node_modules/sql.js/dist/sql-wasm.wasm",
]);
export const REQUIRED_MACHINE_TOKEN_RUNTIME_FILES = Object.freeze([

View File

@@ -1250,22 +1250,6 @@ export default function APIPageClient({ machineId }: Readonly<APIPageClientProps
return (
<div className="flex flex-col gap-8">
{/* Guided connection header (#11228): /v1 URL + test action lead; advanced protocols demoted */}
<div className="flex flex-col gap-2">
<h1 className="text-2xl font-bold">{t("title")}</h1>
<p className="text-text-muted">{t("subtitle")}</p>
<div className="flex items-center gap-3 mt-2">
<code className="text-sm bg-card-subtle px-3 py-1 rounded-md text-text-main font-mono">
{displayBaseUrl}/v1
</code>
<a href="#test" className="text-sm text-action font-medium hover:underline">
{t("testEndpoint")}
</a>
</div>
<div className="flex items-center gap-2 text-xs text-text-muted">
<span>{t("advancedProtocols")}</span>
</div>
</div>
<SegmentedControl
options={ENDPOINT_TABS.map((tab) => ({ ...tab, label: t(tab.labelKey) }))}
value={activeEndpointTab}
@@ -2376,7 +2360,19 @@ function ProviderModelsModal({
<div className="flex flex-col gap-1">
{groupModels.map((m) => {
const copyKey = `modal-${m.id}`;
return (
return (
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-2 mb-2">
<h1 className="text-2xl font-bold">{t("endpoint.title")}</h1>
<p className="text-text-muted">{t("endpoint.subtitle")}</p>
<div className="flex items-center gap-3 mt-2">
<code className="text-sm bg-card-subtle px-3 py-1 rounded-md text-text-main font-mono">{useDisplayBaseUrl()}/v1</code>
<a href="#test" className="text-sm text-action font-medium hover:underline">{t("endpoint.testEndpoint")}</a>
</div>
</div>
<div className="flex items-center gap-2 text-xs text-text-muted">
<span>{t("endpoint.advancedProtocols")}</span>
</div>
<div
key={m.id}
className="flex items-center gap-2 px-3 py-2 rounded-lg hover:bg-surface/60 group"

View File

@@ -33,14 +33,10 @@ export default function QdrantConfigCard() {
const [apiKeyInput, setApiKeyInput] = useState("");
const [saving, setSaving] = useState(false);
const [saveStatus, setSaveStatus] = useState<"" | "saved" | "error">("");
const [health, setHealth] = useState<{
ok: boolean;
latencyMs: number;
error?: string;
collection?: { exists: boolean; vectorSize?: number; vectorName?: string | null };
} | null>(null);
const [searchValidated, setSearchValidated] = useState(false);
const [tutorialOpen, setTutorialOpen] = useState(false); const [checking, setChecking] = useState(false);
const [health, setHealth] = useState<{ ok: boolean; latencyMs: number; error?: string } | null>(
null
);
const [checking, setChecking] = useState(false);
const [searchQuery, setSearchQuery] = useState("");
const [searching, setSearching] = useState(false);
const [searchResults, setSearchResults] = useState<
@@ -109,7 +105,7 @@ export default function QdrantConfigCard() {
// invalidate in-flight checks so they cannot overwrite the new state.
healthSeqRef.current += 1;
setHealth(null);
setSearchValidated(false); setQdrant(next);
setQdrant(next);
setSaving(true);
setSaveStatus("");
try {
@@ -153,7 +149,8 @@ export default function QdrantConfigCard() {
setSaving(false);
}
},
[qdrant, checkHealth] );
[qdrant, checkHealth]
);
// Auto-check on mount once settings load: without this the status badge
// renders red after a page refresh because `health` starts as null and the
@@ -179,13 +176,9 @@ export default function QdrantConfigCard() {
const data = await res.json().catch(() => null);
if (res.ok && data?.ok) {
setSearchResults(Array.isArray(data.results) ? data.results : []);
setSearchValidated(true);
} else {
setSearchValidated(false);
}
} catch {
setSearchResults([]);
setSearchValidated(false);
} finally {
setSearching(false);
}
@@ -228,14 +221,6 @@ export default function QdrantConfigCard() {
<div className="flex-1 min-w-0">
<h3 className="text-sm font-semibold text-text-main">{t("qdrant.title")}</h3>
<p className="text-xs text-text-muted">{t("qdrant.description")}</p>
<button
type="button"
data-testid="qdrant-setup-tutorial"
onClick={() => setTutorialOpen(true)}
className="mt-1 text-xs font-medium text-emerald-500 hover:underline"
>
Como configurar o Qdrant corretamente
</button>
</div>
<span
className={`inline-flex items-center gap-1.5 text-xs font-medium ${
@@ -245,7 +230,8 @@ export default function QdrantConfigCard() {
? "text-text-muted"
: health.ok
? "text-emerald-500"
: "text-red-500" }`}
: "text-red-500"
}`}
>
<span
className={`inline-block w-2.5 h-2.5 rounded-full ${
@@ -291,7 +277,7 @@ export default function QdrantConfigCard() {
<button
data-testid="qdrant-enabled-switch"
onClick={() => save({ enabled: !qdrant.enabled })}
disabled={saving || (!qdrant.enabled && !searchValidated)}
disabled={saving}
role="switch"
aria-checked={qdrant.enabled}
className={`relative w-11 h-6 rounded-full transition-colors ${
@@ -307,13 +293,6 @@ export default function QdrantConfigCard() {
</div>
</div>
{!qdrant.enabled && !searchValidated && (
<p className="-mt-2 mb-4 text-xs text-amber-500">
Execute um teste de busca bem-sucedido antes de ativar. Ele valida o modelo de embedding e
a dimensão da coleção.
</p>
)}
{health && (
<div
className={`mb-4 text-xs font-medium flex items-center gap-1 ${health.ok ? "text-emerald-500" : "text-red-500"}`}
@@ -326,23 +305,6 @@ export default function QdrantConfigCard() {
: (health.error ?? t("qdrant.healthError"))}
</div>
)}
{health?.collection && (
<div className="mb-4 rounded-lg border border-border/50 bg-surface/30 p-3 text-xs text-text-muted">
{health.collection.exists ? (
<>
Coleção compatível com vetores de dimensão{" "}
<strong>{health.collection.vectorSize}</strong>
{health.collection.vectorName ? ` (vetor: ${health.collection.vectorName})` : ""}. O
modelo configurado deve gerar a mesma dimensão.
</>
) : (
<>
A coleção ainda não existe. Ela será criada na primeira gravação com o modelo
validado.
</>
)}
</div>
)}
{saveStatus === "saved" && (
<div className="mb-4 text-xs font-medium text-emerald-500 flex items-center gap-1">
@@ -510,50 +472,6 @@ export default function QdrantConfigCard() {
</div>
{cleanupMsg && <p className="mt-2 text-xs text-text-muted">{cleanupMsg}</p>}
</div>
{tutorialOpen && (
<div
role="dialog"
aria-modal="true"
aria-label="Tutorial de configuração do Qdrant"
className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 p-4"
>
<div className="max-h-[85vh] w-full max-w-2xl overflow-y-auto rounded-xl border border-border bg-background p-6 shadow-xl">
<div className="flex items-start justify-between gap-4">
<div>
<h4 className="text-lg font-semibold">Tutorial rápido: memória com Qdrant</h4>
<p className="mt-2 text-sm text-text-muted">
O Qdrant guarda vetores e metadados das memórias para recuperar contexto
relevante. Ele não comprime tokens diretamente; a economia é indireta, ao evitar
contexto sem relação com a solicitação.
</p>
</div>
<button
type="button"
onClick={() => setTutorialOpen(false)}
aria-label="Fechar tutorial"
>
×
</button>
</div>
<ol className="mt-5 list-decimal space-y-3 pl-5 text-sm text-text-muted">
<li>Proteja o servidor com HTTPS e API key antes de uso produtivo.</li>
<li>
Informe host, porta, coleção e um modelo no formato provider/model com credencial
configurada.
</li>
<li>
A dimensão da coleção precisa ser igual à dimensão produzida pelo modelo. Uma
coleção existente de 2048 dimensões não aceita embeddings de 1536 dimensões.
</li>
<li>Salve, teste a conexão e execute o teste de busca. então ative o Qdrant.</li>
</ol>
<pre className="mt-5 overflow-x-auto rounded-lg bg-surface p-3 text-xs">{`PUT /collections/minha_memoria\n{\n "vectors": { "size": <dimensão-do-modelo>, "distance": "Cosine" }\n}`}</pre>
<p className="mt-5 text-xs text-text-muted">
Créditos: Rafa Martins rafacpti@gmail.com
</p>
</div>
</div>
)}
</Card>
);
}

View File

@@ -409,7 +409,7 @@ export default function ConnectionsListPanel({
? (enabled) => handleToggleConnectionAutoSync(conn.id, enabled)
: undefined
}
isCodex={providerId === "codex" || providerId === "codex-app-server"}
isCodex={providerId === "codex"}
isCcCompatible={isCcCompatible}
cliproxyapiEnabled={cpaProviderEnabled}
onToggleCliproxyapiMode={(enabled) => handleToggleCliproxyapiMode(conn.id, enabled)}
@@ -610,7 +610,7 @@ export default function ConnectionsListPanel({
? (enabled) => handleToggleConnectionAutoSync(conn.id, enabled)
: undefined
}
isCodex={providerId === "codex" || providerId === "codex-app-server"}
isCodex={providerId === "codex"}
isCcCompatible={isCcCompatible}
cliproxyapiEnabled={cpaProviderEnabled}
onToggleCliproxyapiMode={(enabled) =>

View File

@@ -96,7 +96,6 @@ export default function AddApiKeyModal({
const isLocalSelfHostedProvider = !!localProviderMetadata;
const isGooglePse = provider === "google-pse-search";
const isChatGptWebCodex = provider === "chatgpt-web-codex";
const isAwsPolly = provider === "aws-polly";
const webSessionCredential = getWebSessionCredentialRequirement(provider);
const isNoAuthWebSessionCredential = webSessionCredential?.kind === "none";
const isWebSessionCredential = !!webSessionCredential && webSessionCredential.kind !== "none";
@@ -119,8 +118,6 @@ export default function AddApiKeyModal({
baseUrl: initialBaseUrl || defaultBaseUrl,
cx: "",
region: showsRegion ? defaultRegion : "",
awsAccessKeyId: "",
awsSessionToken: "",
apiRegion: "international",
validationModelId: defaultValidationModelIdForProvider(provider), // #5446 item 4 — Modal probe model pre-fill
routingTags: "",
@@ -181,15 +178,13 @@ export default function AddApiKeyModal({
const [bulkWarnings, setBulkWarnings] = useState<string[]>([]);
const apiCredentialLabel = isModal
? providerText(t, "modalTokenIdLabel", "Token ID")
: isAwsPolly
? providerText(t, "awsPollySecretAccessKeyLabel", "AWS Secret Access Key")
: isQoder
? t("personalAccessTokenLabel")
: webSessionCredential
? getWebSessionCredentialLabel(t, webSessionCredential, apiKeyOptional)
: apiKeyOptional
? `${t("apiKeyLabel")} (${t("optional").toLowerCase()})`
: t("apiKeyLabel");
: isQoder
? t("personalAccessTokenLabel")
: webSessionCredential
? getWebSessionCredentialLabel(t, webSessionCredential, apiKeyOptional)
: apiKeyOptional
? `${t("apiKeyLabel")} (${t("optional").toLowerCase()})`
: t("apiKeyLabel");
const apiCredentialPlaceholder = isModal
? "ak-xxxxxxxxxxxxxxxx"
: isVertex
@@ -252,13 +247,7 @@ export default function AddApiKeyModal({
validationModelId: formData.validationModelId || undefined,
customUserAgent: formData.customUserAgent.trim() || undefined,
baseUrl: formData.baseUrl.trim() || undefined,
region: isAwsPolly
? formData.region.trim() || "us-east-1"
: showsRegion
? formData.region.trim() || defaultRegion
: undefined,
accessKeyId: isAwsPolly ? formData.awsAccessKeyId.trim() || undefined : undefined,
sessionToken: isAwsPolly ? formData.awsSessionToken.trim() || undefined : undefined,
region: showsRegion ? formData.region.trim() || defaultRegion : undefined,
cx: formData.cx.trim() || undefined,
runtimeKey: isChatGptWebCodex ? formData.runtimeKey.trim() || undefined : undefined,
tunnelId: isChatGptWebCodex ? formData.tunnelId.trim() || undefined : undefined,
@@ -295,12 +284,7 @@ export default function AddApiKeyModal({
const handleSubmit = async () => {
const credentialInput = resolveCredentialInput();
if (
!provider ||
(!isCompatible && !apiKeyOptional && !credentialInput) ||
(isAwsPolly && !formData.awsAccessKeyId.trim())
)
return;
if (!provider || (!isCompatible && !apiKeyOptional && !credentialInput)) return;
setSaving(true);
setSaveError(null);
@@ -337,13 +321,7 @@ export default function AddApiKeyModal({
validationModelId: formData.validationModelId || undefined,
customUserAgent: formData.customUserAgent.trim() || undefined,
baseUrl: formData.baseUrl.trim() || undefined,
region: isAwsPolly
? formData.region.trim() || "us-east-1"
: showsRegion
? formData.region.trim() || defaultRegion
: undefined,
accessKeyId: isAwsPolly ? formData.awsAccessKeyId.trim() || undefined : undefined,
sessionToken: isAwsPolly ? formData.awsSessionToken.trim() || undefined : undefined,
region: showsRegion ? formData.region.trim() || defaultRegion : undefined,
cx: formData.cx.trim() || undefined,
runtimeKey: isChatGptWebCodex ? formData.runtimeKey.trim() || undefined : undefined,
tunnelId: isChatGptWebCodex ? formData.tunnelId.trim() || undefined : undefined,
@@ -872,25 +850,6 @@ export default function AddApiKeyModal({
</div>
</div>
)}
<div className="pt-6">
<Button
onClick={handleValidate}
disabled={
(!isCompatible && !apiKeyOptional && !formData.apiKey) ||
(isAwsPolly && !formData.awsAccessKeyId.trim()) ||
(isGooglePse && !formData.cx.trim()) ||
validating ||
saving
}
variant="secondary"
>
{validating
? t("checking")
: webSessionCredential
? getWebSessionCredentialCheckLabel(t, webSessionCredential)
: t("check")}
</Button>
</div>
</div>
)}
{isModal && (
@@ -910,56 +869,6 @@ export default function AddApiKeyModal({
autoCapitalize="off"
/>
)}
{isAwsPolly && (
<>
<Input
label={providerText(t, "awsPollyAccessKeyIdLabel", "AWS Access Key ID")}
value={formData.awsAccessKeyId}
onChange={(e) => setFormData({ ...formData, awsAccessKeyId: e.target.value })}
placeholder="AKIA..."
hint={providerText(
t,
"awsPollyAccessKeyIdHint",
"Used with the secret access key to sign Amazon Polly requests."
)}
autoComplete="off"
spellCheck={false}
autoCapitalize="off"
/>
<Input
label={providerText(t, "awsPollyRegionLabel", "AWS Region")}
value={formData.region}
onChange={(e) => setFormData({ ...formData, region: e.target.value })}
placeholder="us-east-1"
hint={providerText(
t,
"awsPollyRegionHint",
"Defaults to us-east-1 when left blank."
)}
autoComplete="off"
spellCheck={false}
autoCapitalize="off"
/>
<Input
label={providerText(
t,
"awsPollySessionTokenLabel",
"AWS Session Token (optional)"
)}
type="password"
value={formData.awsSessionToken}
onChange={(e) => setFormData({ ...formData, awsSessionToken: e.target.value })}
hint={providerText(
t,
"awsPollySessionTokenHint",
"Required only for temporary AWS credentials."
)}
autoComplete="off"
spellCheck={false}
autoCapitalize="off"
/>
</>
)}
{isGooglePse && (
<Input
label={t("searchEngineIdLabel")}

View File

@@ -115,8 +115,6 @@ export default function EditConnectionModal({
targetFormat: "",
cx: "",
region: "",
awsAccessKeyId: "",
awsSessionToken: "",
apiRegion: "international",
validationModelId: "",
defaultModel: "",
@@ -218,7 +216,6 @@ export default function EditConnectionModal({
const isLocalSelfHostedProvider = !!localProviderMetadata;
const isGooglePse = provider === "google-pse-search";
const isChatGptWebCodex = provider === "chatgpt-web-codex";
const isAwsPolly = provider === "aws-polly";
const isM365TierCapable = isM365TierCapableProvider(provider);
const webSessionCredential = getWebSessionCredentialRequirement(provider);
const isNoAuthWebSessionCredential = webSessionCredential?.kind === "none";
@@ -235,11 +232,9 @@ export default function EditConnectionModal({
isOpenAICompatibleProvider(provider) || isAnthropicCompatibleProvider(provider);
const apiCredentialLabel = webSessionCredential
? getWebSessionCredentialLabel(t, webSessionCredential, apiKeyOptional)
: isAwsPolly
? providerText(t, "awsPollySecretAccessKeyLabel", "AWS Secret Access Key")
: apiKeyOptional
? t("apiKeyOptionalLabel")
: t("apiKeyLabel");
: apiKeyOptional
? t("apiKeyOptionalLabel")
: t("apiKeyLabel");
const apiCredentialPlaceholder = isWebSessionCredential
? webSessionCredential.placeholder
: isVertex
@@ -260,9 +255,6 @@ export default function EditConnectionModal({
const existingBaseUrl = stringField(connection.providerSpecificData?.baseUrl);
const existingTargetFormat = stringField(connection.providerSpecificData?.targetFormat);
const existingRegion = stringField(connection.providerSpecificData?.region);
const existingAwsAccessKeyId =
stringField(connection.providerSpecificData?.accessKeyId) ||
stringField(connection.providerSpecificData?.awsAccessKeyId);
const existingCustomUserAgent = stringField(connection.providerSpecificData?.customUserAgent);
const existingOpenRouterPreset = stringField(connection.providerSpecificData?.preset);
const existingCx = stringField(connection.providerSpecificData?.cx);
@@ -321,11 +313,7 @@ export default function EditConnectionModal({
baseUrl: existingBaseUrl || defaultBaseUrl,
targetFormat: existingTargetFormat || "",
cx: existingCx,
region:
existingRegion ||
(effectiveProvider === "aws-polly" ? "us-east-1" : showsRegion ? defaultRegion : ""),
awsAccessKeyId: existingAwsAccessKeyId,
awsSessionToken: "",
region: existingRegion || (showsRegion ? defaultRegion : ""),
apiRegion: (connection.providerSpecificData?.apiRegion as string) || "international",
validationModelId: (connection.providerSpecificData?.validationModelId as string) || "",
defaultModel: (connection.defaultModel as string) || "",
@@ -447,8 +435,7 @@ export default function EditConnectionModal({
if (
!provider ||
isNoAuthWebSessionCredential ||
(!isCompatible && !apiKeyOptional && !formData.apiKey) ||
(isAwsPolly && !formData.awsAccessKeyId.trim())
(!isCompatible && !apiKeyOptional && !formData.apiKey)
) {
return;
}
@@ -464,13 +451,7 @@ export default function EditConnectionModal({
validationModelId: formData.validationModelId || undefined,
customUserAgent: formData.customUserAgent.trim() || undefined,
baseUrl: formData.baseUrl.trim() || undefined,
region: isAwsPolly
? formData.region.trim() || "us-east-1"
: showsRegion
? formData.region.trim() || defaultRegion
: undefined,
accessKeyId: isAwsPolly ? formData.awsAccessKeyId.trim() || undefined : undefined,
sessionToken: isAwsPolly ? formData.awsSessionToken.trim() || undefined : undefined,
region: showsRegion ? formData.region.trim() || defaultRegion : undefined,
cx: formData.cx.trim() || undefined,
runtimeKey: isChatGptWebCodex ? formData.runtimeKey.trim() || undefined : undefined,
tunnelId: isChatGptWebCodex ? formData.tunnelId.trim() || undefined : undefined,
@@ -569,13 +550,7 @@ export default function EditConnectionModal({
validationModelId: formData.validationModelId || undefined,
customUserAgent: formData.customUserAgent.trim() || undefined,
baseUrl: formData.baseUrl.trim() || undefined,
region: isAwsPolly
? formData.region.trim() || "us-east-1"
: showsRegion
? formData.region.trim() || defaultRegion
: undefined,
accessKeyId: isAwsPolly ? formData.awsAccessKeyId.trim() || undefined : undefined,
sessionToken: isAwsPolly ? formData.awsSessionToken.trim() || undefined : undefined,
region: showsRegion ? formData.region.trim() || defaultRegion : undefined,
cx: formData.cx.trim() || undefined,
runtimeKey: isChatGptWebCodex ? formData.runtimeKey.trim() || undefined : undefined,
tunnelId: isChatGptWebCodex ? formData.tunnelId.trim() || undefined : undefined,
@@ -960,7 +935,6 @@ export default function EditConnectionModal({
onClick={handleValidate}
disabled={
(!isCompatible && !apiKeyOptional && !formData.apiKey) ||
(isAwsPolly && !formData.awsAccessKeyId.trim()) ||
(isGooglePse && !formData.cx.trim()) ||
validating ||
saving
@@ -1062,56 +1036,6 @@ export default function EditConnectionModal({
hint={t("searchEngineIdHint")}
/>
)}
{isAwsPolly && (
<>
<Input
label={providerText(t, "awsPollyAccessKeyIdLabel", "AWS Access Key ID")}
value={formData.awsAccessKeyId}
onChange={(e) => setFormData({ ...formData, awsAccessKeyId: e.target.value })}
placeholder="AKIA..."
hint={providerText(
t,
"awsPollyAccessKeyIdHint",
"Used with the secret access key to sign Amazon Polly requests."
)}
autoComplete="off"
spellCheck={false}
autoCapitalize="off"
/>
<Input
label={providerText(t, "awsPollyRegionLabel", "AWS Region")}
value={formData.region}
onChange={(e) => setFormData({ ...formData, region: e.target.value })}
placeholder="us-east-1"
hint={providerText(
t,
"awsPollyRegionHint",
"Defaults to us-east-1 when left blank."
)}
autoComplete="off"
spellCheck={false}
autoCapitalize="off"
/>
<Input
label={providerText(
t,
"awsPollySessionTokenLabel",
"AWS Session Token (optional)"
)}
type="password"
value={formData.awsSessionToken}
onChange={(e) => setFormData({ ...formData, awsSessionToken: e.target.value })}
hint={providerText(
t,
"awsPollySessionTokenHint",
"Required only for temporary AWS credentials."
)}
autoComplete="off"
spellCheck={false}
autoCapitalize="off"
/>
</>
)}
{validationResult && (
<Badge variant={validationResult === "success" ? "success" : "error"}>
{validationResult === "success" ? t("valid") : t("invalid")}

View File

@@ -17,8 +17,6 @@ type FormData = QuotaScrapingFieldValues &
GlmTeamQuotaFieldValues & {
accountId: string;
apiRegion: string;
awsAccessKeyId: string;
awsSessionToken: string;
ccCompatibleContext1m: boolean;
ccCompatibleRedactThinking: boolean;
ccCompatibleSummarizeThinking: boolean;
@@ -92,11 +90,7 @@ export function buildAddProviderSpecificData(options: {
}
assignQuotaScrapingProviderData(provider, formData, data);
if (isGooglePse && formData.cx.trim()) data.cx = formData.cx.trim();
if (provider === "aws-polly") {
data.accessKeyId = formData.awsAccessKeyId.trim() || undefined;
data.region = formData.region.trim() || "us-east-1";
data.sessionToken = formData.awsSessionToken.trim() || undefined;
} else if (usesBaseUrl) data.baseUrl = validatedBaseUrl;
if (usesBaseUrl) data.baseUrl = validatedBaseUrl;
if (showsRegion) data.region = formData.region?.trim() || defaultRegion;
else if (isGlm) {
data.apiRegion = formData.apiRegion;
@@ -156,11 +150,7 @@ export function assignEditApiKeyProviderSpecificData(options: {
assignQuotaScrapingProviderData(o.provider, o.formData, o.target);
if (o.formData.validationModelId) o.target.validationModelId = o.formData.validationModelId;
if (o.isGooglePse) o.target.cx = o.formData.cx.trim() || undefined;
if (o.provider === "aws-polly") {
o.target.accessKeyId = o.formData.awsAccessKeyId.trim() || undefined;
o.target.region = o.formData.region.trim() || "us-east-1";
o.target.sessionToken = o.formData.awsSessionToken.trim() || undefined;
} else if (o.usesBaseUrl) o.target.baseUrl = o.validatedBaseUrl;
if (o.usesBaseUrl) o.target.baseUrl = o.validatedBaseUrl;
if (o.showsRegion) o.target.region = o.formData.region?.trim() || o.defaultRegion;
else if (o.isGlm) {
o.target.apiRegion = o.formData.apiRegion;

View File

@@ -10,15 +10,13 @@
* Auth: Bearer token via Authorization header
*/
import { timingSafeEqual } from "node:crypto";
import { NextRequest, NextResponse } from "next/server";
import { getTaskManager } from "@/lib/a2a/taskManager";
import { logRoutingDecision } from "@/lib/a2a/routingLogger";
import { createA2AStream, SSE_HEADERS } from "@/lib/a2a/streaming";
import { A2A_SKILL_HANDLERS, executeA2ATaskWithState } from "@/lib/a2a/taskExecution";
import { getSettings } from "@/lib/db/settings";
import { isRequireApiKeyEnabled } from "@/shared/utils/featureFlags";
import { extractApiKey, isValidApiKey } from "@/sse/services/auth";
import { authenticateA2ARequest, resolveA2AOwner } from "@/lib/a2a/authenticate";
// ============ A2A v1.0 ↔ v0.3 compatibility layer ============
// A2A 1.0 renamed the JSON-RPC methods (message/send → SendMessage,
@@ -55,7 +53,7 @@ function buildV1Task(
? result.artifacts
.map((a) =>
a && typeof a === "object" && typeof (a as { content?: unknown }).content === "string"
? ((a as { content: string }).content)
? (a as { content: string }).content
: ""
)
.filter((s) => s.length > 0)
@@ -124,39 +122,13 @@ function toMessageArray(raw: unknown): A2AMessage[] | null {
// ============ Auth ============
/**
* Constant-time comparison of the presented bearer token against the configured
* key. A plain `===` short-circuits on the first differing byte, leaking the
* length of the shared prefix through response timing; `timingSafeEqual` does
* not. It requires equal-length buffers, so mismatched lengths are rejected up
* front (the length itself is not secret).
*/
function tokensMatch(provided: string, expected: string): boolean {
const a = Buffer.from(provided);
const b = Buffer.from(expected);
if (a.length !== b.length) return false;
return timingSafeEqual(a, b);
}
async function authenticate(req: NextRequest): Promise<boolean> {
// /a2a is outside the authz proxy matcher, so the REQUIRE_API_KEY posture the
// pipeline enforces for /v1 never ran here — the route accepted every caller
// whenever OMNIROUTE_API_KEY was unset, which is the shipped default
// (GHSA-v54m-6rm3-p565). Apply the same posture directly: when a client key is
// required, demand a valid OmniRoute key; otherwise honor the legacy explicit
// A2A key; otherwise stay keyless (the same local-first default as /v1).
const apiKey = extractApiKey(req);
if (isRequireApiKeyEnabled()) {
return apiKey ? await isValidApiKey(apiKey) : false;
}
const configuredKey = process.env.OMNIROUTE_API_KEY;
if (configuredKey) {
return apiKey ? tokensMatch(apiKey, configuredKey) : false;
}
// No API key required and none configured — allow (keyless local-first).
return true;
// (GHSA-v54m-6rm3-p565). The shared helper applies the same posture on both
// the JSON-RPC and the REST task surfaces (GHSA-jcm5-6wpp-wjj8).
return authenticateA2ARequest(req);
}
// ============ JSON-RPC Helpers ============
@@ -213,6 +185,9 @@ export async function POST(req: NextRequest) {
if (disabledResponse) return disabledResponse;
const tm = getTaskManager();
// GHSA-jcm5-6wpp-wjj8: scope every task read/mutation below to the caller's
// owner id (hashed API key; undefined under the keyless local-first posture).
const callerOwner = resolveA2AOwner(req);
// A2A 1.0 method-name compatibility (SendMessage → message/send, etc.)
const isV1Method = method in V1_METHOD_ALIASES;
@@ -236,7 +211,7 @@ export async function POST(req: NextRequest) {
return jsonRpcError(id, -32601, `Unknown skill: ${skill}`);
}
const task = tm.createTask({ skill, messages, metadata: params?.metadata });
const task = tm.createTask({ skill, messages, metadata: params?.metadata }, callerOwner);
try {
tm.updateTask(task.id, "working");
const result = await handler(task);
@@ -302,7 +277,7 @@ export async function POST(req: NextRequest) {
return jsonRpcError(id, -32601, `Unknown skill: ${skill}`);
}
const task = tm.createTask({ skill, messages, metadata: params?.metadata });
const task = tm.createTask({ skill, messages, metadata: params?.metadata }, callerOwner);
tm.updateTask(task.id, "working");
const stream = createA2AStream(
@@ -323,7 +298,7 @@ export async function POST(req: NextRequest) {
const taskId = params?.taskId || params?.id;
if (!taskId) return jsonRpcError(id, -32602, "Invalid params: taskId required");
const task = tm.getTask(taskId);
const task = tm.getTask(taskId, callerOwner);
if (!task) return jsonRpcError(id, -32601, `Task not found: ${taskId}`);
return jsonRpcResult(id, { task });
@@ -335,7 +310,7 @@ export async function POST(req: NextRequest) {
if (!taskId) return jsonRpcError(id, -32602, "Invalid params: taskId required");
try {
const task = tm.cancelTask(taskId);
const task = tm.cancelTask(taskId, callerOwner);
return jsonRpcResult(id, { task: { id: task.id, state: task.state } });
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);

Some files were not shown because too many files have changed in this diff Show More