feat(codex): self-contained codex app-server transport (executor + provider + sign-in) (#11205)
Merged after conflict resolution: the 5 conflicting test files were the base-red drains that #11201 already landed on the tip — kept the tip versions; the feature content is untouched. Validated on the combined batch board + this branch: codex-app-server + codex-gpt56-catalog 25/25, typecheck:core clean, docs-counts green (351 providers), provider-consistency 268/351/0. The opt-in codex-app-server transport (JSON-RPC-over-WS, turn/completed-awaited close, Responses SSE bridge) leaves the default codex path untouched. Thank you @arminanton — a 3.4k-line transport with the docs wave and tests to match!
24
.env.example
@@ -1303,6 +1303,30 @@ 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)
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
@@ -92,5 +92,9 @@
|
||||
# - 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''',
|
||||
]
|
||||
|
||||
@@ -46,7 +46,7 @@ Repository map and Reference Documentation sections below.
|
||||
|
||||
## Project at a Glance
|
||||
|
||||
**OmniRoute** — unified AI proxy/router. One endpoint, 350 LLM providers, auto-fallback.
|
||||
**OmniRoute** — unified AI proxy/router. One endpoint, 351 LLM providers, auto-fallback.
|
||||
|
||||
| Layer | Location | Purpose |
|
||||
| ------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
|
||||
@@ -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 → 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 15–95% tokens (~89% avg) — never hit limits. 350 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 → 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 15–95% tokens (~89% avg) — never hit limits. 351 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. 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 15–95%, ~89% avg on tool-heavy sessions) · $0 to start (90+ free tiers, 56 free forever — no card needed) · Every tool works (33 coding agents through one config) · One endpoint (OpenAI ↔ Claude ↔ Gemini ↔ Responses API at /v1) · Production-grade (circuit breakers, TLS stealth, MCP 110 tools, A2A, memory, guardrails, evals — 25,000+ tests)."/>
|
||||
<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 15–95%, ~89% avg on tool-heavy sessions) · $0 to start (90+ free tiers, 56 free forever — no card needed) · Every tool works (33 coding agents through one config) · One endpoint (OpenAI ↔ Claude ↔ Gemini ↔ Responses API at /v1) · Production-grade (circuit breakers, TLS stealth, MCP 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: 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'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: 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's docs."/>
|
||||
|
||||
<sub>📊 Full methodology & per-feature detail vs 9router, OpenRouter, CLIProxyAPI & LiteLLM → [`docs/comparison/OMNIROUTE_VS_ALTERNATIVES.md`](docs/comparison/OMNIROUTE_VS_ALTERNATIVES.md)</sub>
|
||||
|
||||
@@ -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: **349 providers**, **90+ with a free tier**, **56 free forever**.
|
||||
> The most complete catalog of any open-source router: **351 providers**, **90+ with a free tier**, **56 free forever**.
|
||||
|
||||
<div align="center">
|
||||
|
||||
|
||||
@@ -43,8 +43,21 @@ 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
|
||||
@@ -290,6 +303,59 @@ 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
|
||||
@@ -301,3 +367,7 @@ 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
|
||||
|
||||
@@ -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 (338 providers, 100 executors)
|
||||
- OpenAI-compatible API surface for CLI/tools (349 providers, 107 executors)
|
||||
- Request/response translation across provider formats
|
||||
- Model combo fallback (multi-model sequence)
|
||||
- Structured combo steps (`provider + model + connection`) with runtime ordering by `compositeTiers`
|
||||
|
||||
@@ -451,7 +451,7 @@ open-sse/
|
||||
├── types.d.ts
|
||||
├── config/ Provider registries, header profiles, identity, …
|
||||
├── handlers/ Request handlers (chat, embeddings, audio, image, …)
|
||||
├── executors/ 101 provider-specific HTTP executors
|
||||
├── executors/ 107 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, …)
|
||||
|
||||
@@ -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 (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.">
|
||||
<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.">
|
||||
<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 |
@@ -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: 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.">
|
||||
<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.">
|
||||
<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 |
@@ -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, 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.">
|
||||
<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.">
|
||||
<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">350 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">351 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 350 providers in</text>
|
||||
<text x="66" y="204" font-size="13.5" fill="#a1a1aa">Auto-fallback across 351 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 |
@@ -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 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.">
|
||||
<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.">
|
||||
<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">350 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">351 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  →  <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 |
89
docs/guides/CODEX-APP-SERVER-PROVIDER.md
Normal file
@@ -0,0 +1,89 @@
|
||||
---
|
||||
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.
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
---
|
||||
|
||||
> OmniRoute is a free, open-source AI 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.
|
||||
> OmniRoute is a free, open-source AI 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.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
## Key Features (v3.8.50)
|
||||
|
||||
### Core Proxy
|
||||
- **350 AI providers** with automatic format translation
|
||||
- **351 AI providers** with automatic format translation
|
||||
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
|
||||
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
---
|
||||
|
||||
> OmniRoute is a free, open-source AI 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.
|
||||
> OmniRoute is a free, open-source AI 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.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
## Key Features (v3.8.50)
|
||||
|
||||
### Core Proxy
|
||||
- **350 AI providers** with automatic format translation
|
||||
- **351 AI providers** with automatic format translation
|
||||
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
|
||||
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
---
|
||||
|
||||
> OmniRoute is a free, open-source AI 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.
|
||||
> OmniRoute is a free, open-source AI 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.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
## Key Features (v3.8.50)
|
||||
|
||||
### Core Proxy
|
||||
- **350 AI providers** with automatic format translation
|
||||
- **351 AI providers** with automatic format translation
|
||||
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
|
||||
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
---
|
||||
|
||||
> OmniRoute is a free, open-source AI 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.
|
||||
> OmniRoute is a free, open-source AI 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.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
## Key Features (v3.8.50)
|
||||
|
||||
### Core Proxy
|
||||
- **350 AI providers** with automatic format translation
|
||||
- **351 AI providers** with automatic format translation
|
||||
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
|
||||
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
---
|
||||
|
||||
> OmniRoute is a free, open-source AI 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.
|
||||
> OmniRoute is a free, open-source AI 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.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
## Key Features (v3.8.50)
|
||||
|
||||
### Core Proxy
|
||||
- **350 AI providers** with automatic format translation
|
||||
- **351 AI providers** with automatic format translation
|
||||
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
|
||||
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
---
|
||||
|
||||
> OmniRoute is a free, open-source AI 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.
|
||||
> OmniRoute is a free, open-source AI 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.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
## Key Features (v3.8.50)
|
||||
|
||||
### Core Proxy
|
||||
- **350 AI providers** with automatic format translation
|
||||
- **351 AI providers** with automatic format translation
|
||||
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
|
||||
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
---
|
||||
|
||||
> OmniRoute is a free, open-source AI 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.
|
||||
> OmniRoute is a free, open-source AI 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.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
## Key Features (v3.8.50)
|
||||
|
||||
### Core Proxy
|
||||
- **350 AI providers** with automatic format translation
|
||||
- **351 AI providers** with automatic format translation
|
||||
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
|
||||
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
---
|
||||
|
||||
> OmniRoute is a free, open-source AI 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.
|
||||
> OmniRoute is a free, open-source AI 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.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
## Key Features (v3.8.50)
|
||||
|
||||
### Core Proxy
|
||||
- **350 AI providers** with automatic format translation
|
||||
- **351 AI providers** with automatic format translation
|
||||
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
|
||||
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
---
|
||||
|
||||
> OmniRoute is a free, open-source AI 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.
|
||||
> OmniRoute is a free, open-source AI 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.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
## Key Features (v3.8.50)
|
||||
|
||||
### Core Proxy
|
||||
- **350 AI providers** with automatic format translation
|
||||
- **351 AI providers** with automatic format translation
|
||||
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
|
||||
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
---
|
||||
|
||||
> OmniRoute is a free, open-source AI 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.
|
||||
> OmniRoute is a free, open-source AI 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.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
## Key Features (v3.8.50)
|
||||
|
||||
### Core Proxy
|
||||
- **350 AI providers** with automatic format translation
|
||||
- **351 AI providers** with automatic format translation
|
||||
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
|
||||
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
---
|
||||
|
||||
> OmniRoute is a free, open-source AI 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.
|
||||
> OmniRoute is a free, open-source AI 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.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
## Key Features (v3.8.50)
|
||||
|
||||
### Core Proxy
|
||||
- **350 AI providers** with automatic format translation
|
||||
- **351 AI providers** with automatic format translation
|
||||
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
|
||||
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
---
|
||||
|
||||
> OmniRoute is a free, open-source AI 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.
|
||||
> OmniRoute is a free, open-source AI 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.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
## Key Features (v3.8.50)
|
||||
|
||||
### Core Proxy
|
||||
- **350 AI providers** with automatic format translation
|
||||
- **351 AI providers** with automatic format translation
|
||||
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
|
||||
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
---
|
||||
|
||||
> OmniRoute is a free, open-source AI 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.
|
||||
> OmniRoute is a free, open-source AI 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.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
## Key Features (v3.8.50)
|
||||
|
||||
### Core Proxy
|
||||
- **350 AI providers** with automatic format translation
|
||||
- **351 AI providers** with automatic format translation
|
||||
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
|
||||
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
---
|
||||
|
||||
> OmniRoute is a free, open-source AI 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.
|
||||
> OmniRoute is a free, open-source AI 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.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
## Key Features (v3.8.50)
|
||||
|
||||
### Core Proxy
|
||||
- **350 AI providers** with automatic format translation
|
||||
- **351 AI providers** with automatic format translation
|
||||
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
|
||||
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
---
|
||||
|
||||
> OmniRoute is a free, open-source AI 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.
|
||||
> OmniRoute is a free, open-source AI 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.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
## Key Features (v3.8.50)
|
||||
|
||||
### Core Proxy
|
||||
- **350 AI providers** with automatic format translation
|
||||
- **351 AI providers** with automatic format translation
|
||||
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
|
||||
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
---
|
||||
|
||||
> OmniRoute is a free, open-source AI 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.
|
||||
> OmniRoute is a free, open-source AI 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.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
## Key Features (v3.8.50)
|
||||
|
||||
### Core Proxy
|
||||
- **350 AI providers** with automatic format translation
|
||||
- **351 AI providers** with automatic format translation
|
||||
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
|
||||
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
---
|
||||
|
||||
> OmniRoute is a free, open-source AI 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.
|
||||
> OmniRoute is a free, open-source AI 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.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
## Key Features (v3.8.50)
|
||||
|
||||
### Core Proxy
|
||||
- **350 AI providers** with automatic format translation
|
||||
- **351 AI providers** with automatic format translation
|
||||
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
|
||||
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
---
|
||||
|
||||
> OmniRoute is a free, open-source AI 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.
|
||||
> OmniRoute is a free, open-source AI 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.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
## Key Features (v3.8.50)
|
||||
|
||||
### Core Proxy
|
||||
- **350 AI providers** with automatic format translation
|
||||
- **351 AI providers** with automatic format translation
|
||||
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
|
||||
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
---
|
||||
|
||||
> OmniRoute is a free, open-source AI 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.
|
||||
> OmniRoute is a free, open-source AI 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.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
## Key Features (v3.8.50)
|
||||
|
||||
### Core Proxy
|
||||
- **350 AI providers** with automatic format translation
|
||||
- **351 AI providers** with automatic format translation
|
||||
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
|
||||
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
---
|
||||
|
||||
> OmniRoute is a free, open-source AI 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.
|
||||
> OmniRoute is a free, open-source AI 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.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
## Key Features (v3.8.50)
|
||||
|
||||
### Core Proxy
|
||||
- **350 AI providers** with automatic format translation
|
||||
- **351 AI providers** with automatic format translation
|
||||
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
|
||||
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
---
|
||||
|
||||
> OmniRoute is a free, open-source AI 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.
|
||||
> OmniRoute is a free, open-source AI 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.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
## Key Features (v3.8.50)
|
||||
|
||||
### Core Proxy
|
||||
- **350 AI providers** with automatic format translation
|
||||
- **351 AI providers** with automatic format translation
|
||||
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
|
||||
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
---
|
||||
|
||||
> OmniRoute is a free, open-source AI 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.
|
||||
> OmniRoute is a free, open-source AI 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.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
## Key Features (v3.8.50)
|
||||
|
||||
### Core Proxy
|
||||
- **350 AI providers** with automatic format translation
|
||||
- **351 AI providers** with automatic format translation
|
||||
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
|
||||
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
---
|
||||
|
||||
> OmniRoute is a free, open-source AI 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.
|
||||
> OmniRoute is a free, open-source AI 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.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
## Key Features (v3.8.50)
|
||||
|
||||
### Core Proxy
|
||||
- **350 AI providers** with automatic format translation
|
||||
- **351 AI providers** with automatic format translation
|
||||
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
|
||||
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
---
|
||||
|
||||
> OmniRoute is a free, open-source AI 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.
|
||||
> OmniRoute is a free, open-source AI 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.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
## Key Features (v3.8.50)
|
||||
|
||||
### Core Proxy
|
||||
- **350 AI providers** with automatic format translation
|
||||
- **351 AI providers** with automatic format translation
|
||||
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
|
||||
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
---
|
||||
|
||||
> OmniRoute is a free, open-source AI 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.
|
||||
> OmniRoute is a free, open-source AI 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.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
## Key Features (v3.8.50)
|
||||
|
||||
### Core Proxy
|
||||
- **350 AI providers** with automatic format translation
|
||||
- **351 AI providers** with automatic format translation
|
||||
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
|
||||
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
---
|
||||
|
||||
> OmniRoute is a free, open-source AI 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.
|
||||
> OmniRoute is a free, open-source AI 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.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
## Key Features (v3.8.50)
|
||||
|
||||
### Core Proxy
|
||||
- **350 AI providers** with automatic format translation
|
||||
- **351 AI providers** with automatic format translation
|
||||
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
|
||||
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
---
|
||||
|
||||
> OmniRoute is a free, open-source AI 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.
|
||||
> OmniRoute is a free, open-source AI 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.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
## Key Features (v3.8.50)
|
||||
|
||||
### Core Proxy
|
||||
- **350 AI providers** with automatic format translation
|
||||
- **351 AI providers** with automatic format translation
|
||||
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
|
||||
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
---
|
||||
|
||||
> OmniRoute is a free, open-source AI 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.
|
||||
> OmniRoute is a free, open-source AI 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.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
## Key Features (v3.8.50)
|
||||
|
||||
### Core Proxy
|
||||
- **350 AI providers** with automatic format translation
|
||||
- **351 AI providers** with automatic format translation
|
||||
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
|
||||
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
---
|
||||
|
||||
> OmniRoute is a free, open-source AI 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.
|
||||
> OmniRoute is a free, open-source AI 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.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
## Key Features (v3.8.50)
|
||||
|
||||
### Core Proxy
|
||||
- **350 AI providers** with automatic format translation
|
||||
- **351 AI providers** with automatic format translation
|
||||
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
|
||||
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
---
|
||||
|
||||
> OmniRoute is a free, open-source AI 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.
|
||||
> OmniRoute is a free, open-source AI 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.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
## Key Features (v3.8.50)
|
||||
|
||||
### Core Proxy
|
||||
- **350 AI providers** with automatic format translation
|
||||
- **351 AI providers** with automatic format translation
|
||||
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
|
||||
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
---
|
||||
|
||||
> OmniRoute is a free, open-source AI 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.
|
||||
> OmniRoute is a free, open-source AI 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.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
## Key Features (v3.8.50)
|
||||
|
||||
### Core Proxy
|
||||
- **350 AI providers** with automatic format translation
|
||||
- **351 AI providers** with automatic format translation
|
||||
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
|
||||
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
---
|
||||
|
||||
> OmniRoute is a free, open-source AI 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.
|
||||
> OmniRoute is a free, open-source AI 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.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
## Key Features (v3.8.50)
|
||||
|
||||
### Core Proxy
|
||||
- **350 AI providers** with automatic format translation
|
||||
- **351 AI providers** with automatic format translation
|
||||
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
|
||||
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
---
|
||||
|
||||
> OmniRoute is a free, open-source AI 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.
|
||||
> OmniRoute is a free, open-source AI 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.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
## Key Features (v3.8.50)
|
||||
|
||||
### Core Proxy
|
||||
- **350 AI providers** with automatic format translation
|
||||
- **351 AI providers** with automatic format translation
|
||||
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
|
||||
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
---
|
||||
|
||||
> OmniRoute is a free, open-source AI 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.
|
||||
> OmniRoute is a free, open-source AI 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.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
## Key Features (v3.8.50)
|
||||
|
||||
### Core Proxy
|
||||
- **350 AI providers** with automatic format translation
|
||||
- **351 AI providers** with automatic format translation
|
||||
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
|
||||
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
---
|
||||
|
||||
> OmniRoute is a free, open-source AI 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.
|
||||
> OmniRoute is a free, open-source AI 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.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
## Key Features (v3.8.50)
|
||||
|
||||
### Core Proxy
|
||||
- **350 AI providers** with automatic format translation
|
||||
- **351 AI providers** with automatic format translation
|
||||
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
|
||||
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
---
|
||||
|
||||
> OmniRoute is a free, open-source AI 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.
|
||||
> OmniRoute is a free, open-source AI 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.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
## Key Features (v3.8.50)
|
||||
|
||||
### Core Proxy
|
||||
- **350 AI providers** with automatic format translation
|
||||
- **351 AI providers** with automatic format translation
|
||||
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
|
||||
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
---
|
||||
|
||||
> OmniRoute is a free, open-source AI 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.
|
||||
> OmniRoute is a free, open-source AI 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.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
## Key Features (v3.8.50)
|
||||
|
||||
### Core Proxy
|
||||
- **350 AI providers** with automatic format translation
|
||||
- **351 AI providers** with automatic format translation
|
||||
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
|
||||
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
---
|
||||
|
||||
> OmniRoute is a free, open-source AI 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.
|
||||
> OmniRoute is a free, open-source AI 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.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
## Key Features (v3.8.50)
|
||||
|
||||
### Core Proxy
|
||||
- **350 AI providers** with automatic format translation
|
||||
- **351 AI providers** with automatic format translation
|
||||
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
|
||||
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
---
|
||||
|
||||
> OmniRoute is a free, open-source AI 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.
|
||||
> OmniRoute is a free, open-source AI 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.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
## Key Features (v3.8.50)
|
||||
|
||||
### Core Proxy
|
||||
- **350 AI providers** with automatic format translation
|
||||
- **351 AI providers** with automatic format translation
|
||||
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
|
||||
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
---
|
||||
|
||||
> OmniRoute is a free, open-source AI 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.
|
||||
> OmniRoute is a free, open-source AI 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.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
## Key Features (v3.8.50)
|
||||
|
||||
### Core Proxy
|
||||
- **350 AI providers** with automatic format translation
|
||||
- **351 AI providers** with automatic format translation
|
||||
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
|
||||
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
---
|
||||
|
||||
> OmniRoute is a free, open-source AI 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.
|
||||
> OmniRoute is a free, open-source AI 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.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
## Key Features (v3.8.50)
|
||||
|
||||
### Core Proxy
|
||||
- **350 AI providers** with automatic format translation
|
||||
- **351 AI providers** with automatic format translation
|
||||
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
|
||||
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
---
|
||||
|
||||
> OmniRoute is a free, open-source AI 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.
|
||||
> OmniRoute is a free, open-source AI 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.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
## Key Features (v3.8.50)
|
||||
|
||||
### Core Proxy
|
||||
- **350 AI providers** with automatic format translation
|
||||
- **351 AI providers** with automatic format translation
|
||||
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
|
||||
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
|
||||
@@ -737,6 +737,12 @@ 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. |
|
||||
|
||||
@@ -10,7 +10,7 @@ lastUpdated: 2026-08-23
|
||||
> Regenerate with: `npm run gen:provider-reference`
|
||||
> **Last generated:** 2026-08-23
|
||||
|
||||
Total providers: **350**. See category breakdown below.
|
||||
Total providers: **351**. 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) (12)
|
||||
## No-auth Providers (no key required) (13)
|
||||
|
||||
| ID | Alias | Name | Tags | Website | Notes | Tool calling |
|
||||
|----|-------|------|------|---------|-------|--------------|
|
||||
@@ -42,6 +42,7 @@ 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. | — |
|
||||
@@ -438,7 +439,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/) (107 implementations)
|
||||
- Executors: [`open-sse/executors/`](../../open-sse/executors/) (108 implementations)
|
||||
- Translators: [`open-sse/translator/`](../../open-sse/translator/)
|
||||
|
||||
## See Also
|
||||
|
||||
4
llm.txt
@@ -1,6 +1,6 @@
|
||||
# OmniRoute
|
||||
|
||||
> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 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.
|
||||
> OmniRoute is a free, open-source AI 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.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -277,7 +277,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
## Key Features (v3.8.50)
|
||||
|
||||
### Core Proxy
|
||||
- **350 AI providers** with automatic format translation
|
||||
- **351 AI providers** with automatic format translation
|
||||
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
|
||||
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
|
||||
@@ -210,6 +210,7 @@ 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";
|
||||
@@ -476,6 +477,7 @@ export const REGISTRY: Record<string, RegistryEntry> = {
|
||||
pollinations: pollinationsProvider,
|
||||
"veoaifree-web": veoaifree_webProvider,
|
||||
codex: codexProvider,
|
||||
"codex-app-server": codexAppServerProvider,
|
||||
venice: veniceProvider,
|
||||
kiro: kiroProvider,
|
||||
byteplus: byteplusProvider,
|
||||
|
||||
36
open-sse/config/providers/registry/codex-app-server/index.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
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],
|
||||
};
|
||||
448
open-sse/executors/codex-app-server.ts
Normal file
@@ -0,0 +1,448 @@
|
||||
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 };
|
||||
@@ -58,6 +58,8 @@ 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";
|
||||
|
||||
@@ -102,6 +104,12 @@ 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({
|
||||
@@ -395,6 +403,34 @@ 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
|
||||
@@ -760,6 +796,8 @@ 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);
|
||||
}
|
||||
@@ -778,6 +816,15 @@ 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()) {
|
||||
|
||||
102
open-sse/executors/codex/appServerAuthProbe.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* 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]);
|
||||
}
|
||||
289
open-sse/executors/codex/appServerClient.ts
Normal file
@@ -0,0 +1,289 @@
|
||||
/**
|
||||
* 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 });
|
||||
}
|
||||
}
|
||||
94
open-sse/executors/codex/appServerConfig.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
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 } : {}) };
|
||||
}
|
||||
208
open-sse/executors/codex/appServerEvents.ts
Normal file
@@ -0,0 +1,208 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ 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";
|
||||
@@ -97,6 +98,7 @@ 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(),
|
||||
|
||||
@@ -187,6 +187,12 @@ 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 {
|
||||
@@ -199,6 +205,16 @@ 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;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "omniroute",
|
||||
"version": "3.8.50",
|
||||
"description": "Unified AI router with 350 providers, RTK+Caveman compression, auto fallback, MCP/A2A, desktop, PWA, and OpenAI-compatible APIs.",
|
||||
"description": "Unified AI router with 351 providers, RTK+Caveman compression, auto fallback, MCP/A2A, desktop, PWA, and OpenAI-compatible APIs.",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"omniroute": "bin/omniroute.mjs",
|
||||
|
||||
@@ -409,7 +409,7 @@ export default function ConnectionsListPanel({
|
||||
? (enabled) => handleToggleConnectionAutoSync(conn.id, enabled)
|
||||
: undefined
|
||||
}
|
||||
isCodex={providerId === "codex"}
|
||||
isCodex={providerId === "codex" || providerId === "codex-app-server"}
|
||||
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"}
|
||||
isCodex={providerId === "codex" || providerId === "codex-app-server"}
|
||||
isCcCompatible={isCcCompatible}
|
||||
cliproxyapiEnabled={cpaProviderEnabled}
|
||||
onToggleCliproxyapiMode={(enabled) =>
|
||||
|
||||
@@ -1,10 +1,22 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { z } from "zod";
|
||||
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
|
||||
import { ensureCliConfigWriteAllowed } from "@/shared/services/cliRuntime";
|
||||
import { CodexAuthFileError, writeCodexAuthFileToLocalCli } from "@/lib/oauth/utils/codexAuthFile";
|
||||
import {
|
||||
CodexAuthFileError,
|
||||
writeCodexAuthFileToLocalCliIfNeeded,
|
||||
} from "@/lib/oauth/utils/codexAuthFile";
|
||||
import { getAuditRequestContext, logAuditEvent } from "@/lib/compliance/index";
|
||||
import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error";
|
||||
|
||||
// Optional body { force?: boolean }. Unknown keys are stripped rather than
|
||||
// rejected so the endpoint stays tolerant of the empty/no-body calls it
|
||||
// historically accepted. Non-boolean `force` is coerced away to the default.
|
||||
const ApplyLocalBodySchema = z
|
||||
.object({ force: z.boolean().optional() })
|
||||
.partial()
|
||||
.passthrough();
|
||||
|
||||
function toErrorResponse(error: unknown) {
|
||||
if (error instanceof CodexAuthFileError) {
|
||||
return NextResponse.json(
|
||||
@@ -33,7 +45,21 @@ export async function POST(request: Request, { params }: { params: Promise<{ id:
|
||||
}
|
||||
|
||||
const { id } = await params;
|
||||
const result = await writeCodexAuthFileToLocalCli(id);
|
||||
|
||||
// Optional { force?: boolean } body. By default we DON'T clobber an existing,
|
||||
// fresh ~/.codex/auth.json (a session the user may be managing themselves);
|
||||
// force overwrites it (a backup is always taken regardless). Malformed/empty
|
||||
// bodies are tolerated — this endpoint historically took no body.
|
||||
let force = false;
|
||||
try {
|
||||
const parsed = ApplyLocalBodySchema.safeParse(await request.json());
|
||||
force = parsed.success ? parsed.data.force === true : false;
|
||||
} catch {
|
||||
/* no body — default force=false */
|
||||
}
|
||||
|
||||
const applied = await writeCodexAuthFileToLocalCliIfNeeded(id, { force });
|
||||
const result = applied.result;
|
||||
|
||||
logAuditEvent({
|
||||
action: "provider.credentials.applied",
|
||||
@@ -45,18 +71,21 @@ export async function POST(request: Request, { params }: { params: Promise<{ id:
|
||||
requestId: auditContext.requestId,
|
||||
metadata: {
|
||||
provider: "codex",
|
||||
authPath: result.authPath,
|
||||
savedBakPath: result.savedBakPath,
|
||||
decision: applied.decision,
|
||||
authPath: applied.authPath,
|
||||
savedBakPath: result?.savedBakPath,
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
connectionId: id,
|
||||
connectionLabel: result.connectionLabel,
|
||||
authPath: result.authPath,
|
||||
savedBakPath: result.savedBakPath,
|
||||
centralizedBackupPath: result.centralizedBackupPath,
|
||||
// "skipped_present_fresh" means an existing healthy auth.json was kept.
|
||||
decision: applied.decision,
|
||||
connectionLabel: result?.connectionLabel,
|
||||
authPath: applied.authPath,
|
||||
savedBakPath: result?.savedBakPath,
|
||||
centralizedBackupPath: result?.centralizedBackupPath,
|
||||
writtenAt: new Date().toISOString(),
|
||||
});
|
||||
} catch (error) {
|
||||
|
||||
136
src/app/api/providers/[id]/test/codexAppServerHealth.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
/**
|
||||
* Build the structured diagnosis object the connection-test route returns.
|
||||
* Lives here (rather than inline in test/route.ts) so both the route and the
|
||||
* codex-app-server health probe share one definition. Pure.
|
||||
*/
|
||||
export function makeDiagnosis(
|
||||
type: string,
|
||||
source: string,
|
||||
message: string | null,
|
||||
code: string | null = null
|
||||
) {
|
||||
return {
|
||||
type,
|
||||
source,
|
||||
message: message || null,
|
||||
code: code ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
export type CodexAppServerHealth = {
|
||||
valid: boolean;
|
||||
error?: string;
|
||||
diagnosis: unknown;
|
||||
refreshed: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* A codex "app-server" connection (providerSpecificData.codexTransport ===
|
||||
* "app-server") does NOT carry a validatable OpenAI token: it drives the codex
|
||||
* CLI's own `codex app-server` process over JSON-RPC/WebSocket, and THAT process
|
||||
* self-manages its OpenAI OAuth (its own ~/.codex/auth.json), exactly like an
|
||||
* interactive codex session. So the ordinary OAuth token probe is meaningless for
|
||||
* these connections — it validates a placeholder and reports a false "Token
|
||||
* invalid or revoked" 401 (which then trips the rate-limit cooldown on retest).
|
||||
*
|
||||
* The correct health signal for this transport is whether the app-server itself
|
||||
* is reachable and ready. The app-server exposes an unauthenticated liveness
|
||||
* endpoint at <httpBase>/readyz (200 = ready) alongside its ws:// listener, so we
|
||||
* derive the http(s) origin from the configured ws(s):// URL and probe /readyz.
|
||||
* Returns null when this connection is NOT an app-server connection (so the caller
|
||||
* falls through to the normal token validation).
|
||||
*/
|
||||
export async function testCodexAppServerConnection(
|
||||
connection: any
|
||||
): Promise<CodexAppServerHealth | null> {
|
||||
const psd = (connection?.providerSpecificData as Record<string, unknown> | undefined) || undefined;
|
||||
// Fire the /readyz probe when EITHER (a) the connection opted into the
|
||||
// app-server transport via the per-connection flag (a `codex` provider
|
||||
// connection with codexTransport==="app-server"), OR (b) this is the
|
||||
// first-class `codex-app-server` provider, which is app-server by definition
|
||||
// and needs no flag. Otherwise return null so the caller falls through to the
|
||||
// normal OAuth/apikey token validation.
|
||||
const isAppServerProvider = connection?.provider === "codex-app-server";
|
||||
const isAppServerFlag = psd?.codexTransport === "app-server";
|
||||
if (!isAppServerProvider && !isAppServerFlag) return null;
|
||||
|
||||
// Dynamic import (not a static top-level import) so this executor-config module
|
||||
// stays behind the open-sse boundary the no-restricted-imports lint rule enforces.
|
||||
const { resolveAppServerConfig } = await import(
|
||||
"@omniroute/open-sse/executors/codex/appServerConfig.ts"
|
||||
);
|
||||
const config = resolveAppServerConfig(psd);
|
||||
if (!config) {
|
||||
const error = "Codex app-server transport is not configured (missing url or token)";
|
||||
return {
|
||||
valid: false,
|
||||
error,
|
||||
refreshed: false,
|
||||
diagnosis: makeDiagnosis("validation_error", "local", error, "app_server_unconfigured"),
|
||||
};
|
||||
}
|
||||
|
||||
// ws://host:port → http://host:port/readyz ; wss:// → https://.
|
||||
const httpBase = config.url.replace(/^ws(s?):\/\//i, (_m, s) => `http${s}://`).replace(/\/+$/, "");
|
||||
const readyzUrl = `${httpBase}/readyz`;
|
||||
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), 8000);
|
||||
try {
|
||||
const res = await fetch(readyzUrl, {
|
||||
method: "GET",
|
||||
headers: { Authorization: `Bearer ${config.token}` },
|
||||
signal: controller.signal,
|
||||
});
|
||||
if (res.status !== 200) {
|
||||
const error = `Codex app-server not ready (${readyzUrl} → HTTP ${res.status})`;
|
||||
return {
|
||||
valid: false,
|
||||
error,
|
||||
refreshed: false,
|
||||
diagnosis: makeDiagnosis("provider_error", "app_server", error, "app_server_not_ready"),
|
||||
};
|
||||
}
|
||||
// The server PROCESS is up. Now confirm its Codex CLI is actually SIGNED IN —
|
||||
// /readyz alone would show green for a logged-out CLI, which then fails on the
|
||||
// first real turn. Probe account/read over the JSON-RPC WebSocket.
|
||||
let authStatus;
|
||||
try {
|
||||
const [{ probeCodexAppServerAuth }, { getCodexAppServerWebsocketTransport }] =
|
||||
await Promise.all([
|
||||
import("@omniroute/open-sse/executors/codex/appServerAuthProbe.ts"),
|
||||
import("@omniroute/open-sse/executors/codex.ts"),
|
||||
]);
|
||||
authStatus = await probeCodexAppServerAuth(config, getCodexAppServerWebsocketTransport(), 8000);
|
||||
} catch (probeErr: any) {
|
||||
// If the auth probe itself fails to load/run, don't fail the whole health
|
||||
// check — the server IS reachable. Treat as unknown-but-reachable (valid).
|
||||
authStatus = { state: "unknown", reason: probeErr?.message ?? "auth probe failed" } as const;
|
||||
}
|
||||
|
||||
if (authStatus.state === "logged_out") {
|
||||
const error =
|
||||
"Codex app-server is running but its Codex CLI is not signed in. Use \u201cSign in with ChatGPT\u201d to authenticate.";
|
||||
return {
|
||||
valid: false,
|
||||
error,
|
||||
refreshed: false,
|
||||
diagnosis: makeDiagnosis("auth_required", "app_server", error, "app_server_login_required"),
|
||||
};
|
||||
}
|
||||
// "authenticated" → healthy; "unknown" (probe unavailable/timed out) → treat
|
||||
// the reachable server as healthy rather than blocking on an inconclusive probe.
|
||||
return { valid: true, refreshed: false, diagnosis: null };
|
||||
} catch (err: any) {
|
||||
const reason = err?.name === "AbortError" ? "timed out" : (err?.message ?? "unreachable");
|
||||
const error = `Codex app-server unreachable (${readyzUrl}: ${reason})`;
|
||||
return {
|
||||
valid: false,
|
||||
error,
|
||||
refreshed: false,
|
||||
diagnosis: makeDiagnosis("provider_error", "app_server", error, "app_server_unreachable"),
|
||||
};
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
} from "@/lib/oauth/gitlab";
|
||||
import { providerAllowsOptionalApiKey } from "@/shared/constants/providers";
|
||||
import { shouldUseApiKeyConnectionTest } from "./webSessionTestDispatch";
|
||||
import { testCodexAppServerConnection, makeDiagnosis } from "./codexAppServerHealth";
|
||||
import { removeConnectionHealth } from "@omniroute/open-sse/services/apiKeyRotator.ts";
|
||||
import { isConnectionUnavailableToAuxiliaryActivity } from "@/lib/exclusiveLeaseIsolation";
|
||||
import { classifyAmbiguousOrAuthError, type ClassifyFailureArgs } from "./mistralAmbiguousAuth";
|
||||
@@ -52,20 +53,6 @@ function toSafeMessage(value: any, fallback = "Unknown error"): string {
|
||||
return trimmed || fallback;
|
||||
}
|
||||
|
||||
function makeDiagnosis(
|
||||
type: string,
|
||||
source: string,
|
||||
message: string | null,
|
||||
code: string | null = null
|
||||
) {
|
||||
return {
|
||||
type,
|
||||
source,
|
||||
message: message || null,
|
||||
code: code ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* A provider/account that the upstream has deactivated (vs. a revoked/expired token).
|
||||
* #1444: a Codex account can have a perfectly healthy OAuth refresh while its ChatGPT
|
||||
@@ -1024,6 +1011,13 @@ export async function testSingleConnection(connectionId: string, validationModel
|
||||
const startTime = Date.now();
|
||||
const runtime = await getProviderRuntimeStatus(connection);
|
||||
|
||||
// Codex app-server connections carry no validatable OpenAI token (the codex
|
||||
// app-server process self-manages its own OAuth). Probe the app-server's
|
||||
// /readyz liveness endpoint instead of the meaningless token check — otherwise
|
||||
// every sweep reports a false "Token invalid or revoked" 401 and cools the
|
||||
// connection down. Returns null for non-app-server connections (fall through).
|
||||
const appServerResult = await testCodexAppServerConnection(connection);
|
||||
|
||||
if ((runtime as any)?.diagnosis) {
|
||||
result = {
|
||||
valid: false,
|
||||
@@ -1031,6 +1025,10 @@ export async function testSingleConnection(connectionId: string, validationModel
|
||||
refreshed: false,
|
||||
diagnosis: (runtime as any).diagnosis,
|
||||
};
|
||||
} else if (appServerResult) {
|
||||
result = await runWithProxyContext(proxyInfo?.proxy || null, () =>
|
||||
Promise.resolve(appServerResult)
|
||||
);
|
||||
} else if (shouldUseApiKeyConnectionTest(connection.authType, provider)) {
|
||||
const enrichedConnection = validationModelId
|
||||
? {
|
||||
|
||||
@@ -13873,5 +13873,12 @@
|
||||
"toolsMismatch": "O provedor nao suporta chamada de ferramentas",
|
||||
"structuredOutputMismatch": "O provedor nao suporta saida estruturada",
|
||||
"contextWindowMismatch": "A requisicao excede a janela de contexto do provedor"
|
||||
},
|
||||
"cheaperInferenceSponsorBanner": {
|
||||
"title": "Cheaper Inference é um Amigo Open Source do OmniRoute",
|
||||
"description": "Um gateway ordenado por custo que revende dezenas de modelos de fronteira atrás de um único endpoint compatível com OpenAI, roteando cada requisição para o provedor elegível mais barato, nunca acima do preço de tabela.",
|
||||
"cta": "Obter uma chave de API",
|
||||
"partnerLinkNote": "Link de parceiro",
|
||||
"dismissAriaLabel": "Dispensar"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,10 @@ export interface NodeSqliteDatabaseLike {
|
||||
run(...p: unknown[]): { changes: number | bigint; lastInsertRowid: number | bigint };
|
||||
get(...p: unknown[]): unknown;
|
||||
all(...p: unknown[]): unknown[];
|
||||
// node:sqlite (DatabaseSync) statements expose these tuning setters. They
|
||||
// are optional here so the shared adapter also accepts lighter test doubles.
|
||||
setAllowUnknownNamedParameters?(enabled: boolean): void;
|
||||
setAllowBareNamedParameters?(enabled: boolean): void;
|
||||
};
|
||||
exec(sql: string): void;
|
||||
close(): void;
|
||||
@@ -13,6 +17,46 @@ export interface NodeSqliteDatabaseLike {
|
||||
|
||||
const MAX_STMT_CACHE_SIZE = 200;
|
||||
|
||||
// node:sqlite hands back rows whose prototype is `null` (Object.create(null)),
|
||||
// whereas better-sqlite3 (the driver we ship and run in production/CI) returns
|
||||
// ordinary Object.prototype rows. The difference is invisible for normal
|
||||
// property access but breaks callers that compare rows with structural
|
||||
// equality that also checks the prototype (e.g. Node's assert.deepStrictEqual,
|
||||
// used by unit tests written against the better-sqlite3 row shape). Normalize
|
||||
// every row to a plain object so the node:sqlite fallback is behaviourally
|
||||
// identical to the native better-sqlite3 path.
|
||||
function toPlainRow<T>(row: T): T {
|
||||
if (row === null || typeof row !== "object") return row;
|
||||
return { ...(row as Record<string, unknown>) } as T;
|
||||
}
|
||||
|
||||
// better-sqlite3 (the production/CI driver) and sql.js both accept `undefined`
|
||||
// as a bound value and treat it as SQL NULL. node:sqlite is stricter and throws
|
||||
// "Provided value cannot be bound to SQLite parameter N" for undefined. Several
|
||||
// call sites pass undefined for absent optional columns (e.g. a capability sync
|
||||
// that omits modalities_input), so coerce undefined -> null here to keep the
|
||||
// node:sqlite fallback behaviourally compatible with the native driver. This
|
||||
// handles both positional params and a single named-params object.
|
||||
function normalizeBindParams(params: unknown[]): unknown[] {
|
||||
const [first] = params;
|
||||
const isLoneNamedParamsObject =
|
||||
params.length === 1 &&
|
||||
first !== null &&
|
||||
typeof first === "object" &&
|
||||
!Array.isArray(first) &&
|
||||
!Buffer.isBuffer(first) &&
|
||||
!(first instanceof Uint8Array);
|
||||
if (isLoneNamedParamsObject) {
|
||||
const source = first as Record<string, unknown>;
|
||||
const normalized: Record<string, unknown> = {};
|
||||
for (const key of Object.keys(source)) {
|
||||
normalized[key] = source[key] === undefined ? null : source[key];
|
||||
}
|
||||
return [normalized];
|
||||
}
|
||||
return params.map((value) => (value === undefined ? null : value));
|
||||
}
|
||||
|
||||
export function createNodeSqliteAdapterFromDatabase(
|
||||
db: NodeSqliteDatabaseLike,
|
||||
filePath: string,
|
||||
@@ -41,6 +85,14 @@ export function createNodeSqliteAdapterFromDatabase(
|
||||
stmtCache.set(sql, entry);
|
||||
} else {
|
||||
const stmt = db.prepare(sql);
|
||||
// better-sqlite3 (the production/CI driver) silently ignores named
|
||||
// parameters supplied in the bind object that the SQL text does not
|
||||
// reference. node:sqlite instead throws "Unknown named parameter '<x>'".
|
||||
// Several call sites deliberately pass a superset params object (e.g. an
|
||||
// UPDATE that omits @createdAt while the shared params builder still
|
||||
// includes it), so relax node:sqlite to match better-sqlite3 and keep the
|
||||
// fallback driver behaviourally compatible.
|
||||
stmt.setAllowUnknownNamedParameters?.(true);
|
||||
if (stmtCache.size >= MAX_STMT_CACHE_SIZE) {
|
||||
const oldestKey = stmtCache.keys().next().value;
|
||||
if (oldestKey !== undefined) {
|
||||
@@ -119,17 +171,19 @@ export function createNodeSqliteAdapterFromDatabase(
|
||||
const stmt = getCached(sql);
|
||||
return {
|
||||
run(...params: unknown[]): RunResult {
|
||||
const r = stmt.run(...params);
|
||||
const r = stmt.run(...normalizeBindParams(params));
|
||||
return {
|
||||
changes: Number(r.changes ?? 0),
|
||||
lastInsertRowid: Number(r.lastInsertRowid ?? 0),
|
||||
};
|
||||
},
|
||||
get(...params: unknown[]): unknown {
|
||||
return stmt.get(...params);
|
||||
return toPlainRow(stmt.get(...normalizeBindParams(params)));
|
||||
},
|
||||
all(...params: unknown[]): unknown[] {
|
||||
return stmt.all(...params);
|
||||
return (stmt.all(...normalizeBindParams(params)) as unknown[]).map((row) =>
|
||||
toPlainRow(row)
|
||||
);
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
@@ -6,6 +6,19 @@ import type { SqliteAdapter, PreparedStatement, RunResult } from "./types";
|
||||
const SAVE_DEBOUNCE_MS = 100;
|
||||
const CHECKPOINT_INTERVAL_MS = 60_000;
|
||||
|
||||
// sql.js's stmt.getAsObject() returns rows whose prototype is `null`
|
||||
// (Object.create(null)), whereas better-sqlite3 (the driver we ship and run in
|
||||
// production/CI) hands back ordinary Object.prototype rows. That difference is
|
||||
// invisible for normal property access but breaks callers that compare rows
|
||||
// with structural equality that also checks the prototype (e.g. Node's
|
||||
// assert.deepStrictEqual, used by several unit tests written against the
|
||||
// better-sqlite3 row shape). Normalize every row to a plain object so the
|
||||
// sql.js fallback is behaviourally identical to the native better-sqlite3 path.
|
||||
function toPlainRow<T>(row: T): T {
|
||||
if (row === null || typeof row !== "object") return row;
|
||||
return { ...(row as Record<string, unknown>) } as T;
|
||||
}
|
||||
|
||||
let _sqlJsLib: Awaited<ReturnType<(typeof import("sql.js"))["default"]>> | null = null;
|
||||
|
||||
function resolveSqlJsWasmPath(): string {
|
||||
@@ -240,7 +253,7 @@ export async function createSqlJsAdapter(filePath: string): Promise<SqliteAdapte
|
||||
try {
|
||||
const bindValue = toBindValue(params);
|
||||
if (bindValue !== undefined) stmt.bind(bindValue);
|
||||
if (stmt.step()) return stmt.getAsObject();
|
||||
if (stmt.step()) return toPlainRow(stmt.getAsObject());
|
||||
return undefined;
|
||||
} finally {
|
||||
stmt.free();
|
||||
@@ -252,7 +265,7 @@ export async function createSqlJsAdapter(filePath: string): Promise<SqliteAdapte
|
||||
const bindValue = toBindValue(params);
|
||||
if (bindValue !== undefined) stmt.bind(bindValue);
|
||||
const rows: unknown[] = [];
|
||||
while (stmt.step()) rows.push(stmt.getAsObject());
|
||||
while (stmt.step()) rows.push(toPlainRow(stmt.getAsObject()));
|
||||
return rows;
|
||||
} finally {
|
||||
stmt.free();
|
||||
|
||||
@@ -350,3 +350,89 @@ export async function writeCodexAuthFileToLocalCli(connectionId: string) {
|
||||
centralizedBackupPath,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Decision for the guarded write (see writeCodexAuthFileToLocalCliIfNeeded).
|
||||
*/
|
||||
export type CodexAuthWriteDecision =
|
||||
| "written" // wrote a fresh auth.json (was absent, stale, or force)
|
||||
| "skipped_present_fresh"; // an existing, non-stale auth.json was left untouched
|
||||
|
||||
/**
|
||||
* Guarded variant of writeCodexAuthFileToLocalCli for the codex-app-server
|
||||
* "Sign in with ChatGPT" flow. Per the design decision (William, Q2):
|
||||
*
|
||||
* - Write ONLY when ~/.codex/auth.json is ABSENT, or STALE (its token is at/
|
||||
* past the refresh buffer), or when `force` is set.
|
||||
* - NEVER clobber an existing, healthy (non-stale) auth.json — a user may be
|
||||
* managing the CLI session themselves. (The underlying writer always makes a
|
||||
* backup regardless, so even a forced overwrite is recoverable.)
|
||||
*
|
||||
* Staleness is read from the existing file's `last_refresh` + the token's own
|
||||
* expiry claim (JWT `exp` on the access_token) when present; if neither is
|
||||
* readable we treat the file as fresh (do not clobber).
|
||||
*
|
||||
* Returns the write decision plus (when written) the underlying write result.
|
||||
*/
|
||||
export async function writeCodexAuthFileToLocalCliIfNeeded(
|
||||
connectionId: string,
|
||||
options: { force?: boolean } = {}
|
||||
): Promise<{ decision: CodexAuthWriteDecision; authPath: string | null; result?: Awaited<ReturnType<typeof writeCodexAuthFileToLocalCli>> }> {
|
||||
const paths = getCliConfigPaths("codex");
|
||||
const authPath = paths?.auth ?? null;
|
||||
|
||||
if (!options.force && authPath) {
|
||||
const existing = await readExistingCodexAuth(authPath);
|
||||
if (existing && !isCodexAuthStale(existing)) {
|
||||
// Present and healthy — do not clobber a session we didn't (or don't need
|
||||
// to) manage. The connection can still authenticate turns via this file.
|
||||
return { decision: "skipped_present_fresh", authPath };
|
||||
}
|
||||
}
|
||||
|
||||
const result = await writeCodexAuthFileToLocalCli(connectionId);
|
||||
return { decision: "written", authPath: result.authPath, result };
|
||||
}
|
||||
|
||||
/** Read + parse an existing ~/.codex/auth.json; null when absent/unreadable. */
|
||||
async function readExistingCodexAuth(authPath: string): Promise<CodexAuthFilePayload | null> {
|
||||
try {
|
||||
const raw = await fs.readFile(authPath, "utf8");
|
||||
const parsed = JSON.parse(raw) as unknown;
|
||||
const rec = toRecord(parsed);
|
||||
const tokens = toRecord(rec.tokens);
|
||||
if (!toNonEmptyString(tokens.access_token)) return null;
|
||||
return parsed as CodexAuthFilePayload;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A stored auth.json is "stale" when its access_token is at/past the refresh
|
||||
* buffer. Prefer the JWT `exp` claim on the access_token; fall back to
|
||||
* `last_refresh` + a conservative validity window; if neither is parseable,
|
||||
* treat as NOT stale (never clobber on ambiguity).
|
||||
*/
|
||||
function isCodexAuthStale(payload: CodexAuthFilePayload): boolean {
|
||||
const accessToken = toNonEmptyString(payload?.tokens?.access_token);
|
||||
if (accessToken) {
|
||||
const claims = decodeJwtPayload(accessToken);
|
||||
const exp = claims && typeof claims.exp === "number" ? claims.exp : null;
|
||||
if (exp) {
|
||||
const expiresAtMs = exp * 1000;
|
||||
return expiresAtMs - Date.now() <= CODEX_REFRESH_BUFFER_MS;
|
||||
}
|
||||
}
|
||||
// No usable exp claim — fall back to last_refresh age. Codex access tokens are
|
||||
// short-lived (~hours); if the file hasn't refreshed in > 6h, consider it stale.
|
||||
const lastRefresh = toNonEmptyString(payload?.last_refresh);
|
||||
if (lastRefresh) {
|
||||
const refreshedMs = new Date(lastRefresh).getTime();
|
||||
if (!Number.isNaN(refreshedMs)) {
|
||||
const SIX_HOURS_MS = 6 * 60 * 60 * 1000;
|
||||
return Date.now() - refreshedMs >= SIX_HOURS_MS;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { getImageProvider } from "@omniroute/open-sse/config/imageRegistry";
|
||||
|
||||
import { getProviderOutboundGuard } from "@/shared/network/outboundUrlGuardPolicy";
|
||||
import { isSecurityBlockError } from "@/lib/providers/validation/transport";
|
||||
import {
|
||||
SAFE_OUTBOUND_FETCH_PRESETS,
|
||||
SafeOutboundFetchError,
|
||||
@@ -62,7 +63,7 @@ function toValidationErrorResult(error: unknown) {
|
||||
...(error instanceof SafeOutboundFetchError && error.code === "TIMEOUT"
|
||||
? { timeout: true }
|
||||
: {}),
|
||||
...(statusCode === 400 ? { securityBlocked: true } : {}),
|
||||
...(isSecurityBlockError(error) ? { securityBlocked: true } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
export const DEFAULT_CODEX_CLIENT_VERSION = "0.146.0";
|
||||
// Kept in lockstep with the codex CLI actually installed in the OmniRoute image
|
||||
// (bin/omniroute-fix.Containerfile installs `codex` latest; app-server runtime is
|
||||
// 0.149.0 as of 2026-08-22). When the image's codex is bumped, refresh this so the
|
||||
// fingerprint OpenAI sees from the OAuth/Responses face matches the real client
|
||||
// version. Overridable per-deployment via the CODEX_CLIENT_VERSION env.
|
||||
export const DEFAULT_CODEX_CLIENT_VERSION = "0.149.0";
|
||||
export const CODEX_CLI_RS_ORIGINATOR = "codex_cli_rs";
|
||||
|
||||
export function getCodexCliRsHeaders(
|
||||
|
||||
@@ -401,6 +401,18 @@ export const FEATURE_FLAG_DEFINITIONS: FeatureFlagDefinition[] = [
|
||||
requiresRestart: false,
|
||||
warningLevel: "info",
|
||||
},
|
||||
{
|
||||
key: "OMNIROUTE_CODEX_APP_SERVER_ENABLED",
|
||||
label: "Codex App-Server Transport",
|
||||
description:
|
||||
"Allow Codex to use the local app-server WebSocket JSON-RPC transport (codexTransport=app-server). When off, connections opted into app-server fall back to Codex's other transports.",
|
||||
descriptionI18nKey: "featureFlagOmnirouteCodexAppServerEnabledDescription",
|
||||
category: "runtime",
|
||||
defaultValue: "true",
|
||||
type: "boolean",
|
||||
requiresRestart: false,
|
||||
warningLevel: "info",
|
||||
},
|
||||
{
|
||||
key: "OMNIROUTE_EMERGENCY_FALLBACK",
|
||||
label: "Emergency Fallback",
|
||||
|
||||
@@ -175,6 +175,30 @@ export const NOAUTH_PROVIDERS = {
|
||||
text: "ZCode runs locally through its native app-server. OmniRoute never receives or stores the Z.ai credential.",
|
||||
},
|
||||
},
|
||||
"codex-app-server": {
|
||||
id: "codex-app-server",
|
||||
alias: "cxa",
|
||||
name: "OpenAI Codex (App-Server)",
|
||||
icon: "code",
|
||||
color: "#10A37F",
|
||||
textIcon: "CA",
|
||||
website: "https://developers.openai.com/codex/cli",
|
||||
noAuth: true,
|
||||
hasFree: false,
|
||||
serviceKinds: ["llm"],
|
||||
isLocalCli: true,
|
||||
// No subscriptionRisk / riskNoticeVariant: unlike the `codex` provider (which
|
||||
// replays your ChatGPT/OpenAI session token to the API), this transport 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 — OmniRoute never replays a token to the API — so the
|
||||
// "official session not authorized for proxy use" caveat does not apply.
|
||||
authHint:
|
||||
"No token stored by OmniRoute. The Codex CLI app-server manages its own ChatGPT sign-in (~/.codex/auth.json, auto-refreshed). Use \u201cSign in with ChatGPT\u201d if the CLI is not yet authenticated.",
|
||||
notice: {
|
||||
text: "OpenAI Codex (App-Server) drives the Codex CLI's local app-server (JSON-RPC over WebSocket). The CLI self-manages its OpenAI OAuth, so OmniRoute never sees or replays your token. Requires the codex CLI reachable at the configured app-server URL; sign in via the CLI or the dashboard \u201cSign in with ChatGPT\u201d action.",
|
||||
},
|
||||
},
|
||||
uncloseai: {
|
||||
id: "uncloseai",
|
||||
alias: "unc",
|
||||
|
||||
@@ -338,6 +338,7 @@ export const CLI_TOOL_ALIASES: Readonly<Record<string, string>> = {
|
||||
"claude-code": "claude",
|
||||
"openai-codex": "codex",
|
||||
openai: "codex",
|
||||
"codex-app-server": "codex",
|
||||
cn: "continue",
|
||||
qodercli: "qoder",
|
||||
};
|
||||
|
||||
@@ -72,6 +72,8 @@
|
||||
"tests/unit/antigravity-prefer-stored-project.test.ts",
|
||||
"tests/unit/api-key-policy-noauth-allowed-connections.test.ts",
|
||||
"tests/unit/api-key-rotator-health.test.ts",
|
||||
"tests/unit/chat-routing-synced-inventory-11089.test.ts",
|
||||
"tests/unit/security-route-guard-tiers.test.ts",
|
||||
"tests/unit/egress-ip-lock-10880.test.ts",
|
||||
"tests/unit/egress-lock-allowlist-10880.test.ts",
|
||||
"tests/unit/api/jobs.test.ts",
|
||||
|
||||
@@ -48,10 +48,10 @@ const route = await import("../../src/app/api/search/providers/route.ts");
|
||||
// Constants
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// 16 search-kind providers: serper, brave, perplexity, exa, tavily, firecrawl,
|
||||
// 17 search-kind providers: serper, brave, perplexity, exa, tavily, firecrawl,
|
||||
// google-pse, linkup, searchapi, youcom, searxng, ollama, zai, jina-search,
|
||||
// duckduckgo-free, x-search (registry open-sse/config/searchRegistry.ts).
|
||||
const EXPECTED_SEARCH_COUNT = 16;
|
||||
// context7 (#11140), duckduckgo-free, x-search (registry open-sse/config/searchRegistry.ts).
|
||||
const EXPECTED_SEARCH_COUNT = 17;
|
||||
const EXPECTED_FETCH_COUNT = 4;
|
||||
const EXPECTED_TOTAL = EXPECTED_SEARCH_COUNT + EXPECTED_FETCH_COUNT;
|
||||
|
||||
@@ -138,7 +138,7 @@ test("search-providers-catalog: returns 401 for unauthenticated requests when au
|
||||
assert.ok(!bodyStr.includes(" at /"), "error body must not contain stack trace");
|
||||
});
|
||||
|
||||
test("search-providers-catalog: returns 16 providers (13 search + 3 fetch)", async () => {
|
||||
test("search-providers-catalog: returns 21 providers (17 search + 4 fetch)", async () => {
|
||||
const req = await buildAuthRequest();
|
||||
const res = await route.GET(req);
|
||||
|
||||
|
||||
@@ -17,6 +17,11 @@
|
||||
"status": 400,
|
||||
"throws": true
|
||||
},
|
||||
"context7": {
|
||||
"message": "Provider \"context7\" is a search provider and does not support chat completions; use the /v1/search endpoint instead.",
|
||||
"status": 400,
|
||||
"throws": true
|
||||
},
|
||||
"duckduckgo-free": {
|
||||
"message": "Provider \"duckduckgo-free\" is a search provider and does not support chat completions; use the /v1/search endpoint instead.",
|
||||
"status": 400,
|
||||
|
||||
@@ -155,6 +155,11 @@
|
||||
"configSource": "codex",
|
||||
"provider": "codex"
|
||||
},
|
||||
"codex-app-server": {
|
||||
"className": "CodexAppServerExecutor",
|
||||
"configSource": "codex-app-server",
|
||||
"provider": "codex-app-server"
|
||||
},
|
||||
"command-code": {
|
||||
"className": "CommandCodeExecutor",
|
||||
"configSource": "<custom-config>",
|
||||
@@ -706,6 +711,6 @@
|
||||
"provider": "zai-web"
|
||||
}
|
||||
},
|
||||
"keyCount": 141,
|
||||
"keyCount": 142,
|
||||
"sharedInstances": []
|
||||
}
|
||||
|
||||
@@ -1311,16 +1311,16 @@
|
||||
"Authorization": "Bearer <TOK>",
|
||||
"Content-Type": "application/json",
|
||||
"Openai-Beta": "responses=experimental",
|
||||
"User-Agent": "codex-cli/0.146.0 (<OS>; <ARCH>)",
|
||||
"Version": "0.146.0",
|
||||
"User-Agent": "codex-cli/0.149.0 (<OS>; <ARCH>)",
|
||||
"Version": "0.149.0",
|
||||
"X-Codex-Beta-Features": "responses_websockets"
|
||||
},
|
||||
"nonStream": {
|
||||
"Authorization": "Bearer <TOK>",
|
||||
"Content-Type": "application/json",
|
||||
"Openai-Beta": "responses=experimental",
|
||||
"User-Agent": "codex-cli/0.146.0 (<OS>; <ARCH>)",
|
||||
"Version": "0.146.0",
|
||||
"User-Agent": "codex-cli/0.149.0 (<OS>; <ARCH>)",
|
||||
"Version": "0.149.0",
|
||||
"X-Codex-Beta-Features": "responses_websockets"
|
||||
},
|
||||
"oauth": {
|
||||
@@ -1328,8 +1328,8 @@
|
||||
"Authorization": "Bearer <TOK>",
|
||||
"Content-Type": "application/json",
|
||||
"Openai-Beta": "responses=experimental",
|
||||
"User-Agent": "codex-cli/0.146.0 (<OS>; <ARCH>)",
|
||||
"Version": "0.146.0",
|
||||
"User-Agent": "codex-cli/0.149.0 (<OS>; <ARCH>)",
|
||||
"Version": "0.149.0",
|
||||
"X-Codex-Beta-Features": "responses_websockets"
|
||||
}
|
||||
},
|
||||
@@ -1338,6 +1338,29 @@
|
||||
"stream": "https://chatgpt.com/backend-api/codex/responses"
|
||||
}
|
||||
},
|
||||
"codex-app-server": {
|
||||
"format": "openai-responses",
|
||||
"headers": {
|
||||
"apiKey": {
|
||||
"Accept": "text/event-stream",
|
||||
"Authorization": "Bearer <TOK>",
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
"nonStream": {
|
||||
"Authorization": "Bearer <TOK>",
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
"oauth": {
|
||||
"Accept": "text/event-stream",
|
||||
"Authorization": "Bearer <TOK>",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
},
|
||||
"url": {
|
||||
"nonStream": "codex-app-server://cli/websocket",
|
||||
"stream": "codex-app-server://cli/websocket"
|
||||
}
|
||||
},
|
||||
"cohere": {
|
||||
"format": "openai",
|
||||
"headers": {
|
||||
|
||||
48
tests/unit/_helpers/betterSqlite3Availability.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
// Shared guard for unit tests that construct a real better-sqlite3 Database as a
|
||||
// test fixture (e.g. seeding a legacy on-disk schema before exercising the
|
||||
// migration runner). better-sqlite3 is a native addon: production and CI load
|
||||
// it fine, but some sandboxes/dev boxes ship a system glibc older than the
|
||||
// prebuilt binary requires (e.g. "GLIBC_2.29 not found"), so `new Database(...)`
|
||||
// throws ERR_DLOPEN_FAILED at fixture-construction time. That is an environment
|
||||
// limitation, NOT a defect in the code under test — the OmniRoute runtime itself
|
||||
// cascades to node:sqlite/sql.js when better-sqlite3 can't load, so the app keeps
|
||||
// working; only tests that reach for better-sqlite3 DIRECTLY (to build a
|
||||
// driver-specific fixture) are affected.
|
||||
//
|
||||
// Tests import `betterSqlite3Available` to decide whether to run or to skip with
|
||||
// a clear, documented reason. In CI (where better-sqlite3 loads) the tests run
|
||||
// normally; only the constrained sandbox skips them.
|
||||
//
|
||||
// Usage:
|
||||
// import { betterSqlite3Available, BETTER_SQLITE3_SKIP_REASON } from "./_helpers/betterSqlite3Availability";
|
||||
// const canUseBetterSqlite3 = betterSqlite3Available();
|
||||
// test("...", { skip: canUseBetterSqlite3 ? false : BETTER_SQLITE3_SKIP_REASON }, () => { ... });
|
||||
|
||||
import { createRequire } from "node:module";
|
||||
|
||||
export const BETTER_SQLITE3_SKIP_REASON =
|
||||
"better-sqlite3 native addon cannot load in this environment (e.g. system " +
|
||||
"glibc older than the prebuilt binary requires — 'GLIBC_2.29 not found'). " +
|
||||
"This is a sandbox/environment limitation, not a code defect: the runtime " +
|
||||
"cascades to node:sqlite/sql.js, and CI runs this test with a working " +
|
||||
"better-sqlite3.";
|
||||
|
||||
let cached: boolean | null = null;
|
||||
|
||||
/**
|
||||
* Returns true when a real better-sqlite3 Database can be constructed in the
|
||||
* current environment. Result is memoized. Never throws.
|
||||
*/
|
||||
export function betterSqlite3Available(): boolean {
|
||||
if (cached !== null) return cached;
|
||||
try {
|
||||
const require = createRequire(import.meta.url);
|
||||
const Database = require("better-sqlite3");
|
||||
const db = new Database(":memory:");
|
||||
db.close();
|
||||
cached = true;
|
||||
} catch {
|
||||
cached = false;
|
||||
}
|
||||
return cached;
|
||||
}
|
||||
@@ -112,7 +112,7 @@ test("AgentRouter Responses requests automatically use the native Responses prot
|
||||
body: structuredClone(body),
|
||||
headers: new Headers({ accept: "application/json", originator: "codex_cli_rs" }),
|
||||
},
|
||||
userAgent: "codex_cli_rs/0.146.0",
|
||||
userAgent: "codex_cli_rs/0.149.0",
|
||||
});
|
||||
|
||||
assert.ok(captured);
|
||||
@@ -176,7 +176,7 @@ test("AgentRouter OpenAI Chat requests automatically use the native Chat protoco
|
||||
body: structuredClone(body),
|
||||
headers: new Headers({ accept: "application/json" }),
|
||||
},
|
||||
userAgent: "codex_cli_rs/0.146.0",
|
||||
userAgent: "codex_cli_rs/0.149.0",
|
||||
});
|
||||
|
||||
assert.equal(result.success, true);
|
||||
@@ -304,7 +304,7 @@ test("AgentRouter Responses streaming stays native without a connection protocol
|
||||
body: structuredClone(body),
|
||||
headers: new Headers({ accept: "text/event-stream", originator: "codex_cli_rs" }),
|
||||
},
|
||||
userAgent: "codex_cli_rs/0.146.0",
|
||||
userAgent: "codex_cli_rs/0.149.0",
|
||||
});
|
||||
|
||||
assert.equal(result.success, true);
|
||||
@@ -383,7 +383,7 @@ test("AgentRouter OpenAI Chat streaming stays native without a connection protoc
|
||||
body: structuredClone(body),
|
||||
headers: new Headers({ accept: "text/event-stream" }),
|
||||
},
|
||||
userAgent: "codex_cli_rs/0.146.0",
|
||||
userAgent: "codex_cli_rs/0.149.0",
|
||||
});
|
||||
|
||||
assert.equal(result.success, true);
|
||||
|
||||
@@ -84,7 +84,7 @@ test("AgentRouter OpenAI Chat dispatch uses Codex identity without Claude-only b
|
||||
assert.equal(captured.url, "https://agentrouter.org/v1/chat/completions");
|
||||
assert.equal(captured.headers.get("authorization"), "Bearer test-agentrouter-key");
|
||||
assert.equal(captured.headers.get("x-api-key"), null);
|
||||
assert.equal(captured.headers.get("user-agent"), "codex_cli_rs/0.146.0");
|
||||
assert.equal(captured.headers.get("user-agent"), "codex_cli_rs/0.149.0");
|
||||
assert.equal(captured.headers.get("originator"), "codex_cli_rs");
|
||||
assert.equal(captured.headers.get("x-app"), null);
|
||||
assert.equal(captured.headers.get("anthropic-version"), null);
|
||||
@@ -130,7 +130,7 @@ test("AgentRouter OpenAI Responses dispatch uses the Responses endpoint and Code
|
||||
assert.ok(captured);
|
||||
assert.equal(captured.url, "https://agentrouter.org/v1/responses");
|
||||
assert.equal(captured.headers.get("authorization"), "Bearer test-agentrouter-key");
|
||||
assert.equal(captured.headers.get("user-agent"), "codex_cli_rs/0.146.0");
|
||||
assert.equal(captured.headers.get("user-agent"), "codex_cli_rs/0.149.0");
|
||||
assert.equal(captured.headers.get("originator"), "codex_cli_rs");
|
||||
assert.equal(captured.headers.get("x-app"), null);
|
||||
assert.equal(captured.headers.get("anthropic-beta"), null);
|
||||
|
||||
@@ -1,3 +1,14 @@
|
||||
// ENVIRONMENT NOTE (node:test runner cancellation, not a code defect):
|
||||
// The subtests below exercise real-timer / AbortSignal.timeout-bounded async
|
||||
// paths and fire-and-forget work guarded by unref()'d timers. In this sandbox
|
||||
// they intermittently surface as `cancelledByParent` ("Promise resolution is
|
||||
// still pending but the event loop has already resolved") rather than pass or
|
||||
// fail: the node:test runner decides the event loop has settled before the
|
||||
// unref'd timer/promise chain finishes. This is a pre-existing test-harness /
|
||||
// runtime interaction (present on the clean tree before the codex-app-server
|
||||
// work, and unrelated to it) — the code under test resolves correctly when
|
||||
// invoked directly (e.g. testOAuthConnection(github, 50) returns a bounded
|
||||
// "timed out" failure in ~50ms). CI, on its runner, completes these normally.
|
||||
// Regression guard for the Antigravity OAuth login hang.
|
||||
//
|
||||
// The dashboard login "just spun forever" because postExchange `await`ed the
|
||||
|
||||
@@ -1,3 +1,13 @@
|
||||
// ENVIRONMENT NOTE (sandbox better-sqlite3 / glibc limitation, not a code defect):
|
||||
// This test constructs or exercises a real better-sqlite3-backed SQLite database.
|
||||
// better-sqlite3 is a native addon; production and CI load it normally, but some
|
||||
// sandboxes/dev boxes ship a system glibc older than the prebuilt binary requires
|
||||
// ("GLIBC_2.29 not found"), so the native module fails to dlopen and any test that
|
||||
// reaches better-sqlite3 directly (or asserts stdout that the load-failure warning
|
||||
// would pollute) fails HERE while passing in CI. This is a known environment
|
||||
// limitation, not a defect in the code under test: the OmniRoute runtime itself
|
||||
// cascades to node:sqlite/sql.js when better-sqlite3 is unavailable. See
|
||||
// tests/unit/_helpers/betterSqlite3Availability.ts for a guard helper.
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
|
||||
@@ -1,3 +1,13 @@
|
||||
// ENVIRONMENT NOTE (sandbox better-sqlite3 / glibc limitation, not a code defect):
|
||||
// This test constructs or exercises a real better-sqlite3-backed SQLite database.
|
||||
// better-sqlite3 is a native addon; production and CI load it normally, but some
|
||||
// sandboxes/dev boxes ship a system glibc older than the prebuilt binary requires
|
||||
// ("GLIBC_2.29 not found"), so the native module fails to dlopen and any test that
|
||||
// reaches better-sqlite3 directly (or asserts stdout that the load-failure warning
|
||||
// would pollute) fails HERE while passing in CI. This is a known environment
|
||||
// limitation, not a defect in the code under test: the OmniRoute runtime itself
|
||||
// cascades to node:sqlite/sql.js when better-sqlite3 is unavailable. See
|
||||
// tests/unit/_helpers/betterSqlite3Availability.ts for a guard helper.
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
|
||||
@@ -120,4 +120,14 @@ test("every @huggingface/transformers consumer loads it lazily so absent install
|
||||
/^\s*import\s+(?:[^'"]*?\s+from\s+)?["']@huggingface\/transformers["']/m,
|
||||
"onnxWorker.ts must not statically import @huggingface/transformers"
|
||||
);
|
||||
// Positive anchor (required by source-scanner-guards.test.ts): prove the read
|
||||
// resolved to the real, non-empty onnxWorker.ts. Without this, renaming or
|
||||
// gutting the worker would leave the negative guard above passing while
|
||||
// protecting nothing. The worker loads the optional transformer deps lazily
|
||||
// via a dynamicImport() helper, so anchor on that stable call.
|
||||
assert.match(
|
||||
workerSrc,
|
||||
/dynamicImport\(["']@huggingface\/transformers["']\)/,
|
||||
"onnxWorker.ts must load @huggingface/transformers via a deferred dynamicImport()"
|
||||
);
|
||||
});
|
||||
|
||||
@@ -88,7 +88,15 @@ describe("ccr protocol instruction (#8033)", () => {
|
||||
const body = makeBody([{ role: "user", content: LARGE_TEXT }]);
|
||||
const result = ccrEngine.apply(body);
|
||||
|
||||
assert.equal(result.compressed, true, "large block should still compress");
|
||||
// #7746 follow-up: a caller whose tools[] does not advertise
|
||||
// omniroute_ccr_retrieve can never resolve a content-addressed marker, so
|
||||
// replacing its text would strand it behind an unresolvable hash. The engine
|
||||
// therefore now SKIPS entirely for such callers (callerSupportsCcrRetrieve →
|
||||
// false ⇒ compressed:false), which is a strictly safer outcome than the old
|
||||
// "compress the block but withhold the instruction" behavior. Either way the
|
||||
// guarantee this test pins holds: no CCR marker/instruction reaches a caller
|
||||
// that cannot use it.
|
||||
assert.equal(result.compressed, false, "no-retrieve-tool caller must not be compressed");
|
||||
const messages = result.body["messages"] as Array<{ role: string; content: unknown }>;
|
||||
|
||||
assert.equal(messages.length, 1, "no system message should be injected");
|
||||
|
||||
@@ -57,9 +57,9 @@ test("Claude CLI wire versions match the captured 2.1.220 binary", () => {
|
||||
assert.equal(hdr.CLAUDE_CLI_BILLING_VERSION, canonical.CLAUDE_CODE_CLIENT_BILLING_VERSION);
|
||||
});
|
||||
|
||||
test("Codex client is pinned to the captured 0.146.0 release", () => {
|
||||
assert.equal(codexCfg.getCodexClientVersion(), "0.146.0");
|
||||
assert.equal(codexCfg.getCodexUserAgent(), "codex-cli/0.146.0 (Windows 10.0.26200; x64)");
|
||||
assert.equal(codexCfg.getCodexDefaultHeaders().Version, "0.146.0");
|
||||
assert.equal(codexCfg.getCodexCliRsHeaders()["User-Agent"], "codex_cli_rs/0.146.0");
|
||||
test("Codex client is pinned to the captured 0.149.0 release", () => {
|
||||
assert.equal(codexCfg.getCodexClientVersion(), "0.149.0");
|
||||
assert.equal(codexCfg.getCodexUserAgent(), "codex-cli/0.149.0 (Windows 10.0.26200; x64)");
|
||||
assert.equal(codexCfg.getCodexDefaultHeaders().Version, "0.149.0");
|
||||
assert.equal(codexCfg.getCodexCliRsHeaders()["User-Agent"], "codex_cli_rs/0.149.0");
|
||||
});
|
||||
|
||||
@@ -1,3 +1,13 @@
|
||||
// ENVIRONMENT NOTE (sandbox better-sqlite3 / glibc limitation, not a code defect):
|
||||
// This test constructs or exercises a real better-sqlite3-backed SQLite database.
|
||||
// better-sqlite3 is a native addon; production and CI load it normally, but some
|
||||
// sandboxes/dev boxes ship a system glibc older than the prebuilt binary requires
|
||||
// ("GLIBC_2.29 not found"), so the native module fails to dlopen and any test that
|
||||
// reaches better-sqlite3 directly (or asserts stdout that the load-failure warning
|
||||
// would pollute) fails HERE while passing in CI. This is a known environment
|
||||
// limitation, not a defect in the code under test: the OmniRoute runtime itself
|
||||
// cascades to node:sqlite/sql.js when better-sqlite3 is unavailable. See
|
||||
// tests/unit/_helpers/betterSqlite3Availability.ts for a guard helper.
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
|
||||
@@ -1,3 +1,13 @@
|
||||
// ENVIRONMENT NOTE (sandbox better-sqlite3 / glibc limitation, not a code defect):
|
||||
// This test constructs or exercises a real better-sqlite3-backed SQLite database.
|
||||
// better-sqlite3 is a native addon; production and CI load it normally, but some
|
||||
// sandboxes/dev boxes ship a system glibc older than the prebuilt binary requires
|
||||
// ("GLIBC_2.29 not found"), so the native module fails to dlopen and any test that
|
||||
// reaches better-sqlite3 directly (or asserts stdout that the load-failure warning
|
||||
// would pollute) fails HERE while passing in CI. This is a known environment
|
||||
// limitation, not a defect in the code under test: the OmniRoute runtime itself
|
||||
// cascades to node:sqlite/sql.js when better-sqlite3 is unavailable. See
|
||||
// tests/unit/_helpers/betterSqlite3Availability.ts for a guard helper.
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
|
||||
@@ -1,3 +1,13 @@
|
||||
// ENVIRONMENT NOTE (sandbox better-sqlite3 / glibc limitation, not a code defect):
|
||||
// This test constructs or exercises a real better-sqlite3-backed SQLite database.
|
||||
// better-sqlite3 is a native addon; production and CI load it normally, but some
|
||||
// sandboxes/dev boxes ship a system glibc older than the prebuilt binary requires
|
||||
// ("GLIBC_2.29 not found"), so the native module fails to dlopen and any test that
|
||||
// reaches better-sqlite3 directly (or asserts stdout that the load-failure warning
|
||||
// would pollute) fails HERE while passing in CI. This is a known environment
|
||||
// limitation, not a defect in the code under test: the OmniRoute runtime itself
|
||||
// cascades to node:sqlite/sql.js when better-sqlite3 is unavailable. See
|
||||
// tests/unit/_helpers/betterSqlite3Availability.ts for a guard helper.
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
|
||||
@@ -2,6 +2,14 @@ import { describe, it, before } from "node:test";
|
||||
import assert from "node:assert";
|
||||
import * as toolDetector from "../../../src/lib/cli-helper/tool-detector.ts";
|
||||
|
||||
// The Hermes tool detector honors a HERMES_HOME env var (#3628) and only falls
|
||||
// back to the default ~/.hermes/config.yaml path when it is unset. CI runs with
|
||||
// HERMES_HOME unset, but this suite can also run inside a Hermes Agent session
|
||||
// that exports HERMES_HOME, which redirects the detected config path and breaks
|
||||
// the ".hermes/config.yaml" assertion below. Unset it so the test is hermetic
|
||||
// and matches CI regardless of the ambient runtime.
|
||||
delete process.env.HERMES_HOME;
|
||||
|
||||
describe("tool-detector", () => {
|
||||
before(() => {
|
||||
// Install mock exec implementation for deterministic testing
|
||||
|
||||
@@ -53,6 +53,16 @@ test("ServerSupervisor starts Node with IPv4-first DNS", async () => {
|
||||
const dataDir = mkdtempSync(join(tmpdir(), "omniroute-ipv4-first-"));
|
||||
const previousDataDir = process.env.DATA_DIR;
|
||||
process.env.DATA_DIR = dataDir;
|
||||
// The supervisor reads process.env (not its own `env`) to decide whether an
|
||||
// explicit --max-old-space-size is already pinned via NODE_OPTIONS, in which
|
||||
// case it suppresses its own heap flag (envHasExplicitHeapFlag). CI runs with
|
||||
// no heap flag in NODE_OPTIONS, but this suite can be launched with an ambient
|
||||
// NODE_OPTIONS=--max-old-space-size=... (e.g. the sandbox exports one), which
|
||||
// would make the supervisor legitimately drop the flag and fail the assertion
|
||||
// below. Neutralize it for the duration of this test so the expectation
|
||||
// matches the CI environment.
|
||||
const previousNodeOptions = process.env.NODE_OPTIONS;
|
||||
delete process.env.NODE_OPTIONS;
|
||||
|
||||
try {
|
||||
const moduleUrl = pathToFileURL(
|
||||
@@ -76,6 +86,8 @@ test("ServerSupervisor starts Node with IPv4-first DNS", async () => {
|
||||
} finally {
|
||||
if (previousDataDir === undefined) delete process.env.DATA_DIR;
|
||||
else process.env.DATA_DIR = previousDataDir;
|
||||
if (previousNodeOptions === undefined) delete process.env.NODE_OPTIONS;
|
||||
else process.env.NODE_OPTIONS = previousNodeOptions;
|
||||
rmSync(dataDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||