Compare commits
7 Commits
fix/codeql
...
release/v3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b39e5ecb20 | ||
|
|
a0ceccc6f0 | ||
|
|
14ca809924 | ||
|
|
c8ca024e29 | ||
|
|
613fc71e98 | ||
|
|
c472dbccc4 | ||
|
|
d82b68274c |
@@ -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, 353 LLM providers, auto-fallback.
|
||||
|
||||
| Layer | Location | Purpose |
|
||||
| ------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
|
||||
12
README.md
@@ -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 → 350 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 → 353 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. 353 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 and 350 providers. Automatic fallback keeps routing while another healthy target is available. Six pillars: resilient fallback across 350 providers · up to 95% token savings on eligible workloads · $0 to start with 90+ free tiers and 56 recurring/keyless free-forever providers · 35 CLI/agent integrations through one config · OpenAI, Claude, Gemini and Responses API compatibility at /v1 · production controls including circuit breakers, TLS stealth, MCP 110 tools, A2A, memory, guardrails, evals and 39,000+ static test declarations across 5,100+ tracked test files."/>
|
||||
<img src="./docs/diagrams/promise-pillars.svg" width="100%" alt="The Promise — One endpoint and 353 providers. Automatic fallback keeps routing while another healthy target is available. Six pillars: resilient fallback across 353 providers · up to 95% token savings on eligible workloads · $0 to start with 90+ free tiers and 56 recurring/keyless free-forever providers · 35 CLI/agent integrations through one config · OpenAI, Claude, Gemini and Responses API compatibility at /v1 · production controls including circuit breakers, TLS stealth, MCP 110 tools, A2A, memory, guardrails, evals and 39,000+ static test declarations across 5,100+ tracked test files."/>
|
||||
|
||||
<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 — a dated feature snapshot vs 9router, OpenRouter, CLIProxyAPI and LiteLLM across 13 capabilities. OmniRoute: 350 providers, 90+ free tiers 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 and 43 i18n UI locales. OmniRoute is MIT-licensed and self-hostable. Competitor capabilities and counts may change; see the linked methodology."/>
|
||||
<img src="./docs/diagrams/comparison-table.svg" width="100%" alt="What sets OmniRoute apart — a dated feature snapshot vs 9router, OpenRouter, CLIProxyAPI and LiteLLM across 13 capabilities. OmniRoute: 353 providers, 90+ free tiers 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 and 43 i18n UI locales. OmniRoute is MIT-licensed and self-hostable. Competitor capabilities and counts may change; see the linked methodology."/>
|
||||
|
||||
<sub>📊 Full methodology & per-feature detail vs 9router, OpenRouter, CLIProxyAPI & LiteLLM → [`docs/comparison/OMNIROUTE_VS_ALTERNATIVES.md`](docs/comparison/OMNIROUTE_VS_ALTERNATIVES.md)</sub>
|
||||
|
||||
@@ -559,7 +559,7 @@ the current catalog at **[radar.omniroute.online/planos](https://radar.omniroute
|
||||
- **🖼️ New endpoints** — `/v1/ocr` (Mistral OCR) and `/v1/audio/translations` (Whisper-style) round out the media surface. → [API Reference](docs/reference/API_REFERENCE.md)
|
||||
- **🎨 Image / video / audio generation** — one API for media: xAI Grok Imagine & Novita AI video, ComfyUI, Freepik, Adobe Firefly, Microsoft Designer, Segmind, EdgeTTS. → [API Reference](docs/reference/API_REFERENCE.md)
|
||||
- **🌍 Deployment & ops** — reverse-proxy `basePath`, browser-language auto-detect, per-key device tracking, root-less MITM trust, zh-TW localization. → [Environment](docs/reference/ENVIRONMENT.md)
|
||||
- **🤝 More providers & agents** — Cursor Cloud Agent, Grok Build (xAI) with browser + OAuth login, Ollama first-class card, Claude Opus 5 & Sonnet 5, Kimi official partnership (Code/Web/Moonshot), Zed, Requesty, SenseNova, Yuanbao, Agnes AI… and a refreshed **350-provider catalog**. → [Providers](docs/reference/PROVIDER_REFERENCE.md)
|
||||
- **🤝 More providers & agents** — Cursor Cloud Agent, Grok Build (xAI) with browser + OAuth login, Ollama first-class card, Claude Opus 5 & Sonnet 5, Kimi official partnership (Code/Web/Moonshot), Zed, Requesty, SenseNova, Yuanbao, Agnes AI… and a refreshed **353-provider catalog**. → [Providers](docs/reference/PROVIDER_REFERENCE.md)
|
||||
- **📡 Routing transparency** — every response carries an `X-OmniRoute-Decision` header naming the strategy/provider/latency that served it, a new `cache-optimized` combo strategy + Auto-Combo `cacheAffinity` factor route repeat requests back to the connection holding the cached prefix, and a read-only `/v1/auto-combo/{channel}/candidates` endpoint exposes an `auto/*` channel's live candidate pool. → [Auto-Combo](docs/routing/AUTO-COMBO.md)
|
||||
- **⚡ Local performance & infra** — one-click local Redis, Cloudflare Workers / Deno Deploy relay deployers, Bifrost & Mux as supervised embedded services. → [Embedded Services](docs/frameworks/EMBEDDED-SERVICES.md)
|
||||
|
||||
@@ -642,11 +642,11 @@ of your shell history. → [CLI Integrations](docs/guides/CLI-INTEGRATIONS.md)
|
||||
|
||||
<div align="center">
|
||||
|
||||
## 🌐 350 AI Providers — 154 Catalog-Marked Free
|
||||
## 🌐 353 AI Providers — 154 Catalog-Marked Free
|
||||
|
||||
</div>
|
||||
|
||||
> **350 registered providers** across the canonical chat, media, search, local, cloud-agent and system collections, including **154 carrying `hasFree: true` discovery metadata**. The chat model registry covers **268 providers / 2,566 distinct provider-model pairs / 1,312 raw model IDs**; the separate free-budget catalog has **455 per-model rows**, **40 recurring pools** and **56 recurring/keyless free-forever providers**. These are different denominators by design; definitions and pool-deduped calculations live in the [Provider Reference](docs/reference/PROVIDER_REFERENCE.md) and [Free Tiers](docs/reference/FREE_TIERS.md).
|
||||
> **353 registered providers** across the canonical chat, media, search, local, cloud-agent and system collections, including **154 carrying `hasFree: true` discovery metadata**. The chat model registry covers **268 providers / 2,566 distinct provider-model pairs / 1,312 raw model IDs**; the separate free-budget catalog has **455 per-model rows**, **40 recurring pools** and **56 recurring/keyless free-forever providers**. These are different denominators by design; definitions and pool-deduped calculations live in the [Provider Reference](docs/reference/PROVIDER_REFERENCE.md) and [Free Tiers](docs/reference/FREE_TIERS.md).
|
||||
|
||||
<div align="center">
|
||||
|
||||
|
||||
1
changelog.d/features/11146-subscription-first-routing.md
Normal file
@@ -0,0 +1 @@
|
||||
- **feat(routing):** subscription-first auto groupings — `auto/subscription` routes only through plan-included connections with a documented hard-stop overage and fails closed on exhaustion, while `auto/thrifty` orders the pool `subscription → keyless → free → cheap → premium` and steps up one rung at a time as each is exhausted. Billing class comes from a curated per-connection catalog (uncurated is treated as metered, never plan-included), both reuse STRICT_ZERO_COST's per-connection verification, and a quota reading whose `resetAt` has passed is now refreshed regardless of TTL so routing returns to plan capacity as soon as it resets ([#11146](https://github.com/diegosouzapw/OmniRoute/pull/11146))
|
||||
1
changelog.d/features/11340-web-session-contract.md
Normal file
@@ -0,0 +1 @@
|
||||
- **feat(providers):** publish a management-authenticated versioned web-session credential contract from OmniRoute's canonical browser credential metadata ([#11340](https://github.com/diegosouzapw/OmniRoute/pull/11340)) — thanks @Zartharas
|
||||
1
changelog.d/features/11370-xquik-search-provider.md
Normal file
@@ -0,0 +1 @@
|
||||
- **feat(search):** Add Xquik X search with typed results, credential validation, REST routing, and MCP selection ([#11370](https://github.com/diegosouzapw/OmniRoute/pull/11370)) — thanks @kriptoburak
|
||||
@@ -0,0 +1 @@
|
||||
- **feat(dashboard):** surface durable exclusive managed leases in the existing Sessions view, keeping leased clients visible across idle gaps while marking connections with in-flight work as active ([#11389](https://github.com/diegosouzapw/OmniRoute/pull/11389)) — thanks @KaspaPulse
|
||||
1
changelog.d/fixes/11441-discontinued-free-models.md
Normal file
@@ -0,0 +1 @@
|
||||
- **fix(free-models):** the shared `isFreeModel()` predicate no longer reports catalog entries marked `freeType: "discontinued"` as free, so `hidePaidModels` can't route to Pollinations' seven premium models that now require a paid key ([#11441](https://github.com/diegosouzapw/OmniRoute/pull/11441))
|
||||
@@ -478,7 +478,8 @@
|
||||
"_rebaseline_2026_08_23_tip_drift_post_batch0823": "Tip drift after the 2026-08-23 merge wave: chatBodyAdmission.ts 1009->1118 (+109, gate count incl. +1) and auth.ts 3337->3344 (+7), both grown by merges already on origin/release/v3.8.50 (verified identical on the pristine tip) — not by the codex-appserver-hardening PR that carries this bump. Owner pre-authorized baseline bumps 2026-08-22.",
|
||||
"_rebaseline_2026_08_24_11355_cooldown_recovery_guards": "PR #11355 own growth: test/route.ts 1215->1237, +22 (startup crash-recovery guard: clearStaleCrashCooldowns() now parses the persisted rate_limited_until deadline and skips clearing rows still genuinely in the future, instead of clearing every non-terminal cooldown unconditionally). Cohesive fix at the existing test-route dispatch chokepoint alongside the #11141 probe builder. Covered by tests/unit/startup-stale-cooldown-recovery.test.ts + tests/unit/repro-zai-cooldown-cleared-by-connection-test.test.ts.",
|
||||
"src/lib/guardrails/videoBridgeRuntime.ts": 1009,
|
||||
"_rebaseline_2026_08_24_video_bridge_fu02_fu07_sampler": "PRs #11344 (FU-02 one-frame scene-aware determinism) + #11381 (FU-07 opt-in segment_aware structural sampling) own growth: videoBridgeRuntime.ts <1000->1009, +9 (sum of both boarded together in the same merge-batch). #11344 adds the deterministic one-frame midpoint fallback + policyEffective=uniform report at the existing scene_aware seam; #11381 adds the bounded local-only FFmpeg structural pre-analysis pass (scene/freeze/blur/exposure/SI-TI) and its budget-reallocation logic. Covered by tests/unit/guardrails/videoBridgeSampler.test.ts, tests/unit/guardrails/videoBridgeFu07StructuralSampling.test.ts, tests/integration/video-bridge-sampler-ffmpeg.test.ts. Owner pre-authorized rebaseline for legitimate PR growth (2026-08-19 directive)."
|
||||
"_rebaseline_2026_08_24_video_bridge_fu02_fu07_sampler": "PRs #11344 (FU-02 one-frame scene-aware determinism) + #11381 (FU-07 opt-in segment_aware structural sampling) own growth: videoBridgeRuntime.ts <1000->1009, +9 (sum of both boarded together in the same merge-batch). #11344 adds the deterministic one-frame midpoint fallback + policyEffective=uniform report at the existing scene_aware seam; #11381 adds the bounded local-only FFmpeg structural pre-analysis pass (scene/freeze/blur/exposure/SI-TI) and its budget-reallocation logic. Covered by tests/unit/guardrails/videoBridgeSampler.test.ts, tests/unit/guardrails/videoBridgeFu07StructuralSampling.test.ts, tests/integration/video-bridge-sampler-ffmpeg.test.ts. Owner pre-authorized rebaseline for legitimate PR growth (2026-08-19 directive).",
|
||||
"open-sse/services/autoCombo/virtualFactory.ts": 1130
|
||||
},
|
||||
"_rebaseline_base_2026_08_10_proxyfetch": "Base-red fix (green-prs sweep, issue #9985): open-sse/utils/proxyFetch.ts 1207 > cap 1000 — new proxied-TLS fetch helper introduced by the Fal reference-image work. Owner-authorized quick rebaseline to green; structural slim tracked for v3.9.0.",
|
||||
"_rebaseline_2026_07_27_v3849_train2": "Merge-train 2 (7 PRs) — owner-approved 2026-07-27. Single entry: chatCore.ts 4955->5006 (#8595, Responses multi-turn image compaction before the context hard-reject). Genuine irreducible growth at the existing compaction chokepoint in handleChatCore — the PR adds a last-resort retry against the concrete budget plus the estimateFinalInputTokens helper, both wired at the pre-existing call site rather than a new branch. Covered by tests/unit/8560-responses-image-compaction.test.ts (4 tests).",
|
||||
@@ -646,5 +647,6 @@
|
||||
"_rebaseline_2026_08_18_imageregistry_merge_train": "merge-train 2026-08-18 (owner-authorized, /merge-prs batch of 84): open-sse/config/imageRegistry.ts crossed the 1000-line new-file cap for the first time purely from combining three independent, already-legitimate provider registrations boarded in the same local merge-train — #10542 (aihorde optional-key image catalog), #10494 (gemini-web image generation), #10594 (freepik/magnific provider rename + validation). 996 on release tip -> 1019 on the train tip. Each PR individually adds a small, additive IMAGE_PROVIDERS registry entry at the existing chokepoint; none crosses the cap alone. Not modularized as part of this train's gate fix (out of scope for a merge reconciliation, not a feature change). Covered by each PR's own focused tests (aihorde-image-catalog/generation, gemini-web image tests, freepik/magnific provider tests).",
|
||||
"_rebaseline_2026_08_20_v3850_merge_train_batch1": "Merge-train batch1 (2026-08-19/20, 30 PRs boarded onto release/v3.8.50): gateways.ts 1255->1268 = PR #10722 (Token Kiosk OpenAI-compatible provider gateway catalog entry, +13 declarative lines, same god-file no-split rationale as prior gateways.ts rebaselines); chatHelpers.ts (uncapped, not previously frozen) new 1017 = PR #10797 (relay/bifrost error normalization, +23/-2, own-PR growth, existing file already near cap from accumulated chokepoint wiring per its own rebaseline history above); chatBodyAdmission.ts (uncapped) new 1005 = pre-existing base-red on the pure release tip (1004>1000 before this train boarded anything, no PR in this batch touches this file) — frozen here at its current size, not authorizing further growth. Owner-authorized rebaseline (2026-08-19 merge-prs session).",
|
||||
"_rebaseline_2026_08_20_8338_cursor_image_provider": "PR (reimplementation of #8338, @valvesss): imageRegistry.ts 1019->1033 = new cursor IMAGE_PROVIDERS entry (Cursor plan image generation via Agent CLI), +14 lines of declarative provider metadata. Same god-registry no-split rationale as prior imageRegistry/gateways rebaselines.",
|
||||
"_rebaseline_2026_08_20_imageregistry_1034": "imageRegistry.ts 1033->1034: +1 line drift between #10842 (cursor image provider, froze at 1033) and its actual merged state on release (measured 1034) — trivial rebaseline, not a new feature."
|
||||
"_rebaseline_2026_08_20_imageregistry_1034": "imageRegistry.ts 1033->1034: +1 line drift between #10842 (cursor image provider, froze at 1033) and its actual merged state on release (measured 1034) — trivial rebaseline, not a new feature.",
|
||||
"_rebaseline_2026_08_25_11146_subscription_first_auto": "PR #11146 (@yourspraveen, subscription-first auto groupings auto/subscription+auto/thrifty): open-sse/services/autoCombo/virtualFactory.ts is a NEW file in this PR landing at 1128 lines (+2 margin) — two opt-in flat auto ids built on the established auto/best-free pattern (connectionBillingCatalog + subscriptionLadder pure functions). Frozen at merge size per owner-authorized rebaseline directive (2026-08-19, merge-batch Step 4); no further growth without split rationale."
|
||||
}
|
||||
|
||||
@@ -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 85 top-level commands: 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 (353 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 85 top-level commands: 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"/>
|
||||
@@ -6,7 +6,7 @@
|
||||
<path d="M 0 34 L 1200 34" stroke="#ffffff" stroke-opacity="0.08" stroke-width="1"/>
|
||||
<circle cx="24" cy="17" r="6" fill="#ff5f56"/><circle cx="46" cy="17" r="6" fill="#ffbd2e"/><circle cx="68" cy="17" r="6" fill="#27c93f"/>
|
||||
<text x="600" y="22" text-anchor="middle" font-family="Consolas, 'Courier New', monospace" font-size="13" fill="#71717a">omniroute — 85 top-level commands</text>
|
||||
<g font-family="Consolas, 'Courier New', monospace" font-size="17"><animate attributeName="opacity" values="1;0;0" keyTimes="0;0.006;1" dur="18s" repeatCount="indefinite"/><text x="64" y="66" fill="#F7F6FC">omniroute providers list</text><text x="40" y="100" font-weight="700" fill="#38bdf8">OmniRoute Providers</text><text x="40" y="128" fill="#a1a1aa">1f3a9c2e  anthropic   Claude Max 20x    <tspan fill='#22c55e'>active</tspan></text><text x="40" y="154" fill="#a1a1aa">8c2d5b1a  codex       Codex Pro (team)  <tspan fill='#22c55e'>active</tspan></text><text x="40" y="180" fill="#a1a1aa">f4e0a97b  glm         GLM Coding Plan   <tspan fill='#22c55e'>active</tspan></text><text x="40" y="206" fill="#a1a1aa">03bd6e5f  kimi        Kimi K2 free      <tspan fill='#22c55e'>active</tspan></text><text x="40" y="232" fill="#71717a">… 346 more providers</text></g><g opacity="1" font-family="Consolas, 'Courier New', monospace" font-size="17">
|
||||
<g font-family="Consolas, 'Courier New', monospace" font-size="17"><animate attributeName="opacity" values="1;0;0" keyTimes="0;0.006;1" dur="18s" repeatCount="indefinite"/><text x="64" y="66" fill="#F7F6FC">omniroute providers list</text><text x="40" y="100" font-weight="700" fill="#38bdf8">OmniRoute Providers</text><text x="40" y="128" fill="#a1a1aa">1f3a9c2e  anthropic   Claude Max 20x    <tspan fill='#22c55e'>active</tspan></text><text x="40" y="154" fill="#a1a1aa">8c2d5b1a  codex       Codex Pro (team)  <tspan fill='#22c55e'>active</tspan></text><text x="40" y="180" fill="#a1a1aa">f4e0a97b  glm         GLM Coding Plan   <tspan fill='#22c55e'>active</tspan></text><text x="40" y="206" fill="#a1a1aa">03bd6e5f  kimi        Kimi K2 free      <tspan fill='#22c55e'>active</tspan></text><text x="40" y="232" fill="#71717a">… 347 more providers</text></g><g opacity="1" font-family="Consolas, 'Courier New', monospace" font-size="17">
|
||||
<animate attributeName="opacity" values="1;1;0;0" keyTimes="0;0.315;0.33;1" dur="18s" repeatCount="indefinite"/>
|
||||
<text x="40" y="66" fill="#22c55e">$</text>
|
||||
<g clip-path="url(#tw0)"><text x="64" y="66" fill="#F7F6FC">omniroute providers list</text></g>
|
||||
@@ -14,7 +14,7 @@
|
||||
<animate attributeName="x" calcMode="discrete" values="64;95;125;156;186;217;248;278;309;309" keyTimes="0.000;0.012;0.018;0.024;0.030;0.036;0.042;0.048;0.054;1" dur="18s" repeatCount="indefinite"/>
|
||||
<animate attributeName="opacity" values="0;0;1;0.2;1;0.2;1;0;0" keyTimes="0;0.011;0.012;0.022;0.032;0.042;0.052;0.074;1" dur="18s" repeatCount="indefinite"/>
|
||||
</rect>
|
||||
<text x="40" y="100" font-weight="700" fill="#38bdf8" opacity="0"><animate attributeName="opacity" calcMode="discrete" values="0;0;1" keyTimes="0;0.045;0.047" dur="18s" repeatCount="indefinite"/>OmniRoute Providers</text><text x="40" y="128" fill="#a1a1aa" opacity="0"><animate attributeName="opacity" calcMode="discrete" values="0;0;1" keyTimes="0;0.053;0.055" dur="18s" repeatCount="indefinite"/>1f3a9c2e  anthropic   Claude Max 20x    <tspan fill='#22c55e'>active</tspan></text><text x="40" y="154" fill="#a1a1aa" opacity="0"><animate attributeName="opacity" calcMode="discrete" values="0;0;1" keyTimes="0;0.061;0.063" dur="18s" repeatCount="indefinite"/>8c2d5b1a  codex       Codex Pro (team)  <tspan fill='#22c55e'>active</tspan></text><text x="40" y="180" fill="#a1a1aa" opacity="0"><animate attributeName="opacity" calcMode="discrete" values="0;0;1" keyTimes="0;0.069;0.07100000000000001" dur="18s" repeatCount="indefinite"/>f4e0a97b  glm         GLM Coding Plan   <tspan fill='#22c55e'>active</tspan></text><text x="40" y="206" fill="#a1a1aa" opacity="0"><animate attributeName="opacity" calcMode="discrete" values="0;0;1" keyTimes="0;0.077;0.079" dur="18s" repeatCount="indefinite"/>03bd6e5f  kimi        Kimi K2 free      <tspan fill='#22c55e'>active</tspan></text><text x="40" y="232" fill="#71717a" opacity="0"><animate attributeName="opacity" calcMode="discrete" values="0;0;1" keyTimes="0;0.085;0.08700000000000001" dur="18s" repeatCount="indefinite"/>… 346 more providers</text>
|
||||
<text x="40" y="100" font-weight="700" fill="#38bdf8" opacity="0"><animate attributeName="opacity" calcMode="discrete" values="0;0;1" keyTimes="0;0.045;0.047" dur="18s" repeatCount="indefinite"/>OmniRoute Providers</text><text x="40" y="128" fill="#a1a1aa" opacity="0"><animate attributeName="opacity" calcMode="discrete" values="0;0;1" keyTimes="0;0.053;0.055" dur="18s" repeatCount="indefinite"/>1f3a9c2e  anthropic   Claude Max 20x    <tspan fill='#22c55e'>active</tspan></text><text x="40" y="154" fill="#a1a1aa" opacity="0"><animate attributeName="opacity" calcMode="discrete" values="0;0;1" keyTimes="0;0.061;0.063" dur="18s" repeatCount="indefinite"/>8c2d5b1a  codex       Codex Pro (team)  <tspan fill='#22c55e'>active</tspan></text><text x="40" y="180" fill="#a1a1aa" opacity="0"><animate attributeName="opacity" calcMode="discrete" values="0;0;1" keyTimes="0;0.069;0.07100000000000001" dur="18s" repeatCount="indefinite"/>f4e0a97b  glm         GLM Coding Plan   <tspan fill='#22c55e'>active</tspan></text><text x="40" y="206" fill="#a1a1aa" opacity="0"><animate attributeName="opacity" calcMode="discrete" values="0;0;1" keyTimes="0;0.077;0.079" dur="18s" repeatCount="indefinite"/>03bd6e5f  kimi        Kimi K2 free      <tspan fill='#22c55e'>active</tspan></text><text x="40" y="232" fill="#71717a" opacity="0"><animate attributeName="opacity" calcMode="discrete" values="0;0;1" keyTimes="0;0.085;0.08700000000000001" dur="18s" repeatCount="indefinite"/>… 347 more providers</text>
|
||||
</g><g opacity="0" font-family="Consolas, 'Courier New', monospace" font-size="17">
|
||||
<animate attributeName="opacity" values="0;0;1;1;0;0" keyTimes="0;0.333;0.34800000000000003;0.648;0.663;1" dur="18s" repeatCount="indefinite"/>
|
||||
<text x="40" y="66" fill="#22c55e">$</text>
|
||||
|
||||
|
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: 353 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 and 350 providers. Six pillars. Resilient fallback: automatic routing continues while another healthy target is available. Save up to 95 percent of eligible tokens: RTK plus Caveman stacked compression averages about 89 percent on tool-heavy sessions. Zero dollars to start: 90+ providers with a free tier and 56 recurring or keyless free-forever providers. Every tool works: 35 CLI and agent integration records, including Claude Code, Codex, Cursor, Cline, Copilot and Antigravity, through one config. One endpoint: OpenAI, Claude, Gemini and Responses API translation at /v1. Production controls: circuit breakers, TLS stealth, MCP with 110 tools, A2A, memory, guardrails, evals, and 39,000+ static test declarations across 5,100+ tracked test files.">
|
||||
<svg viewBox="0 0 1200 540" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="The OmniRoute promise: one endpoint and 353 providers. Six pillars. Resilient fallback: automatic routing continues while another healthy target is available. Save up to 95 percent of eligible tokens: RTK plus Caveman stacked compression averages about 89 percent on tool-heavy sessions. Zero dollars to start: 90+ providers with a free tier and 56 recurring or keyless free-forever providers. Every tool works: 35 CLI and agent integration records, including Claude Code, Codex, Cursor, Cline, Copilot and Antigravity, through one config. One endpoint: OpenAI, Claude, Gemini and Responses API translation at /v1. Production controls: circuit breakers, TLS stealth, MCP with 110 tools, A2A, memory, guardrails, evals, and 39,000+ static test declarations across 5,100+ tracked test files.">
|
||||
<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">353 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 353 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 while a healthy target remains.</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 353 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: 353 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">353 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 |
@@ -66,22 +66,22 @@ Cursor, Cline, and compatible MCP client setup.
|
||||
|
||||
## Essential Tools (13) — Phase 1
|
||||
|
||||
| Tool | Scopes | Description |
|
||||
| :------------------------------ | :-------------------- | :------------------------------------------------------------ |
|
||||
| `omniroute_get_health` | `read:health` | Uptime, memory, circuit breakers, rate limits, cache stats |
|
||||
| `omniroute_list_combos` | `read:combos` | All configured combos with strategies (optional metrics) |
|
||||
| `omniroute_get_combo_metrics` | `read:combos` | Performance metrics for a specific combo |
|
||||
| `omniroute_switch_combo` | `write:combos` | Activate or deactivate a combo |
|
||||
| `omniroute_create_combo` | `write:combos` | Create a validated combo through the existing combo API |
|
||||
| `omniroute_check_quota` | `read:quota` | Quota used/total, percent remaining, reset time, token health |
|
||||
| `omniroute_route_request` | `execute:completions` | Send a chat completion through OmniRoute routing |
|
||||
| `omniroute_cost_report` | `read:usage` | Cost report by period (session/day/week/month) |
|
||||
| `omniroute_list_models_catalog` | `read:models` | Full model catalog with capabilities, status, pricing |
|
||||
| `omniroute_radar_catalog` | `read:radar` | Local signed Radar catalog; optional provider/family filters |
|
||||
| `omniroute_tool_search` | `read:tools` | Discover tools from the registered MCP catalog |
|
||||
| `omniroute_web_search` | `execute:search` | Web search through the configured search providers. Not X/Twitter. |
|
||||
| `omniroute_x_search` | `execute:search` | Search X (Twitter) through SuperGrok / xAI server-side `x_search`. Requires `xai-oauth` or an xAI API key. Not the X Developer Platform MCP. |
|
||||
| `omniroute_web_fetch` | `execute:search` | Fetch web content through the configured fetch providers |
|
||||
| Tool | Scopes | Description |
|
||||
| :------------------------------ | :-------------------- | :----------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `omniroute_get_health` | `read:health` | Uptime, memory, circuit breakers, rate limits, cache stats |
|
||||
| `omniroute_list_combos` | `read:combos` | All configured combos with strategies (optional metrics) |
|
||||
| `omniroute_get_combo_metrics` | `read:combos` | Performance metrics for a specific combo |
|
||||
| `omniroute_switch_combo` | `write:combos` | Activate or deactivate a combo |
|
||||
| `omniroute_create_combo` | `write:combos` | Create a validated combo through the existing combo API |
|
||||
| `omniroute_check_quota` | `read:quota` | Quota used/total, percent remaining, reset time, token health |
|
||||
| `omniroute_route_request` | `execute:completions` | Send a chat completion through OmniRoute routing |
|
||||
| `omniroute_cost_report` | `read:usage` | Cost report by period (session/day/week/month) |
|
||||
| `omniroute_list_models_catalog` | `read:models` | Full model catalog with capabilities, status, pricing |
|
||||
| `omniroute_radar_catalog` | `read:radar` | Local signed Radar catalog; optional provider/family filters |
|
||||
| `omniroute_tool_search` | `read:tools` | Discover tools from the registered MCP catalog |
|
||||
| `omniroute_web_search` | `execute:search` | Web search through the configured search providers. Not X/Twitter. |
|
||||
| `omniroute_x_search` | `execute:search` | Search X through xAI/SuperGrok, or choose `xquik-search` for Xquik API results. Requires credentials for the selected backend. |
|
||||
| `omniroute_web_fetch` | `execute:search` | Fetch web content through the configured fetch providers |
|
||||
|
||||
## Advanced Tools (11) — Phase 2
|
||||
|
||||
|
||||
@@ -24,8 +24,11 @@ it expires.
|
||||
| Antigravity / Devin Desktop | Built-in quotas |
|
||||
|
||||
**Strategy**: route here first for every request that fits the model's
|
||||
strengths. Quota tracker monitors approaching reset; combo strategies
|
||||
`reset-aware` and `subscription` prioritize accordingly.
|
||||
strengths. The quota tracker monitors approaching resets, and the `reset-aware`
|
||||
combo strategy prioritizes accordingly. To route Tier 1 first and only step out
|
||||
to paid tiers as quota runs out, use the `auto/thrifty` id — or `auto/subscription`
|
||||
to stay on plan-included capacity and fail closed instead. See
|
||||
[Subscription-first routing](../routing/SUBSCRIPTION_LADDER.md).
|
||||
|
||||
## Tier 2 — Cheap
|
||||
|
||||
|
||||
@@ -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 353 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
|
||||
- **353 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
|
||||
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
## v3.8.x Highlights
|
||||
|
||||
- **350-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **353-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
|
||||
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
|
||||
- **MCP server expanded to 110 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
|
||||
|
||||
@@ -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 353 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
|
||||
- **353 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
|
||||
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
## v3.8.x Highlights
|
||||
|
||||
- **350-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **353-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
|
||||
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
|
||||
- **MCP server expanded to 110 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
|
||||
|
||||
@@ -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 353 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
|
||||
- **353 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
|
||||
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
## v3.8.x Highlights
|
||||
|
||||
- **350-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **353-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
|
||||
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
|
||||
- **MCP server expanded to 110 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
|
||||
|
||||
@@ -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 353 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
|
||||
- **353 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
|
||||
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
## v3.8.x Highlights
|
||||
|
||||
- **350-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **353-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
|
||||
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
|
||||
- **MCP server expanded to 110 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
|
||||
|
||||
@@ -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 353 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
|
||||
- **353 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
|
||||
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
## v3.8.x Highlights
|
||||
|
||||
- **350-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **353-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
|
||||
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
|
||||
- **MCP server expanded to 110 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
|
||||
|
||||
@@ -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 353 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
|
||||
- **353 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
|
||||
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
## v3.8.x Highlights
|
||||
|
||||
- **350-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **353-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
|
||||
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
|
||||
- **MCP server expanded to 110 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
|
||||
|
||||
@@ -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 353 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
|
||||
- **353 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
|
||||
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
## v3.8.x Highlights
|
||||
|
||||
- **350-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **353-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
|
||||
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
|
||||
- **MCP server expanded to 110 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
|
||||
|
||||
@@ -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 353 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
|
||||
- **353 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
|
||||
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
## v3.8.x Highlights
|
||||
|
||||
- **350-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **353-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
|
||||
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
|
||||
- **MCP server expanded to 110 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
|
||||
|
||||
@@ -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 353 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
|
||||
- **353 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
|
||||
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
## v3.8.x Highlights
|
||||
|
||||
- **350-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **353-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
|
||||
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
|
||||
- **MCP server expanded to 110 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
|
||||
|
||||
@@ -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 353 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
|
||||
- **353 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
|
||||
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
## v3.8.x Highlights
|
||||
|
||||
- **350-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **353-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
|
||||
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
|
||||
- **MCP server expanded to 110 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
|
||||
|
||||
@@ -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 353 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
|
||||
- **353 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
|
||||
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
## v3.8.x Highlights
|
||||
|
||||
- **350-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **353-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
|
||||
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
|
||||
- **MCP server expanded to 110 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
|
||||
|
||||
@@ -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 353 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
|
||||
- **353 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
|
||||
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
## v3.8.x Highlights
|
||||
|
||||
- **350-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **353-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
|
||||
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
|
||||
- **MCP server expanded to 110 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
|
||||
|
||||
@@ -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 353 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
|
||||
- **353 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
|
||||
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
## v3.8.x Highlights
|
||||
|
||||
- **350-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **353-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
|
||||
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
|
||||
- **MCP server expanded to 110 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
|
||||
|
||||
@@ -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 353 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
|
||||
- **353 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
|
||||
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
## v3.8.x Highlights
|
||||
|
||||
- **350-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **353-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
|
||||
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
|
||||
- **MCP server expanded to 110 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
|
||||
|
||||
@@ -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 353 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
|
||||
- **353 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
|
||||
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
## v3.8.x Highlights
|
||||
|
||||
- **350-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **353-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
|
||||
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
|
||||
- **MCP server expanded to 110 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
|
||||
|
||||
@@ -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 353 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
|
||||
- **353 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
|
||||
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
## v3.8.x Highlights
|
||||
|
||||
- **350-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **353-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
|
||||
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
|
||||
- **MCP server expanded to 110 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
|
||||
|
||||
@@ -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 353 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
|
||||
- **353 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
|
||||
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
## v3.8.x Highlights
|
||||
|
||||
- **350-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **353-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
|
||||
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
|
||||
- **MCP server expanded to 110 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
|
||||
|
||||
@@ -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 353 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
|
||||
- **353 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
|
||||
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
## v3.8.x Highlights
|
||||
|
||||
- **350-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **353-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
|
||||
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
|
||||
- **MCP server expanded to 110 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
|
||||
|
||||
@@ -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 353 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
|
||||
- **353 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
|
||||
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
## v3.8.x Highlights
|
||||
|
||||
- **350-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **353-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
|
||||
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
|
||||
- **MCP server expanded to 110 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
|
||||
|
||||
@@ -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 353 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
|
||||
- **353 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
|
||||
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
## v3.8.x Highlights
|
||||
|
||||
- **350-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **353-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
|
||||
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
|
||||
- **MCP server expanded to 110 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
|
||||
|
||||
@@ -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 353 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
|
||||
- **353 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
|
||||
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
## v3.8.x Highlights
|
||||
|
||||
- **350-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **353-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
|
||||
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
|
||||
- **MCP server expanded to 110 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
|
||||
|
||||
@@ -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 353 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
|
||||
- **353 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
|
||||
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
## v3.8.x Highlights
|
||||
|
||||
- **350-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **353-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
|
||||
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
|
||||
- **MCP server expanded to 110 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
|
||||
|
||||
@@ -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 353 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
|
||||
- **353 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
|
||||
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
## v3.8.x Highlights
|
||||
|
||||
- **350-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **353-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
|
||||
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
|
||||
- **MCP server expanded to 110 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
|
||||
|
||||
@@ -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 353 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
|
||||
- **353 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
|
||||
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
## v3.8.x Highlights
|
||||
|
||||
- **350-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **353-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
|
||||
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
|
||||
- **MCP server expanded to 110 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
|
||||
|
||||
@@ -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 353 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
|
||||
- **353 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
|
||||
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
## v3.8.x Highlights
|
||||
|
||||
- **350-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **353-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
|
||||
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
|
||||
- **MCP server expanded to 110 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
|
||||
|
||||
@@ -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 353 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
|
||||
- **353 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
|
||||
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
## v3.8.x Highlights
|
||||
|
||||
- **350-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **353-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
|
||||
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
|
||||
- **MCP server expanded to 110 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
|
||||
|
||||
@@ -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 353 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
|
||||
- **353 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
|
||||
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
## v3.8.x Highlights
|
||||
|
||||
- **350-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **353-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
|
||||
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
|
||||
- **MCP server expanded to 110 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
|
||||
|
||||
@@ -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 353 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
|
||||
- **353 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
|
||||
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
## v3.8.x Highlights
|
||||
|
||||
- **350-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **353-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
|
||||
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
|
||||
- **MCP server expanded to 110 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
|
||||
|
||||
@@ -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 353 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
|
||||
- **353 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
|
||||
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
## v3.8.x Highlights
|
||||
|
||||
- **350-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **353-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
|
||||
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
|
||||
- **MCP server expanded to 110 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
|
||||
|
||||
@@ -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 353 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
|
||||
- **353 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
|
||||
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
## v3.8.x Highlights
|
||||
|
||||
- **350-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **353-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
|
||||
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
|
||||
- **MCP server expanded to 110 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
|
||||
|
||||
@@ -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 353 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
|
||||
- **353 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
|
||||
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
## v3.8.x Highlights
|
||||
|
||||
- **350-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **353-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
|
||||
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
|
||||
- **MCP server expanded to 110 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
|
||||
|
||||
@@ -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 353 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
|
||||
- **353 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
|
||||
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
## v3.8.x Highlights
|
||||
|
||||
- **350-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **353-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
|
||||
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
|
||||
- **MCP server expanded to 110 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
|
||||
|
||||
@@ -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 353 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
|
||||
- **353 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
|
||||
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
## v3.8.x Highlights
|
||||
|
||||
- **350-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **353-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
|
||||
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
|
||||
- **MCP server expanded to 110 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
|
||||
|
||||
@@ -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 353 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
|
||||
- **353 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
|
||||
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
## v3.8.x Highlights
|
||||
|
||||
- **350-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **353-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
|
||||
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
|
||||
- **MCP server expanded to 110 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
|
||||
|
||||
@@ -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 353 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
|
||||
- **353 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
|
||||
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
## v3.8.x Highlights
|
||||
|
||||
- **350-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **353-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
|
||||
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
|
||||
- **MCP server expanded to 110 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
|
||||
|
||||
@@ -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 353 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
|
||||
- **353 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
|
||||
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
## v3.8.x Highlights
|
||||
|
||||
- **350-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **353-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
|
||||
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
|
||||
- **MCP server expanded to 110 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
|
||||
|
||||
@@ -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 353 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
|
||||
- **353 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
|
||||
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
## v3.8.x Highlights
|
||||
|
||||
- **350-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **353-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
|
||||
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
|
||||
- **MCP server expanded to 110 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
|
||||
|
||||
@@ -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 353 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
|
||||
- **353 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
|
||||
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
## v3.8.x Highlights
|
||||
|
||||
- **350-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **353-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
|
||||
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
|
||||
- **MCP server expanded to 110 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
|
||||
|
||||
@@ -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 353 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
|
||||
- **353 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
|
||||
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
## v3.8.x Highlights
|
||||
|
||||
- **350-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **353-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
|
||||
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
|
||||
- **MCP server expanded to 110 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
|
||||
|
||||
@@ -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 353 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
|
||||
- **353 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
|
||||
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
## v3.8.x Highlights
|
||||
|
||||
- **350-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **353-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
|
||||
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
|
||||
- **MCP server expanded to 110 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
|
||||
|
||||
@@ -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 353 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
|
||||
- **353 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
|
||||
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
## v3.8.x Highlights
|
||||
|
||||
- **350-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **353-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
|
||||
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
|
||||
- **MCP server expanded to 110 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
|
||||
|
||||
@@ -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 353 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
|
||||
- **353 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
|
||||
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
## v3.8.x Highlights
|
||||
|
||||
- **350-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **353-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
|
||||
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
|
||||
- **MCP server expanded to 110 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
|
||||
|
||||
@@ -43,6 +43,8 @@ tags:
|
||||
description: Content moderation
|
||||
- name: Rerank
|
||||
description: Document reranking
|
||||
- name: Search
|
||||
description: Unified web, news, and X search
|
||||
- name: Models
|
||||
description: Available model listing
|
||||
- name: Providers
|
||||
@@ -1153,6 +1155,199 @@ paths:
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
# ─── Proxy Endpoints ──────────────────────────────────────────
|
||||
|
||||
/api/v1/search:
|
||||
get:
|
||||
tags: [Search]
|
||||
summary: List search providers
|
||||
description: Lists configured search providers and their supported search types.
|
||||
responses:
|
||||
"200":
|
||||
description: Search provider catalog
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required: [object, data]
|
||||
properties:
|
||||
object:
|
||||
type: string
|
||||
const: list
|
||||
data:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
required: [id, object, created, name, search_types]
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
object:
|
||||
type: string
|
||||
const: search_provider
|
||||
created:
|
||||
type: integer
|
||||
name:
|
||||
type: string
|
||||
search_types:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
enum: [web, news, x]
|
||||
post:
|
||||
tags: [Search]
|
||||
summary: Run a unified search
|
||||
description: >-
|
||||
Searches the web, news, or X through a configured provider. Set `provider`
|
||||
to `xquik-search` to use Xquik for X search. The aliases `xquik` and
|
||||
`xquik_search` resolve to the same provider.
|
||||
security:
|
||||
- BearerAuth: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required: [query]
|
||||
properties:
|
||||
query:
|
||||
type: string
|
||||
minLength: 1
|
||||
maxLength: 500
|
||||
provider:
|
||||
type: string
|
||||
minLength: 1
|
||||
description: A search provider id or registered alias.
|
||||
examples: [xquik-search]
|
||||
max_results:
|
||||
type: integer
|
||||
minimum: 1
|
||||
maximum: 100
|
||||
default: 5
|
||||
search_type:
|
||||
type: string
|
||||
enum: [web, news, x]
|
||||
default: web
|
||||
offset:
|
||||
type: integer
|
||||
minimum: 0
|
||||
default: 0
|
||||
country:
|
||||
type: string
|
||||
maxLength: 2
|
||||
language:
|
||||
type: string
|
||||
minLength: 2
|
||||
maxLength: 5
|
||||
time_range:
|
||||
type: string
|
||||
enum: [any, hour, day, week, month, year]
|
||||
content:
|
||||
type: object
|
||||
properties:
|
||||
snippet: { type: boolean, default: true }
|
||||
full_page: { type: boolean, default: false }
|
||||
format: { type: string, enum: [text, markdown], default: text }
|
||||
max_characters: { type: integer, minimum: 100, maximum: 100000 }
|
||||
filters:
|
||||
type: object
|
||||
properties:
|
||||
include_domains:
|
||||
type: array
|
||||
maxItems: 20
|
||||
items: { type: string, maxLength: 253 }
|
||||
exclude_domains:
|
||||
type: array
|
||||
maxItems: 20
|
||||
items: { type: string, maxLength: 253 }
|
||||
safe_search: { type: string, enum: [off, moderate, strict] }
|
||||
provider_options:
|
||||
type: object
|
||||
additionalProperties: true
|
||||
strict_filters:
|
||||
type: boolean
|
||||
default: false
|
||||
additionalProperties: true
|
||||
responses:
|
||||
"200":
|
||||
description: Normalized search results
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required: [id, provider, query, results, answer, usage, metrics, errors, cached]
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
pattern: ^search-
|
||||
provider:
|
||||
type: string
|
||||
query:
|
||||
type: string
|
||||
cached:
|
||||
type: boolean
|
||||
results:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
required: [title, url, snippet, position, citation]
|
||||
properties:
|
||||
title: { type: string }
|
||||
url: { type: string, format: uri }
|
||||
display_url: { type: string }
|
||||
snippet: { type: string }
|
||||
position: { type: integer, minimum: 1 }
|
||||
score:
|
||||
type: [number, "null"]
|
||||
minimum: 0
|
||||
maximum: 1
|
||||
published_at: { type: [string, "null"] }
|
||||
favicon_url: { type: [string, "null"], format: uri }
|
||||
citation:
|
||||
type: object
|
||||
required: [provider, retrieved_at, rank]
|
||||
properties:
|
||||
provider: { type: string }
|
||||
retrieved_at: { type: string, format: date-time }
|
||||
rank: { type: integer, minimum: 1 }
|
||||
answer:
|
||||
type: [object, "null"]
|
||||
usage:
|
||||
type: object
|
||||
required: [queries_used, search_cost_usd]
|
||||
properties:
|
||||
queries_used: { type: integer, minimum: 0 }
|
||||
search_cost_usd: { type: number, minimum: 0 }
|
||||
llm_tokens: { type: integer, minimum: 0 }
|
||||
metrics:
|
||||
type: object
|
||||
required: [response_time_ms, upstream_latency_ms, total_results_available]
|
||||
properties:
|
||||
response_time_ms: { type: number, minimum: 0 }
|
||||
upstream_latency_ms: { type: number, minimum: 0 }
|
||||
gateway_latency_ms: { type: number, minimum: 0 }
|
||||
total_results_available: { type: [integer, "null"], minimum: 0 }
|
||||
errors:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
required: [provider, code, message]
|
||||
properties:
|
||||
provider: { type: string }
|
||||
code: { type: string }
|
||||
message: { type: string }
|
||||
"400":
|
||||
description: Invalid request, provider, credentials, or search type
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
"403":
|
||||
description: Search provider blocked by API key or security policy
|
||||
"429":
|
||||
description: Every eligible provider credential is rate limited
|
||||
"500":
|
||||
$ref: "#/components/responses/InternalError"
|
||||
"502":
|
||||
description: Search provider failed
|
||||
|
||||
/api/v1/chat/completions:
|
||||
post:
|
||||
tags: [Chat]
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
---
|
||||
title: "Provider Reference"
|
||||
version: 3.8.50
|
||||
lastUpdated: 2026-08-23
|
||||
version: 3.8.51
|
||||
lastUpdated: 2026-08-25
|
||||
---
|
||||
|
||||
# Provider Reference
|
||||
|
||||
> **Auto-generated** from `src/shared/constants/providers.ts` — do not edit by hand.
|
||||
> Regenerate with: `npm run gen:provider-reference`
|
||||
> **Last generated:** 2026-08-23
|
||||
> **Last generated:** 2026-08-25
|
||||
|
||||
Total providers: **350**. See category breakdown below.
|
||||
Total providers: **353**. See category breakdown below.
|
||||
|
||||
## Categories
|
||||
|
||||
@@ -122,7 +122,7 @@ Use the dashboard at `/dashboard/providers` to enable, configure, and test each
|
||||
| `zai-web` | `zw` | Z.ai Web | Web cookie | [link](https://chat.z.ai) | Copy the "token" value from chat.z.ai → DevTools → Application → Local Storage. Do not copy cookies; OmniRoute handles the per-request CAPTCHA through its browser transport. | — |
|
||||
| `zenmux-free` | `zmf` | ZenMux Free (Web) | Web cookie | [link](https://zenmux.ai) | Login at zenmux.ai, then export all cookies using EditThisCookie or Cookie-Editor and paste the full Cookie header string here. Refresh every ~30 days. | — |
|
||||
|
||||
## API Key Providers (paid / paid-with-free-credits) (231)
|
||||
## API Key Providers (paid / paid-with-free-credits) (233)
|
||||
|
||||
| ID | Alias | Name | Tags | Website | Notes |
|
||||
|----|-------|------|------|---------|-------|
|
||||
@@ -342,6 +342,8 @@ Use the dashboard at `/dashboard/providers` to enable, configure, and test each
|
||||
| `vertex-partner` | `vp` | Vertex AI Partners | API key, enterprise | [link](https://cloud.google.com/vertex-ai) | Provide the same Service Account JSON used for Vertex AI partner models. |
|
||||
| `void-ai` | `void-ai` | Void AI | API key, aggregator | [link](https://voidai.app) | The public model catalog marks some models with a free plan requirement, but access is conditional and no numeric quota is confirmed. |
|
||||
| `volcengine` | `volcengine` | Volcengine | API key | [link](https://www.volcengine.com) | — |
|
||||
| `volcengine-agent-plan` | `veap` | Volcengine Ark Agent Plan | API key | [link](https://console.volcengine.com/ark/region:cn-beijing/subscription/agent-plan) | Connect your Volcano Engine account or use an Ark Agent Plan subscription API key. |
|
||||
| `volcengine-coding-plan` | `vecp` | Volcengine Ark Coding Plan | API key | [link](https://console.volcengine.com/ark/region:cn-beijing/subscription/coding-plan) | Connect your Volcano Engine account or use an Ark Coding Plan subscription API key. |
|
||||
| `voyage-ai` | `voyage` | Voyage AI | API key, embed/rerank | [link](https://www.voyageai.com) | Bearer API key for Voyage AI embeddings and rerank APIs. |
|
||||
| `wafer` | `wafer` | Wafer AI | API key | [link](https://wafer.ai) | — |
|
||||
| `wandb` | `wandb` | Weights & Biases Inference | API key | [link](https://wandb.ai) | — |
|
||||
@@ -377,7 +379,7 @@ Use the dashboard at `/dashboard/providers` to enable, configure, and test each
|
||||
| `vllm` | `vllm` | vLLM | Local, self-hosted | [link](https://github.com/vllm-project/vllm) | API key optional. Configure the local vLLM OpenAI-compatible base URL (default: http://localhost:8000/v1). |
|
||||
| `xinference` | `xinference` | XInference | Local, self-hosted | [link](https://inference.readthedocs.io) | API key optional. Configure the local XInference OpenAI-compatible base URL (default: http://localhost:9997/v1). |
|
||||
|
||||
## Search Providers (14)
|
||||
## Search Providers (15)
|
||||
|
||||
| ID | Alias | Name | Tags | Website | Notes |
|
||||
|----|-------|------|------|---------|-------|
|
||||
@@ -394,6 +396,7 @@ Use the dashboard at `/dashboard/providers` to enable, configure, and test each
|
||||
| `serper-search` | `serper-search` | Serper Search | Search | [link](https://serper.dev) | API key from serper.dev dashboard |
|
||||
| `tavily-search` | `tavily-search` | Tavily Search | Search | [link](https://tavily.com) | API key from app.tavily.com (format: tvly-...) |
|
||||
| `x-search` | `x_search` | X Search (Grok) | Search | [link](https://docs.x.ai/developers/tools/x-search) | SuperGrok OAuth (xai-oauth) or xAI API key. This is Grok X Search, not the X Developer MCP. |
|
||||
| `xquik-search` | `xquik` | Xquik X Search | Search | [link](https://docs.xquik.com) | Xquik API key (xq_...). Search is metered per returned post; the catalog estimate uses 5 results. |
|
||||
| `youcom-search` | `youcom-search` | You.com Search | Search | [link](https://you.com/business/api/) | X-API-Key from the You.com platform dashboard |
|
||||
|
||||
## Audio-only Providers (12)
|
||||
@@ -438,7 +441,7 @@ Use the dashboard at `/dashboard/providers` to enable, configure, and test each
|
||||
|
||||
- Catalog: [`src/shared/constants/providers.ts`](../../src/shared/constants/providers.ts)
|
||||
- Registry (per-model details): [`open-sse/config/providerRegistry.ts`](../../open-sse/config/providerRegistry.ts)
|
||||
- Executors: [`open-sse/executors/`](../../open-sse/executors/) (108 implementations)
|
||||
- Executors: [`open-sse/executors/`](../../open-sse/executors/) (109 implementations)
|
||||
- Translators: [`open-sse/translator/`](../../open-sse/translator/)
|
||||
|
||||
## See Also
|
||||
|
||||
182
docs/routing/SUBSCRIPTION_LADDER.md
Normal file
@@ -0,0 +1,182 @@
|
||||
---
|
||||
title: "Subscription-first routing"
|
||||
version: 3.8.50
|
||||
lastUpdated: 2026-08-22
|
||||
---
|
||||
|
||||
# Subscription-first routing
|
||||
|
||||
> Two new `auto/*` ids — `auto/subscription` and `auto/thrifty`. Both are opt-in by being
|
||||
> requested: nothing routes through them unless a caller asks for the id by name, and no
|
||||
> existing pool, strategy, or default changes.
|
||||
|
||||
## Why this exists
|
||||
|
||||
OmniRoute already answers two cost questions, and neither is the one most operators ask.
|
||||
|
||||
| Existing mechanism | Answers |
|
||||
| -------------------------------------------------------- | ----------------------------------- |
|
||||
| `hidePaidModels` (`autoCombo/paidModelFilter.ts`) | "is this model catalogued free?" |
|
||||
| `freeAccessPolicy: "strict"` (`strictZeroCostFilter.ts`) | "can this connection ever bill me?" |
|
||||
| `quotaPreflight` (`combo/quotaExhaustionCutoff.ts`) | "is this connection near its wall?" |
|
||||
| `budgetCap` / `budgetFallback` (`autoCombo/engine.ts`) | "cap spend, degrade to cheapest" |
|
||||
|
||||
Every free-only mechanism **fails closed** — an exhausted free pool is an empty pool, never a
|
||||
step up to a paid option — and every paid-side mechanism is tier-agnostic. Neither answers:
|
||||
|
||||
> "Use the quota I already pay for. When it runs out, either stop, or step up one rung at a
|
||||
> time through the cheapest paid options — and come back the moment it resets."
|
||||
|
||||
## Billing is a connection fact, not a model fact
|
||||
|
||||
`classifyTier()` (`open-sse/services/tierResolver.ts`) keys on `(provider, model)` and returns
|
||||
`free | cheap | premium` from catalog pricing. But whether a request costs incremental money
|
||||
depends on **which connection serves it**: the same model is plan-included through a Claude Code
|
||||
OAuth connection and billed per token through an API-key connection.
|
||||
|
||||
`provider_connections.auth_type` is not a safe proxy in either direction — metered OAuth
|
||||
connections exist, and plan-included API-key connections exist (a Copilot seat token is not a
|
||||
metered API key). So billing class comes from a **curated catalog**,
|
||||
`open-sse/config/connectionBillingCatalog.ts`, hand-set from each provider's published terms —
|
||||
the same pattern `FreeModelBudget.hardStopGuaranteed` already established for free models.
|
||||
|
||||
```ts
|
||||
type ConnectionBillingClass = "subscription" | "metered" | "keyless" | "unknown";
|
||||
type ConnectionOverageBehavior = "hard-stop" | "meters-to-paid" | "unknown";
|
||||
```
|
||||
|
||||
Resolution order (`autoCombo/connectionBilling.ts`): the synthetic no-auth sentinel →
|
||||
`keyless`; a catalog entry matching provider **and** `authType`; a provider-wide entry;
|
||||
otherwise `unknown`. **Uncurated is not free** — `unknown` is consumed as `metered`
|
||||
everywhere, so a provider added tomorrow starts outside the subscription rung and has to be
|
||||
curated in deliberately.
|
||||
|
||||
## The rung model
|
||||
|
||||
Five rungs in escalation order. They differ in more than price — each has its **own**
|
||||
exhaustion signal, which is why this is not merely a sort.
|
||||
|
||||
| # | Rung | Membership | Exhausted when |
|
||||
| --- | -------------- | -------------------------------------------------- | ----------------------------- |
|
||||
| 0 | `subscription` | curated `billing: "subscription"` | quota window at/below cutoff |
|
||||
| 1 | `keyless` | the synthetic no-auth path | connection cooldown / breaker |
|
||||
| 2 | `free` | metered connection, `classifyTier() === "free"` | free allowance exhausted |
|
||||
| 3 | `cheap` | metered connection, `classifyTier() === "cheap"` | per-rung budget consumed |
|
||||
| 4 | `premium` | metered connection, `classifyTier() === "premium"` | per-rung budget consumed |
|
||||
|
||||
Rungs 0-2 exhaust on **quota**, which is observable and already tracked. Rungs 3-4 have no
|
||||
quota — a paid connection serves forever — so their only sane exhaustion signal is a per-rung
|
||||
**budget**. Without one, "escalate when cheap is exhausted" has no trigger.
|
||||
|
||||
## `auto/subscription` — fail closed
|
||||
|
||||
Pool = rung 0 only, restricted to connections whose overage is a documented `hard-stop`, each
|
||||
verified live to have quota headroom. Everything ambiguous is excluded: an uncurated provider,
|
||||
an unverifiable quota reading, a stale reading, or an overage that meters to paid.
|
||||
|
||||
An empty pool is the **intended** answer, not a defect — the caller's existing empty-pool path
|
||||
turns it into a clear error rather than a silent, billable fallback. That is the whole promise
|
||||
of the id.
|
||||
|
||||
`keyless` deliberately does **not** qualify: this grouping means "the plan I pay for", so a
|
||||
no-auth backend does not belong in it. Use `auto/thrifty` (or `auto/best-free`) for that.
|
||||
|
||||
### Connection safety
|
||||
|
||||
A candidate is not always tied to one connection — a logical candidate carries an
|
||||
`allowedConnectionIds` allowlist, and the account actually used is chosen later, at dispatch,
|
||||
by `open-sse/services/combo/autoStrategy.ts`. Both groupings therefore verify **each connection
|
||||
individually** and rewrite `allowedConnectionIds` down to exactly the surviving subset — never
|
||||
the full original list, never one arbitrarily-chosen member. Because `autoStrategy.ts` already
|
||||
enforces that array as a hard allowlist, rewriting it here makes "verified" and "actually used"
|
||||
the same set by construction. This is the same invariant, and the same reasoning, as
|
||||
[STRICT_ZERO_COST](./STRICT_ZERO_COST.md).
|
||||
|
||||
## `auto/thrifty` — escalate one rung at a time
|
||||
|
||||
Pool = all rungs, ordered by rung index, with exhausted candidates gated out. The `auto` engine
|
||||
still scores **within** the surviving pool: the ladder decides which rungs are in play, scoring
|
||||
decides which candidate wins inside them. Ordering is stable within a rung, so the scorer's own
|
||||
ranking is never reshuffled by this overlay.
|
||||
|
||||
This is an ordering + gating overlay, **not** a new dispatcher: `combo.ts`'s speculative loop
|
||||
already walks targets in order and falls through on failure, so a runtime exhaustion the
|
||||
preflight did not catch still escalates to the next rung inside the same request.
|
||||
|
||||
Where `auto/subscription` fails closed, `auto/thrifty` fails **open**: a plan-included
|
||||
connection with no usable quota reading is still tried first. Trying it costs nothing, and if
|
||||
it turns out to be exhausted the fall-through reaches the next rung anyway — whereas refusing
|
||||
to try it would send the request to a paid rung on missing telemetry, the exact outcome the
|
||||
grouping exists to avoid.
|
||||
|
||||
## Returning to the plan after a reset
|
||||
|
||||
Three independent things must expire before routing returns to rung 0. Fixing only one leaves
|
||||
the ladder stuck on paid rungs long after the plan refilled.
|
||||
|
||||
1. **The quota-state cache** — `freeAccessQuota.ts` caches per `(provider, connection)` with a
|
||||
180s TTL. A cached entry whose own `resetAt` has already passed describes a window that no
|
||||
longer exists, so it is now treated as stale **regardless of age** and forces a refresh.
|
||||
Without this, a plan that refilled at midnight keeps reading exhausted until the TTL happens
|
||||
to lapse.
|
||||
2. **The ladder's own state** — there is none, by design. Rung eligibility is recomputed from
|
||||
live quota state on every pool build; no persisted "currently on rung 3" record exists that
|
||||
could outlive a reset and wedge routing.
|
||||
3. **The connection cooldown** — the exhausting 429 sets `rateLimitedUntil` from exponential
|
||||
backoff, which for a plan connection can overshoot the real reset. `clampCooldownToReset()`
|
||||
(`subscriptionLadder.ts`) narrows a cooldown to the upstream's own reset instant and can
|
||||
never extend one. **It is implemented and tested but not yet wired**: the quota cache is
|
||||
invalidated in `src/sse/services/auth.ts` _before_ any cooldown is written, so `resetAt`
|
||||
must be captured earlier in that function — a change to the resilience hot path that
|
||||
belongs in its own reviewed PR. Until then, re-entry waits out the connection cooldown
|
||||
(which already prefers upstream `Retry-After` hints when the provider sends them).
|
||||
|
||||
### Anti-flap
|
||||
|
||||
A rung that just reset is re-admitted only above `reentryMinRemainingPercent` (default 5),
|
||||
while a connection already in play only has to stay above `exitCutoffPercent` (default 2,
|
||||
matching `quotaPreflight.defaultThresholdPercent`). The gap is the hysteresis band — without
|
||||
it, a connection hovering at the cutoff oscillates between rungs on consecutive requests.
|
||||
|
||||
## Configuration
|
||||
|
||||
Tuning only. There is deliberately **no** `enabled` flag: a toggle able to switch these off
|
||||
would leave `auto/subscription` quietly serving the full pool — paid models included — under a
|
||||
name that promises the opposite.
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"subscriptionLadder": {
|
||||
"exitCutoffPercent": 2,
|
||||
"reentryMinRemainingPercent": 5,
|
||||
"rungBudgetUsd": { "cheap": 5.0, "premium": 0 }, // 0 disables a rung outright
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Budget gating is inert until a spend resolver is wired: with no accounting available a paid
|
||||
rung is ordered but never gated. As of v3.8.51 the `rungBudgetUsd` setting is accepted by the
|
||||
schema but NOT yet enforced — treat it as reserved configuration, not an active spend cap. Rung ordering, quota-based exhaustion, and reset re-entry all
|
||||
work without it.
|
||||
|
||||
## Composition
|
||||
|
||||
`subscription` and `thrifty` are `AutoTier` values, so they compose with every category:
|
||||
`auto/coding:thrifty`, `auto/reasoning:subscription`, and so on. The two flat ids
|
||||
(`auto/subscription`, `auto/thrifty`) are advertised in `/v1/models` and the dashboard.
|
||||
|
||||
Neither id is paid-tier, so `isPaidTierAutoId()` returns `false` for both and
|
||||
`auto/subscription` survives `hidePaidModels`.
|
||||
|
||||
## Where the code lives
|
||||
|
||||
| Concern | File |
|
||||
| ------------------------------- | --------------------------------------------------- |
|
||||
| Curated billing facts | `open-sse/config/connectionBillingCatalog.ts` |
|
||||
| Classifier | `open-sse/services/autoCombo/connectionBilling.ts` |
|
||||
| Rungs, both groupings, re-entry | `open-sse/services/autoCombo/subscriptionLadder.ts` |
|
||||
| Wiring into the candidate pool | `open-sse/services/autoCombo/virtualFactory.ts` |
|
||||
| Reset-aware cache staleness | `open-sse/services/autoCombo/freeAccessQuota.ts` |
|
||||
| Tier surface | `open-sse/services/autoCombo/suffixComposition.ts` |
|
||||
| Advertised ids | `open-sse/services/autoCombo/builtinCatalog.ts` |
|
||||
| Tests | `tests/unit/autoCombo/subscription-ladder.test.ts` |
|
||||
6
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 353 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
|
||||
- **353 AI providers** with automatic format translation
|
||||
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
|
||||
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
@@ -475,7 +475,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
## v3.8.x Highlights
|
||||
|
||||
- **350-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **353-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
|
||||
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
|
||||
- **MCP server expanded to 110 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
|
||||
|
||||
143
open-sse/config/connectionBillingCatalog.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
/**
|
||||
* Curated billing classification for provider CONNECTIONS.
|
||||
*
|
||||
* The economic tier resolver (`open-sse/services/tierResolver.ts`) answers
|
||||
* "how much does this MODEL cost per token?" — a fact about the catalog. It
|
||||
* cannot answer the question subscription-first routing actually needs:
|
||||
*
|
||||
* "does serving this request through THIS connection cost incremental money,
|
||||
* or is it already covered by a flat-rate plan the operator pays anyway?"
|
||||
*
|
||||
* That is a property of the credential, not the model: `claude/claude-*` is
|
||||
* plan-included through a Claude Code OAuth connection and billed per token
|
||||
* through an API-key connection — same provider, same model, opposite
|
||||
* economics.
|
||||
*
|
||||
* `provider_connections.auth_type` alone is NOT a safe proxy in either
|
||||
* direction: metered OAuth connections exist (cloud-billed OAuth), and
|
||||
* plan-included API-key connections exist (Copilot seat tokens). So this is a
|
||||
* CURATED table, hand-set from each provider's published terms — deliberately
|
||||
* the same pattern `FreeModelBudget.hardStopGuaranteed`
|
||||
* (`open-sse/config/freeModelCatalog.ts`) already established: a fact about
|
||||
* the upstream's commercial terms, never derived from `authType` and never
|
||||
* inferred from a live API response.
|
||||
*
|
||||
* Uncurated is not "free": anything absent here resolves to `unknown`, which
|
||||
* every consumer treats as `metered`. New providers therefore start OUTSIDE
|
||||
* the subscription rung and have to be curated in deliberately — the same
|
||||
* fail-safe direction STRICT_ZERO_COST uses for uncatalogued models.
|
||||
*/
|
||||
|
||||
/**
|
||||
* How a connection's upstream charges for the requests it serves.
|
||||
*
|
||||
* - `subscription` — covered by a flat-rate plan the operator already pays.
|
||||
* Consuming its quota costs nothing incremental; the plan is a sunk cost.
|
||||
* - `metered` — pay-per-token / pay-per-credit. Every request adds spend.
|
||||
* - `keyless` — no credential exists at all, so no request can be billed by
|
||||
* construction (the synthetic no-auth path).
|
||||
* - `unknown` — not curated. Consumed as `metered` everywhere.
|
||||
*/
|
||||
export type ConnectionBillingClass = "subscription" | "metered" | "keyless" | "unknown";
|
||||
|
||||
/**
|
||||
* What happens when a subscription plan's allowance runs out.
|
||||
*
|
||||
* - `hard-stop` — the upstream refuses further requests until the window
|
||||
* resets. Exhaustion cannot cost money, so such a connection is admissible
|
||||
* to the strictest "never spend a cent extra" grouping.
|
||||
* - `meters-to-paid` — the upstream keeps serving and bills the overage.
|
||||
* Perfectly usable while quota remains, never admissible to the strict
|
||||
* grouping.
|
||||
* - `unknown` — not established. Treated exactly like `meters-to-paid` by
|
||||
* every consumer; it is the conservative default for a provider whose terms
|
||||
* allow an operator to opt into usage-based billing past the plan.
|
||||
*/
|
||||
export type ConnectionOverageBehavior = "hard-stop" | "meters-to-paid" | "unknown";
|
||||
|
||||
export interface ConnectionBillingEntry {
|
||||
/** Provider id as registered in `open-sse/config/providers/registry/`. */
|
||||
provider: string;
|
||||
/**
|
||||
* Restricts the entry to connections whose `authType` matches. Omit for a
|
||||
* provider-wide entry. A matching auth-typed entry always wins over the
|
||||
* provider-wide one, so a provider offering both a plan-included OAuth login
|
||||
* and a metered API key can declare both.
|
||||
*/
|
||||
authType?: string;
|
||||
billing: ConnectionBillingClass;
|
||||
overage: ConnectionOverageBehavior;
|
||||
/** Operator-visible justification for the classification. */
|
||||
reason: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Curated entries. Conservative by design — a provider whose terms let the
|
||||
* operator enable usage-based billing past the plan is recorded as `unknown`
|
||||
* overage, not `hard-stop`, because the strict grouping's entire promise is
|
||||
* that it cannot surprise you.
|
||||
*
|
||||
* Providers already classified free by the economic tier resolver (`kiro`,
|
||||
* `qoder`, and the rest of `LEGACY_FREE_PROVIDERS` /
|
||||
* `deriveNoAuthFreeProviders()` in `open-sse/services/tierConfig.ts`) are
|
||||
* deliberately NOT listed here: they land on the ladder's `free` rung through
|
||||
* `classifyTier()` and would only be double-claimed by an entry here.
|
||||
*/
|
||||
export const CONNECTION_BILLING_CATALOG: readonly ConnectionBillingEntry[] = [
|
||||
{
|
||||
provider: "claude",
|
||||
authType: "oauth",
|
||||
billing: "subscription",
|
||||
overage: "hard-stop",
|
||||
reason:
|
||||
"Claude Code OAuth serves the operator's Anthropic Pro/Max plan windows. " +
|
||||
"Exceeding a window is refused until it resets; no per-token charge accrues.",
|
||||
},
|
||||
{
|
||||
provider: "codex",
|
||||
authType: "oauth",
|
||||
billing: "subscription",
|
||||
overage: "hard-stop",
|
||||
reason:
|
||||
"Codex OAuth serves the ChatGPT plan's included Codex quota. Exhaustion is " +
|
||||
"refused until the plan window resets rather than billed.",
|
||||
},
|
||||
{
|
||||
provider: "antigravity",
|
||||
authType: "oauth",
|
||||
billing: "subscription",
|
||||
overage: "hard-stop",
|
||||
reason:
|
||||
"Antigravity OAuth serves built-in plan quotas that stop serving once consumed; " +
|
||||
"OmniRoute already tracks their reset windows (see antigravityCredits.ts).",
|
||||
},
|
||||
{
|
||||
provider: "cursor",
|
||||
authType: "oauth",
|
||||
billing: "subscription",
|
||||
overage: "unknown",
|
||||
reason:
|
||||
"Cursor Pro includes a request allowance, but usage-based pricing past the plan " +
|
||||
"can be enabled per account and OmniRoute cannot observe that setting. Recorded " +
|
||||
"as unknown overage so the strict grouping excludes it.",
|
||||
},
|
||||
{
|
||||
provider: "copilot-web",
|
||||
authType: "apikey",
|
||||
billing: "subscription",
|
||||
overage: "unknown",
|
||||
reason:
|
||||
"GitHub Copilot is a per-seat subscription (the credential is a seat token, not a " +
|
||||
"metered API key), but additional premium requests can be billed when the account " +
|
||||
"opts in. Recorded as unknown overage.",
|
||||
},
|
||||
{
|
||||
provider: "devin-desktop",
|
||||
authType: "oauth",
|
||||
billing: "subscription",
|
||||
overage: "meters-to-paid",
|
||||
reason:
|
||||
"Devin Desktop draws on the plan's included ACUs and continues billing past them, " +
|
||||
"so it is plan-included while quota remains but never overage-safe.",
|
||||
},
|
||||
];
|
||||
@@ -69,6 +69,31 @@ export interface FreeModelTotals {
|
||||
|
||||
const RECURRING = new Set<FreeModelFreeType>(["recurring-daily", "recurring-monthly", "keyless"]);
|
||||
|
||||
/**
|
||||
* What each free-tier regime engages for "can I route here without paying?".
|
||||
* Exhaustive by construction: adding a member to `FreeModelFreeType` will not
|
||||
* compile until it is classified here. `discontinued` is the one regime a
|
||||
* provider uses to retire a free tier behind a paid key — it does NOT grant
|
||||
* free access, and the shared predicate (`isFreeModel`) must read this instead
|
||||
* of treating every catalogued id as free. `RECURRING` (above) answers a
|
||||
* different question (which regimes feed the headline token totals) and is left
|
||||
* independent on purpose — deriving it from this table would silently change
|
||||
* the homepage totals.
|
||||
*/
|
||||
const FREE_REGIME_TRAITS = {
|
||||
"recurring-daily": { grantsFreeAccess: true },
|
||||
"recurring-monthly": { grantsFreeAccess: true },
|
||||
"recurring-credit": { grantsFreeAccess: true },
|
||||
"recurring-uncapped": { grantsFreeAccess: true },
|
||||
"one-time-initial": { grantsFreeAccess: true },
|
||||
keyless: { grantsFreeAccess: true },
|
||||
discontinued: { grantsFreeAccess: false },
|
||||
} satisfies Record<FreeModelFreeType, { grantsFreeAccess: boolean }>;
|
||||
|
||||
export function grantsFreeAccess(freeType: FreeModelFreeType): boolean {
|
||||
return FREE_REGIME_TRAITS[freeType].grantsFreeAccess;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deposit-unlock boosts: a one-time small top-up that permanently raises a
|
||||
* provider's recurring free quota. Kept OUT of the steady headline and surfaced
|
||||
|
||||
@@ -330,6 +330,25 @@ export const SEARCH_PROVIDERS: Record<string, SearchProviderConfig> = {
|
||||
timeoutMs: 60_000,
|
||||
cacheTTLMs: 5 * 60 * 1000,
|
||||
},
|
||||
|
||||
// Direct X API search through Xquik. Keep it fallback-only so the existing
|
||||
// SuperGrok provider remains the default for search_type "x".
|
||||
"xquik-search": {
|
||||
id: "xquik-search",
|
||||
name: "Xquik X Search",
|
||||
baseUrl: "https://xquik.com/api/v1/x/tweets/search",
|
||||
method: "GET",
|
||||
authType: "apikey",
|
||||
authHeader: "x-api-key",
|
||||
costPerQuery: 0.00075,
|
||||
freeMonthlyQuota: 0,
|
||||
searchTypes: ["x"],
|
||||
defaultMaxResults: 5,
|
||||
maxMaxResults: 20,
|
||||
timeoutMs: 15_000,
|
||||
cacheTTLMs: 5 * 60 * 1000,
|
||||
fallbackOnly: true,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -377,6 +396,8 @@ export const SEARCH_PROVIDER_ALIASES: Record<string, string> = {
|
||||
c7: "context7",
|
||||
x_search: "x-search",
|
||||
x: "x-search",
|
||||
xquik: "xquik-search",
|
||||
xquik_search: "xquik-search",
|
||||
};
|
||||
|
||||
export function resolveSearchProviderId(providerId: string): string {
|
||||
|
||||
@@ -8,6 +8,7 @@ import { randomUUID } from "crypto";
|
||||
* firecrawl, google-pse-search, linkup-search, searchapi-search,
|
||||
* youcom-search, searxng-search, ollama-search, zai-search, jina-search,
|
||||
* duckduckgo-free, x-search (Grok / SuperGrok X Search — explicit or search_type "x")
|
||||
* and xquik-search (direct X API search — explicit or credentialed fallback)
|
||||
*
|
||||
* Request format:
|
||||
* {
|
||||
@@ -28,6 +29,7 @@ import * as fcSearch from "./search/firecrawlSearch.ts";
|
||||
import { type FirecrawlSearchEnvelope } from "./search/firecrawlSearch.ts";
|
||||
import { buildJinaSearchRequest, extractJinaSearchItems } from "./search/jinaSearch.ts";
|
||||
import * as xSearch from "./search/xSearch.ts";
|
||||
import * as xquikSearch from "./search/xquikSearch.ts";
|
||||
import { freeWebSearch } from "../services/freeWebSearch.ts";
|
||||
import { saveCallLog } from "@/lib/usageDb";
|
||||
import { safeOutboundFetch } from "@/shared/network/safeOutboundFetch";
|
||||
@@ -714,6 +716,7 @@ const requestBuilders: Record<string, SearchRequestBuilder> = {
|
||||
"ollama-search": buildOllamaRequest,
|
||||
"jina-search": buildJinaSearchRequest,
|
||||
"x-search": xSearch.buildXSearchRequest,
|
||||
"xquik-search": xquikSearch.buildXquikSearchRequest,
|
||||
};
|
||||
|
||||
function buildRequest(
|
||||
@@ -1290,6 +1293,7 @@ const responseNormalizers: Record<string, SearchResponseNormalizer> = {
|
||||
"ollama-search": normalizeOllamaResponse,
|
||||
"jina-search": normalizeJinaSearchResponse,
|
||||
"x-search": normalizeXSearchResponse,
|
||||
"xquik-search": (data) => xquikSearch.normalizeXquikSearchResponse(data, makeResult),
|
||||
};
|
||||
|
||||
function normalizeResponse(
|
||||
|
||||
158
open-sse/handlers/search/xquikSearch.ts
Normal file
@@ -0,0 +1,158 @@
|
||||
/** Xquik-backed X search for the unified search gateway. */
|
||||
|
||||
import { z } from "zod";
|
||||
import type { SearchProviderConfig } from "../../config/searchRegistry.ts";
|
||||
import type { SearchResult } from "../search.ts";
|
||||
|
||||
export const XQUIK_SEARCH_PROVIDER_ID = "xquik-search";
|
||||
|
||||
export interface XquikSearchParams {
|
||||
query: string;
|
||||
maxResults: number;
|
||||
token?: string;
|
||||
timeRange?: string;
|
||||
providerOptions?: Record<string, unknown>;
|
||||
providerSpecificData?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface XquikSearchHit {
|
||||
title: string;
|
||||
url: string;
|
||||
snippet: string;
|
||||
author?: string;
|
||||
publishedAt?: string;
|
||||
}
|
||||
|
||||
type MakeResult = (
|
||||
providerId: string,
|
||||
item: {
|
||||
title?: string;
|
||||
url?: string;
|
||||
snippet?: string;
|
||||
published_at?: string;
|
||||
author?: string;
|
||||
source_type?: string;
|
||||
},
|
||||
index: number,
|
||||
now: string
|
||||
) => SearchResult;
|
||||
|
||||
const X_HANDLE_RE = /^[A-Za-z0-9_]{1,15}$/;
|
||||
const TWEET_ID_RE = /^\d+$/;
|
||||
|
||||
const XquikTweetSchema = z
|
||||
.object({
|
||||
id: z.string().regex(TWEET_ID_RE),
|
||||
text: z.string(),
|
||||
createdAt: z.string().optional(),
|
||||
author: z
|
||||
.object({
|
||||
username: z.string().regex(X_HANDLE_RE),
|
||||
name: z.string().optional(),
|
||||
})
|
||||
.passthrough()
|
||||
.optional(),
|
||||
})
|
||||
.passthrough();
|
||||
|
||||
const XquikSearchEnvelopeSchema = z
|
||||
.object({
|
||||
tweets: z.array(z.unknown()).default([]),
|
||||
})
|
||||
.passthrough();
|
||||
|
||||
function getProviderSettingString(
|
||||
params: Pick<XquikSearchParams, "providerOptions" | "providerSpecificData">,
|
||||
key: string
|
||||
): string | undefined {
|
||||
const option = params.providerOptions?.[key];
|
||||
if (typeof option === "string" && option.trim()) return option.trim();
|
||||
const configured = params.providerSpecificData?.[key];
|
||||
if (typeof configured === "string" && configured.trim()) return configured.trim();
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function sinceTimeForRange(timeRange: string | undefined, now = Date.now()): string | undefined {
|
||||
const hour = 60 * 60 * 1000;
|
||||
const durations: Record<string, number> = {
|
||||
hour,
|
||||
day: 24 * hour,
|
||||
week: 7 * 24 * hour,
|
||||
month: 30 * 24 * hour,
|
||||
year: 365 * 24 * hour,
|
||||
};
|
||||
const duration = timeRange ? durations[timeRange] : undefined;
|
||||
return duration ? new Date(now - duration).toISOString() : undefined;
|
||||
}
|
||||
|
||||
export function buildXquikSearchRequest(
|
||||
config: SearchProviderConfig,
|
||||
params: XquikSearchParams
|
||||
): { url: string; init: RequestInit } {
|
||||
const queryType = getProviderSettingString(params, "queryType") === "Top" ? "Top" : "Latest";
|
||||
const query = new URLSearchParams({
|
||||
q: params.query,
|
||||
queryType,
|
||||
limit: String(params.maxResults),
|
||||
});
|
||||
const sinceTime = sinceTimeForRange(params.timeRange);
|
||||
if (sinceTime) query.set("sinceTime", sinceTime);
|
||||
|
||||
return {
|
||||
url: `${config.baseUrl.replace(/\/+$/, "")}?${query}`,
|
||||
init: {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
...(params.token ? { "x-api-key": params.token } : {}),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function extractXquikSearchHits(data: unknown, maxResults: number): XquikSearchHit[] {
|
||||
const envelope = XquikSearchEnvelopeSchema.safeParse(data);
|
||||
if (!envelope.success) return [];
|
||||
|
||||
const hits: XquikSearchHit[] = [];
|
||||
for (const value of envelope.data.tweets) {
|
||||
const parsed = XquikTweetSchema.safeParse(value);
|
||||
if (!parsed.success) continue;
|
||||
const tweet = parsed.data;
|
||||
const author = tweet.author?.username;
|
||||
hits.push({
|
||||
title: author ? `@${author}` : "X post",
|
||||
url: author
|
||||
? `https://x.com/${author}/status/${tweet.id}`
|
||||
: `https://x.com/i/status/${tweet.id}`,
|
||||
snippet: tweet.text,
|
||||
author,
|
||||
publishedAt: tweet.createdAt,
|
||||
});
|
||||
if (hits.length >= maxResults) break;
|
||||
}
|
||||
return hits;
|
||||
}
|
||||
|
||||
export function normalizeXquikSearchResponse(
|
||||
data: unknown,
|
||||
makeResult: MakeResult
|
||||
): { results: SearchResult[]; totalResults: number } {
|
||||
const now = new Date().toISOString();
|
||||
const results = extractXquikSearchHits(data, 20).map((hit, index) =>
|
||||
makeResult(
|
||||
XQUIK_SEARCH_PROVIDER_ID,
|
||||
{
|
||||
title: hit.title,
|
||||
url: hit.url,
|
||||
snippet: hit.snippet,
|
||||
published_at: hit.publishedAt,
|
||||
author: hit.author,
|
||||
source_type: "x",
|
||||
},
|
||||
index,
|
||||
now
|
||||
)
|
||||
);
|
||||
return { results, totalResults: results.length };
|
||||
}
|
||||
@@ -358,6 +358,38 @@ describe("omniroute_x_search handler (via MCP dispatch)", () => {
|
||||
expect(body.search_type).toBe("x");
|
||||
expect(body.provider).toBe("x-search");
|
||||
});
|
||||
|
||||
it("should route an explicit Xquik search through xquik-search", async () => {
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
id: "xs2",
|
||||
provider: "xquik-search",
|
||||
query: "agents sdk",
|
||||
results: [
|
||||
{
|
||||
title: "@openai",
|
||||
url: "https://x.com/openai/status/1912345678901234567",
|
||||
snippet: "Agents SDK update",
|
||||
position: 1,
|
||||
},
|
||||
],
|
||||
cached: false,
|
||||
usage: { queries_used: 1, search_cost_usd: 0.00015 },
|
||||
}),
|
||||
});
|
||||
|
||||
const result = await client.callTool({
|
||||
name: "omniroute_x_search",
|
||||
arguments: { query: "agents sdk", max_results: 5, provider: "xquik-search" },
|
||||
});
|
||||
|
||||
expect(result.isError).toBeFalsy();
|
||||
const [, options] = mockFetch.mock.calls[0];
|
||||
const body = JSON.parse(options.body as string);
|
||||
expect(body.search_type).toBe("x");
|
||||
expect(body.provider).toBe("xquik-search");
|
||||
});
|
||||
});
|
||||
|
||||
// ── omniroute_get_health: handler dispatch tests ──────────────────────────────
|
||||
|
||||
@@ -531,12 +531,17 @@ export const xSearchInput = z.object({
|
||||
.max(20)
|
||||
.default(5)
|
||||
.describe("Maximum number of X results to return"),
|
||||
provider: z
|
||||
.enum(["x-search", "xquik-search"])
|
||||
.optional()
|
||||
.default("x-search")
|
||||
.describe("X search backend: x-search uses xAI/SuperGrok; xquik-search uses Xquik"),
|
||||
});
|
||||
|
||||
export const xSearchTool: McpToolDefinition<typeof xSearchInput, typeof webSearchOutput> = {
|
||||
name: "omniroute_x_search",
|
||||
description:
|
||||
"Search X (Twitter) through OmniRoute using SuperGrok / xAI server-side x_search. Requires a connected xai-oauth (SuperGrok) or xAI API key. This is Grok X Search, not web search and not the X Developer Platform MCP.",
|
||||
"Search X (Twitter) through OmniRoute. Uses SuperGrok / xAI server-side x_search by default, or Xquik when provider is xquik-search. Requires credentials for the selected backend. This is not web search.",
|
||||
inputSchema: xSearchInput,
|
||||
outputSchema: webSearchOutput,
|
||||
scopes: ["execute:search"],
|
||||
|
||||
@@ -666,7 +666,11 @@ async function handleWebSearch(args: {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleXSearch(args: { query: string; max_results?: number }) {
|
||||
async function handleXSearch(args: {
|
||||
query: string;
|
||||
max_results?: number;
|
||||
provider?: "x-search" | "xquik-search";
|
||||
}) {
|
||||
const start = Date.now();
|
||||
try {
|
||||
const result = await omniRouteFetch("/v1/search", {
|
||||
@@ -675,7 +679,7 @@ async function handleXSearch(args: { query: string; max_results?: number }) {
|
||||
query: args.query,
|
||||
max_results: args.max_results ?? 5,
|
||||
search_type: "x",
|
||||
provider: "x-search",
|
||||
provider: args.provider ?? "x-search",
|
||||
}),
|
||||
signal: AbortSignal.timeout(120000),
|
||||
});
|
||||
|
||||
@@ -44,6 +44,12 @@ export const AUTO_TEMPLATE_VARIANTS: Record<string, AutoVariant | undefined> = {
|
||||
"auto/claude-opus": "smart",
|
||||
"auto/claude-sonnet": "coding",
|
||||
"auto/best-free": "cheap",
|
||||
// Subscription-first routing (see `subscriptionLadder.ts`). `auto/subscription`
|
||||
// maps to no weight variant on purpose: its pool is already restricted to
|
||||
// plan-included connections, so the scorer should rank them on merit rather
|
||||
// than bias toward cheap/fast within an allowance the operator already paid for.
|
||||
"auto/subscription": undefined,
|
||||
"auto/thrifty": "cheap",
|
||||
// Chaos mode — parallel dispatch to top-N stable models
|
||||
"auto/best-chaos": "chaos",
|
||||
"auto/chaos": "chaos",
|
||||
@@ -67,6 +73,18 @@ export const AUTO_SUFFIX_VARIANTS: string[] = [
|
||||
"auto/multimodal",
|
||||
];
|
||||
|
||||
/**
|
||||
* Flat `auto/*` ids that carry a tier overlay even though they are not written
|
||||
* in `<category>:<tier>` form. `auto/best-free` established the pattern; the
|
||||
* two subscription-first ids reuse it so a caller can ask for the behavior
|
||||
* without also having to pick a category.
|
||||
*/
|
||||
export const FLAT_TIER_OVERLAY_IDS: Record<string, AutoTier> = {
|
||||
"auto/best-free": "free",
|
||||
"auto/subscription": "subscription",
|
||||
"auto/thrifty": "thrifty",
|
||||
};
|
||||
|
||||
type ResolvedAutoVariant =
|
||||
{ recognized: true; variant: AutoVariant | undefined } | { recognized: false };
|
||||
|
||||
@@ -199,8 +217,9 @@ export async function createBuiltinAutoCombo(
|
||||
}
|
||||
|
||||
if ("variant" in spec && spec.variant !== undefined) {
|
||||
const overlayTier = FLAT_TIER_OVERLAY_IDS[modelStr];
|
||||
const virtualCombo = await materialize(spec.variant, {
|
||||
...(modelStr === "auto/best-free" ? { tier: "free" as const } : {}),
|
||||
...(overlayTier ? { tier: overlayTier } : {}),
|
||||
});
|
||||
virtualCombo.name = modelStr;
|
||||
virtualCombo.id = modelStr;
|
||||
@@ -211,7 +230,11 @@ export async function createBuiltinAutoCombo(
|
||||
// auto/best-chat, auto/pro-chat) still materialize via the default
|
||||
// (unconstrained) virtual combo rather than throwing "Unknown built-in".
|
||||
if (Object.prototype.hasOwnProperty.call(AUTO_TEMPLATE_VARIANTS, modelStr)) {
|
||||
const virtualCombo = await materialize(undefined);
|
||||
const overlayTier = FLAT_TIER_OVERLAY_IDS[modelStr];
|
||||
const virtualCombo = await materialize(
|
||||
undefined,
|
||||
overlayTier ? { tier: overlayTier } : undefined
|
||||
);
|
||||
virtualCombo.name = modelStr;
|
||||
virtualCombo.id = modelStr;
|
||||
return virtualCombo;
|
||||
|
||||
100
open-sse/services/autoCombo/connectionBilling.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
/**
|
||||
* Pure classifier over the curated connection-billing catalog
|
||||
* (`open-sse/config/connectionBillingCatalog.ts`).
|
||||
*
|
||||
* Kept dependency-light on purpose — the same constraint `paidModelFilter.ts`
|
||||
* and `strictZeroCostFilter.ts` state in their own headers — so subscription
|
||||
* routing is unit-testable without seeding the DB or the virtual factory. No
|
||||
* provider name appears in this file: a connection is classified purely from
|
||||
* the catalog plus the two facts the caller already has (`provider`,
|
||||
* `authType`), so curating a new provider needs no code change here.
|
||||
*/
|
||||
import {
|
||||
CONNECTION_BILLING_CATALOG,
|
||||
type ConnectionBillingClass,
|
||||
type ConnectionBillingEntry,
|
||||
type ConnectionOverageBehavior,
|
||||
} from "@omniroute/open-sse/config/connectionBillingCatalog.ts";
|
||||
import { SYNTHETIC_NOAUTH_CONNECTION_ID } from "./resilienceCandidateFilter";
|
||||
|
||||
/** The minimum a caller must know about a connection to classify it. */
|
||||
export interface BillableConnection {
|
||||
provider: string;
|
||||
/** `provider_connections.auth_type` — `oauth` / `apikey` / `cookie` / … */
|
||||
authType?: string | null;
|
||||
/** Connection id; the synthetic no-auth sentinel classifies as `keyless`. */
|
||||
connectionId?: string | null;
|
||||
}
|
||||
|
||||
export interface ConnectionBillingVerdict {
|
||||
billing: ConnectionBillingClass;
|
||||
overage: ConnectionOverageBehavior;
|
||||
reason: string;
|
||||
}
|
||||
|
||||
const UNKNOWN_VERDICT: ConnectionBillingVerdict = {
|
||||
billing: "unknown",
|
||||
overage: "unknown",
|
||||
reason: "No curated billing entry for this provider/authType — assumed metered.",
|
||||
};
|
||||
|
||||
const KEYLESS_VERDICT: ConnectionBillingVerdict = {
|
||||
billing: "keyless",
|
||||
overage: "hard-stop",
|
||||
reason:
|
||||
"Synthetic no-auth connection: no credential exists, so no request against it can be billed.",
|
||||
};
|
||||
|
||||
/**
|
||||
* Classify one connection.
|
||||
*
|
||||
* Resolution order, first match wins:
|
||||
* 1. the synthetic no-auth sentinel → `keyless` (no credential can be billed);
|
||||
* 2. a catalog entry matching BOTH provider and `authType`;
|
||||
* 3. a provider-wide catalog entry (no `authType` declared);
|
||||
* 4. otherwise `unknown`.
|
||||
*
|
||||
* `unknown` is never treated as free by any caller — `isPlanIncluded()` below
|
||||
* returns false for it, so an uncurated provider stays outside the
|
||||
* subscription rung until someone curates it deliberately.
|
||||
*/
|
||||
export function classifyConnectionBilling(
|
||||
connection: BillableConnection,
|
||||
catalog: readonly ConnectionBillingEntry[] = CONNECTION_BILLING_CATALOG
|
||||
): ConnectionBillingVerdict {
|
||||
if (connection.connectionId === SYNTHETIC_NOAUTH_CONNECTION_ID) return KEYLESS_VERDICT;
|
||||
|
||||
const provider = connection.provider;
|
||||
if (!provider) return UNKNOWN_VERDICT;
|
||||
|
||||
const providerEntries = catalog.filter((entry) => entry.provider === provider);
|
||||
if (providerEntries.length === 0) return UNKNOWN_VERDICT;
|
||||
|
||||
const authType = typeof connection.authType === "string" ? connection.authType : null;
|
||||
const authMatch = authType
|
||||
? providerEntries.find((entry) => entry.authType === authType)
|
||||
: undefined;
|
||||
const entry = authMatch ?? providerEntries.find((entry) => entry.authType === undefined);
|
||||
if (!entry) return UNKNOWN_VERDICT;
|
||||
|
||||
return { billing: entry.billing, overage: entry.overage, reason: entry.reason };
|
||||
}
|
||||
|
||||
/**
|
||||
* True when serving a request through this connection consumes an allowance
|
||||
* the operator already pays for, rather than adding incremental spend.
|
||||
* `keyless` qualifies: it costs nothing by construction.
|
||||
*/
|
||||
export function isPlanIncluded(verdict: ConnectionBillingVerdict): boolean {
|
||||
return verdict.billing === "subscription" || verdict.billing === "keyless";
|
||||
}
|
||||
|
||||
/**
|
||||
* True when exhausting this connection's allowance cannot start costing money.
|
||||
* The strict `auto/subscription` grouping admits nothing else: an operator who
|
||||
* asked never to spend extra must not be surprised by a provider that meters
|
||||
* past the plan, nor by one whose terms simply are not established.
|
||||
*/
|
||||
export function isOverageSafe(verdict: ConnectionBillingVerdict): boolean {
|
||||
return verdict.overage === "hard-stop";
|
||||
}
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
import { getCachedProviderConnections } from "@/lib/db/readCache";
|
||||
import { defaultLogger as log } from "@omniroute/open-sse/utils/logger";
|
||||
import type { FreeAccessState } from "./strictZeroCostFilter";
|
||||
import { isStateStaleForReset } from "./subscriptionLadder";
|
||||
|
||||
const USAGE_FETCHER_PROVIDER_SET = new Set<string>(USAGE_FETCHER_PROVIDERS);
|
||||
|
||||
@@ -191,7 +192,13 @@ export function resolveFreeAccessState(
|
||||
|
||||
const key = cacheKey(provider, connectionId);
|
||||
const entry = cache.get(key);
|
||||
const fresh = entry && Date.now() - entry.fetchedAtMs <= ttlMs();
|
||||
// Subscription-first routing (decision 3): an entry whose own `resetAt` has
|
||||
// already passed describes a quota window that no longer exists, so it is
|
||||
// stale REGARDLESS of its age. Without this, a plan that refilled at
|
||||
// midnight keeps reading EXHAUSTED until the TTL happens to lapse, and
|
||||
// routing stays on paid rungs for no reason. See `subscriptionLadder.ts`.
|
||||
const resetElapsed = entry !== undefined && isStateStaleForReset(entry.state);
|
||||
const fresh = entry && !resetElapsed && Date.now() - entry.fetchedAtMs <= ttlMs();
|
||||
if (!fresh) {
|
||||
void refresh(provider, connectionId);
|
||||
}
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
*/
|
||||
import {
|
||||
FREE_MODEL_BUDGETS,
|
||||
grantsFreeAccess,
|
||||
type FreeModelBudget,
|
||||
} from "@omniroute/open-sse/config/freeModelCatalog.ts";
|
||||
import { SYNTHETIC_NOAUTH_CONNECTION_ID } from "./resilienceCandidateFilter";
|
||||
@@ -184,7 +185,7 @@ export function evaluateCandidateConnections(
|
||||
// entries today, so it will correctly exclude).
|
||||
if (isGenuineNoAuthCandidate) return [SYNTHETIC_NOAUTH_CONNECTION_ID];
|
||||
}
|
||||
if (budgetEntry.freeType === "discontinued") return [];
|
||||
if (!grantsFreeAccess(budgetEntry.freeType)) return [];
|
||||
if (isGenuineNoAuthCandidate) return []; // no-auth path but a non-keyless catalog entry: contradictory metadata, fail closed
|
||||
|
||||
// Every remaining freeType (recurring-*, one-time-initial, a keyless entry
|
||||
|
||||
411
open-sse/services/autoCombo/subscriptionLadder.ts
Normal file
@@ -0,0 +1,411 @@
|
||||
/**
|
||||
* Subscription-first routing: the rung model, its two groupings, and the
|
||||
* reset re-entry rules.
|
||||
*
|
||||
* OmniRoute already answers "is this model free?" (`paidModelFilter.ts`) and
|
||||
* "can this connection ever bill me?" (`strictZeroCostFilter.ts`). Both fail
|
||||
* CLOSED — an exhausted free pool is an empty pool, never a step up to a paid
|
||||
* option. And every paid-side mechanism (`cost-optimized`, `budgetCap`,
|
||||
* the `cost-saver` mode pack) is tier-agnostic. Neither side answers:
|
||||
*
|
||||
* "use the quota I already pay for; when it runs out either stop, or step up
|
||||
* one rung at a time; and come back the moment it resets."
|
||||
*
|
||||
* This module supplies both halves of that, sharing one rung model:
|
||||
*
|
||||
* - `filterSubscriptionOnlyCandidates` — the strict grouping (`auto/subscription`).
|
||||
* Rung 0 only, overage-safe connections only, verified live. Fails CLOSED.
|
||||
* - `orderPoolByRung` — the escalating grouping (`auto/thrifty`). All rungs,
|
||||
* ordered, with exhausted rungs gated out. Fails OPEN, one rung at a time.
|
||||
*
|
||||
* Design mirrors `strictZeroCostFilter.ts` deliberately: pure functions, the
|
||||
* live quota lookup injected as a synchronous resolver, no DB or network
|
||||
* import, and the SAME connection-safety invariant — every connection in a
|
||||
* candidate's `allowedConnectionIds` is verified INDIVIDUALLY and the array is
|
||||
* rewritten to exactly the surviving subset, never the full original list.
|
||||
* `autoStrategy.ts` enforces `allowedConnectionIds` as a hard allowlist before
|
||||
* selecting a connection at dispatch, so rewriting it here is sufficient to
|
||||
* make "verified" and "actually used" the same set by construction.
|
||||
*/
|
||||
import {
|
||||
classifyConnectionBilling,
|
||||
isOverageSafe,
|
||||
type BillableConnection,
|
||||
} from "./connectionBilling";
|
||||
import type { ConnectionBillingEntry } from "@omniroute/open-sse/config/connectionBillingCatalog.ts";
|
||||
import type { FreeAccessState } from "./strictZeroCostFilter";
|
||||
|
||||
/**
|
||||
* Rungs in escalation order. Index is the ordering key; membership is decided
|
||||
* by `assignRung` below.
|
||||
*
|
||||
* The rungs differ in more than price — each has its OWN exhaustion signal,
|
||||
* which is why this is not just a sort:
|
||||
*
|
||||
* subscription / keyless / free → exhausted on QUOTA (observable, tracked)
|
||||
* cheap / premium → exhausted on BUDGET (no quota exists; a paid
|
||||
* connection serves forever)
|
||||
*/
|
||||
export const RUNG_ORDER = ["subscription", "keyless", "free", "cheap", "premium"] as const;
|
||||
|
||||
export type LadderRung = (typeof RUNG_ORDER)[number];
|
||||
|
||||
/** Rungs whose exhaustion is observable from provider quota state. */
|
||||
const QUOTA_BEARING_RUNGS: ReadonlySet<LadderRung> = new Set<LadderRung>([
|
||||
"subscription",
|
||||
"keyless",
|
||||
"free",
|
||||
]);
|
||||
|
||||
export function rungIndex(rung: LadderRung): number {
|
||||
return RUNG_ORDER.indexOf(rung);
|
||||
}
|
||||
|
||||
/** A candidate as this module needs to see it — a structural subset of
|
||||
* `VirtualAutoComboCandidate` (`virtualFactory.ts`), so this file has no
|
||||
* dependency on that module's full type. */
|
||||
export interface LadderCandidate {
|
||||
provider: string;
|
||||
model: string;
|
||||
connectionId: string | null;
|
||||
allowedConnectionIds?: string[];
|
||||
}
|
||||
|
||||
export interface LadderOptions {
|
||||
/** Master switch. When false every exported filter is the identity function
|
||||
* — the same off-by-default contract `filterPaidOnlyCandidates` holds. */
|
||||
enabled: boolean;
|
||||
/**
|
||||
* Live allowance/quota state for ONE (provider, connection) pair, resolved
|
||||
* from the cache in `freeAccessQuota.ts`. Synchronous by design: nothing in
|
||||
* a candidate-pool build may await a network call.
|
||||
*
|
||||
* `undefined` means "no usage adapter for this provider, or nothing fresh
|
||||
* cached". The two groupings interpret that OPPOSITELY on purpose — see
|
||||
* `admitUnknownQuota` below.
|
||||
*/
|
||||
resolveFreeAccessState: (provider: string, connectionId: string) => FreeAccessState | undefined;
|
||||
/**
|
||||
* `authType` for a connection id (`provider_connections.auth_type`), needed
|
||||
* to classify billing. Unknown ids resolve to `null` → the provider-wide
|
||||
* catalog entry, or `unknown` billing.
|
||||
*/
|
||||
resolveAuthType: (connectionId: string) => string | null;
|
||||
/** Economic tier of a (provider, model) pair — `classifyTier()` in
|
||||
* production, injected so this module needs no registry/pricing import. */
|
||||
resolveEconomicTier: (provider: string, model: string) => "free" | "cheap" | "premium";
|
||||
/**
|
||||
* Remaining-percent at or below which a quota-bearing connection counts as
|
||||
* exhausted. Default 2, matching `quotaPreflight.defaultThresholdPercent`
|
||||
* (`src/lib/resilience/settings/types.ts`) so the two agree.
|
||||
*/
|
||||
exitCutoffPercent?: number;
|
||||
/**
|
||||
* Remaining-percent a quota-bearing connection must EXCEED to be re-admitted
|
||||
* after having been exhausted. Strictly greater than `exitCutoffPercent`;
|
||||
* the gap is the hysteresis band that stops a connection hovering at the
|
||||
* cutoff from oscillating between rungs on consecutive requests. Default 5.
|
||||
*/
|
||||
reentryMinRemainingPercent?: number;
|
||||
/** Max age of a `FreeAccessState.checkedAt` before it is treated as stale. */
|
||||
maxStateAgeMs: number;
|
||||
/**
|
||||
* Whether a connection with no usable quota reading is admitted.
|
||||
*
|
||||
* - `auto/thrifty` passes TRUE: trying a plan-included connection costs
|
||||
* nothing, and if it turns out to be exhausted the dispatcher's
|
||||
* fall-through reaches the next rung anyway. Refusing to try it would
|
||||
* send a request to a PAID rung on missing telemetry — the exact
|
||||
* outcome the grouping exists to avoid.
|
||||
* - `auto/subscription` passes FALSE: its promise is that no request can
|
||||
* cost extra, and an unverifiable connection cannot support that promise.
|
||||
*/
|
||||
admitUnknownQuota: boolean;
|
||||
/**
|
||||
* Budget consumed so far on a paid rung, in USD, for the operator's current
|
||||
* budget window. `null`/`undefined` means no spend accounting is available,
|
||||
* in which case paid rungs are NOT budget-gated (they still order after
|
||||
* every plan-included rung). See the spec's open question on the ledger.
|
||||
*/
|
||||
resolveRungSpendUsd?: (rung: LadderRung) => number | null;
|
||||
/** Per-rung budget in USD. A rung mapped to 0 is disabled outright. */
|
||||
rungBudgetUsd?: Partial<Record<LadderRung, number>>;
|
||||
/** `now` injection for deterministic tests. */
|
||||
now?: () => number;
|
||||
/** Catalog override for tests; production callers never pass this. */
|
||||
catalog?: readonly ConnectionBillingEntry[];
|
||||
}
|
||||
|
||||
const DEFAULT_EXIT_CUTOFF_PERCENT = 2;
|
||||
const DEFAULT_REENTRY_MIN_REMAINING_PERCENT = 5;
|
||||
|
||||
/**
|
||||
* Which rung a specific (candidate, connection) pair belongs to.
|
||||
*
|
||||
* Billing class decides first because it is the fact that actually determines
|
||||
* whether money moves; only a genuinely metered connection falls through to
|
||||
* the model's economic tier. `unknown` billing is metered by definition
|
||||
* (`connectionBilling.ts`), so an uncurated provider lands on a paid rung
|
||||
* rather than silently joining the subscription rung.
|
||||
*/
|
||||
export function assignRung(
|
||||
candidate: Pick<LadderCandidate, "provider" | "model">,
|
||||
connection: BillableConnection,
|
||||
options: Pick<LadderOptions, "resolveEconomicTier" | "catalog">
|
||||
): LadderRung {
|
||||
const verdict = classifyConnectionBilling(connection, options.catalog);
|
||||
if (verdict.billing === "subscription") return "subscription";
|
||||
if (verdict.billing === "keyless") return "keyless";
|
||||
return options.resolveEconomicTier(candidate.provider, candidate.model);
|
||||
}
|
||||
|
||||
/**
|
||||
* Is this connection's plan allowance usable right now?
|
||||
*
|
||||
* `hasBeenExhausted` selects which side of the hysteresis band applies: a
|
||||
* connection that is currently in play only has to stay above the exit cutoff,
|
||||
* while one that already dropped out has to climb back above the (higher)
|
||||
* re-entry threshold before it is admitted again.
|
||||
*/
|
||||
export function isQuotaUsable(
|
||||
state: FreeAccessState | undefined,
|
||||
options: Pick<
|
||||
LadderOptions,
|
||||
| "exitCutoffPercent"
|
||||
| "reentryMinRemainingPercent"
|
||||
| "maxStateAgeMs"
|
||||
| "admitUnknownQuota"
|
||||
| "now"
|
||||
>,
|
||||
hasBeenExhausted = false
|
||||
): boolean {
|
||||
if (!state) return options.admitUnknownQuota;
|
||||
if (state.status === "EXHAUSTED") return false;
|
||||
if (state.status === "UNKNOWN") return options.admitUnknownQuota;
|
||||
|
||||
const now = (options.now ?? Date.now)();
|
||||
const checkedAtMs = Date.parse(state.checkedAt);
|
||||
if (!Number.isFinite(checkedAtMs) || now - checkedAtMs > options.maxStateAgeMs) {
|
||||
return options.admitUnknownQuota;
|
||||
}
|
||||
|
||||
if (state.remainingFreeAllowance === null) return options.admitUnknownQuota;
|
||||
|
||||
const exitCutoff = options.exitCutoffPercent ?? DEFAULT_EXIT_CUTOFF_PERCENT;
|
||||
const reentryFloor = Math.max(
|
||||
options.reentryMinRemainingPercent ?? DEFAULT_REENTRY_MIN_REMAINING_PERCENT,
|
||||
exitCutoff
|
||||
);
|
||||
const threshold = hasBeenExhausted ? reentryFloor : exitCutoff;
|
||||
return state.remainingFreeAllowance > threshold;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decision 3 — re-entry after a plan quota resets.
|
||||
*
|
||||
* A cached state whose own `resetAt` has already passed describes a window
|
||||
* that no longer exists. Waiting out the cache TTL before re-reading it is
|
||||
* pure lag on the single transition subscription-first routing cares most
|
||||
* about, so such an entry is stale REGARDLESS of its age.
|
||||
*
|
||||
* Consumed by `freeAccessQuota.ts`, which owns the cache; kept here so the
|
||||
* rule sits with the rest of the ladder's semantics and is testable without
|
||||
* touching the cache.
|
||||
*/
|
||||
export function isStateStaleForReset(
|
||||
state: Pick<FreeAccessState, "resetAt"> | undefined,
|
||||
now: number = Date.now()
|
||||
): boolean {
|
||||
if (!state?.resetAt) return false;
|
||||
const resetAtMs = Date.parse(state.resetAt);
|
||||
if (!Number.isFinite(resetAtMs)) return false;
|
||||
return resetAtMs <= now;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decision 3, second half — never hold a plan-included connection in cooldown
|
||||
* past the moment its own upstream says the quota is back.
|
||||
*
|
||||
* The exhausting 429 sets `rateLimitedUntil` from exponential backoff
|
||||
* (`baseCooldownMs * 2 ** failureIndex`, `src/sse/services/auth.ts`), which for
|
||||
* a subscription connection routinely overshoots the real reset — leaving
|
||||
* routing stuck on paid rungs long after the plan refilled.
|
||||
*
|
||||
* This only ever NARROWS a cooldown, and only when the upstream itself
|
||||
* supplied the reset instant. An absent, unparseable, or already-past
|
||||
* `resetAt` returns the original cooldown untouched.
|
||||
*/
|
||||
export function clampCooldownToReset(
|
||||
cooldownMs: number,
|
||||
resetAt: string | null | undefined,
|
||||
now: number = Date.now()
|
||||
): number {
|
||||
if (!resetAt) return cooldownMs;
|
||||
const resetAtMs = Date.parse(resetAt);
|
||||
if (!Number.isFinite(resetAtMs)) return cooldownMs;
|
||||
const untilResetMs = resetAtMs - now;
|
||||
if (untilResetMs <= 0) return cooldownMs;
|
||||
return Math.min(cooldownMs, untilResetMs);
|
||||
}
|
||||
|
||||
/** True when a paid rung has consumed its configured budget for the window. */
|
||||
export function isRungBudgetExhausted(rung: LadderRung, options: LadderOptions): boolean {
|
||||
const budget = options.rungBudgetUsd?.[rung];
|
||||
if (budget === undefined) return false;
|
||||
if (budget <= 0) return true; // explicitly disabled
|
||||
const spent = options.resolveRungSpendUsd?.(rung);
|
||||
if (spent === null || spent === undefined) return false; // no accounting → not gated
|
||||
return spent >= budget;
|
||||
}
|
||||
|
||||
/**
|
||||
* Connections on a candidate that are usable right now, paired with the rung
|
||||
* each one sits on. Quota-bearing rungs are verified per connection; paid
|
||||
* rungs have nothing per-connection to verify (they are gated per rung).
|
||||
*/
|
||||
function evaluateConnections(
|
||||
candidate: LadderCandidate,
|
||||
options: LadderOptions,
|
||||
accept: (rung: LadderRung) => boolean
|
||||
): { rung: LadderRung; connectionIds: string[] } | null {
|
||||
const connectionIds = candidate.connectionId
|
||||
? [candidate.connectionId]
|
||||
: (candidate.allowedConnectionIds ?? []);
|
||||
if (connectionIds.length === 0) return null;
|
||||
|
||||
let bestRung: LadderRung | null = null;
|
||||
const usable: string[] = [];
|
||||
|
||||
for (const connectionId of connectionIds) {
|
||||
const rung = assignRung(
|
||||
candidate,
|
||||
{
|
||||
provider: candidate.provider,
|
||||
authType: options.resolveAuthType(connectionId),
|
||||
connectionId,
|
||||
},
|
||||
options
|
||||
);
|
||||
if (!accept(rung)) continue;
|
||||
|
||||
if (QUOTA_BEARING_RUNGS.has(rung)) {
|
||||
const state = options.resolveFreeAccessState(candidate.provider, connectionId);
|
||||
if (!isQuotaUsable(state, options)) continue;
|
||||
} else if (isRungBudgetExhausted(rung, options)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
usable.push(connectionId);
|
||||
// A candidate reachable through several accounts is represented by its
|
||||
// CHEAPEST usable rung: that is the rung a request through it would
|
||||
// actually land on once dispatch picks from the surviving allowlist.
|
||||
if (bestRung === null || rungIndex(rung) < rungIndex(bestRung)) bestRung = rung;
|
||||
}
|
||||
|
||||
if (usable.length === 0 || bestRung === null) return null;
|
||||
return { rung: bestRung, connectionIds: usable };
|
||||
}
|
||||
|
||||
/** Rewrite a candidate's connection allowlist to the verified subset, keeping
|
||||
* the identity-when-nothing-changed contract the sibling filters hold. */
|
||||
function withVerifiedConnections<T extends LadderCandidate>(
|
||||
candidate: T,
|
||||
connectionIds: string[]
|
||||
): { candidate: T; changed: boolean } {
|
||||
if (candidate.connectionId !== null) return { candidate, changed: false };
|
||||
const original = candidate.allowedConnectionIds ?? [];
|
||||
const isSameSet =
|
||||
original.length === connectionIds.length && connectionIds.every((id) => original.includes(id));
|
||||
if (isSameSet) return { candidate, changed: false };
|
||||
return { candidate: { ...candidate, allowedConnectionIds: connectionIds }, changed: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* `auto/subscription` — the strict grouping. Keeps only candidates servable by
|
||||
* a plan-included connection whose overage is a documented hard stop, with
|
||||
* live quota headroom verified per connection.
|
||||
*
|
||||
* Fails CLOSED in every ambiguous case: uncurated provider, unverifiable
|
||||
* quota, or an overage that meters to paid. An empty result is the correct,
|
||||
* intended answer for an operator who asked never to spend extra — the
|
||||
* caller's existing empty-pool path handles it, exactly as `hidePaidModels`
|
||||
* already does.
|
||||
*/
|
||||
export function filterSubscriptionOnlyCandidates<T extends LadderCandidate>(
|
||||
pool: T[],
|
||||
options: LadderOptions
|
||||
): T[] {
|
||||
if (!options.enabled) return pool;
|
||||
|
||||
const strictOptions: LadderOptions = { ...options, admitUnknownQuota: false };
|
||||
const kept: T[] = [];
|
||||
let changed = false;
|
||||
|
||||
for (const candidate of pool) {
|
||||
const connectionIds = candidate.connectionId
|
||||
? [candidate.connectionId]
|
||||
: (candidate.allowedConnectionIds ?? []);
|
||||
|
||||
const safe = connectionIds.filter((connectionId) => {
|
||||
const connection: BillableConnection = {
|
||||
provider: candidate.provider,
|
||||
authType: options.resolveAuthType(connectionId),
|
||||
connectionId,
|
||||
};
|
||||
const verdict = classifyConnectionBilling(connection, options.catalog);
|
||||
// `keyless` is plan-included in the ladder's sense but is NOT a
|
||||
// subscription: this grouping is "the plan I pay for", so a no-auth
|
||||
// backend does not belong in it.
|
||||
if (verdict.billing !== "subscription") return false;
|
||||
if (!isOverageSafe(verdict)) return false;
|
||||
const state = strictOptions.resolveFreeAccessState(candidate.provider, connectionId);
|
||||
return isQuotaUsable(state, strictOptions);
|
||||
});
|
||||
|
||||
if (safe.length === 0) {
|
||||
changed = true;
|
||||
continue;
|
||||
}
|
||||
const result = withVerifiedConnections(candidate, safe);
|
||||
if (result.changed) changed = true;
|
||||
kept.push(result.candidate);
|
||||
}
|
||||
|
||||
return changed ? kept : pool;
|
||||
}
|
||||
|
||||
/**
|
||||
* `auto/thrifty` — the escalating grouping. Returns the pool ordered by rung,
|
||||
* with candidates whose every connection is exhausted (quota) or whose rung is
|
||||
* budget-exhausted removed.
|
||||
*
|
||||
* Ordering only — the `auto` engine still scores WITHIN the surviving pool, so
|
||||
* this decides which rungs are in play, not which candidate wins on one. The
|
||||
* combo dispatcher already walks targets in order and falls through on
|
||||
* failure, so a runtime exhaustion the preflight did not catch still escalates
|
||||
* to the next rung inside the same request.
|
||||
*
|
||||
* Rung eligibility is recomputed from live state on every pool build and
|
||||
* nothing is persisted: there is deliberately no sticky "currently on rung 3"
|
||||
* record that could outlive a quota reset and wedge routing on paid rungs.
|
||||
*/
|
||||
export function orderPoolByRung<T extends LadderCandidate>(pool: T[], options: LadderOptions): T[] {
|
||||
if (!options.enabled) return pool;
|
||||
|
||||
const ranked: Array<{ candidate: T; rung: LadderRung; order: number }> = [];
|
||||
for (const [order, candidate] of pool.entries()) {
|
||||
const evaluated = evaluateConnections(candidate, options, () => true);
|
||||
if (!evaluated) continue;
|
||||
const result = withVerifiedConnections(candidate, evaluated.connectionIds);
|
||||
ranked.push({ candidate: result.candidate, rung: evaluated.rung, order });
|
||||
}
|
||||
|
||||
ranked.sort((a, b) => {
|
||||
const byRung = rungIndex(a.rung) - rungIndex(b.rung);
|
||||
// Stable within a rung: preserve the pool's incoming order so the auto
|
||||
// scorer's own ranking is not reshuffled by this overlay.
|
||||
return byRung !== 0 ? byRung : a.order - b.order;
|
||||
});
|
||||
|
||||
return ranked.map((entry) => entry.candidate);
|
||||
}
|
||||
@@ -23,7 +23,20 @@ import { isVisionModelId } from "@/shared/constants/visionModels";
|
||||
import { isVisionBridgeForcedModel } from "@/shared/constants/visionBridgeDefaults";
|
||||
|
||||
export type AutoCategory = "coding" | "reasoning" | "vision" | "chat" | "multimodal";
|
||||
export type AutoTier = "fast" | "cheap" | "floor" | "free" | "reliable" | "pro";
|
||||
export type AutoTier =
|
||||
| "fast"
|
||||
| "cheap"
|
||||
| "floor"
|
||||
| "free"
|
||||
| "reliable"
|
||||
| "pro"
|
||||
// Subscription-first routing. Unlike every tier above, these two narrow by
|
||||
// the CONNECTION's billing class, not the model's price — so they are
|
||||
// applied in `virtualFactory.ts` against live connection state rather than
|
||||
// by `buildAutoCandidateFilter` below, which only sees (provider, model).
|
||||
// See `subscriptionLadder.ts` and `docs/routing/SUBSCRIPTION_LADDER.md`.
|
||||
| "subscription"
|
||||
| "thrifty";
|
||||
|
||||
export const AUTO_CATEGORIES: readonly AutoCategory[] = [
|
||||
"coding",
|
||||
@@ -39,6 +52,8 @@ export const AUTO_TIERS: readonly AutoTier[] = [
|
||||
"free",
|
||||
"reliable",
|
||||
"pro",
|
||||
"subscription",
|
||||
"thrifty",
|
||||
];
|
||||
|
||||
const CATEGORY_SET = new Set<string>(AUTO_CATEGORIES);
|
||||
@@ -84,6 +99,9 @@ export function tierToWeightVariant(tier?: AutoTier): AutoVariant | "reliability
|
||||
return "fast";
|
||||
case "cheap":
|
||||
case "floor":
|
||||
// The ladder already orders plan-included rungs first; within a rung it
|
||||
// should still lean cheap rather than reach for the most expensive model.
|
||||
case "thrifty":
|
||||
return "cheap";
|
||||
case "reliable":
|
||||
return "reliability";
|
||||
@@ -118,8 +136,7 @@ export function buildAutoCandidateFilter(
|
||||
}
|
||||
try {
|
||||
const caps = getResolvedModelCapabilities({ provider: c.provider, model: c.model });
|
||||
const capable =
|
||||
caps.supportsVision === true || isVisionModelId(c.model);
|
||||
const capable = caps.supportsVision === true || isVisionModelId(c.model);
|
||||
if (!capable) return false;
|
||||
// #vison-pool: registry entries whose catalog OVERSTATES vision support
|
||||
// (opencode-go/opencode-zen/tokenrouter — the backend models are text-only)
|
||||
|
||||
@@ -28,6 +28,11 @@ import { buildFamilyCandidateFilter, type ModelFamily } from "./modelFamily";
|
||||
import { getHiddenModelsByProvider } from "@/models";
|
||||
import { getSyncedAvailableModelsByConnection, getCustomModels } from "@/lib/db/models";
|
||||
import { filterPaidOnlyCandidates } from "./paidModelFilter";
|
||||
import {
|
||||
filterSubscriptionOnlyCandidates,
|
||||
orderPoolByRung,
|
||||
type LadderOptions,
|
||||
} from "./subscriptionLadder";
|
||||
import { filterStrictZeroCostCandidates, filterTosAvoidCandidates } from "./strictZeroCostFilter";
|
||||
import { resolveFreeAccessState } from "./freeAccessQuota";
|
||||
import { isModelExcludedByConnection } from "@/domain/connectionModelRules";
|
||||
@@ -145,6 +150,98 @@ type VirtualAutoCombo = AutoComboConfig & {
|
||||
export interface PreparedVirtualAutoComboInputs {
|
||||
readonly regularCandidates: readonly VirtualAutoComboCandidate[];
|
||||
readonly familyCandidates: readonly VirtualAutoComboCandidate[];
|
||||
/**
|
||||
* `provider_connections.auth_type` per connection id. Subscription-first
|
||||
* routing classifies billing per CONNECTION (`connectionBilling.ts`), and
|
||||
* the candidate pool only carries connection ids — so the auth types are
|
||||
* captured here, during the one bulk connection read this function already
|
||||
* does, instead of re-reading the DB per pool narrowing.
|
||||
*/
|
||||
readonly authTypeByConnectionId?: ReadonlyMap<string, string | null>;
|
||||
/** Operator settings for the subscription ladder; absent = feature off. */
|
||||
readonly subscriptionLadder?: SubscriptionLadderSettings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Operator-facing knobs for subscription-first routing. Mirrors the Zod shape
|
||||
* in `src/shared/validation/settingsSchemas.ts`.
|
||||
*
|
||||
* Deliberately TUNING ONLY — there is no `enabled` flag here. `auto/subscription`
|
||||
* and `auto/thrifty` are new ids that nothing routes through unless a caller
|
||||
* asks for them by name, so requesting the id IS the opt-in. A settings toggle
|
||||
* that could switch them off would be actively dangerous: it would leave an id
|
||||
* whose whole promise is "plan-included only" quietly serving the full pool,
|
||||
* paid models included.
|
||||
*/
|
||||
export interface SubscriptionLadderSettings {
|
||||
exitCutoffPercent?: number;
|
||||
reentryMinRemainingPercent?: number;
|
||||
rungBudgetUsd?: Record<string, number>;
|
||||
/** Staleness bound for a cached quota reading, derived from the existing
|
||||
* `autoRefreshProviderQuotaInterval` exactly as STRICT_ZERO_COST does. */
|
||||
maxStateAgeMs: number;
|
||||
}
|
||||
|
||||
function readSubscriptionLadderSettings(
|
||||
settings: Record<string, unknown>
|
||||
): SubscriptionLadderSettings {
|
||||
const maxStateAgeMs = (Number(settings.autoRefreshProviderQuotaInterval) || 180) * 1000;
|
||||
const raw = settings.subscriptionLadder;
|
||||
if (!raw || typeof raw !== "object") return { maxStateAgeMs };
|
||||
const value = raw as Record<string, unknown>;
|
||||
const numeric = (key: string): number | undefined =>
|
||||
typeof value[key] === "number" && Number.isFinite(value[key] as number)
|
||||
? (value[key] as number)
|
||||
: undefined;
|
||||
const exitCutoffPercent = numeric("exitCutoffPercent");
|
||||
const reentryMinRemainingPercent = numeric("reentryMinRemainingPercent");
|
||||
return {
|
||||
maxStateAgeMs,
|
||||
...(exitCutoffPercent === undefined ? {} : { exitCutoffPercent }),
|
||||
...(reentryMinRemainingPercent === undefined ? {} : { reentryMinRemainingPercent }),
|
||||
...(value.rungBudgetUsd && typeof value.rungBudgetUsd === "object"
|
||||
? { rungBudgetUsd: value.rungBudgetUsd as Record<string, number> }
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the injected dependencies the pure ladder module needs. Everything it
|
||||
* touches is resolved here — the live quota cache, connection auth types, and
|
||||
* the economic tier resolver — so `subscriptionLadder.ts` itself stays free of
|
||||
* DB, network, and registry imports.
|
||||
*/
|
||||
function buildLadderOptions(
|
||||
prepared: PreparedVirtualAutoComboInputs,
|
||||
tier: "subscription" | "thrifty"
|
||||
): LadderOptions {
|
||||
const tuning = prepared.subscriptionLadder;
|
||||
const authTypes = prepared.authTypeByConnectionId;
|
||||
return {
|
||||
enabled: true,
|
||||
resolveFreeAccessState,
|
||||
resolveAuthType: (connectionId: string) => authTypes?.get(connectionId) ?? null,
|
||||
resolveEconomicTier: (provider: string, model: string) => {
|
||||
try {
|
||||
const resolved = classifyTier(provider, model).tier;
|
||||
return resolved === "free" || resolved === "premium" ? resolved : "cheap";
|
||||
} catch {
|
||||
// Same conservative default `safeClassifyTier` uses in suffixComposition.
|
||||
return "cheap";
|
||||
}
|
||||
},
|
||||
maxStateAgeMs: tuning?.maxStateAgeMs ?? 180_000,
|
||||
// The two groupings read a missing quota reading OPPOSITELY on purpose —
|
||||
// see `LadderOptions.admitUnknownQuota`.
|
||||
admitUnknownQuota: tier === "thrifty",
|
||||
...(tuning?.exitCutoffPercent === undefined
|
||||
? {}
|
||||
: { exitCutoffPercent: tuning.exitCutoffPercent }),
|
||||
...(tuning?.reentryMinRemainingPercent === undefined
|
||||
? {}
|
||||
: { reentryMinRemainingPercent: tuning.reentryMinRemainingPercent }),
|
||||
...(tuning?.rungBudgetUsd ? { rungBudgetUsd: tuning.rungBudgetUsd } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
function toExpiryMs(value: unknown): number | null {
|
||||
@@ -656,8 +753,15 @@ export async function prepareVirtualAutoComboInputs(
|
||||
const regularCandidates = buildPreparedPool(false);
|
||||
// #6453/#8183: family selectors bypass the reliability-curated no-auth allowlist.
|
||||
const familyCandidates = buildPreparedPool(true);
|
||||
// Subscription-first routing inputs, captured from the connection read above
|
||||
// so no later stage has to touch the DB again.
|
||||
const authTypeByConnectionId = new Map<string, string | null>();
|
||||
for (const conn of connections) {
|
||||
authTypeByConnectionId.set(conn.id, typeof conn.authType === "string" ? conn.authType : null);
|
||||
}
|
||||
const subscriptionLadder = readSubscriptionLadderSettings(settings);
|
||||
if (!options.includeResolvedCapabilities) {
|
||||
return { regularCandidates, familyCandidates };
|
||||
return { regularCandidates, familyCandidates, authTypeByConnectionId, subscriptionLadder };
|
||||
}
|
||||
|
||||
// One uninterrupted bulk read of all three capability tables for this prepare only.
|
||||
@@ -671,6 +775,8 @@ export async function prepareVirtualAutoComboInputs(
|
||||
return {
|
||||
regularCandidates: await attachPreparedCapabilityValues(regularCandidates, capabilityState),
|
||||
familyCandidates: await attachPreparedCapabilityValues(familyCandidates, capabilityState),
|
||||
authTypeByConnectionId,
|
||||
subscriptionLadder,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -842,6 +948,32 @@ export async function createVirtualAutoComboFromPrepared(
|
||||
}
|
||||
}
|
||||
|
||||
// Subscription-first routing (`auto/subscription`, `auto/thrifty`). Applied
|
||||
// AFTER the category/tier narrowing above because, unlike every other tier,
|
||||
// these two select on the connection's billing class and its live quota
|
||||
// state rather than on the model's catalog price — see
|
||||
// `subscriptionLadder.ts` and `docs/routing/SUBSCRIPTION_LADDER.md`.
|
||||
if (spec?.tier === "subscription" || spec?.tier === "thrifty") {
|
||||
const ladderOptions = buildLadderOptions(prepared, spec.tier);
|
||||
const beforeCount = effectivePool.length;
|
||||
effectivePool =
|
||||
spec.tier === "subscription"
|
||||
? filterSubscriptionOnlyCandidates(effectivePool, ladderOptions)
|
||||
: orderPoolByRung(effectivePool, ladderOptions);
|
||||
if (spec.tier === "subscription" && effectivePool.length === 0 && beforeCount > 0) {
|
||||
// Intended, not a defect: the operator asked for plan-included capacity
|
||||
// only, and right now there is none with verified headroom. Failing
|
||||
// closed here is the entire promise of the id — the caller's existing
|
||||
// empty-pool path turns it into a clear error rather than a silent,
|
||||
// billable fallback.
|
||||
warnEmptyAutoPoolOnce(
|
||||
"auto/subscription",
|
||||
"auto/subscription: no plan-included connection has verified quota headroom; " +
|
||||
"returning an empty pool rather than falling back to paid capacity."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let weights: ScoringWeights = { ...DEFAULT_WEIGHTS };
|
||||
let explorationRate = 0.05; // Default exploration rate
|
||||
let routerStrategy = "lkgp"; // All auto variants use LKGP
|
||||
|
||||
@@ -96,6 +96,13 @@ const ARK_CONSOLE_URL =
|
||||
/** Cookie names required for a valid console session (mirrors tokenExtractionConfig) */
|
||||
const REQUIRED_COOKIES = ["digest", "AccountID", "csrfToken", "userInfo"] as const;
|
||||
|
||||
/** Exact-domain match for session cookies — substring checks would also accept
|
||||
* look-alike hosts (e.g. `volcengine.com.evil.test`). Playwright may report the
|
||||
* domain with or without a leading dot. */
|
||||
function isVolcengineCookieDomain(domain: string): boolean {
|
||||
return domain === "volcengine.com" || domain.endsWith(".volcengine.com");
|
||||
}
|
||||
|
||||
const DEFAULT_SESSION_TIMEOUT = 300_000;
|
||||
const SUBMIT_COOKIE_TIMEOUT = 90_000;
|
||||
const CAPTURE_POLL_INTERVAL = 1_000;
|
||||
@@ -618,7 +625,7 @@ export class VolcengineConsoleAutoLoginService {
|
||||
for (const cookie of cookies as Array<{ name: string; domain: string; value: string }>) {
|
||||
if (
|
||||
REQUIRED_COOKIES.includes(cookie.name as (typeof REQUIRED_COOKIES)[number]) &&
|
||||
cookie.domain.includes("volcengine.com")
|
||||
isVolcengineCookieDomain(cookie.domain)
|
||||
) {
|
||||
credentials[cookie.name] = cookie.value;
|
||||
}
|
||||
@@ -763,7 +770,7 @@ export class VolcengineConsoleAutoLoginService {
|
||||
domain: string;
|
||||
}>;
|
||||
const present = REQUIRED_COOKIES.filter((name) =>
|
||||
cookies.some((c) => c.name === name && c.domain.includes("volcengine.com"))
|
||||
cookies.some((c) => c.name === name && isVolcengineCookieDomain(c.domain))
|
||||
);
|
||||
parts.push(
|
||||
`cookies=[${present.join(",") || "none of digest/AccountID/csrfToken/userInfo"}]`
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "omniroute",
|
||||
"version": "3.8.51",
|
||||
"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 353 providers, RTK+Caveman compression, auto fallback, MCP/A2A, desktop, PWA, and OpenAI-compatible APIs.",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"omniroute": "bin/omniroute.mjs",
|
||||
|
||||
@@ -32,6 +32,30 @@ curl -X POST https://localhost:20128/api/v1/session-leases \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### GET /api/v1/search
|
||||
|
||||
List search providers
|
||||
|
||||
Lists configured search providers and their supported search types.
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/v1/search \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
```
|
||||
|
||||
### POST /api/v1/search
|
||||
|
||||
Run a unified search
|
||||
|
||||
Searches the web, news, or X through a configured provider. Set `provider` to `xquik-search` to use Xquik for X search. The aliases `xquik` and `xquik_search` resolve to the same provider.
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/v1/search \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### POST /api/v1/chat/completions
|
||||
|
||||
Create chat completion
|
||||
|
||||
@@ -2,19 +2,47 @@
|
||||
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
mergeDashboardSessions,
|
||||
type DashboardSession,
|
||||
type ExclusiveDashboardSession,
|
||||
type RecentSessionForDashboard,
|
||||
} from "@/lib/sessionObservability";
|
||||
import { Card } from "@/shared/components";
|
||||
|
||||
type SessionsResponse = {
|
||||
sessions: RecentSessionForDashboard[];
|
||||
exclusiveSessions: ExclusiveDashboardSession[];
|
||||
};
|
||||
|
||||
const EMPTY_DATA: SessionsResponse = {
|
||||
sessions: [],
|
||||
exclusiveSessions: [],
|
||||
};
|
||||
|
||||
function isLeaseBackedSession(session: DashboardSession): session is ExclusiveDashboardSession {
|
||||
return "leaseBacked" in session && session.leaseBacked;
|
||||
}
|
||||
|
||||
export default function SessionsTab() {
|
||||
const t = useTranslations("usage");
|
||||
const [data, setData] = useState({ count: 0, sessions: [] });
|
||||
const tCommon = useTranslations("common");
|
||||
const [data, setData] = useState<SessionsResponse>(EMPTY_DATA);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const loadSessions = useCallback(async () => {
|
||||
try {
|
||||
const res = await fetch("/api/sessions");
|
||||
if (res.ok) setData(await res.json());
|
||||
if (res.ok) {
|
||||
const next = await res.json();
|
||||
setData({
|
||||
sessions: Array.isArray(next.sessions) ? next.sessions : [],
|
||||
exclusiveSessions: Array.isArray(next.exclusiveSessions) ? next.exclusiveSessions : [],
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
// A failed background poll leaves the last successful Sessions snapshot visible.
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -26,7 +54,12 @@ export default function SessionsTab() {
|
||||
return () => clearInterval(interval);
|
||||
}, [loadSessions]);
|
||||
|
||||
const formatAge = (ms) => {
|
||||
const displaySessions = useMemo(() => {
|
||||
return mergeDashboardSessions(data.exclusiveSessions, data.sessions);
|
||||
}, [data.exclusiveSessions, data.sessions]);
|
||||
|
||||
const formatAge = (ms: number | null) => {
|
||||
if (ms == null) return t("notAvailableSymbol");
|
||||
if (ms < 60000) return t("durationSecondsShort", { value: Math.floor(ms / 1000) });
|
||||
if (ms < 3600000) return t("durationMinutesShort", { value: Math.floor(ms / 60000) });
|
||||
return t("durationHoursShort", { value: Math.floor(ms / 3600000) });
|
||||
@@ -47,12 +80,17 @@ export default function SessionsTab() {
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-cyan-500/10 border border-cyan-500/20">
|
||||
<span className="w-2 h-2 rounded-full bg-cyan-500 animate-pulse" />
|
||||
<span className="text-sm font-semibold tabular-nums text-cyan-400">{data.count}</span>
|
||||
<span
|
||||
className="text-sm font-semibold tabular-nums text-cyan-400"
|
||||
data-testid="session-count"
|
||||
>
|
||||
{displaySessions.length}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{data.sessions.length === 0 ? (
|
||||
{displaySessions.length === 0 ? (
|
||||
<div className="text-center py-8 text-text-muted">
|
||||
<span
|
||||
className="material-symbols-outlined text-[40px] mb-2 block opacity-40"
|
||||
@@ -83,31 +121,46 @@ export default function SessionsTab() {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.sessions.map((s) => (
|
||||
<tr
|
||||
key={s.sessionId}
|
||||
className="border-b border-border/10 hover:bg-surface/20 transition-colors"
|
||||
>
|
||||
<td className="py-2.5 px-3">
|
||||
<span className="font-mono text-xs px-2 py-1 rounded bg-surface/40 text-text-muted">
|
||||
{s.sessionId.slice(0, 12)}…
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-2.5 px-3 text-text-muted tabular-nums">{formatAge(s.ageMs)}</td>
|
||||
<td className="py-2.5 px-3 text-right">
|
||||
<span className="font-semibold tabular-nums">{s.requestCount}</span>
|
||||
</td>
|
||||
<td className="py-2.5 px-3">
|
||||
{s.connectionId ? (
|
||||
<span className="text-xs font-mono text-cyan-400">
|
||||
{s.connectionId.slice(0, 10)}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-text-muted/40">{t("notAvailableSymbol")}</span>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{displaySessions.map((s) => {
|
||||
const leaseBacked = isLeaseBackedSession(s);
|
||||
return (
|
||||
<tr
|
||||
key={s.sessionId}
|
||||
className="border-b border-border/10 hover:bg-surface/20 transition-colors"
|
||||
>
|
||||
<td className="py-2.5 px-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className="font-mono text-xs px-2 py-1 rounded bg-surface/40 text-text-muted"
|
||||
title={s.sessionId}
|
||||
>
|
||||
{s.sessionId.slice(0, 12)}…
|
||||
</span>
|
||||
{leaseBacked && s.active && (
|
||||
<span className="text-[10px] font-semibold tracking-wide px-2 py-0.5 rounded-full border text-green-400 border-green-500/30 bg-green-500/10">
|
||||
{tCommon("active")}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td className="py-2.5 px-3 text-text-muted tabular-nums">
|
||||
{formatAge(s.ageMs)}
|
||||
</td>
|
||||
<td className="py-2.5 px-3 text-right">
|
||||
<span className="font-semibold tabular-nums">{s.requestCount}</span>
|
||||
</td>
|
||||
<td className="py-2.5 px-3">
|
||||
{s.connectionId ? (
|
||||
<span className="text-xs font-mono text-cyan-400" title={s.connectionId}>
|
||||
{(leaseBacked && s.connectionName) || s.connectionId.slice(0, 10)}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-text-muted/40">{t("notAvailableSymbol")}</span>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
10
src/app/api/providers/web-session-contract/route.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
|
||||
import { buildWebSessionContract } from "@/lib/providers/webSessionContract";
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const authError = await requireManagementAuth(request);
|
||||
if (authError) return authError;
|
||||
|
||||
return NextResponse.json(buildWebSessionContract());
|
||||
}
|
||||
@@ -5,13 +5,44 @@ import {
|
||||
getAllActiveSessionCountsByKey,
|
||||
} from "@omniroute/open-sse/services/sessionManager.ts";
|
||||
import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error";
|
||||
import { getExclusiveLeaseConnectionIds } from "@/lib/db/apiKeys";
|
||||
import { getExclusiveLeaseOccupancy } from "@/lib/db/exclusiveConnectionLeases";
|
||||
import { getProviderConnectionDisplayMetadata } from "@/lib/db/providers";
|
||||
import { getAccountDisplayName } from "@/lib/display/names";
|
||||
import { getPendingRequests } from "@/lib/usage/usageHistory";
|
||||
import { buildExclusiveDashboardSessions } from "@/lib/sessionObservability";
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const sessions = getActiveSessions();
|
||||
const count = getActiveSessionCount();
|
||||
const byApiKey = getAllActiveSessionCountsByKey();
|
||||
return NextResponse.json({ count, sessions, byApiKey });
|
||||
|
||||
// Reuse the hard-lease authority added by #10362. The API-key policy derives
|
||||
// the managed candidate set; SQLite occupancy is the source of truth for
|
||||
// which of those connections are actually leased right now.
|
||||
const managedConnectionIds = Array.from(await getExclusiveLeaseConnectionIds());
|
||||
const occupancy = getExclusiveLeaseOccupancy(managedConnectionIds);
|
||||
const leasedConnectionIds = new Set(occupancy.keys());
|
||||
const connectionNames = new Map(
|
||||
getProviderConnectionDisplayMetadata([...leasedConnectionIds]).map((connection) => [
|
||||
connection.id,
|
||||
getAccountDisplayName(connection),
|
||||
])
|
||||
);
|
||||
const exclusiveSessions = buildExclusiveDashboardSessions(
|
||||
leasedConnectionIds,
|
||||
getPendingRequests().byAccount,
|
||||
sessions,
|
||||
connectionNames
|
||||
);
|
||||
|
||||
return NextResponse.json({
|
||||
count,
|
||||
sessions,
|
||||
byApiKey,
|
||||
exclusiveSessions,
|
||||
});
|
||||
} catch (error) {
|
||||
return NextResponse.json({ error: sanitizeErrorMessage(error) }, { status: 500 });
|
||||
}
|
||||
|
||||
@@ -344,6 +344,43 @@ export async function getProviderConnectionById(id: string) {
|
||||
);
|
||||
}
|
||||
|
||||
export interface ProviderConnectionDisplayMetadata {
|
||||
id: string;
|
||||
name: string | null;
|
||||
displayName: string | null;
|
||||
email: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads only the non-credential fields needed by account display-name resolvers.
|
||||
*
|
||||
* This avoids decrypting provider credentials when a dashboard only needs labels.
|
||||
*/
|
||||
export function getProviderConnectionDisplayMetadata(
|
||||
connectionIds: readonly string[]
|
||||
): ProviderConnectionDisplayMetadata[] {
|
||||
const ids = [...new Set(connectionIds.filter((id) => id.length > 0))];
|
||||
if (ids.length === 0) return [];
|
||||
|
||||
const db = getDbInstance() as unknown as DbLike;
|
||||
const rows = db
|
||||
.prepare(
|
||||
`SELECT id, name, display_name, email FROM provider_connections
|
||||
WHERE id IN (${ids.map(() => "?").join(", ")})`
|
||||
)
|
||||
.all(...ids);
|
||||
|
||||
return rows.map((row) => {
|
||||
const view = rowToCamel(row) as JsonRecord;
|
||||
return {
|
||||
id: toStringOrNull(view.id) || "",
|
||||
name: toStringOrNull(view.name),
|
||||
displayName: toStringOrNull(view.displayName),
|
||||
email: toStringOrNull(view.email),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// #3368 PR6 — dedup web-session cookie/token credentials on connection create.
|
||||
// Re-importing the same session (e.g. via bulk web-session import) under a
|
||||
// different or blank name must update the existing connection instead of
|
||||
|
||||
@@ -410,16 +410,24 @@ export async function validateOpenAICompatibleProvider({ apiKey, providerSpecifi
|
||||
const chatSuffix = apiType === "responses" ? "/responses" : "/chat/completions";
|
||||
const chatUrl = `${baseUrl}${chatSuffix}`;
|
||||
const testModelId = validationModelId;
|
||||
const testBody =
|
||||
apiType === "responses"
|
||||
? {
|
||||
model: testModelId,
|
||||
input: [{ role: "user", content: "test" }],
|
||||
max_output_tokens: 1,
|
||||
}
|
||||
: {
|
||||
model: testModelId,
|
||||
messages: [{ role: "user", content: "test" }],
|
||||
max_tokens: 1,
|
||||
};
|
||||
|
||||
try {
|
||||
const chatRes = await validationWrite(chatUrl, {
|
||||
method: "POST",
|
||||
headers: buildBearerHeaders(apiKey, providerSpecificData),
|
||||
body: JSON.stringify({
|
||||
model: testModelId,
|
||||
messages: [{ role: "user", content: "test" }],
|
||||
max_tokens: 1,
|
||||
}),
|
||||
body: JSON.stringify(testBody),
|
||||
});
|
||||
|
||||
if (chatRes.ok) {
|
||||
|
||||
@@ -167,6 +167,13 @@ export const SEARCH_VALIDATOR_CONFIGS: Record<
|
||||
}),
|
||||
},
|
||||
}),
|
||||
"xquik-search": (apiKey) => ({
|
||||
url: "https://xquik.com/api/v1/x/tweets/search?q=test&limit=1",
|
||||
init: {
|
||||
method: "GET",
|
||||
headers: { Accept: "application/json", "x-api-key": apiKey },
|
||||
},
|
||||
}),
|
||||
"zai-search": (apiKey, providerSpecificData = {}) => {
|
||||
const baseUrl =
|
||||
typeof providerSpecificData?.baseUrl === "string" && providerSpecificData.baseUrl.trim()
|
||||
|
||||
58
src/lib/providers/webSessionContract.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import {
|
||||
listExtractionConfigs,
|
||||
type TokenSource,
|
||||
} from "@omniroute/open-sse/services/tokenExtractionConfig.ts";
|
||||
import { getWebSessionCredentialRequirement } from "@/shared/providers/webSessionCredentials";
|
||||
|
||||
export const WEB_SESSION_CONTRACT_VERSION = 1;
|
||||
|
||||
export interface WebSessionContractProvider {
|
||||
providerId: string;
|
||||
displayName: string;
|
||||
loginUrl: string;
|
||||
homeUrl: string;
|
||||
tokenSources: TokenSource[];
|
||||
credential: {
|
||||
kind: "cookie" | "token";
|
||||
storageKeys: string[];
|
||||
acceptsFullCookieHeader: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface WebSessionContract {
|
||||
version: typeof WEB_SESSION_CONTRACT_VERSION;
|
||||
providers: WebSessionContractProvider[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish only the canonical, non-secret metadata needed by external
|
||||
* credential brokers to capture credentials in the same shape OmniRoute
|
||||
* accepts. Provider instructions, polling state, and credential values are
|
||||
* intentionally excluded.
|
||||
*/
|
||||
export function buildWebSessionContract(): WebSessionContract {
|
||||
const providers = listExtractionConfigs().flatMap<WebSessionContractProvider>((config) => {
|
||||
const requirement = getWebSessionCredentialRequirement(config.providerId);
|
||||
if (!requirement || requirement.kind === "none") return [];
|
||||
|
||||
return [
|
||||
{
|
||||
providerId: config.providerId,
|
||||
displayName: config.displayName,
|
||||
loginUrl: config.loginUrl,
|
||||
homeUrl: config.homeUrl,
|
||||
tokenSources: config.tokenSources.map((source) => ({ ...source })),
|
||||
credential: {
|
||||
kind: requirement.kind,
|
||||
storageKeys: [...requirement.storageKeys],
|
||||
acceptsFullCookieHeader: requirement.acceptsFullCookieHeader,
|
||||
},
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
return {
|
||||
version: WEB_SESSION_CONTRACT_VERSION,
|
||||
providers,
|
||||
};
|
||||
}
|
||||
@@ -127,7 +127,10 @@ export async function executeWebSearch(
|
||||
|
||||
const log = input.log || defaultLog;
|
||||
if (input.provider === "x_search") input.provider = "x-search";
|
||||
if (input.provider === "x-search") input.search_type = "x";
|
||||
if (input.provider === "xquik" || input.provider === "xquik_search") {
|
||||
input.provider = "xquik-search";
|
||||
}
|
||||
if (input.provider === "x-search" || input.provider === "xquik-search") input.search_type = "x";
|
||||
const searchType = input.search_type || "web";
|
||||
|
||||
if (input.provider) {
|
||||
|
||||
93
src/lib/sessionObservability.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
export type RecentSessionForDashboard = {
|
||||
sessionId: string;
|
||||
ageMs: number;
|
||||
requestCount: number;
|
||||
connectionId: string | null;
|
||||
};
|
||||
|
||||
export type PendingRequestsByAccount = Record<string, Record<string, number>>;
|
||||
|
||||
export type ExclusiveDashboardSession = {
|
||||
sessionId: string;
|
||||
ageMs: null;
|
||||
requestCount: number;
|
||||
connectionId: string;
|
||||
connectionName: string | null;
|
||||
leaseBacked: true;
|
||||
active: boolean;
|
||||
};
|
||||
|
||||
export type DashboardSession = RecentSessionForDashboard | ExclusiveDashboardSession;
|
||||
|
||||
function positiveCount(value: unknown): number {
|
||||
const count = Number(value);
|
||||
return Number.isFinite(count) && count > 0 ? count : 0;
|
||||
}
|
||||
|
||||
function countInFlightRequests(
|
||||
pendingByAccount: PendingRequestsByAccount,
|
||||
connectionId: string
|
||||
): number {
|
||||
return Object.values(pendingByAccount[connectionId] ?? {}).reduce(
|
||||
(total, count) => total + positiveCount(count),
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the dashboard-only view of durable exclusive leases.
|
||||
*
|
||||
* The lease table remains the lifecycle authority. The request tracker is used
|
||||
* only to flag work currently in flight for an already-held lease; it never
|
||||
* creates, extends, or removes lease ownership.
|
||||
*
|
||||
* Deliberately does not expose the persisted owner hash, API-key id, or lease
|
||||
* generation. The dashboard needs occupancy, connection binding, and activity
|
||||
* state — not fencing material.
|
||||
*/
|
||||
export function buildExclusiveDashboardSessions(
|
||||
leasedConnectionIds: ReadonlySet<string>,
|
||||
pendingByAccount: PendingRequestsByAccount,
|
||||
recentSessions: readonly RecentSessionForDashboard[],
|
||||
connectionNames: ReadonlyMap<string, string> = new Map()
|
||||
): ExclusiveDashboardSession[] {
|
||||
const recentRequestsByConnection = new Map<string, number>();
|
||||
for (const session of recentSessions) {
|
||||
if (!session.connectionId) continue;
|
||||
recentRequestsByConnection.set(
|
||||
session.connectionId,
|
||||
(recentRequestsByConnection.get(session.connectionId) ?? 0) +
|
||||
positiveCount(session.requestCount)
|
||||
);
|
||||
}
|
||||
|
||||
return Array.from(leasedConnectionIds)
|
||||
.map((connectionId) => ({
|
||||
sessionId: `lease:${connectionId}`,
|
||||
ageMs: null,
|
||||
requestCount: recentRequestsByConnection.get(connectionId) ?? 0,
|
||||
connectionId,
|
||||
connectionName: connectionNames.get(connectionId) ?? null,
|
||||
leaseBacked: true as const,
|
||||
active: countInFlightRequests(pendingByAccount, connectionId) > 0,
|
||||
}))
|
||||
.sort((left, right) => {
|
||||
if (left.active !== right.active) return left.active ? -1 : 1;
|
||||
return left.connectionId.localeCompare(right.connectionId);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Lease-backed rows replace request-derived rows for the same connection.
|
||||
* Sessions without a connection binding remain untouched.
|
||||
*/
|
||||
export function mergeDashboardSessions(
|
||||
leaseSessions: readonly ExclusiveDashboardSession[],
|
||||
recentSessions: readonly RecentSessionForDashboard[]
|
||||
): DashboardSession[] {
|
||||
const leasedConnectionIds = new Set(leaseSessions.map((session) => session.connectionId));
|
||||
const unleasedRecentSessions = recentSessions.filter(
|
||||
(session) => !session.connectionId || !leasedConnectionIds.has(session.connectionId)
|
||||
);
|
||||
return [...leaseSessions, ...unleasedRecentSessions];
|
||||
}
|
||||
@@ -141,6 +141,18 @@ export const SEARCH_PROVIDERS = {
|
||||
"SuperGrok OAuth (xai-oauth) or xAI API key. This is Grok X Search, not the X Developer MCP.",
|
||||
serviceKinds: ["webSearch"],
|
||||
},
|
||||
"xquik-search": {
|
||||
id: "xquik-search",
|
||||
alias: "xquik",
|
||||
name: "Xquik X Search",
|
||||
icon: "tag",
|
||||
color: "#111827",
|
||||
textIcon: "XQ",
|
||||
website: "https://docs.xquik.com",
|
||||
authHint:
|
||||
"Xquik API key (xq_...). Search is metered per returned post; the catalog estimate uses 5 results.",
|
||||
serviceKinds: ["webSearch"],
|
||||
},
|
||||
"ollama-search": {
|
||||
id: "ollama-search",
|
||||
alias: "ollama-search",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FREE_MODEL_BUDGETS } from "@omniroute/open-sse/config/freeModelCatalog";
|
||||
import { FREE_MODEL_BUDGETS, grantsFreeAccess } from "@omniroute/open-sse/config/freeModelCatalog";
|
||||
import { resolveProviderId } from "@/shared/constants/providers";
|
||||
import { globToRegex } from "@/shared/utils/globPattern";
|
||||
import { AI_MODELS } from "@/shared/constants/models";
|
||||
@@ -12,16 +12,25 @@ import { AI_MODELS } from "@/shared/constants/models";
|
||||
* considered free when its id carries the OpenRouter-style `:free` suffix, when
|
||||
* both its prompt and completion prices are zero, or when its id is listed as a
|
||||
* free model for that provider in the catalog.
|
||||
*
|
||||
* The catalog also records the regime of every entry via `freeType`
|
||||
* (`FreeModelFreeType`). A regime can retire a free tier behind a paid key
|
||||
* (`discontinued`); `grantsFreeAccess` is the single predicate that decides
|
||||
* whether a regime still grants free access, and the two structures below are
|
||||
* derived only from entries whose regime grants it — so a `discontinued` entry
|
||||
* is never reported free, and a future regime that forgets to be classified
|
||||
* fails to compile rather than defaulting silently.
|
||||
*/
|
||||
|
||||
/** Catalogued entries whose regime still grants free access. */
|
||||
const FREE_BUDGETS = FREE_MODEL_BUDGETS.filter((m) => grantsFreeAccess(m.freeType));
|
||||
|
||||
/** Provider ids that have at least one documented free model. */
|
||||
export const PROVIDERS_WITH_FREE_MODELS: Set<string> = new Set(
|
||||
FREE_MODEL_BUDGETS.map((m) => m.provider)
|
||||
);
|
||||
export const PROVIDERS_WITH_FREE_MODELS: Set<string> = new Set(FREE_BUDGETS.map((m) => m.provider));
|
||||
|
||||
const FREE_MODEL_IDS_BY_PROVIDER: Map<string, Set<string>> = (() => {
|
||||
const map = new Map<string, Set<string>>();
|
||||
for (const m of FREE_MODEL_BUDGETS) {
|
||||
for (const m of FREE_BUDGETS) {
|
||||
let set = map.get(m.provider);
|
||||
if (!set) {
|
||||
set = new Set<string>();
|
||||
|
||||
@@ -20,10 +20,7 @@ import {
|
||||
} from "@/shared/reasoning/effortStandardization";
|
||||
|
||||
import { modelIdSchema, nonEmptyStringSchema } from "./misc.ts";
|
||||
import {
|
||||
isCanonicalEmbeddingItem,
|
||||
JINA_NATIVE_MEDIA_KEYS,
|
||||
} from "../jinaNativeEmbeddingInput.ts";
|
||||
import { isCanonicalEmbeddingItem, JINA_NATIVE_MEDIA_KEYS } from "../jinaNativeEmbeddingInput.ts";
|
||||
import { isGeminiNativeEmbeddingItem } from "../geminiNativeEmbeddingInput.ts";
|
||||
|
||||
export const embeddingTokenArraySchema = z
|
||||
@@ -215,7 +212,9 @@ const jinaNativeMediaStringSchema = z.string().trim().min(1).superRefine(refineJ
|
||||
|
||||
function exactlyOneJinaMediaKey(value: Record<string, unknown>, key: string): boolean {
|
||||
if (isCanonicalEmbeddingItem(value)) return false;
|
||||
return JINA_NATIVE_MEDIA_KEYS.filter((mediaKey) => mediaKey in value).length === 1 && key in value;
|
||||
return (
|
||||
JINA_NATIVE_MEDIA_KEYS.filter((mediaKey) => mediaKey in value).length === 1 && key in value
|
||||
);
|
||||
}
|
||||
|
||||
const jinaTextDocSchema = z
|
||||
@@ -264,7 +263,9 @@ export const jinaNativeDocSchema = z.union([
|
||||
export const jinaMergedContentGroupSchema = z
|
||||
.object({
|
||||
content: z
|
||||
.array(z.union([jinaTextDocSchema, jinaImageDocSchema, jinaAudioDocSchema, jinaVideoDocSchema]))
|
||||
.array(
|
||||
z.union([jinaTextDocSchema, jinaImageDocSchema, jinaAudioDocSchema, jinaVideoDocSchema])
|
||||
)
|
||||
.min(1, "content must contain at least one chunk"),
|
||||
})
|
||||
.passthrough();
|
||||
@@ -330,9 +331,12 @@ export const geminiNativePartSchema = z
|
||||
fileData: geminiFileDataSchema.optional(),
|
||||
})
|
||||
.passthrough()
|
||||
.refine((value) => isGeminiNativeEmbeddingItem(value) && !("parts" in value) && !("content" in value), {
|
||||
message: "Gemini part must be { text }, { inline_data }, or { file_data }",
|
||||
});
|
||||
.refine(
|
||||
(value) => isGeminiNativeEmbeddingItem(value) && !("parts" in value) && !("content" in value),
|
||||
{
|
||||
message: "Gemini part must be { text }, { inline_data }, or { file_data }",
|
||||
}
|
||||
);
|
||||
|
||||
export const geminiNativeContentSchema = z
|
||||
.object({
|
||||
@@ -445,7 +449,6 @@ export const v1ImageUpscaleSchema = z
|
||||
})
|
||||
.catchall(z.unknown());
|
||||
|
||||
|
||||
export const v1AudioSpeechSchema = z
|
||||
.object({
|
||||
model: modelIdSchema,
|
||||
@@ -565,71 +568,72 @@ export const v1SearchSchema = z.preprocess(
|
||||
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return raw;
|
||||
const o = { ...(raw as Record<string, unknown>) };
|
||||
if (o.provider === "x_search") o.provider = "x-search";
|
||||
if (o.provider === "x-search") o.search_type = "x";
|
||||
if (o.provider === "xquik" || o.provider === "xquik_search") o.provider = "xquik-search";
|
||||
if (o.provider === "x-search" || o.provider === "xquik-search") o.search_type = "x";
|
||||
return o;
|
||||
},
|
||||
z
|
||||
.object({
|
||||
// Core
|
||||
query: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Query is required")
|
||||
.max(500, "Query must be 500 characters or fewer"),
|
||||
// Not a z.enum: the runtime catalog (SEARCH_PROVIDERS + SEARCH_PROVIDER_ALIASES in
|
||||
// open-sse/config/searchRegistry.ts) is the source of truth via resolveSearchProvider(),
|
||||
// which already returns a named "Unknown search provider: <id>" error for bad ids (see
|
||||
// src/app/api/v1/search/route.ts). A hard-coded enum here would 400 before that check
|
||||
// ever runs, hiding the informative message behind a generic Zod failure (#10849).
|
||||
// Known catalog ids as of this writing: serper-search, brave-search, perplexity-search,
|
||||
// exa-search, tavily-search, firecrawl, google-pse-search, linkup-search, ollama-search,
|
||||
// searchapi-search, youcom-search, searxng-search, zai-search, jina-search, jina-ai,
|
||||
// jina, duckduckgo-free, x-search, x_search (plus short aliases resolved by
|
||||
// SEARCH_PROVIDER_ALIASES).
|
||||
provider: z.string().min(1).optional(),
|
||||
max_results: z.coerce.number().int().min(1).max(100).default(5),
|
||||
search_type: z.enum(["web", "news", "x"]).default("web"),
|
||||
offset: z.coerce.number().int().min(0).default(0),
|
||||
// Core
|
||||
query: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Query is required")
|
||||
.max(500, "Query must be 500 characters or fewer"),
|
||||
// Not a z.enum: the runtime catalog (SEARCH_PROVIDERS + SEARCH_PROVIDER_ALIASES in
|
||||
// open-sse/config/searchRegistry.ts) is the source of truth via resolveSearchProvider(),
|
||||
// which already returns a named "Unknown search provider: <id>" error for bad ids (see
|
||||
// src/app/api/v1/search/route.ts). A hard-coded enum here would 400 before that check
|
||||
// ever runs, hiding the informative message behind a generic Zod failure (#10849).
|
||||
// Known catalog ids as of this writing: serper-search, brave-search, perplexity-search,
|
||||
// exa-search, tavily-search, firecrawl, google-pse-search, linkup-search, ollama-search,
|
||||
// searchapi-search, youcom-search, searxng-search, zai-search, jina-search, jina-ai,
|
||||
// jina, duckduckgo-free, x-search, x_search, xquik-search, xquik (plus short aliases resolved by
|
||||
// SEARCH_PROVIDER_ALIASES).
|
||||
provider: z.string().min(1).optional(),
|
||||
max_results: z.coerce.number().int().min(1).max(100).default(5),
|
||||
search_type: z.enum(["web", "news", "x"]).default("web"),
|
||||
offset: z.coerce.number().int().min(0).default(0),
|
||||
|
||||
// Locale
|
||||
country: z.string().max(2).toUpperCase().optional(),
|
||||
language: z.string().min(2).max(5).optional(),
|
||||
time_range: z.enum(["any", "hour", "day", "week", "month", "year"]).optional(),
|
||||
// Locale
|
||||
country: z.string().max(2).toUpperCase().optional(),
|
||||
language: z.string().min(2).max(5).optional(),
|
||||
time_range: z.enum(["any", "hour", "day", "week", "month", "year"]).optional(),
|
||||
|
||||
// Content control
|
||||
content: z
|
||||
.object({
|
||||
snippet: z.boolean().default(true),
|
||||
full_page: z.boolean().default(false),
|
||||
format: z.enum(["text", "markdown"]).default("text"),
|
||||
max_characters: z.coerce.number().int().min(100).max(100000).optional(),
|
||||
})
|
||||
.optional(),
|
||||
// Content control
|
||||
content: z
|
||||
.object({
|
||||
snippet: z.boolean().default(true),
|
||||
full_page: z.boolean().default(false),
|
||||
format: z.enum(["text", "markdown"]).default("text"),
|
||||
max_characters: z.coerce.number().int().min(100).max(100000).optional(),
|
||||
})
|
||||
.optional(),
|
||||
|
||||
// Filters
|
||||
filters: z
|
||||
.object({
|
||||
include_domains: z.array(z.string().max(253)).max(20).optional(),
|
||||
exclude_domains: z.array(z.string().max(253)).max(20).optional(),
|
||||
safe_search: z.enum(["off", "moderate", "strict"]).optional(),
|
||||
})
|
||||
.optional(),
|
||||
// Filters
|
||||
filters: z
|
||||
.object({
|
||||
include_domains: z.array(z.string().max(253)).max(20).optional(),
|
||||
exclude_domains: z.array(z.string().max(253)).max(20).optional(),
|
||||
safe_search: z.enum(["off", "moderate", "strict"]).optional(),
|
||||
})
|
||||
.optional(),
|
||||
|
||||
// Answer synthesis (Phase 2 — returns null until implemented)
|
||||
synthesis: z
|
||||
.object({
|
||||
strategy: z.enum(["none", "auto", "provider", "internal"]).default("none"),
|
||||
model: z.string().optional(),
|
||||
max_tokens: z.coerce.number().int().min(1).max(4000).optional(),
|
||||
})
|
||||
.optional(),
|
||||
// Answer synthesis (Phase 2 — returns null until implemented)
|
||||
synthesis: z
|
||||
.object({
|
||||
strategy: z.enum(["none", "auto", "provider", "internal"]).default("none"),
|
||||
model: z.string().optional(),
|
||||
max_tokens: z.coerce.number().int().min(1).max(4000).optional(),
|
||||
})
|
||||
.optional(),
|
||||
|
||||
// Provider-specific passthrough
|
||||
provider_options: z.record(z.string(), z.unknown()).optional(),
|
||||
// Provider-specific passthrough
|
||||
provider_options: z.record(z.string(), z.unknown()).optional(),
|
||||
|
||||
// Strict mode — reject if provider doesn't support a requested filter
|
||||
strict_filters: z.boolean().default(false),
|
||||
})
|
||||
// Strict mode — reject if provider doesn't support a requested filter
|
||||
strict_filters: z.boolean().default(false),
|
||||
})
|
||||
.catchall(z.unknown())
|
||||
);
|
||||
|
||||
|
||||
@@ -138,6 +138,24 @@ export const updateSettingsSchema = z.object({
|
||||
// curated `tos` verdict is "avoid" (proxy/self-hosted use conflicts with the
|
||||
// provider's own terms) — a contractual concern, not an economic one.
|
||||
excludeTosAvoid: z.boolean().optional(),
|
||||
// Subscription-first routing tuning (`auto/subscription`, `auto/thrifty`).
|
||||
// TUNING ONLY — there is deliberately no `enabled` flag: both ids are opt-in
|
||||
// by being requested, and a toggle able to switch them off would leave
|
||||
// `auto/subscription` silently serving paid capacity under a name that
|
||||
// promises the opposite. See open-sse/services/autoCombo/subscriptionLadder.ts.
|
||||
subscriptionLadder: z
|
||||
.object({
|
||||
// Remaining-% at or below which a plan-included connection counts as
|
||||
// exhausted. Matches quotaPreflight.defaultThresholdPercent's default.
|
||||
exitCutoffPercent: z.number().min(0).max(100).optional(),
|
||||
// Remaining-% a connection must EXCEED to be re-admitted after having
|
||||
// been exhausted. The gap above exitCutoffPercent is the hysteresis band
|
||||
// that stops a connection hovering at the cutoff from oscillating.
|
||||
reentryMinRemainingPercent: z.number().min(0).max(100).optional(),
|
||||
// Per-rung spend ceiling in USD. 0 disables a rung outright.
|
||||
rungBudgetUsd: z.record(z.string().max(32), z.number().min(0)).optional(),
|
||||
})
|
||||
.optional(),
|
||||
hideHealthCheckLogs: z.boolean().optional(),
|
||||
hideEndpointCloudflaredTunnel: z.boolean().optional(),
|
||||
hideEndpointTailscaleFunnel: z.boolean().optional(),
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Integration tests for GET /api/search/providers — extended catalog (F4).
|
||||
*
|
||||
* Tests:
|
||||
* - Returns 20 items total (16 search + 4 fetch providers).
|
||||
* - Returns 22 items total (18 search + 4 fetch providers).
|
||||
* - Each item carries the correct `kind` field.
|
||||
* - Status reflects actual DB credential state:
|
||||
* - "configured" when an active, non-rate-limited connection exists.
|
||||
@@ -48,10 +48,10 @@ const route = await import("../../src/app/api/search/providers/route.ts");
|
||||
// Constants
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// 17 search-kind providers: serper, brave, perplexity, exa, tavily, firecrawl,
|
||||
// 18 search-kind providers: serper, brave, perplexity, exa, tavily, firecrawl,
|
||||
// google-pse, linkup, searchapi, youcom, searxng, ollama, zai, jina-search,
|
||||
// context7 (#11140), duckduckgo-free, x-search (registry open-sse/config/searchRegistry.ts).
|
||||
const EXPECTED_SEARCH_COUNT = 17;
|
||||
// context7 (#11140), duckduckgo-free, x-search, xquik-search.
|
||||
const EXPECTED_SEARCH_COUNT = 18;
|
||||
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 21 providers (17 search + 4 fetch)", async () => {
|
||||
test("search-providers-catalog: returns 22 providers (18 search + 4 fetch)", async () => {
|
||||
const req = await buildAuthRequest();
|
||||
const res = await route.GET(req);
|
||||
|
||||
@@ -361,6 +361,11 @@ test("search-providers-catalog: search providers have correct fields", async ()
|
||||
assert.ok(xSearch, "x-search must be in search providers");
|
||||
assert.equal(xSearch.kind, "search");
|
||||
assert.deepEqual(xSearch.searchTypes, ["x"]);
|
||||
|
||||
const xquikSearch = searchProviders.find((p: { id: string }) => p.id === "xquik-search");
|
||||
assert.ok(xquikSearch, "xquik-search must be in search providers");
|
||||
assert.equal(xquikSearch.kind, "search");
|
||||
assert.deepEqual(xquikSearch.searchTypes, ["x"]);
|
||||
});
|
||||
|
||||
test("search-providers-catalog: response validates against SearchProviderCatalogResponseSchema", async () => {
|
||||
|
||||
@@ -87,6 +87,11 @@
|
||||
"status": 400,
|
||||
"throws": true
|
||||
},
|
||||
"xquik-search": {
|
||||
"message": "Provider \"xquik-search\" is a search provider and does not support chat completions; use the /v1/search endpoint instead.",
|
||||
"status": 400,
|
||||
"throws": true
|
||||
},
|
||||
"youcom-search": {
|
||||
"message": "Provider \"youcom-search\" is a search provider and does not support chat completions; use the /v1/search endpoint instead.",
|
||||
"status": 400,
|
||||
|
||||
141
tests/unit/autoCombo/free-regime-not-read-by-predicate.test.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
/**
|
||||
* Follow-up to #6328 / #6495 / #6512 — the shared free-model predicate ignored
|
||||
* the catalog's own `freeType`, so entries a provider has since put behind a
|
||||
* paid key were still reported free.
|
||||
*
|
||||
* The catalog already records the regime of every entry, and
|
||||
* `strictZeroCostFilter` already reads it. These guards pin the same rule into
|
||||
* the predicate that `hidePaidModels` and `/v1/models` go through.
|
||||
*/
|
||||
import { test } from "vitest";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import {
|
||||
FREE_MODEL_BUDGETS,
|
||||
grantsFreeAccess,
|
||||
type FreeModelFreeType,
|
||||
} from "../../../open-sse/config/freeModelCatalog.ts";
|
||||
import { isFreeModel, providerHasFreeModels } from "../../../src/shared/utils/freeModels.ts";
|
||||
import { filterPaidOnlyCandidates } from "../../../open-sse/services/autoCombo/paidModelFilter.ts";
|
||||
import {
|
||||
evaluateCandidateConnections,
|
||||
findBudgetEntry,
|
||||
} from "../../../open-sse/services/autoCombo/strictZeroCostFilter.ts";
|
||||
|
||||
/** Catalogued under `pollinations` as `discontinued`: the provider moved them
|
||||
* behind an API key, and their `displayName` says so. */
|
||||
const DISCONTINUED = [
|
||||
"gemini",
|
||||
"gemini-fast",
|
||||
"midijourney",
|
||||
"midijourney-large",
|
||||
"claude-fast",
|
||||
"claude",
|
||||
"claude-large",
|
||||
];
|
||||
|
||||
/** Same provider, still keyless — the guard against over-filtering. */
|
||||
const STILL_FREE = ["openai", "openai-fast", "qwen-coder", "mistral", "deepseek"];
|
||||
|
||||
test("a model the catalog marks discontinued is not free", () => {
|
||||
for (const id of DISCONTINUED) {
|
||||
assert.equal(
|
||||
isFreeModel("pollinations", { id }),
|
||||
false,
|
||||
`pollinations/${id} is catalogued discontinued and must not qualify as free`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("the provider's still-free models are untouched", () => {
|
||||
for (const id of STILL_FREE) {
|
||||
assert.equal(
|
||||
isFreeModel("pollinations", { id }),
|
||||
true,
|
||||
`pollinations/${id} is catalogued keyless and must stay free`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("the provider itself still counts as having free models", () => {
|
||||
assert.equal(
|
||||
providerHasFreeModels("pollinations"),
|
||||
true,
|
||||
"pollinations keeps ten keyless entries; only the discontinued ones change"
|
||||
);
|
||||
});
|
||||
|
||||
test("hidePaidModels drops them from the auto/* candidate pool", () => {
|
||||
const discontinued = { provider: "pollinations", model: "claude" };
|
||||
const stillFree = { provider: "pollinations", model: "openai" };
|
||||
|
||||
assert.deepEqual(
|
||||
filterPaidOnlyCandidates([discontinued, stillFree], true),
|
||||
[stillFree],
|
||||
"an operator who asked not to route to paid models must not get one that needs a paid key"
|
||||
);
|
||||
assert.deepEqual(
|
||||
filterPaidOnlyCandidates([discontinued, stillFree], false),
|
||||
[discontinued, stillFree],
|
||||
"opt-in off stays an identity no-op"
|
||||
);
|
||||
});
|
||||
|
||||
test("no provider loses its free status", () => {
|
||||
const withFreeRegime = new Set(
|
||||
FREE_MODEL_BUDGETS.filter((m) => grantsFreeAccess(m.freeType)).map((m) => m.provider)
|
||||
);
|
||||
const lost = [...new Set(FREE_MODEL_BUDGETS.map((m) => m.provider))].filter(
|
||||
(p) => !withFreeRegime.has(p)
|
||||
);
|
||||
assert.deepEqual(
|
||||
lost,
|
||||
[],
|
||||
"no catalogued provider is discontinued across the board today; if one ever is, decide deliberately"
|
||||
);
|
||||
});
|
||||
|
||||
test("every regime is classified, with the expected verdict", () => {
|
||||
const expected: Record<FreeModelFreeType, boolean> = {
|
||||
"recurring-daily": true,
|
||||
"recurring-monthly": true,
|
||||
"recurring-credit": true,
|
||||
"recurring-uncapped": true,
|
||||
"one-time-initial": true,
|
||||
keyless: true,
|
||||
discontinued: false,
|
||||
};
|
||||
for (const [freeType, verdict] of Object.entries(expected)) {
|
||||
assert.equal(
|
||||
grantsFreeAccess(freeType as FreeModelFreeType),
|
||||
verdict,
|
||||
`${freeType} must be classified ${verdict}`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("the strict filter (G1c) excludes a discontinued entry, matching its prior literal", () => {
|
||||
const budgetEntry = findBudgetEntry({ provider: "pollinations", model: "claude" });
|
||||
assert.ok(budgetEntry, "discontinued pollinations/claude must be in the catalog");
|
||||
assert.equal(budgetEntry.freeType, "discontinued", "sanity: the entry this guard protects");
|
||||
|
||||
// A discontinued entry must be excluded by the strict filter regardless of
|
||||
// connection safety — it collapses the regime to "no free access" before any
|
||||
// quota lookup, exactly as the previous `freeType === "discontinued"` literal did.
|
||||
const excluded = evaluateCandidateConnections(
|
||||
{ provider: "pollinations", model: "claude", connectionId: "some-real-conn" },
|
||||
budgetEntry,
|
||||
() => ({
|
||||
status: "SAFE",
|
||||
remainingFreeAllowance: 1000,
|
||||
resetAt: null,
|
||||
checkedAt: new Date().toISOString(),
|
||||
}),
|
||||
{ minRemainingAllowance: 0, maxStateAgeMs: 1e9 }
|
||||
);
|
||||
assert.deepEqual(
|
||||
excluded,
|
||||
[],
|
||||
"a discontinued entry is excluded by the strict filter, independent of connection safety"
|
||||
);
|
||||
});
|
||||
407
tests/unit/autoCombo/subscription-ladder.test.ts
Normal file
@@ -0,0 +1,407 @@
|
||||
/**
|
||||
* Subscription-first routing — regression guard for `connectionBilling.ts` and
|
||||
* `subscriptionLadder.ts`, wired into
|
||||
* `open-sse/services/autoCombo/virtualFactory.ts::createVirtualAutoComboFromPrepared`
|
||||
* for the `auto/subscription` and `auto/thrifty` ids.
|
||||
*
|
||||
* Pure and dependency-light by design, mirroring
|
||||
* `strict-zero-cost-filter.test.ts`: every side-effecting dependency (live
|
||||
* quota state, connection auth types, the economic tier resolver, the billing
|
||||
* catalog) is injected, so nothing here touches the DB, the network, or global
|
||||
* state.
|
||||
*/
|
||||
import { test } from "vitest";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import type { ConnectionBillingEntry } from "@omniroute/open-sse/config/connectionBillingCatalog.ts";
|
||||
import {
|
||||
classifyConnectionBilling,
|
||||
isOverageSafe,
|
||||
isPlanIncluded,
|
||||
} from "@omniroute/open-sse/services/autoCombo/connectionBilling.ts";
|
||||
import { SYNTHETIC_NOAUTH_CONNECTION_ID } from "@omniroute/open-sse/services/autoCombo/resilienceCandidateFilter.ts";
|
||||
import {
|
||||
RUNG_ORDER,
|
||||
assignRung,
|
||||
clampCooldownToReset,
|
||||
filterSubscriptionOnlyCandidates,
|
||||
isQuotaUsable,
|
||||
isStateStaleForReset,
|
||||
orderPoolByRung,
|
||||
type LadderCandidate,
|
||||
type LadderOptions,
|
||||
} from "@omniroute/open-sse/services/autoCombo/subscriptionLadder.ts";
|
||||
import type { FreeAccessState } from "@omniroute/open-sse/services/autoCombo/strictZeroCostFilter.ts";
|
||||
|
||||
const NOW = Date.parse("2026-08-22T12:00:00.000Z");
|
||||
|
||||
/** Synthetic catalog — never the real one, so these tests keep passing when
|
||||
* the curated entries are edited (the autodiscovery contract). */
|
||||
const CATALOG: readonly ConnectionBillingEntry[] = [
|
||||
{
|
||||
provider: "planned",
|
||||
authType: "oauth",
|
||||
billing: "subscription",
|
||||
overage: "hard-stop",
|
||||
reason: "test fixture: plan-included, refuses past the allowance",
|
||||
},
|
||||
{
|
||||
provider: "planned",
|
||||
authType: "apikey",
|
||||
billing: "metered",
|
||||
overage: "meters-to-paid",
|
||||
reason: "test fixture: same provider, metered credential",
|
||||
},
|
||||
{
|
||||
provider: "overflowing",
|
||||
billing: "subscription",
|
||||
overage: "meters-to-paid",
|
||||
reason: "test fixture: plan-included but bills past the allowance",
|
||||
},
|
||||
{
|
||||
provider: "metered-co",
|
||||
billing: "metered",
|
||||
overage: "meters-to-paid",
|
||||
reason: "test fixture: pay per token",
|
||||
},
|
||||
];
|
||||
|
||||
function state(overrides: Partial<FreeAccessState> = {}): FreeAccessState {
|
||||
return {
|
||||
status: "SAFE",
|
||||
remainingFreeAllowance: 50,
|
||||
resetAt: null,
|
||||
checkedAt: new Date(NOW - 1000).toISOString(),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function options(overrides: Partial<LadderOptions> = {}): LadderOptions {
|
||||
return {
|
||||
enabled: true,
|
||||
resolveFreeAccessState: () => state(),
|
||||
resolveAuthType: () => "oauth",
|
||||
resolveEconomicTier: () => "cheap",
|
||||
maxStateAgeMs: 180_000,
|
||||
admitUnknownQuota: false,
|
||||
now: () => NOW,
|
||||
catalog: CATALOG,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function candidate(overrides: Partial<LadderCandidate> = {}): LadderCandidate {
|
||||
return {
|
||||
provider: "planned",
|
||||
model: "m1",
|
||||
connectionId: "c1",
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
// ── classification ──────────────────────────────────────────────────────────
|
||||
|
||||
test("the synthetic no-auth connection classifies as keyless without consulting the catalog", () => {
|
||||
const verdict = classifyConnectionBilling(
|
||||
{ provider: "metered-co", connectionId: SYNTHETIC_NOAUTH_CONNECTION_ID },
|
||||
CATALOG
|
||||
);
|
||||
assert.equal(verdict.billing, "keyless");
|
||||
assert.equal(isPlanIncluded(verdict), true);
|
||||
});
|
||||
|
||||
test("an authType-specific entry wins over the provider-wide one", () => {
|
||||
const oauth = classifyConnectionBilling(
|
||||
{ provider: "planned", authType: "oauth", connectionId: "c1" },
|
||||
CATALOG
|
||||
);
|
||||
const apikey = classifyConnectionBilling(
|
||||
{ provider: "planned", authType: "apikey", connectionId: "c2" },
|
||||
CATALOG
|
||||
);
|
||||
assert.equal(oauth.billing, "subscription");
|
||||
assert.equal(apikey.billing, "metered");
|
||||
});
|
||||
|
||||
test("a provider-wide entry applies when no authType entry matches", () => {
|
||||
const verdict = classifyConnectionBilling(
|
||||
{ provider: "overflowing", authType: "cookie", connectionId: "c1" },
|
||||
CATALOG
|
||||
);
|
||||
assert.equal(verdict.billing, "subscription");
|
||||
assert.equal(isOverageSafe(verdict), false);
|
||||
});
|
||||
|
||||
test("an uncurated provider is unknown — never silently plan-included", () => {
|
||||
const verdict = classifyConnectionBilling(
|
||||
{ provider: "brand-new", authType: "oauth", connectionId: "c1" },
|
||||
CATALOG
|
||||
);
|
||||
assert.equal(verdict.billing, "unknown");
|
||||
assert.equal(isPlanIncluded(verdict), false);
|
||||
assert.equal(isOverageSafe(verdict), false);
|
||||
});
|
||||
|
||||
test("rung assignment prefers billing class, falling back to the economic tier", () => {
|
||||
const opts = options({ resolveEconomicTier: () => "premium" });
|
||||
assert.equal(
|
||||
assignRung(
|
||||
{ provider: "planned", model: "m1" },
|
||||
{ provider: "planned", authType: "oauth" },
|
||||
opts
|
||||
),
|
||||
"subscription"
|
||||
);
|
||||
assert.equal(
|
||||
assignRung(
|
||||
{ provider: "metered-co", model: "m1" },
|
||||
{ provider: "metered-co", authType: "apikey" },
|
||||
opts
|
||||
),
|
||||
"premium"
|
||||
);
|
||||
assert.equal(
|
||||
assignRung(
|
||||
{ provider: "whatever", model: "m1" },
|
||||
{ provider: "whatever", connectionId: SYNTHETIC_NOAUTH_CONNECTION_ID },
|
||||
opts
|
||||
),
|
||||
"keyless"
|
||||
);
|
||||
});
|
||||
|
||||
// ── auto/subscription — fails closed ────────────────────────────────────────
|
||||
|
||||
test("disabled leaves the pool byte-identical (the opt-in contract)", () => {
|
||||
const pool = [candidate()];
|
||||
assert.equal(filterSubscriptionOnlyCandidates(pool, options({ enabled: false })), pool);
|
||||
assert.equal(orderPoolByRung(pool, options({ enabled: false })), pool);
|
||||
});
|
||||
|
||||
test("a plan-included, hard-stop connection with headroom is kept", () => {
|
||||
const pool = [candidate()];
|
||||
assert.deepEqual(filterSubscriptionOnlyCandidates(pool, options()), pool);
|
||||
});
|
||||
|
||||
test("a subscription that meters past the plan is excluded", () => {
|
||||
const pool = [candidate({ provider: "overflowing" })];
|
||||
assert.deepEqual(filterSubscriptionOnlyCandidates(pool, options()), []);
|
||||
});
|
||||
|
||||
test("a metered connection is excluded even on a provider that also sells a plan", () => {
|
||||
const pool = [candidate({ connectionId: "c2" })];
|
||||
const result = filterSubscriptionOnlyCandidates(
|
||||
pool,
|
||||
options({ resolveAuthType: () => "apikey" })
|
||||
);
|
||||
assert.deepEqual(result, []);
|
||||
});
|
||||
|
||||
test("keyless is not a subscription — auto/subscription means the plan you pay for", () => {
|
||||
const pool = [candidate({ provider: "freebie", connectionId: SYNTHETIC_NOAUTH_CONNECTION_ID })];
|
||||
assert.deepEqual(filterSubscriptionOnlyCandidates(pool, options()), []);
|
||||
});
|
||||
|
||||
test("an unverifiable quota reading fails closed", () => {
|
||||
const pool = [candidate()];
|
||||
assert.deepEqual(
|
||||
filterSubscriptionOnlyCandidates(pool, options({ resolveFreeAccessState: () => undefined })),
|
||||
[]
|
||||
);
|
||||
assert.deepEqual(
|
||||
filterSubscriptionOnlyCandidates(
|
||||
pool,
|
||||
options({ resolveFreeAccessState: () => state({ status: "UNKNOWN" }) })
|
||||
),
|
||||
[]
|
||||
);
|
||||
});
|
||||
|
||||
test("a stale quota reading fails closed even when it says SAFE", () => {
|
||||
const stale = state({ checkedAt: new Date(NOW - 10 * 60_000).toISOString() });
|
||||
assert.deepEqual(
|
||||
filterSubscriptionOnlyCandidates(
|
||||
[candidate()],
|
||||
options({ resolveFreeAccessState: () => stale })
|
||||
),
|
||||
[]
|
||||
);
|
||||
});
|
||||
|
||||
test("a multi-account candidate keeps only the connections proven safe", () => {
|
||||
const pool = [candidate({ connectionId: null, allowedConnectionIds: ["a", "b", "c"] })];
|
||||
const result = filterSubscriptionOnlyCandidates(
|
||||
pool,
|
||||
options({
|
||||
resolveFreeAccessState: (_provider, connectionId) =>
|
||||
connectionId === "b" ? state({ status: "EXHAUSTED", remainingFreeAllowance: 0 }) : state(),
|
||||
})
|
||||
);
|
||||
assert.equal(result.length, 1);
|
||||
assert.deepEqual(result[0].allowedConnectionIds, ["a", "c"]);
|
||||
});
|
||||
|
||||
test("a multi-account candidate with no safe connection is dropped, not emptied", () => {
|
||||
const pool = [candidate({ connectionId: null, allowedConnectionIds: ["a", "b"] })];
|
||||
const result = filterSubscriptionOnlyCandidates(
|
||||
pool,
|
||||
options({
|
||||
resolveFreeAccessState: () => state({ status: "EXHAUSTED", remainingFreeAllowance: 0 }),
|
||||
})
|
||||
);
|
||||
assert.deepEqual(result, []);
|
||||
});
|
||||
|
||||
// ── auto/thrifty — escalates ───────────────────────────────────────────────
|
||||
|
||||
test("rungs order plan-included capacity ahead of every paid rung", () => {
|
||||
const pool: LadderCandidate[] = [
|
||||
{ provider: "metered-co", model: "premium-model", connectionId: "p1" },
|
||||
{ provider: "metered-co", model: "cheap-model", connectionId: "c1" },
|
||||
{ provider: "planned", model: "plan-model", connectionId: "s1" },
|
||||
{ provider: "anything", model: "keyless-model", connectionId: SYNTHETIC_NOAUTH_CONNECTION_ID },
|
||||
];
|
||||
const result = orderPoolByRung(
|
||||
pool,
|
||||
options({
|
||||
admitUnknownQuota: true,
|
||||
resolveAuthType: (id) => (id === "s1" ? "oauth" : "apikey"),
|
||||
resolveEconomicTier: (_provider, model) => (model === "premium-model" ? "premium" : "cheap"),
|
||||
})
|
||||
);
|
||||
assert.deepEqual(
|
||||
result.map((c) => c.model),
|
||||
["plan-model", "keyless-model", "cheap-model", "premium-model"]
|
||||
);
|
||||
});
|
||||
|
||||
test("an exhausted plan connection steps aside so a paid rung can serve", () => {
|
||||
const pool: LadderCandidate[] = [
|
||||
{ provider: "planned", model: "plan-model", connectionId: "s1" },
|
||||
{ provider: "metered-co", model: "cheap-model", connectionId: "c1" },
|
||||
];
|
||||
const result = orderPoolByRung(
|
||||
pool,
|
||||
options({
|
||||
admitUnknownQuota: true,
|
||||
resolveAuthType: (id) => (id === "s1" ? "oauth" : "apikey"),
|
||||
resolveFreeAccessState: (_provider, connectionId) =>
|
||||
connectionId === "s1"
|
||||
? state({ status: "EXHAUSTED", remainingFreeAllowance: 0 })
|
||||
: undefined,
|
||||
})
|
||||
);
|
||||
assert.deepEqual(
|
||||
result.map((c) => c.model),
|
||||
["cheap-model"]
|
||||
);
|
||||
});
|
||||
|
||||
test("the ladder admits an unverifiable plan connection rather than paying on missing telemetry", () => {
|
||||
const pool = [candidate({ connectionId: "s1", model: "plan-model" })];
|
||||
const result = orderPoolByRung(
|
||||
pool,
|
||||
options({ admitUnknownQuota: true, resolveFreeAccessState: () => undefined })
|
||||
);
|
||||
assert.equal(result.length, 1);
|
||||
});
|
||||
|
||||
test("ordering is stable within a rung so the auto scorer is not reshuffled", () => {
|
||||
const pool: LadderCandidate[] = [
|
||||
{ provider: "metered-co", model: "first", connectionId: "a" },
|
||||
{ provider: "metered-co", model: "second", connectionId: "b" },
|
||||
{ provider: "metered-co", model: "third", connectionId: "c" },
|
||||
];
|
||||
const result = orderPoolByRung(
|
||||
pool,
|
||||
options({ admitUnknownQuota: true, resolveAuthType: () => "apikey" })
|
||||
);
|
||||
assert.deepEqual(
|
||||
result.map((c) => c.model),
|
||||
["first", "second", "third"]
|
||||
);
|
||||
});
|
||||
|
||||
test("a rung budgeted at zero is disabled outright", () => {
|
||||
const pool: LadderCandidate[] = [
|
||||
{ provider: "metered-co", model: "cheap-model", connectionId: "c1" },
|
||||
{ provider: "metered-co", model: "premium-model", connectionId: "p1" },
|
||||
];
|
||||
const result = orderPoolByRung(
|
||||
pool,
|
||||
options({
|
||||
admitUnknownQuota: true,
|
||||
resolveAuthType: () => "apikey",
|
||||
resolveEconomicTier: (_p, model) => (model === "premium-model" ? "premium" : "cheap"),
|
||||
rungBudgetUsd: { premium: 0 },
|
||||
})
|
||||
);
|
||||
assert.deepEqual(
|
||||
result.map((c) => c.model),
|
||||
["cheap-model"]
|
||||
);
|
||||
});
|
||||
|
||||
test("a paid rung drops out once its budget is spent, and is ungated without accounting", () => {
|
||||
const pool: LadderCandidate[] = [
|
||||
{ provider: "metered-co", model: "cheap-model", connectionId: "c1" },
|
||||
];
|
||||
const base = {
|
||||
admitUnknownQuota: true,
|
||||
resolveAuthType: () => "apikey",
|
||||
rungBudgetUsd: { cheap: 5 },
|
||||
};
|
||||
assert.deepEqual(orderPoolByRung(pool, options({ ...base, resolveRungSpendUsd: () => 5 })), []);
|
||||
assert.equal(orderPoolByRung(pool, options({ ...base, resolveRungSpendUsd: () => 1 })).length, 1);
|
||||
// No spend accounting available → the rung is ordered, never gated.
|
||||
assert.equal(
|
||||
orderPoolByRung(pool, options({ ...base, resolveRungSpendUsd: () => null })).length,
|
||||
1
|
||||
);
|
||||
});
|
||||
|
||||
// ── decision 3: returning to the plan after a reset ─────────────────────────
|
||||
|
||||
test("a cached reading whose own resetAt has passed is stale regardless of age", () => {
|
||||
assert.equal(
|
||||
isStateStaleForReset(state({ resetAt: new Date(NOW - 1).toISOString() }), NOW),
|
||||
true
|
||||
);
|
||||
assert.equal(
|
||||
isStateStaleForReset(state({ resetAt: new Date(NOW + 60_000).toISOString() }), NOW),
|
||||
false
|
||||
);
|
||||
assert.equal(isStateStaleForReset(state({ resetAt: null }), NOW), false);
|
||||
assert.equal(isStateStaleForReset(state({ resetAt: "not-a-date" }), NOW), false);
|
||||
assert.equal(isStateStaleForReset(undefined, NOW), false);
|
||||
});
|
||||
|
||||
test("hysteresis: re-entry needs more headroom than staying in did", () => {
|
||||
const opts = options({ exitCutoffPercent: 2, reentryMinRemainingPercent: 5 });
|
||||
const hovering = state({ remainingFreeAllowance: 3 });
|
||||
// Still in play at 3% remaining…
|
||||
assert.equal(isQuotaUsable(hovering, opts, false), true);
|
||||
// …but not enough to climb back after having dropped out.
|
||||
assert.equal(isQuotaUsable(hovering, opts, true), false);
|
||||
assert.equal(isQuotaUsable(state({ remainingFreeAllowance: 6 }), opts, true), true);
|
||||
});
|
||||
|
||||
test("a re-entry floor below the exit cutoff cannot create a re-entry gap", () => {
|
||||
const opts = options({ exitCutoffPercent: 10, reentryMinRemainingPercent: 1 });
|
||||
assert.equal(isQuotaUsable(state({ remainingFreeAllowance: 5 }), opts, true), false);
|
||||
});
|
||||
|
||||
test("cooldown is clamped to the upstream's own reset instant, never extended", () => {
|
||||
const resetIn60s = new Date(NOW + 60_000).toISOString();
|
||||
assert.equal(clampCooldownToReset(600_000, resetIn60s, NOW), 60_000);
|
||||
// Already shorter than the reset → untouched.
|
||||
assert.equal(clampCooldownToReset(10_000, resetIn60s, NOW), 10_000);
|
||||
// Absent / unparseable / already elapsed → untouched, never widened.
|
||||
assert.equal(clampCooldownToReset(600_000, null, NOW), 600_000);
|
||||
assert.equal(clampCooldownToReset(600_000, "nonsense", NOW), 600_000);
|
||||
assert.equal(clampCooldownToReset(600_000, new Date(NOW - 1).toISOString(), NOW), 600_000);
|
||||
});
|
||||
|
||||
test("rung order is the documented escalation order", () => {
|
||||
assert.deepEqual([...RUNG_ORDER], ["subscription", "keyless", "free", "cheap", "premium"]);
|
||||
});
|
||||
332
tests/unit/exclusive-session-observability.test.ts
Normal file
@@ -0,0 +1,332 @@
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import test from "node:test";
|
||||
|
||||
import {
|
||||
buildExclusiveDashboardSessions,
|
||||
mergeDashboardSessions,
|
||||
} from "../../src/lib/sessionObservability.ts";
|
||||
|
||||
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-session-observability-"));
|
||||
process.env.DATA_DIR = TEST_DATA_DIR;
|
||||
process.env.DISABLE_SQLITE_AUTO_BACKUP = "true";
|
||||
process.env.API_KEY_SECRET = "ab".repeat(32);
|
||||
|
||||
const core = await import("../../src/lib/db/core.ts");
|
||||
const apiKeys = await import("../../src/lib/db/apiKeys.ts");
|
||||
const leases = await import("../../src/lib/db/exclusiveConnectionLeases.ts");
|
||||
const providers = await import("../../src/lib/db/providers.ts");
|
||||
const sessionManager = await import("../../open-sse/services/sessionManager.ts");
|
||||
const usageHistory = await import("../../src/lib/usage/usageHistory.ts");
|
||||
const sessionsRoute = await import("../../src/app/api/sessions/route.ts");
|
||||
|
||||
const OWNER_A = "vlo_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
||||
const OWNER_B = "vlo_BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";
|
||||
const BASE_TIME = Date.parse("2026-08-24T12:00:00.000Z");
|
||||
|
||||
function at(offsetMs: number): string {
|
||||
return new Date(BASE_TIME + offsetMs).toISOString();
|
||||
}
|
||||
|
||||
function projectOfficialOccupancy(connectionIds: string[], now: string) {
|
||||
const occupancy = leases.getExclusiveLeaseOccupancy(connectionIds, now);
|
||||
return buildExclusiveDashboardSessions(new Set(occupancy.keys()), {}, []);
|
||||
}
|
||||
|
||||
test.afterEach(() => {
|
||||
sessionManager.clearSessions();
|
||||
usageHistory.clearPendingRequests();
|
||||
});
|
||||
|
||||
test.after(() => {
|
||||
core.resetDbInstance();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test("projects idle and active leases, distinct connections, legacy rows, and de-duplication", () => {
|
||||
const leaseRows = buildExclusiveDashboardSessions(
|
||||
new Set(["conn-idle", "conn-active"]),
|
||||
{ "conn-active": { "gpt-5.6-sol (codex)": 2 }, "conn-idle": { ignored: 0 } },
|
||||
[
|
||||
{
|
||||
sessionId: "legacy-duplicate-a",
|
||||
ageMs: 10_000,
|
||||
requestCount: 2,
|
||||
connectionId: "conn-active",
|
||||
},
|
||||
{
|
||||
sessionId: "legacy-duplicate-b",
|
||||
ageMs: 5_000,
|
||||
requestCount: 3,
|
||||
connectionId: "conn-active",
|
||||
},
|
||||
],
|
||||
new Map([
|
||||
["conn-active", "Managed Active"],
|
||||
["conn-idle", "Managed Idle"],
|
||||
])
|
||||
);
|
||||
|
||||
assert.equal(leaseRows.length, 2);
|
||||
assert.deepEqual(
|
||||
leaseRows.map((row) => [row.connectionId, row.active, row.requestCount]),
|
||||
[
|
||||
["conn-active", true, 5],
|
||||
["conn-idle", false, 0],
|
||||
]
|
||||
);
|
||||
assert.equal(leaseRows[0].connectionName, "Managed Active");
|
||||
|
||||
const displayed = mergeDashboardSessions(leaseRows, [
|
||||
{
|
||||
sessionId: "legacy-duplicate-a",
|
||||
ageMs: 10_000,
|
||||
requestCount: 2,
|
||||
connectionId: "conn-active",
|
||||
},
|
||||
{
|
||||
sessionId: "legacy-unmanaged",
|
||||
ageMs: 2_000,
|
||||
requestCount: 1,
|
||||
connectionId: "conn-unmanaged",
|
||||
},
|
||||
{
|
||||
sessionId: "legacy-unbound",
|
||||
ageMs: 1_000,
|
||||
requestCount: 1,
|
||||
connectionId: null,
|
||||
},
|
||||
]);
|
||||
|
||||
assert.deepEqual(
|
||||
displayed.map((row) => row.sessionId),
|
||||
["lease:conn-active", "lease:conn-idle", "legacy-unmanaged", "legacy-unbound"]
|
||||
);
|
||||
});
|
||||
|
||||
test("lease projection is a minimum privacy-safe observability payload", () => {
|
||||
const secretOwnerHash = "c".repeat(64);
|
||||
const rows = buildExclusiveDashboardSessions(
|
||||
new Set(["conn-private"]),
|
||||
{},
|
||||
[],
|
||||
new Map([["conn-private", "Private account"]])
|
||||
);
|
||||
const payload = JSON.stringify(rows);
|
||||
|
||||
assert.deepEqual(Object.keys(rows[0]).sort(), [
|
||||
"active",
|
||||
"ageMs",
|
||||
"connectionId",
|
||||
"connectionName",
|
||||
"leaseBacked",
|
||||
"requestCount",
|
||||
"sessionId",
|
||||
]);
|
||||
for (const forbidden of [
|
||||
secretOwnerHash,
|
||||
"leaseOwnerHash",
|
||||
"lease_owner_hash",
|
||||
"generation",
|
||||
"apiKeyId",
|
||||
"leaseOwnerId",
|
||||
"expiresAt",
|
||||
"IDLE",
|
||||
]) {
|
||||
assert.equal(payload.includes(forbidden), false, `payload must not contain ${forbidden}`);
|
||||
}
|
||||
});
|
||||
|
||||
test("official SQLite lease lifecycle remains visible through idle renew, release, and expiry", async () => {
|
||||
const connectionA = "11111111-1111-4111-8111-111111111111";
|
||||
const connectionB = "22222222-2222-4222-8222-222222222222";
|
||||
const managedKey = await apiKeys.createApiKey(
|
||||
"Lifecycle managed key",
|
||||
"0123456789abcdef",
|
||||
["lease:exclusive"],
|
||||
{ allowedConnections: [connectionA, connectionB] }
|
||||
);
|
||||
const managed = await apiKeys.getExclusiveLeaseConnectionIds();
|
||||
assert.equal(managed.has(connectionA), true);
|
||||
assert.equal(managed.has(connectionB), true);
|
||||
|
||||
const acquired = leases.acquireExclusiveConnectionLease({
|
||||
leaseOwnerId: OWNER_A,
|
||||
apiKeyId: managedKey.id,
|
||||
provider: "codex",
|
||||
connectionId: connectionA,
|
||||
now: at(0),
|
||||
ttlMs: 120_000,
|
||||
});
|
||||
assert.equal(acquired.kind, "ACQUIRED");
|
||||
if (acquired.kind !== "ACQUIRED") return;
|
||||
assert.equal(projectOfficialOccupancy([connectionA], at(30_000)).length, 1);
|
||||
assert.equal(projectOfficialOccupancy([connectionA], at(30_000))[0].active, false);
|
||||
|
||||
const renewed = leases.renewExclusiveConnectionLease({
|
||||
leaseOwnerId: OWNER_A,
|
||||
generation: acquired.lease.generation,
|
||||
apiKeyId: managedKey.id,
|
||||
now: at(60_000),
|
||||
ttlMs: 120_000,
|
||||
});
|
||||
assert.equal(renewed.kind, "RENEWED");
|
||||
if (renewed.kind !== "RENEWED") return;
|
||||
assert.equal(renewed.lease.generation, acquired.lease.generation);
|
||||
assert.equal(projectOfficialOccupancy([connectionA], at(150_000)).length, 1);
|
||||
assert.equal(
|
||||
leases.assertExclusiveConnectionLeaseFence({
|
||||
leaseOwnerId: OWNER_A,
|
||||
generation: acquired.lease.generation,
|
||||
apiKeyId: managedKey.id,
|
||||
connectionId: connectionA,
|
||||
now: at(150_000),
|
||||
}).kind,
|
||||
"VALID"
|
||||
);
|
||||
assert.equal(
|
||||
leases.releaseExclusiveConnectionLease({
|
||||
leaseOwnerId: OWNER_A,
|
||||
generation: acquired.lease.generation + 1,
|
||||
apiKeyId: managedKey.id,
|
||||
now: at(151_000),
|
||||
}).kind,
|
||||
"STALE"
|
||||
);
|
||||
assert.equal(projectOfficialOccupancy([connectionA], at(152_000)).length, 1);
|
||||
|
||||
assert.equal(
|
||||
leases.releaseExclusiveConnectionLease({
|
||||
leaseOwnerId: OWNER_A,
|
||||
generation: acquired.lease.generation,
|
||||
apiKeyId: managedKey.id,
|
||||
now: at(153_000),
|
||||
}).kind,
|
||||
"RELEASED"
|
||||
);
|
||||
assert.equal(projectOfficialOccupancy([connectionA], at(154_000)).length, 0);
|
||||
|
||||
const expiring = leases.acquireExclusiveConnectionLease({
|
||||
leaseOwnerId: OWNER_B,
|
||||
apiKeyId: managedKey.id,
|
||||
provider: "codex",
|
||||
connectionId: connectionB,
|
||||
now: at(200_000),
|
||||
ttlMs: 1_000,
|
||||
});
|
||||
assert.equal(expiring.kind, "ACQUIRED");
|
||||
if (expiring.kind !== "ACQUIRED") return;
|
||||
assert.equal(projectOfficialOccupancy([connectionB], at(200_500)).length, 1);
|
||||
assert.equal(leases.reconcileExpiredExclusiveConnectionLeases(at(202_000)), 1);
|
||||
assert.equal(projectOfficialOccupancy([connectionB], at(202_000)).length, 0);
|
||||
|
||||
const reacquired = leases.acquireExclusiveConnectionLease({
|
||||
leaseOwnerId: OWNER_B,
|
||||
apiKeyId: managedKey.id,
|
||||
provider: "codex",
|
||||
connectionId: connectionB,
|
||||
now: at(203_000),
|
||||
});
|
||||
assert.equal(reacquired.kind, "ACQUIRED");
|
||||
if (reacquired.kind !== "ACQUIRED") return;
|
||||
assert.equal(reacquired.lease.generation, expiring.lease.generation + 1);
|
||||
assert.equal(
|
||||
leases.assertExclusiveConnectionLeaseFence({
|
||||
leaseOwnerId: OWNER_B,
|
||||
generation: expiring.lease.generation,
|
||||
apiKeyId: managedKey.id,
|
||||
connectionId: connectionB,
|
||||
now: at(204_000),
|
||||
}).kind,
|
||||
"STALE"
|
||||
);
|
||||
assert.equal(
|
||||
leases.releaseExclusiveConnectionLease({
|
||||
leaseOwnerId: OWNER_B,
|
||||
generation: reacquired.lease.generation,
|
||||
apiKeyId: managedKey.id,
|
||||
now: at(205_000),
|
||||
}).kind,
|
||||
"RELEASED"
|
||||
);
|
||||
});
|
||||
|
||||
test("sessions API keeps legacy fields additive and decorates only in-flight leased work", async () => {
|
||||
const connection = await providers.createProviderConnection({
|
||||
provider: "codex",
|
||||
authType: "oauth",
|
||||
name: "Friendly Lease Account",
|
||||
accessToken: "synthetic-local-token",
|
||||
});
|
||||
const managedKey = await apiKeys.createApiKey(
|
||||
"Route managed key",
|
||||
"fedcba9876543210",
|
||||
["lease:exclusive"],
|
||||
{ allowedConnections: [connection.id] }
|
||||
);
|
||||
const acquired = leases.acquireExclusiveConnectionLease({
|
||||
leaseOwnerId: "vlo_CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC",
|
||||
apiKeyId: managedKey.id,
|
||||
provider: "codex",
|
||||
connectionId: connection.id,
|
||||
});
|
||||
assert.equal(acquired.kind, "ACQUIRED");
|
||||
if (acquired.kind !== "ACQUIRED") return;
|
||||
|
||||
sessionManager.touchSession("legacy-unmanaged", "legacy-connection");
|
||||
const idleResponse = await sessionsRoute.GET();
|
||||
const idleBody = (await idleResponse.json()) as Record<string, unknown>;
|
||||
assert.equal(idleResponse.status, 200);
|
||||
assert.equal(idleBody.count, 1);
|
||||
assert.equal(Array.isArray(idleBody.sessions), true);
|
||||
assert.equal(
|
||||
(idleBody.sessions as Array<{ sessionId: string }>)[0].sessionId,
|
||||
"legacy-unmanaged"
|
||||
);
|
||||
assert.deepEqual(idleBody.byApiKey, {});
|
||||
const idleLease = (idleBody.exclusiveSessions as Array<Record<string, unknown>>)[0];
|
||||
assert.equal(idleLease.connectionId, connection.id);
|
||||
assert.equal(idleLease.connectionName, "Friendly Lease Account");
|
||||
assert.equal(idleLease.active, false);
|
||||
for (const forbidden of ["leaseOwnerHash", "lease_owner_hash", "generation", "apiKeyId"]) {
|
||||
assert.equal(JSON.stringify(idleBody).includes(forbidden), false);
|
||||
}
|
||||
|
||||
usageHistory.trackPendingRequest("gpt-5.6-sol", "codex", connection.id, true);
|
||||
const activeBody = (await (await sessionsRoute.GET()).json()) as {
|
||||
exclusiveSessions: Array<{ active: boolean }>;
|
||||
};
|
||||
assert.equal(activeBody.exclusiveSessions[0].active, true);
|
||||
usageHistory.trackPendingRequest("gpt-5.6-sol", "codex", connection.id, false);
|
||||
|
||||
assert.equal(
|
||||
leases.releaseExclusiveConnectionLease({
|
||||
leaseOwnerId: "vlo_CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC",
|
||||
generation: acquired.lease.generation,
|
||||
apiKeyId: managedKey.id,
|
||||
}).kind,
|
||||
"RELEASED"
|
||||
);
|
||||
const releasedBody = (await (await sessionsRoute.GET()).json()) as {
|
||||
count: number;
|
||||
sessions: Array<{ sessionId: string }>;
|
||||
exclusiveSessions: unknown[];
|
||||
};
|
||||
assert.equal(releasedBody.count, 1);
|
||||
assert.equal(releasedBody.sessions[0].sessionId, "legacy-unmanaged");
|
||||
assert.deepEqual(releasedBody.exclusiveSessions, []);
|
||||
});
|
||||
|
||||
test("sessions route keeps raw lease SQL out of the API and sanitizes failures", () => {
|
||||
const route = fs.readFileSync(
|
||||
new URL("../../src/app/api/sessions/route.ts", import.meta.url),
|
||||
"utf8"
|
||||
);
|
||||
assert.match(route, /getExclusiveLeaseConnectionIds/);
|
||||
assert.match(route, /getExclusiveLeaseOccupancy/);
|
||||
assert.match(route, /getPendingRequests/);
|
||||
assert.match(route, /sanitizeErrorMessage\(error\)/);
|
||||
assert.doesNotMatch(route, /SELECT\s|exclusive_connection_leases/i);
|
||||
});
|
||||
@@ -16,7 +16,6 @@ const { __setTlsFetchOverrideForTesting: __setPplxTlsFetchOverride } =
|
||||
const { __setTlsFetchOverrideForTesting: __setGrokTlsFetchOverride } =
|
||||
await import("../../open-sse/services/grokTlsClient.ts");
|
||||
|
||||
|
||||
const originalFetch = globalThis.fetch;
|
||||
|
||||
test.afterEach(() => {
|
||||
@@ -1232,9 +1231,13 @@ test("local OpenAI-style providers validate without sending Authorization when a
|
||||
});
|
||||
|
||||
test("OpenAI-compatible validator covers /responses mode and final ping fallback", async () => {
|
||||
const calls = [];
|
||||
const calls: Array<{ url: string; method: string; body: string | undefined }> = [];
|
||||
globalThis.fetch = async (url, init = {}) => {
|
||||
calls.push({ url: String(url), method: init.method || "GET" });
|
||||
calls.push({
|
||||
url: String(url),
|
||||
method: init.method || "GET",
|
||||
body: typeof init.body === "string" ? init.body : undefined,
|
||||
});
|
||||
if (String(url).endsWith("/models")) {
|
||||
return new Response(JSON.stringify({ error: "no models" }), { status: 500 });
|
||||
}
|
||||
@@ -1282,6 +1285,11 @@ test("OpenAI-compatible validator covers /responses mode and final ping fallback
|
||||
calls.map((call) => call.url),
|
||||
["https://openai-like.example.com/v1/models", "https://openai-like.example.com/v1/responses"]
|
||||
);
|
||||
const responsesBody = JSON.parse(calls[1].body || "{}");
|
||||
assert.deepEqual(responsesBody.input, [{ role: "user", content: "test" }]);
|
||||
assert.equal(responsesBody.max_output_tokens, 1);
|
||||
assert.equal(responsesBody.messages, undefined);
|
||||
assert.equal(responsesBody.max_tokens, undefined);
|
||||
assert.equal(pingFallback.valid, true);
|
||||
assert.equal(pingFallback.error, null);
|
||||
});
|
||||
|
||||
@@ -36,9 +36,10 @@ test("SEARCH_PROVIDERS has all registered providers", () => {
|
||||
assert.ok(SEARCH_PROVIDERS["jina-search"], "jina-search should exist");
|
||||
assert.ok(SEARCH_PROVIDERS["duckduckgo-free"], "duckduckgo-free should exist");
|
||||
assert.ok(SEARCH_PROVIDERS["x-search"], "x-search should exist");
|
||||
// #11140: context7 (library-docs search) is the 17th registered provider
|
||||
assert.ok(SEARCH_PROVIDERS["xquik-search"], "xquik-search should exist");
|
||||
// #11140: context7 provides library-docs search
|
||||
assert.ok(SEARCH_PROVIDERS["context7"], "context7 should exist");
|
||||
assert.equal(Object.keys(SEARCH_PROVIDERS).length, 17);
|
||||
assert.equal(Object.keys(SEARCH_PROVIDERS).length, 18);
|
||||
});
|
||||
|
||||
test("duckduckgo-free config is a no-key, fallback-only provider", () => {
|
||||
@@ -172,11 +173,11 @@ test("zai-search config is correct", () => {
|
||||
|
||||
test("getAllSearchProviders returns flat list", () => {
|
||||
const all = getAllSearchProviders();
|
||||
// #11140: 17 providers with context7 registered
|
||||
assert.equal(all.length, 17);
|
||||
assert.equal(all.length, 18);
|
||||
assert.ok(all.some((p) => p.id === "duckduckgo-free"));
|
||||
assert.ok(all.some((p) => p.id === "jina-search"));
|
||||
assert.ok(all.some((p) => p.id === "x-search"));
|
||||
assert.ok(all.some((p) => p.id === "xquik-search"));
|
||||
assert.ok(all.some((p) => p.id === "serper-search"));
|
||||
assert.ok(all.some((p) => p.id === "brave-search"));
|
||||
assert.ok(all.some((p) => p.id === "perplexity-search"));
|
||||
@@ -420,6 +421,7 @@ test("v1SearchSchema accepts new search providers", async () => {
|
||||
"duckduckgo-free",
|
||||
"firecrawl",
|
||||
"x-search",
|
||||
"xquik-search",
|
||||
] as const;
|
||||
|
||||
for (const provider of providers) {
|
||||
|
||||
@@ -52,7 +52,7 @@ test("v1 search GET lists all search providers", async () => {
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(body.object, "list");
|
||||
assert.equal(body.data.length, 17);
|
||||
assert.equal(body.data.length, 18);
|
||||
assert.deepEqual(ids, [
|
||||
"serper-search",
|
||||
"brave-search",
|
||||
@@ -71,6 +71,7 @@ test("v1 search GET lists all search providers", async () => {
|
||||
"context7",
|
||||
"duckduckgo-free",
|
||||
"x-search",
|
||||
"xquik-search",
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
@@ -41,11 +41,13 @@ describe("S2 — agent-card topology sanitisation", () => {
|
||||
assert.equal(res.status, 200);
|
||||
const card = (await res.json()) as { url?: string; supportedInterfaces?: { url?: string }[] };
|
||||
assert.ok(card.url, "card must have a url");
|
||||
assert.ok(card.url.startsWith("https://gateway.example.com"), `expected gateway.example.com, got ${card.url}`);
|
||||
assert.equal(new URL(card.url).origin, "https://gateway.example.com", `expected gateway.example.com origin, got ${card.url}`);
|
||||
if (card.supportedInterfaces && card.supportedInterfaces.length > 0) {
|
||||
assert.ok(
|
||||
card.supportedInterfaces[0].url?.startsWith("https://gateway.example.com"),
|
||||
`interface URL should use dynamic origin, got ${card.supportedInterfaces[0].url}`
|
||||
const ifaceUrl = card.supportedInterfaces[0].url;
|
||||
assert.equal(
|
||||
ifaceUrl ? new URL(ifaceUrl).origin : undefined,
|
||||
"https://gateway.example.com",
|
||||
`interface URL should use dynamic origin, got ${ifaceUrl}`
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -62,7 +64,8 @@ describe("S2 — agent-card topology sanitisation", () => {
|
||||
const res = await mod.GET(request);
|
||||
assert.equal(res.status, 200);
|
||||
const card = (await res.json()) as { url?: string };
|
||||
assert.ok(card.url?.startsWith("https://custom.example.com"), `expected custom.example.com, got ${card.url}`);
|
||||
assert.ok(card.url, "card must have a url");
|
||||
assert.equal(new URL(card.url).origin, "https://custom.example.com", `expected custom.example.com origin, got ${card.url}`);
|
||||
});
|
||||
|
||||
it("agent.json derives URL from request.nextUrl.origin when OMNIROUTE_BASE_URL is unset", async () => {
|
||||
@@ -76,7 +79,8 @@ describe("S2 — agent-card topology sanitisation", () => {
|
||||
const res = await mod.GET(request);
|
||||
assert.equal(res.status, 200);
|
||||
const card = (await res.json()) as { url?: string };
|
||||
assert.ok(card.url?.startsWith("https://gateway.example.com"), `expected gateway.example.com, got ${card.url}`);
|
||||
assert.ok(card.url, "card must have a url");
|
||||
assert.equal(new URL(card.url).origin, "https://gateway.example.com", `expected gateway.example.com origin, got ${card.url}`);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||