Merge remote-tracking branch 'origin/release/v3.8.47' into tmp/implement-prs-6697-b

This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-07-09 16:56:37 -03:00
69 changed files with 2472 additions and 897 deletions

View File

@@ -211,6 +211,17 @@ NODE_ENV=production
# gives the correct fix instructions (podman unshare chown vs sudo chown).
CONTAINER_HOST=docker
# Container runtime override for skill sandboxing.
# Used by: src/lib/skills/sandbox.ts + src/lib/skills/containerProvider.ts
# Values: auto | docker | apple | wsl | orbstack | podman
# - auto: OS-aware auto-detect (apple/orbstack on macOS, wsl on Windows, podman on Linux)
# - apple: Apple Container (native OCI on macOS 26+)
# - wsl: WSL Container CLI (wslc.exe on Windows)
# - orbstack: OrbStack (high-perf Linux VM + docker shim on macOS)
# - podman: Podman (rootless, daemonless)
# - docker: Docker (default fallback)
SKILLS_SANDBOX_RUNTIME=auto
# ═══════════════════════════════════════════════════════════════════════════════
# 4. SECURITY & AUTHENTICATION
# ═══════════════════════════════════════════════════════════════════════════════
@@ -1821,6 +1832,15 @@ APP_LOG_TO_FILE=true
# SKILLS_SANDBOX_NETWORK_ENABLED=0
# SKILLS_ALLOWED_SANDBOX_IMAGES=
# Container runtime used by the skill sandbox. Accepted values:
# auto — pick the best installed runtime per host OS (default)
# docker — Docker Engine / Docker Desktop
# apple — Apple Container (macOS native, micro-VM)
# wsl — WSL Container (Windows native via wslc.exe)
# orbstack — OrbStack (high-perf Linux VM + docker shim on macOS)
# podman — Podman (rootless, daemonless)
# SKILLS_SANDBOX_RUNTIME=auto
# ═══════════════════════════════════════════════════════════════════════════════
# 25. TEST & E2E
# ═══════════════════════════════════════════════════════════════════════════════

View File

@@ -1,2 +1,5 @@
# Long reference tables are manually aligned; formatting the whole file causes noisy diffs.
docs/reference/ENVIRONMENT.md
# Dense auto-generated free-tier budget rows (one object per line) — prettier multi-line expand blows past file-size cap 800.
open-sse/config/freeModelCatalog.data.ts

10
.vscode/settings.json vendored
View File

@@ -48,11 +48,19 @@
"**/.build",
"**/dist",
"**/coverage",
"**/.worktrees"
"**/.worktrees",
"**/.claude/worktrees",
"**/electron",
"**/_references",
"**/_mono_repo",
"**/_tasks"
]
},
// Para esconder os diretórios gerados da árvore do Explorer, descomente:
// (MANTIDO comentado — o dono precisa ver _references/_mono_repo/_tasks na árvore.
// A performance é resolvida por watcherExclude + search.exclude + tsserver, sem
// precisar escondê-los do Explorer.)
// "files.exclude": {
// "**/.worktrees": true,
// "**/coverage": true,

View File

@@ -3,12 +3,12 @@
## Project
Unified AI proxy/router — route any LLM through one endpoint. Multi-provider support
with **237 provider entries** (OpenAI, Anthropic, Gemini, DeepSeek, Groq, xAI, Mistral, Fireworks,
with **248 provider entries** (OpenAI, Anthropic, Gemini, DeepSeek, Groq, xAI, Mistral, Fireworks,
Cohere, NVIDIA, Cerebras, Pollinations, Puter, Cloudflare AI, HuggingFace, DeepInfra,
SambaNova, Meta Llama API, Moonshot AI, AI21 Labs, Databricks, Snowflake, and many more)
with **MCP Server** (94 tools), **A2A v0.3 Protocol**, and **Electron desktop app**.
> **Live counts (v3.8.43)**: providers 237 · MCP tools 94 · MCP scopes 30 · A2A skills 6 ·
> **Live counts (v3.8.47)**: providers 248 · MCP tools 94 · MCP scopes 30 · A2A skills 6 ·
> open-sse services 134 · routing strategies 17 · auto-combo scoring factors 12 ·
> DB modules 95 · DB migrations 110 · base tables 17 · search providers 11 ·
> i18n locales 42. **Refresh with `npm run check:docs-all`.**
@@ -539,7 +539,7 @@ For any non-trivial change, read the matching deep-dive first:
| Repo navigation | [`docs/architecture/REPOSITORY_MAP.md`](docs/architecture/REPOSITORY_MAP.md) |
| Architecture | [`docs/architecture/ARCHITECTURE.md`](docs/architecture/ARCHITECTURE.md) |
| Engineering reference | [`docs/architecture/CODEBASE_DOCUMENTATION.md`](docs/architecture/CODEBASE_DOCUMENTATION.md) |
| Auto-Combo (12-factor, 17 strategies) | [`docs/routing/AUTO-COMBO.md`](docs/routing/AUTO-COMBO.md) |
| Auto-Combo (12-factor, 18 strategies) | [`docs/routing/AUTO-COMBO.md`](docs/routing/AUTO-COMBO.md) |
| Resilience (3 layers) | [`docs/architecture/RESILIENCE_GUIDE.md`](docs/architecture/RESILIENCE_GUIDE.md) |
| Skills | [`docs/frameworks/SKILLS.md`](docs/frameworks/SKILLS.md) |
| Memory | [`docs/frameworks/MEMORY.md`](docs/frameworks/MEMORY.md) |

View File

@@ -35,7 +35,7 @@ For full test matrix, see `CONTRIBUTING.md` → "Running Tests". For deep archit
## Project at a Glance
**OmniRoute** — unified AI proxy/router. One endpoint, 237 LLM providers, auto-fallback.
**OmniRoute** — unified AI proxy/router. One endpoint, 248 LLM providers, auto-fallback.
| Layer | Location | Purpose |
| ------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
@@ -72,7 +72,7 @@ Client → /v1/chat/completions (Next.js route)
API routes follow a consistent pattern: `Route → CORS preflight → Zod body validation → Optional auth (extractApiKey/isValidApiKey) → API key policy enforcement → Handler delegation (open-sse)`. No global Next.js middleware — interception is route-specific.
**Combo routing** (`open-sse/services/combo.ts`): 18 strategies (priority, weighted, fill-first, round-robin, P2C, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline). Each target calls `handleSingleModel()` which wraps `handleChatCore()` with per-target error handling and circuit breaker checks. The `fusion` strategy is the exception: it fans out to a panel of models in parallel, then a judge model synthesizes one final answer (`open-sse/services/fusion.ts`). See `docs/routing/AUTO-COMBO.md` for the 12-factor Auto-Combo scoring + the full strategy table and `docs/architecture/RESILIENCE_GUIDE.md` for the 3 resilience layers.
**Combo routing** (`open-sse/services/combo.ts`): 18 strategies (priority, weighted, fill-first, round-robin, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline). Each target calls `handleSingleModel()` which wraps `handleChatCore()` with per-target error handling and circuit breaker checks. The `fusion` strategy is the exception: it fans out to a panel of models in parallel, then a judge model synthesizes one final answer (`open-sse/services/fusion.ts`). See `docs/routing/AUTO-COMBO.md` for the 12-factor Auto-Combo scoring + the full strategy table and `docs/architecture/RESILIENCE_GUIDE.md` for the 3 resilience layers.
---

View File

@@ -6,7 +6,7 @@
# 🚀 OmniRoute — The Free AI Gateway
### Never stop coding. Connect every AI tool to **237 providers** — **90+ free** — through one endpoint.
### Never stop coding. Connect every AI tool to **248 providers** — **90+ free** — through one endpoint.
**Plug Claude Code, Codex, Cursor, Cline, Copilot & Antigravity into FREE Claude / GPT / Gemini. Auto-fallback.**
<br/>
@@ -21,16 +21,21 @@
<h3>
⭐ Star the repo if OMNIROUTE helped you save money and make your work easier. [![Stars](https://img.shields.io/github/stars/diegosouzapw/OmniRoute?style=social)](https://github.com/diegosouzapw/OmniRoute)
⭐ Star the repo if OMNIROUTE helped you save money and make your work easier.
</h3>
[![Stars](https://img.shields.io/github/stars/diegosouzapw/OmniRoute?style=social)](https://github.com/diegosouzapw/OmniRoute)
<a href="https://trendshift.io/repositories/23589" target="_blank"><img src="https://trendshift.io/api/badge/repositories/23589" alt="diegosouzapw%2FOmniRoute | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
[![Star History Rank](https://api.star-history.com/badge?repo=diegosouzapw/OmniRoute&theme=dark)](https://www.star-history.com/diegosouzapw/omniroute)
[![237 AI Providers](https://img.shields.io/badge/237-AI_Providers-6C5CE7?style=for-the-badge)](#-237-ai-providers--90-free)
[![90+ Free](https://img.shields.io/badge/90%2B-Free_Tiers-00B894?style=for-the-badge)](#-237-ai-providers--90-free)
</br>
[![248 AI Providers](https://img.shields.io/badge/248-AI_Providers-6C5CE7?style=for-the-badge)](#-248-ai-providers--90-free)
[![90+ Free](https://img.shields.io/badge/90%2B-Free_Tiers-00B894?style=for-the-badge)](#-248-ai-providers--90-free)
[![1.6B Free Tokens/mo](https://img.shields.io/badge/1.6B-Free_Tokens%2Fmo-00B894?style=for-the-badge)](docs/reference/FREE_TIERS.md)
[![Token Savings](https://img.shields.io/badge/up_to_95%25-Token_Savings-E17055?style=for-the-badge)](#%EF%B8%8F-save-1595-tokens--automatically)
[![17 Strategies](https://img.shields.io/badge/17-Routing_Strategies-0984E3?style=for-the-badge)](#-combos--the-flagship)
[![18 Strategies](https://img.shields.io/badge/18-Routing_Strategies-0984E3?style=for-the-badge)](#-combos--the-flagship)
[![$0 to start](https://img.shields.io/badge/%240-To_Start-FDCB6E?style=for-the-badge&logoColor=black)](#-quick-start)
<br/>
@@ -56,7 +61,7 @@
![Docker Pulls](https://img.shields.io/docker/pulls/diegosouzapw/omniroute?label=docker%20pulls&logo=docker&color=2496ED)
![Electron Downloads](https://img.shields.io/github/downloads/diegosouzapw/omniroute/total?style=flat&label=electron%20downloads&logo=electron&color=47848F)
[**🚀 Quick Start**](#-quick-start) • [**🎯 Combos**](#-combos--the-flagship) • [**🌐 Providers**](#-237-ai-providers--90-free) • [**🔌 CLI & MCP**](#-full-cli--a2a--mcp) • [**🗜️ Compression**](#%EF%B8%8F-save-1595-tokens--automatically) • [**🌍 Website**](https://omniroute.online)
[**🚀 Quick Start**](#-quick-start) • [**🎯 Combos**](#-combos--the-flagship) • [**🌐 Providers**](#-248-ai-providers--90-free) • [**🔌 CLI & MCP**](#-full-cli--a2a--mcp) • [**🗜️ Compression**](#%EF%B8%8F-save-1595-tokens--automatically) • [**🌍 Website**](https://omniroute.online)
[💥 The Promise](#-the-promise) • [🤔 Why](#-why-omniroute) • [🏆 What Sets Apart](#-what-sets-omniroute-apart) • [🤖 Compatible CLIs](#-compatible-clis--coding-agents) • [🖥️ Where It Runs](#%EF%B8%8F-where-omniroute-runs--anywhere) • [🔒 Private](#-private--local-first) • [🎬 In Action](#-omniroute-in-action) • [📚 Explore More](#-explore-more) • [📧 Support](#-support--community)
@@ -144,11 +149,11 @@
</div>
> One endpoint. **237 providers.** Never stop building — and let OmniRoute pick the cheapest one that works.
> One endpoint. **248 providers.** Never stop building — and let OmniRoute pick the cheapest one that works.
<table>
<tr>
<td width="33%" valign="top"><b>🚫 Never hit limits</b><br/><sub>Auto-fallback across 237 providers in milliseconds. Quota out? Next provider takes over — zero downtime.</sub></td>
<td width="33%" valign="top"><b>🚫 Never hit limits</b><br/><sub>Auto-fallback across 248 providers in milliseconds. Quota out? Next provider takes over — zero downtime.</sub></td>
<td width="33%" valign="top"><b>💸 Save up to 95% tokens</b><br/><sub>RTK + Caveman stacked compression cuts 1595% of eligible tokens (~89% avg on tool-heavy sessions).</sub></td>
<td width="33%" valign="top"><b>🆓 $0 to start</b><br/><sub>90+ providers with a free tier, 11 free <i>forever</i> (Kiro, Qoder, Pollinations, LongCat…). No card needed.</sub></td>
</tr>
@@ -308,7 +313,7 @@ Result: 4 layers of fallback = zero downtime
| Feature | OmniRoute | Other routers |
| -------------------------------------- | ------------------------------------------------------------------- | ------------- |
| 🌐 Providers | **237** | 20100 |
| 🌐 Providers | **248** | 20100 |
| 🆓 Free providers | **90+ (11 free forever)** | 15 |
| 🔀 Routing strategies | **17** (priority, weighted, cost-optimized, context-relay, fusion…) | 13 |
| 🗜️ Token compression | **RTK + Caveman stacked (1595%)** | None / 2040% |
@@ -346,7 +351,7 @@ Result: 4 layers of fallback = zero downtime
- **🛡️ Security** — a prompt-injection guard across every LLM route (backed by a red-team suite), plus a free DuckDuckGo last-resort web search. → [Guardrails](docs/security/GUARDRAILS.md)
- **🖼️ New endpoints** — `/v1/ocr` (Mistral OCR) and `/v1/audio/translations` (Whisper-style audio translation) round out the media API surface. → [API Reference](docs/reference/API_REFERENCE.md)
- **🌍 Deployment & ops** — reverse-proxy `basePath` deployment (`OMNIROUTE_BASE_PATH`, e.g. serving OmniRoute under `/omniroute/`), browser-language auto-detect on first visit, per-API-key device/connection tracking (IP+UA fingerprint, masked, in-memory only), root-less MITM cert trust for user-namespaced containers (`OMNIROUTE_NO_SUDO`), and server-side configured-only / available-only filters on the Free Provider Rankings page. → [Environment](docs/reference/ENVIRONMENT.md)
- **🤝 More providers & agents** — Cursor Cloud Agent (a 4th cloud agent), CodeBuddy CN (`copilot.tencent.com`), a Google Flow video-generation provider, new gateways **DGrid** and **Pioneer AI** (Fastino Labs), inbound **xAI Grok** translators plus **Grok Build (xAI)** with an OAuth import-token flow, GPT-4 / GPT-4o-mini on the GitHub Copilot provider, multi-model **Factory Droid**, **ZenMux Free** (session-cookie free tier), **Alibaba DashScope** text-to-video (`wan2.7-t2v`), a refreshed 237-provider catalog (OrcaRouter, Wafer AI, OpenAdapter, dit.ai, TokenRouter, …), Vertex AI media generation (speech/transcription/music/video), a first-class **Ollama** local-provider card, the **SenseNova** free Token Plan (chat + text-to-image), one-click account import from CLIProxyAPI (`~/.cli-proxy-api/`), **Claude Sonnet 5** wired end-to-end, a new provider wave (**Kenari**, **SumoPod**, **X5Lab**, **Charm Hyper**, **Nube.sh**, **b.ai**, **Qiniu**, **ModelScope**, **Augment/Auggie CLI**, **ClinePass**, NVIDIA NIM image generation), Codex account import from a raw ChatGPT access token, the **Requesty** gateway (BYOK, ~200 free req/day), **Yuanbao (web)** as a cookie-session provider (DeepSeek V3/R1 + Hunyuan), the **Zed** hosted LLM aggregator (OAuth), **Claude 5 Sonnet** on the Claude Web provider, Kiro **adaptive-thinking reasoning** surfaced as `reasoning_content`, and **bulk API-key add for Cloudflare Workers AI**. → [Providers](docs/reference/PROVIDER_REFERENCE.md)
- **🤝 More providers & agents** — Cursor Cloud Agent (a 4th cloud agent), CodeBuddy CN (`copilot.tencent.com`), a Google Flow video-generation provider, new gateways **DGrid** and **Pioneer AI** (Fastino Labs), inbound **xAI Grok** translators plus **Grok Build (xAI)** with an OAuth import-token flow, GPT-4 / GPT-4o-mini on the GitHub Copilot provider, multi-model **Factory Droid**, **ZenMux Free** (session-cookie free tier), **Alibaba DashScope** text-to-video (`wan2.7-t2v`), a refreshed 248-provider catalog (OrcaRouter, Wafer AI, OpenAdapter, dit.ai, TokenRouter, …), Vertex AI media generation (speech/transcription/music/video), a first-class **Ollama** local-provider card, the **SenseNova** free Token Plan (chat + text-to-image), one-click account import from CLIProxyAPI (`~/.cli-proxy-api/`), **Claude Sonnet 5** wired end-to-end, a new provider wave (**Kenari**, **SumoPod**, **X5Lab**, **Charm Hyper**, **Nube.sh**, **b.ai**, **Qiniu**, **ModelScope**, **Augment/Auggie CLI**, **ClinePass**, NVIDIA NIM image generation), Codex account import from a raw ChatGPT access token, the **Requesty** gateway (BYOK, ~200 free req/day), **Yuanbao (web)** as a cookie-session provider (DeepSeek V3/R1 + Hunyuan), the **Zed** hosted LLM aggregator (OAuth), **Claude 5 Sonnet** on the Claude Web provider, Kiro **adaptive-thinking reasoning** surfaced as `reasoning_content`, and **bulk API-key add for Cloudflare Workers AI**. → [Providers](docs/reference/PROVIDER_REFERENCE.md)
- **⚡ Local performance & infra** — a one-click local Redis launcher (`omniroute redis up`, plus a dashboard Redis panel), one-click **Cloudflare Workers** and **Deno Deploy** relay deployers wired into the proxy pool, a relay-backend selector (`OMNIROUTE_RELAY_BACKEND=ts|bifrost|auto`) so `/v1/relay` stays the stable surface while choosing the fastest backend internally, **Bifrost** (Go AI-gateway) and **Mux** (agent-orchestration daemon) promoted to first-class embedded/supervised services alongside 9Router/CLIProxyAPI, and **Webshare** added as a paid fourth source in the free-proxy provider framework. → [Embedded Services](docs/frameworks/EMBEDDED-SERVICES.md)
<br/>
@@ -389,11 +394,11 @@ Result: 4 layers of fallback = zero downtime
<div align="center">
# 🌐 237 AI Providers — 90+ Free
# 🌐 248 AI Providers — 90+ Free
</div>
> The most complete catalog of any open-source router: **237 providers**, **90+ with a free tier**, **11 free forever**.
> The most complete catalog of any open-source router: **248 providers**, **90+ with a free tier**, **11 free forever**.
<div align="center">
@@ -875,7 +880,7 @@ Compression: aggressive (~50%) → double your free quota · Cost: $0/mo
<br/>
**Routing:** 17 strategies · task-aware smart routing · thinking budget controls · wildcard routing · system prompt injection.
**Routing:** 18 strategies · task-aware smart routing · thinking budget controls · wildcard routing · system prompt injection.
**Compatibility:** OpenAI ↔ Claude ↔ Gemini ↔ Responses API · auto OAuth refresh (PKCE, 8 providers) · multi-account round-robin · Batch + Files API · live OpenAPI 3.0.
**Protocols:** MCP (95 tools, 3 transports, 30 scopes) · A2A (JSON-RPC 2.0, SSE, 6 skills) · ACP · cloud agents (Codex, Cursor, Devin, Jules).
**Plugins:** custom plugin marketplace (system-configured registry URL with SSRF-guarded fetch) · install / enable / disable · Notion + Obsidian knowledge-base integrations (WebDAV file server, vault search, note CRUD).
@@ -901,7 +906,7 @@ Compression: aggressive (~50%) → double your free quota · Cost: $0/mo
**Will I be charged by OmniRoute?** No — it's free, open-source software on your machine. You only pay paid providers directly. OmniRoute has no billing system.
**Are FREE providers really unlimited?** Mostly — Qoder, Pollinations, LongCat, and Cloudflare are free with no per-account credit cap. Kiro is free too but capped at ~50 credits/month per account. Stack multiple free providers in a combo and auto-fallback keeps you serving for $0.
**Will compression hurt quality?** No — it only compresses the **input**; code, URLs, JSON are always protected.
**Does it work where AI is blocked?** Yes — 3-level proxy + 1proxy marketplace reach all 237 providers.
**Does it work where AI is blocked?** Yes — 3-level proxy + 1proxy marketplace reach all 248 providers.
📖 [User Guide](docs/guides/USER_GUIDE.md) · [API Reference](docs/reference/API_REFERENCE.md) · [Environment Config](docs/reference/ENVIRONMENT.md)
@@ -1150,14 +1155,13 @@ gh release create v3.8.2 --title "v3.8.2" --generate-notes
## 📊 Stars
<a href="https://www.star-history.com/?repos=diegosouzapw%2Fomniroute&type=date&legend=top-left">
<a href="https://www.star-history.com/?repos=diegosouzapw%2FOmniRoute&type=date&legend=top-left">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=diegosouzapw/omniroute&type=date&theme=dark&legend=top-left" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=diegosouzapw/omniroute&type=date&legend=top-left" />
<img alt="Star History Chart" src="https://api.star-history.com/chart?repos=diegosouzapw/omniroute&type=date&legend=top-left" />
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=diegosouzapw/OmniRoute&type=date&theme=dark&legend=top-left&sealed_token=XP_ycEjv7s31p1edvhsMOXry51OWYsUjDRWjflSG7jQKRpO9hPGg7i_EHvwhI6QtrARTMH-YGjJhi8sumRYflEJD0DPlH_MMHjizhBYCX8fbHFrHEiNvVA" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=diegosouzapw/OmniRoute&type=date&legend=top-left&sealed_token=XP_ycEjv7s31p1edvhsMOXry51OWYsUjDRWjflSG7jQKRpO9hPGg7i_EHvwhI6QtrARTMH-YGjJhi8sumRYflEJD0DPlH_MMHjizhBYCX8fbHFrHEiNvVA" />
<img alt="Star History Chart" src="https://api.star-history.com/chart?repos=diegosouzapw/OmniRoute&type=date&legend=top-left&sealed_token=XP_ycEjv7s31p1edvhsMOXry51OWYsUjDRWjflSG7jQKRpO9hPGg7i_EHvwhI6QtrARTMH-YGjJhi8sumRYflEJD0DPlH_MMHjizhBYCX8fbHFrHEiNvVA" />
</picture>
</a>
</div>
<br/>

View File

@@ -1789,7 +1789,7 @@
},
"tests/unit/provider-models-route.test.ts": {
"@typescript-eslint/no-explicit-any": {
"count": 58
"count": 57
}
},
"tests/unit/provider-models-token-limits.test.ts": {

View File

@@ -5,6 +5,7 @@
"_rebaseline_2026_07_03_v3844_ipfilter_release_green": "testFrozen bumps: models-catalog-route 1507->1600, perplexity-web 959->999, route-edge-coverage 1234->1241 (last is my #5975 comment +7). v3.8.44 cycle drift measured on release tip 32e4c906e during the #6131/#5975 release-green rebaseline. Inherited from the merge burst (Quality Ratchet does not run on PR->release fast-gates). route-edge-coverage +7 is my #5975 test comment; the rest is parallel-session drift. Tighten via --update next cycle.",
"_rebaseline_2026_07_03_v3844_residual_release_green": "Residual file-size drift on tip 716041223: providerLimits.ts 955->982 + accountFallback.ts 1790->1864 (production god-files grown by parallel-session merges e.g. #6128; ideally DECOMPOSE not rebaseline, tracked as debt) + sse-auth.test.ts 1553->1600. None mine.",
"_comment": "Catraca de tamanho (check-file-size.mjs). frozen so pode encolher; arquivos novos <= cap. --update ratcheta.",
"_rebaseline_2026_07_09_pr6647_winget_claude_detect": "PR #6647 (enjoyer-hub, /implement-prs sync): cliRuntime.ts 1100->1110 (split('\\n').length metric; +10, was already exactly at the 1100 frozen cap). Adds the WinGet-installed Claude Code fallback path (%LOCALAPPDATA%\\Microsoft\\WinGet\\Packages\\Anthropic.ClaudeCode_Microsoft.Winget.Source_8wekyb3d8bbwe\\claude.exe) to getKnownToolPaths() alongside the two sibling Claude Code paths, so WinGet installs are auto-detected without CLI_CLAUDE_BIN. The package folder name (62 chars) forces Prettier's 100-char width to break the path.join call across the full 10-line multi-arg form used elsewhere in this same function for long paths; irreducible without changing the shared getKnownToolPaths() structure. Covered by the PR's own regression test (tests/unit/cli-runtime-detection.test.ts, win32-gated).",
"_rebaseline_2026_07_03_review_prs_release_green": "Release-green unblock (2026-07-03, /review-prs): the quality.yml fast-gates job was base-red for EVERY PR->release from growth inherited via already-merged PRs on the release tip — no offending PR branch left to fix in-place. Prod frozen raised: ApiManagerPageClient.tsx 3017->3058, OAuthModal.tsx 969->989, cliRuntime.ts 1090->1100, webProvidersA.ts 805->809. Test frozen raised: deepseek-web.test.ts 1081->1092. Real sizes (check-file-size.mjs reported). These stay frozen (cannot grow further); structural shrink tracked under decomposition roadmap #3501; the release captain's rebaseline-at-release supersedes this note. Bundled with the #5695 quick-start test regex fix (multi-line <Link> tolerance) in the same release-green PR.",
"_rebaseline_2026_07_02_5798_release_green": "Release-green unblock #5798 / PR #5896 (2026-07-02): the quality.yml fast-gates job was base-red for EVERY PR->release (whole queue failing), from growth inherited via already-merged PRs — no offending PR branch left to fix. Prod frozen raised: AddApiKeyModal.tsx 869->905, providerPageHelpers.ts 996->1021, RequestLoggerV2.tsx 1316->1553, src/sse/services/auth.ts 2403->2405, antigravity.ts 1806->1813, base.ts 1502->1536 (1533 inherited + 3 lines from this PR's own typecheck:core fix in resolveBaseUrl), advancedTools.ts 1118->1120, accountFallback.ts 1783->1790, openai-to-kiro.ts 842->853, openai-responses.ts 1035->1092, stream.ts 2710->2727; new-above-cap frozen: webProvidersA.ts 805, tokenHealthCheck.ts 830. Test frozen raised: cc-compatible-provider 1179->1217, translator-openai-to-kiro 999->1088, web-cookie-providers-new 827->845; new-above-cap: response-sanitizer.test.ts 906. These files remain frozen (cannot grow further); the release captain's rebaseline-at-release supersedes this note.",
"_rebaseline_2026_06_30_5552_flat_rate_cost": "Issue #5552 own growth: src/app/api/usage/analytics/route.ts 941->942 (+1 = the `flatRateAsZero: true` cost option at the existing computeUsageRowCost chokepoint, so subscription/cookie-web providers show $0 instead of an inflated per-token estimate in analytics). The flat-rate classifier (isFlatRateProvider + the provider-id set) lives in a new leaf src/lib/usage/flatRateProviders.ts (61 LOC, <cap) and the guard in computeCostFromPricing (src/lib/usage/costCalculator.ts, not baselined). Irreducible 1-line wiring at the analytics chokepoint; covered by tests/unit/flat-rate-cost-5552.test.ts.",
@@ -56,8 +57,8 @@
"_rebaseline_2026_06_20_4380_parse_once": "PR #4380 own growth: src/sse/handlers/chat.ts 1486->1491 (+5 = thread the once-parsed request body from the route guard into handleChat, replacing the duplicate re-parse). The reusable body accessor lives in the new src/sse/handlers/requestBody.ts (<cap). Thin wiring at the single entry chokepoint; not extractable. Covered by tests/unit/chat-request-body-parse-once-4380.test.ts.",
"_rebaseline_2026_06_20_escalated_api_airforce_live_discovery": "escalated cmqlvxg4o (WhatsApp): providers/[id]/models/route.ts 2586->2590 (+4 = one NAMED_OPENAI_STYLE_PROVIDERS Set entry `api-airforce` + a 3-line comment). Same fix shape as the provider-sweep rows (#4249/#4202/#3976): api-airforce carries a real live `https://api.airforce/v1/models` catalog but was left out of the sweep, so import served its stale hardcoded seed (grok-3/grok-2-1212/claude-3.7-sonnet …) — models that no longer exist upstream, so chat failed though the connection test still passed. The `<baseUrl>/models` probe (after stripping /chat/completions and a trailing /v1) resolves to https://api.airforce/v1/models; the registry seed stays as the offline fallback so import never breaks. Pure additive Set membership; not extractable (it IS the list). Covered by tests/unit/provider-sweep-live-discovery.test.ts. Structural shrink of this route tracked in #3789.",
"_rebaseline_2026_06_19_4313_harvest_x_sweep_reconcile": "RECONCILIACAO ao mergear #4313 (5 harvested features) APOS o provider-sweep #4324: valores MEDIDOS na arvore combinada release(com #4324)+#4313. route.ts 2580->2586 (sweep +19 NAMED_OPENAI_STYLE + #4313 +3 openadapter/dit/tokenrouter = uniao no Set, regioes disjuntas). providers.ts 3198->3242 (sweep 6 dead-provider marks + #4313 3 novas entries APIKEY_PROVIDERS). combos/page.tsx 4350->4385 (#4313 #3266 allowlist UI) e providers/page.tsx 1925->1927 (#4313 #4240 serviceKind state) — intocados pelo sweep, crescimento puro do #4313. Mesmo padrao release-volatil de medir-no-merge dos baselines de complexity/zizmor deste lote.",
"_rebaseline_2026_06_19_provider_sweep_merge_reconcile": "provider-model-sweep PR merge into release/v3.8.30: the sweep's route.ts growth (NAMED_OPENAI_STYLE_PROVIDERS +19 entries, base 2538->2564) and #4259's cloudflare parseResponse (2538->2554) are disjoint regions that both land, so the merged route.ts frozen value is reconciled to its measured size here. constants/providers.ts carries the sweep's 6 dead-provider deprecation marks on top of release. chatCore.ts stays at #4266's 5128 (sweep does not touch it).",
"_rebaseline_2026_06_19_provider_sweep_dead_providers": "provider-model-sweep Track C: src/shared/constants/providers.ts 3169->3198 (+29 = deprecated:true + riskNoticeVariant:\"deprecated\" + a deprecationReason for six providers the sweep confirmed dead — kluster/glhf/predibase/inclusionai/galadriel (DNS no longer resolves) + phind (API shut down 2026-01). Mirrors the existing qwen deprecation-flag pattern; pure additive metadata so the UI surfaces a deprecation notice instead of silently offering a non-working provider. Not extractable (it IS the per-provider constant data).",
"_rebaseline_2026_06_19_provider_sweep_merge_reconcile": "provider-model-sweep PR merge into release/v3.8.30: the sweep's route.ts growth (NAMED_OPENAI_STYLE_PROVIDERS +19 entries, base 2538->2564) and #4259's cloudflare parseResponse (2538->2554) are disjoint regions that both land, so the merged route.ts frozen value is reconciled to its measured size here. constants/providers.ts carries the sweep's dead-provider deprecation marks on top of release. chatCore.ts stays at #4266's 5128 (sweep does not touch it).",
"_rebaseline_2026_06_19_provider_sweep_dead_providers": "provider-model-sweep Track C: src/shared/constants/providers.ts 3169->3198 (+29 = deprecated:true + riskNoticeVariant:\"deprecated\" + a deprecationReason for providers the sweep confirmed dead — kluster/predibase/inclusionai/galadriel (DNS no longer resolves) + phind (API shut down 2026-01). Mirrors the existing qwen deprecation-flag pattern; pure additive metadata so the UI surfaces a deprecation notice instead of silently offering a non-working provider. Not extractable (it IS the per-provider constant data).",
"_rebaseline_2026_06_19_provider_sweep_live_discovery_2": "provider-model-sweep cont.: providers/[id]/models/route.ts 2548->2564 (+16 = twelve more NAMED_OPENAI_STYLE_PROVIDERS Set entries `crof`/`featherless-ai`/`ovhcloud`/`sambanova`/`orcarouter`/`uncloseai`/`opencode-go`/`baseten`/`hyperbolic`/`nebius`/`scaleway`/`together` + a 4-line comment). Same fix shape: GPU-cloud / aggregator marketplaces hosting large volatile OSS catalogs, each with a live `<baseUrl>/v1/models` endpoint the sweep probed (200 public or 401/403 = exists+keyed); live fetch keeps the catalog fresh, the registry seed stays as the offline fallback. Pure additive Set membership; not extractable. Covered by tests/unit/provider-sweep-live-discovery.test.ts.",
"_rebaseline_2026_06_19_provider_sweep_live_discovery": "provider-model-sweep own growth: providers/[id]/models/route.ts 2538->2548 (+10 = seven NAMED_OPENAI_STYLE_PROVIDERS Set entries `venice`/`deepinfra`/`wandb`/`pollinations`/`nscale`/`inference-net`/`moonshot` + a 3-line comment). Same fix shape as #4249 (vercel-ai-gateway) / #4202 (zenmux) / #3976 (llm7/byteplus): each is a keyed format-openai provider that exposes a real live `<baseUrl>/models` catalog (the sweep probed each: venice/deepinfra/pollinations/inference-net public 200, wandb/nscale/moonshot 401 = exists+keyed) but was unclassified by every live-fetch branch, so import served its small hardcoded seed instead of the upstream list, re-staling the catalog the sweep set out to fix. The `<baseUrl>/models` probe (after stripping /chat/completions and a trailing /v1) resolves to the endpoints pinned in tests/unit/provider-sweep-live-discovery.test.ts; the small registry seed stays as the offline fallback so import never breaks. Pure additive Set membership; not extractable (it IS the list). Structural shrink of this route tracked in #3789.",
"_rebaseline_2026_06_19_4266_pollinations_auth": "PR #4266 own growth: chatCore.ts 5125->5128 (+3 net, +4/-1 at the existing error-classify chokepoint ~line 3514 — preserve numeric HTTP status from executor-thrown errors and set authentication_error type for 401s instead of mapping all to 502). The reusable guard lives at the existing catch chokepoint; not extractable without hiding the error boundary. (Rebased onto release tip 5125 from the PR's original 5111 base.)",
@@ -255,7 +256,7 @@
"src/shared/constants/pricing.ts": 1662,
"src/shared/constants/providers.ts": 3276,
"src/shared/constants/sidebarVisibility.ts": 1198,
"src/shared/services/cliRuntime.ts": 1100,
"src/shared/services/cliRuntime.ts": 1110,
"src/shared/validation/schemas.ts": 2523,
"_rebaseline_2026_06_28_5275_correlation_id_extract": "Extraction of the safe CorrelationId subset of #5275 (hartmark) — request correlation id stored in call_logs (migration 109) and returned via the X-Correlation-Id response header, WITHOUT the combo/resilience or build/lazy-loading changes (those stay in #5275). Own growth: callLogs.ts 975->985 (correlation_id column on CallLogSummaryRow + read/map), usageHistory.ts 983->988 (correlationId metadata normalize), chat.ts 1575->1632 (withCorrelationId response wiring + combo-failure log carrying correlationId), chatHelpers.ts new 811 (withCorrelationId helper + reqId threading; was 791<cap pre-feature). Cohesive request/logging chokepoint wiring; structural shrink of chat.ts tracked in #3501.",
"src/sse/handlers/chat.ts": 1778,

View File

@@ -23,7 +23,7 @@ The v3.7.x → v3.8.0 cycle added zero-config auto routing, new providers, OAuth
- 🆕 **Z.AI provider** — new free-tier provider with quota labels
- 🎬 **KIE media expansion** — extended catalog including video generation models
- 🔐 **Windsurf + Devin CLI OAuth flows** (#2168) — end-to-end browser-based login
- 🆓 **9 new free providers** — LLM7, Lepton, Kluster, UncloseAI, BazaarLink, Completions, Enally, FreeTheAi, Command Code
- 🆓 **8 new free providers** — LLM7, Lepton, UncloseAI, BazaarLink, Completions, Enally, FreeTheAi, Command Code
- 🎯 **Manifest-aware tier routing W1W4** — provider manifests drive weighted tier selection
- 🎨 **Cursor full OpenAI parity** — tool calls, streaming, session management end-to-end
- 📊 **Cursor Pro plan usage** — quota & cycle data surfaced in the provider-limits dashboard

View File

@@ -30,7 +30,7 @@
[![50+ Free](https://img.shields.io/badge/50%2B-Free_Tiers-00B894?style=for-the-badge)](#-231-ai-providers--50-free)
[![1.6B Free Tokens/mo](https://img.shields.io/badge/1.6B-Free_Tokens%2Fmo-00B894?style=for-the-badge)](../../reference/FREE_TIERS.md)
[![Token Savings](https://img.shields.io/badge/up_to_95%25-Token_Savings-E17055?style=for-the-badge)](#%EF%B8%8F-save-1595-tokens--automatically)
[![17 Strategies](https://img.shields.io/badge/17-Routing_Strategies-0984E3?style=for-the-badge)](#-combos--the-flagship)
[![18 Strategies](https://img.shields.io/badge/18-Routing_Strategies-0984E3?style=for-the-badge)](#-combos--the-flagship)
[![$0 to start](https://img.shields.io/badge/%240-To_Start-FDCB6E?style=for-the-badge&logoColor=black)](#-quick-start)
<br/>

View File

@@ -30,7 +30,7 @@
[![50+ Free](https://img.shields.io/badge/50%2B-Free_Tiers-00B894?style=for-the-badge)](#-231-ai-providers--50-free)
[![1.6B Free Tokens/mo](https://img.shields.io/badge/1.6B-Free_Tokens%2Fmo-00B894?style=for-the-badge)](../../reference/FREE_TIERS.md)
[![Token Savings](https://img.shields.io/badge/up_to_95%25-Token_Savings-E17055?style=for-the-badge)](#%EF%B8%8F-save-1595-tokens--automatically)
[![17 Strategies](https://img.shields.io/badge/17-Routing_Strategies-0984E3?style=for-the-badge)](#-combos--the-flagship)
[![18 Strategies](https://img.shields.io/badge/18-Routing_Strategies-0984E3?style=for-the-badge)](#-combos--the-flagship)
[![$0 to start](https://img.shields.io/badge/%240-To_Start-FDCB6E?style=for-the-badge&logoColor=black)](#-quick-start)
<br/>

View File

@@ -990,6 +990,7 @@ Limits and safety knobs applied when the Skills framework (`src/lib/skills/`) ex
| `SKILLS_SANDBOX_NETWORK_ENABLED` | `false` | `src/lib/skills/builtins.ts` | Set `1`/`true` to allow outbound network from inside the sandbox. Defaults to **isolated** for safety. |
| `SKILLS_ALLOWED_SANDBOX_IMAGES` | _(empty)_ | `src/lib/skills/builtins.ts` | Comma-separated allowlist of container images permitted for sandbox execution. Empty means built-in default only. |
| `SKILLS_SANDBOX_DOCKER_IMAGE` | _(built-in default)_ | `src/lib/skills/` | Container image used when spawning a Docker-backed sandbox. Override to pin a custom hardened base image. |
| `SKILLS_SANDBOX_RUNTIME` | `auto` | `src/lib/skills/sandbox.ts`, `src/lib/skills/containerProvider.ts` | Container runtime for skill sandboxing: `auto` \| `docker` \| `apple` \| `wsl` \| `orbstack` \| `podman`. `auto` picks the best installed runtime per host OS (Apple Container/OrbStack on macOS, WSL Container on Windows, Podman on Linux), falling back to Docker. |
> [!CAUTION]
> Enabling `SKILLS_SANDBOX_NETWORK_ENABLED=true` opens an egress path from arbitrary skill code. Pair with `OUTBOUND_SSRF_GUARD_ENABLED=true` and a strict `CORS_ORIGIN`/proxy policy in shared deployments.

View File

@@ -23,7 +23,7 @@ lastUpdated: 2026-06-28
**Honest headline:** _OmniRoute aggregates **~1.6B documented free tokens per month** (up to ~2.1B in your first month with signup credits) across 40+ free-tier pools — plus a long tail of permanently-free, no-cap providers — and RTK + Caveman compression (1595% token savings) stretches that further._
> **Why this dropped from the previous ~1.94B.** The 2026-06-17 refresh is an honesty correction, not a loss: `gemini` is now pool-deduped (was inflated by counting each Flash variant separately, 462M → 60M), `cloudflare-ai` corrected to its real 10k-Neurons/day (122M → 30M), `doubao` reclassified as a one-time signup credit (not recurring), and shut-down tiers removed (`github-models` closed to new signups, `chutes`/`phind`/`kluster`/`glhf` discontinued). Partly offset by `llm7` (correct 5M/day → 150M) and new free providers (Kilo, OpenCode Zen, Z.AI GLM-Flash).
> **Why this dropped from the previous ~1.94B.** The 2026-06-17 refresh is an honesty correction, not a loss: `gemini` is now pool-deduped (was inflated by counting each Flash variant separately, 462M → 60M), `cloudflare-ai` corrected to its real 10k-Neurons/day (122M → 30M), `doubao` reclassified as a one-time signup credit (not recurring), and shut-down tiers removed (`github-models` closed to new signups, `chutes`/`phind`/`kluster` discontinued). Partly offset by `llm7` (correct 5M/day → 150M) and new free providers (Kilo, OpenCode Zen, Z.AI GLM-Flash).
Biggest **documented** contributors: `mistral` 1.00B, `llm7` 150M, `groq` 117M, `gemini` 60M, `cerebras` 30M, `cloudflare-ai` 30M, `sambanova` 30M. (`longcat` is excluded — its 10M LongCat-2.0 grant is a one-time, KYC-gated signup credit, not a recurring monthly budget.)
@@ -35,7 +35,7 @@ Biggest **documented** contributors: `mistral` 1.00B, `llm7` 150M, `groq` 117M,
A 50-agent web-research pass (official docs + last-7-days news, adversarially verified) refreshed the whole catalog. Highlights:
- **Removed / no free tier (2026):** `chutes` (free tier ended 2026-03), `phind` (company shut down 2026-01), `kluster` (sunset 2026-06-09 → MITO), `glhf` (beta ended), `gitlawb` + `gitlawb-gmi` (MiMo free revoked 2026-05-24, Nemotron promo ended 2026-06 — re-verified 2026-06-18), `aimlapi` (free tier paused — re-verified 2026-06-18), `yi` (Yi-Light retired, pay-as-you-go — re-verified 2026-06-18), `theoldllm` / `featherless-ai` (no current free tier). `iflytek` / `sparkdesk` stay listed but carry a ToS-caution note (Spark Lite is free; the ToS restricts proxy/relay use).
- **Removed / no free tier (2026):** `chutes` (free tier ended 2026-03), `phind` (company shut down 2026-01), `kluster` (sunset 2026-06-09 → MITO), `gitlawb` + `gitlawb-gmi` (MiMo free revoked 2026-05-24, Nemotron promo ended 2026-06 — re-verified 2026-06-18), `aimlapi` (free tier paused — re-verified 2026-06-18), `yi` (Yi-Light retired, pay-as-you-go — re-verified 2026-06-18), `theoldllm` / `featherless-ai` (no current free tier). `iflytek` / `sparkdesk` stay listed but carry a ToS-caution note (Spark Lite is free; the ToS restricts proxy/relay use).
- **GitHub Models** — closed to **new** customers on 2026-06-16; existing accounts keep API/playground access, so it stays in the catalog with a note (not removed).
- **Gemini** — `2.0 Flash` / `2.0 Flash-Lite` shut down 2026-06-01 and `2.5 Pro` left the free tier (2026-04); free tier is now **Flash-family only** (2.5/3/3.1/3.5 Flash + Gemma). The catalog now **pools** the Flash family (was inflated by counting each variant separately: 462M → 60M).
- **Corrected numbers:** `cloudflare-ai` 122M → **30M** (real 10k-Neurons/day), `doubao` reclassified as a one-time signup credit (not recurring), `llm7` 4M → **150M** (documented 5M tokens/day), `together` "-Free" endpoints discontinued → only the **$25** signup credit remains, `longcat` Preview ended + Flash models retired → **LongCat-2.0** only, reclassified as a one-time **10M**-token signup credit (KYC-gated, not recurring).
@@ -73,11 +73,11 @@ A 50-agent web-research pass (official docs + last-7-days news, adversarially ve
| `coze` | Coze ToS explicitly restricts use to "personal and non-commercial use" and prohibits renting, distributing, sublicensing, or reselling the service; a… |
| `duckduckgo-web` | Duck.ai ToS (duckduckgo.com/duckai/privacy-terms) explicitly prohibits "automated querying and developing or offering AI services" and circumventing … |
| `featherless-ai` | Individual plans explicitly restricted to "interactive use or proto-typing and experimentation by the purchaser" — inference resale and proxy use req… |
| `fireworks` | ToS explicitly prohibits proxy/intermediary use, API key transfers, and sublicensing (Sections 2.1 and 2.2(i)(j)); self-hosted personal proxies are n… |
| `friendliai` | ToS Section 8(e) and 8(f) explicitly prohibit using FriendliAI as a proxy or allowing third-party access on a standalone basis, and forbid reselling/… |
| `iflytek` | Section 2.4(3) of the iFlytek Spark LLM Service Agreement explicitly prohibits "using any automated or programmatic methods to extract data or output… |
| `kiro` | Kiro FAQ explicitly prohibits use with "OpenClaw and similar tools that leverage third-party harnesses" — a self-hosted AI proxy (like OmniRoute) rou… |
| `modal` | ToS Section 1.3 explicitly prohibits "rent, resell or otherwise allow any third party direct access to or use of the Service" — building a self-hoste… |
| `fireworks` | ToS explicitly prohibits proxy/intermediary use, API key transfers, and sublicensing (Sections 2.1 and 2.2(i)(j)); self-hosted personal proxies are n… |
| `friendliai` | ToS Section 8(e) and 8(f) explicitly prohibit using FriendliAI as a proxy or allowing third-party access on a standalone basis, and forbid reselling/… |
| `iflytek` | Section 2.4(3) of the iFlytek Spark LLM Service Agreement explicitly prohibits "using any automated or programmatic methods to extract data or output… |
| `kiro` | Kiro FAQ explicitly prohibits use with "OpenClaw and similar tools that leverage third-party harnesses" — a self-hosted AI proxy (like OmniRoute) rou… |
| `modal` | ToS Section 1.3 explicitly prohibits "rent, resell or otherwise allow any third party direct access to or use of the Service" — building a self-hoste… |
| `muse-spark-web` | Meta ToS explicitly prohibits automated access without prior permission, reverse engineering without written permission, and circumventing technologi… |
| `nlpcloud` | ToS explicitly prohibits "setting up a proxy or other device that allows others to access the Service through it" and grants only a non-transferable,… |
| `opencode` | ToS (Anomaly Innovations, Inc.) explicitly restricts use to "your own internal use, and not on behalf of or for the benefit of any third party" — ope… |
@@ -86,85 +86,82 @@ A 50-agent web-research pass (official docs + last-7-days news, adversarially ve
### ✅ Generally permissive — caution / ambiguous / ok (the rest)
| Provider | ToS | Note |
|---|---|---|
| `aimlapi` | ambiguous | ToS grants a non-exclusive use license but does not explicitly permit or prohibit self-hosted proxy or resale; no "pers… |
| `baichuan` | ambiguous | No explicit prohibition on self-hosted personal proxies found in publicly accessible docs; however, the M3 Plus free pl… |
| `bluesminds` | ambiguous | No explicit ToS clauses found regarding self-hosted proxying or resale; the pricing page focuses on feature/rate limits… |
| `bytez` | ambiguous | No explicit ToS page was accessible (404); no public evaluation-only or no-proxy clauses found in docs, but the platfor… |
| `doubao` | ambiguous | No explicit proxy/resale prohibition found in publicly indexed documentation; Volcengine is a developer-oriented cloud … |
| `gitlawb-gmi` | ambiguous | No explicit ToS clause found prohibiting self-hosted personal proxy use; the free Nemotron model carries an NVIDIA disc… |
| `inclusionai` | ambiguous | No explicit ToS found prohibiting proxy/self-hosted use, but the platform is operated by Ant Group (Chinese company) an… |
| `kluster` | ambiguous | ToS primarily covers website content rights and does not specifically address API proxy use, resale, or self-hosted pro… |
| `monsterapi` | ambiguous | MonsterAPI's ToS page (monsterapi.ai/terms-of-service) was unreachable during research; no specific proxy/resale/person… |
| `nous-research` | ambiguous | Nous Portal itself is an aggregator/proxy service; using it as a backend for another self-hosted proxy creates a proxy-… |
| `ollama-cloud` | ambiguous | ToS prohibits using the service "to develop competing products" but has no explicit ban on self-hosted personal proxies… |
| `stepfun` | ambiguous | No explicit prohibition on self-hosted personal proxy found, but the Step Plan ToS targets developers using specific co… |
| `api-airforce` | caution | ToS explicitly prohibits "building competing services without permission" and "credential sharing" — a self-hosted pers… |
| `arcee-ai` | caution | Free access is via OpenRouter's :free routing layer (not Arcee's direct API terms); OpenRouter ToS permits personal dev… |
| `baidu` | caution | ToS not explicitly reviewed for proxy/resale clauses, but platform requires real-name authentication (Chinese ID typica… |
| `baseten` | caution | ToS restricts use to "Customer's internal business purposes" and explicitly prohibits sublicensing, reselling, or allow… |
| `bazaarlink` | caution | ToS explicitly prohibits reselling or sublicensing API keys to third parties; a self-hosted personal proxy for personal… |
| `brave-search` | caution | ToS prohibits redistribution, resale, and sublicensing of search results; using the API to "replicate or attempt to rep… |
| `byteplus` | caution | Tokens are non-transferable and single-account only; no explicit proxy prohibition, but BytePlus reserves the right to … |
| `cerebras` | caution | ToS grants a non-exclusive, non-transferable, non-sublicensable right for personal or business use; prohibits resale, s… |
| `cloudflare-ai` | caution | Cloudflare Self-Serve ToS §2.2.1(j) prohibits using Services to "provide a virtual private network or other similar pro… |
| `cohere` | caution | Cohere explicitly prohibits trial keys for "production or commercial purposes"; a self-hosted personal proxy routing re… |
| `deepinfra` | caution | ToS allows legal commercial use broadly, but prohibits use "directly or indirectly competitive with any business of the… |
| `deepseek` | caution | Open Platform ToS (effective 2026-04-29) permits broad use including "derivative product development" and personal/comm… |
| `dify` | caution | Self-hosted single-user personal proxy is permitted under the modified Apache 2.0 license; however, multi-tenant deploy… |
| `exa-search` | caution | No explicit "no proxy" or "evaluation only" clauses found; Exa actively offers a reseller partner program allowing API … |
| `firecrawl` | caution | Cloud API ToS has no explicit personal-proxy prohibition found, but the open-source self-hosted version is AGPL-3.0 (re… |
| `gemini` | caution | ToS explicitly states the free tier is for "developers building with Google AI models for professional or business purp… |
| `github-models` | caution | GitHub's Acceptable Use Policy prohibits reselling/proxying the service; GitHub Models ToS delegates to each model's ho… |
| `glhf` | caution | ToS explicitly prohibits sharing account credentials or making the account available to any third party, which makes a … |
| `groq` | caution | Services Agreement §6.3 prohibits reselling, sublicensing, or distributing API access; §3.2 bars reselling/leasing acco… |
| `hackclub` | caution | Service is explicitly scoped to Hack Club teen members building projects/learning; no public ToS found explicitly permi… |
| `huggingchat` | caution | Hugging Face ToS does not explicitly ban personal self-hosted proxies, but supplemental terms (referenced but not fully… |
| `huggingface` | caution | ToS grants a limited license to access/use the service; the document does not explicitly permit or forbid a single-user… |
| `hyperbolic` | caution | ToS grants API access "solely for your own personal or internal business purposes" and explicitly prohibits licensing, … |
| `inference-net` | caution | ToS explicitly prohibits "sublicense, resell, distribute" and transferring API keys without written consent; a single-u… |
| `jina-ai` | caution | Free 10M tokens are explicitly non-commercial (CC-BY-NC 4.0 model license); a single-user personal proxy for personal L… |
| `jina-reader` | caution | ToS prohibits using outputs to build competing services and bans "automated methods to extract information via scraping… |
| `llm7` | caution | ToS positions the service as for "experimentation, development, and research"; no explicit ban on self-hosted personal … |
| `longcat` | caution | The API Platform Service Agreement (longcat.chat/platform/private/) permits commercial integration and self-hosted apps… |
| `mistral` | caution | Consumer ToS explicitly states APIs may only be used for "personal needs" and prohibits making API keys available to th… |
| `morph` | caution | ToS allows commercial use generally; self-hosted proxy deployments require explicit arrangement with sales. Section 18.… |
| `nebius` | caution | ToS (Section 5f) explicitly prohibits resale, redistribution, or offering the service "on a standalone basis" — a self-… |
| `nomic` | caution | ToS grants a non-exclusive, non-transferable API license; Section 6.b prohibits building a competitive service. Using t… |
| `novita` | caution | ToS prohibits resale and competing services but does not explicitly address personal self-hosted proxies; personal use … |
| `nscale` | caution | AUP prohibits "copy, modify, duplicate... frame, mirror, republish... distribute all or any part of the Nscale Platform… |
| `nvidia` | caution | Free tier is explicitly for prototyping/dev/research/evaluation only — production use (serving real end-users) requires… |
| `openrouter` | caution | ToS explicitly prohibits reselling API access or developing a competing service; single-user self-hosted personal proxy… |
| `pollinations` | caution | MIT License cited in API docs suggests liberal reuse; no explicit prohibition on self-hosted proxying found. However, u… |
| `predibase` | caution | Predibase is positioned as an enterprise fine-tuning/serving platform; the free trial is explicitly for exploration and… |
| `publicai` | caution | ToS (publicai.co/tc) designates services as "primarily for research and educational use"; no explicit proxy or resale p… |
| `puter` | caution | Puter ToS forbids using services for "commercial purpose" without written consent; a self-hosted personal proxy consumi… |
| `qoder` | caution | ToS page returned no readable content; Qoder is a coding IDE client (not a public API), and third-party proxy wrappers … |
| `reka` | caution | Business Terms prohibit sublicensing or distributing access to third parties; a personal single-user proxy is likely fi… |
| `sambanova` | caution | ToS Section 1.5(c) explicitly prohibits reselling, sublicensing, or making the service available to third parties; a se… |
| `sensenova` | caution | No explicit proxy or resale prohibition found in reviewed ToS, but the free tier is a promotional beta with no SLA, Sen… |
| `serper-search` | caution | ToS explicitly prohibits "mirroring materials on any other server as-is with no-value-added" — a simple pass-through pr… |
| `siliconflow` | caution | ToS (Clause 3.4(e)(f)(p)) explicitly prohibits making the service available to any third party, reselling/sublicensing,… |
| `sparkdesk` | caution | SparkDesk User Agreement grants only personal, non-commercial use rights; API Interface Policy prohibits automated data… |
| `tavily-search` | caution | ToS explicitly states the API "may not be transferred, assigned, shared, or otherwise made available to any third party… |
| `tencent` | caution | Tencent Cloud ToS explicitly prohibits sublicensing or reselling API access; a self-hosted personal proxy for personal … |
| `together` | caution | ToS Section 4.3(d) explicitly prohibits transferring, distributing, reselling, leasing, or offering the Services on a s… |
| `uncloseai` | caution | Personal proxy use is plausible but not explicitly permitted; ToS bans building "competing machine learning services wi… |
| `veoaifree-web` | caution | ToS explicitly bans automated bots or scripts running at "inhuman speeds" and prohibits copying the platform to create … |
| `vertex` | caution | Google Cloud Service Terms restrict resale to authorized resellers only (Section 14 requires a Reseller Agreement); a s… |
| `voyage-ai` | caution | ToS grants "personal, non-commercial use" for site content and prohibits credential/account sharing with third parties;… |
| `360ai` | unknown | ToS for developer API not publicly accessible without registration; access requires application approval which implies … |
| `chutes` | unknown | ToS page exists at chutes.ai/terms but content was not accessible via fetch; no explicit proxy/resale clauses found in … |
| `freemodel-dev` | unknown | The Terms of Service page (freemodel.dev/terms) returned only a header with no readable content via WebFetch; no clause… |
| `gitlawb` | unknown | No ToS or acceptable-use policy found; proxy/resale restrictions unknown — assume caution for self-hosted proxy use. |
| `liquid` | unknown | No hosted API exists to proxy; open-source model commercial use is free for orgs under $10M annual revenue. No self-hos… |
| `theoldllm` | unknown | No terms of service document was found on the site; proxying, resale, or self-hosted use policy is entirely undocumente… |
| `yi` | unknown | ToS not publicly accessible without login; no proxy/resale clauses could be reviewed. Self-hosted personal proxy use st… |
| `comfyui` | ok | GPL-3.0 open-source license explicitly permits self-hosted personal proxy use; Comfy Org ToS confirms commercial use of… |
| `scaleway` | ok | Scaleway's General Terms of Services are a standard commercial cloud agreement with no explicit prohibition on self-hos… |
| `sdwebui` | ok | AGPL-3.0 license: free to self-host for personal use with no restrictions on usage volume; a personal proxy using this … |
| `searxng-search` | ok | AGPL-3.0 open-source license explicitly permits self-hosted personal proxy use with no restriction on usage type, resal… |
| Provider | ToS | Note |
| ---------------- | --------- | ------------------------------------------------------------------------------------------------------------------------ |
| `aimlapi` | ambiguous | ToS grants a non-exclusive use license but does not explicitly permit or prohibit self-hosted proxy or resale; no "pers… |
| `baichuan` | ambiguous | No explicit prohibition on self-hosted personal proxies found in publicly accessible docs; however, the M3 Plus free pl… |
| `bluesminds` | ambiguous | No explicit ToS clauses found regarding self-hosted proxying or resale; the pricing page focuses on feature/rate limits… |
| `bytez` | ambiguous | No explicit ToS page was accessible (404); no public evaluation-only or no-proxy clauses found in docs, but the platfor… |
| `doubao` | ambiguous | No explicit proxy/resale prohibition found in publicly indexed documentation; Volcengine is a developer-oriented cloud … |
| `gitlawb-gmi` | ambiguous | No explicit ToS clause found prohibiting self-hosted personal proxy use; the free Nemotron model carries an NVIDIA disc… |
| `monsterapi` | ambiguous | MonsterAPI's ToS page (monsterapi.ai/terms-of-service) was unreachable during research; no specific proxy/resale/person… |
| `nous-research` | ambiguous | Nous Portal itself is an aggregator/proxy service; using it as a backend for another self-hosted proxy creates a proxy-… |
| `ollama-cloud` | ambiguous | ToS prohibits using the service "to develop competing products" but has no explicit ban on self-hosted personal proxies… |
| `stepfun` | ambiguous | No explicit prohibition on self-hosted personal proxy found, but the Step Plan ToS targets developers using specific co… |
| `api-airforce` | caution | ToS explicitly prohibits "building competing services without permission" and "credential sharing" — a self-hosted pers… |
| `arcee-ai` | caution | Free access is via OpenRouter's :free routing layer (not Arcee's direct API terms); OpenRouter ToS permits personal dev… |
| `baidu` | caution | ToS not explicitly reviewed for proxy/resale clauses, but platform requires real-name authentication (Chinese ID typica… |
| `baseten` | caution | ToS restricts use to "Customer's internal business purposes" and explicitly prohibits sublicensing, reselling, or allow… |
| `bazaarlink` | caution | ToS explicitly prohibits reselling or sublicensing API keys to third parties; a self-hosted personal proxy for personal… |
| `brave-search` | caution | ToS prohibits redistribution, resale, and sublicensing of search results; using the API to "replicate or attempt to rep… |
| `byteplus` | caution | Tokens are non-transferable and single-account only; no explicit proxy prohibition, but BytePlus reserves the right to … |
| `cerebras` | caution | ToS grants a non-exclusive, non-transferable, non-sublicensable right for personal or business use; prohibits resale, s… |
| `cloudflare-ai` | caution | Cloudflare Self-Serve ToS §2.2.1(j) prohibits using Services to "provide a virtual private network or other similar pro… |
| `cohere` | caution | Cohere explicitly prohibits trial keys for "production or commercial purposes"; a self-hosted personal proxy routing re… |
| `deepinfra` | caution | ToS allows legal commercial use broadly, but prohibits use "directly or indirectly competitive with any business of the… |
| `deepseek` | caution | Open Platform ToS (effective 2026-04-29) permits broad use including "derivative product development" and personal/comm… |
| `dify` | caution | Self-hosted single-user personal proxy is permitted under the modified Apache 2.0 license; however, multi-tenant deploy… |
| `exa-search` | caution | No explicit "no proxy" or "evaluation only" clauses found; Exa actively offers a reseller partner program allowing API … |
| `firecrawl` | caution | Cloud API ToS has no explicit personal-proxy prohibition found, but the open-source self-hosted version is AGPL-3.0 (re… |
| `gemini` | caution | ToS explicitly states the free tier is for "developers building with Google AI models for professional or business purp… |
| `github-models` | caution | GitHub's Acceptable Use Policy prohibits reselling/proxying the service; GitHub Models ToS delegates to each model's ho… |
| `groq` | caution | Services Agreement §6.3 prohibits reselling, sublicensing, or distributing API access; §3.2 bars reselling/leasing acco… |
| `hackclub` | caution | Service is explicitly scoped to Hack Club teen members building projects/learning; no public ToS found explicitly permi… |
| `huggingchat` | caution | Hugging Face ToS does not explicitly ban personal self-hosted proxies, but supplemental terms (referenced but not fully… |
| `huggingface` | caution | ToS grants a limited license to access/use the service; the document does not explicitly permit or forbid a single-user… |
| `hyperbolic` | caution | ToS grants API access "solely for your own personal or internal business purposes" and explicitly prohibits licensing, … |
| `inference-net` | caution | ToS explicitly prohibits "sublicense, resell, distribute" and transferring API keys without written consent; a single-u… |
| `jina-ai` | caution | Free 10M tokens are explicitly non-commercial (CC-BY-NC 4.0 model license); a single-user personal proxy for personal L… |
| `jina-reader` | caution | ToS prohibits using outputs to build competing services and bans "automated methods to extract information via scraping… |
| `llm7` | caution | ToS positions the service as for "experimentation, development, and research"; no explicit ban on self-hosted personal … |
| `longcat` | caution | The API Platform Service Agreement (longcat.chat/platform/private/) permits commercial integration and self-hosted apps… |
| `mistral` | caution | Consumer ToS explicitly states APIs may only be used for "personal needs" and prohibits making API keys available to th… |
| `morph` | caution | ToS allows commercial use generally; self-hosted proxy deployments require explicit arrangement with sales. Section 18.… |
| `nebius` | caution | ToS (Section 5f) explicitly prohibits resale, redistribution, or offering the service "on a standalone basis" — a self-… |
| `nomic` | caution | ToS grants a non-exclusive, non-transferable API license; Section 6.b prohibits building a competitive service. Using t… |
| `novita` | caution | ToS prohibits resale and competing services but does not explicitly address personal self-hosted proxies; personal use … |
| `nscale` | caution | AUP prohibits "copy, modify, duplicate... frame, mirror, republish... distribute all or any part of the Nscale Platform… |
| `nvidia` | caution | Free tier is explicitly for prototyping/dev/research/evaluation only — production use (serving real end-users) requires… |
| `openrouter` | caution | ToS explicitly prohibits reselling API access or developing a competing service; single-user self-hosted personal proxy… |
| `pollinations` | caution | MIT License cited in API docs suggests liberal reuse; no explicit prohibition on self-hosted proxying found. However, u… |
| `predibase` | caution | Predibase is positioned as an enterprise fine-tuning/serving platform; the free trial is explicitly for exploration and… |
| `publicai` | caution | ToS (publicai.co/tc) designates services as "primarily for research and educational use"; no explicit proxy or resale p… |
| `puter` | caution | Puter ToS forbids using services for "commercial purpose" without written consent; a self-hosted personal proxy consumi… |
| `qoder` | caution | ToS page returned no readable content; Qoder is a coding IDE client (not a public API), and third-party proxy wrappers … |
| `reka` | caution | Business Terms prohibit sublicensing or distributing access to third parties; a personal single-user proxy is likely fi… |
| `sambanova` | caution | ToS Section 1.5(c) explicitly prohibits reselling, sublicensing, or making the service available to third parties; a se… |
| `sensenova` | caution | No explicit proxy or resale prohibition found in reviewed ToS, but the free tier is a promotional beta with no SLA, Sen… |
| `serper-search` | caution | ToS explicitly prohibits "mirroring materials on any other server as-is with no-value-added" — a simple pass-through pr… |
| `siliconflow` | caution | ToS (Clause 3.4(e)(f)(p)) explicitly prohibits making the service available to any third party, reselling/sublicensing,… |
| `sparkdesk` | caution | SparkDesk User Agreement grants only personal, non-commercial use rights; API Interface Policy prohibits automated data… |
| `tavily-search` | caution | ToS explicitly states the API "may not be transferred, assigned, shared, or otherwise made available to any third party… |
| `tencent` | caution | Tencent Cloud ToS explicitly prohibits sublicensing or reselling API access; a self-hosted personal proxy for personal … |
| `together` | caution | ToS Section 4.3(d) explicitly prohibits transferring, distributing, reselling, leasing, or offering the Services on a s… |
| `uncloseai` | caution | Personal proxy use is plausible but not explicitly permitted; ToS bans building "competing machine learning services wi… |
| `veoaifree-web` | caution | ToS explicitly bans automated bots or scripts running at "inhuman speeds" and prohibits copying the platform to create … |
| `vertex` | caution | Google Cloud Service Terms restrict resale to authorized resellers only (Section 14 requires a Reseller Agreement); a s… |
| `voyage-ai` | caution | ToS grants "personal, non-commercial use" for site content and prohibits credential/account sharing with third parties;… |
| `360ai` | unknown | ToS for developer API not publicly accessible without registration; access requires application approval which implies … |
| `chutes` | unknown | ToS page exists at chutes.ai/terms but content was not accessible via fetch; no explicit proxy/resale clauses found in … |
| `freemodel-dev` | unknown | The Terms of Service page (freemodel.dev/terms) returned only a header with no readable content via WebFetch; no clause… |
| `gitlawb` | unknown | No ToS or acceptable-use policy found; proxy/resale restrictions unknown — assume caution for self-hosted proxy use. |
| `liquid` | unknown | No hosted API exists to proxy; open-source model commercial use is free for orgs under $10M annual revenue. No self-hos… |
| `theoldllm` | unknown | No terms of service document was found on the site; proxying, resale, or self-hosted use policy is entirely undocumente… |
| `yi` | unknown | ToS not publicly accessible without login; no proxy/resale clauses could be reviewed. Self-hosted personal proxy use st… |
| `comfyui` | ok | GPL-3.0 open-source license explicitly permits self-hosted personal proxy use; Comfy Org ToS confirms commercial use of… |
| `scaleway` | ok | Scaleway's General Terms of Services are a standard commercial cloud agreement with no explicit prohibition on self-hos… |
| `sdwebui` | ok | AGPL-3.0 license: free to self-host for personal use with no restrictions on usage volume; a personal proxy using this … |
| `searxng-search` | ok | AGPL-3.0 open-source license explicitly permits self-hosted personal proxy use with no restriction on usage type, resal… |
---
@@ -172,78 +169,77 @@ A 50-agent web-research pass (official docs + last-7-days news, adversarially ve
> Regenerated from the per-model catalog (`open-sse/config/freeModelCatalog.ts`), pool-deduped. Sorted by recurring steady tokens/mo. `uncapped*` = permanently free but no published token cap (rate/concurrency-limited) — real access, **not** summed into the headline. `—` = credit-only / keyless / not token-quantifiable.
| Provider | Free type | Steady tokens/mo | First-month credit | ToS | Models |
|---|---|---|---|---|---|
| `mistral` | recurring | ~1.00B | — | caution | 5 |
| `llm7` | recurring | ~150M | — | caution | 4 |
| `longcat` | one-time | — | 10M | caution | 1 |
| `gemini` | recurring | ~60M | — | caution | 6 |
| `cerebras` | recurring | ~30M | — | caution | 2 |
| `cloudflare-ai` | recurring | ~30M | — | caution | 6 |
| `api-airforce` | recurring | ~24M | — | caution | 7 |
| `ollama-cloud` | recurring | ~20M | — | ambiguous | 8 |
| `github-models` | recurring | ~18M | — | caution | 14 |
| `groq` | recurring | ~15M | — | caution | 5 |
| `inclusionai` | recurring | ~15M | — | ambiguous | 1 |
| `bluesminds` | recurring | ~7M | — | ambiguous | 22 |
| `sambanova` | recurring | ~6M | — | caution | 5 |
| `arcee-ai` | recurring | ~5M | — | caution | 1 |
| `bazaarlink` | recurring | ~4M | — | caution | 32 |
| `openrouter` | recurring | ~1M | — | caution | 1 |
| `cohere` | recurring | ~800K | — | caution | 6 |
| `huggingchat` | recurring | ~500K | — | caution | 4 |
| `morph` | recurring | ~400K | — | ok | 2 |
| `huggingface` | recurring | ~200K | — | caution | 6 |
| `kiro` | recurring | ~25K | — | avoid | 12 |
| `glm-cn` | uncapped | uncapped\* | ~20M | ok | 4 |
| `baidu` | uncapped | uncapped\* | — | caution | 1 |
| `kilo-gateway` | uncapped | uncapped\* | — | caution | 7 |
| `opencode-zen` | uncapped | uncapped\* | — | caution | 6 |
| `siliconflow` | uncapped | uncapped\* | — | caution | 10 |
| `tencent` | uncapped | uncapped\* | — | caution | 1 |
| `vertex` | signup credit | — | ~300M | caution | 10 |
| `agentrouter` | signup credit | — | ~200M | caution | 4 |
| `predibase` | signup credit | — | ~25M | caution | 1 |
| `together` | signup credit | — | ~25M | caution | 1 |
| `doubao` | signup credit | — | ~15M | ambiguous | 1 |
| `ai21` | signup credit | — | ~10M | avoid | 2 |
| `deepseek` | signup credit | — | ~5M | ok | 2 |
| `hyperbolic` | signup credit | — | ~5M | ok | 8 |
| `nscale` | signup credit | — | ~5M | caution | 6 |
| `bytez` | signup credit | — | ~1M | ambiguous | 3 |
| `deepinfra` | signup credit | — | ~1M | caution | 22 |
| `fireworks` | signup credit | — | ~1M | avoid | 10 |
| `nebius` | signup credit | — | ~1M | caution | 1 |
| `qoder` | signup credit | — | ~1M | caution | 14 |
| `scaleway` | signup credit | — | ~1M | ok | 6 |
| `novita` | signup credit | — | ~500K | caution | 1 |
| `agy` | keyless | — | — | avoid | 16 |
| `baichuan` | keyless | — | — | ambiguous | 1 |
| `blackbox` | keyless | — | — | avoid | 6 |
| `coze` | keyless | — | — | avoid | 1 |
| `duckduckgo-web` | keyless | — | — | avoid | 6 |
| `freemodel-dev` | keyless | — | — | unknown | 4 |
| `friendliai` | keyless | — | — | avoid | 2 |
| `hackclub` | keyless | — | — | caution | 3 |
| `iflytek` | keyless | — | — | avoid | 1 |
| `inference-net` | keyless | — | — | caution | 3 |
| `liquid` | keyless | — | — | unknown | 1 |
| `monsterapi` | keyless | — | — | ambiguous | 1 |
| `muse-spark-web` | keyless | — | — | avoid | 3 |
| `nlpcloud` | keyless | — | — | avoid | 1 |
| `nous-research` | keyless | — | — | ambiguous | 2 |
| `nvidia` | keyless | — | — | caution | 13 |
| `opencode` | keyless | — | — | avoid | 7 |
| `pollinations` | keyless | — | — | caution | 31 |
| `publicai` | keyless | — | — | caution | 3 |
| `puter` | keyless | — | — | caution | 33 |
| `qwen-web` | keyless | — | — | avoid | 3 |
| `reka` | keyless | — | — | caution | 2 |
| `sensenova` | keyless | — | — | caution | 1 |
| `sparkdesk` | keyless | — | — | caution | 1 |
| `stepfun` | keyless | — | — | ok | 1 |
| `t3-web` | keyless | — | — | avoid | 23 |
| `uncloseai` | keyless | — | — | caution | 3 |
| Provider | Free type | Steady tokens/mo | First-month credit | ToS | Models |
| ---------------- | ------------- | ---------------- | ------------------ | --------- | ------ |
| `mistral` | recurring | ~1.00B | — | caution | 5 |
| `llm7` | recurring | ~150M | — | caution | 4 |
| `longcat` | one-time | — | 10M | caution | 1 |
| `gemini` | recurring | ~60M | — | caution | 6 |
| `cerebras` | recurring | ~30M | — | caution | 2 |
| `cloudflare-ai` | recurring | ~30M | — | caution | 6 |
| `api-airforce` | recurring | ~24M | — | caution | 7 |
| `ollama-cloud` | recurring | ~20M | — | ambiguous | 8 |
| `github-models` | recurring | ~18M | — | caution | 14 |
| `groq` | recurring | ~15M | — | caution | 5 |
| `bluesminds` | recurring | ~7M | — | ambiguous | 22 |
| `sambanova` | recurring | ~6M | — | caution | 5 |
| `arcee-ai` | recurring | ~5M | — | caution | 1 |
| `bazaarlink` | recurring | ~4M | — | caution | 32 |
| `openrouter` | recurring | ~1M | — | caution | 1 |
| `cohere` | recurring | ~800K | — | caution | 6 |
| `huggingchat` | recurring | ~500K | — | caution | 4 |
| `morph` | recurring | ~400K | — | ok | 2 |
| `huggingface` | recurring | ~200K | — | caution | 6 |
| `kiro` | recurring | ~25K | — | avoid | 12 |
| `glm-cn` | uncapped | uncapped\* | ~20M | ok | 4 |
| `baidu` | uncapped | uncapped\* | — | caution | 1 |
| `kilo-gateway` | uncapped | uncapped\* | — | caution | 7 |
| `opencode-zen` | uncapped | uncapped\* | — | caution | 6 |
| `siliconflow` | uncapped | uncapped\* | — | caution | 10 |
| `tencent` | uncapped | uncapped\* | — | caution | 1 |
| `vertex` | signup credit | — | ~300M | caution | 10 |
| `agentrouter` | signup credit | — | ~200M | caution | 4 |
| `predibase` | signup credit | — | ~25M | caution | 1 |
| `together` | signup credit | — | ~25M | caution | 1 |
| `doubao` | signup credit | — | ~15M | ambiguous | 1 |
| `ai21` | signup credit | — | ~10M | avoid | 2 |
| `deepseek` | signup credit | — | ~5M | ok | 2 |
| `hyperbolic` | signup credit | — | ~5M | ok | 8 |
| `nscale` | signup credit | — | ~5M | caution | 6 |
| `bytez` | signup credit | — | ~1M | ambiguous | 3 |
| `deepinfra` | signup credit | — | ~1M | caution | 22 |
| `fireworks` | signup credit | — | ~1M | avoid | 10 |
| `nebius` | signup credit | — | ~1M | caution | 1 |
| `qoder` | signup credit | — | ~1M | caution | 14 |
| `scaleway` | signup credit | — | ~1M | ok | 6 |
| `novita` | signup credit | — | ~500K | caution | 1 |
| `agy` | keyless | — | — | avoid | 16 |
| `baichuan` | keyless | — | — | ambiguous | 1 |
| `blackbox` | keyless | — | — | avoid | 6 |
| `coze` | keyless | — | — | avoid | 1 |
| `duckduckgo-web` | keyless | — | — | avoid | 6 |
| `freemodel-dev` | keyless | — | — | unknown | 4 |
| `friendliai` | keyless | — | — | avoid | 2 |
| `hackclub` | keyless | — | — | caution | 3 |
| `iflytek` | keyless | — | — | avoid | 1 |
| `inference-net` | keyless | — | — | caution | 3 |
| `liquid` | keyless | — | — | unknown | 1 |
| `monsterapi` | keyless | — | — | ambiguous | 1 |
| `muse-spark-web` | keyless | — | — | avoid | 3 |
| `nlpcloud` | keyless | — | — | avoid | 1 |
| `nous-research` | keyless | — | — | ambiguous | 2 |
| `nvidia` | keyless | — | — | caution | 13 |
| `opencode` | keyless | — | — | avoid | 7 |
| `pollinations` | keyless | — | — | caution | 31 |
| `publicai` | keyless | — | — | caution | 3 |
| `puter` | keyless | — | — | caution | 33 |
| `qwen-web` | keyless | — | — | avoid | 3 |
| `reka` | keyless | — | — | caution | 2 |
| `sensenova` | keyless | — | — | caution | 1 |
| `sparkdesk` | keyless | — | — | caution | 1 |
| `stepfun` | keyless | — | — | ok | 1 |
| `t3-web` | keyless | — | — | avoid | 23 |
| `uncloseai` | keyless | — | — | caution | 3 |
---
@@ -283,18 +279,15 @@ A 50-agent web-research pass (official docs + last-7-days news, adversarially ve
- **`github-models`** — Catalog note "Free GPT-5, o-series, DeepSeek-R1, Llama 4, Grok 3" is directionally correct about model availability but omits the daily rate limits (50 RPD for high-tier models, 150 RPD for low-tier)…
- **`gitlawb`** — The shipped freeNote "Free tier available" is effectively stale. The original free MiMo access was removed in May 2026; the only remaining "free" option is a temporary promotional model (Nemotron 3 U…
- **`gitlawb-gmi`** — Partially still accurate — free tier exists but is now narrowed to a single model (Nemotron 3 Ultra) after MiMo free access was revoked in late May 2026. The shipped note "Free tier available" unders…
- **`glhf`** — The shipped freeNote ("Free tier for open-source model inference") is now stale. The free beta ended in January 2025; GLHF Chat is now a paid pay-as-you-go service. There is no ongoing recurring free…
- **`groq`** — The shipped freeNote "30 RPM / 14.4K RPD" is accurate only for llama-3.1-8b-instant. Most other models (including llama-3.3-70b-versatile) have a much lower 1K RPD cap. The note omits model-specific …
- **`hackclub`** — The "30+ models" count appears accurate and still matches. The core offering remains free for Hack Club members. No evidence of tightening — still "$0 ALWAYS FREE" per the homepage. The freeNote omit…
- **`huggingchat`** — The shipped freeNote ("Free LLM chat — no subscription required. Rate limits apply.") is partially accurate but significantly understates the restrictions. The free tier now operates on a hard $0.10/…
- **`huggingface`** — Significantly tightened. The shipped freeNote ("Free Inference API for thousands of models") implied unlimited/generous free access, but as of mid-2025 the free tier is capped at $0.10/month in recur…
- **`hyperbolic`** — Our shipped freeNote says "$1-5 trial credits on signup" — the $1 trial credit portion is accurate, but the "$5" figure refers to the minimum deposit required to unlock GPU rental (not free credits g…
- **`iflytek`** — Catalog says "Free Spark Lite models" — this is broadly accurate. However the current reality is more nuanced: only Spark Lite is free (the Max 100M token offer was a one-time promo, not recurring); …
- **`inclusionai`** — Our shipped freeNote says "Free Ling-2.6-flash model (262K context)" without specifying token limits. Reality is more specific: the free tier is 500K tokens/day (shared across all models), with a 2 Q…
- **`inference-net`** — The shipped freeNote states "$25 free credits on signup plus research grants." The current pricing page shows only $1 recurring monthly credits with no mention of a $25 signup bonus or research grant…
- **`jina-reader`** — Our shipped freeNote was "(none)", which is incorrect. Jina Reader has had a publicly documented free tier since launch: keyless access at 20 RPM plus a 10M one-time token grant with a free API key. …
- **`kiro`** — Catalog shipped freeNote "(none)" — but Kiro has a documented, perpetual free tier of 50 credits/month. The free tier existed since Kiro's public launch (pricing formalized ~October 2025). This is a …
- **`kluster`** — The $5 free credits on signup appears to still match. However, there is evidence of an additional permanent free tier (post-credit) with undocumented limits, which may represent an improvement over t…
- **`llm7`** — Rate limits have increased from the shipped freeNote (20 RPM / 100 req/hr → 40 RPM / 200 req/hr). The "no signup required" claim is now outdated — a free token from token.llm7.io is now required (tho…
- **`longcat`** — The public preview/beta ended and the Flash models were retired; only the GA `LongCat-2.0` remains. The free tier is a **one-time 10M-token grant** unlocked after account signup + **KYC verification** — it does **not** reset daily or monthly. Beyond the grant it is pay-as-you-go.
- **`mistral`** — The shipped freeNote ("Free Experiment tier: rate-limited access to all models") is directionally correct but understated. Current reality adds specific documented limits: 2 RPM, 500K TPM, 1B tokens/…

View File

@@ -1,16 +1,16 @@
---
title: "Provider Reference"
version: 3.8.43
lastUpdated: 2026-07-02
version: 3.8.47
lastUpdated: 2026-07-08
---
# 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-07-02
> **Last generated:** 2026-07-08
Total providers: **237**. See category breakdown below.
Total providers: **248**. See category breakdown below.
## Categories
@@ -31,287 +31,298 @@ Use the dashboard at `/dashboard/providers` to enable, configure, and test each
---
## OAuth Providers (20)
## OAuth Providers (21)
| ID | Alias | Name | Tags | Website | Notes |
|----|-------|------|------|---------|-------|
| `agy` | `agy` | Antigravity CLI | OAuth | [link](https://antigravity.google) | Import your Antigravity CLI (`agy`) login (paste/upload its token file), auto-detect a local CLI login, or sign in with Google. Shares the Antigravity backend (incl. Claude models). |
| `amazon-q` | `aq` | Amazon Q | OAuth | [link](https://aws.amazon.com/q/developer/) | Uses the same AWS Builder ID or imported refresh-token flow as Kiro, but keeps Amazon Q connections separate. |
| `antigravity` | — | Antigravity | OAuth | — | — |
| `claude` | `cc` | Claude Code | OAuth | — | — |
| `cline` | `cl` | Cline | OAuth | — | — |
| `codebuddy-cn` | `cbcn` | CodeBuddy CN | OAuth | [link](https://copilot.tencent.com) | Tencent CodeBuddy CN (copilot.tencent.com). Sign in via the official CLI device-code flow, or paste a direct API key (sent as Authorization: Bearer). Catalog: GLM / Kimi / MiniMax / DeepSeek / Hunyuan. |
| `codex` | `cx` | OpenAI Codex | OAuth | — | — |
| `cursor` | `cu` | Cursor IDE | OAuth | — | — |
| `devin-cli` | `dv` | Devin CLI (Official) | OAuth | [link](https://cli.devin.ai) | Requires the Devin CLI binary. Run `devin auth login` to authenticate, or provide your WINDSURF_API_KEY. Install: https://cli.devin.ai |
| `github` | `gh` | GitHub Copilot | OAuth | — | — |
| `gitlab-duo` | `gitlab-duo` | GitLab Duo | OAuth | [link](https://docs.gitlab.com/user/duo_agent_platform/code_suggestions/) | OAuth application with ai_features + read_user scopes. Configure GITLAB_DUO_OAUTH_CLIENT_ID and optionally GITLAB_DUO_OAUTH_CLIENT_SECRET on this OmniRoute instance. |
| `grok-cli` | `gc` | Grok Build | OAuth | — | Paste your ~/.grok/auth.json (or the JWT access token) from the Grok Build CLI; refresh_token is rotated automatically. |
| `kilocode` | `kc` | Kilo Code | OAuth | — | — |
| `kimi-coding` | `kmc` | Kimi Coding | OAuth | — | — |
| `kiro` | `kr` | Kiro AI | OAuth | — | Free tier: 50 credits/month (~25K100K tokens). ⚠️ Kiro ToS prohibits third-party proxy/harness use. |
| `qoder` | `if` | Qoder | OAuth | — | — |
| `qwen` | `qw` | Qwen Code | OAuth | — | ⚠️ **DEPRECATED.** Qwen OAuth free tier was discontinued on 2026-04-15. Use 'bailian-coding-plan', 'alibaba', 'alibaba-cn', or 'openrouter' provider with API key instead. |
| `trae` | `tr` | Trae | OAuth | [link](https://trae.ai) | Trae is an AI-native IDE by ByteDance (SOLO remote agent). Authorize via trae.ai in the popup, or sign in at solo.trae.ai and paste the Cloud-IDE-JWT (sent as 'Authorization: Cloud-IDE-JWT <token>', ~14-day lifetime) as the access token; web_id/biz_user_id/user_unique_id/scope/tenant/region propagate via providerSpecificData. No headless refresh for pasted tokens — re-paste on expiry. |
| `windsurf` | `ws` | Windsurf (Devin CLI) | OAuth | [link](https://windsurf.com) | In the Windsurf / VS Code IDE, open the command palette and run `Windsurf: Provide Auth Token` (or click the Jupyter "Get Windsurf Authentication Token" button), then copy the shown token and paste it here. Note: opening windsurf.com/show-auth-token directly only renders a "Redirecting" page — the IDE must initiate the flow (it adds a `?state=...` param) for the token to appear. |
| `zed` | `zd` | Zed IDE | OAuth | [link](https://zed.dev) | Zed stores LLM provider credentials (OpenAI, Anthropic, Google, Mistral, xAI) in the OS keychain. Use the Import button below to discover and import them automatically. |
| ID | Alias | Name | Tags | Website | Notes |
| -------------- | ------------ | -------------------- | ----- | ------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `agy` | `agy` | Antigravity CLI | OAuth | [link](https://antigravity.google) | Import your Antigravity CLI (`agy`) login (paste/upload its token file), auto-detect a local CLI login, or sign in with Google. Shares the Antigravity backend (incl. Claude models). |
| `amazon-q` | `aq` | Amazon Q | OAuth | [link](https://aws.amazon.com/q/developer/) | Uses the same AWS Builder ID or imported refresh-token flow as Kiro, but keeps Amazon Q connections separate. |
| `antigravity` | — | Antigravity | OAuth | — | — |
| `claude` | `cc` | Claude Code | OAuth | — | — |
| `cline` | `cl` | Cline | OAuth | — | — |
| `codebuddy-cn` | `cbcn` | CodeBuddy CN | OAuth | [link](https://copilot.tencent.com) | Tencent CodeBuddy CN (copilot.tencent.com). Sign in via the official CLI device-code flow, or paste a direct API key (sent as Authorization: Bearer). Catalog: GLM / Kimi / MiniMax / DeepSeek / Hunyuan. |
| `codex` | `cx` | OpenAI Codex | OAuth | — | — |
| `cursor` | `cu` | Cursor IDE | OAuth | — | — |
| `devin-cli` | `dv` | Devin CLI (Official) | OAuth | [link](https://cli.devin.ai) | Requires the Devin CLI binary. Run `devin auth login` to authenticate, or provide your WINDSURF_API_KEY. Install: https://cli.devin.ai |
| `github` | `gh` | GitHub Copilot | OAuth | — | — |
| `gitlab-duo` | `gitlab-duo` | GitLab Duo | OAuth | [link](https://docs.gitlab.com/user/duo_agent_platform/code_suggestions/) | OAuth application with ai_features + read_user scopes. Configure GITLAB_DUO_OAUTH_CLIENT_ID and optionally GITLAB_DUO_OAUTH_CLIENT_SECRET on this OmniRoute instance. |
| `grok-cli` | `gc` | Grok Build | OAuth | — | Paste your ~/.grok/auth.json (or the JWT access token) from the Grok Build CLI; refresh_token is rotated automatically. |
| `kilocode` | `kc` | Kilo Code | OAuth | — | — |
| `kimi-coding` | `kmc` | Kimi Coding | OAuth | — | — |
| `kiro` | `kr` | Kiro AI | OAuth | — | Free tier: 50 credits/month (~25K100K tokens). ⚠️ Kiro ToS prohibits third-party proxy/harness use. |
| `qoder` | `if` | Qoder | OAuth | — | — |
| `qwen` | `qw` | Qwen Code | OAuth | — | ⚠️ **DEPRECATED.** Qwen OAuth free tier was discontinued on 2026-04-15. Use 'bailian-coding-plan', 'alibaba', 'alibaba-cn', or 'openrouter' provider with API key instead. |
| `trae` | `tr` | Trae | OAuth | [link](https://trae.ai) | Trae is an AI-native IDE by ByteDance (SOLO remote agent). Authorize via trae.ai in the popup, or sign in at solo.trae.ai and paste the Cloud-IDE-JWT (sent as 'Authorization: Cloud-IDE-JWT <token>', ~14-day lifetime) as the access token; web_id/biz_user_id/user_unique_id/scope/tenant/region propagate via providerSpecificData. No headless refresh for pasted tokens — re-paste on expiry. |
| `windsurf` | `ws` | Windsurf (Devin CLI) | OAuth | [link](https://windsurf.com) | In the Windsurf / VS Code IDE, open the command palette and run `Windsurf: Provide Auth Token` (or click the Jupyter "Get Windsurf Authentication Token" button), then copy the shown token and paste it here. Note: opening windsurf.com/show-auth-token directly only renders a "Redirecting" page — the IDE must initiate the flow (it adds a `?state=...` param) for the token to appear. |
| `zed` | `zd` | Zed IDE | OAuth | [link](https://zed.dev) | Zed stores LLM provider credentials (OpenAI, Anthropic, Google, Mistral, xAI) in the OS keychain. Use the Import button below to discover and import them automatically. |
| `zed-hosted` | — | Zed Hosted Models | OAuth | [link](https://zed.dev) | Sign in with your Zed account (native-app sign-in). OmniRoute generates a one-time RSA keypair and opens zed.dev to authorize it — on a remote/headless install, copy the resulting 127.0.0.1 callback URL from your browser's address bar and paste it back here. Distinct from the 'Zed IDE' credential-import entry above: this proxies chat completions through Zed's own hosted model aggregator (cloud.zed.dev), fronting Anthropic/OpenAI/Google/xAI models under your Zed plan. |
## Web Cookie Providers (23)
## Web Cookie Providers (24)
| ID | Alias | Name | Tags | Website | Notes |
|----|-------|------|------|---------|-------|
| `adapta-web` | `adp-web` | Adapta.org (Adapta One Web) | Web cookie | [link](https://agent.adapta.one) | Paste your __client cookie value from .clerk.agent.adapta.one (DevTools → Application → Cookies) |
| `blackbox-web` | `bb-web` | Blackbox Web (Subscription) | Web cookie | [link](https://app.blackbox.ai) | Paste your __Secure-authjs.session-token value or full cookie header from app.blackbox.ai |
| `chatgpt-web` | `cgpt-web` | ChatGPT Web (Plus/Pro) | Web cookie | [link](https://chatgpt.com) | Paste your __Secure-next-auth.session-token cookie value from chatgpt.com |
| `claude-web` | `cw` | Claude Web | Web cookie | [link](https://claude.ai) | Paste your session cookie from claude.ai |
| ID | Alias | Name | Tags | Website | Notes |
| ------------------ | ------------- | ------------------------------- | ---------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `adapta-web` | `adp-web` | Adapta.org (Adapta One Web) | Web cookie | [link](https://agent.adapta.one) | Paste your __client cookie value from .clerk.agent.adapta.one (DevTools → Application → Cookies) |
| `blackbox-web` | `bb-web` | Blackbox Web (Subscription) | Web cookie | [link](https://app.blackbox.ai) | Paste your __Secure-authjs.session-token value or full cookie header from app.blackbox.ai |
| `chatgpt-web` | `cgpt-web` | ChatGPT Web (Plus/Pro) | Web cookie | [link](https://chatgpt.com) | Paste your __Secure-next-auth.session-token cookie value from chatgpt.com |
| `claude-web` | `cw` | Claude Web | Web cookie | [link](https://claude.ai) | Paste your session cookie from claude.ai |
| `copilot-m365-web` | `m365copilot` | Microsoft 365 Copilot (BizChat) | Web cookie | [link](https://m365.cloud.microsoft/chat) | Sign in at m365.cloud.microsoft/chat, then open DevTools → Network → filter 'WS' → click the Chathub WebSocket connection. Copy both the access_token query parameter AND the account-specific Chathub path segment from its request URL (wss://…/Chathub/<path>?…&access_token=…). It is NOT an Authorization: Bearer header on an XHR/Fetch request. The token is short-lived; this is an unofficial integration. |
| `copilot-web` | `copilot` | Microsoft Copilot Web | Web cookie | [link](https://copilot.microsoft.com) | Paste your access_token from copilot.microsoft.com (or export a .har file from DevTools while logged in) |
| `deepseek-web` | `ds-web` | DeepSeek Web | Web cookie | [link](https://chat.deepseek.com) | Paste your userToken from chat.deepseek.com — DevTools → Application → Local Storage → userToken |
| `doubao-web` | `db` | Doubao Web (ByteDance) | Web cookie | [link](https://www.doubao.com) | Paste your session cookie from doubao.com (DevTools → Application → Cookies) |
| `gemini-business` | `gembiz` | Gemini Business (Enterprise) | Web cookie | [link](https://business.gemini.google) | From your enterprise account: open business.gemini.google/home/cid/{your-cid}, then copy __Secure-1PSID and __Secure-1PSIDTS cookies from DevTools → Application → Cookies. Paste as a cookie header below. |
| `gemini-web` | `gweb` | Gemini Web (Free) | Web cookie | [link](https://gemini.google.com) | Paste your __Secure-1PSID cookie value from gemini.google.com. Optionally add __Secure-1PSIDTS separated by semicolon. |
| `grok-web` | `gw` | Grok Web (Subscription) | Web cookie | [link](https://grok.com) | Paste the full grok.com cookie line from DevTools → Application → Cookies. Include both `sso` and `sso-rw` (e.g. `sso=...; sso-rw=...`) — Grok's anti-bot rejects `sso` on its own. |
| `huggingchat` | `huggingchat` | HuggingChat (Free) | Web cookie | [link](https://huggingface.co/chat) | Paste the full Cookie header from huggingface.co/chat (DevTools → Network → /chat/conversation → Request Headers → Cookie). It should include hf-chat and may also include token / aws-waf-token. |
| `inner-ai` | `in-ai` | Inner.ai (Subscription) | Web cookie | [link](https://app.innerai.com) | Paste your token cookie and email separated by a space: open DevTools → Application → Cookies → .innerai.com, copy the token value, then append a space and your Inner.ai login email. Example: eyJhbG... user@example.com |
| `kimi-web` | `kimi-web` | Kimi Web (Moonshot AI) | Web cookie | [link](https://www.kimi.com) | Paste your Cookie header from www.kimi.com (must contain kimi-auth=...). Find it via DevTools → Network → request → Cookie. |
| `lmarena` | `lma` | LMArena (Free) | Web cookie | [link](https://lmarena.ai) | Paste the full Cookie header from lmarena.ai (DevTools → Network → request → Cookie). The session is now split across arena-auth-prod-v1.0, .1, … — copy the whole header. Optional — works with free tier for basic comparisons. |
| `muse-spark-web` | `ms-web` | Muse Spark Web (Meta AI) | Web cookie | [link](https://www.meta.ai) | Paste your ecto_1_sess value or full cookie header from meta.ai |
| `perplexity-web` | `pplx-web` | Perplexity Web (Pro/Max) | Web cookie | [link](https://www.perplexity.ai) | Paste your __Secure-next-auth.session-token cookie value from perplexity.ai |
| `poe-web` | `poe` | Poe Web (Subscription) | Web cookie | [link](https://poe.com) | Paste your p-b cookie value from poe.com (DevTools → Application → Cookies → p-b) |
| `qwen-web` | `qwen-web` | Qwen Web (Free) | Web cookie | [link](https://chat.qwen.ai) | Open chat.qwen.ai, log in, then open DevTools → Application → Local Storage → copy the "token" value (or use tongyi_sso_ticket cookie as Bearer token). |
| `t3-web` | `t3chat` | t3.chat (Pro/Free) | Web cookie | [link](https://t3.chat) | Open t3.chat in your browser, log in, then open DevTools → Application → Local Storage → https://t3.chat. Copy the value of 'convex-session-id'. Also open DevTools → Network, copy the Cookie header from any request. Paste both values here. See provider setup docs for a step-by-step guide. |
| `v0-vercel-web` | `v0` | v0 Vercel Web (Code Gen) | Web cookie | [link](https://v0.dev) | Paste your session cookie from v0.dev (DevTools → Application → Cookies) |
| `venice-web` | `ven` | Venice Web (Privacy) | Web cookie | [link](https://venice.ai) | Paste your session cookie from venice.ai (DevTools → Application → Cookies) |
| `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. |
| `copilot-web` | `copilot` | Microsoft Copilot Web | Web cookie | [link](https://copilot.microsoft.com) | Paste your access_token from copilot.microsoft.com (or export a .har file from DevTools while logged in) |
| `deepseek-web` | `ds-web` | DeepSeek Web | Web cookie | [link](https://chat.deepseek.com) | Paste your userToken from chat.deepseek.com — DevTools → Application → Local Storage → userToken |
| `doubao-web` | `db` | Dola Web (ByteDance) | Web cookie | [link](https://www.dola.com) | Paste the full Cookie header from www.dola.com. It should include sessionid, ttwid, and s_v_web_id. If s_v_web_id is unavailable, fp=verify_... from a chat/completion request URL can be used as a fallback. |
| `gemini-business` | `gembiz` | Gemini Business (Enterprise) | Web cookie | [link](https://business.gemini.google) | From your enterprise account: open business.gemini.google/home/cid/{your-cid}, then copy __Secure-1PSID and __Secure-1PSIDTS cookies from DevTools → Application → Cookies. Paste as a cookie header below. |
| `gemini-web` | `gweb` | Gemini Web (Free) | Web cookie | [link](https://gemini.google.com) | Paste your __Secure-1PSID cookie value from gemini.google.com. Optionally add __Secure-1PSIDTS separated by semicolon. |
| `grok-web` | `gw` | Grok Web (Subscription) | Web cookie | [link](https://grok.com) | Paste the full grok.com cookie line from DevTools → Application → Cookies. Include both `sso` and `sso-rw` (e.g. `sso=...; sso-rw=...`) — Grok's anti-bot rejects `sso` on its own. |
| `huggingchat` | `huggingchat` | HuggingChat (Free) | Web cookie | [link](https://huggingface.co/chat) | Paste the full Cookie header from huggingface.co/chat (DevTools → Network → /chat/conversation → Request Headers → Cookie). It should include hf-chat and may also include token / aws-waf-token. |
| `inner-ai` | `in-ai` | Inner.ai (Subscription) | Web cookie | [link](https://app.innerai.com) | Paste your token cookie and email separated by a space: open DevTools → Application → Cookies → .innerai.com, copy the token value, then append a space and your Inner.ai login email. Example: eyJhbG... user@example.com |
| `kimi-web` | `kimi-web` | Kimi Web (Moonshot AI) | Web cookie | [link](https://www.kimi.com) | Paste your Cookie header from www.kimi.com (must contain kimi-auth=...). Find it via DevTools → Network → request → Cookie. |
| `lmarena` | `lma` | LMArena (Free) | Web cookie | [link](https://lmarena.ai) | Paste the full Cookie header from lmarena.ai (DevTools → Network → request → Cookie). The session is now split across arena-auth-prod-v1.0, .1, … — copy the whole header. Optional — works with free tier for basic comparisons. |
| `muse-spark-web` | `ms-web` | Muse Spark Web (Meta AI) | Web cookie | [link](https://www.meta.ai) | Paste your ecto_1_sess value or full cookie header from meta.ai |
| `perplexity-web` | `pplx-web` | Perplexity Web (Pro/Max) | Web cookie | [link](https://www.perplexity.ai) | Paste your __Secure-next-auth.session-token cookie value from perplexity.ai |
| `poe-web` | `poe` | Poe Web (Subscription) | Web cookie | [link](https://poe.com) | Paste your p-b cookie value from poe.com (DevTools → Application → Cookies → p-b) |
| `qwen-web` | `qwen-web` | Qwen Web (Free) | Web cookie | [link](https://chat.qwen.ai) | Open chat.qwen.ai, log in, then open DevTools → Application → Local Storage → copy the "token" value (or use tongyi_sso_ticket cookie as Bearer token). |
| `t3-web` | `t3chat` | t3.chat (Pro/Free) | Web cookie | [link](https://t3.chat) | Open t3.chat in your browser, log in, then open DevTools → Application → Local Storage → https://t3.chat. Copy the value of 'convex-session-id'. Also open DevTools → Network, copy the Cookie header from any request. Paste both values here. See provider setup docs for a step-by-step guide. |
| `v0-vercel-web` | `v0` | v0 Vercel Web (Code Gen) | Web cookie | [link](https://v0.dev) | Paste your session cookie from v0.dev (DevTools → Application → Cookies) |
| `venice-web` | `ven` | Venice Web (Privacy) | Web cookie | [link](https://venice.ai) | Paste your session cookie from venice.ai (DevTools → Application → Cookies) |
| `yuanbao-web` | `ybw` | Tencent Yuanbao (Free) | Web cookie | [link](https://yuanbao.tencent.com) | Log in to yuanbao.tencent.com, then paste the full Cookie header (DevTools → Network → any /api request → Request Headers → Cookie). It must contain hy_user and hy_token. |
| `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) (158)
## API Key Providers (paid / paid-with-free-credits) (167)
| ID | Alias | Name | Tags | Website | Notes |
|----|-------|------|------|---------|-------|
| `360ai` | `360ai` | 360 AI | API key | [link](https://ai.360.cn) | Get API key at ai.360.cn |
| `agentrouter` | `agentrouter` | AgentRouter | API key, aggregator | [link](https://agentrouter.org) | $200 free credits on signup - multi-model routing gateway |
| `ai21` | `ai21` | AI21 Labs | API key | [link](https://www.ai21.com) | $10 trial credits on signup (valid 3 months), no credit card required |
| `aimlapi` | `aiml` | AI/ML API | API key, aggregator | [link](https://aimlapi.com) | Free tier paused (2026) — AI/ML API is now pay-as-you-go only (min $20 top-up); no recurring free credits. |
| `alibaba` | `ali` | Alibaba | API key | [link](https://bailian.console.alibabacloud.com/) | — |
| `alibaba-cn` | `ali-cn` | Alibaba (China) | API key | [link](https://dashscope.console.aliyun.com/) | — |
| `anthropic` | `anthropic` | Anthropic | API key | [link](https://platform.claude.com) | — |
| `api-airforce` | `af` | Api.airforce | API key | [link](https://api.airforce) | 55 free tier models including Grok-3, Claude 3.7, Qwen3, Kimi-K2, Gemini 2.5 Flash, DeepSeek-V3 |
| `arcee-ai` | `arcee` | Arcee AI | API key | [link](https://arcee.ai) | Get API key at arcee.ai |
| `azure-ai` | `azure-ai` | Azure AI Foundry | API key, enterprise | [link](https://learn.microsoft.com/azure/ai-foundry) | Use your Azure AI Foundry key. Base URL can be https://<resource>.services.ai.azure.com/openai/v1/ or https://<resource>.openai.azure.com/openai/v1/. |
| `azure-openai` | `azure` | Azure OpenAI | API key, enterprise | [link](https://azure.microsoft.com/products/ai-services/openai-service) | Use your Azure OpenAI API key. Base URL should be your resource endpoint, for example https://my-resource.openai.azure.com. |
| `baichuan` | `baichuan` | Baichuan | API key | [link](https://baichuan.com) | Get API key at platform.baichuan-ai.com |
| `baidu` | `baidu` | Baidu (ERNIE) | API key | [link](https://yiyan.baidu.com) | Get API key at console.bce.baidu.com |
| `bailian-coding-plan` | `bcp` | Alibaba Coding Plan | API key | [link](https://www.alibabacloud.com/help/en/model-studio/coding-plan) | — |
| `baseten` | `baseten` | Baseten | API key | [link](https://baseten.co) | $30 free trial credits for GPU inference |
| `bazaarlink` | `bzl` | BazaarLink | API key | [link](https://bazaarlink.ai) | Use your BazaarLink API key (starts with sk-bl-) in Authorization: Bearer <key>. OpenAI SDK works with base URL https://bazaarlink.ai/api/v1. Models use provider/model-name format. |
| `bedrock` | `bedrock` | Amazon Bedrock | API key, enterprise | [link](https://aws.amazon.com/bedrock) | Use your Amazon Bedrock API key and configure the AWS region where your models are enabled (for example eu-west-2). OmniRoute calls Bedrock's native Converse API directly. |
| `black-forest-labs` | `bfl` | Black Forest Labs | API key, image | [link](https://blackforestlabs.ai) | — |
| `blackbox` | `bb` | Blackbox AI | API key | [link](https://blackbox.ai) | Free tier: unlimited basic chat plus Minimax-M2.5, no credit card required |
| `bluesminds` | `bm` | BluesMinds | API key | [link](https://www.bluesminds.com) | Free daily pi credits — supports 200+ models including GPT-4o, GPT-4.1, Claude Sonnet 4.5, Gemini 2.0 Flash, DeepSeek V4, Qwen, Kimi K2 |
| `byteplus` | `bpm` | BytePlus ModelArk | API key | [link](https://console.byteplus.com/ark) | — |
| `bytez` | `bytez` | Bytez | API key | [link](https://bytez.com) | $1 free credits, refreshes every 4 weeks |
| `cablyai` | `cablyai` | CablyAI | API key, aggregator | [link](https://cablyai.com) | ⚠️ **DEPRECATED.** cablyai.com no longer resolves (DNS NXDOMAIN, verified 2026-06-30) — the domain is gone and every request fails with a DNS error (#5568). |
| `cerebras` | `cerebras` | Cerebras | API key | [link](https://inference.cerebras.ai) | Free Trial: 1M tokens/day, 30K TPM, 5 RPM — no credit card. |
| `chutes` | `chutes` | Chutes.ai | API key, aggregator | [link](https://chutes.ai) | Bearer API key for the Chutes OpenAI-compatible gateway. |
| `clarifai` | `clarifai` | Clarifai | API key, enterprise | [link](https://docs.clarifai.com) | Use your Clarifai PAT or app-specific API key. OmniRoute targets the OpenAI-compatible endpoint at https://api.clarifai.com/v2/ext/openai/v1 and authenticates with Authorization: Key <token>. |
| `cloudflare-ai` | `cf` | Cloudflare Workers AI | API key | [link](https://developers.cloudflare.com/workers-ai) | Requires API Token AND Account ID (found at dash.cloudflare.com) |
| `codestral` | `codestral` | Codestral | API key | [link](https://mistral.ai) | — |
| `cohere` | `cohere` | Cohere | API key | [link](https://cohere.com) | Free Trial: 1,000 API calls/month for testing, no credit card required |
| `command-code` | `cmd` | Command Code | API key | [link](https://commandcode.ai/) | Use a Command Code API key. Requests are sent to Command Code's /alpha/generate endpoint. |
| `coze` | `coze` | Coze | API key | [link](https://coze.com) | Get API key at coze.com/open/api |
| `crof` | `crof` | CrofAI | API key | [link](https://crof.ai) | — |
| `databricks` | `databricks` | Databricks | API key, enterprise | [link](https://www.databricks.com) | — |
| `datarobot` | `datarobot` | DataRobot | API key, enterprise | [link](https://docs.datarobot.com) | Use your DataRobot API token. Optional Base URL can be the account root (for LLM Gateway) or a deployment URL under /api/v2/deployments/<id>. |
| `deepinfra` | `deepinfra` | DeepInfra | API key | [link](https://deepinfra.com) | Free signup credits for API testing and model exploration |
| `deepseek` | `ds` | DeepSeek | API key | [link](https://platform.deepseek.com) | 5M free tokens on signup - no credit card required |
| `dgrid` | `dgrid` | DGrid | API key | [link](https://dgrid.ai) | DGrid Free Models Router: 10 requests/minute and 100 requests/day. A $5 lifetime top-up unlocks up to 20 requests/minute and 1,000 requests/day. |
| `dify` | `dify` | Dify | API key | [link](https://dify.ai) | Get API key from your Dify instance. |
| `dit` | `dai` | DIT.ai | API key | [link](https://dit.ai) | Use your dit.ai API key in Authorization: Bearer <key>. Fully OpenAI-compatible — a drop-in replacement, just change the base URL to https://api.dit.ai/v1. |
| `doubao` | `doubao` | Doubao | API key | [link](https://doubao.com) | Get API key at console.volcengine.com |
| `empower` | `empower` | Empower | API key, aggregator | [link](https://docs.empower.dev) | Bearer API key for the Empower OpenAI-compatible endpoint. |
| `factory` | `factory` | Factory | API key | [link](https://factory.ai) | Bearer API key for the Factory OpenAI-compatible gateway. |
| `fal-ai` | `fal` | Fal.ai | API key, image | [link](https://fal.ai) | — |
| `featherless-ai` | `featherless` | Featherless AI | API key | [link](https://featherless.ai) | Free tier available — no credit card required |
| `fenayai` | `fenayai` | FenayAI | API key, aggregator | [link](https://fenayai.com) | Bearer API key for the FenayAI OpenAI-compatible gateway. |
| `firecrawl` | `fc` | Firecrawl | API key | [link](https://firecrawl.dev) | — |
| `fireworks` | `fireworks` | Fireworks AI | API key | [link](https://fireworks.ai) | $1 free starter credits on signup for API testing |
| `freeaiapikey` | `faik` | FreeAIAPIKey | API key | [link](https://freeaiapikey.com) | — |
| `freemodel-dev` | `fmd` | FreeModel.dev | API key | [link](https://freemodel.dev) | $300 free credits on signup — no credit card required. Access GPT-5.4 and GPT-5.5 (OpenAI's latest flagship models) through an OpenAI-compatible API. |
| `friendliai` | `friendli` | FriendliAI | API key | [link](https://friendli.ai) | Free tier for serverless inference — no credit card required |
| `galadriel` | `galadriel` | Galadriel | API key | [link](https://galadriel.com) | ⚠️ **DEPRECATED.** api.galadriel.ai no longer resolves (sweep 2026-06-19); the inference API appears discontinued. |
| `gemini` | `gemini` | Gemini (Google AI Studio) | API key | [link](https://aistudio.google.com) | Free forever: 1,500 req/day for Gemini 2.5 Flash — no credit card, get key at aistudio.google.com |
| `getgoapi` | `ggo` | GoAPI | API key, aggregator | [link](https://api.getgoapi.com) | — |
| `gigachat` | `gigachat` | GigaChat (Sber) | API key | [link](https://developers.sber.ru) | — |
| `github-models` | `ghm` | GitHub Models | API key | [link](https://github.com/marketplace/models) | Create a GitHub PAT with 'models: read' scope at github.com/settings/tokens |
| `gitlab` | `gitlab` | GitLab Duo PAT | API key | [link](https://docs.gitlab.com/user/duo_agent_platform/code_suggestions/) | GitLab personal access token for the public Code Suggestions API. Configure a self-hosted base URL when not using gitlab.com. |
| `gitlawb` | `glb` | Gitlawb Opengateway (MiMo) | API key | [link](https://opengateway.gitlawb.com) | Free MiMo (xiaomi/mimo-v2.5) revoked 2026-05 — Opengateway is now a pay-as-you-go credit gateway; no recurring free model. |
| `gitlawb-gmi` | `glb-gmi` | Gitlawb Opengateway (GMI Cloud) | API key | [link](https://opengateway.gitlawb.com) | Free Nemotron promo ended 2026-06 — the GMI Cloud route is now pay-as-you-go credit only. |
| `glhf` | `glhf` | GLHF Chat | API key, aggregator | [link](https://glhf.chat) | ⚠️ **DEPRECATED.** glhf.chat shut down (2026); its api.laf.run gateway no longer serves the catalog (sweep 2026-06-19). |
| `glm` | `glm` | GLM Coding | API key | [link](https://z.ai/subscribe) | — |
| `glm-cn` | `glmcn` | GLM Coding (China) | API key | [link](https://open.bigmodel.cn) | — |
| `glmt` | `glmt` | GLM Thinking | API key | [link](https://open.bigmodel.cn) | — |
| `groq` | `groq` | Groq | API key | [link](https://groq.com) | Free tier: 30 RPM / 14.4K RPD — no credit card |
| `hackclub` | `hc` | Hackclub AI | API key, aggregator | [link](https://ai.hackclub.com) | Sign in with your Hack Club account at ai.hackclub.com. |
| `haiper` | `hp` | Haiper | API key, video | [link](https://haiper.ai) | Get API key at haiper.ai/haiper-api |
| `heroku` | `heroku` | Heroku AI | API key, enterprise | [link](https://www.heroku.com) | — |
| `huggingface` | `hf` | HuggingFace | API key | [link](https://huggingface.co) | Free Inference API for thousands of models (Whisper, VITS, SDXL…) |
| `hyperbolic` | `hyp` | Hyperbolic | API key | [link](https://hyperbolic.xyz) | $1-5 trial credits on signup for serverless inference |
| `ideogram` | `ideo` | Ideogram | API key | [link](https://ideogram.ai) | Get API key at ideogram.ai/docs/api |
| `iflytek` | `iflytek` | iFlytek Spark | API key | [link](https://xinghuo.xfyun.cn) | Get API key at console.xfyun.cn |
| `inclusionai` | `inclusion` | InclusionAI | API key | [link](https://inclusionai.com) | ⚠️ **DEPRECATED.** api.inclusionai.tech no longer resolves (sweep 2026-06-19); the inference API appears discontinued. |
| `inference-net` | `inet` | Inference.net | API key | [link](https://inference.net) | $25 free credits on signup plus research grants available |
| `jina-ai` | `jina` | Jina AI | API key, embed/rerank | [link](https://jina.ai) | Bearer API key for the Jina AI rerank API. |
| `jina-reader` | `jr` | Jina Reader | API key | [link](https://jina.ai/reader) | — |
| `kie` | `kie` | KIE.AI | API key | [link](https://kie.ai) | — |
| `kilo-gateway` | `kg` | Kilo Gateway | API key, aggregator | [link](https://kilo.ai) | — |
| `kimi` | `kimi` | Kimi | API key | [link](https://platform.moonshot.ai) | — |
| `kimi-coding-apikey` | `kmca` | Kimi Coding (API Key) | API key | [link](https://www.kimi.com/code) | — |
| `kluster` | `kluster` | Kluster AI | API key | [link](https://kluster.ai) | ⚠️ **DEPRECATED.** kluster.ai shut down (2026-06-09); api.kluster.ai no longer resolves (sweep 2026-06-19). Use another OpenAI-compatible provider. |
| `lambda-ai` | `lambda` | Lambda AI | API key | [link](https://lambda.ai) | — |
| `laozhang` | `lz` | LaoZhang AI | API key, aggregator | [link](https://api.laozhang.ai) | — |
| `leonardo` | `leo` | Leonardo AI | API key, video | [link](https://leonardo.ai) | Get API key at leonardo.ai/developer |
| `liquid` | `liquid` | Liquid AI | API key | [link](https://liquid.ai) | Get API key at liquid.ai |
| `llamagate` | `llamagate` | LlamaGate | API key | [link](https://llamagate.ai) | — |
| `llm7` | `llm7` | LLM7.io | API key | [link](https://llm7.io) | No signup required - 2 req/s, 20 RPM, 100 req/hr free tier |
| `longcat` | `lc` | LongCat AI | API key | [link](https://longcat.chat/platform/docs) | Free: one-time 10M-token grant after account signup + KYC verification (LongCat-2.0). One-time only — not a recurring daily/monthly allowance. |
| `maritalk` | `maritalk` | Maritalk | API key | [link](https://www.maritaca.ai) | — |
| `meta-llama` | `meta` | Meta Llama API | API key | [link](https://llama.developer.meta.com) | — |
| `minimax` | `minimax` | Minimax Coding | API key, video | [link](https://www.minimax.io) | — |
| `minimax-cn` | `minimax-cn` | Minimax (China) | API key | [link](https://www.minimaxi.com) | — |
| `mistral` | `mistral` | Mistral | API key | [link](https://mistral.ai) | Free Experiment tier: rate-limited access to all models, no credit card required |
| `modal` | `mdl` | Modal | API key, enterprise | [link](https://modal.com/docs) | Use the bearer token that protects your Modal deployment, if enabled. Base URL should point to your OpenAI-compatible Modal app, for example https://<workspace>--<app>.modal.run/v1. |
| `monsterapi` | `monster` | MonsterAPI | API key | [link](https://monsterapi.ai) | Get API key at monsterapi.ai |
| `moonshot` | `moonshot` | Moonshot AI | API key | [link](https://platform.moonshot.ai) | — |
| `morph` | `morph` | Morph | API key | [link](https://morphllm.com) | Free tier: 250K credits/month, $0 |
| `nanogpt` | `nanogpt` | NanoGPT | API key | [link](https://nano-gpt.com) | — |
| `nebius` | `nebius` | Nebius AI | API key | [link](https://nebius.com) | ~$1 trial credits on signup for API testing |
| `nlpcloud` | `nlpc` | NLP Cloud | API key | [link](https://docs.nlpcloud.com) | Use your NLP Cloud API key in Authorization: Token <key>. OmniRoute targets the chatbot endpoint on https://api.nlpcloud.io/v1/gpu/<model>/chatbot by default. |
| `nomic` | `nomic` | Nomic | API key | [link](https://nomic.ai) | Get API key at atlas.nomic.ai |
| `nous-research` | `nous` | Nous Research | API key | [link](https://portal.nousresearch.com/help) | Use your Nous Portal API key. OmniRoute targets the official OpenAI-compatible inference endpoint at https://inference-api.nousresearch.com/v1. |
| `novita` | `novita` | Novita AI | API key, aggregator | [link](https://novita.ai) | $0.50 trial credits on signup (valid about 1 year) |
| `nscale` | `nscale` | nScale | API key | [link](https://nscale.com) | $5 free credits on signup for inference testing |
| `nvidia` | `nvidia` | NVIDIA NIM | API key | [link](https://build.nvidia.com) | Free dev access: ~40 RPM, 70+ models (Kimi K2.5, GLM 4.7, DeepSeek V3.2...) |
| `oci` | `oci` | OCI Generative AI | API key, enterprise | [link](https://www.oracle.com/artificial-intelligence/generative-ai) | Use your OCI Generative AI API key or IAM bearer token. Base URL can be https://inference.generativeai.<region>.oci.oraclecloud.com/openai/v1/. |
| `ollama-cloud` | `ollamacloud` | Ollama Cloud | API key | [link](https://ollama.com/settings/keys) | — |
| `openadapter` | `oad` | OpenAdapter | API key | [link](https://openadapter.dev) | Use your OpenAdapter API key in Authorization: Bearer sk-cv-<key>. Fully OpenAI-compatible. API base URL: https://api.openadapter.in/v1. |
| `openai` | `openai` | OpenAI | API key | [link](https://platform.openai.com) | — |
| `opencode-go` | `opencode-go` | OpenCode Go | API key | [link](https://opencode.ai/go) | — |
| `opencode-zen` | `opencode-zen` | OpenCode Zen | API key | [link](https://opencode.ai/zen) | — |
| `openrouter` | `openrouter` | OpenRouter | API key, aggregator | [link](https://openrouter.ai) | Free models at $0/token with :free suffix - 20 RPM / 200 RPD |
| `orcarouter` | `orcarouter` | OrcaRouter | API key | [link](https://www.orcarouter.ai) | — |
| `ovhcloud` | `ovh` | OVHcloud AI | API key | [link](https://www.ovhcloud.com) | — |
| `perplexity` | `pplx` | Perplexity | API key | [link](https://www.perplexity.ai) | — |
| `piapi` | `pi` | PiAPI | API key, aggregator | [link](https://piapi.ai) | — |
| `pioneer` | `pn` | Pioneer AI | API key | [link](https://pioneer.ai) | $75 free usage credits — no credit card required |
| `poe` | `poe` | Poe | API key, aggregator | [link](https://creator.poe.com/api-reference) | Bearer API key for the Poe OpenAI-compatible API. |
| `pollinations` | `pol` | Pollinations AI | API key, video | [link](https://pollinations.ai) | Free keyless tier: openai, openai-fast, openai-large, qwen-coder, mistral, deepseek, grok, gemini-flash-lite-3.1, perplexity-fast, perplexity-reasoning. Premium models (claude, gemini, midijourney) require a Pollinations API key from enter.pollinations.ai. |
| `predibase` | `predibase` | Predibase | API key | [link](https://predibase.com) | ⚠️ **DEPRECATED.** serving.app.predibase.com no longer resolves (sweep 2026-06-19); the managed serving API appears discontinued. |
| `publicai` | `publicai` | PublicAI | API key | [link](https://publicai.co) | Requires an API key — one-time signup credit, then paid |
| `puter` | `pu` | Puter AI | API key | [link](https://puter.com) | Get token at puter.com/dashboard → Copy Auth Token |
| `qianfan` | `qianfan` | Baidu Qianfan | API key | [link](https://cloud.baidu.com/product/wenxinworkshop) | — |
| `recraft` | `recraft` | Recraft | API key, image | [link](https://recraft.ai) | — |
| `reka` | `reka` | Reka | API key | [link](https://docs.reka.ai/chat/overview) | Use your Reka API key. OmniRoute supports the OpenAI-compatible base URL https://api.reka.ai/v1 and sends both Authorization and X-Api-Key headers for compatibility. |
| `runwayml` | `runway` | Runway | API key, video | [link](https://docs.dev.runwayml.com) | Use your Runway API key in Authorization: Bearer <key>. OmniRoute targets the current Runway API at https://api.dev.runwayml.com/v1 and sends the required X-Runway-Version header automatically. |
| `sambanova` | `samba` | SambaNova | API key | [link](https://sambanova.ai) | $5 free credits on signup (30-day validity), no credit card required |
| `sap` | `sap` | SAP Generative AI Hub | API key, enterprise | [link](https://help.sap.com/docs/sap-ai-core/sap-ai-core-service-guide/generative-ai-hub-in-sap-ai-core) | Use your SAP AI Core bearer token. Base URL can be your AI_API_URL root or a deploymentUrl from Generative AI Hub. |
| `scaleway` | `scw` | Scaleway AI | API key | [link](https://www.scaleway.com/en/docs/ai-data/generative-apis/) | 1M free tokens for new accounts — EU/GDPR compliant (Paris), Qwen3 235B & Llama 70B |
| `sensenova` | `sensenova` | SenseNova | API key | [link](https://platform.sensenova.cn) | Get API key at platform.sensenova.cn |
| `siliconflow` | `siliconflow` | SiliconFlow | API key | [link](https://cloud.siliconflow.com) | $1 free credits plus permanently free models after identity verification |
| `snowflake` | `snowflake` | Snowflake Cortex | API key, enterprise | [link](https://www.snowflake.com) | — |
| `sparkdesk` | `sparkdesk` | SparkDesk | API key | [link](https://xinghuo.xfyun.cn) | Get API key at console.xfyun.cn |
| `stability-ai` | `stability` | Stability AI | API key, image | [link](https://stability.ai) | — |
| `stepfun` | `stepfun` | StepFun | API key | [link](https://stepfun.com) | Get API key at platform.stepfun.com |
| `suno` | `suno` | Suno | API key | [link](https://suno.ai) | Paste session cookie from suno.ai (Clerk auth) |
| `synthetic` | `synthetic` | Synthetic | API key, aggregator | [link](https://synthetic.new) | — |
| `tencent` | `tencent` | Tencent Hunyuan | API key | [link](https://hunyuan.tencent.com) | Get API key at console.cloud.tencent.com |
| `thebai` | `thebai` | TheB.AI | API key, aggregator | [link](https://theb.ai) | Bearer API key for the TheB.AI OpenAI-compatible gateway. |
| `together` | `together` | Together AI | API key, video | [link](https://www.together.ai) | — |
| `tokenrouter` | `trk` | TokenRouter | API key | [link](https://tokenrouter.com) | Use your TokenRouter API key in Authorization: Bearer <key>. Fully OpenAI-compatible. API base URL: https://api.tokenrouter.com/v1. |
| `topaz` | `topaz` | Topaz | API key, image | [link](https://topazlabs.com) | — |
| `udio` | `udio` | Udio | API key | [link](https://udio.com) | Paste session cookie from udio.com (Supabase auth) |
| `uncloseai` | `unc` | UncloseAI | API key | [link](https://uncloseai.com) | No auth required. API accepts any non-empty string as key for identification. |
| `upstage` | `upstage` | Upstage | API key | [link](https://www.upstage.ai) | — |
| `v0-vercel` | `v0` | v0 (Vercel) | API key | [link](https://v0.dev) | — |
| `venice` | `venice` | Venice.ai | API key | [link](https://venice.ai) | — |
| `vercel-ai-gateway` | `vag` | Vercel AI Gateway | API key, aggregator | [link](https://vercel.com/docs/ai-gateway) | — |
| `vertex` | `vertex` | Vertex AI | API key, enterprise | [link](https://cloud.google.com/vertex-ai) | Provide Service Account JSON or OAuth access_token |
| `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. |
| `volcengine` | `volcengine` | Volcengine | API key | [link](https://www.volcengine.com) | — |
| `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) | — |
| `watsonx` | `watsonx` | IBM watsonx.ai Gateway | API key, enterprise | [link](https://www.ibm.com/products/watsonx-ai) | Use your watsonx bearer token. Base URL can be https://<region>.ml.cloud.ibm.com/ml/gateway/v1/ or a self-managed /ml/gateway/v1 endpoint. |
| `xai` | `xai` | xAI (Grok) | API key | [link](https://x.ai) | — |
| `xiaomi-mimo` | `mimo` | Xiaomi MiMo | API key | [link](https://mimo.mi.com) | — |
| `yi` | `yi` | Yi (01.AI) | API key | [link](https://01.ai) | Get API key at platform.lingyiwanwu.com |
| `zai` | `zai` | Z.AI | API key | [link](https://open.bigmodel.cn) | — |
| `zenmux` | `zm` | ZenMux | API key | [link](https://zenmux.ai) | Use your ZenMux API key in Authorization: Bearer <key>. ZenMux is fully OpenAI-compatible. Base URL: https://zenmux.ai/api/v1. |
| ID | Alias | Name | Tags | Website | Notes |
| --------------------- | -------------- | ------------------------------- | --------------------- | -------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `360ai` | `360ai` | 360 AI | API key | [link](https://ai.360.cn) | Get API key at ai.360.cn |
| `agentrouter` | `agentrouter` | AgentRouter | API key, aggregator | [link](https://agentrouter.org) | $200 free credits on signup - multi-model routing gateway |
| `ai21` | `ai21` | AI21 Labs | API key | [link](https://www.ai21.com) | $10 trial credits on signup (valid 3 months), no credit card required |
| `aimlapi` | `aiml` | AI/ML API | API key, aggregator | [link](https://aimlapi.com) | Free tier paused (2026) — AI/ML API is now pay-as-you-go only (min $20 top-up); no recurring free credits. |
| `alibaba` | `ali` | Alibaba | API key | [link](https://bailian.console.alibabacloud.com/) | — |
| `alibaba-cn` | `ali-cn` | Alibaba (China) | API key | [link](https://dashscope.console.aliyun.com/) | — |
| `anthropic` | `anthropic` | Anthropic | API key | [link](https://platform.claude.com) | — |
| `api-airforce` | `af` | Api.airforce | API key | [link](https://api.airforce) | 55 free tier models including Grok-3, Claude 3.7, Qwen3, Kimi-K2, Gemini 2.5 Flash, DeepSeek-V3 |
| `arcee-ai` | `arcee` | Arcee AI | API key | [link](https://arcee.ai) | Get API key at arcee.ai |
| `azure-ai` | `azure-ai` | Azure AI Foundry | API key, enterprise | [link](https://learn.microsoft.com/azure/ai-foundry) | Use your Azure AI Foundry key. Base URL can be https://<resource>.services.ai.azure.com/openai/v1/ or https://<resource>.openai.azure.com/openai/v1/. |
| `azure-openai` | `azure` | Azure OpenAI | API key, enterprise | [link](https://azure.microsoft.com/products/ai-services/openai-service) | Use your Azure OpenAI API key. Base URL should be your resource endpoint, for example https://my-resource.openai.azure.com. |
| `bai` | `bai` | b.ai | API key | [link](https://b.ai) | Bearer API key for the b.ai OpenAI-compatible LLM gateway (distinct from TheB.AI). Create a key at https://docs.b.ai, then use https://api.b.ai/v1 as the OpenAI-compatible base URL. |
| `baichuan` | `baichuan` | Baichuan | API key | [link](https://baichuan.com) | Get API key at platform.baichuan-ai.com |
| `baidu` | `baidu` | Baidu (ERNIE) | API key | [link](https://yiyan.baidu.com) | Get API key at console.bce.baidu.com |
| `bailian-coding-plan` | `bcp` | Alibaba Coding Plan | API key | [link](https://www.alibabacloud.com/help/en/model-studio/coding-plan) | — |
| `baseten` | `baseten` | Baseten | API key | [link](https://baseten.co) | $30 free trial credits for GPU inference |
| `bazaarlink` | `bzl` | BazaarLink | API key | [link](https://bazaarlink.ai) | Use your BazaarLink API key (starts with sk-bl-) in Authorization: Bearer <key>. OpenAI SDK works with base URL https://bazaarlink.ai/api/v1. Models use provider/model-name format. |
| `bedrock` | `bedrock` | Amazon Bedrock | API key, enterprise | [link](https://aws.amazon.com/bedrock) | Use your Amazon Bedrock API key and configure the AWS region where your models are enabled (for example eu-west-2). OmniRoute calls Bedrock's native Converse API directly. |
| `black-forest-labs` | `bfl` | Black Forest Labs | API key, image | [link](https://blackforestlabs.ai) | — |
| `blackbox` | `bb` | Blackbox AI | API key | [link](https://blackbox.ai) | Free tier: unlimited basic chat plus Minimax-M2.5, no credit card required |
| `bluesminds` | `bm` | BluesMinds | API key | [link](https://www.bluesminds.com) | Free daily pi credits — supports 200+ models including GPT-4o, GPT-4.1, Claude Sonnet 4.5, Gemini 2.0 Flash, DeepSeek V4, Qwen, Kimi K2 |
| `byteplus` | `bpm` | BytePlus ModelArk | API key | [link](https://console.byteplus.com/ark) | — |
| `bytez` | `bytez` | Bytez | API key | [link](https://bytez.com) | $1 free credits, refreshes every 4 weeks |
| `cerebras` | `cerebras` | Cerebras | API key | [link](https://inference.cerebras.ai) | Free Trial: 1M tokens/day, 30K TPM, 5 RPM — no credit card. |
| `charm-hyper` | `charm-hyper` | Charm Hyper | API key | [link](https://hyper.charm.land) | 100 free monthly Hypercredits on signup |
| `chutes` | `chutes` | Chutes.ai | API key, aggregator | [link](https://chutes.ai) | Bearer API key for the Chutes OpenAI-compatible gateway. |
| `clarifai` | `clarifai` | Clarifai | API key, enterprise | [link](https://docs.clarifai.com) | Use your Clarifai PAT or app-specific API key. OmniRoute targets the OpenAI-compatible endpoint at https://api.clarifai.com/v2/ext/openai/v1 and authenticates with Authorization: Key <token>. |
| `clinepass` | `clinepass` | ClinePass | API key | [link](https://cline.bot) | — |
| `cloudflare-ai` | `cf` | Cloudflare Workers AI | API key | [link](https://developers.cloudflare.com/workers-ai) | Requires API Token AND Account ID (found at dash.cloudflare.com) |
| `codestral` | `codestral` | Codestral | API key | [link](https://mistral.ai) | — |
| `cohere` | `cohere` | Cohere | API key | [link](https://cohere.com) | Free Trial: 1,000 API calls/month for testing, no credit card required |
| `command-code` | `cmd` | Command Code | API key | [link](https://commandcode.ai/) | Use a Command Code API key. Requests are sent to Command Code's /alpha/generate endpoint. |
| `coze` | `coze` | Coze | API key | [link](https://coze.com) | Get API key at coze.com/open/api |
| `crof` | `crof` | CrofAI | API key | [link](https://crof.ai) | — |
| `databricks` | `databricks` | Databricks | API key, enterprise | [link](https://www.databricks.com) | — |
| `datarobot` | `datarobot` | DataRobot | API key, enterprise | [link](https://docs.datarobot.com) | Use your DataRobot API token. Optional Base URL can be the account root (for LLM Gateway) or a deployment URL under /api/v2/deployments/<id>. |
| `deepinfra` | `deepinfra` | DeepInfra | API key | [link](https://deepinfra.com) | Free signup credits for API testing and model exploration |
| `deepseek` | `ds` | DeepSeek | API key | [link](https://platform.deepseek.com) | 5M free tokens on signup - no credit card required |
| `dgrid` | `dgrid` | DGrid | API key | [link](https://dgrid.ai) | DGrid Free Models Router: 10 requests/minute and 100 requests/day. A $5 lifetime top-up unlocks up to 20 requests/minute and 1,000 requests/day. |
| `dify` | `dify` | Dify | API key | [link](https://dify.ai) | Get API key from your Dify instance. |
| `digitalocean` | `digitalocean` | DigitalOcean | API key | [link](https://docs.digitalocean.com/products/ai-platform/) | — |
| `dit` | `dai` | DIT.ai | API key | [link](https://dit.ai) | Use your dit.ai API key in Authorization: Bearer <key>. Fully OpenAI-compatible — a drop-in replacement, just change the base URL to https://api.dit.ai/v1. |
| `doubao` | `doubao` | Doubao | API key | [link](https://doubao.com) | Get API key at console.volcengine.com |
| `empower` | `empower` | Empower | API key, aggregator | [link](https://docs.empower.dev) | Bearer API key for the Empower OpenAI-compatible endpoint. |
| `factory` | `factory` | Factory | API key | [link](https://factory.ai) | Bearer API key for the Factory OpenAI-compatible gateway. |
| `fal-ai` | `fal` | Fal.ai | API key, image | [link](https://fal.ai) | — |
| `featherless-ai` | `featherless` | Featherless AI | API key | [link](https://featherless.ai) | Free tier available — no credit card required |
| `fenayai` | `fenayai` | FenayAI | API key, aggregator | [link](https://fenayai.com) | Bearer API key for the FenayAI OpenAI-compatible gateway. |
| `firecrawl` | `fc` | Firecrawl | API key | [link](https://firecrawl.dev) | — |
| `fireworks` | `fireworks` | Fireworks AI | API key | [link](https://fireworks.ai) | $1 free starter credits on signup for API testing |
| `freeaiapikey` | `faik` | FreeAIAPIKey | API key | [link](https://freeaiapikey.com) | — |
| `freemodel-dev` | `fmd` | FreeModel.dev | API key | [link](https://freemodel.dev) | $300 free credits on signup — no credit card required. Access GPT-5.4 and GPT-5.5 (OpenAI's latest flagship models) through an OpenAI-compatible API. |
| `friendliai` | `friendli` | FriendliAI | API key | [link](https://friendli.ai) | Free tier for serverless inference — no credit card required |
| `galadriel` | `galadriel` | Galadriel | API key | [link](https://galadriel.com) | ⚠️ **DEPRECATED.** api.galadriel.ai no longer resolves (sweep 2026-06-19); the inference API appears discontinued. |
| `gemini` | `gemini` | Gemini (Google AI Studio) | API key | [link](https://aistudio.google.com) | Free forever: 1,500 req/day for Gemini 2.5 Flash — no credit card, get key at aistudio.google.com |
| `getgoapi` | `ggo` | GoAPI | API key, aggregator | [link](https://api.getgoapi.com) | — |
| `gigachat` | `gigachat` | GigaChat (Sber) | API key | [link](https://developers.sber.ru) | — |
| `github-models` | `ghm` | GitHub Models | API key | [link](https://github.com/marketplace/models) | Create a GitHub PAT with 'models: read' scope at github.com/settings/tokens |
| `gitlab` | `gitlab` | GitLab Duo PAT | API key | [link](https://docs.gitlab.com/user/duo_agent_platform/code_suggestions/) | GitLab personal access token for the public Code Suggestions API. Configure a self-hosted base URL when not using gitlab.com. |
| `gitlawb` | `glb` | Gitlawb Opengateway (MiMo) | API key | [link](https://opengateway.gitlawb.com) | Free MiMo (xiaomi/mimo-v2.5) revoked 2026-05 — Opengateway is now a pay-as-you-go credit gateway; no recurring free model. |
| `gitlawb-gmi` | `glb-gmi` | Gitlawb Opengateway (GMI Cloud) | API key | [link](https://opengateway.gitlawb.com) | Free Nemotron promo ended 2026-06 — the GMI Cloud route is now pay-as-you-go credit only. |
| `glm` | `glm` | GLM Coding | API key | [link](https://z.ai/subscribe) | — |
| `glm-cn` | `glmcn` | GLM Coding (China) | API key | [link](https://open.bigmodel.cn) | — |
| `glmt` | `glmt` | GLM Thinking | API key | [link](https://open.bigmodel.cn) | — |
| `groq` | `groq` | Groq | API key | [link](https://groq.com) | Free tier: 30 RPM / 14.4K RPD — no credit card |
| `hackclub` | `hc` | Hackclub AI | API key, aggregator | [link](https://ai.hackclub.com) | Sign in with your Hack Club account at ai.hackclub.com. |
| `haiper` | `hp` | Haiper | API key, video | [link](https://haiper.ai) | Get API key at haiper.ai/haiper-api |
| `hcnsec` | `hcnsec` | Huancheng Public API | API key | [link](https://api.hcnsec.cn) | Get API key at api.hcnsec.cn |
| `heroku` | `heroku` | Heroku AI | API key, enterprise | [link](https://www.heroku.com) | — |
| `huggingface` | `hf` | HuggingFace | API key | [link](https://huggingface.co) | Free Inference API for thousands of models (Whisper, VITS, SDXL…) |
| `hyperbolic` | `hyp` | Hyperbolic | API key | [link](https://hyperbolic.xyz) | $1-5 trial credits on signup for serverless inference |
| `ideogram` | `ideo` | Ideogram | API key | [link](https://ideogram.ai) | Get API key at ideogram.ai/docs/api |
| `iflytek` | `iflytek` | iFlytek Spark | API key | [link](https://xinghuo.xfyun.cn) | Get API key at console.xfyun.cn |
| `inference-net` | `inet` | Inference.net | API key | [link](https://inference.net) | $25 free credits on signup plus research grants available |
| `jina-ai` | `jina` | Jina AI | API key, embed/rerank | [link](https://jina.ai) | Bearer API key for the Jina AI rerank API. |
| `jina-reader` | `jr` | Jina Reader | API key | [link](https://jina.ai/reader) | — |
| `kenari` | `kenari` | Kenari | API key | [link](https://kenari.id) | Use your Kenari API key (kn-...) in Authorization: Bearer <key>. Fully OpenAI-compatible. API base URL: https://kenari.id/v1. |
| `kie` | `kie` | KIE.AI | API key | [link](https://kie.ai) | — |
| `kilo-gateway` | `kg` | Kilo Gateway | API key, aggregator | [link](https://kilo.ai) | — |
| `kimi` | `kimi` | Kimi | API key | [link](https://platform.moonshot.ai) | — |
| `kimi-coding-apikey` | `kmca` | Kimi Coding (API Key) | API key | [link](https://www.kimi.com/code) | — |
| `lambda-ai` | `lambda` | Lambda AI | API key | [link](https://lambda.ai) | — |
| `laozhang` | `lz` | LaoZhang AI | API key, aggregator | [link](https://api.laozhang.ai) | — |
| `leonardo` | `leo` | Leonardo AI | API key, video | [link](https://leonardo.ai) | Get API key at leonardo.ai/developer |
| `liquid` | `liquid` | Liquid AI | API key | [link](https://liquid.ai) | Get API key at liquid.ai |
| `llamagate` | `llamagate` | LlamaGate | API key | [link](https://llamagate.ai) | — |
| `llm7` | `llm7` | LLM7.io | API key | [link](https://llm7.io) | No signup required - 2 req/s, 20 RPM, 100 req/hr free tier |
| `longcat` | `lc` | LongCat AI | API key | [link](https://longcat.chat/platform/docs) | Free: one-time 10M-token grant after account signup + KYC verification (LongCat-2.0). One-time only — not a recurring daily/monthly allowance. |
| `maritalk` | `maritalk` | Maritalk | API key | [link](https://www.maritaca.ai) | — |
| `meta-llama` | `meta` | Meta Llama API | API key | [link](https://llama.developer.meta.com) | — |
| `minimax` | `minimax` | Minimax Coding | API key, video | [link](https://www.minimax.io) | — |
| `minimax-cn` | `minimax-cn` | Minimax (China) | API key | [link](https://www.minimaxi.com) | — |
| `mistral` | `mistral` | Mistral | API key | [link](https://mistral.ai) | Free Experiment tier: rate-limited access to all models, no credit card required |
| `modal` | `mdl` | Modal | API key, enterprise | [link](https://modal.com/docs) | Use the bearer token that protects your Modal deployment, if enabled. Base URL should point to your OpenAI-compatible Modal app, for example https://<workspace>--<app>.modal.run/v1. |
| `modelscope` | `ms` | ModelScope | API key | [link](https://modelscope.cn) | Free tier via ModelScope API-Inference — Alibaba account required. |
| `monsterapi` | `monster` | MonsterAPI | API key | [link](https://monsterapi.ai) | Get API key at monsterapi.ai |
| `moonshot` | `moonshot` | Moonshot AI | API key | [link](https://platform.moonshot.ai) | — |
| `morph` | `morph` | Morph | API key | [link](https://morphllm.com) | Free tier: 250K credits/month, $0 |
| `nanogpt` | `nanogpt` | NanoGPT | API key | [link](https://nano-gpt.com) | — |
| `nebius` | `nebius` | Nebius AI | API key | [link](https://nebius.com) | ~$1 trial credits on signup for API testing |
| `nlpcloud` | `nlpc` | NLP Cloud | API key | [link](https://docs.nlpcloud.com) | Use your NLP Cloud API key in Authorization: Token <key>. OmniRoute targets the chatbot endpoint on https://api.nlpcloud.io/v1/gpu/<model>/chatbot by default. |
| `nomic` | `nomic` | Nomic | API key | [link](https://nomic.ai) | Get API key at atlas.nomic.ai |
| `nous-research` | `nous` | Nous Research | API key | [link](https://portal.nousresearch.com/help) | Use your Nous Portal API key. OmniRoute targets the official OpenAI-compatible inference endpoint at https://inference-api.nousresearch.com/v1. |
| `novita` | `novita` | Novita AI | API key, aggregator | [link](https://novita.ai) | $0.50 trial credits on signup (valid about 1 year) |
| `nscale` | `nscale` | nScale | API key | [link](https://nscale.com) | $5 free credits on signup for inference testing |
| `nube` | `nube` | Nube.sh | API key | [link](https://nube.sh) | — |
| `nvidia` | `nvidia` | NVIDIA NIM | API key | [link](https://build.nvidia.com) | Free dev access: ~40 RPM, 70+ models (Kimi K2.5, GLM 4.7, DeepSeek V3.2...) |
| `oci` | `oci` | OCI Generative AI | API key, enterprise | [link](https://www.oracle.com/artificial-intelligence/generative-ai) | Use your OCI Generative AI API key or IAM bearer token. Base URL can be https://inference.generativeai.<region>.oci.oraclecloud.com/openai/v1/. |
| `ollama-cloud` | `ollamacloud` | Ollama Cloud | API key | [link](https://ollama.com/settings/keys) | — |
| `openadapter` | `oad` | OpenAdapter | API key | [link](https://openadapter.dev) | Use your OpenAdapter API key in Authorization: Bearer sk-cv-<key>. Fully OpenAI-compatible. API base URL: https://api.openadapter.in/v1. |
| `openai` | `openai` | OpenAI | API key | [link](https://platform.openai.com) | — |
| `opencode-go` | `opencode-go` | OpenCode Go | API key | [link](https://opencode.ai/go) | — |
| `opencode-zen` | `opencode-zen` | OpenCode Zen | API key | [link](https://opencode.ai/zen) | — |
| `openrouter` | `openrouter` | OpenRouter | API key, aggregator | [link](https://openrouter.ai) | Free models at $0/token with :free suffix - 20 RPM / 200 RPD |
| `orcarouter` | `orcarouter` | OrcaRouter | API key | [link](https://www.orcarouter.ai) | — |
| `ovhcloud` | `ovh` | OVHcloud AI | API key | [link](https://www.ovhcloud.com) | — |
| `perplexity` | `pplx` | Perplexity | API key | [link](https://www.perplexity.ai) | — |
| `piapi` | `pi` | PiAPI | API key, aggregator | [link](https://piapi.ai) | — |
| `pioneer` | `pn` | Pioneer AI | API key | [link](https://pioneer.ai) | $75 free usage credits — no credit card required |
| `poe` | `poe` | Poe | API key, aggregator | [link](https://creator.poe.com/api-reference) | Bearer API key for the Poe OpenAI-compatible API. |
| `pollinations` | `pol` | Pollinations AI | API key, video | [link](https://pollinations.ai) | Free keyless tier: openai, openai-fast, openai-large, qwen-coder, mistral, deepseek, grok, gemini-flash-lite-3.1, perplexity-fast, perplexity-reasoning. Premium models (claude, gemini, midijourney) require a Pollinations API key from enter.pollinations.ai. |
| `predibase` | `predibase` | Predibase | API key | [link](https://predibase.com) | ⚠️ **DEPRECATED.** serving.app.predibase.com no longer resolves (sweep 2026-06-19); the managed serving API appears discontinued. |
| `publicai` | `publicai` | PublicAI | API key | [link](https://publicai.co) | Requires an API key — one-time signup credit, then paid |
| `puter` | `pu` | Puter AI | API key | [link](https://puter.com) | Get token at puter.com/dashboard → Copy Auth Token |
| `qianfan` | `qianfan` | Baidu Qianfan | API key | [link](https://cloud.baidu.com/product/wenxinworkshop) | — |
| `qiniu` | `qiniu` | Qiniu | API key | [link](https://www.qiniu.com) | — |
| `recraft` | `recraft` | Recraft | API key, image | [link](https://recraft.ai) | — |
| `reka` | `reka` | Reka | API key | [link](https://docs.reka.ai/chat/overview) | Use your Reka API key. OmniRoute supports the OpenAI-compatible base URL https://api.reka.ai/v1 and sends both Authorization and X-Api-Key headers for compatibility. |
| `requesty` | `requesty` | Requesty | API key | [link](https://requesty.ai) | Free tier ~200 requests/day - multi-model routing gateway (300+ models) |
| `runwayml` | `runway` | Runway | API key, video | [link](https://docs.dev.runwayml.com) | Use your Runway API key in Authorization: Bearer <key>. OmniRoute targets the current Runway API at https://api.dev.runwayml.com/v1 and sends the required X-Runway-Version header automatically. |
| `sambanova` | `samba` | SambaNova | API key | [link](https://sambanova.ai) | $5 free credits on signup (30-day validity), no credit card required |
| `sap` | `sap` | SAP Generative AI Hub | API key, enterprise | [link](https://help.sap.com/docs/sap-ai-core/sap-ai-core-service-guide/generative-ai-hub-in-sap-ai-core) | Use your SAP AI Core bearer token. Base URL can be your AI_API_URL root or a deploymentUrl from Generative AI Hub. |
| `scaleway` | `scw` | Scaleway AI | API key | [link](https://www.scaleway.com/en/docs/ai-data/generative-apis/) | 1M free tokens for new accounts — EU/GDPR compliant (Paris), Qwen3 235B & Llama 70B |
| `sensenova` | `sensenova` | SenseNova | API key | [link](https://platform.sensenova.cn) | Get API key at platform.sensenova.cn |
| `siliconflow` | `siliconflow` | SiliconFlow | API key | [link](https://cloud.siliconflow.com) | $1 free credits plus permanently free models after identity verification |
| `snowflake` | `snowflake` | Snowflake Cortex | API key, enterprise | [link](https://www.snowflake.com) | — |
| `sparkdesk` | `sparkdesk` | SparkDesk | API key | [link](https://xinghuo.xfyun.cn) | Get API key at console.xfyun.cn |
| `stability-ai` | `stability` | Stability AI | API key, image | [link](https://stability.ai) | — |
| `stepfun` | `stepfun` | StepFun | API key | [link](https://stepfun.com) | Get API key at platform.stepfun.com |
| `sumopod` | `sumopod` | SumoPod | API key | [link](https://ai.sumopod.com) | Use your SumoPod API key (sk-...) in Authorization: Bearer <key>. Fully OpenAI-compatible. API base URL: https://ai.sumopod.com/v1. |
| `suno` | `suno` | Suno | API key | [link](https://suno.ai) | Paste session cookie from suno.ai (Clerk auth) |
| `synthetic` | `synthetic` | Synthetic | API key, aggregator | [link](https://synthetic.new) | — |
| `tencent` | `tencent` | Tencent Hunyuan | API key | [link](https://hunyuan.tencent.com) | Get API key at console.cloud.tencent.com |
| `thebai` | `thebai` | TheB.AI | API key, aggregator | [link](https://theb.ai) | Bearer API key for the TheB.AI OpenAI-compatible gateway. |
| `tinyfish` | `tf` | TinyFish Fetch | API key | [link](https://docs.tinyfish.ai/fetch-api) | X-API-Key from agent.tinyfish.ai/api-keys |
| `together` | `together` | Together AI | API key, video | [link](https://www.together.ai) | — |
| `tokenrouter` | `trk` | TokenRouter | API key | [link](https://tokenrouter.com) | Use your TokenRouter API key in Authorization: Bearer <key>. Fully OpenAI-compatible. API base URL: https://api.tokenrouter.com/v1. |
| `topaz` | `topaz` | Topaz | API key, image | [link](https://topazlabs.com) | — |
| `udio` | `udio` | Udio | API key | [link](https://udio.com) | Paste session cookie from udio.com (Supabase auth) |
| `uncloseai` | `unc` | UncloseAI | API key | [link](https://uncloseai.com) | No auth required. API accepts any non-empty string as key for identification. |
| `upstage` | `upstage` | Upstage | API key | [link](https://www.upstage.ai) | — |
| `v0-vercel` | `v0` | v0 (Vercel) | API key | [link](https://v0.dev) | — |
| `venice` | `venice` | Venice.ai | API key | [link](https://venice.ai) | — |
| `vercel-ai-gateway` | `vag` | Vercel AI Gateway | API key, aggregator | [link](https://vercel.com/docs/ai-gateway) | — |
| `vertex` | `vertex` | Vertex AI | API key, enterprise | [link](https://cloud.google.com/vertex-ai) | Provide Service Account JSON or OAuth access_token |
| `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. |
| `volcengine` | `volcengine` | Volcengine | API key | [link](https://www.volcengine.com) | — |
| `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) | — |
| `watsonx` | `watsonx` | IBM watsonx.ai Gateway | API key, enterprise | [link](https://www.ibm.com/products/watsonx-ai) | Use your watsonx bearer token. Base URL can be https://<region>.ml.cloud.ibm.com/ml/gateway/v1/ or a self-managed /ml/gateway/v1 endpoint. |
| `x5lab` | `x5lab` | X5Lab | API key | [link](https://x5lab.dev) | Use your X5Lab API key (x5-...) in Authorization: Bearer <key>. Fully OpenAI-compatible. API base URL: https://api.x5lab.dev/v1. |
| `xai` | `xai` | xAI (Grok) | API key | [link](https://x.ai) | — |
| `xiaomi-mimo` | `mimo` | Xiaomi MiMo | API key | [link](https://mimo.mi.com) | — |
| `yi` | `yi` | Yi (01.AI) | API key | [link](https://01.ai) | Get API key at platform.lingyiwanwu.com |
| `zai` | `zai` | Z.AI | API key | [link](https://open.bigmodel.cn) | — |
| `zenmux` | `zm` | ZenMux | API key | [link](https://zenmux.ai) | Use your ZenMux API key in Authorization: Bearer <key>. ZenMux is fully OpenAI-compatible. Base URL: https://zenmux.ai/api/v1. |
## Local Providers (12)
| ID | Alias | Name | Tags | Website | Notes |
|----|-------|------|------|---------|-------|
| `comfyui` | `comfyui` | ComfyUI | Local | [link](https://github.com/comfyanonymous/ComfyUI) | No API key required. Configure the local ComfyUI base URL (default: http://localhost:8188). |
| `docker-model-runner` | `dmr` | Docker Model Runner | Local, self-hosted | [link](https://docs.docker.com/ai/model-runner/) | API key optional. Configure the local Docker Model Runner OpenAI-compatible base URL (default: http://localhost:12434/v1). |
| `lemonade` | `lemonade` | Lemonade Server | Local, self-hosted | [link](https://lemonade-server.ai) | API key optional. Configure the local Lemonade OpenAI-compatible base URL (default: http://localhost:13305/api/v1). |
| `llama-cpp` | `llamacpp` | llama.cpp | Local, self-hosted | [link](https://github.com/ggml-org/llama.cpp) | API key optional (use any value, e.g. sk-no-key-required). Configure the llama-server OpenAI-compatible base URL (default: http://127.0.0.1:8080/v1). Note: if Llamafile is also installed, both default to port 8080 — run only one at a time or override the port. |
| `llamafile` | `llamafile` | Llamafile | Local, self-hosted | [link](https://github.com/Mozilla-Ocho/llamafile) | API key optional. Configure the local Llamafile OpenAI-compatible base URL (default: http://127.0.0.1:8080/v1). |
| `lm-studio` | `lmstudio` | LM Studio | Local, self-hosted | [link](https://lmstudio.ai) | API key optional. Configure the local LM Studio OpenAI-compatible base URL (default: http://localhost:1234/v1). |
| `ollama-local` | `ollama` | Ollama | Local, self-hosted | [link](https://ollama.com) | No API key required. Ollama runs locally — configure its OpenAI-compatible base URL (default: http://localhost:11434/v1) and make sure Ollama is running before connecting. |
| `oobabooga` | `ooba` | oobabooga | Local, self-hosted | [link](https://github.com/oobabooga/text-generation-webui) | API key optional. Configure the local oobabooga OpenAI-compatible base URL (default: http://localhost:5000/v1). |
| `sdwebui` | `sdwebui` | SD WebUI | Local | [link](https://github.com/AUTOMATIC1111/stable-diffusion-webui) | No API key required. Configure the local WebUI base URL (default: http://localhost:7860). |
| `triton` | `triton` | NVIDIA Triton | Local, self-hosted | [link](https://developer.nvidia.com/triton-inference-server) | API key optional. Configure the Triton OpenAI-compatible base URL (default: http://localhost:8000/v1). |
| `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). |
| ID | Alias | Name | Tags | Website | Notes |
| --------------------- | ------------ | ------------------- | ------------------ | --------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `comfyui` | `comfyui` | ComfyUI | Local | [link](https://github.com/comfyanonymous/ComfyUI) | No API key required. Configure the local ComfyUI base URL (default: http://localhost:8188). |
| `docker-model-runner` | `dmr` | Docker Model Runner | Local, self-hosted | [link](https://docs.docker.com/ai/model-runner/) | API key optional. Configure the local Docker Model Runner OpenAI-compatible base URL (default: http://localhost:12434/v1). |
| `lemonade` | `lemonade` | Lemonade Server | Local, self-hosted | [link](https://lemonade-server.ai) | API key optional. Configure the local Lemonade OpenAI-compatible base URL (default: http://localhost:13305/api/v1). |
| `llama-cpp` | `llamacpp` | llama.cpp | Local, self-hosted | [link](https://github.com/ggml-org/llama.cpp) | API key optional (use any value, e.g. sk-no-key-required). Configure the llama-server OpenAI-compatible base URL (default: http://127.0.0.1:8080/v1). Note: if Llamafile is also installed, both default to port 8080 — run only one at a time or override the port. |
| `llamafile` | `llamafile` | Llamafile | Local, self-hosted | [link](https://github.com/Mozilla-Ocho/llamafile) | API key optional. Configure the local Llamafile OpenAI-compatible base URL (default: http://127.0.0.1:8080/v1). |
| `lm-studio` | `lmstudio` | LM Studio | Local, self-hosted | [link](https://lmstudio.ai) | API key optional. Configure the local LM Studio OpenAI-compatible base URL (default: http://localhost:1234/v1). |
| `ollama-local` | `ollama` | Ollama | Local, self-hosted | [link](https://ollama.com) | No API key required. Ollama runs locally — configure its OpenAI-compatible base URL (default: http://localhost:11434/v1) and make sure Ollama is running before connecting. |
| `oobabooga` | `ooba` | oobabooga | Local, self-hosted | [link](https://github.com/oobabooga/text-generation-webui) | API key optional. Configure the local oobabooga OpenAI-compatible base URL (default: http://localhost:5000/v1). |
| `sdwebui` | `sdwebui` | SD WebUI | Local | [link](https://github.com/AUTOMATIC1111/stable-diffusion-webui) | No API key required. Configure the local WebUI base URL (default: http://localhost:7860). |
| `triton` | `triton` | NVIDIA Triton | Local, self-hosted | [link](https://developer.nvidia.com/triton-inference-server) | API key optional. Configure the Triton OpenAI-compatible base URL (default: http://localhost:8000/v1). |
| `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 (11)
| ID | Alias | Name | Tags | Website | Notes |
|----|-------|------|------|---------|-------|
| `brave-search` | `brave-search` | Brave Search | Search | [link](https://brave.com/search/api) | Subscription token from Brave Search API dashboard |
| `exa-search` | `exa-search` | Exa Search | Search | [link](https://exa.ai) | API key from dashboard.exa.ai |
| `google-pse-search` | `google-pse` | Google Programmable Search | Search | [link](https://developers.google.com/custom-search/v1/overview) | Requires a Google API key and your Programmable Search Engine ID (cx) |
| `linkup-search` | `linkup` | Linkup Search | Search | [link](https://docs.linkup.so) | Bearer API key from the Linkup dashboard |
| `ollama-search` | `ollama-search` | Ollama Search | Search | [link](https://ollama.com/settings/keys) | Same API key as Ollama Cloud (from ollama.com/settings/keys) |
| `perplexity-search` | `pplx-search` | Perplexity Search | Search | [link](https://docs.perplexity.ai/guides/search-quickstart) | Same API key as Perplexity (pplx-...) |
| `searchapi-search` | `searchapi` | SearchAPI | Search | [link](https://www.searchapi.io/docs/google) | API key from SearchAPI (query param or Bearer auth) |
| `searxng-search` | `searxng` | SearXNG Search | Search | [link](https://docs.searxng.org) | API key is optional. Set your SearXNG base URL. Some instances may require a bearer token for access. |
| `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-...) |
| `youcom-search` | `youcom-search` | You.com Search | Search | [link](https://you.com/business/api/) | X-API-Key from the You.com platform dashboard |
| ID | Alias | Name | Tags | Website | Notes |
| ------------------- | --------------- | -------------------------- | ------ | --------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
| `brave-search` | `brave-search` | Brave Search | Search | [link](https://brave.com/search/api) | Subscription token from Brave Search API dashboard |
| `exa-search` | `exa-search` | Exa Search | Search | [link](https://exa.ai) | API key from dashboard.exa.ai |
| `google-pse-search` | `google-pse` | Google Programmable Search | Search | [link](https://developers.google.com/custom-search/v1/overview) | Requires a Google API key and your Programmable Search Engine ID (cx) |
| `linkup-search` | `linkup` | Linkup Search | Search | [link](https://docs.linkup.so) | Bearer API key from the Linkup dashboard |
| `ollama-search` | `ollama-search` | Ollama Search | Search | [link](https://ollama.com/settings/keys) | Same API key as Ollama Cloud (from ollama.com/settings/keys) |
| `perplexity-search` | `pplx-search` | Perplexity Search | Search | [link](https://docs.perplexity.ai/guides/search-quickstart) | Same API key as Perplexity (pplx-...) |
| `searchapi-search` | `searchapi` | SearchAPI | Search | [link](https://www.searchapi.io/docs/google) | API key from SearchAPI (query param or Bearer auth) |
| `searxng-search` | `searxng` | SearXNG Search | Search | [link](https://docs.searxng.org) | API key is optional. Set your SearXNG base URL. Some instances may require a bearer token for access. |
| `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-...) |
| `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 (7)
| ID | Alias | Name | Tags | Website | Notes |
|----|-------|------|------|---------|-------|
| `assemblyai` | `aai` | AssemblyAI | Audio | [link](https://assemblyai.com) | — |
| `aws-polly` | `polly` | AWS Polly | Audio | [link](https://aws.amazon.com/polly/) | Use AWS Secret Access Key as API key; set providerSpecificData.accessKeyId and optional region. |
| `cartesia` | `cartesia` | Cartesia | Audio | [link](https://cartesia.ai) | — |
| `deepgram` | `dg` | Deepgram | Audio | [link](https://deepgram.com) | — |
| `elevenlabs` | `el` | ElevenLabs | Audio | [link](https://elevenlabs.io) | — |
| `inworld` | `inworld` | Inworld | Audio | [link](https://inworld.ai) | — |
| `playht` | `playht` | PlayHT | Audio | [link](https://play.ht) | — |
| ID | Alias | Name | Tags | Website | Notes |
| ------------ | ---------- | ---------- | ----- | ------------------------------------- | ----------------------------------------------------------------------------------------------- |
| `assemblyai` | `aai` | AssemblyAI | Audio | [link](https://assemblyai.com) | — |
| `aws-polly` | `polly` | AWS Polly | Audio | [link](https://aws.amazon.com/polly/) | Use AWS Secret Access Key as API key; set providerSpecificData.accessKeyId and optional region. |
| `cartesia` | `cartesia` | Cartesia | Audio | [link](https://cartesia.ai) | — |
| `deepgram` | `dg` | Deepgram | Audio | [link](https://deepgram.com) | — |
| `elevenlabs` | `el` | ElevenLabs | Audio | [link](https://elevenlabs.io) | — |
| `inworld` | `inworld` | Inworld | Audio | [link](https://inworld.ai) | — |
| `playht` | `playht` | PlayHT | Audio | [link](https://play.ht) | — |
## Upstream Proxy Providers (2)
| ID | Alias | Name | Tags | Website | Notes |
|----|-------|------|------|---------|-------|
| `9router` | `nr` | 9router | Upstream proxy | [link](https://www.npmjs.com/package/9router) | — |
| `cliproxyapi` | `cpa` | CLIProxyAPI | Upstream proxy | [link](https://github.com/router-for-me/CLIProxyAPI) | — |
| ID | Alias | Name | Tags | Website | Notes |
| ------------- | ----- | ----------- | -------------- | ---------------------------------------------------- | ----- |
| `9router` | `nr` | 9router | Upstream proxy | [link](https://www.npmjs.com/package/9router) | — |
| `cliproxyapi` | `cpa` | CLIProxyAPI | Upstream proxy | [link](https://github.com/router-for-me/CLIProxyAPI) | — |
## Cloud Agent Providers (3)
| ID | Alias | Name | Tags | Website | Notes |
|----|-------|------|------|---------|-------|
| `codex-cloud` | `codex-cloud` | Codex Cloud | Cloud agent | [link](https://openai.com/codex) | OpenAI API key with Codex Cloud task access. |
| `devin` | `devin` | Devin | Cloud agent | [link](https://devin.ai) | Devin API key for cloud agent sessions. |
| `jules` | `jules` | Google Jules | Cloud agent | [link](https://jules.google) | Jules API key for creating and managing cloud coding tasks. |
| ID | Alias | Name | Tags | Website | Notes |
| ------------- | ------------- | ------------ | ----------- | -------------------------------- | ----------------------------------------------------------- |
| `codex-cloud` | `codex-cloud` | Codex Cloud | Cloud agent | [link](https://openai.com/codex) | OpenAI API key with Codex Cloud task access. |
| `devin` | `devin` | Devin | Cloud agent | [link](https://devin.ai) | Devin API key for cloud agent sessions. |
| `jules` | `jules` | Google Jules | Cloud agent | [link](https://jules.google) | Jules API key for creating and managing cloud coding tasks. |
## System Providers (1)
| ID | Alias | Name | Tags | Website | Notes |
|----|-------|------|------|---------|-------|
| `auto` | `auto` | Auto (Zero-Config) | System | — | — |
| ID | Alias | Name | Tags | Website | Notes |
| ------ | ------ | ------------------ | ------ | ------- | ----- |
| `auto` | `auto` | Auto (Zero-Config) | System | — | — |
## Sources of truth

View File

@@ -602,7 +602,7 @@ See `docs/marketing/TIERS.md` for tier definitions and provider classification.
### Deterministic routing-decision matrix (`npm run test:combo:matrix`)
`tests/integration/combo-matrix/*.test.ts` proves the routing **decision** of all 17
`tests/integration/combo-matrix/*.test.ts` proves the routing **decision** of all 18
public strategies end-to-end through the real combo pipeline with a mocked upstream.
Coverage includes:

View File

@@ -234,7 +234,6 @@ export const FREE_MODEL_BUDGETS: FreeModelBudget[] = [
{ provider: "hyperbolic", modelId: "Qwen/Qwen2.5-Coder-32B-Instruct", displayName: "Qwen 2.5 Coder 32B", monthlyTokens: 0, creditTokens: 5000000, freeType: "one-time-initial", poolKey: "hyperbolic", tos: "ok" },
{ provider: "hyperbolic", modelId: "NousResearch/Hermes-3-Llama-3.1-70B", displayName: "Hermes 3 70B", monthlyTokens: 0, creditTokens: 5000000, freeType: "one-time-initial", poolKey: "hyperbolic", tos: "ok" },
{ provider: "iflytek", modelId: "generalv3.5", displayName: "General V3.5", monthlyTokens: 0, creditTokens: 0, freeType: "keyless", poolKey: "iflytek", tos: "avoid" },
{ provider: "inclusionai", modelId: "inclusion-model", displayName: "Inclusion Model", monthlyTokens: 15000000, creditTokens: 0, freeType: "recurring-daily", poolKey: "inclusionai", tos: "ambiguous" },
{ provider: "inference-net", modelId: "meta-llama/Llama-3.3-70B-Instruct", displayName: "meta-llama/Llama-3.3-70B-Instruct", monthlyTokens: 0, creditTokens: 0, freeType: "recurring-monthly", poolKey: "inference-net", tos: "caution" },
{ provider: "inference-net", modelId: "deepseek-ai/DeepSeek-R1", displayName: "deepseek-ai/DeepSeek-R1", monthlyTokens: 0, creditTokens: 0, freeType: "recurring-monthly", poolKey: "inference-net", tos: "caution" },
{ provider: "inference-net", modelId: "Qwen/Qwen2.5-72B-Instruct", displayName: "Qwen/Qwen2.5-72B-Instruct", monthlyTokens: 0, creditTokens: 0, freeType: "recurring-monthly", poolKey: "inference-net", tos: "caution" },

View File

@@ -21,7 +21,6 @@ export const FREE_TIER_BUDGETS: Record<string, number> = {
"ollama-cloud": 20_000_000,
"github-models": 18_000_000,
groq: 15_000_000,
inclusionai: 15_000_000,
bluesminds: 7_200_000,
sambanova: 6_000_000,
"arcee-ai": 4_800_000,

View File

@@ -16,7 +16,6 @@ import { upstageProvider } from "./registry/upstage/index.ts";
import { nebiusProvider } from "./registry/nebius/index.ts";
import { fireworksProvider } from "./registry/fireworks/index.ts";
import { llamagateProvider } from "./registry/llamagate/index.ts";
import { inclusionaiProvider } from "./registry/inclusionai/index.ts";
import { glmProvider } from "./registry/glm/index.ts";
import { glmtProvider } from "./registry/glm/t/index.ts";
import { glm_cnProvider } from "./registry/glm/cn/index.ts";
@@ -88,7 +87,6 @@ import { sensenovaProvider } from "./registry/sensenova/index.ts";
import { hyperbolicProvider } from "./registry/hyperbolic/index.ts";
import { lambda_aiProvider } from "./registry/lambda-ai/index.ts";
import { t3_webProvider } from "./registry/t3-web/index.ts";
import { klusterProvider } from "./registry/kluster/index.ts";
import { iflytekProvider } from "./registry/iflytek/index.ts";
import { crofProvider } from "./registry/crof/index.ts";
import { moonshotProvider } from "./registry/moonshot/index.ts";
@@ -106,7 +104,6 @@ import { nscaleProvider } from "./registry/nscale/index.ts";
import { chatgpt_webProvider } from "./registry/chatgpt-web/index.ts";
import { openrouterProvider } from "./registry/openrouter/index.ts";
import { orcarouterProvider } from "./registry/orcarouter/index.ts";
import { glhfProvider } from "./registry/glhf/index.ts";
import { copilot_webProvider } from "./registry/copilot-web/index.ts";
import { copilot_m365_webProvider } from "./registry/copilot-m365-web/index.ts";
import { stepfunProvider } from "./registry/stepfun/index.ts";
@@ -202,7 +199,6 @@ export const REGISTRY: Record<string, RegistryEntry> = {
nebius: nebiusProvider,
fireworks: fireworksProvider,
llamagate: llamagateProvider,
inclusionai: inclusionaiProvider,
glm: glmProvider,
glmt: glmtProvider,
"glm-cn": glm_cnProvider,
@@ -274,7 +270,6 @@ export const REGISTRY: Record<string, RegistryEntry> = {
hyperbolic: hyperbolicProvider,
"lambda-ai": lambda_aiProvider,
"t3-web": t3_webProvider,
kluster: klusterProvider,
iflytek: iflytekProvider,
crof: crofProvider,
moonshot: moonshotProvider,
@@ -292,7 +287,6 @@ export const REGISTRY: Record<string, RegistryEntry> = {
"chatgpt-web": chatgpt_webProvider,
openrouter: openrouterProvider,
orcarouter: orcarouterProvider,
glhf: glhfProvider,
"copilot-web": copilot_webProvider,
"copilot-m365-web": copilot_m365_webProvider,
stepfun: stepfunProvider,

View File

@@ -1,12 +0,0 @@
import type { RegistryEntry } from "../../shared.ts";
export const glhfProvider: RegistryEntry = {
id: "glhf",
alias: "glhf",
format: "openai",
executor: "default",
baseUrl: "https://api.laf.run/v1/chat/completions",
authType: "apikey",
authHeader: "bearer",
models: [{ id: "deepseek-7b-chat", name: "DeepSeek 7B Chat" }],
};

View File

@@ -1,12 +0,0 @@
import type { RegistryEntry } from "../../shared.ts";
export const inclusionaiProvider: RegistryEntry = {
id: "inclusionai",
alias: "inclusionai",
format: "openai",
executor: "default",
baseUrl: "https://api.inclusionai.tech/v1/chat/completions",
authType: "apikey",
authHeader: "bearer",
models: [{ id: "inclusion-model", name: "Inclusion Model" }],
};

View File

@@ -1,12 +0,0 @@
import type { RegistryEntry } from "../../shared.ts";
export const klusterProvider: RegistryEntry = {
id: "kluster",
alias: "kluster",
format: "openai",
executor: "default",
baseUrl: "https://api.kluster.ai/v1/chat/completions",
authType: "apikey",
authHeader: "bearer",
models: [{ id: "auto", name: "Auto" }],
};

View File

@@ -10,12 +10,72 @@ export const syntheticProvider: RegistryEntry = {
authType: "apikey",
authHeader: "bearer",
models: [
{ id: "hf:nvidia/Kimi-K2.5-NVFP4", name: "Kimi K2.5 (NVFP4)" },
{ id: "hf:MiniMaxAI/MiniMax-M2.5", name: "MiniMax M2.5" },
{ id: "hf:zai-org/GLM-4.7-Flash", name: "GLM 4.7 Flash" },
{ id: "hf:zai-org/GLM-4.7", name: "GLM 4.7" },
{ id: "hf:moonshotai/Kimi-K2.5", name: "Kimi K2.5" },
{ id: "hf:deepseek-ai/DeepSeek-V3.2", name: "DeepSeek V3.2" },
{
id: "hf:openai/gpt-oss-120b",
name: "openai/gpt-oss-120b",
aliases: ["syn:gpt-oss-120b"],
contextLength: 131072,
maxOutputTokens: 65536,
toolCalling: true,
supportsReasoning: true,
},
{
id: "hf:zai-org/GLM-5.2",
name: "zai-org/GLM-5.2",
aliases: ["syn:large:text"],
contextLength: 524288,
maxOutputTokens: 65536,
toolCalling: true,
supportsReasoning: true,
},
{
id: "hf:moonshotai/Kimi-K2.7-Code",
name: "moonshotai/Kimi-K2.7-Code",
aliases: ["syn:large:vision"],
contextLength: 262144,
maxOutputTokens: 65536,
toolCalling: true,
supportsReasoning: true,
supportsVision: true,
},
{
id: "hf:Qwen/Qwen3.6-27B",
name: "Qwen/Qwen3.6-27B",
aliases: ["syn:small:vision"],
contextLength: 262144,
maxOutputTokens: 65536,
toolCalling: true,
supportsReasoning: true,
supportsVision: true,
},
{
id: "hf:MiniMaxAI/MiniMax-M3",
name: "MiniMaxAI/MiniMax-M3",
aliases: ["syn:minimax-m3"],
contextLength: 262144,
maxOutputTokens: 65536,
toolCalling: true,
supportsReasoning: true,
supportsVision: true,
},
{
id: "hf:zai-org/GLM-4.7-Flash",
name: "zai-org/GLM-4.7-Flash",
aliases: ["syn:small:text"],
contextLength: 196608,
maxOutputTokens: 65536,
toolCalling: true,
supportsReasoning: true,
},
{
id: "hf:nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-NVFP4",
name: "nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-NVFP4",
aliases: ["syn:nemotron-3-super"],
contextLength: 262144,
maxOutputTokens: 65536,
toolCalling: true,
supportsReasoning: true,
},
],
passthroughModels: true,
};

View File

@@ -46,6 +46,7 @@ import { buildGitLabOAuthEndpoints, GITLAB_DUO_DEFAULT_BASE_URL } from "@/lib/oa
export interface RegistryModel {
id: string;
name: string;
aliases?: readonly string[];
toolCalling?: boolean;
supportsReasoning?: boolean;
supportsVision?: boolean;

View File

@@ -57,14 +57,41 @@ function trackToolName(
getRequestToolNameMap(body).set(titleCaseName, originalName);
}
/**
* Names of Anthropic server-side tools declared in this request's tools[].
* A server tool's `name` is a reserved literal validated against its `type`
* (web_search_20250305 ⇒ "web_search", bash_20250124 ⇒ "bash", …), so every
* rewrite below must leave both the declaration AND any history/tool_choice
* reference to it untouched — renaming only one side produces
* `Tool 'WebSearch' not found in provided tools` (history renamed, tools[]
* preserved) or `tools.N.<type>.name: Input should be '<literal>'` (tools[]
* renamed).
*/
function collectServerToolNames(tools: unknown): Set<string> {
const names = new Set<string>();
if (!Array.isArray(tools)) return names;
for (const tool of tools) {
const t = tool as Record<string, unknown> | null;
if (t && isAnthropicServerToolType(t.type) && typeof t.name === "string") {
names.add(t.name);
}
}
return names;
}
export function remapToolNamesInRequest(body: Record<string, unknown>): boolean {
let hasLowercase = false;
let hasTitleCase = false;
const serverToolNames = collectServerToolNames(body.tools);
// Remap tool definitions
const tools = body.tools as Array<Record<string, unknown>> | undefined;
if (Array.isArray(tools)) {
for (const tool of tools) {
if (!tool) continue;
// Server tools (bash_20250124 / web_search_20250305 / …) keep their
// type-bound literal name.
if (isAnthropicServerToolType(tool.type)) continue;
const name = String(tool.name || "");
if (TOOL_RENAME_MAP[name]) {
const mapped = TOOL_RENAME_MAP[name];
@@ -85,6 +112,7 @@ export function remapToolNamesInRequest(body: Record<string, unknown>): boolean
if (!Array.isArray(content)) continue;
for (const block of content) {
if (block.type === "tool_use" && typeof block.name === "string") {
if (serverToolNames.has(block.name)) continue;
const mapped = TOOL_RENAME_MAP[block.name];
if (mapped) {
const originalName = block.name;
@@ -101,7 +129,11 @@ export function remapToolNamesInRequest(body: Record<string, unknown>): boolean
// Remap tool_choice
const toolChoice = body.tool_choice as Record<string, unknown> | undefined;
if (toolChoice?.type === "tool" && typeof toolChoice.name === "string") {
if (
toolChoice?.type === "tool" &&
typeof toolChoice.name === "string" &&
!serverToolNames.has(toolChoice.name)
) {
const mapped = TOOL_RENAME_MAP[toolChoice.name];
if (mapped) {
const originalName = toolChoice.name;
@@ -248,6 +280,11 @@ export function cloakThirdPartyToolNames(
const shouldCloak = (name: string): boolean =>
needsThirdPartyCloak(name) && !(options?.skip ? options.skip(name) : false);
const tools = body.tools as Array<Record<string, unknown>> | undefined;
// Reserved literal names of declared server tools — never cloaked, neither
// in tools[] (guarded below) nor in message-history / tool_choice references
// (renaming only the reference yields "Tool 'WebSearch' not found in
// provided tools").
const serverToolNames = collectServerToolNames(tools);
const used = new Set<string>();
if (Array.isArray(tools)) {
@@ -274,7 +311,9 @@ export function cloakThirdPartyToolNames(
// subagents->SubDispatch, session_status->CheckStatus, webfetch->WebFetch, …
// Then harness-canonical (read_file->Read), then a generic PascalCase.
const base =
TOOL_RENAME_MAP[original] ?? HARNESS_CANONICAL_MAP[original] ?? toPascalCaseToolName(original);
TOOL_RENAME_MAP[original] ??
HARNESS_CANONICAL_MAP[original] ??
toPascalCaseToolName(original);
let alias = base;
let suffix = 2;
while (alias !== original && used.has(alias)) {
@@ -315,6 +354,7 @@ export function cloakThirdPartyToolNames(
if (
block?.type === "tool_use" &&
typeof block.name === "string" &&
!serverToolNames.has(block.name) &&
shouldCloak(block.name)
) {
changed = true;
@@ -330,6 +370,7 @@ export function cloakThirdPartyToolNames(
if (
toolChoice?.type === "tool" &&
typeof toolChoice.name === "string" &&
!serverToolNames.has(toolChoice.name) &&
shouldCloak(toolChoice.name)
) {
body.tool_choice = { ...toolChoice, name: aliasFor(toolChoice.name) };

View File

@@ -3032,7 +3032,10 @@ async function handleRoundRobinCombo({
resilienceSettings.providerCooldown.enabled &&
provider &&
provider !== "unknown" &&
!(result.status === 500 && hasPerModelQuota(provider, parseModel(modelStr).model || modelStr))
!(
result.status === 500 &&
hasPerModelQuota(provider, parseModel(modelStr).model || modelStr)
)
) {
recordProviderCooldown(
provider,

View File

@@ -90,9 +90,7 @@ export function phaseComboSetup(ctx: ComboContext): ComboSetup {
const universalHandoffConfig = resolveUniversalHandoffConfig(
(combo.universal_handoff || combo.universalHandoff) as
| Record<string, unknown>
| null
| undefined,
Record<string, unknown> | null | undefined,
relayOptions?.universalHandoffConfig as Record<string, unknown> | null | undefined
);

View File

@@ -66,6 +66,15 @@ const PROVIDER_MODEL_ALIASES: ProviderModelAliasMap = {
"gpt-oss-20b": "openai/gpt-oss-20b",
"nvidia/gpt-oss-20b": "openai/gpt-oss-20b",
},
synthetic: {
"syn:gpt-oss-120b": "hf:openai/gpt-oss-120b",
"syn:large:text": "hf:zai-org/GLM-5.2",
"syn:large:vision": "hf:moonshotai/Kimi-K2.7-Code",
"syn:small:vision": "hf:Qwen/Qwen3.6-27B",
"syn:minimax-m3": "hf:MiniMaxAI/MiniMax-M3",
"syn:small:text": "hf:zai-org/GLM-4.7-Flash",
"syn:nemotron-3-super": "hf:nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-NVFP4",
},
// Antigravity model aliases must be applied by the Antigravity executor, not by
// the global model resolver. Applying them here rewrites the client-visible model
// before credential/account routing and before UI/logging, causing clean IDs like

View File

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

@@ -0,0 +1,52 @@
import Tooltip from "@/shared/components/Tooltip";
type TranslationFn = {
(key: string): string;
has?: (key: string) => boolean;
};
type Props = {
config: Record<string, any>;
setConfig: (config: Record<string, any>) => void;
t: TranslationFn;
};
function getI18nOrFallback(t: TranslationFn, key: string, fallback: string): string {
try {
if (typeof t.has === "function" && t.has(key)) return t(key);
} catch {}
return fallback;
}
export default function ReasoningTokenBufferToggle({ config, setConfig, t }: Props) {
return (
<div className="flex items-center gap-2 py-1">
<input
type="checkbox"
id="reasoningTokenBufferEnabled"
data-testid="combo-reasoning-token-buffer-enabled"
checked={config.reasoningTokenBufferEnabled !== false}
onChange={(e) => setConfig({ ...config, reasoningTokenBufferEnabled: e.target.checked })}
className="w-3.5 h-3.5 rounded border border-black/20 dark:border-white/20 accent-primary cursor-pointer"
/>
<label
htmlFor="reasoningTokenBufferEnabled"
className="text-xs text-text-muted cursor-pointer select-none"
>
{getI18nOrFallback(t, "reasoningTokenBuffer", "Reasoning token buffer")}
</label>
<Tooltip
position="bottom"
content={getI18nOrFallback(
t,
"advancedHelp.reasoningTokenBuffer",
"When enabled (default), OmniRoute may increase max_tokens for reasoning-capable models so they have headroom to think. Turn this off if you need this combo to preserve the client's exact max_tokens."
)}
>
<span className="material-symbols-outlined text-[12px] text-text-muted cursor-help">
help
</span>
</Tooltip>
</div>
);
}

View File

@@ -15,6 +15,7 @@ import Tooltip from "@/shared/components/Tooltip";
import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard";
import { FieldLabelWithHelp, WeightTotalBar } from "./parts";
import { ResponseValidationEditor, type ResponseValidationValue } from "./ResponseValidationEditor";
import ReasoningTokenBufferToggle from "./ReasoningTokenBufferToggle";
import { pickDisplayValue } from "@/shared/utils/maskEmail";
import useEmailPrivacyStore from "@/store/emailPrivacyStore";
import { useNotificationStore } from "@/store/notificationStore";
@@ -217,10 +218,6 @@ function sanitizeComboRuntimeConfig(config) {
);
}
// Build the next combo config when a Fusion tuning field changes. Prunes empty /
// non-finite entries and drops the whole `fusionTuning` object when no field is
// set, so an empty `{}` is never persisted (sanitizeComboRuntimeConfig keeps any
// non-null object as-is).
function updateFusionTuning(config, field, rawValue) {
const value = rawValue === "" ? undefined : Number(rawValue);
const next = { ...(config.fusionTuning || {}), [field]: value };
@@ -522,9 +519,7 @@ function getStrategyBadgeClass(strategy) {
function getI18nOrFallback(t, key, fallback) {
try {
if (typeof t.has === "function" && t.has(key)) return t(key);
} catch {
// Some translations require ICU variables; fallback keeps optional helper text safe.
}
} catch {}
return fallback;
}
@@ -1023,7 +1018,6 @@ export default function CombosPage() {
return (
<div className="flex flex-col gap-6">
{/* Header */}
<div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
<div>
<h1 className="text-2xl font-semibold">{t("title")}</h1>
@@ -2063,7 +2057,6 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders, combo
}
}, [builderStage, comboBuilderStages]);
// DnD state
const hasPricingForModel = useCallback(
(modelValue) => {
const parsed = parseQualifiedModel(modelValue);
@@ -2752,9 +2745,7 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders, combo
saveData.description = null;
}
// Include config only if any values are set
const configToSave = sanitizeComboRuntimeConfig(config);
// Add round-robin specific fields to config
if (strategy === "round-robin") {
if (config.concurrencyPerModel !== undefined)
configToSave.concurrencyPerModel = config.concurrencyPerModel;
@@ -3740,7 +3731,6 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders, combo
/>
</div>
</div>
{/* failoverBeforeRetry + maxSetRetries + setRetryDelayMs */}
<div className="grid grid-cols-2 gap-2 pt-2 border-t border-black/5 dark:border-white/5">
<div className="col-span-2">
<div className="flex items-center gap-2 py-1">
@@ -3776,6 +3766,9 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders, combo
</Tooltip>
</div>
</div>
<div className="col-span-2">
<ReasoningTokenBufferToggle config={config} setConfig={setConfig} t={t} />
</div>
<div>
<FieldLabelWithHelp
label={t("maxSetRetries")}
@@ -4166,7 +4159,11 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders, combo
</div>
<div>
<FieldLabelWithHelp
label={getI18nOrFallback(t, "fusionStragglerGraceMs", "Straggler grace (ms)")}
label={getI18nOrFallback(
t,
"fusionStragglerGraceMs",
"Straggler grace (ms)"
)}
help={getI18nOrFallback(
t,
"fusionStragglerGraceMsHelp",
@@ -4181,7 +4178,9 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders, combo
value={config.fusionTuning?.stragglerGraceMs ?? ""}
placeholder="8000"
onChange={(e) =>
setConfig(updateFusionTuning(config, "stragglerGraceMs", e.target.value))
setConfig(
updateFusionTuning(config, "stragglerGraceMs", e.target.value)
)
}
className="w-full text-xs py-1.5 px-2 rounded border border-black/10 dark:border-white/10 bg-transparent focus:border-primary focus:outline-none"
/>
@@ -4580,7 +4579,6 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders, combo
</div>
)}
{/* Actions */}
{isExpertMode ? (
<div className="flex gap-2 pt-1">
<Button onClick={onClose} variant="ghost" fullWidth size="sm">

View File

@@ -227,6 +227,68 @@ describe("PassthroughModelRow — render smoke test", () => {
});
});
describe("PassthroughModelsSection — catalog model fallback", () => {
let container: HTMLElement;
let root: ReturnType<typeof createRoot>;
beforeEach(() => {
container = document.createElement("div");
document.body.appendChild(container);
root = createRoot(container);
});
afterEach(() => {
act(() => {
root.unmount();
});
container.remove();
});
it("renders built-in catalog models even when no models were imported", async () => {
const { default: PassthroughModelsSection } =
await import("../components/PassthroughModelsSection");
await act(async () => {
root.render(
<PassthroughModelsSection
providerAlias="synthetic"
providerId="synthetic"
connectionId=""
modelAliases={{}}
catalogModels={[
{
id: "hf:zai-org/GLM-5.2",
name: "zai-org/GLM-5.2",
aliases: ["syn:large:text"],
},
]}
availableModels={[]}
customModels={[]}
description="Synthetic accepts provider-native model IDs."
inputLabel="Model ID"
inputPlaceholder="hf:zai-org/GLM-5.2"
copied={undefined}
onCopy={vi.fn()}
onSetAlias={vi.fn().mockResolvedValue(undefined)}
onDeleteAlias={vi.fn()}
t={(k) => k}
effectiveModelNormalize={() => false}
effectiveModelPreserveDeveloper={() => true}
getUpstreamHeadersRecord={() => ({})}
saveModelCompatFlags={vi.fn().mockResolvedValue(undefined)}
isModelHidden={() => false}
onToggleHidden={vi.fn().mockResolvedValue(undefined)}
onBulkToggleHidden={vi.fn().mockResolvedValue(undefined)}
/>
);
});
expect(container.textContent).toContain("synthetic/syn:large:text");
expect(container.textContent).toContain("syn:large:text");
expect(container.textContent).toContain("Built-in");
});
});
describe("ModelVisibilityToolbar — render smoke test", () => {
let container: HTMLElement;
let root: ReturnType<typeof createRoot>;

View File

@@ -48,6 +48,7 @@ export type ModelCompatSavePatchPassthrough = {
export interface PassthroughModelsSectionProps {
providerAlias: string;
modelAliases: Record<string, string>;
catalogModels?: CompatModelRow[];
availableModels?: CompatModelRow[];
customModels?: CompatModelRow[];
description: string;
@@ -80,6 +81,11 @@ export interface PassthroughModelsSectionProps {
onAutoHideFailedChange?: (v: boolean) => void;
}
function getDefaultModelAlias(model: CompatModelRow): string | null {
const [firstAlias] = model.aliases || [];
return typeof firstAlias === "string" && firstAlias.trim() ? firstAlias.trim() : null;
}
// ---------------------------------------------------------------------------
// Component
// ---------------------------------------------------------------------------
@@ -87,6 +93,7 @@ export interface PassthroughModelsSectionProps {
export default function PassthroughModelsSection({
providerAlias,
modelAliases,
catalogModels = [],
availableModels = [],
customModels = [],
description,
@@ -237,11 +244,13 @@ export default function PassthroughModelsSection({
const addModel = (model: CompatModelRow, source: string) => {
if (!model?.id || seenModelIds.has(model.id)) return;
const fullModel = fullModelByModelId.get(model.id) || `${providerAlias}/${model.id}`;
const defaultAlias = getDefaultModelAlias(model);
const fullModel =
fullModelByModelId.get(model.id) || `${providerAlias}/${defaultAlias || model.id}`;
rows.push({
modelId: model.id,
fullModel,
alias: aliasByModelId.get(model.id) || null,
alias: aliasByModelId.get(model.id) || defaultAlias,
displayName: model.name || model.id,
source,
isFree:
@@ -258,6 +267,10 @@ export default function PassthroughModelsSection({
addModel(model, "imported");
}
for (const model of catalogModels) {
addModel(model, "system");
}
for (const model of customModels) {
addModel(
model,
@@ -291,6 +304,7 @@ export default function PassthroughModelsSection({
return rows;
}, [
availableModels,
catalogModels,
customModelMap,
customModels,
isModelHidden,

View File

@@ -306,6 +306,7 @@ export default function ProviderModelsSection({
<PassthroughModelsSection
providerAlias={providerAlias}
modelAliases={modelAliases}
catalogModels={models}
availableModels={syncedAvailableModels}
customModels={modelMeta.customModels}
description={passthroughDescription}

View File

@@ -64,6 +64,7 @@ export type CompatByProtocolMap = Partial<
export type CompatModelRow = {
id?: string;
name?: string;
/** optional registry aliases for display/import */ aliases?: readonly string[];
source?: string;
apiFormat?: string;
supportedEndpoints?: string[];
@@ -77,7 +78,6 @@ export type CompatModelRow = {
};
export type CompatModelMap = Map<string, CompatModelRow>;
export type HeaderDraftRow = { id: string; name: string; value: string };
// ---------------------------------------------------------------------------
@@ -85,7 +85,6 @@ export type HeaderDraftRow = { id: string; name: string; value: string };
// outside the .tsx). Returns the i18n key for a targetFormat value, or null when the
// value is unknown (the caller then renders the raw value verbatim).
// ---------------------------------------------------------------------------
const TARGET_FORMAT_BADGE_I18N_KEYS: Record<string, string> = {
openai: "compatProtocolOpenAI",
"openai-responses": "compatProtocolOpenAIResponses",

View File

@@ -292,8 +292,7 @@ export default function ComboDefaultsTab() {
// Filtered provider list — excludes already-added ones, filtered by search query
const filteredProviders = availableProviders.filter(
(p) =>
!providerOverrides[p.provider] && matchesSearch(p.provider, searchQuery)
(p) => !providerOverrides[p.provider] && matchesSearch(p.provider, searchQuery)
);
const handleDropdownKeyDown = (e: React.KeyboardEvent) => {

View File

@@ -131,14 +131,6 @@ export const PROVIDER_MODELS_CONFIG: Record<string, ProviderModelsConfigEntry> =
authPrefix: "Bearer ",
parseResponse: (data) => data.data || [],
},
glhf: {
url: "https://glhf.chat/api/openai/v1/models",
method: "GET",
headers: { "Content-Type": "application/json" },
authHeader: "Authorization",
authPrefix: "Bearer ",
parseResponse: (data) => data.data || data.models || [],
},
aimlapi: {
// #5570: AI/ML API's live catalog (400+ models) lives at the public,
// auth-free /models database endpoint (NOT /v1/models). The registry has no

View File

@@ -11,6 +11,7 @@ import { upsertAgentBridgeState } from "@/lib/db/agentBridgeState";
import { getCachedPassword } from "@/mitm/manager";
import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error";
import { createErrorResponse } from "@/lib/api/errorResponse";
import { ALL_TARGETS } from "@/mitm/targets/index";
type Params = { params: { id: string } };
@@ -33,6 +34,12 @@ export async function POST(request: Request, { params }: Params): Promise<Respon
});
}
// Validate the agent ID maps to a known target.
const target = ALL_TARGETS.find((t) => t.id === id);
if (!target) {
return createErrorResponse({ status: 404, message: `Unknown agent: ${id}` });
}
const { enabled } = parsed.data;
const raw = body as Record<string, unknown>;
const sudoPassword =
@@ -40,9 +47,9 @@ export async function POST(request: Request, { params }: Params): Promise<Respon
try {
if (enabled) {
await addDNSEntry(sudoPassword);
await addDNSEntry(sudoPassword, id);
} else {
await removeDNSEntry(sudoPassword);
await removeDNSEntry(sudoPassword, id);
}
upsertAgentBridgeState({ agent_id: id, dns_enabled: enabled });

View File

@@ -107,11 +107,16 @@ export { isRetryableProxyTarget, isSecurityBlockError } from "./validation/trans
export async function validateWebCookieProvider({
provider,
apiKey,
providerSpecificData = {},
}: any) {
providerSpecificData: _providerSpecificData = {},
}: {
provider: string;
apiKey?: string;
providerSpecificData?: Record<string, unknown>;
}) {
try {
const entry = getRegistryEntry(provider);
if (!entry) {
const cookieProvider = WEB_COOKIE_PROVIDERS[provider as keyof typeof WEB_COOKIE_PROVIDERS];
if (!entry && !cookieProvider) {
return { valid: false, error: "Provider not found in registry", unsupported: true };
}
@@ -121,9 +126,26 @@ export async function validateWebCookieProvider({
return { valid: false, error: "Cookie required for web-cookie provider", unsupported: false };
}
if (!entry) {
// Providers listed in WEB_COOKIE_PROVIDERS without a providerRegistry entry (e.g.
// lmarena, gemini-business, poe-web, venice-web, v0-vercel-web) only expose a
// marketing website URL, not a real API host. Probing `${website}/models`
// does not reliably signal session validity for these —
// live verification showed most return redirects or SPA 200s regardless of
// cookie validity, which would silently report an expired/garbage cookie as
// "OK" (worse than an honest "not supported"). Until each of these providers
// has a verified, side-effect-free auth probe against its real API host, report
// unsupported instead of a false positive.
return {
valid: false,
error: "Provider validation not supported",
unsupported: true,
};
}
// Attempt a minimal request to check if the session is valid
// Use /models endpoint or a minimal completion request depending on the provider
const baseUrl = entry.baseUrl || "";
const baseUrl = normalizeBaseUrl(entry.baseUrl || "");
const testUrl = `${baseUrl}/models`;
const res = await directHttpsRequest(
@@ -132,6 +154,7 @@ export async function validateWebCookieProvider({
method: "GET",
headers: {
"User-Agent": STANDARD_USER_AGENT,
Cookie: cookie,
},
},
10_000
@@ -150,7 +173,7 @@ export async function validateWebCookieProvider({
// a 401/403 from the /models probe is the only definitive "session expired" signal
// for web-cookie auth, so a non-auth status is treated as a valid session.
return { valid: true, error: null, unsupported: false };
} catch (error: any) {
} catch (error: unknown) {
return toValidationErrorResult(error);
}
}

View File

@@ -0,0 +1,479 @@
/**
* Container runtime providers for the OmniRoute skill sandbox.
*
* The sandbox historically hardcoded the `docker` CLI. This module abstracts
* the container runtime so OmniRoute can pick the most performant / native
* runtime available on each host:
*
* - macOS: Apple Container (`container` CLI) > OrbStack (docker shim) > Podman > Docker
* - Windows: WSL Container (`wslc` CLI) > Docker Desktop > Podman
* - Linux: Podman (rootless, daemonless) > Docker
*
* The user can override the auto-detected choice with `SKILLS_SANDBOX_RUNTIME`
* (`auto | docker | apple | wsl | orbstack | podman`). Each provider maps the
* sandbox's intent (resource caps, network isolation, capability drops,
* read-only fs, tmpfs workspaces) onto the runtime's native flag set.
*/
import { createRequire } from "module";
import os from "os";
const require = createRequire(import.meta.url);
const childProcess = require("child_process") as typeof import("child_process");
export type SandboxRuntimeId = "docker" | "apple" | "wsl" | "orbstack" | "podman";
export interface SandboxConfig {
cpuLimit: number;
memoryLimit: number;
timeout: number;
networkEnabled: boolean;
readOnly: boolean;
}
export interface ResolvedContainerCommand {
/** Absolute command to spawn (e.g. `"docker"`, `"container"`, `"wslc"`). */
command: string;
/** Arguments for the command. */
args: string[];
/** Arguments appended for the `kill` cleanup path. */
killArgs: (containerName: string) => string[];
}
export interface ContainerProvider {
readonly id: SandboxRuntimeId;
readonly displayName: string;
/** Returns true when this runtime is installed and usable on the host. */
detect(): boolean;
/** Build a run command for the given image, command, and config. */
buildRun(
image: string,
command: string[],
sandboxId: string,
config: SandboxConfig,
): ResolvedContainerCommand;
/** Build a kill/stop command for a running container. */
killCommand: string;
buildKillArgs(name: string): string[];
}
// ----------------------------------------------------------------
// Helpers
// ----------------------------------------------------------------
const SANDBOX_NAME = (sandboxId: string) => `omniroute-${sandboxId}`;
/**
* Probe whether a CLI binary exists on PATH.
* Uses `where` on Windows, `which` on *nix — both via spawnSync so existing
* test mocks on `spawn` (but not `spawnSync`) are not disturbed.
*/
function probeCommand(binary: string): boolean {
const args =
process.platform === "win32" ? ["where", binary] : ["which", binary];
const r = childProcess.spawnSync(args[0], args.slice(1), {
encoding: "utf8",
stdio: "ignore",
});
return r.status === 0;
}
/**
* Probe whether a binary responds to `--version` with exit 0 and
* a plausible version string.
*/
function probeVersion(binary: string, expects = "v"): boolean {
const r = childProcess.spawnSync(binary, ["--version"], {
encoding: "utf8",
stdio: "pipe",
});
return r.status === 0 && !!r.stdout?.trim()?.includes(expects);
}
// ----------------------------------------------------------------
// DockerProvider
// ----------------------------------------------------------------
class DockerProvider implements ContainerProvider {
readonly id: SandboxRuntimeId = "docker";
readonly displayName = "Docker";
readonly killCommand = "docker";
detect(): boolean {
return probeCommand("docker") && probeVersion("docker");
}
buildRun(
image: string,
command: string[],
sandboxId: string,
config: SandboxConfig,
): ResolvedContainerCommand {
const args = [
"run",
"--rm",
"--name",
SANDBOX_NAME(sandboxId),
"--cpus",
`${config.cpuLimit / 100}`,
"--memory",
`${config.memoryLimit}m`,
"--network",
config.networkEnabled ? "bridge" : "none",
"--cap-drop",
"ALL",
"--security-opt",
"no-new-privileges",
"--pids-limit",
"100",
"--tmpfs",
"/tmp:rw,noexec,nosuid,size=64m",
"--tmpfs",
"/workspace:rw,noexec,nosuid,size=64m",
"--workdir",
"/workspace",
];
if (config.readOnly) args.push("--read-only");
args.push(image, ...command);
return {
command: "docker",
args,
killArgs: (name) => ["kill", name],
};
}
buildKillArgs(name: string): string[] {
return ["kill", name];
}
}
// ----------------------------------------------------------------
// AppleContainerProvider (native Apple Container on macOS)
// ----------------------------------------------------------------
class AppleContainerProvider implements ContainerProvider {
readonly id: SandboxRuntimeId = "apple";
readonly displayName = "Apple Container";
readonly killCommand = "container";
detect(): boolean {
return probeCommand("container") && probeVersion("container", "c");
}
buildRun(
image: string,
command: string[],
sandboxId: string,
config: SandboxConfig,
): ResolvedContainerCommand {
const args = [
"run",
"--rm",
"--name",
SANDBOX_NAME(sandboxId),
"--cpus",
`${config.cpuLimit}`,
"--memory",
`${config.memoryLimit}m`,
"--network",
config.networkEnabled ? "bridge" : "none",
"--cap-drop",
"ALL",
"--security-opt",
"no-new-privileges",
"--tmpfs",
"/tmp:rw,noexec,nosuid,size=64m",
"--tmpfs",
"/workspace:rw,noexec,nosuid,size=64m",
"--workdir",
"/workspace",
];
if (config.readOnly) args.push("--read-only");
args.push(image, ...command);
return {
command: "container",
args,
killArgs: (name) => ["kill", name],
};
}
buildKillArgs(name: string): string[] {
return ["kill", name];
}
}
// ----------------------------------------------------------------
// WslContainerProvider (WSL 2 container CLI on Windows)
// ----------------------------------------------------------------
class WslContainerProvider implements ContainerProvider {
readonly id: SandboxRuntimeId = "wsl";
readonly displayName = "WSL Container";
readonly killCommand = "wslc";
detect(): boolean {
return probeCommand("wslc") && probeVersion("wslc");
}
buildRun(
image: string,
command: string[],
sandboxId: string,
config: SandboxConfig,
): ResolvedContainerCommand {
const args = [
"run",
"--rm",
"--name",
SANDBOX_NAME(sandboxId),
"--cpus",
`${config.cpuLimit}`,
"--memory",
`${config.memoryLimit}m`,
"--network",
config.networkEnabled ? "bridge" : "none",
"--tmpfs",
"/tmp:rw,noexec,nosuid,size=64m",
"--tmpfs",
"/workspace:rw,noexec,nosuid,size=64m",
"--workdir",
"/workspace",
];
if (config.readOnly) args.push("--read-only");
args.push(image, ...command);
return {
command: "wslc",
args,
killArgs: (name) => ["kill", name],
};
}
buildKillArgs(name: string): string[] {
return ["kill", name];
}
}
// ----------------------------------------------------------------
// OrbStackProvider (high-perf Linux VM on macOS)
// ----------------------------------------------------------------
class OrbStackProvider implements ContainerProvider {
readonly id: SandboxRuntimeId = "orbstack";
readonly displayName = "OrbStack";
readonly killCommand = "orbstack";
detect(): boolean {
return probeCommand("orbstack") && probeVersion("orbstack");
}
buildRun(
image: string,
command: string[],
sandboxId: string,
config: SandboxConfig,
): ResolvedContainerCommand {
// OrbStack wraps Docker inside a Linux VM. We invoke the `orbstack`
// binary which shims `docker` transparently.
const args = [
"run",
"--rm",
"--name",
SANDBOX_NAME(sandboxId),
"--cpus",
`${config.cpuLimit}`,
"--memory",
`${config.memoryLimit}m`,
"--network",
config.networkEnabled ? "bridge" : "none",
"--tmpfs",
"/tmp:rw,noexec,nosuid,size=64m",
"--tmpfs",
"/workspace:rw,noexec,nosuid,size=64m",
"--workdir",
"/workspace",
];
if (config.readOnly) args.push("--read-only");
args.push(image, ...command);
return {
command: "orbstack",
args,
killArgs: (name) => ["kill", name],
};
}
buildKillArgs(name: string): string[] {
return ["kill", name];
}
}
// ----------------------------------------------------------------
// PodmanProvider (rootless Linux alternative)
// ----------------------------------------------------------------
class PodmanProvider implements ContainerProvider {
readonly id: SandboxRuntimeId = "podman";
readonly displayName = "Podman";
readonly killCommand = "podman";
detect(): boolean {
return probeCommand("podman") && probeVersion("podman");
}
buildRun(
image: string,
command: string[],
sandboxId: string,
config: SandboxConfig,
): ResolvedContainerCommand {
const args = [
"run",
"--rm",
"--name",
SANDBOX_NAME(sandboxId),
"--cpus",
`${config.cpuLimit / 100}`,
"--memory",
`${config.memoryLimit}m`,
"--network",
config.networkEnabled ? "bridge" : "none",
"--cap-drop",
"ALL",
"--security-opt",
"no-new-privileges",
"--tmpfs",
"/tmp:rw,noexec,nosuid,size=64m",
"--tmpfs",
"/workspace:rw,noexec,nosuid,size=64m",
"--workdir",
"/workspace",
];
if (config.readOnly) args.push("--read-only");
args.push(image, ...command);
return {
command: "podman",
args,
killArgs: (name) => ["kill", name],
};
}
buildKillArgs(name: string): string[] {
return ["kill", name];
}
}
// ----------------------------------------------------------------
// Registry & auto-detection
// ----------------------------------------------------------------
export const ALL_PROVIDERS: ContainerProvider[] = [
new DockerProvider(),
new AppleContainerProvider(),
new WslContainerProvider(),
new OrbStackProvider(),
new PodmanProvider(),
];
export const PROVIDER_BY_ID = new Map<SandboxRuntimeId, ContainerProvider>(
ALL_PROVIDERS.map((p) => [p.id, p]),
);
/** Priority order for auto-detection on each platform. */
export function platformPriority(): SandboxRuntimeId[] {
switch (os.platform()) {
case "darwin":
// Apple Container is the native micro-VM runtime on Apple Silicon —
// fastest startup, lowest overhead. OrbStack provides a Docker shim
// inside a tuned Linux VM; better than stock Docker Desktop.
return ["apple", "orbstack", "podman", "docker"];
case "win32":
// WSL Container CLI (wslc.exe) is Windows-native via WSL 2.
return ["wsl", "docker", "podman"];
default:
// Linux — podman is rootless + daemonless and therefore preferred.
return ["podman", "docker"];
}
}
// Detect-once memoization
let detectionInFlight: Promise<void> | null = null;
const detectionCache = new Map<SandboxRuntimeId, boolean>();
function clearDetectionCache(): void {
detectionInFlight = null;
detectionCache.clear();
}
async function runDetection(): Promise<void> {
// Run all probes in parallel for speed
await Promise.all(
ALL_PROVIDERS.map(async (provider) => {
const ok = await Promise.resolve(provider.detect());
detectionCache.set(provider.id, ok);
}),
);
}
function normaliseRuntimeOverride(
raw: string | undefined,
): SandboxRuntimeId | null {
if (!raw || raw === "auto") return null;
const lowered = raw.toLowerCase().trim();
if (PROVIDER_BY_ID.has(lowered as SandboxRuntimeId))
return lowered as SandboxRuntimeId;
return null;
}
/**
* Resolves which runtime the sandbox should use for the current host.
*
* Resolution rules (in order):
* 1. Explicit override via `SKILLS_SANDBOX_RUNTIME`.
* 2. Auto-detect: walk the platform priority list and pick the first
* runtime whose `detect()` succeeds.
* 3. Fall back to the Docker provider (the historical default) even if
* detection fails — the spawn will surface a clear "docker not
* found" error if Docker really is missing.
*/
export async function resolveProvider(): Promise<ContainerProvider> {
if (!detectionInFlight) {
detectionInFlight = runDetection();
}
await detectionInFlight;
const override = normaliseRuntimeOverride(
process.env.SKILLS_SANDBOX_RUNTIME,
);
if (override) {
const provider = PROVIDER_BY_ID.get(override)!;
if (detectionCache.get(provider.id)) return provider;
// Honour the explicit override even if detection failed — the user may
// be running inside an environment where the runtime is reachable but
// our probe failed (e.g. very locked-down CI).
return provider;
}
for (const id of platformPriority()) {
if (detectionCache.get(id)) return PROVIDER_BY_ID.get(id)!;
}
return PROVIDER_BY_ID.get("docker")!;
}
/** Exposed for tests — forces a fresh detection pass. */
export function _resetProviderCacheForTests(): void {
clearDetectionCache();
}
/**
* Returns the kill command for the given provider, parameterised with the
* sandbox's container name. Used by SandboxRunner.kill/killAll.
*/
export function buildKillCommand(
provider: ContainerProvider,
sandboxId: string,
): { command: string; args: string[] } {
const name = SANDBOX_NAME(sandboxId);
return {
command: provider.killCommand,
args: provider.buildKillArgs(name),
};
}

View File

@@ -1,20 +1,20 @@
import { createRequire } from "module";
import type { ChildProcess } from "child_process";
import { randomUUID } from "crypto";
import {
resolveProvider,
buildKillCommand,
type ContainerProvider,
type SandboxConfig,
type SandboxRuntimeId,
} from "./containerProvider.ts";
const require = createRequire(import.meta.url);
const childProcess = require("child_process") as typeof import("child_process");
interface SandboxConfig {
cpuLimit: number;
memoryLimit: number;
timeout: number;
networkEnabled: boolean;
readOnly: boolean;
}
interface SandboxResult {
id: string;
runtime: SandboxRuntimeId;
exitCode: number | null;
stdout: string;
stderr: string;
@@ -34,6 +34,7 @@ class SandboxRunner {
private static instance: SandboxRunner;
private runningContainers: Map<string, ChildProcess> = new Map();
private config: SandboxConfig;
private cachedProvider: ContainerProvider | null = null;
private constructor(config: Partial<SandboxConfig> = {}) {
this.config = { ...DEFAULT_CONFIG, ...config };
@@ -50,6 +51,19 @@ class SandboxRunner {
this.config = { ...this.config, ...config };
}
/**
* Returns the container provider that the next `run()` call will use.
* Resolution is async (it shells out to probe installed runtimes) so the
* caller must `await`. The result is cached on the runner for the
* remainder of the process so subsequent `run()` calls stay sync-friendly.
*/
async getProvider(): Promise<ContainerProvider> {
if (!this.cachedProvider) {
this.cachedProvider = await resolveProvider();
}
return this.cachedProvider;
}
async run(
image: string,
command: string[],
@@ -59,40 +73,11 @@ class SandboxRunner {
const sandboxId = randomUUID();
const startTime = Date.now();
const config = { ...this.config, ...configOverride };
const dockerArgs = [
"run",
"--rm",
"--name",
`omniroute-sandbox-${sandboxId}`,
"--cpus",
`${config.cpuLimit / 1000}`,
"--memory",
`${config.memoryLimit}m`,
"--network",
config.networkEnabled ? "bridge" : "none",
"--cap-drop",
"ALL",
"--security-opt",
"no-new-privileges",
"--pids-limit",
"100",
"--tmpfs",
"/tmp:rw,noexec,nosuid,size=64m",
"--tmpfs",
"/workspace:rw,noexec,nosuid,size=64m",
"--workdir",
"/workspace",
];
if (config.readOnly) {
dockerArgs.push("--read-only");
}
dockerArgs.push(image, ...command);
const provider = await this.getProvider();
const resolved = provider.buildRun(image, command, sandboxId, config);
return new Promise((resolve) => {
const proc = childProcess.spawn("docker", dockerArgs, {
const proc = childProcess.spawn(resolved.command, resolved.args, {
env: { ...process.env, ...env },
stdio: ["ignore", "pipe", "pipe"],
});
@@ -120,6 +105,7 @@ class SandboxRunner {
resolve({
id: sandboxId,
runtime: provider.id,
exitCode: code,
stdout,
stderr,
@@ -134,6 +120,7 @@ class SandboxRunner {
resolve({
id: sandboxId,
runtime: provider.id,
exitCode: -1,
stdout,
stderr: err.message,
@@ -149,18 +136,32 @@ class SandboxRunner {
if (proc) {
proc.kill("SIGTERM");
this.runningContainers.delete(sandboxId);
childProcess.spawn("docker", ["kill", `omniroute-sandbox-${sandboxId}`], {
stdio: "ignore",
});
const provider = this.cachedProvider;
if (provider) {
const kill = buildKillCommand(provider, sandboxId);
childProcess.spawn(kill.command, kill.args, { stdio: "ignore" });
} else {
childProcess.spawn("docker", ["kill", `omniroute-${sandboxId}`], {
stdio: "ignore",
});
}
return true;
}
return false;
}
killAll(): void {
const provider = this.cachedProvider;
for (const [id, proc] of this.runningContainers) {
proc.kill("SIGTERM");
childProcess.spawn("docker", ["kill", `omniroute-sandbox-${id}`], { stdio: "ignore" });
if (provider) {
const kill = buildKillCommand(provider, id);
childProcess.spawn(kill.command, kill.args, { stdio: "ignore" });
} else {
childProcess.spawn("docker", ["kill", `omniroute-${id}`], {
stdio: "ignore",
});
}
}
this.runningContainers.clear();
}
@@ -175,4 +176,4 @@ class SandboxRunner {
}
export const sandboxRunner = SandboxRunner.getInstance();
export type { SandboxConfig, SandboxResult };
export type { SandboxConfig, SandboxResult };

View File

@@ -8,6 +8,7 @@ import {
quotePowerShell,
runElevatedPowerShell,
} from "../systemCommands.ts";
import { ALL_TARGETS } from "../targets/index.ts";
// Legacy Antigravity defaults preserved for backward compat.
const ANTIGRAVITY_HOSTS = [
@@ -17,6 +18,12 @@ const ANTIGRAVITY_HOSTS = [
"autopush-cloudcode-pa.sandbox.googleapis.com",
];
function resolveHostsForAgent(agentId?: string): string[] {
if (!agentId) return ANTIGRAVITY_HOSTS;
const target = ALL_TARGETS.find((t) => t.id === agentId);
return target?.hosts ?? ANTIGRAVITY_HOSTS;
}
const IS_WIN = process.platform === "win32";
const HOSTS_FILE = IS_WIN
? path.join(process.env.SystemRoot || "C:\\Windows", "System32", "drivers", "etc", "hosts")
@@ -108,9 +115,13 @@ function hasHostEntry(hostsContent: string, hostname: string): boolean {
* Add /etc/hosts entries for every hostname in `hosts`.
* Idempotent — existing entries are not duplicated.
* Complies with Hard Rule #13: no string interpolation in shell commands.
*
* On Windows, all missing entries are batched into a single elevated PowerShell
* invocation so the user gets one UAC prompt instead of one per line.
*/
export async function addDNSEntries(hosts: string[], sudoPassword: string): Promise<void> {
const hostsContent = readHostsFile();
const missingEntries: string[] = [];
for (const hostname of hosts) {
const lines = dnsLines(hostname);
@@ -122,29 +133,23 @@ export async function addDNSEntries(hosts: string[], sudoPassword: string): Prom
return parts.length >= 2 && parts[0] === ip && parts.includes(host);
});
});
missingEntries.push(...missing);
}
for (const entry of missing) {
if (IS_WIN) {
// HR#13: build PowerShell command via concat (not template literal) so grep
// for `\${` inside script bodies returns zero hits. Values pass through
// `quotePowerShell()` for single-quote escaping — safe against injection
// since both HOSTS_FILE (OS const) and entry (internal `IP host` string)
// are non-user-supplied.
const cmd =
"Add-Content -LiteralPath " +
quotePowerShell(HOSTS_FILE) +
" -Value " +
quotePowerShell(entry);
await runElevatedPowerShell(cmd);
} else {
// Hard Rule #13: entry is passed as stdin data, not interpolated into the command.
await execFileWithPassword(
"sudo",
["-S", "tee", "-a", HOSTS_FILE],
sudoPassword,
`${entry}\n`
);
}
if (missingEntries.length === 0) return;
if (IS_WIN) {
const psHostsFile = quotePowerShell(HOSTS_FILE);
const psEntries = missingEntries.map((e) => quotePowerShell(e)).join(", ");
const script = "Add-Content -LiteralPath " + psHostsFile + " -Value " + psEntries;
await runElevatedPowerShell(script);
for (const entry of missingEntries) {
console.log(`[DNS] Added entry: ${entry}`);
}
} else {
const data = missingEntries.map((e) => `${e}\n`).join("");
await execFileWithPassword("sudo", ["-S", "tee", "-a", HOSTS_FILE], sudoPassword, data);
for (const entry of missingEntries) {
console.log(`[DNS] Added entry: ${entry}`);
}
}
@@ -168,46 +173,43 @@ fs.writeFileSync(filePath, filtered.join("\\n").replace(/\\n*$/, "\\n"));
* Remove /etc/hosts entries for every hostname in `hosts`.
* Idempotent — silently skips hosts that are not present.
* Complies with Hard Rule #13: HOSTS_FILE and hostname are passed as argv, not interpolated.
*
* On Windows, all hostnames are filtered in a single elevated PowerShell
* invocation so the user gets one UAC prompt instead of one per host.
*/
export async function removeDNSEntries(hosts: string[], sudoPassword: string): Promise<void> {
const hostsContent = readHostsFile();
const presentHosts = hosts.filter((h) => hasHostEntry(hostsContent, h));
for (const hostname of hosts) {
if (!hasHostEntry(hostsContent, hostname)) {
console.log(`[DNS] Entry for ${hostname} not present — skipping`);
continue;
}
if (presentHosts.length === 0) return;
try {
if (IS_WIN) {
// HR#13: build PowerShell script via concat (not template literal) so grep
// for `\${` inside script bodies returns zero hits. `psHostsFile` and
// `psTargetHost` are quotePowerShell-escaped values (single-quote escape).
const psHostsFile = quotePowerShell(HOSTS_FILE);
const psTargetHost = quotePowerShell(hostname);
const script =
"\n $hostsFile = " +
psHostsFile +
";\n $targetHost = " +
psTargetHost +
";\n $lines = Get-Content -LiteralPath $hostsFile;\n" +
" $filtered = $lines | Where-Object {\n" +
" $parts = ($_ -split '\\s+') | Where-Object { $_ };\n" +
" -not (($parts.Length -ge 2) -and ($parts -contains $targetHost))\n" +
" };\n" +
" Set-Content -LiteralPath $hostsFile -Value $filtered;\n ";
await runElevatedPowerShell(script);
} else {
// Hard Rule #13: HOSTS_FILE and hostname are argv arguments, not interpolated.
await execFileWithPassword(
"sudo",
["-S", process.execPath, "-e", REMOVE_HOSTS_ENTRY_SCRIPT, HOSTS_FILE, hostname],
sudoPassword
);
}
if (IS_WIN) {
const psHostsFile = quotePowerShell(HOSTS_FILE);
const psTargets = presentHosts.map((h) => quotePowerShell(h)).join(", ");
const script =
"$hostsFile = " +
psHostsFile +
";\n $targetHosts = @(" +
psTargets +
");\n" +
" $lines = Get-Content -LiteralPath $hostsFile;\n" +
" $filtered = $lines | Where-Object {\n" +
" $part = ($_ -split '\\s+') | Where-Object { $_ };\n" +
" -not ($part.Length -ge 2 -and ($targetHosts -contains $part[1]))\n" +
" };\n" +
" Set-Content -LiteralPath $hostsFile -Value $filtered;\n ";
await runElevatedPowerShell(script);
for (const hostname of presentHosts) {
console.log(`[DNS] Removed entries for ${hostname}`);
}
} else {
for (const hostname of presentHosts) {
await execFileWithPassword(
"sudo",
["-S", process.execPath, "-e", REMOVE_HOSTS_ENTRY_SCRIPT, HOSTS_FILE, hostname],
sudoPassword
);
console.log(`[DNS] Removed entries for ${hostname}`);
} catch (error) {
throw new Error(`Failed to remove DNS entry for ${hostname}: ${getErrorMessage(error)}`);
}
}
}
@@ -226,17 +228,19 @@ export function checkDNSEntry(): boolean {
}
/**
* Add DNS entries for the Antigravity default hosts.
* Add DNS entries for the Antigravity default hosts, or for a specific agent
* when `agentId` is provided.
* Delegates to `addDNSEntries` — backward compat wrapper.
*/
export async function addDNSEntry(sudoPassword: string): Promise<void> {
await addDNSEntries(ANTIGRAVITY_HOSTS, sudoPassword);
export async function addDNSEntry(sudoPassword: string, agentId?: string): Promise<void> {
await addDNSEntries(resolveHostsForAgent(agentId), sudoPassword);
}
/**
* Remove DNS entries for the Antigravity default hosts.
* Remove DNS entries for the Antigravity default hosts, or for a specific agent
* when `agentId` is provided.
* Delegates to `removeDNSEntries` — backward compat wrapper.
*/
export async function removeDNSEntry(sudoPassword: string): Promise<void> {
await removeDNSEntries(ANTIGRAVITY_HOSTS, sudoPassword);
export async function removeDNSEntry(sudoPassword: string, agentId?: string): Promise<void> {
await removeDNSEntries(resolveHostsForAgent(agentId), sudoPassword);
}

View File

@@ -552,7 +552,12 @@ async function startMitmInternal(
const certPath = path.join(resolveMitmDataDir(), "mitm", "server.crt");
if (!fs.existsSync(certPath)) {
log.info("Generating SSL certificate...");
await generateCert();
try {
await generateCert();
} catch (err) {
log.error({ err }, "Failed to generate SSL certificate");
throw err;
}
}
// 2. Install certificate to system keychain. A failure here must NOT abort the
@@ -576,7 +581,11 @@ async function startMitmInternal(
// 3. Add DNS entries: Antigravity defaults + all agents with dns_enabled=true +
// all custom hosts with enabled=true. Best-effort — see provisionDnsEntries.
log.info("Adding DNS entries...");
await provisionDnsEntries(sudoPassword);
try {
await provisionDnsEntries(sudoPassword);
} catch (err) {
log.error({ err }, "DNS provisioning threw unexpectedly (continuing)");
}
// 4. Start MITM server
log.info("Starting MITM server...");
@@ -619,9 +628,13 @@ async function startMitmInternal(
const proc = serverProcess;
serverPid = proc.pid ?? null;
// Save PID to file
// Save PID to file — best-effort, must not orphan spawned child process
if (serverPid !== null) {
fs.writeFileSync(PID_FILE, String(serverPid));
try {
fs.writeFileSync(PID_FILE, String(serverPid));
} catch (err) {
log.error({ err, pid: serverPid }, "Failed to write MITM PID file (continuing)");
}
}
// Buffer recent stderr so a startup failure can be reported with its real

View File

@@ -98,7 +98,6 @@ const KNOWN_SVGS = new Set([
"iflytek",
"sparkdesk",
"arcee-ai",
"inclusionai",
"liquid",
"monsterapi",
"nomic",
@@ -161,7 +160,10 @@ const ProviderIcon = memo(function ProviderIcon({
// without requiring `images.remotePatterns` allow-listing for arbitrary domains.
if (trimmedSrc && !remoteSrcFailed) {
return (
<span className={className} style={{ display: "inline-flex", alignItems: "center", ...style }}>
<span
className={className}
style={{ display: "inline-flex", alignItems: "center", ...style }}
>
{/* eslint-disable-next-line @next/next/no-img-element -- operator-supplied remote URL, not a static/known asset */}
<img
src={trimmedSrc}

View File

@@ -15,6 +15,57 @@ export const DEFAULT_PRICING_INFERENCE = {
cache_creation: 2.0,
},
},
synthetic: {
"hf:openai/gpt-oss-120b": {
input: 0.1,
output: 0.1,
cached: 0.1,
reasoning: 0,
cache_creation: 0,
},
"hf:zai-org/GLM-5.2": {
input: 1.4,
output: 4.4,
cached: 1.4,
reasoning: 0,
cache_creation: 0,
},
"hf:moonshotai/Kimi-K2.7-Code": {
input: 0.95,
output: 4,
cached: 0.95,
reasoning: 0,
cache_creation: 0,
},
"hf:Qwen/Qwen3.6-27B": {
input: 0.45,
output: 3.6,
cached: 0.45,
reasoning: 0,
cache_creation: 0,
},
"hf:MiniMaxAI/MiniMax-M3": {
input: 0.6,
output: 1.2,
cached: 0.6,
reasoning: 0,
cache_creation: 0,
},
"hf:zai-org/GLM-4.7-Flash": {
input: 0.1,
output: 0.5,
cached: 0.1,
reasoning: 0,
cache_creation: 0,
},
"hf:nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-NVFP4": {
input: 0.3,
output: 1,
cached: 0.3,
reasoning: 0,
cache_creation: 0,
},
},
groq: {
"openai/gpt-oss-120b": { input: 0, output: 0, cached: 0, reasoning: 0, cache_creation: 0 },
"gpt-oss-120b": { input: 0, output: 0, cached: 0, reasoning: 0, cache_creation: 0 },

View File

@@ -71,8 +71,6 @@ export const AGGREGATOR_PROVIDER_IDS = new Set([
"laozhang",
"vercel-ai-gateway",
"agentrouter",
"glhf",
"cablyai",
"thebai",
"fenayai",
"empower",

View File

@@ -14,8 +14,7 @@ export const APIKEY_PROVIDERS_GATEWAYS = {
website: "https://hyper.charm.land",
hasFree: true,
freeNote: "100 free monthly Hypercredits on signup",
apiHint:
"Create an API key at https://hyper.charm.land, then paste it here as a Bearer token.",
apiHint: "Create an API key at https://hyper.charm.land, then paste it here as a Bearer token.",
},
agentrouter: {
id: "agentrouter",
@@ -275,23 +274,6 @@ export const APIKEY_PROVIDERS_GATEWAYS = {
apiHint:
"Works without API key (use 'unused' as key). Get free token at token.llm7.io for higher limits.",
},
kluster: {
id: "kluster",
alias: "kluster",
name: "Kluster AI",
icon: "hub",
color: "#8B5CF6",
textIcon: "KL",
website: "https://kluster.ai",
hasFree: false,
freeNote: "Discontinued 2026 — kluster.ai sunset (2026-06-09); no free tier.",
apiHint: "Get API key at https://kluster.ai/dashboard/api-keys",
subscriptionRisk: true,
riskNoticeVariant: "deprecated",
deprecated: true,
deprecationReason:
"kluster.ai shut down (2026-06-09); api.kluster.ai no longer resolves (sweep 2026-06-19). Use another OpenAI-compatible provider.",
},
llamagate: {
id: "llamagate",
alias: "llamagate",
@@ -391,40 +373,6 @@ export const APIKEY_PROVIDERS_GATEWAYS = {
website: "https://api.laozhang.ai",
passthroughModels: true,
},
glhf: {
id: "glhf",
alias: "glhf",
name: "GLHF Chat",
icon: "hub",
color: "#10B981",
textIcon: "GH",
website: "https://glhf.chat",
authHint: "Bearer API key for the GLHF OpenAI-compatible gateway.",
hasFree: false,
freeNote: "Discontinued 2026 — glhf.chat free beta ended; no free tier.",
passthroughModels: true,
subscriptionRisk: true,
riskNoticeVariant: "deprecated",
deprecated: true,
deprecationReason:
"glhf.chat shut down (2026); its api.laf.run gateway no longer serves the catalog (sweep 2026-06-19).",
},
cablyai: {
id: "cablyai",
alias: "cablyai",
name: "CablyAI",
icon: "hub",
color: "#FF4081",
textIcon: "CA",
website: "https://cablyai.com",
authHint: "Bearer API key for the CablyAI OpenAI-compatible gateway.",
passthroughModels: true,
subscriptionRisk: true,
riskNoticeVariant: "deprecated",
deprecated: true,
deprecationReason:
"cablyai.com no longer resolves (DNS NXDOMAIN, verified 2026-06-30) — the domain is gone and every request fails with a DNS error (#5568).",
},
thebai: {
id: "thebai",
alias: "thebai",

View File

@@ -337,24 +337,6 @@ export const APIKEY_PROVIDERS_REGIONAL = {
passthroughModels: true,
authHint: "Get API key at console.xfyun.cn",
},
inclusionai: {
id: "inclusionai",
alias: "inclusion",
name: "InclusionAI",
icon: "psychology",
color: "#10B981",
textIcon: "IA",
website: "https://inclusionai.com",
hasFree: true,
freeNote: "Free Ling-2.6-flash model (1T-param MoE, 262K context). No credit card required.",
passthroughModels: true,
authHint: "Get API key at inclusionai.com",
subscriptionRisk: true,
riskNoticeVariant: "deprecated",
deprecated: true,
deprecationReason:
"api.inclusionai.tech no longer resolves (sweep 2026-06-19); the inference API appears discontinued.",
},
hcnsec: {
id: "hcnsec",
alias: "hcnsec",

View File

@@ -584,6 +584,16 @@ export const getKnownToolPaths = (toolId: string): string[] => {
if (localAppData) {
paths.push(path.join(localAppData, "Programs", "Claude", "claude.exe"));
paths.push(path.join(localAppData, "claude-code", "claude.exe"));
paths.push(
path.join(
localAppData,
"Microsoft",
"WinGet",
"Packages",
"Anthropic.ClaudeCode_Microsoft.Winget.Source_8wekyb3d8bbwe",
"claude.exe"
)
);
}
}

View File

@@ -889,27 +889,25 @@ export async function handleChat(
// Record telemetry
recordTelemetry(telemetry);
// Log combo failures that bypassed handleChatCore (e.g. all targets skipped by circuit breaker)
// Log combo failures that bypassed handleChatCore (e.g. all targets skipped by circuit breaker).
// Records BOTH a call_logs row (dashboard/logs) AND a usage_history row attributed to the api key
// (success:false) so gate/breaker-rejected traffic is counted per key — support-mesh 2026-07-08.
if (!response.ok) {
try {
const { saveCallLog } = await import("@/lib/usageDb");
saveCallLog({
id: undefined,
method: "POST",
path: clientRawRequest?.endpoint || "/v1/chat/completions",
const { recordRejectedRequestUsage } = await import("./rejectedRequestUsage");
await recordRejectedRequestUsage({
status: response.status,
model: body?.model || resolvedModelStr,
requestedModel: body?.model || resolvedModelStr,
provider: "-",
connectionId: undefined,
duration: Date.now() - (telemetry?.startTime || Date.now()),
tokens: {},
endpoint: clientRawRequest?.endpoint,
error: `[${response.status}] Combo "${combo.name}" failed — all targets exhausted`,
comboName: combo.name,
comboStepId: null,
comboExecutionKey: null,
apiKeyId: apiKeyInfo?.id ?? null,
apiKeyName: apiKeyInfo?.name ?? null,
correlationId: reqId,
}).catch(() => {});
startTime: telemetry?.startTime,
});
} catch {}
}
return withCorrelationId(withSessionHeader(response, sessionId), reqId);
@@ -1113,26 +1111,26 @@ async function handleSingleModelChat(
...(bypassReason ? { bypassReason } : {}),
});
if (gate) {
// Log the rejected request so it appears in /dashboard/logs
// Log the rejected request so it appears in /dashboard/logs AND is counted in the
// per-api-key usage analytics (usage_history, success:false) — otherwise a key whose
// traffic is entirely gate/breaker-rejected shows "zero requests" (support-mesh 2026-07-08).
try {
const { saveCallLog } = await import("@/lib/usageDb");
saveCallLog({
id: undefined,
method: "POST",
path: clientRawRequest?.endpoint || "/v1/chat/completions",
const { recordRejectedRequestUsage } = await import("./rejectedRequestUsage");
await recordRejectedRequestUsage({
status: gate.status,
model,
requestedModel: body?.model || modelStr,
provider,
connectionId: undefined,
duration: Date.now() - (telemetry?.startTime || Date.now()),
tokens: {},
endpoint: clientRawRequest?.endpoint,
error: `[${gate.status}] Pipeline gate rejected`,
comboName: isCombo ? comboName : null,
comboStepId: isCombo ? (runtimeOptions?.comboStepId ?? null) : null,
comboExecutionKey: isCombo ? (runtimeOptions?.comboExecutionKey ?? null) : null,
apiKeyId: apiKeyInfo?.id ?? null,
apiKeyName: apiKeyInfo?.name ?? null,
correlationId: runtimeOptions?.correlationId ?? null,
}).catch(() => {});
startTime: telemetry?.startTime,
});
} catch {}
return gate;
}

View File

@@ -0,0 +1,98 @@
/**
* Records a request that was rejected BEFORE reaching handleChatCore — i.e. a
* pipeline-gate rejection (provider circuit breaker OPEN / model cooldown) or a
* combo whose targets were all exhausted. These paths short-circuit in
* `chat.ts` and used to write only a `call_logs` row via `saveCallLog`, which
* kept them visible in /dashboard/logs but left them absent from `usage_history`
* — the table `getApiKeyUsageRows` reads. The effect was an API key whose
* traffic was entirely gate-rejected showing "zero requests" despite real
* usage (support-mesh escalation, 2026-07-08).
*
* This helper writes BOTH:
* 1. the `call_logs` row (unchanged dashboard/logs visibility), and
* 2. a `usage_history` row attributed to the api key with `success: false`,
* mirroring `persistFailureUsage` in the post-executor failure path,
* so rejected traffic is counted per key just like executor-level failures.
*
* Best-effort: both writes swallow their own errors — logging a rejection must
* never turn into a second failure on the response path.
*/
import { saveCallLog, saveRequestUsage } from "@/lib/usageDb";
export interface RejectedRequestUsageInput {
status: number;
model: string;
requestedModel?: string;
provider: string;
endpoint?: string | null;
error?: string | null;
comboName?: string | null;
comboStepId?: string | null;
comboExecutionKey?: string | null;
correlationId?: string | null;
apiKeyId?: string | null;
apiKeyName?: string | null;
connectionId?: string | null;
/** When the request started, for the duration/latency columns. */
startTime?: number;
}
export async function recordRejectedRequestUsage(input: RejectedRequestUsageInput): Promise<void> {
const {
status,
model,
requestedModel,
provider,
endpoint,
error,
comboName = null,
comboStepId = null,
comboExecutionKey = null,
correlationId = null,
apiKeyId = null,
apiKeyName = null,
connectionId = undefined,
startTime,
} = input;
const now = Date.now();
const duration = typeof startTime === "number" ? now - startTime : 0;
// 1. call_logs — preserves /dashboard/logs visibility (unchanged behavior).
saveCallLog({
id: undefined,
method: "POST",
path: endpoint || "/v1/chat/completions",
status,
model,
requestedModel: requestedModel || model,
provider,
connectionId,
duration,
tokens: {},
error: error || null,
comboName,
comboStepId,
comboExecutionKey,
apiKeyId,
apiKeyName,
correlationId,
}).catch(() => {});
// 2. usage_history — so the per-api-key usage counter reflects rejected
// traffic (success:false), matching persistFailureUsage semantics.
await saveRequestUsage({
provider,
model,
connectionId: connectionId ?? null,
apiKeyId,
apiKeyName,
tokens: {},
serviceTier: "standard",
status: String(status),
success: false,
latencyMs: duration,
comboStrategy: comboName || null,
endpoint: endpoint || "/v1/chat/completions",
}).catch(() => {});
}

View File

@@ -1834,29 +1834,6 @@
"stream": "https://opengateway.gitlawb.com/v1/gmi-cloud"
}
},
"glhf": {
"format": "openai",
"headers": {
"apiKey": {
"Accept": "text/event-stream",
"Authorization": "Bearer <TOK>",
"Content-Type": "application/json"
},
"nonStream": {
"Authorization": "Bearer <TOK>",
"Content-Type": "application/json"
},
"oauth": {
"Accept": "text/event-stream",
"Authorization": "Bearer <TOK>",
"Content-Type": "application/json"
}
},
"url": {
"nonStream": "https://api.laf.run/v1/chat/completions",
"stream": "https://api.laf.run/v1/chat/completions"
}
},
"glm": {
"format": "openai",
"headers": {
@@ -2202,29 +2179,6 @@
"stream": "https://spark-api.xf-yun.com/v1/chat/completions"
}
},
"inclusionai": {
"format": "openai",
"headers": {
"apiKey": {
"Accept": "text/event-stream",
"Authorization": "Bearer <TOK>",
"Content-Type": "application/json"
},
"nonStream": {
"Authorization": "Bearer <TOK>",
"Content-Type": "application/json"
},
"oauth": {
"Accept": "text/event-stream",
"Authorization": "Bearer <TOK>",
"Content-Type": "application/json"
}
},
"url": {
"nonStream": "https://api.inclusionai.tech/v1/chat/completions",
"stream": "https://api.inclusionai.tech/v1/chat/completions"
}
},
"inference-net": {
"format": "openai",
"headers": {
@@ -2497,29 +2451,6 @@
"stream": "https://codewhisperer.us-east-1.amazonaws.com/generateAssistantResponse"
}
},
"kluster": {
"format": "openai",
"headers": {
"apiKey": {
"Accept": "text/event-stream",
"Authorization": "Bearer <TOK>",
"Content-Type": "application/json"
},
"nonStream": {
"Authorization": "Bearer <TOK>",
"Content-Type": "application/json"
},
"oauth": {
"Accept": "text/event-stream",
"Authorization": "Bearer <TOK>",
"Content-Type": "application/json"
}
},
"url": {
"nonStream": "https://api.kluster.ai/v1/chat/completions",
"stream": "https://api.kluster.ai/v1/chat/completions"
}
},
"lambda-ai": {
"format": "openai",
"headers": {

View File

@@ -0,0 +1,60 @@
/**
* Unit tests: POST /api/tools/agent-bridge/agents/[id]/dns — unknown-agent 404 guard.
*
* Before this fix, an unknown `id` fell through straight into `addDNSEntry`/
* `removeDNSEntry`, which silently resolved the legacy Antigravity default hosts
* instead of rejecting the request. The route now validates `id` against
* `ALL_TARGETS` first and returns 404 for unknown agents — verified here without
* touching /etc/hosts (the 404 short-circuits before any DNS call).
*/
import test from "node:test";
import assert from "node:assert/strict";
const dnsRoute = await import(
"../../src/app/api/tools/agent-bridge/agents/[id]/dns/route.ts"
);
function makeRequest(body: unknown): Request {
return new Request("http://127.0.0.1/api/tools/agent-bridge/agents/x/dns", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
}
test("POST .../[id]/dns: unknown agent id returns 404 before any DNS call", async () => {
const res = await dnsRoute.POST(makeRequest({ enabled: true }), {
params: { id: "__nonexistent_agent__" },
});
assert.equal(res.status, 404);
const body = (await res.json()) as { error?: { message?: string } };
assert.ok(
JSON.stringify(body).includes("__nonexistent_agent__"),
"404 body should reference the unknown agent id"
);
});
test("POST .../[id]/dns: invalid body still returns 400 (schema validated first)", async () => {
const res = await dnsRoute.POST(makeRequest({ enabled: "not-a-boolean" }), {
params: { id: "__nonexistent_agent__" },
});
assert.equal(res.status, 400);
});
test("POST .../[id]/dns: malformed JSON body returns 400", async () => {
const req = new Request("http://127.0.0.1/api/tools/agent-bridge/agents/x/dns", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: "{not-json",
});
const res = await dnsRoute.POST(req, { params: { id: "cursor" } });
assert.equal(res.status, 400);
});
test("POST .../[id]/dns: error responses do not leak stack traces", async () => {
const res = await dnsRoute.POST(makeRequest({ enabled: true }), {
params: { id: "__nonexistent_agent__" },
});
const text = await res.text();
assert.ok(!text.includes("at /"), "stack trace leaked in dns route 404 response");
});

View File

@@ -0,0 +1,158 @@
/**
* Anthropic server (built-in) tools must keep their literal `name` in EVERY
* request section — tools[], message-history `tool_use` blocks, and
* `tool_choice` — not just in the tools array.
*
* Anthropic's server tools are identified by a versioned `type`
* (e.g. `web_search_20250305`) paired with a FIXED literal `name`
* (`web_search`, `bash`, …) that the API validates as a pair. The tools-array
* rewrite is already guarded by `isAnthropicServerToolType`, but the
* message-history and `tool_choice` rewrites were not. That asymmetry renames
* only the history/tool_choice reference (`web_search` → `WebSearch`) while
* tools[] keeps the literal `web_search`, so Anthropic rejects the request:
*
* [400] Tool 'WebSearch' not found in provided tools
*
* Same class for the fixed Claude Code rename map: `bash_20250124` carries the
* literal name `bash`, which `remapToolNamesInRequest` would rewrite to `Bash`
* (→ `tools.0.bash_20250124.name: Input should be 'bash'`).
*
* Regression surfaced on Claude Code 2.1.x native web-search calls; same class
* as CLIProxyAPI #1094/#1179.
*/
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import {
cloakThirdPartyToolNames,
remapToolNamesInRequest,
} from "../../open-sse/services/claudeCodeToolRemapper.ts";
type AnyRecord = Record<string, unknown>;
describe("cloakThirdPartyToolNames — server-tool names in message history", () => {
it("keeps a history tool_use reference to a declared web_search server tool", () => {
const body: AnyRecord = {
tools: [{ type: "web_search_20250305", name: "web_search", max_uses: 8 }],
messages: [
{
role: "assistant",
content: [{ type: "tool_use", id: "toolu_1", name: "web_search", input: { query: "x" } }],
},
],
};
cloakThirdPartyToolNames(body);
assert.equal((body.tools as AnyRecord[])[0].name, "web_search");
const block = ((body.messages as AnyRecord[])[0].content as AnyRecord[])[0];
assert.equal(block.name, "web_search");
});
it("keeps a tool_choice reference to a declared web_search server tool", () => {
const body: AnyRecord = {
tools: [{ type: "web_search_20250305", name: "web_search" }],
tool_choice: { type: "tool", name: "web_search" },
};
cloakThirdPartyToolNames(body);
assert.equal((body.tool_choice as AnyRecord).name, "web_search");
});
it("still cloaks a third-party history tool_use next to a server tool", () => {
const body: AnyRecord = {
tools: [{ type: "web_search_20250305", name: "web_search" }, { name: "mixture_of_agents" }],
messages: [
{
role: "assistant",
content: [
{ type: "tool_use", id: "toolu_1", name: "web_search", input: {} },
{ type: "tool_use", id: "toolu_2", name: "mixture_of_agents", input: {} },
],
},
],
};
cloakThirdPartyToolNames(body);
const blocks = (body.messages as AnyRecord[])[0].content as AnyRecord[];
assert.equal(blocks[0].name, "web_search");
assert.equal(blocks[1].name, "MixtureOfAgents");
assert.deepEqual(
(body.tools as AnyRecord[]).map((t) => t.name),
["web_search", "MixtureOfAgents"]
);
});
it("still cloaks a snake_case history name when no server tool declares it", () => {
const body: AnyRecord = {
tools: [{ name: "web_search", input_schema: { type: "object" } }],
messages: [
{
role: "assistant",
content: [{ type: "tool_use", id: "toolu_1", name: "web_search", input: {} }],
},
],
};
cloakThirdPartyToolNames(body);
// Plain custom tool named web_search (no server type) remains cloakable —
// symmetrically in tools[] and history.
assert.equal((body.tools as AnyRecord[])[0].name, "WebSearch");
const block = ((body.messages as AnyRecord[])[0].content as AnyRecord[])[0];
assert.equal(block.name, "WebSearch");
});
});
describe("remapToolNamesInRequest — Anthropic server tools", () => {
it("does not rename a bash server tool to Bash in tools[]", () => {
const body: AnyRecord = {
tools: [{ type: "bash_20250124", name: "bash" }],
};
remapToolNamesInRequest(body);
assert.equal((body.tools as AnyRecord[])[0].name, "bash");
assert.equal((body._toolNameMap as Map<string, string> | undefined)?.size ?? 0, 0);
});
it("does not rename history/tool_choice references to a declared bash server tool", () => {
const body: AnyRecord = {
tools: [{ type: "bash_20250124", name: "bash" }],
messages: [
{
role: "assistant",
content: [{ type: "tool_use", id: "toolu_1", name: "bash", input: { command: "ls" } }],
},
],
tool_choice: { type: "tool", name: "bash" },
};
remapToolNamesInRequest(body);
const block = ((body.messages as AnyRecord[])[0].content as AnyRecord[])[0];
assert.equal(block.name, "bash");
assert.equal((body.tool_choice as AnyRecord).name, "bash");
});
it("tolerates null entries in tools[] without throwing", () => {
const body: AnyRecord = {
tools: [null, { type: "bash_20250124", name: "bash" }],
messages: [
{
role: "assistant",
content: [{ type: "tool_use", id: "toolu_1", name: "bash", input: {} }],
},
],
};
remapToolNamesInRequest(body);
assert.equal((body.tools as AnyRecord[])[1].name, "bash");
const block = ((body.messages as AnyRecord[])[0].content as AnyRecord[])[0];
assert.equal(block.name, "bash");
});
it("still renames a plain lowercase custom bash tool to Bash", () => {
const body: AnyRecord = {
tools: [{ name: "bash", input_schema: { type: "object" } }],
messages: [
{
role: "assistant",
content: [{ type: "tool_use", id: "toolu_1", name: "bash", input: {} }],
},
],
};
remapToolNamesInRequest(body);
assert.equal((body.tools as AnyRecord[])[0].name, "Bash");
const block = ((body.messages as AnyRecord[])[0].content as AnyRecord[])[0];
assert.equal(block.name, "Bash");
});
});

View File

@@ -9,7 +9,7 @@ import fs from "node:fs";
import os from "node:os";
import path from "node:path";
const { getCliRuntimeStatus, CLI_TOOL_IDS } =
const { getCliRuntimeStatus, getKnownToolPaths, CLI_TOOL_IDS } =
await import("../../src/shared/services/cliRuntime.ts");
// ─── Helpers ──────────────────────────────────────────────────
@@ -22,6 +22,29 @@ function createTempDir() {
return fs.mkdtempSync(path.join(testRoot, "cli-test-"));
}
describe("Claude Code Windows known paths", () => {
it("should include the WinGet Anthropic.ClaudeCode install path", () => {
const localAppData = process.env.LOCALAPPDATA;
const expected = localAppData
? path.join(
localAppData,
"Microsoft",
"WinGet",
"Packages",
"Anthropic.ClaudeCode_Microsoft.Winget.Source_8wekyb3d8bbwe",
"claude.exe"
)
: null;
if (process.platform !== "win32" || !expected) return;
assert.ok(
getKnownToolPaths("claude").includes(expected),
"Claude Code installed by WinGet should be discoverable without CLI_CLAUDE_BIN"
);
});
});
function createFile(dir, name, content) {
const filePath = path.join(dir, name);
fs.writeFileSync(filePath, content);

View File

@@ -13,31 +13,46 @@ describe("2026 discontinued free tiers — providers.ts hasFree reconciliation",
// they are KEPT with hasFree:false. phind is NOT here: the whole phind.com service
// shut down 2026-01-16, so it was removed entirely (registry/executor/catalogs),
// matching the dead-service-removal precedent (#5246 Gemini CLI).
for (const id of ["chutes", "kluster", "glhf", "gitlawb", "gitlawb-gmi", "aimlapi", "yi"]) {
for (const id of ["chutes", "gitlawb", "gitlawb-gmi", "aimlapi", "yi"]) {
const p = (APIKEY_PROVIDERS as Record<string, { hasFree?: boolean }>)[id];
assert.ok(p, `${id} should still exist in APIKEY_PROVIDERS (provider not removed, only its free flag)`);
assert.strictEqual(p.hasFree, false, `${id} should have hasFree:false (discontinued in 2026)`);
assert.ok(
p,
`${id} should still exist in APIKEY_PROVIDERS (provider not removed, only its free flag)`
);
assert.strictEqual(
p.hasFree,
false,
`${id} should have hasFree:false (discontinued in 2026)`
);
}
});
it("phind is fully removed (service shut down 2026-01) from both catalogs", async () => {
const { APIKEY_PROVIDERS, WEB_COOKIE_PROVIDERS } = await import(
"../../src/shared/constants/providers.ts"
);
const { APIKEY_PROVIDERS, WEB_COOKIE_PROVIDERS } =
await import("../../src/shared/constants/providers.ts");
assert.ok(!("phind" in APIKEY_PROVIDERS), "phind must not be in APIKEY_PROVIDERS");
assert.ok(!("phind" in WEB_COOKIE_PROVIDERS), "phind must not be in WEB_COOKIE_PROVIDERS");
});
it("intentionally-kept providers still advertise free (genuinely free / ToS-flagged, not flipped)", async () => {
const { NOAUTH_PROVIDERS, APIKEY_PROVIDERS } = await import("../../src/shared/constants/providers.ts");
const { NOAUTH_PROVIDERS, APIKEY_PROVIDERS } =
await import("../../src/shared/constants/providers.ts");
// theoldllm is a keyless, no-signup web chat (genuinely free, just no catalogable API tier) — kept.
// iflytek/sparkdesk stay hasFree:true but carry a ToS-caution freeNote (Spark Lite is free, the ToS
// restricts proxy/relay use). gitlawb/gitlawb-gmi/aimlapi/yi were re-verified dead 2026-06-18 and are
// asserted false above — keeping them out of this list guards against a silent re-flip-to-true.
const noauth = NOAUTH_PROVIDERS as Record<string, { hasFree?: boolean }>;
const apikey = APIKEY_PROVIDERS as Record<string, { hasFree?: boolean; freeNote?: string }>;
assert.strictEqual(noauth["theoldllm"]?.hasFree, true, "theoldllm intentionally kept hasFree:true");
assert.strictEqual(
noauth["theoldllm"]?.hasFree,
true,
"theoldllm intentionally kept hasFree:true"
);
assert.strictEqual(apikey["iflytek"]?.hasFree, true, "iflytek kept free with ToS-caution note");
assert.match(apikey["iflytek"]?.freeNote ?? "", /caution/i, "iflytek freeNote should carry a caution");
assert.match(
apikey["iflytek"]?.freeNote ?? "",
/caution/i,
"iflytek freeNote should carry a caution"
);
});
});

View File

@@ -69,6 +69,7 @@ const tmpHostsFile = path.join(tmpDir, "hosts");
// Import the module under test AFTER all setup.
const dnsModule = await import("../../src/mitm/dns/dnsConfig.ts");
const { addDNSEntries, removeDNSEntries, addDNSEntry, removeDNSEntry, checkDNSEntry } = dnsModule;
const { ALL_TARGETS } = await import("../../src/mitm/targets/index.ts");
// ---------------------------------------------------------------------------
// Tests
@@ -95,10 +96,60 @@ test("addDNSEntry (legacy) is a function that delegates for Antigravity hosts",
assert.equal(typeof addDNSEntry, "function");
});
test("addDNSEntry with agentId resolves agent-specific hosts from ALL_TARGETS", async () => {
// Cursor target has hosts: ["api2.cursor.sh"]
// Verify that calling addDNSEntry with agentId="cursor" passes the right hosts.
// We call with [] to skip actual exec — just verifying the function signature accepts agentId.
await assert.doesNotReject(
addDNSEntry("fake-sudo", "cursor"),
"addDNSEntry must accept optional agentId parameter"
);
});
test("addDNSEntry without agentId falls back to Antigravity hosts (backward compat)", async () => {
await assert.doesNotReject(
addDNSEntry("fake-sudo"),
"addDNSEntry without agentId must still work for backward compat"
);
});
test("addDNSEntry with unknown agentId falls back to Antigravity hosts", async () => {
await assert.doesNotReject(
addDNSEntry("fake-sudo", "__nonexistent_agent__"),
"addDNSEntry with unknown agentId must fall back to Antigravity hosts"
);
});
test("removeDNSEntry (legacy) is a function that delegates for Antigravity hosts", () => {
assert.equal(typeof removeDNSEntry, "function");
});
test("removeDNSEntry with agentId resolves agent-specific hosts from ALL_TARGETS", async () => {
await assert.doesNotReject(
removeDNSEntry("fake-sudo", "copilot"),
"removeDNSEntry must accept optional agentId parameter"
);
});
test("resolveHostsForAgent returns Antigravity hosts when agentId is undefined", () => {
// Verify ALL_TARGETS exists and cursor target has expected hosts
const cursorTarget = ALL_TARGETS.find((t) => t.id === "cursor");
assert.ok(cursorTarget, "cursor target must exist in ALL_TARGETS");
assert.ok(
cursorTarget.hosts.includes("api2.cursor.sh"),
"cursor target must include api2.cursor.sh"
);
// Codex target
const codexTarget = ALL_TARGETS.find((t) => t.id === "codex");
assert.ok(codexTarget, "codex target must exist in ALL_TARGETS");
assert.ok(codexTarget.hosts.includes("chatgpt.com"), "codex target must include chatgpt.com");
});
test("addDNSEntries batches missing entries with no-op on empty list", async () => {
// Empty list must resolve immediately (no exec, no error)
await assert.doesNotReject(addDNSEntries([], "fake-sudo"));
});
test("addDNSEntries: skips hosts already in /etc/hosts (idempotency)", async () => {
// Read live /etc/hosts and pick the first entry that already exists.
// If localhost is in /etc/hosts we use it; otherwise skip this sub-assertion.
@@ -157,11 +208,17 @@ test("addDNSEntries: entry passed as stdin data, not shell-interpolated", () =>
const srcPath = new URL("../../src/mitm/dns/dnsConfig.ts", import.meta.url).pathname;
const src = fs.readFileSync(srcPath, "utf8");
// The stdin data `${entry}\n` is the body text sent to tee via pipe — not
// part of the command array. Verify the pattern appears in the source.
// The stdin `data` is built from the batched entries and sent to tee via pipe —
// not part of the command array. Verify the pattern appears in the source and
// that it's passed as the 4th positional arg to execFileWithPassword (stdin),
// not interpolated into the argv array.
assert.ok(
src.includes("`${entry}\\n`"),
"entry content must be passed as stdin to tee, not interpolated in args"
src.includes('missingEntries.map((e) => `${e}\\n`).join("")'),
"entry content must be built from missingEntries for stdin, not interpolated in args"
);
assert.ok(
src.includes('execFileWithPassword("sudo", ["-S", "tee", "-a", HOSTS_FILE], sudoPassword, data)'),
"entry data must be passed as stdin to tee, not interpolated in args"
);
});

View File

@@ -60,7 +60,8 @@ test("recurring-uncapped models are surfaced but NEVER summed into the steady he
const t = computeFreeModelTotals();
// every uncapped record must carry monthlyTokens 0 (un-quantifiable, not counted)
for (const m of FREE_MODEL_BUDGETS) {
if (m.freeType === "recurring-uncapped") assert.equal(m.monthlyTokens, 0, `${m.provider}/${m.modelId} uncapped but counted`);
if (m.freeType === "recurring-uncapped")
assert.equal(m.monthlyTokens, 0, `${m.provider}/${m.modelId} uncapped but counted`);
}
// uncappedProviders is the de-duped provider list and is non-empty (siliconflow, glm-cn, kilo…)
assert.ok(Array.isArray(t.uncappedProviders) && t.uncappedProviders.length >= 3);
@@ -81,7 +82,7 @@ test("deposit-unlock boost is reported separately, not folded into steady", () =
test("2026-06-17 refresh: discontinued providers dropped, new free providers added", () => {
const providers = new Set(FREE_MODEL_BUDGETS.map((m) => m.provider));
// dead in 2026 — must be gone from the budget catalog
for (const dead of ["chutes", "phind", "kluster", "glhf", "gitlawb", "aimlapi", "theoldllm"]) {
for (const dead of ["chutes", "phind", "kluster", "gitlawb", "aimlapi", "theoldllm"]) {
assert.ok(!providers.has(dead), `${dead} should be removed (discontinued)`);
}
// qwen-web is KEPT on purpose: only its OAuth API tier died — OmniRoute uses the cookie/web path.

View File

@@ -27,7 +27,7 @@ test("FREE_TIER_TOS marks proxy-prohibited providers as avoid", () => {
test("computeFreeTierTotals sums the documented budgets", () => {
const t = computeFreeTierTotals();
assert.equal(t.providerCount, 21);
assert.equal(t.providerCount, 20);
assert.ok(t.documentedMonthlyTokens >= 1_350_000_000);
assert.ok(t.documentedMonthlyTokens <= 1_450_000_000);
assert.equal(typeof t.headline, "string");
@@ -38,5 +38,5 @@ test("computeFreeTierTotals can exclude ToS-avoid providers", () => {
const all = computeFreeTierTotals();
const clean = computeFreeTierTotals({ excludeTosAvoid: true });
assert.equal(all.documentedMonthlyTokens - clean.documentedMonthlyTokens, 25_000);
assert.equal(clean.providerCount, 20);
assert.equal(clean.providerCount, 19);
});

View File

@@ -0,0 +1,17 @@
import test from "node:test";
import assert from "node:assert/strict";
import { FREE_MODEL_BUDGETS } from "../../open-sse/config/freeModelCatalog.ts";
import { FREE_TIER_BUDGETS } from "../../open-sse/config/freeTierCatalog.ts";
import { REGISTRY } from "../../open-sse/config/providerRegistry.ts";
import { APIKEY_PROVIDERS } from "../../src/shared/constants/providers/apikey/index.ts";
test("inclusionai is no longer registered as a first-party provider", () => {
assert.equal(Object.hasOwn(REGISTRY, "inclusionai"), false);
assert.equal(Object.hasOwn(APIKEY_PROVIDERS, "inclusionai"), false);
assert.equal(Object.hasOwn(FREE_TIER_BUDGETS, "inclusionai"), false);
assert.equal(
FREE_MODEL_BUDGETS.some((entry) => entry.provider === "inclusionai"),
false
);
});

View File

@@ -1,8 +1,8 @@
// Split-guard for the provider-models discovery route decomposition
// (refactor: extract 4 pure leaves — helpers / normalizers / providerModelsConfig /
// providerSets — out of src/app/api/providers/[id]/models/route.ts). The leaves are
// DB-free and state-free; this guard pins their public surface, the host wiring, and
// the #5570 cablyai→aimlapi swap so a future edit that silently breaks the split fails.
// DB-free and state-free; this guard pins their public surface and the host wiring
// so a future edit that silently breaks the split fails.
import test from "node:test";
import assert from "node:assert/strict";
@@ -120,9 +120,8 @@ test("providerModelsConfig.PROVIDER_MODELS_CONFIG keeps core provider entries",
assert.equal(PROVIDER_MODELS_CONFIG["qwen-web"].url, "https://chat.qwen.ai/api/v2/models");
});
test("providerModelsConfig aimlapi entry replaced the removed cablyai entry (#5570)", () => {
test("providerModelsConfig keeps the aimlapi live catalog entry", () => {
assert.equal(PROVIDER_MODELS_CONFIG.aimlapi.url, "https://api.aimlapi.com/models");
assert.equal(PROVIDER_MODELS_CONFIG.cablyai, undefined);
});
test("providerModelsConfig aimlapi.parseResponse keeps only chat-completion models when present", () => {

View File

@@ -120,23 +120,6 @@ test("provider models route falls back to the local AI/ML API catalog when the l
assert.ok(body.models.length > 0);
});
test("cablyai is flagged deprecated (domain NXDOMAIN) and no longer 500s on model import (#5568)", async () => {
const { APIKEY_PROVIDERS_GATEWAYS } =
await import("../../src/shared/constants/providers/apikey/gateways.ts");
const cablyai = (APIKEY_PROVIDERS_GATEWAYS as Record<string, any>).cablyai;
assert.equal(cablyai?.deprecated, true, "cablyai must be marked deprecated (domain is NXDOMAIN)");
assert.ok(
typeof cablyai?.deprecationReason === "string" && cablyai.deprecationReason.length > 0,
"cablyai must carry a deprecationReason"
);
// Removed from PROVIDER_MODELS_CONFIG → no live fetch to the dead domain → a
// controlled 400 instead of an unhandled 500 crash.
const connection = await seedConnection("cablyai", { apiKey: "dead-key" });
const response = await callRoute(connection.id);
assert.notEqual(response.status, 500, "cablyai must not 500-crash on a dead domain");
});
test("provider models route returns 404 for unknown connections", async () => {
const response = await callRoute("missing-connection");
@@ -735,7 +718,11 @@ test("provider models route returns the expanded local catalog for Kiro", async
assert.equal(body.provider, "kiro");
assert.equal(body.source, "local_catalog");
const kiroIds = new Set(body.models.map((model) => model.id)); // #6170: real upstream lineup
assert.ok(kiroIds.has("claude-sonnet-5") && kiroIds.has("claude-sonnet-4.5") && kiroIds.has("claude-haiku-4.5"));
assert.ok(
kiroIds.has("claude-sonnet-5") &&
kiroIds.has("claude-sonnet-4.5") &&
kiroIds.has("claude-haiku-4.5")
);
assert.equal(kiroIds.has("claude-opus-4.7") || kiroIds.has("claude-sonnet-4.6"), false); // fabricated ids removed
});

View File

@@ -1,8 +1,9 @@
// Characterization of the providers.ts catalog split (god-file decomposition): the host became a
// barrel that re-exports 10 data catalogs now living under constants/providers/*, and APIKEY is
// merged from 6 semantic family files (apikey/<family>.ts). Locks: the public surface (every catalog
// + helpers still exported), the spread-merge integrity (171 APIKEY entries, no loss/dup), and that
// + helpers still exported), the spread-merge integrity (167 APIKEY entries, no loss/dup), and that
// load-time Zod validation still runs. Pure-data move → behavior must be identical.
// Count was 171 before obsolete provider removals (PR #6675: glhf/kluster/cablyai/inclusionai etc.).
import { test } from "node:test";
import assert from "node:assert/strict";
@@ -31,12 +32,12 @@ test("barrel still exports every catalog + key helpers", () => {
}
});
test("APIKEY_PROVIDERS merges the 6 family files into 171 entries (no loss / no dup)", async () => {
test("APIKEY_PROVIDERS merges the 6 family files into 167 entries (no loss / no dup)", async () => {
const keys = Object.keys((P as Record<string, object>).APIKEY_PROVIDERS);
assert.equal(keys.length, 171);
assert.equal(new Set(keys).size, 171, "duplicate keys after spread-merge");
assert.equal(keys.length, 167);
assert.equal(new Set(keys).size, 167, "duplicate keys after spread-merge");
// the merged object's entry-count equals the sum of the 6 semantic family files; families are a
// strict partition (every provider in exactly one), so the sum must be exactly 171.
// strict partition (every provider in exactly one), so the sum must be exactly 167.
const families: [string, string][] = [
["gateways", "APIKEY_PROVIDERS_GATEWAYS"],
["frontier-labs", "APIKEY_PROVIDERS_FRONTIER"],
@@ -56,7 +57,7 @@ test("APIKEY_PROVIDERS merges the 6 family files into 171 entries (no loss / no
seen.add(k);
}
}
assert.equal(famTotal, 171, "families must partition all 171 providers");
assert.equal(famTotal, 167, "families must partition all 167 providers");
});
test("AI_PROVIDERS Proxy aggregates all sections; lookups resolve", () => {

View File

@@ -391,7 +391,7 @@ test("static catalog entries resolve local, search, audio, web-cookie and upstre
const audioProvider = providerPageUtils.resolveDashboardProviderInfo("assemblyai");
const awsPollyProvider = providerPageUtils.resolveDashboardProviderInfo("aws-polly");
const webCookieProvider = providerPageUtils.resolveDashboardProviderInfo("grok-web");
const apiKeyProvider = providerPageUtils.resolveDashboardProviderInfo("glhf");
const apiKeyProvider = providerPageUtils.resolveDashboardProviderInfo("synthetic");
const gitlabProvider = providerPageUtils.resolveDashboardProviderInfo("gitlab");
const gitlabDuoProvider = providerPageUtils.resolveDashboardProviderInfo("gitlab-duo");
const chutesProvider = providerPageUtils.resolveDashboardProviderInfo("chutes");
@@ -437,7 +437,7 @@ test("static catalog entries resolve local, search, audio, web-cookie and upstre
assert.equal(awsPollyProvider?.name, providers.AUDIO_ONLY_PROVIDERS["aws-polly"].name);
assert.equal(apiKeyProvider?.category, "apikey");
assert.equal(apiKeyProvider?.name, providers.APIKEY_PROVIDERS.glhf.name);
assert.equal(apiKeyProvider?.name, providers.APIKEY_PROVIDERS.synthetic.name);
assert.equal(gitlabProvider?.category, "apikey");
assert.equal(gitlabProvider?.name, providers.APIKEY_PROVIDERS.gitlab.name);
assert.equal(gitlabDuoProvider?.category, "oauth");
@@ -500,9 +500,8 @@ test("static catalog entries resolve local, search, audio, web-cookie and upstre
test("managed provider connection ids include supported static categories and exclude upstream proxy", () => {
assert.equal(providerCatalog.isManagedProviderConnectionId("qoder"), true);
assert.equal(providerCatalog.isManagedProviderConnectionId("glhf"), true);
assert.equal(providerCatalog.isManagedProviderConnectionId("synthetic"), true);
assert.equal(providerCatalog.isManagedProviderConnectionId("gitlab"), true);
assert.equal(providerCatalog.isManagedProviderConnectionId("cablyai"), true);
assert.equal(providerCatalog.isManagedProviderConnectionId("thebai"), true);
assert.equal(providerCatalog.isManagedProviderConnectionId("fenayai"), true);
assert.equal(providerCatalog.isManagedProviderConnectionId("chutes"), true);
@@ -559,10 +558,9 @@ test("grok-web taxonomy stays web-cookie only and does not leak into api-key ent
assert.equal("blackbox-web" in providers.WEB_COOKIE_PROVIDERS, true);
assert.equal("muse-spark-web" in providers.APIKEY_PROVIDERS, false);
assert.equal("muse-spark-web" in providers.WEB_COOKIE_PROVIDERS, true);
assert.equal("glhf" in providers.APIKEY_PROVIDERS, true);
assert.equal("synthetic" in providers.APIKEY_PROVIDERS, true);
assert.equal("gitlab" in providers.APIKEY_PROVIDERS, true);
assert.equal("gitlab-duo" in providers.OAUTH_PROVIDERS, true);
assert.equal("cablyai" in providers.APIKEY_PROVIDERS, true);
assert.equal("thebai" in providers.APIKEY_PROVIDERS, true);
assert.equal("fenayai" in providers.APIKEY_PROVIDERS, true);
assert.equal("chutes" in providers.APIKEY_PROVIDERS, true);
@@ -618,17 +616,13 @@ test("grok-web taxonomy stays web-cookie only and does not leak into api-key ent
false
);
assert.equal(
apiKeyEntries.some((entry) => entry.providerId === "glhf"),
apiKeyEntries.some((entry) => entry.providerId === "synthetic"),
true
);
assert.equal(
apiKeyEntries.some((entry) => entry.providerId === "gitlab"),
true
);
assert.equal(
apiKeyEntries.some((entry) => entry.providerId === "cablyai"),
true
);
assert.equal(
apiKeyEntries.some((entry) => entry.providerId === "thebai"),
true

View File

@@ -31,11 +31,11 @@ test.after(() => {
test("providers route accepts managed local, audio, web-cookie and search providers", async () => {
const cases = [
{
provider: "glhf",
provider: "synthetic",
body: {
provider: "glhf",
apiKey: "glhf-key",
name: "GLHF Chat",
provider: "synthetic",
apiKey: "synthetic-key",
name: "Synthetic",
},
},
{
@@ -46,14 +46,6 @@ test("providers route accepts managed local, audio, web-cookie and search provid
name: "GitLab Duo PAT",
},
},
{
provider: "cablyai",
body: {
provider: "cablyai",
apiKey: "cably-key",
name: "CablyAI Primary",
},
},
{
provider: "thebai",
body: {
@@ -457,9 +449,9 @@ test("DELETE /api/providers batch deletes connections", async () => {
})
);
const r1 = await createReq("glhf", "Conn 1", "key-1");
const r2 = await createReq("glhf", "Conn 2", "key-2");
const r3 = await createReq("glhf", "Conn 3", "key-3");
const r1 = await createReq("synthetic", "Conn 1", "key-1");
const r2 = await createReq("synthetic", "Conn 2", "key-2");
const r3 = await createReq("synthetic", "Conn 3", "key-3");
const id1 = (await r1.json()).connection.id;
const id2 = (await r2.json()).connection.id;
const id3 = (await r3.json()).connection.id;

View File

@@ -0,0 +1,84 @@
// Regression guard — support-mesh escalation (2026-07-08, whatsbrasil):
// an OmniRoute API key ("opencode-mac") showed "zero requisições" even though
// it received traffic. Root cause: requests rejected *before* handleChatCore
// (pipeline-gate / provider circuit breaker OPEN, or a combo with every target
// exhausted) short-circuit in src/sse/handlers/chat.ts and only wrote a
// call_logs row via saveCallLog — they never reached persistFailureUsage, so
// no usage_history row was created and the per-api-key usage counter
// (getApiKeyUsageRows, which reads usage_history) never incremented.
//
// The fix routes those rejections through recordRejectedRequestUsage(), which
// writes BOTH the call_logs row (dashboard/logs visibility, preserved) AND a
// usage_history row attributed to the api key with success:false — so the
// rejected traffic is counted per key.
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-rejected-usage-"));
process.env.DATA_DIR = TEST_DATA_DIR;
const core = await import("../../src/lib/db/core.ts");
const usageHistory = await import("../../src/lib/usage/usageHistory.ts");
const callLogs = await import("../../src/lib/usage/callLogs.ts");
const { recordRejectedRequestUsage } = await import("../../src/sse/handlers/rejectedRequestUsage.ts");
test.beforeEach(() => {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
usageHistory.clearPendingRequests();
});
test.after(() => {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
});
test("gate-rejected request is attributed to the api key in usage_history", async () => {
await recordRejectedRequestUsage({
status: 503,
model: "claude-sonnet-5",
requestedModel: "claude-sonnet-5",
provider: "anthropic",
endpoint: "/v1/chat/completions",
error: "[503] Pipeline gate rejected",
apiKeyId: "key-opencode-mac",
apiKeyName: "opencode-mac",
startTime: Date.now() - 5,
});
// usage_history row exists, attributed to the key, marked as a failure.
const rows = (await usageHistory.getUsageDb()).data.history;
const keyRows = rows.filter((r: { apiKeyId?: string | null }) => r.apiKeyId === "key-opencode-mac");
assert.equal(keyRows.length, 1, "expected one usage_history row for the rejected request");
assert.equal(keyRows[0].success, false, "rejected request must be recorded as success:false");
// call_logs visibility is preserved (dashboard/logs).
const logs = await callLogs.getCallLogs({});
const rejected = (logs.logs ?? logs).filter?.((l: { apiKeyName?: string | null }) => l.apiKeyName === "opencode-mac");
assert.ok(rejected && rejected.length >= 1, "expected a call_logs row for the rejected request");
});
test("combo-exhausted rejection is also counted per api key", async () => {
await recordRejectedRequestUsage({
status: 502,
model: "gpt-5",
requestedModel: "gpt-5",
provider: "-",
endpoint: "/v1/chat/completions",
error: '[502] Combo "prod" failed — all targets exhausted',
comboName: "prod",
apiKeyId: "key-opencode-mac",
apiKeyName: "opencode-mac",
startTime: Date.now() - 3,
});
const rows = (await usageHistory.getUsageDb()).data.history;
const keyRows = rows.filter((r: { apiKeyId?: string | null }) => r.apiKeyId === "key-opencode-mac");
assert.equal(keyRows.length, 1);
assert.equal(keyRows[0].success, false);
});

View File

@@ -1,4 +1,4 @@
import test from "node:test";
import test from "node:test";
import assert from "node:assert/strict";
import { EventEmitter } from "node:events";
import { createRequire } from "node:module";
@@ -56,6 +56,11 @@ function createFakeProcess({ onKill } = {}) {
async function withSandboxModule(fakeSpawn, fn) {
const originalSpawn = childProcess.spawn;
const originalRuntime = process.env["SKILLS_SANDBOX_RUNTIME"];
// Pin to docker for existing tests so the hardcoded args[0] === "run" /
// args[0] === "kill" assertions remain deterministic regardless of the
// host's installed container runtimes.
process.env["SKILLS_SANDBOX_RUNTIME"] = "docker";
childProcess.spawn = fakeSpawn;
try {
@@ -65,6 +70,11 @@ async function withSandboxModule(fakeSpawn, fn) {
return await fn(module);
} finally {
childProcess.spawn = originalSpawn;
if (originalRuntime === undefined) {
delete process.env["SKILLS_SANDBOX_RUNTIME"];
} else {
process.env["SKILLS_SANDBOX_RUNTIME"] = originalRuntime;
}
}
}
@@ -350,3 +360,205 @@ test("sandboxRunner handles success, spawn errors, timeouts, and killAll cleanup
}
);
});
test("sandboxRunner kill/killAll fallback naming matches containerProvider's SANDBOX_NAME convention", async () => {
const calls = [];
await withSandboxModule(
(_command, args) => {
calls.push({ args });
return createFakeProcess();
},
async ({ sandboxRunner }) => {
// A freshly-imported sandboxRunner has never called run(), so
// cachedProvider is still null and kill()/killAll() must fall back to
// the docker CLI directly — that fallback name must still match
// containerProvider.ts's SANDBOX_NAME (`omniroute-${id}`), not the
// pre-PR `omniroute-sandbox-${id}` convention.
const proc = createFakeProcess();
sandboxRunner.runningContainers.set("fallback-id", proc);
sandboxRunner.kill("fallback-id");
const killCall = calls.find((entry) => entry.args[0] === "kill");
assert.ok(killCall, "kill command should have been issued");
assert.equal(killCall.args[1], "omniroute-fallback-id");
const procA = createFakeProcess();
const procB = createFakeProcess();
sandboxRunner.runningContainers.set("fallback-a", procA);
sandboxRunner.runningContainers.set("fallback-b", procB);
sandboxRunner.killAll();
const killAllNames = calls
.filter((entry) => entry.args[0] === "kill")
.map((entry) => entry.args[1]);
assert.ok(killAllNames.includes("omniroute-fallback-a"));
assert.ok(killAllNames.includes("omniroute-fallback-b"));
}
);
});
// -------------------------------------------------------------
// Container Provider Unit Tests
// -------------------------------------------------------------
test("containerProvider: all five providers registered", () => {
// Dynamic import to avoid polluting the sandbox module's state
return importFresh("src/lib/skills/containerProvider.ts").then((mod) => {
assert.ok(mod.ALL_PROVIDERS.length === 5);
assert.deepStrictEqual(
mod.ALL_PROVIDERS.map((p) => p.id),
["docker", "apple", "wsl", "orbstack", "podman"],
);
assert.ok(mod.PROVIDER_BY_ID.has("docker"));
assert.ok(mod.PROVIDER_BY_ID.has("apple"));
assert.ok(mod.PROVIDER_BY_ID.has("wsl"));
assert.ok(mod.PROVIDER_BY_ID.has("orbstack"));
assert.ok(mod.PROVIDER_BY_ID.has("podman"));
});
});
test("containerProvider: platformPriority returns correct order per OS", () => {
return importFresh("src/lib/skills/containerProvider.ts").then((mod) => {
const originalPlatform = Object.getOwnPropertyDescriptor(
process,
"platform",
);
// darwin
Object.defineProperty(process, "platform", { value: "darwin" });
assert.deepStrictEqual(mod.platformPriority(), [
"apple",
"orbstack",
"podman",
"docker",
]);
// win32
Object.defineProperty(process, "platform", { value: "win32" });
assert.deepStrictEqual(mod.platformPriority(), [
"wsl",
"docker",
"podman",
]);
// linux
Object.defineProperty(process, "platform", { value: "linux" });
assert.deepStrictEqual(mod.platformPriority(), ["podman", "docker"]);
// Restore
if (originalPlatform) {
Object.defineProperty(
process,
"platform",
originalPlatform,
);
}
});
});
test("containerProvider: buildRun produces run as args[0] for all providers", () => {
return importFresh("src/lib/skills/containerProvider.ts").then((mod) => {
const config = {
cpuLimit: 100,
memoryLimit: 256,
timeout: 30000,
networkEnabled: false,
readOnly: true,
};
for (const provider of mod.ALL_PROVIDERS) {
const resolved = provider.buildRun(
"alpine",
["echo", "hi"],
"test-id",
config,
);
assert.equal(
resolved.args[0],
"run",
`${provider.id}: args[0] must be "run"`,
);
assert.ok(
resolved.args.includes("--rm"),
`${provider.id}: should include --rm`,
);
assert.ok(
resolved.args.includes("alpine"),
`${provider.id}: should include image`,
);
// killArgs must return something callable
const kill = resolved.killArgs("test-cont");
assert.ok(Array.isArray(kill), `${provider.id}: killArgs returns array`);
assert.ok(kill.length > 0, `${provider.id}: killArgs non-empty`);
}
});
});
test("containerProvider: buildKillArgs returns kill|stop for cleanup", () => {
return importFresh("src/lib/skills/containerProvider.ts").then((mod) => {
// Every provider should return an array whose first element is
// its known cleanup verb.
const verbs = new Map([
["docker", "kill"],
["apple", "kill"],
["wsl", "kill"],
["orbstack", "kill"],
["podman", "kill"],
]);
for (const provider of mod.ALL_PROVIDERS) {
const expectedVerb = verbs.get(provider.id);
const args = provider.buildKillArgs("test-cont");
assert.equal(args[0], expectedVerb, `${provider.id} kill verb`);
}
});
});
test("containerProvider: buildKillCommand utility", () => {
return importFresh("src/lib/skills/containerProvider.ts").then((mod) => {
const dockerProvider = mod.PROVIDER_BY_ID.get("docker")!;
const result = mod.buildKillCommand(dockerProvider, "test-id");
assert.equal(result.command, "docker");
assert.equal(result.args[0], "kill");
assert.equal(result.args[1], "omniroute-test-id");
});
});
test("containerProvider: resolveProvider respects SKILLS_SANDBOX_RUNTIME override", async () => {
// Unpin the global env for this test
delete process.env.SKILLS_SANDBOX_RUNTIME;
const mod = await importFresh("src/lib/skills/containerProvider.ts");
mod._resetProviderCacheForTests();
process.env.SKILLS_SANDBOX_RUNTIME = "docker";
const provider = await mod.resolveProvider();
assert.equal(provider.id, "docker");
process.env.SKILLS_SANDBOX_RUNTIME = "apple";
mod._resetProviderCacheForTests();
const provider2 = await mod.resolveProvider();
assert.equal(provider2.id, "apple");
process.env.SKILLS_SANDBOX_RUNTIME = "wsl";
mod._resetProviderCacheForTests();
const provider3 = await mod.resolveProvider();
assert.equal(provider3.id, "wsl");
delete process.env.SKILLS_SANDBOX_RUNTIME;
mod._resetProviderCacheForTests();
});
test("containerProvider: resolveProvider falls back to docker when no runtime installed", async () => {
delete process.env.SKILLS_SANDBOX_RUNTIME;
const mod = await importFresh("src/lib/skills/containerProvider.ts");
mod._resetProviderCacheForTests();
// Auto-detect walks platform priority — if nothing is installed we
// always land on docker as the fallback.
const provider = await mod.resolveProvider();
assert.ok(
["docker", "apple", "wsl", "podman", "orbstack"].includes(provider.id),
);
// Ensure the fallback is always docker when probes fail
// (this test is best-effort — on a host with docker installed,
// the auto-detect will legitimately pick docker)
});

View File

@@ -0,0 +1,94 @@
import test from "node:test";
import assert from "node:assert/strict";
const { getRegistryEntry } = await import("../../open-sse/config/providerRegistry.ts");
const { getModelInfoCore } = await import("../../open-sse/services/model.ts");
const { DEFAULT_PRICING_INFERENCE } =
await import("../../src/shared/constants/pricing/inference-hosts.ts");
const INCLUDED_DIRECT_MODELS = [
"hf:openai/gpt-oss-120b",
"hf:zai-org/GLM-5.2",
"hf:moonshotai/Kimi-K2.7-Code",
"hf:Qwen/Qwen3.6-27B",
"hf:MiniMaxAI/MiniMax-M3",
"hf:zai-org/GLM-4.7-Flash",
"hf:nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-NVFP4",
] as const;
const SYNTHETIC_MODEL_ALIASES = {
"syn:gpt-oss-120b": "hf:openai/gpt-oss-120b",
"syn:large:text": "hf:zai-org/GLM-5.2",
"syn:large:vision": "hf:moonshotai/Kimi-K2.7-Code",
"syn:small:vision": "hf:Qwen/Qwen3.6-27B",
"syn:minimax-m3": "hf:MiniMaxAI/MiniMax-M3",
"syn:small:text": "hf:zai-org/GLM-4.7-Flash",
"syn:nemotron-3-super": "hf:nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-NVFP4",
} as const;
function syntheticEntry() {
const entry = getRegistryEntry("synthetic");
assert.ok(entry, "synthetic provider should be registered");
return entry;
}
test("synthetic provider targets api.synthetic.new", () => {
const entry = syntheticEntry();
assert.equal(entry.baseUrl, "https://api.synthetic.new/openai/v1/chat/completions");
assert.equal(entry.modelsUrl, "https://api.synthetic.new/openai/v1/models");
assert.equal(entry.passthroughModels, true);
});
test("synthetic static catalog tracks the included direct models", () => {
const ids = syntheticEntry().models.map((model) => model.id);
assert.deepEqual(ids, INCLUDED_DIRECT_MODELS);
});
test("synthetic catalog carries current context and capability metadata", () => {
const models = new Map(syntheticEntry().models.map((model) => [model.id, model]));
assert.equal(models.get("hf:zai-org/GLM-5.2")?.contextLength, 524288);
assert.equal(models.get("hf:zai-org/GLM-4.7-Flash")?.contextLength, 196608);
assert.equal(models.get("hf:openai/gpt-oss-120b")?.contextLength, 131072);
for (const id of INCLUDED_DIRECT_MODELS) {
assert.equal(models.get(id)?.toolCalling, true, `${id} should advertise tool calling`);
assert.equal(models.get(id)?.supportsReasoning, true, `${id} should advertise reasoning`);
assert.equal(models.get(id)?.maxOutputTokens, 65536, `${id} should advertise max output`);
}
for (const id of [
"hf:moonshotai/Kimi-K2.7-Code",
"hf:Qwen/Qwen3.6-27B",
"hf:MiniMaxAI/MiniMax-M3",
]) {
assert.equal(models.get(id)?.supportsVision, true, `${id} should advertise vision`);
}
});
test("synthetic catalog exposes short aliases for the provider-native ids", async () => {
const models = new Map(syntheticEntry().models.map((model) => [model.id, model]));
for (const [alias, canonicalId] of Object.entries(SYNTHETIC_MODEL_ALIASES)) {
assert.deepEqual(models.get(canonicalId)?.aliases, [alias]);
const resolved = await getModelInfoCore(`synthetic/${alias}`, null);
assert.equal(resolved.provider, "synthetic");
assert.equal(resolved.model, canonicalId);
}
});
test("synthetic pricing covers every curated static model", () => {
const pricing = DEFAULT_PRICING_INFERENCE.synthetic;
for (const id of INCLUDED_DIRECT_MODELS) {
assert.ok(pricing[id], `missing synthetic pricing for ${id}`);
}
assert.deepEqual(pricing["hf:zai-org/GLM-5.2"], {
input: 1.4,
output: 4.4,
cached: 1.4,
reasoning: 0,
cache_creation: 0,
});
});

View File

@@ -0,0 +1,110 @@
// Tests for validateWebCookieProvider fallback when no registry entry exists.
// Covers providers like lmarena, gemini-business, poe-web, venice-web and v0-vercel-web
// that are listed in WEB_COOKIE_PROVIDERS but have no entry in providerRegistry.ts.
//
// These providers only expose a marketing website URL (WEB_COOKIE_PROVIDERS[id].website),
// not a real API host. Probing `${website}/models` does not reliably signal session
// validity — live verification showed most of these hosts return redirects or SPA 200s
// regardless of cookie validity, which would silently report an expired/garbage cookie as
// "OK". Until each provider has a verified, side-effect-free auth probe against its real
// API host, validateWebCookieProvider reports `unsupported: true` for this fallback case
// instead of a false "valid" — and does so WITHOUT making any network probe.
import test from "node:test";
import assert from "node:assert/strict";
const { validateProviderApiKey } = await import("../../src/lib/providers/validation.ts");
const originalFetch = globalThis.fetch;
const fetchCalls: Array<{ url: string; headers: Record<string, string> }> = [];
test.beforeEach(() => {
fetchCalls.length = 0;
globalThis.fetch = (async (url: string | URL, init?: RequestInit) => {
const headers: Record<string, string> = {};
if (init?.headers) {
if (init.headers instanceof Headers) {
init.headers.forEach((v, k) => {
headers[k] = v;
});
} else if (Array.isArray(init.headers)) {
for (const [k, v] of init.headers) headers[k] = v;
} else {
Object.assign(headers, init.headers);
}
}
fetchCalls.push({ url: String(url), headers });
// If this mock is ever hit for a no-registry-entry provider, the test will fail on
// the `fetchCalls.length` assertion below — this response is never meant to be read.
return new Response("", { status: 404 });
}) as typeof fetch;
});
test.afterEach(() => {
globalThis.fetch = originalFetch;
});
// ── lmarena (no registry entry, falls back to WEB_COOKIE_PROVIDERS) ──
test("lmarena validation is unsupported (no verified auth probe) and makes no network call", async () => {
const result = await validateProviderApiKey({
provider: "lmarena",
apiKey: "test-lmarena-cookie",
});
assert.strictEqual(result.valid, false);
assert.equal(result.unsupported, true);
assert.equal(fetchCalls.length, 0, "must not probe the marketing website");
});
test("lmarena validation rejects empty cookie before checking support", async () => {
const result = await validateProviderApiKey({
provider: "lmarena",
apiKey: "",
});
assert.strictEqual(result.valid, false);
assert.match(result.error, /api key required|cookie/i);
assert.equal(fetchCalls.length, 0);
});
// ── gemini-business (no registry entry, falls back to WEB_COOKIE_PROVIDERS) ──
test("gemini-business validation is unsupported and makes no network call", async () => {
const result = await validateProviderApiKey({
provider: "gemini-business",
apiKey: "test-gemini-cookie",
});
assert.strictEqual(result.valid, false);
assert.equal(result.unsupported, true);
assert.equal(fetchCalls.length, 0);
});
// ── remaining WEB_COOKIE_PROVIDERS-only providers (no registry entry) ──
// NOTE: doubao-web and zenmux-free are intentionally NOT covered here — unlike when this
// fix was proposed, both now carry a providerRegistry.ts entry (added independently of
// this PR), so they no longer exercise the no-registry-entry fallback branch this test
// file targets; they go through the pre-existing entry-based probe instead, which is out
// of scope for this fix.
for (const provider of ["poe-web", "venice-web", "v0-vercel-web"]) {
test(`${provider} validation is unsupported and makes no network call`, async () => {
const result = await validateProviderApiKey({
provider,
apiKey: "some-cookie-value",
});
assert.strictEqual(result.valid, false);
assert.equal(result.unsupported, true);
assert.equal(fetchCalls.length, 0);
});
}
// ── generic fallback guard ──
test("unknown web-cookie provider without registry returns unsupported", async () => {
const result = await validateProviderApiKey({
provider: "fake-web",
apiKey: "some-key",
});
assert.strictEqual(result.valid, false);
assert.equal(result.unsupported, true);
});