Compare commits

...

3 Commits

Author SHA1 Message Date
diegosouzapw
bc27bbb0fd Merge remote-tracking branch 'origin/release/v3.8.51' into HEAD
# Conflicts:
#	tests/unit/provider-node-reserved-prefix.test.ts
2026-09-17 13:20:02 -03:00
diegosouzapw
f121836fd0 test(providers): document the agnes-cn/chipotle count coincidence (#13131)
provider-node-reserved-prefix.test.ts's REGISTRY id+alias walk was
already red on the base tip (414 vs. expected 412) from agnes-cn
(#13399, +id/+alias). Removing chipotle's REGISTRY id/alias in this
PR nets it back to 412, making the test pass again without a numeric
edit — record why in a comment so it doesn't read as an untracked
coincidence later.
2026-09-16 14:18:45 -03:00
diegosouzapw
bf0b618e94 fix(providers): remove the chipotle/pepper provider (#13131)
amelia.chipotle.com (the reverse-engineered Amelia chat-widget backend
chipotle/pepper-1 talked to) now returns 404 on every route, including
root, from its Azure Application Gateway — confirmed live 2026-09-15.
This regressed from a WS handshake timeout (#4037, June 2026) to a
fully decommissioned host, so the upstream protocol cannot be fixed.
Owner decided to retire the provider entirely (Option B), following
the phind/kluster quiet-removal precedent: no REMOVED_PROVIDERS.md
entry (reserved for operator takedowns), just a one-line note under
FREE_TIERS.md "Removed / no free tier".

Removed every surface: executor, registry entry, executors/index.ts
and providers/index.ts wiring, noauth provider catalog entry,
ProviderIcon generic-fallback set, the autoCombo exclusion-list
comment, the chipotle_error code from the sanitizer allowlist,
PROVIDER_REFERENCE.md (regenerated), and every doc/test reference.

Regression test: tests/unit/issue-13131-chipotle-provider-removed.test.ts
asserts the provider is fully gone from the executor registry, the
provider REGISTRY and the noauth catalog, and that the executor module
no longer resolves — not a live-network repro (flaky/third-party).

Several existing tests used "chipotle" only as a generic noAuth-provider
example (proxy scoping, error classification, onboarding, fallback
text) with no chipotle-specific behavior under test; those were
re-pointed at another still-existing noAuth provider
(cloudflare-playground / duckduckgo-web) rather than weakened.
2026-09-16 14:15:24 -03:00
40 changed files with 120 additions and 649 deletions

View File

@@ -7,7 +7,7 @@
# 🚀 OmniRoute — The Free AI Gateway
<img src="./docs/diagrams/readme-hero.svg" width="100%" alt="OmniRoute — Never stop coding. Every AI tool → 359 providers — 150+ free — through one endpoint. Claude Code, Codex, Cursor, Cline, Copilot & Antigravity into FREE Claude / GPT / Gemini with auto-fallback. RTK + Caveman stacked compression saves 1595% tokens (~89% avg) — never hit limits. 359 AI providers · 150+ free tiers · ~1.47B free tokens/mo · 19 routing strategies · $0 to start."/>
<img src="./docs/diagrams/readme-hero.svg" width="100%" alt="OmniRoute — Never stop coding. Every AI tool → 358 providers — 150+ free — through one endpoint. Claude Code, Codex, Cursor, Cline, Copilot & Antigravity into FREE Claude / GPT / Gemini with auto-fallback. RTK + Caveman stacked compression saves 1595% tokens (~89% avg) — never hit limits. 358 AI providers · 150+ free tiers · ~1.47B free tokens/mo · 19 routing strategies · $0 to start."/>
</div>
@@ -233,7 +233,7 @@ curl http://localhost:20128/v1/chat/completions \
</div>
<img src="./docs/diagrams/promise-pillars.svg" width="100%" alt="The Promise — One endpoint and 359 providers. Automatic fallback keeps routing while another healthy target is available. Six pillars: resilient fallback across 359 providers · up to 95% token savings on eligible workloads · $0 to start with 150+ free tiers and 53 recurring/keyless free-forever providers · 36 CLI/agent integrations through one config · OpenAI, Claude, Gemini and Responses API compatibility at /v1 · production controls including circuit breakers, TLS stealth, MCP 110 tools, A2A, memory, guardrails, evals and 39,000+ static test declarations across 5,100+ tracked test files."/>
<img src="./docs/diagrams/promise-pillars.svg" width="100%" alt="The Promise — One endpoint and 358 providers. Automatic fallback keeps routing while another healthy target is available. Six pillars: resilient fallback across 358 providers · up to 95% token savings on eligible workloads · $0 to start with 150+ free tiers and 53 recurring/keyless free-forever providers · 36 CLI/agent integrations through one config · OpenAI, Claude, Gemini and Responses API compatibility at /v1 · production controls including circuit breakers, TLS stealth, MCP 110 tools, A2A, memory, guardrails, evals and 39,000+ static test declarations across 5,100+ tracked test files."/>
<br/>
<br/>
@@ -486,7 +486,7 @@ All **19** strategies — mix & match per combo step:
</div>
<img src="./docs/diagrams/comparison-table.svg" width="100%" alt="What sets OmniRoute apart — a dated feature snapshot vs 9router, OpenRouter, CLIProxyAPI and LiteLLM across 13 capabilities. OmniRoute: 359 providers, 150+ free tiers built in, 19 routing strategies, 12-engine token compression, built-in MCP server with 110 tools, A2A agent protocol, persistent memory, guardrails, cloud agents, TLS fingerprint stealth, Desktop/Termux/PWA and 42 i18n UI locales. OmniRoute is MIT-licensed and self-hostable. Competitor capabilities and counts may change; see the linked methodology."/>
<img src="./docs/diagrams/comparison-table.svg" width="100%" alt="What sets OmniRoute apart — a dated feature snapshot vs 9router, OpenRouter, CLIProxyAPI and LiteLLM across 13 capabilities. OmniRoute: 358 providers, 150+ free tiers built in, 19 routing strategies, 12-engine token compression, built-in MCP server with 110 tools, A2A agent protocol, persistent memory, guardrails, cloud agents, TLS fingerprint stealth, Desktop/Termux/PWA and 42 i18n UI locales. OmniRoute is MIT-licensed and self-hostable. Competitor capabilities and counts may change; see the linked methodology."/>
<sub>📊 Full methodology &amp; per-feature detail vs 9router, OpenRouter, CLIProxyAPI &amp; LiteLLM → [`docs/comparison/OMNIROUTE_VS_ALTERNATIVES.md`](docs/comparison/OMNIROUTE_VS_ALTERNATIVES.md)</sub>

View File

@@ -0,0 +1 @@
- **fix(providers):** remove the `chipotle`/`pepper` provider — its upstream (`amelia.chipotle.com`) now 404s on every route and is fully decommissioned (#13131, #4037) — thanks @Falco20100

View File

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

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -1,4 +1,4 @@
<svg viewBox="0 0 1200 780" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Comparison table: OmniRoute versus 9router, OpenRouter, CLIProxyAPI and LiteLLM across 13 capabilities. OmniRoute is the only one with the full set: 359 providers, 150+ free providers built-in, 20 routing strategies, 12-engine token compression, a built-in MCP server with 110 tools, A2A protocol, persistent memory, guardrails, cloud agents, TLS fingerprint stealth, desktop/Termux/PWA, 42 UI locales and 100% MIT self-hosted. 9router has free providers, RTK compression and translation but no MCP, A2A, memory, guardrails, cloud agents or stealth. OpenRouter is a hosted SaaS with 400+ models, guardrails and a hosted MCP but is not self-hosted and lacks A2A, memory, cloud agents and stealth. CLIProxyAPI is a light OAuth proxy with two routing strategies. LiteLLM has 100+ providers, A2A and extensive guardrails but no memory, compression, free tier, stealth or cloud agents.">
<svg viewBox="0 0 1200 780" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Comparison table: OmniRoute versus 9router, OpenRouter, CLIProxyAPI and LiteLLM across 13 capabilities. OmniRoute is the only one with the full set: 358 providers, 150+ free providers built-in, 20 routing strategies, 12-engine token compression, a built-in MCP server with 110 tools, A2A protocol, persistent memory, guardrails, cloud agents, TLS fingerprint stealth, desktop/Termux/PWA, 42 UI locales and 100% MIT self-hosted. 9router has free providers, RTK compression and translation but no MCP, A2A, memory, guardrails, cloud agents or stealth. OpenRouter is a hosted SaaS with 400+ models, guardrails and a hosted MCP but is not self-hosted and lacks A2A, memory, cloud agents and stealth. CLIProxyAPI is a light OAuth proxy with two routing strategies. LiteLLM has 100+ providers, A2A and extensive guardrails but no memory, compression, free tier, stealth or cloud agents.">
<desc>Static-header comparison table where each capability row fades in top to bottom; the OmniRoute column is highlighted and shows a check or a leading value in every row, while competitors show a mix of checks, partials and crosses.</desc>
<defs>
<pattern id="gC" width="32" height="32" patternUnits="userSpaceOnUse"><path d="M 32 0 L 0 0 0 32" fill="none" stroke="#ffffff" stroke-opacity="0.05" stroke-width="1"/></pattern>

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -1,4 +1,4 @@
<svg viewBox="0 0 1200 540" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="The OmniRoute promise: one endpoint and 359 providers. Six pillars. Resilient fallback: automatic routing continues while another healthy target is available. Save up to 95 percent of eligible tokens: RTK plus Caveman stacked compression averages about 89 percent on tool-heavy sessions. Zero dollars to start: 150+ providers with a free tier and 53 recurring or keyless free-forever providers. Every tool works: 36 CLI and agent integration records, including Claude Code, Codex, Cursor, Cline, Copilot and Antigravity, through one config. One endpoint: OpenAI, Claude, Gemini and Responses API translation at /v1. Production controls: circuit breakers, TLS stealth, MCP with 110 tools, A2A, memory, guardrails, evals, and 39,000+ static test declarations across 5,100+ tracked test files.">
<svg viewBox="0 0 1200 540" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="The OmniRoute promise: one endpoint and 358 providers. Six pillars. Resilient fallback: automatic routing continues while another healthy target is available. Save up to 95 percent of eligible tokens: RTK plus Caveman stacked compression averages about 89 percent on tool-heavy sessions. Zero dollars to start: 150+ providers with a free tier and 53 recurring or keyless free-forever providers. Every tool works: 36 CLI and agent integration records, including Claude Code, Codex, Cursor, Cline, Copilot and Antigravity, through one config. One endpoint: OpenAI, Claude, Gemini and Responses API translation at /v1. Production controls: circuit breakers, TLS stealth, MCP with 110 tools, A2A, memory, guardrails, evals, and 39,000+ static test declarations across 5,100+ tracked test files.">
<desc>Animated promise card: six pillar tiles fade in in reading order, then a soft colored border highlight sweeps from tile to tile in a continuous cycle.</desc>
<defs>
<pattern id="gridPaperP" width="32" height="32" patternUnits="userSpaceOnUse">
@@ -21,7 +21,7 @@
<line x1="150" y1="53" x2="1160" y2="53" stroke="#232b38" stroke-width="1.5"/>
</g>
<g>
<text x="40" y="100" font-family="Inter, 'Segoe UI', Arial, Helvetica, system-ui, sans-serif" font-size="23" font-weight="600" fill="#c9d1d9">One endpoint. <tspan fill="#a78bfa" font-weight="800">359 providers.</tspan> Never stop building — OmniRoute picks <tspan fill="#7ee787" font-weight="700">the cheapest one that works</tspan>.</text>
<text x="40" y="100" font-family="Inter, 'Segoe UI', Arial, Helvetica, system-ui, sans-serif" font-size="23" font-weight="600" fill="#c9d1d9">One endpoint. <tspan fill="#a78bfa" font-weight="800">358 providers.</tspan> Never stop building — OmniRoute picks <tspan fill="#7ee787" font-weight="700">the cheapest one that works</tspan>.</text>
</g>
<g font-family="Inter, 'Segoe UI', Arial, Helvetica, system-ui, sans-serif">
@@ -38,7 +38,7 @@
<line x1="3.9" y1="3.9" x2="18.1" y2="18.1"/>
</g>
<text x="102" y="170" font-size="18" font-weight="800" fill="#74b9ff">Never hit limits</text>
<text x="66" y="204" font-size="13.5" fill="#a1a1aa">Auto-fallback across 359 providers in</text>
<text x="66" y="204" font-size="13.5" fill="#a1a1aa">Auto-fallback across 358 providers in</text>
<text x="66" y="226" font-size="13.5" fill="#a1a1aa">milliseconds. Quota out? The next provider</text>
<text x="66" y="248" font-size="13.5" fill="#a1a1aa">takes over while a healthy target remains.</text>
</g>

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

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

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 7.3 KiB

View File

@@ -1615,7 +1615,7 @@ Admin-only endpoints for operational management.
## CLI Tools Management
Manage CLI tools that integrate with OmniRoute (antigravity, chipotle, commandCode,
Manage CLI tools that integrate with OmniRoute (antigravity, commandCode,
devin-cli, etc.). See [Provider Reference](./PROVIDER_REFERENCE.md) for the full list.
| Method | Path | Description |

View File

@@ -44,7 +44,7 @@ Biggest **documented** contributors: `mistral` 1.00B, `nara` 210M, `llm7` 150M,
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), `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), `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), `featherless-ai` (no current free tier), `chipotle` (Chipotle's "Pepper AI" — the reverse-engineered `amelia.chipotle.com` Amelia chat widget the provider talked to now 404s on every route, including root; decommissioned/moved backend, confirmed 2026-09-15 — fully removed from the catalog, #13131/#4037). `iflytek` / `sparkdesk` stay listed but carry a ToS-caution note (Spark Lite is free; the ToS restricts proxy/relay use).
- **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 → $25 signup credit also removed (requires $5 minimum purchase, no free trial), `longcat` Preview ended + Flash models retired → **LongCat-2.0** only, reclassified as a one-time **10M**-token signup credit (KYC-gated, not recurring).
- **New free providers discovered:** ⭐ **Kilo Code** (`kilo-gateway` — rotating "Auto Free" set: NVIDIA Nemotron 3 family, StepFun, Poolside, Nex-N2-Pro), ⭐ **OpenCode Zen** (`opencode-zen` — 6 rotating free coding models), ⭐ **Z.AI / Zhipu** (`glm-cn` — GLM-4-Flash / 4.5-Flash / 4.7-Flash permanently free + 20M signup bonus), and `arcee-ai` Trinity Large Preview.

View File

@@ -10,7 +10,7 @@ lastUpdated: 2026-09-16
> Regenerate with: `npm run gen:provider-reference`
> **Last generated:** 2026-09-16
Total providers: **359**. See category breakdown below.
Total providers: **358**. See category breakdown below.
## Categories
@@ -34,13 +34,12 @@ Use the dashboard at `/dashboard/providers` to enable, configure, and test each
---
## No-auth Providers (no key required) (11)
## No-auth Providers (no key required) (10)
| ID | Alias | Name | Tags | Website | Notes | Tool calling |
|----|-------|------|------|---------|-------|--------------|
| `aihorde` | `horde` | AI Horde | No-auth | [link](https://aihorde.net) | No API key required — uses AI Horde's documented anonymous key. Adding a free aihorde.net key is optional and only buys higher queue priority (kudos). | — |
| `auggie` | `aug` | Augment (Auggie CLI) | No-auth | [link](https://augmentcode.com) | No API key stored by OmniRoute. Install the Auggie CLI and run `auggie login` on this machine, then OmniRoute spawns it locally for each request. | — |
| `chipotle` | `pepper` | Chipotle Pepper AI (Free) | No-auth | [link](https://amelia.chipotle.com) | No credentials required. Uses Chipotle's public support chatbot via reverse-engineered SockJS/STOMP protocol. | — |
| `cloudflare-playground` | `cfp` | Cloudflare AI Playground | No-auth | [link](https://playground.ai.cloudflare.com) | No credentials required — anonymous browser sessions over a reverse-engineered cf_agent WebSocket protocol (Playwright transport). | — |
| `codex-app-server` | `cxa` | OpenAI Codex (App-Server) | No-auth | [link](https://developers.openai.com/codex/cli) | No token stored by OmniRoute. The Codex CLI app-server manages its own ChatGPT sign-in (~/.codex/auth.json, auto-refreshed). Use “Sign in with ChatGPT” if the CLI is not yet authenticated. | — |
| `devin-cli-agentic` | `dva` | Devin CLI Agentic Bridge | No-auth | [link](https://docs.devin.ai/work-with-devin/devin-cli) | Authentication is owned by the official Devin CLI in its isolated bridge volume. | emulated |

View File

@@ -89,7 +89,6 @@ import { antigravityProvider } from "./registry/antigravity/index.ts";
import { openaiProvider } from "./registry/openai/index.ts";
import { snowflakeProvider } from "./registry/snowflake/index.ts";
import { huggingfaceProvider } from "./registry/huggingface/index.ts";
import { chipotleProvider } from "./registry/chipotle/index.ts";
import { freeaiapikeyProvider } from "./registry/freeaiapikey/index.ts";
import { qwen_cloudProvider } from "./registry/qwen-cloud/index.ts";
import { qwen_cloud_token_planProvider } from "./registry/qwen-cloud-token-plan/index.ts";
@@ -363,7 +362,6 @@ export const REGISTRY: Record<string, RegistryEntry> = {
openai: openaiProvider,
snowflake: snowflakeProvider,
huggingface: huggingfaceProvider,
chipotle: chipotleProvider,
freeaiapikey: freeaiapikeyProvider,
"qwen-cloud": qwen_cloudProvider,
"qwen-cloud-token-plan": qwen_cloud_token_planProvider,

View File

@@ -1,14 +0,0 @@
import type { RegistryEntry } from "../../shared.ts";
export const chipotleProvider: RegistryEntry = {
id: "chipotle",
alias: "pepper",
format: "openai",
executor: "chipotle",
baseUrl: "https://amelia.chipotle.com",
baseUrls: ["https://amelia.chipotle.com"],
authType: "none",
authHeader: "none",
models: [{ id: "pepper-1", name: "Pepper (Chipotle AI 🌯)" }],
passthroughModels: true,
};

View File

@@ -1,432 +0,0 @@
import { randomInt, randomUUID } from "node:crypto";
import { BaseExecutor, type ExecuteInput } from "./base.ts";
import type { ProviderCredentials } from "./base.ts";
import { sanitizeErrorMessage } from "../utils/error.ts";
const BASE_URL = "https://amelia.chipotle.com";
const DOMAIN_CODE = "chipotle";
const DOMAIN_ID = "23700760-e1e5-4c3c-931d-8804e29a6775";
// Exported for unit testing — these run at WS-connect time, so a missing
// node:crypto import (crypto.randomInt is NOT on the Web Crypto global) would
// otherwise only surface as a runtime crash deep in _connect().
export function randomServerId(): string {
return String(randomInt(0, 1000)).padStart(3, "0");
}
export function randomSessionId(): string {
return randomUUID().replace(/-/g, "").slice(0, 32);
}
interface AmeliaSession {
csrfToken: string;
userId: string;
cookieHeader: string;
}
class AmeliaClient {
private session: AmeliaSession | null = null;
private ws: import("ws").WebSocket | null = null;
private stompConnected = false;
private messageCallbacks: Map<string, (msg: string) => void> = new Map();
private connectPromise: Promise<void> | null = null;
async init(): Promise<void> {
const res = await fetch(`${BASE_URL}/Amelia/api/init`, {
headers: {
"User-Agent":
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36",
Origin: BASE_URL,
Referer: `${BASE_URL}/Amelia/ui/chipotle/chat?embed=iframe`,
},
redirect: "manual",
});
if (!res.ok) throw new Error(`Amelia init failed: ${res.status}`);
const data = (await res.json()) as {
csrfToken: string;
user: { userId: string; anonymous: boolean };
};
const setCookies = res.headers.getSetCookie?.() ?? [];
const cookieHeader = setCookies.map((c) => c.split(";")[0]).join("; ");
this.session = {
csrfToken: data.csrfToken,
userId: data.user.userId,
cookieHeader,
};
}
async connect(): Promise<void> {
if (!this.session) throw new Error("Call init() first");
if (this.connectPromise !== null) return this.connectPromise;
this.connectPromise = this._connect();
try {
await this.connectPromise;
} finally {
this.connectPromise = null;
}
}
private async _connect(): Promise<void> {
const { WebSocket } = await import("ws");
const server = randomServerId();
const sessionId = randomSessionId();
const wsUrl = `wss://amelia.chipotle.com/Amelia/api/sock/${server}/${sessionId}/websocket`;
return new Promise<void>((resolve, reject) => {
const timeout = setTimeout(() => reject(new Error("WS connect timeout")), 15_000);
const ws = new WebSocket(wsUrl, {
headers: {
Cookie: this.session!.cookieHeader,
Origin: BASE_URL,
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36",
},
});
ws.on("open", () => {});
ws.on("message", (raw: Buffer | string) => {
const data = raw.toString();
this.handleSockJSFrame(data, resolve, reject, timeout);
});
ws.on("error", (err: Error) => {
clearTimeout(timeout);
reject(err);
});
ws.on("close", () => {
this.stompConnected = false;
this.ws = null;
});
this.ws = ws;
});
}
private handleSockJSFrame(
frame: string,
resolveConnect: () => void,
rejectConnect: (e: Error) => void,
timeout: NodeJS.Timeout
): void {
if (frame === "o") {
this.sendSockJS(this.buildStompConnect());
return;
}
if (frame === "h") return;
if (frame.startsWith("a")) {
try {
const arr = JSON.parse(frame.slice(1)) as string[];
for (const msg of arr) {
this.handleStompFrame(msg, resolveConnect, rejectConnect, timeout);
}
} catch {
// ignore
}
}
}
private handleStompFrame(
frame: string,
resolveConnect: () => void,
rejectConnect: (e: Error) => void,
timeout: NodeJS.Timeout
): void {
const command = frame.split("\n")[0].replace(/\r$/, "");
if (command === "CONNECTED") {
this.stompConnected = true;
this.sendSockJS(this.buildStompSubscribe(`/queue/session.${this.session!.userId}`, "sub-0"));
this.sendSockJS(this.buildStompSubscribe("/user/queue/session", "sub-1"));
clearTimeout(timeout);
resolveConnect();
return;
}
if (command === "MESSAGE") {
this.handleStompMessage(frame);
return;
}
if (command === "ERROR") {
clearTimeout(timeout);
rejectConnect(new Error(`STOMP ERROR: ${frame}`));
}
}
private handleStompMessage(frame: string): void {
const nullIdx = frame.indexOf("\0");
let bodyStart = frame.indexOf("\n\n");
if (bodyStart === -1) bodyStart = frame.indexOf("\r\n\r\n");
const headerLen = bodyStart !== -1 ? (frame[bodyStart + 2] === "\r" ? 4 : 2) : 0;
let body = "";
if (bodyStart !== -1) {
body = frame
.substring(bodyStart + headerLen, nullIdx !== -1 ? nullIdx : undefined)
.replace(/\0$/, "");
}
if (!body) return;
let text = body;
try {
const parsed = JSON.parse(body) as Record<string, unknown>;
if (parsed.type === "message" && parsed.body) {
const b = parsed.body as Record<string, unknown>;
text = (b.text as string) || JSON.stringify(b);
} else if (parsed.text) {
text = parsed.text as string;
} else if (parsed.message) {
text = parsed.message as string;
} else {
return;
}
} catch {
// plain text
}
for (const [id, cb] of this.messageCallbacks.entries()) {
cb(text);
this.messageCallbacks.delete(id);
}
}
async chat(message: string, timeoutMs = 15_000, signal?: AbortSignal | null): Promise<string> {
if (!this.stompConnected) {
await this.init();
await this.connect();
}
if (signal?.aborted) {
throw new DOMException("Aborted", "AbortError");
}
const callbackId = crypto.randomUUID();
return new Promise<string>((resolve, reject) => {
const timer = setTimeout(() => {
this.messageCallbacks.delete(callbackId);
reject(new Error("Response timeout"));
}, timeoutMs);
const onAbort = () => {
clearTimeout(timer);
this.messageCallbacks.delete(callbackId);
reject(new DOMException("Aborted", "AbortError"));
};
signal?.addEventListener("abort", onAbort, { once: true });
this.messageCallbacks.set(callbackId, (text) => {
clearTimeout(timer);
signal?.removeEventListener("abort", onAbort);
resolve(text);
});
const payload = JSON.stringify({
message,
domainCode: DOMAIN_CODE,
conversationId: null,
type: "text",
});
this.sendSockJS(this.buildStompSend("/app/send", payload));
});
}
private buildStompConnect(): string {
return `CONNECT\naccept-version:1.1,1.0\nheart-beat:0,0\nX-CSRF-TOKEN:${this.session!.csrfToken}\n\n\0`;
}
private buildStompSubscribe(destination: string, id: string): string {
return `SUBSCRIBE\ndestination:${destination}\nid:${id}\n\n\0`;
}
private buildStompSend(destination: string, body: string): string {
return `SEND\ndestination:${destination}\ncontent-type:application/json\ncontent-length:${Buffer.byteLength(body)}\n\n${body}\0`;
}
private sendSockJS(stompFrame: string): void {
if (!this.ws || this.ws.readyState !== 1) {
throw new Error("WebSocket not open");
}
this.ws.send(JSON.stringify([stompFrame]));
}
async close(): Promise<void> {
if (this.ws) {
this.ws.close();
this.ws = null;
}
this.stompConnected = false;
this.session = null;
}
}
// ── Client pool ──────────────────────────────────────────────────────────
const POOL_MAX = 5;
const pool: AmeliaClient[] = [];
async function getClient(): Promise<AmeliaClient> {
if (pool.length > 0) return pool.pop()!;
const client = new AmeliaClient();
await client.init();
await client.connect();
return client;
}
function releaseClient(client: AmeliaClient): void {
if (pool.length < POOL_MAX) {
pool.push(client);
} else {
client.close().catch(() => {});
}
}
// ── Executor ─────────────────────────────────────────────────────────────
export class ChipotleExecutor extends BaseExecutor {
constructor() {
super("chipotle", { format: "openai" });
}
buildUrl(_model: string, _stream: boolean): string {
return `${BASE_URL}/Amelia/api/chat`;
}
buildHeaders(_credentials: ProviderCredentials): Record<string, string> {
return { "Content-Type": "application/json" };
}
transformRequest(model: string, body: unknown, _stream: boolean): unknown {
if (typeof body === "object" && body !== null) {
return { ...(body as Record<string, unknown>), model };
}
return body;
}
async execute(input: ExecuteInput): Promise<{
response: Response;
url: string;
headers: Record<string, string>;
transformedBody: unknown;
}> {
const { model, stream, body, signal, log } = input;
const encoder = new TextEncoder();
if (signal?.aborted) {
return {
response: new Response(
encoder.encode(
JSON.stringify({
error: {
message: "Request aborted",
type: "abort",
code: "ABORTED",
},
})
),
{ status: 499, headers: { "Content-Type": "application/json" } }
),
url: this.buildUrl(model, stream),
headers: this.buildHeaders(input.credentials),
transformedBody: body,
};
}
const messages =
(body as { messages?: Array<{ role: string; content: string }> })?.messages ?? [];
const lastUser = [...messages].reverse().find((m) => m.role === "user");
const prompt = lastUser?.content ?? "";
let client: AmeliaClient | null = null;
try {
client = await getClient();
log?.info?.("CHIPOTLE", `Sending to Pepper (model=${model})`);
const responseText = await client.chat(prompt, 15_000, signal);
releaseClient(client);
client = null;
const requestId = `chatcmpl-${Date.now()}`;
const created = Math.floor(Date.now() / 1000);
if (stream) {
const sse = [
`data: ${JSON.stringify({ id: requestId, object: "chat.completion.chunk", created, model, choices: [{ index: 0, delta: { role: "assistant", content: responseText }, finish_reason: null }] })}`,
`data: ${JSON.stringify({ id: requestId, object: "chat.completion.chunk", created, model, choices: [{ index: 0, delta: {}, finish_reason: "stop" }] })}`,
"data: [DONE]",
"",
].join("\n");
return {
response: new Response(encoder.encode(sse), {
status: 200,
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
},
}),
url: this.buildUrl(model, stream),
headers: this.buildHeaders(input.credentials),
transformedBody: body,
};
}
const json = JSON.stringify({
id: requestId,
object: "chat.completion",
created,
model,
choices: [
{
index: 0,
message: { role: "assistant", content: responseText },
finish_reason: "stop",
},
],
usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },
});
return {
response: new Response(encoder.encode(json), {
status: 200,
headers: { "Content-Type": "application/json" },
}),
url: this.buildUrl(model, stream),
headers: this.buildHeaders(input.credentials),
transformedBody: body,
};
} catch (err) {
if (client) client.close().catch(() => {});
const msg = err instanceof Error ? err.message : String(err);
log?.error?.("CHIPOTLE", `Error: ${msg}`);
return {
response: new Response(
encoder.encode(
JSON.stringify({
error: {
message: sanitizeErrorMessage(msg),
type: "upstream_error",
code: "CHIPOTLE_ERROR",
},
})
),
{ status: 502, headers: { "Content-Type": "application/json" } }
),
url: this.buildUrl(model, stream),
headers: this.buildHeaders(input.credentials),
transformedBody: body,
};
}
}
}
export default ChipotleExecutor;

View File

@@ -159,8 +159,6 @@ const lazyExecutors: Record<string, () => Promise<BaseExecutor>> = {
db: () => import("./doubao-web.ts").then((m) => new m.DoubaoWebExecutor()), // Alias
"zai-web": () => import("./zai-web.ts").then((m) => new m.ZaiWebExecutor()),
zw: () => import("./zai-web.ts").then((m) => new m.ZaiWebExecutor()), // Alias
chipotle: () => import("./chipotle.ts").then((m) => new m.ChipotleExecutor()),
pepper: () => import("./chipotle.ts").then((m) => new m.ChipotleExecutor()), // Alias
lmarena: () => import("./lmarena.ts").then((m) => new m.LMArenaExecutor()),
lma: () => import("./lmarena.ts").then((m) => new m.LMArenaExecutor()), // Alias
"grok-cli": () => import("./grok-cli.ts").then((m) => new m.GrokCliExecutor()),

View File

@@ -339,7 +339,7 @@ const SYNTHETIC_NOAUTH_CONNECTION_ID = RESILIENCE_NOAUTH_CONNECTION_ID;
// candidate pool. Narrowed to the backends verified to answer without any
// configuration on our reference egress (VPS .15): `opencode` returns 200
// there, while duckduckgo-web (429/VQD rate limit),
// chipotle (502), aihorde (401, anon key rejected)
// aihorde (401, anon key rejected)
// and the others are unreliable. The excluded providers stay fully usable via
// direct `<alias>/<model>` calls — they are just kept OUT of auto-routing until
// re-verified. Re-add an id here to bring it back into every auto/* pool.

View File

@@ -75,7 +75,6 @@ const SAFE_PUBLIC_ERROR_IDENTIFIERS = new Set([
"chatgpt_subscription_unavailable",
"chatgpt_web_codex_error",
"chatgpt_web_codex_turn_failed",
"chipotle_error",
"claude_web_protocol_error",
"cli_not_found",
"client_cancelled",

View File

@@ -605,7 +605,7 @@ function resolveSilentCloseOutcome(input: {
// #10443: every known path that produces OpenAI chat chunks emits a
// terminal — the response translators (gemini/claude/kiro/cursor-to-openai)
// all emit a finish_reason chunk, the non-standard executors (kiro, cursor,
// nlpcloud, poe-web, copilot-m365-web, chipotle, gitlab)
// nlpcloud, poe-web, copilot-m365-web, gitlab)
// enqueue `data: [DONE]` themselves, and standard OpenAI-compatible
// upstreams end with finish_reason + [DONE] per spec. So a close that
// forwarded content but no terminal marker is an upstream drop, not a

View File

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

View File

@@ -1,6 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 420" font-family="system-ui, -apple-system, Segoe UI, Arial, Helvetica, sans-serif" role="img" aria-label="OmniRoute 4-tier fallback: your IDE or CLI calls one local endpoint and the OmniRoute Smart Router fails over across 359 providers in 4 tiers — Tier 1 Subscription, Tier 2 API, Tier 3 Cheap, Tier 4 Free.">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 420" font-family="system-ui, -apple-system, Segoe UI, Arial, Helvetica, sans-serif" role="img" aria-label="OmniRoute 4-tier fallback: your IDE or CLI calls one local endpoint and the OmniRoute Smart Router fails over across 358 providers in 4 tiers — Tier 1 Subscription, Tier 2 API, Tier 3 Cheap, Tier 4 Free.">
<title>OmniRoute 4-tier fallback</title>
<desc>OmniRoute 4-tier fallback: your IDE or CLI calls one local endpoint and the OmniRoute Smart Router fails over across 359 providers in 4 tiers — Tier 1 Subscription, Tier 2 API, Tier 3 Cheap, Tier 4 Free.</desc>
<desc>OmniRoute 4-tier fallback: your IDE or CLI calls one local endpoint and the OmniRoute Smart Router fails over across 358 providers in 4 tiers — Tier 1 Subscription, Tier 2 API, Tier 3 Cheap, Tier 4 Free.</desc>
<defs>
<marker id="arrow-dark" markerWidth="10" markerHeight="10" refX="9" refY="3" orient="auto">
<path d="M0,0 L0,6 L9,3 z" fill="#9ca3af"/>
@@ -15,7 +15,7 @@
<!-- Title -->
<text x="400" y="32" text-anchor="middle" font-size="18" font-weight="700" fill="#f9fafb">OmniRoute 4-tier fallback</text>
<text x="400" y="52" text-anchor="middle" font-size="12" fill="#6b7280">Never stop building — automatic zero-config failover across 359 providers</text>
<text x="400" y="52" text-anchor="middle" font-size="12" fill="#6b7280">Never stop building — automatic zero-config failover across 358 providers</text>
<!-- Client box -->
<rect x="275" y="70" width="250" height="52" rx="8" fill="#1f2937" stroke="#374151" stroke-width="1.5"/>

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

View File

@@ -1,6 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 420" font-family="system-ui, -apple-system, Segoe UI, Arial, Helvetica, sans-serif" role="img" aria-label="OmniRoute 4-tier fallback: your IDE or CLI calls one local endpoint and the OmniRoute Smart Router fails over across 359 providers in 4 tiers — Tier 1 Subscription, Tier 2 API, Tier 3 Cheap, Tier 4 Free.">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 420" font-family="system-ui, -apple-system, Segoe UI, Arial, Helvetica, sans-serif" role="img" aria-label="OmniRoute 4-tier fallback: your IDE or CLI calls one local endpoint and the OmniRoute Smart Router fails over across 358 providers in 4 tiers — Tier 1 Subscription, Tier 2 API, Tier 3 Cheap, Tier 4 Free.">
<title>OmniRoute 4-tier fallback</title>
<desc>OmniRoute 4-tier fallback: your IDE or CLI calls one local endpoint and the OmniRoute Smart Router fails over across 359 providers in 4 tiers — Tier 1 Subscription, Tier 2 API, Tier 3 Cheap, Tier 4 Free.</desc>
<desc>OmniRoute 4-tier fallback: your IDE or CLI calls one local endpoint and the OmniRoute Smart Router fails over across 358 providers in 4 tiers — Tier 1 Subscription, Tier 2 API, Tier 3 Cheap, Tier 4 Free.</desc>
<defs>
<marker id="arrow-light" markerWidth="10" markerHeight="10" refX="9" refY="3" orient="auto">
<path d="M0,0 L0,6 L9,3 z" fill="#6b7280"/>
@@ -15,7 +15,7 @@
<!-- Title -->
<text x="400" y="32" text-anchor="middle" font-size="18" font-weight="700" fill="#111827">OmniRoute 4-tier fallback</text>
<text x="400" y="52" text-anchor="middle" font-size="12" fill="#6b7280">Never stop building — automatic zero-config failover across 359 providers</text>
<text x="400" y="52" text-anchor="middle" font-size="12" fill="#6b7280">Never stop building — automatic zero-config failover across 358 providers</text>
<!-- Client box -->
<rect x="275" y="70" width="250" height="52" rx="8" fill="#ffffff" stroke="#d1d5db" stroke-width="1.5"/>

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

View File

@@ -222,7 +222,6 @@ const GENERIC_PROVIDER_IDS = new Set([
"byteplus",
"cartesia",
"cheaperinference",
"chipotle",
"clarifai",
"command-code",
"digitalocean",

View File

@@ -74,22 +74,6 @@ export const NOAUTH_PROVIDERS = {
text: "Cloudflare AI Playground uses a reverse-engineered anonymous WebSocket protocol (no official API). Requires Playwright with a Chromium browser on first request. Rate limits apply per IP (error 3021).",
},
},
chipotle: {
id: "chipotle",
alias: "pepper",
name: "Chipotle Pepper AI (Free)",
icon: "restaurant",
color: "#C41230",
textIcon: "🌯",
website: "https://amelia.chipotle.com",
noAuth: true,
hasFree: true,
serviceKinds: ["llm"],
freeNote:
"Free — Chipotle's Pepper AI (IPsoft Amelia). Anonymous sessions, no API key. Rate-limited.",
authHint:
"No credentials required. Uses Chipotle's public support chatbot via reverse-engineered SockJS/STOMP protocol.",
},
"veoaifree-web": {
id: "veoaifree-web",
alias: "veo-free",

View File

@@ -44,7 +44,7 @@ function body(model: string) {
// connections each test seeds, which is what these assertions are actually
// about (LKGP pinning and variant pool resolution) — rather than weakening the
// assertions to accept whatever the open pool happens to pick.
const NO_AUTH_PROVIDER_IDS = ["opencode", "duckduckgo-web", "chipotle", "veoaifree-web", "auggie"];
const NO_AUTH_PROVIDER_IDS = ["opencode", "duckduckgo-web", "veoaifree-web", "auggie"];
test.beforeEach(async () => {
BaseExecutor.RETRY_CONFIG.delayMs = 0;

View File

@@ -105,11 +105,6 @@
"configSource": "cheaperinference",
"provider": "cheaperinference"
},
"chipotle": {
"className": "ChipotleExecutor",
"configSource": "<custom-config>",
"provider": "chipotle"
},
"cinf": {
"className": "CheaperInferenceExecutor",
"configSource": "cheaperinference",
@@ -490,11 +485,6 @@
"configSource": "opencode-zen",
"provider": "opencode-zen"
},
"pepper": {
"className": "ChipotleExecutor",
"configSource": "<custom-config>",
"provider": "chipotle"
},
"perplexity-web": {
"className": "PerplexityWebExecutor",
"configSource": "<custom-config>",
@@ -676,6 +666,6 @@
"provider": "zai-web"
}
},
"keyCount": 135,
"keyCount": 133,
"sharedInstances": []
}

View File

@@ -980,29 +980,6 @@
"stream": "https://chenzk.top/v1/chat/completions"
}
},
"chipotle": {
"format": "openai",
"headers": {
"apiKey": {
"Accept": "text/event-stream",
"Authorization": "Bearer <TOK>",
"Content-Type": "application/json"
},
"nonStream": {
"Authorization": "Bearer <TOK>",
"Content-Type": "application/json"
},
"oauth": {
"Accept": "text/event-stream",
"Authorization": "Bearer <TOK>",
"Content-Type": "application/json"
}
},
"url": {
"nonStream": "https://amelia.chipotle.com",
"stream": "https://amelia.chipotle.com"
}
},
"chutes": {
"format": "openai",
"headers": {

View File

@@ -18,14 +18,14 @@ test("#4976 400 with rate-limit text (MiMoCode) → fallback with RATE_LIMIT_EXC
"Detected high-frequency non-compliant requests from you.",
0,
null,
"chipotle"
"mimocode"
);
assert.equal(res.shouldFallback, true);
assert.equal(res.reason, RateLimitReason.RATE_LIMIT_EXCEEDED);
});
test("#4976 400 with Chinese rate-limit text → fallback with RATE_LIMIT_EXCEEDED", () => {
const res = checkFallbackError(400, "检测到您的请求频率过高,请稍后再试", 0, null, "chipotle");
const res = checkFallbackError(400, "检测到您的请求频率过高,请稍后再试", 0, null, "mimocode");
assert.equal(res.shouldFallback, true);
assert.equal(res.reason, RateLimitReason.RATE_LIMIT_EXCEEDED);
});

View File

@@ -5,7 +5,7 @@ import { BaseExecutor } from "../../open-sse/executors/base.ts";
/**
* Generic BaseExecutor consumer — no buildHeaders() override — representing
* every provider (xai, cliproxyapi, chipotle, mimocode, ninerouter,
* every provider (xai, cliproxyapi, mimocode, ninerouter,
* gitlab, ...) that relies on BaseExecutor.buildHeaders() as-is.
*
* Regression guard for #8467/#8493: resolveEffectiveKey() already rotates to

View File

@@ -1,77 +0,0 @@
import { describe, it } from "node:test";
import assert from "node:assert";
import {
ChipotleExecutor,
randomServerId,
randomSessionId,
} from "../../open-sse/executors/chipotle.ts";
const executor = new ChipotleExecutor();
describe("ChipotleExecutor", () => {
it("buildHeaders returns static headers", () => {
const headers = (executor as any).buildHeaders({});
assert.strictEqual(headers["Content-Type"], "application/json");
});
it("buildUrl returns Amelia endpoint", () => {
const url = executor.buildUrl("pepper-1", false);
const parsed = new URL(url);
assert.strictEqual(parsed.hostname, "amelia.chipotle.com");
});
// Regression guard for the node:crypto import — randomInt is NOT on the Web
// Crypto global, so a bare `crypto.randomInt` would throw at WS-connect time.
it("randomServerId yields a 3-digit numeric string (crypto.randomInt available)", () => {
for (let i = 0; i < 50; i++) {
const id = randomServerId();
assert.match(id, /^\d{3}$/, `expected 3 digits, got "${id}"`);
assert.ok(Number(id) >= 0 && Number(id) <= 999);
}
});
it("randomSessionId yields 32 hex chars (crypto.randomUUID available)", () => {
const id = randomSessionId();
assert.match(id, /^[0-9a-f]{32}$/, `expected 32 hex chars, got "${id}"`);
assert.notStrictEqual(randomSessionId(), randomSessionId());
});
it("transformRequest passes model through", () => {
const result = (executor as any).transformRequest(
"pepper-1",
{ model: "pepper-1", messages: [{ role: "user", content: "hi" }] },
false,
);
assert.strictEqual(result.model, "pepper-1");
});
it("returns 499 on pre-aborted signal", async () => {
const controller = new AbortController();
controller.abort(new Error("cancelled"));
const result = await executor.execute({
model: "pepper-1",
body: { messages: [{ role: "user", content: "hi" }], stream: false },
stream: false,
signal: controller.signal,
credentials: {},
log: { debug: () => {}, info: () => {}, warn: () => {}, error: () => {} },
});
assert.strictEqual((result as any).response.status, 499);
});
it("is registered in executor index", async () => {
const { getExecutor } = await import("../../open-sse/executors/index.ts");
const exec = await getExecutor("chipotle");
assert.ok(exec, "chipotle executor should be registered");
assert.ok(exec instanceof ChipotleExecutor);
});
it("pepper alias works", async () => {
const { getExecutor } = await import("../../open-sse/executors/index.ts");
const exec = await getExecutor("pepper");
assert.ok(exec, "pepper alias should be registered");
assert.ok(exec instanceof ChipotleExecutor);
});
});

View File

@@ -11,7 +11,7 @@ import { classifyProviderError } from "../../open-sse/services/errorClassifier.t
test("#6345: no-credential provider 'Request blocked'/access_denied 403 -> recoverable (null), not FORBIDDEN", () => {
const body = { error: "Request blocked", type: "access_denied" };
assert.equal(classifyProviderError(403, body, "chipotle"), null);
assert.equal(classifyProviderError(403, body, "duckduckgo-web"), null);
});
test("control: apikey-provider bare 403 still recoverable (null) — no regression", () => {

View File

@@ -254,7 +254,6 @@ test("public identifier vocabulary preserves current internal machine-readable c
"BLACKBOX_RATE_LIMIT",
"abort",
"ABORTED",
"CHIPOTLE_ERROR",
"premium_model_requires_key",
"GROK_ERROR",
"TLS_CLIENT_UNAVAILABLE",

View File

@@ -17,7 +17,7 @@ test("free onboarding candidates come from the no-auth registry and exclude loca
assert.ok(ids.includes("opencode"));
assert.ok(ids.includes("duckduckgo-web"));
assert.ok(!ids.includes("felo-web"));
assert.ok(ids.includes("chipotle"));
assert.ok(ids.includes("cloudflare-playground"));
assert.ok(ids.includes("aihorde"));
assert.ok(!ids.includes("devin-cli-agentic"));
assert.ok(!ids.includes("auggie"));

View File

@@ -10,7 +10,7 @@ test("batch setup creates missing providers, skips existing ones, and is retry-s
const existing = [{ provider: "opencode", name: "My customized OpenCode" }];
const created: Array<{ provider: string; name: string }> = [];
const candidates = getEligibleFreeOnboardingProviders();
const requestedIds = ["opencode", "chipotle"];
const requestedIds = ["opencode", "cloudflare-playground"];
const first = await setupFreeProviderConnections({
requestedIds,
@@ -33,14 +33,20 @@ test("batch setup creates missing providers, skips existing ones, and is retry-s
assert.deepEqual(first.results, [
{ providerId: "opencode", status: "skipped", reason: "already-configured" },
{ providerId: "chipotle", status: "created", connectionId: "created-chipotle" },
{
providerId: "cloudflare-playground",
status: "created",
connectionId: "created-cloudflare-playground",
},
]);
assert.deepEqual(second.results, [
{ providerId: "opencode", status: "skipped", reason: "already-configured" },
{ providerId: "chipotle", status: "skipped", reason: "already-configured" },
{ providerId: "cloudflare-playground", status: "skipped", reason: "already-configured" },
]);
assert.deepEqual(existing, [{ provider: "opencode", name: "My customized OpenCode" }]);
assert.deepEqual(created, [{ provider: "chipotle", name: "Chipotle Pepper AI (Free)" }]);
assert.deepEqual(created, [
{ provider: "cloudflare-playground", name: "Cloudflare AI Playground" },
]);
});
test("batch setup rejects unknown or ineligible IDs before creating anything", async () => {
@@ -63,13 +69,14 @@ test("batch setup rejects unknown or ineligible IDs before creating anything", a
test("partial failures are reported per provider and can be retried", async () => {
const created = new Set<string>();
let chipotleAttempts = 0;
let cloudflarePlaygroundAttempts = 0;
const input = {
requestedIds: ["opencode", "chipotle"],
requestedIds: ["opencode", "cloudflare-playground"],
candidates: getEligibleFreeOnboardingProviders(),
listExisting: async () => [...created].map((provider) => ({ provider })),
create: async ({ provider }: { provider: string }) => {
if (provider === "chipotle" && chipotleAttempts++ === 0) throw new Error("upstream detail");
if (provider === "cloudflare-playground" && cloudflarePlaygroundAttempts++ === 0)
throw new Error("upstream detail");
created.add(provider);
return { id: `created-${provider}` };
},
@@ -80,10 +87,14 @@ test("partial failures are reported per provider and can be retried", async () =
assert.deepEqual(first.results, [
{ providerId: "opencode", status: "created", connectionId: "created-opencode" },
{ providerId: "chipotle", status: "failed", reason: "Failed to create provider" },
{ providerId: "cloudflare-playground", status: "failed", reason: "Failed to create provider" },
]);
assert.deepEqual(retry.results, [
{ providerId: "opencode", status: "skipped", reason: "already-configured" },
{ providerId: "chipotle", status: "created", connectionId: "created-chipotle" },
{
providerId: "cloudflare-playground",
status: "created",
connectionId: "created-cloudflare-playground",
},
]);
});

View File

@@ -0,0 +1,39 @@
// #13131 / #4037 — chipotle/pepper's upstream (amelia.chipotle.com) 404s on every route
// (verified live 2026-09-15, Azure Application Gateway with no backend route left). The
// upstream protocol cannot be "fixed" — the owner decided to retire the provider entirely
// (Option B), following the phind/kluster quiet-removal precedent. This is the permanent
// regression guard: it asserts the provider is fully GONE from every runtime surface, not a
// live-network repro (which would be flaky/slow/depend on a third party we don't control).
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { test } from "node:test";
import assert from "node:assert/strict";
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-13131-"));
process.env.DATA_DIR = TEST_DATA_DIR;
const { hasSpecializedExecutor } = await import("../../open-sse/executors/index.ts");
const { REGISTRY } = await import("../../open-sse/config/providers/index.ts");
const { NOAUTH_PROVIDERS } = await import("../../src/shared/constants/providers/noauth.ts");
test.after(() => {
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
});
test("issue #13131: chipotle executor is no longer registered", () => {
assert.equal(hasSpecializedExecutor("chipotle"), false);
assert.equal(hasSpecializedExecutor("pepper"), false);
});
test("issue #13131: chipotle is no longer in the provider registry", () => {
assert.equal(Object.prototype.hasOwnProperty.call(REGISTRY, "chipotle"), false);
});
test("issue #13131: chipotle is no longer in the noauth provider catalog", () => {
assert.equal(Object.prototype.hasOwnProperty.call(NOAUTH_PROVIDERS, "chipotle"), false);
});
test("issue #13131: the chipotle executor module no longer exists", async () => {
await assert.rejects(() => import("../../open-sse/executors/chipotle.ts"));
});

View File

@@ -4,7 +4,7 @@
* our reference egress. As of this change that allowlist is narrowed to
* `opencode`: on the reference VPS (.15) it answers 200 with zero configuration.
* The other no-auth providers
* (duckduckgo-web, chipotle, aihorde) stay OUT of every auto/* pool
* (duckduckgo-web, aihorde) stay OUT of every auto/* pool
* until re-verified — they remain usable via direct `<alias>/<model>` calls, they
* are just not auto-routed to.
*
@@ -47,7 +47,7 @@ test.after(async () => {
});
const ALLOWED_NOAUTH_PROVIDERS = ["opencode"];
const EXCLUDED_NOAUTH_PROVIDERS = ["duckduckgo-web", "chipotle", "aihorde"];
const EXCLUDED_NOAUTH_PROVIDERS = ["duckduckgo-web", "aihorde"];
test("fresh install: the allowlisted no-auth providers are present in the auto-combo pool", async () => {
const combo = await virtualFactory.createVirtualAutoCombo(undefined);

View File

@@ -4,7 +4,7 @@
//
// Root cause: the custom-models loop in catalog.ts gated every model through
// hasEligibleConnectionForModel(getConnectionsForProvider(...)). noAuth providers
// (e.g. chipotle / alias "pepper") have NO DB connection rows, so getConnectionsForProvider
// (e.g. duckduckgo-web / alias "ddgw") have NO DB connection rows, so getConnectionsForProvider
// returns [] and hasEligibleConnectionForModel([]) === false → the model was dropped.
// Built-in models survived because they go through providerSupportsModel(), which has a
// noAuth bypass (#2798). This test asserts an IMPORTED model on a noAuth provider appears.
@@ -42,12 +42,12 @@ test.after(async () => {
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
});
test("#3200 imported model on a noAuth provider (chipotle) appears in /api/v1/models", async () => {
// chipotle is a noAuth provider (alias "pepper") — it never creates a DB connection row.
// Import a model that is NOT a built-in chipotle model, so its presence is solely due
test("#3200 imported model on a noAuth provider (duckduckgo-web) appears in /api/v1/models", async () => {
// duckduckgo-web is a noAuth provider (alias "ddgw") — it never creates a DB connection row.
// Import a model that is NOT a built-in duckduckgo-web model, so its presence is solely due
// to the custom/imported path (the path the bug breaks).
await modelsDb.addCustomModel(
"chipotle",
"duckduckgo-web",
"my-imported-model-3200",
"My Imported Model",
"imported"
@@ -61,7 +61,7 @@ test("#3200 imported model on a noAuth provider (chipotle) appears in /api/v1/mo
assert.equal(response.status, 200);
assert.ok(
ids.has("pepper/my-imported-model-3200"),
ids.has("ddgw/my-imported-model-3200"),
"imported model on noAuth provider must appear under its alias prefix"
);
});
@@ -89,9 +89,9 @@ test("#3200 custom/imported models on auth providers still appear (no regression
});
test("#3200 imported models on noAuth providers are hidden when the provider is disabled", async () => {
await settingsDb.updateSettings({ blockedProviders: ["chipotle"] });
await settingsDb.updateSettings({ blockedProviders: ["duckduckgo-web"] });
await modelsDb.addCustomModel(
"chipotle",
"duckduckgo-web",
"my-imported-model-disabled",
"Hidden Imported Model",
"imported"
@@ -105,7 +105,7 @@ test("#3200 imported models on noAuth providers are hidden when the provider is
assert.equal(response.status, 200);
assert.equal(
ids.has("pepper/my-imported-model-disabled"),
ids.has("ddgw/my-imported-model-disabled"),
false,
"imported noAuth provider models must stay hidden while the provider is disabled"
);

View File

@@ -1,6 +1,6 @@
/**
* Tests for noAuth provider validation:
* - Bug 1: `chipotle` missing from providerAllowsOptionalApiKey
* - Bug 1: a noAuth provider missing from providerAllowsOptionalApiKey
* - `kimi` API key provider stays on the dedicated Moonshot executor
*/
import test from "node:test";
@@ -14,7 +14,7 @@ import {
import { hasSpecializedExecutor } from "../../open-sse/executors/index.ts";
// Bug 1: all noAuth providers should allow optional API key
for (const provider of ["chipotle", "opencode", "duckduckgo-web", "veoaifree-web"]) {
for (const provider of ["cloudflare-playground", "opencode", "duckduckgo-web", "veoaifree-web"]) {
test(`${provider} allows optional API key (noAuth provider)`, () => {
assert.equal(providerAllowsOptionalApiKey(provider), true);
});

View File

@@ -17,7 +17,6 @@ const LOCAL_SVG_IDS_WITHOUT_PROVENANCE = [
"byteplus",
"cartesia",
"cheaperinference",
"chipotle",
"clarifai",
"command-code",
"digitalocean",
@@ -178,9 +177,9 @@ const AUDITED_REFERENCE_FILES = [
...referenceRoots.flatMap((directory) => collectTextFiles(join(root, directory))),
];
test("provider bundle retires exactly the 79 unresolved assets and keeps the generic icon", () => {
assert.equal(retiredAssetNames.length, 79);
assert.equal(new Set(retiredAssetNames).size, 79);
test("provider bundle retires exactly the 78 unresolved assets and keeps the generic icon", () => {
assert.equal(retiredAssetNames.length, 78);
assert.equal(new Set(retiredAssetNames).size, 78);
for (const assetName of retiredAssetNames) {
assert.equal(

View File

@@ -183,9 +183,11 @@ test("shared set size includes live REGISTRY and retired Designer + Felo + Qwen
// #13024 (2b9e7fb3e) GreenPT and #13025 (22473dee5) EURouter each add one REGISTRY member (id ==
// alias); #13277 (02128f334) registers Arcee AI, adding id "arcee-ai" + alias "arcee" (408 → 412).
// #13399 (cdcde97c7) registers Agnes AI (China): id "agnes-cn" + alias "agnescn" — the only
// two provider-level members added since; everything else in that range is model ids
// (412 -> 414). Same entry that moved the apikey/regional count to 241 in #13905.
assert.equal(RESERVED_PREFIX_COUNT, 414);
// two provider-level members added since (412 -> 414); everything else in that range is
// model ids. Same entry that moved the apikey/regional count to 241 in #13905.
// #13131 then retires `chipotle`/`pepper` (dead upstream), removing its id "chipotle" and
// alias "pepper" from the REGISTRY walk (414 -> 412) — the two land back on the same total.
assert.equal(RESERVED_PREFIX_COUNT, 412);
});
test("isReservedProviderPrefix rejects non-string input", () => {

View File

@@ -59,17 +59,17 @@ test("resolveProxyForConnection keeps provider-level no-auth proxies isolated",
host: "127.0.0.2",
port: 8889,
});
await settingsDb.setProxyForLevel("provider", "chipotle", {
await settingsDb.setProxyForLevel("provider", "duckduckgo-web", {
type: "http",
host: "127.0.0.3",
port: 8890,
});
const opencode = await settingsDb.resolveProxyForConnection("noauth", undefined, "opencode");
const chipotle = await settingsDb.resolveProxyForConnection("noauth", undefined, "chipotle");
const ddgw = await settingsDb.resolveProxyForConnection("noauth", undefined, "duckduckgo-web");
assert.equal(opencode?.proxy?.host, "127.0.0.2");
assert.equal(chipotle?.proxy?.host, "127.0.0.3");
assert.equal(ddgw?.proxy?.host, "127.0.0.3");
});
test("safeResolveProxy keeps the synthetic no-auth connection provider-specific", async () => {
@@ -79,15 +79,15 @@ test("safeResolveProxy keeps the synthetic no-auth connection provider-specific"
host: "127.0.0.4",
port: 8891,
});
await settingsDb.setProxyForLevel("provider", "chipotle", {
await settingsDb.setProxyForLevel("provider", "duckduckgo-web", {
type: "http",
host: "127.0.0.5",
port: 8892,
});
const opencode = await safeResolveProxy("noauth", undefined, "opencode");
const chipotle = await safeResolveProxy("noauth", undefined, "chipotle");
const ddgw = await safeResolveProxy("noauth", undefined, "duckduckgo-web");
assert.equal(opencode?.proxy?.host, "127.0.0.4");
assert.equal(chipotle?.proxy?.host, "127.0.0.5");
assert.equal(ddgw?.proxy?.host, "127.0.0.5");
});

View File

@@ -36,7 +36,6 @@ const PROVIDER_IDS_WITHOUT_LOCAL_ASSET_PROVENANCE = [
"byteplus",
"cartesia",
"cheaperinference",
"chipotle",
"clarifai",
"command-code",
"digitalocean",
@@ -246,8 +245,8 @@ describe("ProviderIcon — local SVG dimensions", () => {
describe("ProviderIcon — unresolved local asset provenance", () => {
it("covers the complete provider and alias inventory", () => {
expect(PROVIDER_IDS_WITHOUT_LOCAL_ASSET_PROVENANCE).toHaveLength(79);
expect(new Set(PROVIDER_IDS_WITHOUT_LOCAL_ASSET_PROVENANCE)).toHaveLength(79);
expect(PROVIDER_IDS_WITHOUT_LOCAL_ASSET_PROVENANCE).toHaveLength(78);
expect(new Set(PROVIDER_IDS_WITHOUT_LOCAL_ASSET_PROVENANCE)).toHaveLength(78);
});
it.each(PROVIDER_IDS_WITHOUT_LOCAL_ASSET_PROVENANCE)(

View File

@@ -292,7 +292,7 @@ test("createVirtualAutoCombo restricts the no-auth pool to the allowlist", async
);
}
for (const excluded of ["duckduckgo-web", "chipotle", "aihorde"]) {
for (const excluded of ["duckduckgo-web", "aihorde"]) {
assert.equal(
combo.models.some((model) => model.providerId === excluded),
false,