From f4643a247687d2253eaa44a87790fd9acd7f1ef9 Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza Date: Wed, 8 Jul 2026 22:33:35 -0300 Subject: [PATCH 01/15] docs(changelog): add v3.8.47 Contributors section (32 contributors) --- CHANGELOG.md | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ec18fa8293..11bef1abed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -56,6 +56,45 @@ _Living section — bullets land here as PRs merge into `release/v3.8.47` (paral - **chore(cli):** the shell-completion cache paths (`readCache`/`refreshCache`/`writeCache`) in `bin/cli/commands/completion.mjs` no longer swallow errors into a bare `catch {}` ([#6257](https://github.com/diegosouzapw/OmniRoute/pull/6257)) — each now binds the error and, when the new `OMNIROUTE_DEBUG_COMPLETION` env var is set, emits a `[omniroute completion]` diagnostic to `stderr`; the caches still fail silently by default so a missing/corrupt cache never breaks tab-completion. (thanks @KooshaPari) - **chore(quality):** `validate-release-green --full-ci` reproduces the full `ci.yml` static gate set locally — the pre-flight now reads `ci.yml` itself and runs every `npm run check:*` from the `lint` / `quality-gate` / `quality-extended` / `docs-sync-strict` / `pr-test-policy` jobs (`--` ratchet flags preserved, `test-masking` against `GITHUB_BASE_REF=main`), skipping only the non-local `pr-evidence`/`codeql-ratchet`. Closes the gap where 11 static base-reds leaked to the v3.8.46 release PR in ~2h of layered CI. Also wired into `nightly-release-green` so a static base-red opens a tracking issue the night it lands. Regression guard: `tests/unit/validate-release-green.test.ts` (+5 `extractCiGates` cases). +### 🙌 Contributors + +Thanks to everyone whose work landed in v3.8.47: + +| Contributor | PRs / Issues | +| -------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | +| [@anki1kr](https://github.com/anki1kr) | #6041, #6078 | +| [@arssnndr](https://github.com/arssnndr) | #6163 | +| [@backryun](https://github.com/backryun) | #6154, #6235, #6248, #6331 | +| [@charleszolot](https://github.com/charleszolot) | direct commit / report | +| [@chirag127](https://github.com/chirag127) | #6145, #6189, #6265, #6328, #6400, #6402, #6404, #6405, #6406, #6407, #6408, #6412, #6414, … | +| [@developerjillur](https://github.com/developerjillur) | #6451, #6452, #6541, #6542, #6543, #6545, #6553, #6554, #6558 | +| [@dilneiss](https://github.com/dilneiss) | #6499 | +| [@DKotsyuba](https://github.com/DKotsyuba) | #6193, #6292 | +| [@dtybnrj](https://github.com/dtybnrj) | #6349 | +| [@eidoog](https://github.com/eidoog) | direct commit / report | +| [@hao3039032](https://github.com/hao3039032) | #6351 | +| [@hartmark](https://github.com/hartmark) | #6216 | +| [@Iammilansoni](https://github.com/Iammilansoni) | #6200, #6209, #6245, #6366 | +| [@jmengit](https://github.com/jmengit) | #6372, #6443 | +| [@jordansilly77-stack](https://github.com/jordansilly77-stack) | #6316 | +| [@JxnLexn](https://github.com/JxnLexn) | #6361 | +| [@kanztu](https://github.com/kanztu) | #6181 | +| [@karimalsalah](https://github.com/karimalsalah) | #6291 | +| [@KooshaPari](https://github.com/KooshaPari) | #6144, #6166, #6173, #6257 | +| [@LuisAlejandroVega](https://github.com/LuisAlejandroVega) | #6177 | +| [@makcimbx](https://github.com/makcimbx) | #6303 | +| [@Moseyuh333](https://github.com/Moseyuh333) | #6186 | +| [@pizzav-xyz](https://github.com/pizzav-xyz) | #6648 | +| [@rianonehub](https://github.com/rianonehub) | #6204 | +| [@serverless83](https://github.com/serverless83) | #6212 | +| [@shabeer](https://github.com/shabeer) | direct commit / report | +| [@swingtempo](https://github.com/swingtempo) | #6312 | +| [@Theadd](https://github.com/Theadd) | #6195 | +| [@ThongAccount](https://github.com/ThongAccount) | #6649 | +| [@vinayakkulkarni](https://github.com/vinayakkulkarni) | #6324, #6332 | +| [@VXNCXNX](https://github.com/VXNCXNX) | #6213 | +| [@xz-dev](https://github.com/xz-dev) | #6322, #6336 | + --- ## [3.8.46] — 2026-07-07 From 902e66805cd1a777554c0a2b213b7c3c97239b31 Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza Date: Wed, 8 Jul 2026 23:29:30 -0300 Subject: [PATCH 02/15] chore(vscode): update search exclude patterns and add documentation Add several directories to the search exclude list to improve search performance and add a comment explaining why certain directories are not being hidden from the file explorer. --- .vscode/settings.json | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 261dc258aa..2f303c5a82 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -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, From f4fb0d310be69d52a8271b4b4f9c29d46d9472cd Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza Date: Thu, 9 Jul 2026 01:33:33 -0300 Subject: [PATCH 03/15] docs(readme): update star badges and star history chart links --- README.md | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 8bce5bdf96..7ca2c34529 100644 --- a/README.md +++ b/README.md @@ -21,10 +21,15 @@

-⭐ 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. +

+[![Stars](https://img.shields.io/github/stars/diegosouzapw/OmniRoute?style=social)](https://github.com/diegosouzapw/OmniRoute) diegosouzapw%2FOmniRoute | Trendshift +[![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) @@ -1150,14 +1155,13 @@ gh release create v3.8.2 --title "v3.8.2" --generate-notes ## 📊 Stars - + - - - Star History Chart + + + Star History Chart -
From 0e4145d2577cb618e7cb89b101a14d0b4014a72e Mon Sep 17 00:00:00 2001 From: backryun Date: Fri, 10 Jul 2026 04:44:28 +0900 Subject: [PATCH 04/15] fix(providers): remove obsolete providers (glhf, kluster, cablyai, inclusionai) (#6675) Drop dead catalog/registry entries, keep Synthetic as the GLHF replacement path, regenerate provider reference/docs counts, and lock APIKEY family-split + file-size gates so CI stays green. Ignore prettier on freeModelCatalog.data.ts so dense one-line budget rows are not expanded past the 800-line new-file cap. --- .prettierignore | 3 + AGENTS.md | 4 +- CLAUDE.md | 2 +- README.md | 22 +- config/quality/eslint-suppressions.json | 2 +- config/quality/file-size-baseline.json | 4 +- docs/guides/FEATURES.md | 2 +- docs/reference/FREE_TIERS.md | 315 +++++------ docs/reference/PROVIDER_REFERENCE.md | 533 +++++++++--------- open-sse/config/freeModelCatalog.data.ts | 1 - open-sse/config/freeTierCatalog.ts | 1 - open-sse/config/providers/index.ts | 6 - .../config/providers/registry/glhf/index.ts | 12 - .../providers/registry/inclusionai/index.ts | 12 - .../providers/registry/kluster/index.ts | 12 - .../providers/registry/synthetic/index.ts | 72 ++- open-sse/config/providers/shared.ts | 1 + open-sse/services/model.ts | 9 + .../providers/[id]/__tests__/phase1e.test.tsx | 62 ++ .../components/PassthroughModelsSection.tsx | 18 +- .../[id]/components/ProviderModelsSection.tsx | 1 + .../providers/[id]/providerPageHelpers.ts | 3 +- .../models/discovery/providerModelsConfig.ts | 8 - src/shared/components/ProviderIcon.tsx | 6 +- .../constants/pricing/inference-hosts.ts | 51 ++ src/shared/constants/providers.ts | 2 - .../constants/providers/apikey/gateways.ts | 54 +- .../constants/providers/apikey/regional.ts | 18 - tests/snapshots/provider/translate-path.json | 69 --- .../unit/discontinued-providers-2026.test.ts | 33 +- tests/unit/free-model-catalog.test.ts | 5 +- tests/unit/free-tier-catalog.test.ts | 4 +- tests/unit/inclusionai-provider.test.ts | 17 + .../provider-models-discovery-split.test.ts | 7 +- tests/unit/provider-models-route.test.ts | 23 +- tests/unit/providers-constants-split.test.ts | 13 +- tests/unit/providers-page-utils.test.ts | 16 +- .../providers-route-managed-catalog.test.ts | 22 +- tests/unit/synthetic-provider.test.ts | 94 +++ 39 files changed, 826 insertions(+), 713 deletions(-) delete mode 100644 open-sse/config/providers/registry/glhf/index.ts delete mode 100644 open-sse/config/providers/registry/inclusionai/index.ts delete mode 100644 open-sse/config/providers/registry/kluster/index.ts create mode 100644 tests/unit/inclusionai-provider.test.ts create mode 100644 tests/unit/synthetic-provider.test.ts diff --git a/.prettierignore b/.prettierignore index b2256c1219..a0e473264e 100644 --- a/.prettierignore +++ b/.prettierignore @@ -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 diff --git a/AGENTS.md b/AGENTS.md index 00ef4b2547..c82bd8b669 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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`.** diff --git a/CLAUDE.md b/CLAUDE.md index 43f746ca4e..a8de5434a3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 | | ------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | diff --git a/README.md b/README.md index 7ca2c34529..6b50bf4c8b 100644 --- a/README.md +++ b/README.md @@ -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.**
@@ -31,8 +31,8 @@
-[![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) +[![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) @@ -61,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) @@ -149,11 +149,11 @@ -> 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. - + @@ -313,7 +313,7 @@ Result: 4 layers of fallback = zero downtime | Feature | OmniRoute | Other routers | | -------------------------------------- | ------------------------------------------------------------------- | ------------- | -| 🌐 Providers | **237** | 20–100 | +| 🌐 Providers | **248** | 20–100 | | 🆓 Free providers | **90+ (11 free forever)** | 1–5 | | 🔀 Routing strategies | **17** (priority, weighted, cost-optimized, context-relay, fusion…) | 1–3 | | 🗜️ Token compression | **RTK + Caveman stacked (15–95%)** | None / 20–40% | @@ -351,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)
@@ -394,11 +394,11 @@ Result: 4 layers of fallback = zero downtime
-# 🌐 237 AI Providers — 90+ Free +# 🌐 248 AI Providers — 90+ Free
-> 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**.
@@ -906,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) diff --git a/config/quality/eslint-suppressions.json b/config/quality/eslint-suppressions.json index 075ef65db1..1481b37aa9 100644 --- a/config/quality/eslint-suppressions.json +++ b/config/quality/eslint-suppressions.json @@ -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": { diff --git a/config/quality/file-size-baseline.json b/config/quality/file-size-baseline.json index 81739a3e88..600568f937 100644 --- a/config/quality/file-size-baseline.json +++ b/config/quality/file-size-baseline.json @@ -56,8 +56,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 (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 `/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 `/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 `/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 `/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.)", diff --git a/docs/guides/FEATURES.md b/docs/guides/FEATURES.md index 807352b977..6f7ece1eb3 100644 --- a/docs/guides/FEATURES.md +++ b/docs/guides/FEATURES.md @@ -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 W1–W4** — 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 diff --git a/docs/reference/FREE_TIERS.md b/docs/reference/FREE_TIERS.md index 5651dd658f..775586ab0a 100644 --- a/docs/reference/FREE_TIERS.md +++ b/docs/reference/FREE_TIERS.md @@ -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 (15–95% 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/… diff --git a/docs/reference/PROVIDER_REFERENCE.md b/docs/reference/PROVIDER_REFERENCE.md index 63733efa31..e26558e11b 100644 --- a/docs/reference/PROVIDER_REFERENCE.md +++ b/docs/reference/PROVIDER_REFERENCE.md @@ -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 (~25K–100K 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 ', ~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 (~25K–100K 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 ', ~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/?…&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://.services.ai.azure.com/openai/v1/ or https://.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 . 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 . | -| `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/. | -| `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 . 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://--.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 . OmniRoute targets the chatbot endpoint on https://api.nlpcloud.io/v1/gpu//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..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-. 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 . 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 . 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://.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 . 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://.services.ai.azure.com/openai/v1/ or https://.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 . 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 . | +| `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/. | +| `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 . 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 . 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://--.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 . OmniRoute targets the chatbot endpoint on https://api.nlpcloud.io/v1/gpu//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..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-. 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 . 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 . 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 . 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://.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 . 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 . 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 diff --git a/open-sse/config/freeModelCatalog.data.ts b/open-sse/config/freeModelCatalog.data.ts index 81cfb498a7..a9accafb60 100644 --- a/open-sse/config/freeModelCatalog.data.ts +++ b/open-sse/config/freeModelCatalog.data.ts @@ -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" }, diff --git a/open-sse/config/freeTierCatalog.ts b/open-sse/config/freeTierCatalog.ts index 48a08179fb..5262697625 100644 --- a/open-sse/config/freeTierCatalog.ts +++ b/open-sse/config/freeTierCatalog.ts @@ -21,7 +21,6 @@ export const FREE_TIER_BUDGETS: Record = { "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, diff --git a/open-sse/config/providers/index.ts b/open-sse/config/providers/index.ts index 5e49767b63..a7e0fa51eb 100644 --- a/open-sse/config/providers/index.ts +++ b/open-sse/config/providers/index.ts @@ -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 = { nebius: nebiusProvider, fireworks: fireworksProvider, llamagate: llamagateProvider, - inclusionai: inclusionaiProvider, glm: glmProvider, glmt: glmtProvider, "glm-cn": glm_cnProvider, @@ -274,7 +270,6 @@ export const REGISTRY: Record = { 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 = { "chatgpt-web": chatgpt_webProvider, openrouter: openrouterProvider, orcarouter: orcarouterProvider, - glhf: glhfProvider, "copilot-web": copilot_webProvider, "copilot-m365-web": copilot_m365_webProvider, stepfun: stepfunProvider, diff --git a/open-sse/config/providers/registry/glhf/index.ts b/open-sse/config/providers/registry/glhf/index.ts deleted file mode 100644 index 845a19dce7..0000000000 --- a/open-sse/config/providers/registry/glhf/index.ts +++ /dev/null @@ -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" }], -}; diff --git a/open-sse/config/providers/registry/inclusionai/index.ts b/open-sse/config/providers/registry/inclusionai/index.ts deleted file mode 100644 index d7a3919e5a..0000000000 --- a/open-sse/config/providers/registry/inclusionai/index.ts +++ /dev/null @@ -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" }], -}; diff --git a/open-sse/config/providers/registry/kluster/index.ts b/open-sse/config/providers/registry/kluster/index.ts deleted file mode 100644 index 4fae0ec376..0000000000 --- a/open-sse/config/providers/registry/kluster/index.ts +++ /dev/null @@ -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" }], -}; diff --git a/open-sse/config/providers/registry/synthetic/index.ts b/open-sse/config/providers/registry/synthetic/index.ts index 611676ebed..cc9ab28e8f 100644 --- a/open-sse/config/providers/registry/synthetic/index.ts +++ b/open-sse/config/providers/registry/synthetic/index.ts @@ -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, }; diff --git a/open-sse/config/providers/shared.ts b/open-sse/config/providers/shared.ts index 4059673697..9582f66a9b 100644 --- a/open-sse/config/providers/shared.ts +++ b/open-sse/config/providers/shared.ts @@ -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; diff --git a/open-sse/services/model.ts b/open-sse/services/model.ts index 03b8ba4853..a742155147 100644 --- a/open-sse/services/model.ts +++ b/open-sse/services/model.ts @@ -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 diff --git a/src/app/(dashboard)/dashboard/providers/[id]/__tests__/phase1e.test.tsx b/src/app/(dashboard)/dashboard/providers/[id]/__tests__/phase1e.test.tsx index a65bad6019..bee9371593 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/__tests__/phase1e.test.tsx +++ b/src/app/(dashboard)/dashboard/providers/[id]/__tests__/phase1e.test.tsx @@ -227,6 +227,68 @@ describe("PassthroughModelRow — render smoke test", () => { }); }); +describe("PassthroughModelsSection — catalog model fallback", () => { + let container: HTMLElement; + let root: ReturnType; + + 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( + 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; diff --git a/src/app/(dashboard)/dashboard/providers/[id]/components/PassthroughModelsSection.tsx b/src/app/(dashboard)/dashboard/providers/[id]/components/PassthroughModelsSection.tsx index 5e2f2a89be..ea36db0671 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/components/PassthroughModelsSection.tsx +++ b/src/app/(dashboard)/dashboard/providers/[id]/components/PassthroughModelsSection.tsx @@ -48,6 +48,7 @@ export type ModelCompatSavePatchPassthrough = { export interface PassthroughModelsSectionProps { providerAlias: string; modelAliases: Record; + 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, diff --git a/src/app/(dashboard)/dashboard/providers/[id]/components/ProviderModelsSection.tsx b/src/app/(dashboard)/dashboard/providers/[id]/components/ProviderModelsSection.tsx index 1b252b209e..d642fcf6f8 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/components/ProviderModelsSection.tsx +++ b/src/app/(dashboard)/dashboard/providers/[id]/components/ProviderModelsSection.tsx @@ -306,6 +306,7 @@ export default function ProviderModelsSection({ ; - 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 = { openai: "compatProtocolOpenAI", "openai-responses": "compatProtocolOpenAIResponses", diff --git a/src/app/api/providers/[id]/models/discovery/providerModelsConfig.ts b/src/app/api/providers/[id]/models/discovery/providerModelsConfig.ts index b7bb1940a8..6e8af36e05 100644 --- a/src/app/api/providers/[id]/models/discovery/providerModelsConfig.ts +++ b/src/app/api/providers/[id]/models/discovery/providerModelsConfig.ts @@ -131,14 +131,6 @@ export const PROVIDER_MODELS_CONFIG: Record = 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 diff --git a/src/shared/components/ProviderIcon.tsx b/src/shared/components/ProviderIcon.tsx index c12fb7ec7c..564ec6ca1d 100644 --- a/src/shared/components/ProviderIcon.tsx +++ b/src/shared/components/ProviderIcon.tsx @@ -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 ( - + {/* eslint-disable-next-line @next/next/no-img-element -- operator-supplied remote URL, not a static/known asset */} ", - "Content-Type": "application/json" - }, - "nonStream": { - "Authorization": "Bearer ", - "Content-Type": "application/json" - }, - "oauth": { - "Accept": "text/event-stream", - "Authorization": "Bearer ", - "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 ", - "Content-Type": "application/json" - }, - "nonStream": { - "Authorization": "Bearer ", - "Content-Type": "application/json" - }, - "oauth": { - "Accept": "text/event-stream", - "Authorization": "Bearer ", - "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 ", - "Content-Type": "application/json" - }, - "nonStream": { - "Authorization": "Bearer ", - "Content-Type": "application/json" - }, - "oauth": { - "Accept": "text/event-stream", - "Authorization": "Bearer ", - "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": { diff --git a/tests/unit/discontinued-providers-2026.test.ts b/tests/unit/discontinued-providers-2026.test.ts index 79fef1ead5..1979a7cd9a 100644 --- a/tests/unit/discontinued-providers-2026.test.ts +++ b/tests/unit/discontinued-providers-2026.test.ts @@ -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)[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; const apikey = APIKEY_PROVIDERS as Record; - 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" + ); }); }); diff --git a/tests/unit/free-model-catalog.test.ts b/tests/unit/free-model-catalog.test.ts index 60dfa2e6a4..96e11e3f3d 100644 --- a/tests/unit/free-model-catalog.test.ts +++ b/tests/unit/free-model-catalog.test.ts @@ -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. diff --git a/tests/unit/free-tier-catalog.test.ts b/tests/unit/free-tier-catalog.test.ts index b0845e015b..e5c77306d7 100644 --- a/tests/unit/free-tier-catalog.test.ts +++ b/tests/unit/free-tier-catalog.test.ts @@ -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); }); diff --git a/tests/unit/inclusionai-provider.test.ts b/tests/unit/inclusionai-provider.test.ts new file mode 100644 index 0000000000..dff2608f65 --- /dev/null +++ b/tests/unit/inclusionai-provider.test.ts @@ -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 + ); +}); diff --git a/tests/unit/provider-models-discovery-split.test.ts b/tests/unit/provider-models-discovery-split.test.ts index 26da4cfe14..e6bcd11ed0 100644 --- a/tests/unit/provider-models-discovery-split.test.ts +++ b/tests/unit/provider-models-discovery-split.test.ts @@ -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", () => { diff --git a/tests/unit/provider-models-route.test.ts b/tests/unit/provider-models-route.test.ts index ed8e5d5e8d..ef507dc214 100644 --- a/tests/unit/provider-models-route.test.ts +++ b/tests/unit/provider-models-route.test.ts @@ -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).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 }); diff --git a/tests/unit/providers-constants-split.test.ts b/tests/unit/providers-constants-split.test.ts index fcd6a272a9..419cddaa78 100644 --- a/tests/unit/providers-constants-split.test.ts +++ b/tests/unit/providers-constants-split.test.ts @@ -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/.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).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", () => { diff --git a/tests/unit/providers-page-utils.test.ts b/tests/unit/providers-page-utils.test.ts index 17f30b9038..33f4718ffe 100644 --- a/tests/unit/providers-page-utils.test.ts +++ b/tests/unit/providers-page-utils.test.ts @@ -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 diff --git a/tests/unit/providers-route-managed-catalog.test.ts b/tests/unit/providers-route-managed-catalog.test.ts index b56d12004f..ed4a4afd6b 100644 --- a/tests/unit/providers-route-managed-catalog.test.ts +++ b/tests/unit/providers-route-managed-catalog.test.ts @@ -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; diff --git a/tests/unit/synthetic-provider.test.ts b/tests/unit/synthetic-provider.test.ts new file mode 100644 index 0000000000..2a50b0c92f --- /dev/null +++ b/tests/unit/synthetic-provider.test.ts @@ -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, + }); +}); From 606d1cbbd36cdbd79510d12fb6f8a9e54c401e7d Mon Sep 17 00:00:00 2001 From: Septianata Rizky Pratama <19322988+ianriizky@users.noreply.github.com> Date: Fri, 10 Jul 2026 02:44:39 +0700 Subject: [PATCH 05/15] fix: move tier-flow SVG images to public directory (#6538) Co-authored-by: Diego Rodrigues de Sa e Souza --- {images => public/images}/tier-flow-dark.svg | 0 {images => public/images}/tier-flow-light.svg | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename {images => public/images}/tier-flow-dark.svg (100%) rename {images => public/images}/tier-flow-light.svg (100%) diff --git a/images/tier-flow-dark.svg b/public/images/tier-flow-dark.svg similarity index 100% rename from images/tier-flow-dark.svg rename to public/images/tier-flow-dark.svg diff --git a/images/tier-flow-light.svg b/public/images/tier-flow-light.svg similarity index 100% rename from images/tier-flow-light.svg rename to public/images/tier-flow-light.svg From aba98fd227ebf032a3a394611fcd95945f58bd0a Mon Sep 17 00:00:00 2001 From: Hamsa_M <116961508+hamsa0x7@users.noreply.github.com> Date: Fri, 10 Jul 2026 01:14:50 +0530 Subject: [PATCH 06/15] fix(cli): per-agent DNS, startup guards, and batched Windows hosts writes (#6338) DNS toggle in AgentBridge was broken for 8 of 9 agents: addDNSEntry/ removeDNSEntry always resolved the legacy Antigravity default hosts regardless of which agent's dns_enabled flag was flipped. Both now accept an optional agentId and resolve hosts via ALL_TARGETS; the [id]/dns route passes id through and returns 404 for an unknown agent instead of silently falling back to the defaults. startMitmInternal() now wraps generateCert(), the provisionDnsEntries() call, and the PID-file write in try/catch so a mid-startup failure can't orphan the already-spawned MITM child process. On Windows, addDNSEntries/removeDNSEntries batch every missing/present entry into a single elevated PowerShell invocation instead of one UAC prompt per host line. Scope note: this PR originally bundled an unrelated SkillOpt feature (DB migration, 6 API routes, dashboard UI) and a checks-free CI build workflow alongside this DNS/startup fix. Both were dropped here as out-of-scope per review-group-prs analysis (2-implementing plan); only the DNS/startup-guard delta (dnsConfig.ts, manager.ts, the [id]/dns route, and their tests) is applied. Co-authored-by: hamsa0x7 Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --- CHANGELOG.md | 1 + .../agent-bridge/agents/[id]/dns/route.ts | 11 +- src/mitm/dns/dnsConfig.ts | 128 +++++++++--------- src/mitm/manager.ts | 21 ++- .../agent-bridge-dns-route-validation.test.ts | 60 ++++++++ tests/unit/dns-config-generic.test.ts | 65 ++++++++- 6 files changed, 214 insertions(+), 72 deletions(-) create mode 100644 tests/unit/agent-bridge-dns-route-validation.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 11bef1abed..5b67956b90 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ _Living section — bullets land here as PRs merge into `release/v3.8.47` (paral ### 🐛 Bug Fixes +- **fix(cli):** per-agent AgentBridge DNS toggle was broken for 8 of the 9 supported agents, and a failed MITM startup step could orphan the spawned proxy child — `addDNSEntry`/`removeDNSEntry` (`src/mitm/dns/dnsConfig.ts`) always resolved the legacy Antigravity default hosts regardless of which agent's toggle was flipped, so enabling DNS for Cursor/Codex/Claude Code/etc. silently added only `daily-cloudcode-pa.googleapis.com` while the DB recorded `dns_enabled=true` for the selected agent. Both functions now accept an optional `agentId` and resolve hosts via `ALL_TARGETS`; `POST /api/tools/agent-bridge/agents/[id]/dns` passes the route's `id` through and now returns 404 for an id that doesn't match a known target instead of silently falling back. Separately, `startMitmInternal()` (`src/mitm/manager.ts`) now wraps `generateCert()` (log + rethrow), the `provisionDnsEntries()` call, and the PID-file write in try/catch so a mid-startup failure can't orphan the already-spawned MITM child process. On Windows, `addDNSEntries`/`removeDNSEntries` also batch every missing/present entry into a single elevated PowerShell invocation instead of one UAC prompt per host line. Regression guard: `tests/unit/dns-config-generic.test.ts` (agent-specific resolution + batching), `tests/unit/agent-bridge-dns-route-validation.test.ts` (404 for unknown agent id). ([#6338](https://github.com/diegosouzapw/OmniRoute/pull/6338) — thanks @hamsa0x7) - **fix(auth):** an API key restricted via `allowedModels`/`allowedCombos` could bypass that restriction entirely over the Codex Responses-over-WebSocket bridge ([#6564](https://github.com/diegosouzapw/OmniRoute/issues/6564)) — `prepare()` in `src/app/api/internal/codex-responses-ws/route.ts` authenticated the WS bridge's API key (`authenticate()`/`authorizeWebSocketHandshake()`) and honored `allowedConnections`, but never called `enforceApiKeyPolicy()`, the same model/combo policy gate the HTTP `/v1/responses` path enforces via `handleChat()` — so a key scoped to e.g. `combo/model-1.0` could still reach a direct Codex model like `gpt-5.5` through this transport, as long as an eligible Codex OAuth connection existed. The bridge's WS auth token arrives via query params (`api_key`/`token`/`access_token`), not a normal `Authorization` header, so a new `enforceCodexWsApiKeyPolicy()` builds an equivalent `Request` carrying an explicit `Authorization: Bearer ` header and calls `enforceApiKeyPolicy()` against the CLIENT-requested model, before any Codex-specific model remapping or credential selection. Regression guard: `tests/unit/codex-ws-policy-enforcement-6564.test.ts` (a model-restricted key is rejected 403 before reaching credential selection; a combo-restricted key is rejected 403 requesting a disallowed combo; a key that DOES allow the requested model still proceeds past policy). - **fix(security):** loopback-gate `/api/middleware/*` so a leaked JWT over a tunnel can't install or trigger a middleware hook — middleware hooks compile + run arbitrary JS via `new vm.Script` on the request hot path (`src/lib/middleware/registry.ts`), the same RCE class as the already-gated `/api/plugins/*`; `/api/middleware/` is now in `LOCAL_ONLY_API_PREFIXES` so loopback enforcement runs unconditionally before any auth check (Hard Rules #15 + #17). Regression guard: `tests/unit/route-guard-middleware-local-only.test.ts`. ([#6541](https://github.com/diegosouzapw/OmniRoute/pull/6541)) — see PR. (thanks @developerjillur) - **fix(startup):** AgentBridge's MITM server no longer fails to start with `ROUTER_API_KEY is required` on a normal install ([#6403](https://github.com/diegosouzapw/OmniRoute/issues/6403)) — `POST /api/tools/agent-bridge/server` resolved the spawned MITM child's router key from only an explicit `apiKey` body field (never sent by the AgentBridge UI — the schema has no such field) and the `ROUTER_API_KEY` env var (unset by default), so `startMitm()` always received `""` and the child hard-exited, even though OmniRoute already had a usable API key in its own DB. A new `resolveRouterApiKey()` now falls back to `pickApiKeyForInternalUse()` (the same DB-backed selector the combo-health-check / cloud-sync internal probes use), resolving in order: explicit key → `ROUTER_API_KEY` env → an existing DB key. Regression guard: `tests/unit/agentbridge-mitm-router-key-6403.test.ts`. diff --git a/src/app/api/tools/agent-bridge/agents/[id]/dns/route.ts b/src/app/api/tools/agent-bridge/agents/[id]/dns/route.ts index 2aec5c7712..368f8bcded 100644 --- a/src/app/api/tools/agent-bridge/agents/[id]/dns/route.ts +++ b/src/app/api/tools/agent-bridge/agents/[id]/dns/route.ts @@ -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 t.id === id); + if (!target) { + return createErrorResponse({ status: 404, message: `Unknown agent: ${id}` }); + } + const { enabled } = parsed.data; const raw = body as Record; const sudoPassword = @@ -40,9 +47,9 @@ export async function POST(request: Request, { params }: Params): Promise 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 { 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 { 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 { - await addDNSEntries(ANTIGRAVITY_HOSTS, sudoPassword); +export async function addDNSEntry(sudoPassword: string, agentId?: string): Promise { + 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 { - await removeDNSEntries(ANTIGRAVITY_HOSTS, sudoPassword); +export async function removeDNSEntry(sudoPassword: string, agentId?: string): Promise { + await removeDNSEntries(resolveHostsForAgent(agentId), sudoPassword); } diff --git a/src/mitm/manager.ts b/src/mitm/manager.ts index 856c3c84fd..47765bf515 100644 --- a/src/mitm/manager.ts +++ b/src/mitm/manager.ts @@ -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 diff --git a/tests/unit/agent-bridge-dns-route-validation.test.ts b/tests/unit/agent-bridge-dns-route-validation.test.ts new file mode 100644 index 0000000000..5b0cff8712 --- /dev/null +++ b/tests/unit/agent-bridge-dns-route-validation.test.ts @@ -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"); +}); diff --git a/tests/unit/dns-config-generic.test.ts b/tests/unit/dns-config-generic.test.ts index e267af1337..2f77cd671d 100644 --- a/tests/unit/dns-config-generic.test.ts +++ b/tests/unit/dns-config-generic.test.ts @@ -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" ); }); From 1291e9bf263f8c8a9d8df7ac87104cecbe323a0d Mon Sep 17 00:00:00 2001 From: Paijo <14921983+oyi77@users.noreply.github.com> Date: Fri, 10 Jul 2026 02:45:01 +0700 Subject: [PATCH 07/15] fix(providers): web-cookie fallback validation reports unsupported instead of a false valid (#6309) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit validateWebCookieProvider() previously required a providerRegistry.ts entry and returned "Provider not found in registry" for web-cookie-only providers like lmarena, gemini-business, poe-web, venice-web and v0-vercel-web. A fallback to WEB_COOKIE_PROVIDERS[provider].website was proposed, but live verification showed probing `${website}/models` does not reliably signal session validity for these (redirects/SPA 200s regardless of cookie validity) — it would report an expired or garbage cookie as valid, which is worse than an honest "not supported". Until each provider has a verified, side-effect-free auth probe against its real API host, the fallback now returns `unsupported: true` with no network call. Also reverts the probe transport from validationRead back to directHttpsRequest, which fixes a globalThis.fetch mock/patch-timing mismatch that made the pre-existing tests/unit/provider-validation-web-cookie-auth007.test.ts hit the live network in CI, and adds the missing Cookie header to the probe request. Regression guard: tests/unit/web-cookie-validation-fallback.test.ts. Co-authored-by: oyi77 Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --- CHANGELOG.md | 1 + src/lib/providers/validation.ts | 33 +++++- .../web-cookie-validation-fallback.test.ts | 110 ++++++++++++++++++ 3 files changed, 139 insertions(+), 5 deletions(-) create mode 100644 tests/unit/web-cookie-validation-fallback.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b67956b90..e0203f2f9a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,6 +50,7 @@ _Living section — bullets land here as PRs merge into `release/v3.8.47` (paral - **fix(api):** editing any existing OpenAI Codex provider connection in the dashboard returned "Invalid request" and the edit could never be saved ([#6562](https://github.com/diegosouzapw/OmniRoute/issues/6562)) — `createProviderConnection()` (`src/lib/db/providers.ts`) auto-increments a new connection's `priority` to `MAX(priority)+1` per provider with no upper bound, and OAuth-imported connections (Codex `codex-auth/import` / `import-bulk`, up to 50 accounts per call, callable repeatedly — the standard Codex bulk-account-rotation workflow) never pass through `createProviderSchema`'s Zod validation at all, so nothing ever capped that value; `EditConnectionModal`'s `handleSubmit` always resends the connection's current `priority` unchanged on every save, and `updateProviderConnectionSchema` capped `priority`/`globalPriority` at `max(100)` — a UI-only ceiling the create path never enforced — so the first edit of any connection whose priority had already grown past 100 (routine once a Codex account count exceeds 100) failed validation regardless of which field the user actually changed. Raised the ceiling to `max(100_000)` on both fields — still bounded (a genuinely out-of-range value is still rejected), just wide enough to accept priorities the app itself already produces. Regression guard: `tests/unit/codex-connection-edit-6562.test.ts` (a Codex OAuth connection whose priority already exceeds the old 100 cap now validates + persists on edit; a control payload with a still-genuinely-invalid priority is still rejected with "Invalid request"). - **mimocode**: rotate accounts on MiMoCode's rate-limit-style 400s (body-classified) instead of failing on the first account; malformed 400s still fail fast with the real upstream error (#6648 — thanks @pizzav-xyz) - **fix(cli):** compression CLI REST fallback now reads/writes the canonical `defaultMode` field (surfaced as `strategy`) instead of a nonexistent `engine` key, and table output renders nested objects as JSON instead of `[object Object]` (#6571 — thanks @charleszolot) +- **fix(providers):** web-cookie providers without a `providerRegistry.ts` entry (`lmarena`, `gemini-business`, `poe-web`, `venice-web`, `v0-vercel-web`) now report `unsupported: true` instead of silently "OK" ([#6309](https://github.com/diegosouzapw/OmniRoute/pull/6309)) — `validateWebCookieProvider()` (`src/lib/providers/validation.ts`) previously required a registry entry and returned "Provider not found in registry" for these; a fallback to `WEB_COOKIE_PROVIDERS[provider].website` was proposed, but live verification showed the `${website}/models` probe does not reliably signal session validity for these providers (redirects/SPA 200s regardless of cookie validity — e.g. lmarena's real API is `arena.ai`, not `lmarena.ai`; Poe's real endpoint is a GraphQL POST, not a REST `/models`), so it would report an expired or garbage cookie as valid. Until each provider has a verified, side-effect-free auth probe against its real API host, the fallback now returns `unsupported` (no network call) instead of a false positive. Regression guard: `tests/unit/web-cookie-validation-fallback.test.ts`. (thanks @oyi77) ### 📝 Maintenance diff --git a/src/lib/providers/validation.ts b/src/lib/providers/validation.ts index 867625ccb6..70a1dda4a5 100644 --- a/src/lib/providers/validation.ts +++ b/src/lib/providers/validation.ts @@ -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; +}) { 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); } } diff --git a/tests/unit/web-cookie-validation-fallback.test.ts b/tests/unit/web-cookie-validation-fallback.test.ts new file mode 100644 index 0000000000..6e1a1d75f7 --- /dev/null +++ b/tests/unit/web-cookie-validation-fallback.test.ts @@ -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 }> = []; + +test.beforeEach(() => { + fetchCalls.length = 0; + globalThis.fetch = (async (url: string | URL, init?: RequestInit) => { + const headers: Record = {}; + 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); +}); From 6e16ae7b32a6c632f61948e27c0f52cc1225be94 Mon Sep 17 00:00:00 2001 From: Chirag Singhal <76880977+chirag127@users.noreply.github.com> Date: Fri, 10 Jul 2026 01:15:11 +0530 Subject: [PATCH 08/15] docs(claude): fix p2c casing to match ROUTING_STRATEGY_VALUES (#6643) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(electron): bump electron 42→43 + build better-sqlite3 from source (ABI 148) (#6605) fix(electron): bump electron 42→43 + rebuild better-sqlite3 from source against the Electron ABI (148). Electron 43 raises NODE_MODULE_VERSION to 148; better-sqlite3@12.11.1 has no electron-v148 prebuild, so the packaged app died with 'Nenhum driver SQLite disponível'. prepare-electron-standalone now compiles better-sqlite3 from source against the electron headers into build/Release (where 'bindings' resolves it). Validated by Electron Package Smoke (green) + local (node_register_module_v148). Supersedes #6378. (--admin: the only reds are SonarQube/SonarCloud failing on a coverage-report artifact digest-mismatch — a GitHub Actions infra flake, not this diff; Sonar is green on main and the diff touches only the electron build.) * deps: bump the development group across 1 directory with 6 updates (#6588) deps: bump the development group (6 updates). Rebased onto current main; all checks green after the electron-smoke fix (#6605). * fix(proxy): force CONNECT tunnel for HTTP proxied requests (undici 8.7) + production deps bump (#6620) fix(proxy): force CONNECT tunnel for HTTP proxied requests (undici 8.7) + production deps bump. undici 8.6+ changed ProxyAgent to forward plain-HTTP via request-proxy instead of CONNECT, breaking OAuth refresh through a connection proxy (501). proxyDispatcher now passes proxyTunnel:true. Validated: Unit Tests 3/8 (the OAuth-proxy test) green, new regression test green (fails without the fix on undici 8.7), SonarQube green. Supersedes #6380. (--admin: the only red is Electron Package Smoke failing on a next-build artifact 'digest-mismatch' — a GitHub Actions infra flake corrupting the asar ('file data stream has unexpected number of bytes'); the better-sqlite3 rebuild itself succeeded (gyp ok) and the electron path is unchanged from #6605 which passed the smoke. Not this diff.) * docs(claude): fix p2c casing to match ROUTING_STRATEGY_VALUES * chore(merge): drop unrelated main-drift from PR fork (deps/electron/proxy files belong to #6620, not this PR) Restores electron/package-lock.json, electron/package.json, package-lock.json, package.json, open-sse/utils/proxyDispatcher.ts, scripts/build/prepare-electron-standalone.mjs and tests/unit/proxy-dispatcher-family.test.ts to origin/release/v3.8.47's content. The PR fork branched from a state of main that already includes #6620 (proxy CONNECT tunnel fix + deps bump), which is not yet synced into release/v3.8.47 — the 3-way merge would otherwise silently carry that unrelated content into this doc-only PR. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Chirag Singhal Co-authored-by: Diego Rodrigues de Sa e Souza --- CLAUDE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index a8de5434a3..1635bfcf7b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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. --- From 484abed8c1dd3f463a9f011225f785e52bda2d82 Mon Sep 17 00:00:00 2001 From: Chirag Singhal <76880977+chirag127@users.noreply.github.com> Date: Fri, 10 Jul 2026 01:15:42 +0530 Subject: [PATCH 09/15] docs: sync routing-strategy count to 18 across README + AGENTS.md (#6644) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(electron): bump electron 42→43 + build better-sqlite3 from source (ABI 148) (#6605) fix(electron): bump electron 42→43 + rebuild better-sqlite3 from source against the Electron ABI (148). Electron 43 raises NODE_MODULE_VERSION to 148; better-sqlite3@12.11.1 has no electron-v148 prebuild, so the packaged app died with 'Nenhum driver SQLite disponível'. prepare-electron-standalone now compiles better-sqlite3 from source against the electron headers into build/Release (where 'bindings' resolves it). Validated by Electron Package Smoke (green) + local (node_register_module_v148). Supersedes #6378. (--admin: the only reds are SonarQube/SonarCloud failing on a coverage-report artifact digest-mismatch — a GitHub Actions infra flake, not this diff; Sonar is green on main and the diff touches only the electron build.) * deps: bump the development group across 1 directory with 6 updates (#6588) deps: bump the development group (6 updates). Rebased onto current main; all checks green after the electron-smoke fix (#6605). * fix(proxy): force CONNECT tunnel for HTTP proxied requests (undici 8.7) + production deps bump (#6620) fix(proxy): force CONNECT tunnel for HTTP proxied requests (undici 8.7) + production deps bump. undici 8.6+ changed ProxyAgent to forward plain-HTTP via request-proxy instead of CONNECT, breaking OAuth refresh through a connection proxy (501). proxyDispatcher now passes proxyTunnel:true. Validated: Unit Tests 3/8 (the OAuth-proxy test) green, new regression test green (fails without the fix on undici 8.7), SonarQube green. Supersedes #6380. (--admin: the only red is Electron Package Smoke failing on a next-build artifact 'digest-mismatch' — a GitHub Actions infra flake corrupting the asar ('file data stream has unexpected number of bytes'); the better-sqlite3 rebuild itself succeeded (gyp ok) and the electron path is unchanged from #6605 which passed the smoke. Not this diff.) * docs: sync routing-strategy count to 18 across README + AGENTS.md * chore(merge): drop unrelated main-drift from PR fork (deps/electron/proxy files belong to #6620, not this PR) Restores electron/package-lock.json, electron/package.json, package-lock.json, package.json, open-sse/utils/proxyDispatcher.ts, scripts/build/prepare-electron-standalone.mjs and tests/unit/proxy-dispatcher-family.test.ts to origin/release/v3.8.47's content. The PR fork branched from a state of main that already includes #6620 (proxy CONNECT tunnel fix + deps bump), which is not yet synced into release/v3.8.47 — the 3-way merge would otherwise silently carry that unrelated content into this doc-only PR. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Chirag Singhal Co-authored-by: Diego Rodrigues de Sa e Souza --- AGENTS.md | 2 +- README.md | 4 ++-- docs/i18n/zh-CN/README.md | 2 +- docs/i18n/zh-TW/README.md | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index c82bd8b669..cadfff6e33 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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) | diff --git a/README.md b/README.md index 6b50bf4c8b..a2447a82f6 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ [![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)
@@ -880,7 +880,7 @@ Compression: aggressive (~50%) → double your free quota · Cost: $0/mo
-**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). diff --git a/docs/i18n/zh-CN/README.md b/docs/i18n/zh-CN/README.md index 33d254a5e2..7b8cc1f556 100644 --- a/docs/i18n/zh-CN/README.md +++ b/docs/i18n/zh-CN/README.md @@ -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)
diff --git a/docs/i18n/zh-TW/README.md b/docs/i18n/zh-TW/README.md index 40137a2f05..3dfdaaada2 100644 --- a/docs/i18n/zh-TW/README.md +++ b/docs/i18n/zh-TW/README.md @@ -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)
From 2b33c6c635ebe141b6280beeee118d2e6e37df5b Mon Sep 17 00:00:00 2001 From: Chirag Singhal <76880977+chirag127@users.noreply.github.com> Date: Fri, 10 Jul 2026 01:16:11 +0530 Subject: [PATCH 10/15] docs(routing): reconcile 17 vs 18 public-strategy count in AUTO-COMBO (#6646) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(electron): bump electron 42→43 + build better-sqlite3 from source (ABI 148) (#6605) fix(electron): bump electron 42→43 + rebuild better-sqlite3 from source against the Electron ABI (148). Electron 43 raises NODE_MODULE_VERSION to 148; better-sqlite3@12.11.1 has no electron-v148 prebuild, so the packaged app died with 'Nenhum driver SQLite disponível'. prepare-electron-standalone now compiles better-sqlite3 from source against the electron headers into build/Release (where 'bindings' resolves it). Validated by Electron Package Smoke (green) + local (node_register_module_v148). Supersedes #6378. (--admin: the only reds are SonarQube/SonarCloud failing on a coverage-report artifact digest-mismatch — a GitHub Actions infra flake, not this diff; Sonar is green on main and the diff touches only the electron build.) * deps: bump the development group across 1 directory with 6 updates (#6588) deps: bump the development group (6 updates). Rebased onto current main; all checks green after the electron-smoke fix (#6605). * fix(proxy): force CONNECT tunnel for HTTP proxied requests (undici 8.7) + production deps bump (#6620) fix(proxy): force CONNECT tunnel for HTTP proxied requests (undici 8.7) + production deps bump. undici 8.6+ changed ProxyAgent to forward plain-HTTP via request-proxy instead of CONNECT, breaking OAuth refresh through a connection proxy (501). proxyDispatcher now passes proxyTunnel:true. Validated: Unit Tests 3/8 (the OAuth-proxy test) green, new regression test green (fails without the fix on undici 8.7), SonarQube green. Supersedes #6380. (--admin: the only red is Electron Package Smoke failing on a next-build artifact 'digest-mismatch' — a GitHub Actions infra flake corrupting the asar ('file data stream has unexpected number of bytes'); the better-sqlite3 rebuild itself succeeded (gyp ok) and the electron path is unchanged from #6605 which passed the smoke. Not this diff.) * docs(routing): reconcile 17 vs 18 public-strategy count in AUTO-COMBO * chore(merge): drop unrelated main-drift from PR fork (deps/electron/proxy files belong to #6620, not this PR) Restores electron/package-lock.json, electron/package.json, package-lock.json, package.json, open-sse/utils/proxyDispatcher.ts, scripts/build/prepare-electron-standalone.mjs and tests/unit/proxy-dispatcher-family.test.ts to origin/release/v3.8.47's content. The PR fork branched from a state of main that already includes #6620 (proxy CONNECT tunnel fix + deps bump), which is not yet synced into release/v3.8.47 — the 3-way merge would otherwise silently carry that unrelated content into this doc-only PR. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Chirag Singhal Co-authored-by: Diego Rodrigues de Sa e Souza --- docs/routing/AUTO-COMBO.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/routing/AUTO-COMBO.md b/docs/routing/AUTO-COMBO.md index 483730569a..42ccec94c2 100644 --- a/docs/routing/AUTO-COMBO.md +++ b/docs/routing/AUTO-COMBO.md @@ -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: From e697670046f00ae17a713de165a049c85e18f7aa Mon Sep 17 00:00:00 2001 From: enjoyer-hub Date: Thu, 9 Jul 2026 22:46:41 +0300 Subject: [PATCH 11/15] fix(cli): detect WinGet Claude Code on Windows (#6647) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(electron): bump electron 42→43 + build better-sqlite3 from source (ABI 148) (#6605) fix(electron): bump electron 42→43 + rebuild better-sqlite3 from source against the Electron ABI (148). Electron 43 raises NODE_MODULE_VERSION to 148; better-sqlite3@12.11.1 has no electron-v148 prebuild, so the packaged app died with 'Nenhum driver SQLite disponível'. prepare-electron-standalone now compiles better-sqlite3 from source against the electron headers into build/Release (where 'bindings' resolves it). Validated by Electron Package Smoke (green) + local (node_register_module_v148). Supersedes #6378. (--admin: the only reds are SonarQube/SonarCloud failing on a coverage-report artifact digest-mismatch — a GitHub Actions infra flake, not this diff; Sonar is green on main and the diff touches only the electron build.) * deps: bump the development group across 1 directory with 6 updates (#6588) deps: bump the development group (6 updates). Rebased onto current main; all checks green after the electron-smoke fix (#6605). * fix(proxy): force CONNECT tunnel for HTTP proxied requests (undici 8.7) + production deps bump (#6620) fix(proxy): force CONNECT tunnel for HTTP proxied requests (undici 8.7) + production deps bump. undici 8.6+ changed ProxyAgent to forward plain-HTTP via request-proxy instead of CONNECT, breaking OAuth refresh through a connection proxy (501). proxyDispatcher now passes proxyTunnel:true. Validated: Unit Tests 3/8 (the OAuth-proxy test) green, new regression test green (fails without the fix on undici 8.7), SonarQube green. Supersedes #6380. (--admin: the only red is Electron Package Smoke failing on a next-build artifact 'digest-mismatch' — a GitHub Actions infra flake corrupting the asar ('file data stream has unexpected number of bytes'); the better-sqlite3 rebuild itself succeeded (gyp ok) and the electron path is unchanged from #6605 which passed the smoke. Not this diff.) * fix(cli): detect WinGet Claude Code on Windows * chore(merge): drop unrelated main-drift from PR fork (deps/electron/proxy files belong to #6620, not this PR) Restores electron/package-lock.json, electron/package.json, package-lock.json, package.json, open-sse/utils/proxyDispatcher.ts, scripts/build/prepare-electron-standalone.mjs and tests/unit/proxy-dispatcher-family.test.ts to origin/release/v3.8.47's content. The PR fork branched from a state of main that already includes #6620 (proxy CONNECT tunnel fix + deps bump), which is not yet synced into release/v3.8.47 — the 3-way merge would otherwise silently carry that unrelated content into this doc-only PR. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * chore(quality): rebaseline cliRuntime.ts file-size freeze for #6647 (1100->1110) The file was already exactly at the frozen 1100-line cap on release/v3.8.47. PR #6647's WinGet Claude Code detection path adds 10 lines (irreducible — the 62-char package folder name forces Prettier's 100-char width to break the path.join call across the same multi-line form used by every other long path in this function), tripping the Fast Quality Gates check:file-size job. Bumping the frozen cap to the file's real new size per the documented allowlist-with-justification policy (this is a pass/fail policy gate, not the ratchet metrics system). Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: quanturbo Co-authored-by: Diego Rodrigues de Sa e Souza --- config/quality/file-size-baseline.json | 3 ++- src/shared/services/cliRuntime.ts | 10 ++++++++++ tests/unit/cli-runtime-detection.test.ts | 25 +++++++++++++++++++++++- 3 files changed, 36 insertions(+), 2 deletions(-) diff --git a/config/quality/file-size-baseline.json b/config/quality/file-size-baseline.json index 600568f937..ca1153119e 100644 --- a/config/quality/file-size-baseline.json +++ b/config/quality/file-size-baseline.json @@ -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 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, 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 { 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" + ) + ); } } diff --git a/tests/unit/cli-runtime-detection.test.ts b/tests/unit/cli-runtime-detection.test.ts index 4306ebdf34..5c46a14161 100644 --- a/tests/unit/cli-runtime-detection.test.ts +++ b/tests/unit/cli-runtime-detection.test.ts @@ -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); From abfced8b2806d71670be989e6ce1c70471f346dd Mon Sep 17 00:00:00 2001 From: KooshaPari <42529354+KooshaPari@users.noreply.github.com> Date: Thu, 9 Jul 2026 12:48:49 -0700 Subject: [PATCH 12/15] feat(sandbox): native Apple Container, WSL, OrbStack, Podman runtime support (#6611) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(sandbox): native Apple Container, WSL, OrbStack, Podman runtime support * fix(skills): align sandbox fallback kill container-name convention sandbox.ts's docker-fallback kill path (used only when cachedProvider is unexpectedly null) still targeted the pre-PR omniroute-sandbox-${id} container name, while containerProvider.ts's SANDBOX_NAME now produces omniroute-${id}. Align the fallback naming so it matches the provider convention, with a regression test covering kill()/killAll() before a provider has ever been resolved. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * fix(docs): document SKILLS_SANDBOX_RUNTIME and drop unrelated env leftovers Two fixes surfaced by CI's env/docs contract gate: - Add the SKILLS_SANDBOX_RUNTIME row to docs/reference/ENVIRONMENT.md so the new container-runtime override introduced by this PR is documented, matching .env.example. - Remove the Substrate/Bifrost/OTEL .env.example blocks that leaked in from this branch's stale main-based history during the release-branch sync merge — none of that belongs to this PR (native container runtimes for the skill sandbox) and none of it exists on release/v3.8.47 yet. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: Diego Rodrigues de Sa e Souza Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --- .env.example | 20 + docs/reference/ENVIRONMENT.md | 1 + src/lib/skills/containerProvider.ts | 479 +++++++++++++++++++++ src/lib/skills/sandbox.ts | 91 ++-- tests/unit/skills-builtins-sandbox.test.ts | 214 ++++++++- 5 files changed, 759 insertions(+), 46 deletions(-) create mode 100644 src/lib/skills/containerProvider.ts diff --git a/.env.example b/.env.example index 734402a092..31fe954c12 100644 --- a/.env.example +++ b/.env.example @@ -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 # ═══════════════════════════════════════════════════════════════════════════════ diff --git a/docs/reference/ENVIRONMENT.md b/docs/reference/ENVIRONMENT.md index 1a987faff9..3bc0371570 100644 --- a/docs/reference/ENVIRONMENT.md +++ b/docs/reference/ENVIRONMENT.md @@ -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. diff --git a/src/lib/skills/containerProvider.ts b/src/lib/skills/containerProvider.ts new file mode 100644 index 0000000000..129ac2f2ae --- /dev/null +++ b/src/lib/skills/containerProvider.ts @@ -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( + 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 | null = null; +const detectionCache = new Map(); + +function clearDetectionCache(): void { + detectionInFlight = null; + detectionCache.clear(); +} + +async function runDetection(): Promise { + // 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 { + 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), + }; +} \ No newline at end of file diff --git a/src/lib/skills/sandbox.ts b/src/lib/skills/sandbox.ts index 820d58c80c..556506b5f1 100644 --- a/src/lib/skills/sandbox.ts +++ b/src/lib/skills/sandbox.ts @@ -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 = new Map(); private config: SandboxConfig; + private cachedProvider: ContainerProvider | null = null; private constructor(config: Partial = {}) { 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 { + 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 }; \ No newline at end of file diff --git a/tests/unit/skills-builtins-sandbox.test.ts b/tests/unit/skills-builtins-sandbox.test.ts index 9f70ea219e..a696d3bf7a 100644 --- a/tests/unit/skills-builtins-sandbox.test.ts +++ b/tests/unit/skills-builtins-sandbox.test.ts @@ -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) +}); From edae0cf33ff723aa3a24cfb8461c72085477a5f0 Mon Sep 17 00:00:00 2001 From: Xiangzhe <32761048+xz-dev@users.noreply.github.com> Date: Fri, 10 Jul 2026 03:49:20 +0800 Subject: [PATCH 13/15] Expose per-combo reasoning token buffer toggle (#6702) * fix(combos): default reasoning token buffer off * feat(combos): expose reasoning token buffer toggle * fix(combos): keep reasoning-token buffer default enabled, opt-out toggle #6702 shipped bundled with #6536's own commit (identical SHA 37ac38f17f), flipping the combo-wide reasoningTokenBufferEnabled default from true to false. #6536 was subsequently closed by the author in favor of #6714, which explicitly keeps the existing default-enabled buffer behavior and instead clamps the buffer to the model's known output cap. Reconciled #6702 with that resolution: dropped the default-flip changes across comboConfig.ts, combo.ts, comboSetup.ts, ComboDefaultsTab.tsx, and the combo-defaults settings route (plus their test assertions), and inverted the new per-combo ReasoningTokenBufferToggle to opt-out semantics (`!== false`) so an existing combo's behavior is unchanged unless the operator explicitly unchecks it. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: Diego Rodrigues de Sa e Souza Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --- CHANGELOG.md | 1 + open-sse/services/combo.ts | 5 +- open-sse/services/combo/comboSetup.ts | 4 +- .../combos/ReasoningTokenBufferToggle.tsx | 52 +++++++++++++++++++ src/app/(dashboard)/dashboard/combos/page.tsx | 28 +++++----- .../settings/components/ComboDefaultsTab.tsx | 3 +- 6 files changed, 72 insertions(+), 21 deletions(-) create mode 100644 src/app/(dashboard)/dashboard/combos/ReasoningTokenBufferToggle.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index e0203f2f9a..f209f9bcad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ _Living section — bullets land here as PRs merge into `release/v3.8.47` (paral ### ✨ New Features - **Provider/model param filters**: config-driven parameter denylist/allowlist per provider/model with auto-learn from upstream 400s (#6649 — thanks @ThongAccount, closes #6625) +- **Per-combo reasoning token buffer toggle**: the combo builder now exposes an explicit checkbox for the `#3587` reasoning-model `max_tokens` buffer, defaulting to the existing enabled behavior, so a combo can opt out without hand-editing raw JSON config (#6702 — thanks @xz-dev) ### 🐛 Bug Fixes diff --git a/open-sse/services/combo.ts b/open-sse/services/combo.ts index 94a090c87c..f4608d83c4 100644 --- a/open-sse/services/combo.ts +++ b/open-sse/services/combo.ts @@ -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, diff --git a/open-sse/services/combo/comboSetup.ts b/open-sse/services/combo/comboSetup.ts index 4b7e8b56e9..99197743ea 100644 --- a/open-sse/services/combo/comboSetup.ts +++ b/open-sse/services/combo/comboSetup.ts @@ -90,9 +90,7 @@ export function phaseComboSetup(ctx: ComboContext): ComboSetup { const universalHandoffConfig = resolveUniversalHandoffConfig( (combo.universal_handoff || combo.universalHandoff) as - | Record - | null - | undefined, + Record | null | undefined, relayOptions?.universalHandoffConfig as Record | null | undefined ); diff --git a/src/app/(dashboard)/dashboard/combos/ReasoningTokenBufferToggle.tsx b/src/app/(dashboard)/dashboard/combos/ReasoningTokenBufferToggle.tsx new file mode 100644 index 0000000000..643658346e --- /dev/null +++ b/src/app/(dashboard)/dashboard/combos/ReasoningTokenBufferToggle.tsx @@ -0,0 +1,52 @@ +import Tooltip from "@/shared/components/Tooltip"; + +type TranslationFn = { + (key: string): string; + has?: (key: string) => boolean; +}; + +type Props = { + config: Record; + setConfig: (config: Record) => 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 ( +
+ 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" + /> + + + + help + + +
+ ); +} diff --git a/src/app/(dashboard)/dashboard/combos/page.tsx b/src/app/(dashboard)/dashboard/combos/page.tsx index c5c068000c..fefc15aeda 100644 --- a/src/app/(dashboard)/dashboard/combos/page.tsx +++ b/src/app/(dashboard)/dashboard/combos/page.tsx @@ -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 (
- {/* Header */}

{t("title")}

@@ -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 />
- {/* failoverBeforeRetry + maxSetRetries + setRetryDelayMs */}
@@ -3776,6 +3766,9 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders, combo
+
+ +
- 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
)} - {/* Actions */} {isExpertMode ? (
🚫 Never hit limits
Auto-fallback across 237 providers in milliseconds. Quota out? Next provider takes over — zero downtime.
🚫 Never hit limits
Auto-fallback across 248 providers in milliseconds. Quota out? Next provider takes over — zero downtime.
💸 Save up to 95% tokens
RTK + Caveman stacked compression cuts 15–95% of eligible tokens (~89% avg on tool-heavy sessions).
🆓 $0 to start
90+ providers with a free tier, 11 free forever (Kiro, Qoder, Pollinations, LongCat…). No card needed.