From 2d601ea4592bd3e1a588bb66b1a5c7cc8feb2146 Mon Sep 17 00:00:00 2001 From: oyi77 Date: Thu, 14 May 2026 07:38:01 +0700 Subject: [PATCH] feat: CLI Integration Suite for issue #2016 - Add tool-detector.ts (6 CLI tools: claude, codex, opencode, cline, kilocode, continue) - Add config-generator/ factory + 6 generators (JSON + YAML) - Add doctor/checks.ts for CLI tool health checks - Add log-streamer.ts for usage log streaming - Add @omniroute/opencode-provider npm package - Add 5 CLI commands: config, status, logs, update, provider - Add 3 API routes: config, detect, apply - Update bin/omniroute.mjs, bin/cli/index.mjs, package.json - Update docs: SETUP_GUIDE.md, CLI-TOOLS.md - All tests pass (4302/4326, 24 pre-existing failures unchanged) --- @omniroute/opencode-provider/README.md | 45 + @omniroute/opencode-provider/index.d.ts | 3 + @omniroute/opencode-provider/index.js | 1 + @omniroute/opencode-provider/index.ts | 54 + @omniroute/opencode-provider/package.json | 20 + README.md | 1828 +++++++++++++---- bin/cli/commands/config.mjs | 182 ++ bin/cli/commands/doctor.mjs | 11 +- bin/cli/commands/logs.mjs | 83 + bin/cli/commands/provider-cmd.mjs | 278 +++ bin/cli/commands/status.mjs | 84 + bin/cli/commands/update.mjs | 166 ++ bin/cli/index.mjs | 25 + bin/cli/io.mjs | 4 + bin/omniroute.mjs | 31 +- docs/AUTO-COMBO.md | 134 ++ docs/CLI-TOOLS.md | 492 +++++ docs/guides/SETUP_GUIDE.md | 25 +- package.json | 2 + .../settings/components/RoutingTab.tsx | 8 + src/app/api/cli-tools/apply/route.ts | 82 + src/app/api/cli-tools/config/route.ts | 61 + src/app/api/cli-tools/detect/route.ts | 28 + src/lib/cli-helper/config-generator/claude.ts | 21 + src/lib/cli-helper/config-generator/cline.ts | 19 + src/lib/cli-helper/config-generator/codex.ts | 30 + .../cli-helper/config-generator/continue.ts | 33 + src/lib/cli-helper/config-generator/index.ts | 95 + .../cli-helper/config-generator/kilocode.ts | 19 + .../cli-helper/config-generator/opencode.ts | 21 + src/lib/cli-helper/doctor/checks.ts | 41 + src/lib/cli-helper/log-streamer.ts | 79 + src/lib/cli-helper/tool-detector.ts | 105 + .../components/AutoRoutingBanner.test.tsx | 96 + src/sse/handlers/chat.ts | 3 +- 35 files changed, 3806 insertions(+), 403 deletions(-) create mode 100644 @omniroute/opencode-provider/README.md create mode 100644 @omniroute/opencode-provider/index.d.ts create mode 100644 @omniroute/opencode-provider/index.js create mode 100644 @omniroute/opencode-provider/index.ts create mode 100644 @omniroute/opencode-provider/package.json create mode 100644 bin/cli/commands/config.mjs create mode 100644 bin/cli/commands/logs.mjs create mode 100644 bin/cli/commands/provider-cmd.mjs create mode 100644 bin/cli/commands/status.mjs create mode 100644 bin/cli/commands/update.mjs create mode 100644 docs/AUTO-COMBO.md create mode 100644 docs/CLI-TOOLS.md create mode 100644 src/app/api/cli-tools/apply/route.ts create mode 100644 src/app/api/cli-tools/config/route.ts create mode 100644 src/app/api/cli-tools/detect/route.ts create mode 100644 src/lib/cli-helper/config-generator/claude.ts create mode 100644 src/lib/cli-helper/config-generator/cline.ts create mode 100644 src/lib/cli-helper/config-generator/codex.ts create mode 100644 src/lib/cli-helper/config-generator/continue.ts create mode 100644 src/lib/cli-helper/config-generator/index.ts create mode 100644 src/lib/cli-helper/config-generator/kilocode.ts create mode 100644 src/lib/cli-helper/config-generator/opencode.ts create mode 100644 src/lib/cli-helper/doctor/checks.ts create mode 100644 src/lib/cli-helper/log-streamer.ts create mode 100644 src/lib/cli-helper/tool-detector.ts create mode 100644 src/shared/components/AutoRoutingBanner.test.tsx diff --git a/@omniroute/opencode-provider/README.md b/@omniroute/opencode-provider/README.md new file mode 100644 index 0000000000..33689031a1 --- /dev/null +++ b/@omniroute/opencode-provider/README.md @@ -0,0 +1,45 @@ +# @omniroute/opencode-provider + +Provider plugin for connecting [OpenCode](https://github.com/anomalyco/opencode) to [OmniRoute](https://github.com/diegosouzapw/OmniRoute). + +## Installation + +```bash +npm install @omniroute/opencode-provider +``` + +## Usage + +```javascript +import { createOmniRouteProvider } from "@omniroute/opencode-provider"; + +const provider = createOmniRouteProvider({ + baseURL: "http://localhost:20128/v1", + apiKey: "your-omniroute-api-key", +}); +``` + +Then configure OpenCode to use the provider: + +```jsonc +// OpenCode settings +{ + "provider": provider +} +``` + +## API + +### `createOmniRouteProvider(options)` + +Creates an OpenCode-compatible provider object that routes requests through OmniRoute. + +**Options:** + +| Option | Type | Required | Description | +| --------- | -------- | -------- | ---------------------------------------------------------- | +| `baseURL` | `string` | Yes | OmniRoute API base URL (e.g., `http://localhost:20128/v1`) | +| `apiKey` | `string` | Yes | OmniRoute API key | +| `model` | `string` | No | Model identifier (default: `"opencode"`) | + +**Returns:** An OpenCode-compatible provider object with `id`, `name`, `npm`, `options`, and `auth` fields. diff --git a/@omniroute/opencode-provider/index.d.ts b/@omniroute/opencode-provider/index.d.ts new file mode 100644 index 0000000000..32b235cb9e --- /dev/null +++ b/@omniroute/opencode-provider/index.d.ts @@ -0,0 +1,3 @@ +import OmniRouteProvider from "./index.js"; +export { OmniRouteProvider }; +export default OmniRouteProvider; diff --git a/@omniroute/opencode-provider/index.js b/@omniroute/opencode-provider/index.js new file mode 100644 index 0000000000..3fb4f441d1 --- /dev/null +++ b/@omniroute/opencode-provider/index.js @@ -0,0 +1 @@ +export { createOmniRouteProvider, default as default } from "./index.ts"; diff --git a/@omniroute/opencode-provider/index.ts b/@omniroute/opencode-provider/index.ts new file mode 100644 index 0000000000..e053e65c65 --- /dev/null +++ b/@omniroute/opencode-provider/index.ts @@ -0,0 +1,54 @@ +/** + * OpenCode provider plugin for OmniRoute AI Gateway + * + * Usage: + * import { createOmniRouteProvider } from "@omniroute/opencode-provider"; + * const provider = createOmniRouteProvider({ + * baseURL: "http://localhost:20128/v1", + * apiKey: "your-api-key", + * }); + * + * Then add to OpenCode settings: + * { "provider": provider } + */ + +export interface OmniRouteProviderOptions { + baseURL: string; + apiKey: string; + model?: string; +} + +export interface OmniRouteProvider { + id: string; + name: string; + npm: string; + options: Record; + auth: { type: string; apiKey: string }; +} + +export function createOmniRouteProvider(options: OmniRouteProviderOptions): OmniRouteProvider { + if (!options.baseURL) { + throw new Error("baseURL is required"); + } + if (!options.apiKey) { + throw new Error("apiKey is required"); + } + + const baseURL = options.baseURL.replace(/\/+$/, ""); + + return { + id: "omniroute", + name: "OmniRoute AI Gateway", + npm: "@omniroute/opencode-provider", + options: { + baseURL: `${baseURL}/v1`, + model: options.model || "opencode", + }, + auth: { + type: "apiKey", + apiKey: options.apiKey, + }, + }; +} + +export default createOmniRouteProvider; diff --git a/@omniroute/opencode-provider/package.json b/@omniroute/opencode-provider/package.json new file mode 100644 index 0000000000..4ffc071ca2 --- /dev/null +++ b/@omniroute/opencode-provider/package.json @@ -0,0 +1,20 @@ +{ + "name": "@omniroute/opencode-provider", + "version": "1.0.0", + "description": "OpenCode provider plugin for OmniRoute AI Gateway", + "type": "module", + "main": "index.js", + "types": "index.d.ts", + "files": [ + "index.js", + "index.d.ts", + "README.md" + ], + "keywords": [ + "omniroute", + "opencode", + "provider" + ], + "license": "MIT", + "peerDependencies": {} +} diff --git a/README.md b/README.md index 5ea1659691..501c21dc6a 100644 --- a/README.md +++ b/README.md @@ -1,493 +1,1549 @@ - - - - - - - - - - - - - - - - - - - - -
-# ๐Ÿš€ OmniRoute +# ๐Ÿš€ OmniRoute โ€” The Free AI Gateway -### **The Free AI Gateway โ€” one endpoint, 177+ providers, zero downtime.** +### Never stop coding. Save 15-95% eligible tokens with RTK+Caveman compression + auto-fallback to **FREE & low-cost AI models**. -**Auto-fallback to free models. Stop coding interruptions. Cut tokens 15-95%.** +_The most complete open-source AI proxy โ€” **one endpoint**, **160+ providers**, **13 routing strategies**, zero downtime. Multi-platform: **Web**, **Desktop (Electron)**, **Mobile (PWA + Termux)**. Fully extensible via **MCP Server (37 tools)**, **A2A Protocol**, and **Memory/Skills** systems. Available in **40+ languages**._ -[![npm](https://img.shields.io/npm/v/omniroute?logo=npm&style=flat-square)](https://www.npmjs.com/package/omniroute) -[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=flat-square)](LICENSE) -[![Node](https://img.shields.io/badge/node-%E2%89%A520.20.2-brightgreen?style=flat-square)](package.json) -[![Stars](https://img.shields.io/github/stars/diegosouzapw/OmniRoute?style=social)](https://github.com/diegosouzapw/OmniRoute) -[![Trendshift](https://trendshift.io/api/badge/repositories/23589)](https://trendshift.io/repositories/23589) +**Chat Completions โ€ข Responses API โ€ข Embeddings โ€ข Image Generation โ€ข Video โ€ข Music โ€ข Audio Speech/Transcription โ€ข Reranking โ€ข Moderations โ€ข Web Search โ€ข MCP Server โ€ข A2A Protocol โ€ข 4,600+ Tests โ€ข 100% TypeScript** -[**Website**](https://omniroute.online) ยท [**Quick Start**](#-quick-start) ยท [**Docs**](#-documentation) ยท [**Discord/WhatsApp**](#-community) +
-v3.8.0 ยท MIT ยท Production-ready ยท Self-hosted + + Get $100 Free AI Credits + + +๐Ÿ”ฅ Limited offer: Sign up at AgentRouter and get $100 in free AI credits
Access GPT-5, Claude, Gemini, DeepSeek & 100+ models. No credit card required. Claim your credits โ†’
+ +
+ +diegosouzapw%2FOmniRoute | Trendshift + +[๐Ÿš€ Quick Start](#-quick-start) โ€ข [๐Ÿ’ก Features](#-key-features) โ€ข [๐Ÿ—œ๏ธ Compression](#%EF%B8%8F-prompt-compression--save-15-95-eligible-tokens-automatically) โ€ข [๐Ÿ’ฐ Pricing](#-pricing-at-a-glance) โ€ข [๐ŸŽฏ Use Cases](#-use-cases--ready-made-combo-playbooks) โ€ข [๐ŸŒ Proxy](#-bypass-geographic-blocks--use-ai-from-any-country) โ€ข [โ“ FAQ](#-frequently-asked-questions) โ€ข [๐Ÿ“– Docs](#-documentation) โ€ข [๐Ÿ’ฌ WhatsApp](https://chat.whatsapp.com/JI7cDQ1GyaiDHhVBpLxf8b?mode=gi_t)
--- -## โšก The Pitch (60 seconds) +
-| ๐ŸŽฏ The problem | โœ… How OmniRoute solves it | -| ------------------------------ | -------------------------------------------------------------------------------------------------- | -| Hit rate limits on Claude/GPT? | **Auto-fallback** across 177 providers โ€” never see a 429 again | -| Bored of switching API keys? | **One endpoint** (`localhost:20128`) speaks OpenAI, Anthropic, Gemini, Claude Code, Cursor formats | -| Paying $200/mo for AI? | **11 free providers** + intelligent routing โ†’ most users pay $0 | -| Tokens too expensive? | **RTK + Caveman compression** saves 15-95% on eligible payloads | -| Blocked region? | **4-level proxy** (account/provider/combo/global) + **1proxy free marketplace** | -| Want CLI agents free? | Plug **Cursor, Cline, Codex, Claude Code, Aider, 15+ CLIs** at OmniRoute | +๐ŸŒ **Available in:** ๐Ÿ‡บ๐Ÿ‡ธ [English](README.md) | ๐Ÿ‡ง๐Ÿ‡ท [Portuguรชs (Brasil)](docs/i18n/pt-BR/README.md) | ๐Ÿ‡ช๐Ÿ‡ธ [Espaรฑol](docs/i18n/es/README.md) | ๐Ÿ‡ซ๐Ÿ‡ท [Franรงais](docs/i18n/fr/README.md) | ๐Ÿ‡ฎ๐Ÿ‡น [Italiano](docs/i18n/it/README.md) | ๐Ÿ‡ท๐Ÿ‡บ [ะ ัƒััะบะธะน](docs/i18n/ru/README.md) | ๐Ÿ‡จ๐Ÿ‡ณ [ไธญๆ–‡ (็ฎ€ไฝ“)](docs/i18n/zh-CN/README.md) | ๐Ÿ‡ฉ๐Ÿ‡ช [Deutsch](docs/i18n/de/README.md) | ๐Ÿ‡ฎ๐Ÿ‡ณ [เคนเคฟเคจเฅเคฆเฅ€](docs/i18n/in/README.md) | ๐Ÿ‡น๐Ÿ‡ญ [เน„เธ—เธข](docs/i18n/th/README.md) | ๐Ÿ‡บ๐Ÿ‡ฆ [ะฃะบั€ะฐั—ะฝััŒะบะฐ](docs/i18n/uk-UA/README.md) | ๐Ÿ‡ธ๐Ÿ‡ฆ [ุงู„ุนุฑุจูŠุฉ](docs/i18n/ar/README.md) | ๐Ÿ‡ฏ๐Ÿ‡ต [ๆ—ฅๆœฌ่ชž](docs/i18n/ja/README.md) | ๐Ÿ‡ป๐Ÿ‡ณ [Tiแบฟng Viแป‡t](docs/i18n/vi/README.md) | ๐Ÿ‡ง๐Ÿ‡ฌ [ะ‘ัŠะปะณะฐั€ัะบะธ](docs/i18n/bg/README.md) | ๐Ÿ‡ฉ๐Ÿ‡ฐ [Dansk](docs/i18n/da/README.md) | ๐Ÿ‡ซ๐Ÿ‡ฎ [Suomi](docs/i18n/fi/README.md) | ๐Ÿ‡ฎ๐Ÿ‡ฑ [ืขื‘ืจื™ืช](docs/i18n/he/README.md) | ๐Ÿ‡ญ๐Ÿ‡บ [Magyar](docs/i18n/hu/README.md) | ๐Ÿ‡ฎ๐Ÿ‡ฉ [Bahasa Indonesia](docs/i18n/id/README.md) | ๐Ÿ‡ฐ๐Ÿ‡ท [ํ•œ๊ตญ์–ด](docs/i18n/ko/README.md) | ๐Ÿ‡ฒ๐Ÿ‡พ [Bahasa Melayu](docs/i18n/ms/README.md) | ๐Ÿ‡ณ๐Ÿ‡ฑ [Nederlands](docs/i18n/nl/README.md) | ๐Ÿ‡ณ๐Ÿ‡ด [Norsk](docs/i18n/no/README.md) | ๐Ÿ‡ต๐Ÿ‡น [Portuguรชs (Portugal)](docs/i18n/pt/README.md) | ๐Ÿ‡ท๐Ÿ‡ด [Romรขnฤƒ](docs/i18n/ro/README.md) | ๐Ÿ‡ต๐Ÿ‡ฑ [Polski](docs/i18n/pl/README.md) | ๐Ÿ‡ธ๐Ÿ‡ฐ [Slovenฤina](docs/i18n/sk/README.md) | ๐Ÿ‡ธ๐Ÿ‡ช [Svenska](docs/i18n/sv/README.md) | ๐Ÿ‡ต๐Ÿ‡ญ [Filipino](docs/i18n/phi/README.md) | ๐Ÿ‡จ๐Ÿ‡ฟ [ฤŒeลกtina](docs/i18n/cs/README.md) -๐Ÿ“บ **Watch in action:** [Video demo](https://www.youtube.com/@diegosouza-pw) +
+ +
+ +[![npm version](https://img.shields.io/npm/v/omniroute?color=cb3837&logo=npm)](https://www.npmjs.com/package/omniroute) +![NPM Weekly](https://img.shields.io/npm/dw/omniroute?label=npm/week&color=cb3837&logo=npm) +![NPM Monthly](https://img.shields.io/npm/dm/omniroute?label=npm/month&color=cb3837&logo=npm) +![NPM Yearly](https://img.shields.io/npm/d18m/omniroute?label=npm/year&color=cb3837&logo=npm) + +[![Docker Hub](https://img.shields.io/docker/v/diegosouzapw/omniroute?label=Docker%20Hub&logo=docker&color=2496ED)](https://hub.docker.com/r/diegosouzapw/omniroute) +![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) +[![license](https://custom-icon-badges.demolab.com/github/license/diegosouzapw/OmniRoute?logo=law)](https://github.com/diegosouzapw/OmniRoute/blob/main/LICENSE) + + + +[![total contributions](https://custom-icon-badges.demolab.com/badge/dynamic/json?logo=graph&logoColor=fff&color=blue&label=total%20contributions&query=%24.totalContributions&url=https%3A%2F%2Fstreak-stats.demolab.com%2F%3Fuser%3Ddiegosouzapw%26type%3Djson)](https://github.com/diegosouzapw) +[![github streak](https://custom-icon-badges.demolab.com/badge/dynamic/json?logo=fire&logoColor=fff&color=orange&label=github%20streak&query=%24.currentStreak.length&suffix=%20days&url=https%3A%2F%2Fstreak-stats.demolab.com%2F%3Fuser%3Ddiegosouzapw%26type%3Djson)](https://github.com/diegosouzapw) +[![Website](https://img.shields.io/badge/Website-omniroute.online-blue?logo=google-chrome&logoColor=white)](https://omniroute.online) +[![WhatsApp](https://img.shields.io/badge/WhatsApp-Community-25D366?logo=whatsapp&logoColor=white)](https://chat.whatsapp.com/JI7cDQ1GyaiDHhVBpLxf8b?mode=gi_t) + +
--- -## ๐Ÿ–ผ๏ธ Dashboard Preview +## ๐Ÿ–ผ๏ธ Main Dashboard -![OmniRoute Main Dashboard](docs/screenshots/MainOmniRoute.png) +
+ OmniRoute Dashboard +
+ +--- + +## ๐Ÿ“ธ Dashboard Preview
-Click for more screenshots (Providers, Combos, Memory, MCP, Auditโ€ฆ) +Click to see dashboard screenshots -| | | -| --------------------------------------------- | ---------------------------------------------------- | -| ![Providers](docs/screenshots/Provedores.png) | ![Combos](docs/screenshots/Combos.png) | -| ![Routing](docs/screenshots/Roteamento.png) | ![Compression](docs/screenshots/Compress%C3%A3o.png) | -| ![Audit](docs/screenshots/Auditoria.png) | ![Analytics](docs/screenshots/Analytics.png) | +| Page | Screenshot | +| -------------- | ------------------------------------------------- | +| **Providers** | ![Providers](docs/screenshots/01-providers.png) | +| **Combos** | ![Combos](docs/screenshots/02-combos.png) | +| **Analytics** | ![Analytics](docs/screenshots/03-analytics.png) | +| **Health** | ![Health](docs/screenshots/04-health.png) | +| **Translator** | ![Translator](docs/screenshots/05-translator.png) | +| **Settings** | ![Settings](docs/screenshots/06-settings.png) | +| **CLI Tools** | ![CLI Tools](docs/screenshots/07-cli-tools.png) | +| **Usage Logs** | ![Usage](docs/screenshots/08-usage.png) | +| **Endpoints** | ![Endpoints](docs/screenshots/09-endpoint.png) |
--- +### ๐Ÿค– Free AI Provider for your favorite coding agents + +_Connect any AI-powered IDE or CLI tool through OmniRoute โ€” free API gateway for unlimited coding._ + + + + + + + + + + + + + + + + +
+ + OpenClaw
+ OpenClaw +

+ โญ 205K +
+ + NanoBot
+ NanoBot +

+ โญ 20.9K +
+ + PicoClaw
+ PicoClaw +

+ โญ 14.6K +
+ + ZeroClaw
+ ZeroClaw +

+ โญ 9.9K +
+ + IronClaw
+ IronClaw +

+ โญ 2.1K +
+ + OpenCode
+ OpenCode +

+ โญ 106K +
+ + Codex CLI
+ Codex CLI +

+ โญ 60.8K +
+ + Claude Code
+ Claude Code +

+ โญ 67.3K +
+ + Gemini CLI
+ Gemini CLI +

+ โญ 94.7K +
+ + Kilo Code
+ Kilo Code +

+ โญ 15.5K +
+ +๐Ÿ“ก All agents connect via http://localhost:20128/v1 or http://cloud.omniroute.online/v1 โ€” one config, unlimited models and quota + +--- + +## ๐Ÿ“บ OmniRoute in Action โ€” Video Guides + +
+ + + + + + + +
+ + OmniRoute โ€” Guia em Portuguรชs +
+ ๐Ÿ‡ง๐Ÿ‡ท Portuguรชs
+ Guia completo do OmniRoute +
+ + OmniRoute โ€” English Guide +
+ ๐Ÿ‡บ๐Ÿ‡ธ English
+ Complete OmniRoute walkthrough +
+ + OmniRoute โ€” ะ ัƒะบะพะฒะพะดัั‚ะฒะพ ะฝะฐ ั€ัƒััะบะพะผ +
+ ๐Ÿ‡ท๐Ÿ‡บ ะ ัƒััะบะธะน
+ ะŸะพะปะฝะพะต ั€ัƒะบะพะฒะพะดัั‚ะฒะพ ะฟะพ OmniRoute +
+ +
+ +> ๐ŸŽฌ **Made a video about OmniRoute?** We'd love to feature it here! Open an [issue](https://github.com/diegosouzapw/OmniRoute/issues/new) or [discussion](https://github.com/diegosouzapw/OmniRoute/discussions) with the link and we'll add it to this showcase. + +--- + +## ๐Ÿค” Why OmniRoute? + +**Stop wasting money, tokens and hitting limits:** + +โŒ Subscription quota expires unused every month +โŒ Rate limits stop you mid-coding +โŒ Tool outputs (`git diff`, `grep`, `ls`...) burn tokens fast +โŒ Expensive APIs ($20-50/month per provider) +โŒ Manual switching between providers +โŒ Each provider has a different API format +โŒ AI providers blocked in your country + +**OmniRoute solves all of this:** + +โœ… **Prompt Compression** โ€” auto-compress prompts & tool outputs, save 15-95% eligible tokens per request with RTK+Caveman stacked mode +โœ… **Maximize subscriptions** โ€” track quota, use every bit before reset +โœ… **Auto fallback** โ€” Subscription โ†’ API Key โ†’ Cheap โ†’ Free, zero downtime +โœ… **Multi-account** โ€” round-robin between accounts per provider +โœ… **Format translation** โ€” OpenAI โ†” Claude โ†” Gemini โ†” Responses API, any tool works +โœ… **3-level proxy** โ€” bypass geo-blocks with global, per-provider, and per-key proxies +โœ… **10 multi-modal APIs** โ€” chat, images, video, music, audio, search in one endpoint +โœ… **MCP + A2A** โ€” 29 MCP tools + agent-to-agent protocol, production-ready +โœ… **Universal** โ€” works with Claude Code, Codex, Gemini CLI, Cursor, Cline, OpenClaw, any CLI tool + +--- + +## ๐Ÿ“ง Support + +> ๐Ÿ’ฌ **Join our community!** [WhatsApp Group](https://chat.whatsapp.com/JI7cDQ1GyaiDHhVBpLxf8b?mode=gi_t) โ€” Get help, share tips, and stay updated. + +- **Website**: [omniroute.online](https://omniroute.online) +- **GitHub**: [github.com/diegosouzapw/OmniRoute](https://github.com/diegosouzapw/OmniRoute) +- **Issues**: [github.com/diegosouzapw/OmniRoute/issues](https://github.com/diegosouzapw/OmniRoute/issues) +- **WhatsApp**: [Community Group](https://chat.whatsapp.com/JI7cDQ1GyaiDHhVBpLxf8b?mode=gi_t) +- **Contributing**: See [CONTRIBUTING.md](CONTRIBUTING.md), open a PR, or pick a `good first issue` +- **Original Project**: [9router by decolua](https://github.com/decolua/9router) + +### ๐Ÿ› Reporting a Bug? + +When opening an issue, please run the system-info command and attach the generated file: + +```bash +npm run system-info +``` + +This generates a `system-info.txt` with your Node.js version, OmniRoute version, OS details, installed CLI tools (qoder, gemini, claude, codex, antigravity, droid, etc.), Docker/PM2 status, and system packages โ€” everything we need to reproduce your issue quickly. Attach the file directly to your GitHub issue. + +--- + +## ๐Ÿ› ๏ธ Supported CLI Tools + +OmniRoute works seamlessly with **16+ AI coding tools** โ€” one config, all tools: + + + + + + + + + + + + + + + + + + + + + + + + + + +
Claude Code
Anthropic
Codex CLI
OpenAI
Gemini CLI
Google
Cursor
IDE
OpenClaw
CLI
Antigravity
VS Code
Cline
Extension
Continue
Extension
Kilo Code
Extension
Kiro
AWS IDE
OpenCode
CLI
Droid
CLI
AMP
CLI
Copilot
GitHub
Windsurf
IDE
Hermes
CLI
Qwen CLI
Alibaba
Custom
Any tool
+ +๐Ÿ“– Full setup for each tool: [`docs/CLI-TOOLS.md`](docs/CLI-TOOLS.md) + +--- + +## ๐ŸŒ Supported Providers โ€” 160+ + +### ๐Ÿ” OAuth Providers + + + + + + + + + + + + + + + +
Claude Code
Anthropic OAuth
Antigravity
Google OAuth
Codex
OpenAI OAuth
GitHub Copilot
GitHub OAuth
Cursor
Cursor OAuth
Kimi Coding
Moonshot OAuth
Kilo Code
Kilo OAuth
Cline
Cline OAuth
+ +### ๐Ÿ†“ Free Providers (No Cost) + + + + + + + + + + + + + + +
๐ŸŸข Kiro AI
Claude Sonnet/Haiku
Unlimited FREE
๐ŸŸข Qoder AI
Kimi-K2, DeepSeek-R1
Unlimited FREE
๐ŸŸข Pollinations
GPT-5, Claude, Llama 4
No API key needed
๐ŸŸข Qwen Code
Qwen3 Coder Plus
Unlimited FREE
๐ŸŸข LongCat AI
Flash-Lite
50M tokens/day
๐ŸŸข Cloudflare AI
50+ models
10K neurons/day
๐ŸŸข Puter AI
GPT-4.1, Claude
Rate-limited free
๐ŸŸข NVIDIA NIM
Llama, Mistral
1K req/day free
+ +### ๐Ÿ”‘ API Key Providers (120+) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
OpenAIAnthropicGeminiDeepSeekGroqxAI (Grok)
MistralOpenRouterGLMKimiMiniMaxFireworks
Together AICerebrasCohereNVIDIAPerplexitySiliconFlow
NebiusHuggingFaceDeepInfraSambaNovaVertex AIAzure OpenAI
AWS BedrockSnowflakeDatabricksVenice.aiAI21 LabsMeta Llama
+ +
+...and 90+ more providers + +Alibaba ยท Amazon Q ยท AssemblyAI ยท Baidu Qianfan ยท Baseten ยท Black Forest Labs ยท Blackbox ยท Brave Search ยท Bytez ยท CablyAI ยท Cartesia ยท ChatGPT Web ยท Chutes.ai ยท Clarifai ยท Codestral ยท CrofAI ยท DataRobot ยท Deepgram ยท ElevenLabs ยท Empower ยท Exa Search ยท Fal.ai ยท Featherless AI ยท FenayAI ยท FriendliAI ยท Galadriel ยท GigaChat ยท GitLab Duo ยท GLHF Chat ยท GoAPI ยท Heroku AI ยท Hyperbolic ยท IBM watsonx ยท Inference.net ยท Inworld ยท Jina AI ยท Kilo Gateway ยท Lambda AI ยท LaoZhang ยท Linkup Search ยท LlamaGate ยท Maritalk ยท Modal ยท Moonshot AI ยท Morph ยท Muse Spark ยท NanoBanana ยท NanoGPT ยท NLP Cloud ยท Nous Research ยท Novita AI ยท nScale ยท OCI ยท Ollama Cloud ยท OVHcloud ยท PiAPI ยท PlayHT ยท Poe ยท Predibase ยท PublicAI ยท Qwen Code ยท Recraft ยท Reka ยท Runway ยท SAP ยท Scaleway ยท SearchAPI ยท SearXNG ยท Serper ยท Stability AI ยท Synthetic ยท Tavily ยท TheB.AI ยท Topaz ยท Upstage ยท v0 (Vercel) ยท Vercel AI Gateway ยท Volcengine ยท Voyage AI ยท W&B Inference ยท Xiaomi MiMo ยท You.com ยท Z.AI ยท + OpenAI/Anthropic-compatible custom endpoints + +
+ +### ๐Ÿ  Self-Hosted + + + + + + + + + + + + + + + + +
LM StudioOllamavLLMLlamafileDocker Model Runner
NVIDIA TritonXInferenceoobaboogaComfyUISD WebUI
+ +--- + +## ๐Ÿ”„ How It Works + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ Your CLI โ”‚ (Claude Code, Codex, Gemini CLI, OpenClaw, Cursor, Cline...) +โ”‚ Tool โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ http://localhost:20128/v1 + โ†“ +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ OmniRoute (Smart Router) โ”‚ +โ”‚ โ€ข ๐Ÿ—œ๏ธ Prompt Compression (save 15-95% eligible) โ”‚ +โ”‚ โ€ข Format translation (OpenAI โ†” Claude โ†” Gemini) โ”‚ +โ”‚ โ€ข Quota tracking + Embeddings + Images โ”‚ +โ”‚ โ€ข Auto token refresh + Rate limit management โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ + โ”œโ”€โ†’ [Tier 1: SUBSCRIPTION] Claude Code, Codex, Gemini CLI + โ”‚ โ†“ quota exhausted + โ”œโ”€โ†’ [Tier 2: API KEY] DeepSeek, Groq, xAI, Mistral, NVIDIA NIM, etc. + โ”‚ โ†“ budget limit + โ”œโ”€โ†’ [Tier 3: CHEAP] GLM ($0.6/1M), MiniMax ($0.2/1M) + โ”‚ โ†“ budget limit + โ””โ”€โ†’ [Tier 4: FREE] Qoder, Qwen, Kiro (unlimited) + +Result: Never stop coding, minimal cost + 15-95% eligible token savings +``` + +--- + +## ๐Ÿ—œ๏ธ Prompt Compression โ€” Save 15-95% Eligible Tokens Automatically + +> **Why use many token when few token do trick?** OmniRoute's built-in compression pipeline reduces token usage before requests reach the provider. It combines ideas from [RTK - Rust Token Killer](https://github.com/rtk-ai/rtk) and [Caveman](https://github.com/JuliusBrussee/caveman) (โญ 51K+). + +### How It Works + +Every request passes through the compression pipeline **transparently** โ€” no client changes needed: + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ Client sends โ”‚โ”€โ”€โ”€โ”€โ–ถโ”‚ OmniRoute Compression โ”‚โ”€โ”€โ”€โ”€โ–ถโ”‚ Provider โ”‚ +โ”‚ full prompt โ”‚ โ”‚ Pipeline (7 options) โ”‚ โ”‚ receives โ”‚ +โ”‚ (10,000 tok) โ”‚ โ”‚ โ”‚ โ”‚ compressed โ”‚ +โ”‚ โ”‚ โ”‚ ๐Ÿชถ Lite ........... ~15% โ”‚ โ”‚ (~1,080 tok)โ”‚ +โ”‚ โ”‚ โ”‚ ๐Ÿชจ Standard ....... ~30% โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ โšก Aggressive ..... ~50% โ”‚ โ”‚ ๐Ÿ’ฐ up to 95%โ”‚ +โ”‚ โ”‚ โ”‚ ๐Ÿ”ฅ Ultra .......... ~75% โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ ๐Ÿงฐ RTK ............ 60-90% โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ ๐Ÿ”— Stacked ........ 78-95% โ”‚ โ”‚ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +### 7 Compression Options + +| Mode | Savings | Technique | Best For | +| ------------------------- | ------- | ----------------------------------------------------------------------------------------------- | -------------------------------------- | +| **Off** | 0% | No compression | When you need exact prompts | +| **๐Ÿชถ Lite** | ~15% | Whitespace collapse, dedup system prompts, image URL shortening | Always-on safe default | +| **๐Ÿชจ Standard (Caveman)** | ~30% | 30+ regex rules: filler removal, context condensation, structural compression, multi-turn dedup | Daily coding with Claude/Codex | +| **โšก Aggressive** | ~50% | All standard + progressive message aging + tool result summarization + LLM-based compression | Long sessions with many tool calls | +| **๐Ÿ”ฅ Ultra** | ~75% | All aggressive + heuristic token pruning + stopword removal + score-based filtering | Maximum savings when tokens are scarce | +| **๐Ÿงฐ RTK** | 60-90% | 49 command-aware filters, RTK-style JSON DSL, verify gate, trust-gated custom filters | Shell/test/build/git output in agents | +| **๐Ÿ”— Stacked** | 78-95% | RTK first, then Caveman input condensation; ~89% with upstream average math | Mixed prompts with tool logs + prose | + +### RTK + Caveman Savings Math + +These numbers are based on the upstream project READMEs under `_references/_outros`: + +| Source | Upstream claim used by OmniRoute docs | +| ------- | ------------------------------------------------------------------------------------------------------------------- | +| Caveman | `~75%` fewer output tokens; benchmark average `65%` output savings, range `22-87%`; `~46%` input compression tool | +| RTK | `60-90%` command-output token savings; sample session `~118,000 -> ~23,900` tokens, which is `79.7%` saved (`~80%`) | + +For the default stacked compression combo, OmniRoute runs: + +```txt +RTK -> Caveman +``` + +When both engines can act on the same tool/context payload, the savings compound: + +```txt +combined = 1 - (1 - RTK savings) * (1 - Caveman input savings) +average = 1 - (1 - 0.80) * (1 - 0.46) = 89.2% +range = 1 - (1 - 0.60..0.90) * (1 - 0.46) = 78.4-94.6% +``` + +Caveman output mode is separate from prompt compression. When enabled for responses, use Caveman's +own upstream output numbers: `65%` average, `~75%` headline, `22-87%` observed range. Total bill +savings depend on the prompt/output mix, but coding-agent sessions are often tool-context heavy, so +the `RTK -> Caveman` combo is the best default for maximum context savings. + +### Before & After (Standard/Caveman Mode) + +**๐Ÿ—ฃ๏ธ Before compression (69 tokens):** + +> "The reason your React component is re-rendering is likely because you're creating a new object reference on each render cycle. When you pass an inline object as a prop, React's shallow comparison sees it as a different object every time, which triggers a re-render. I would recommend using useMemo to memoize the object." + +**๐Ÿชจ After compression (19 tokens):** + +> "New object ref each render. Inline object prop = new ref = re-render. Wrap in useMemo." + +**Same answer. 72% less tokens. Zero accuracy loss.** + +### Architecture + +``` +Request Body + โ”‚ + โ”œโ”€ strategySelector.ts โ”€โ”€โ”€ Picks mode (config / combo override / auto-trigger) + โ”‚ + โ”œโ”€ lite.ts โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Whitespace, dedup, image URLs, redundant content + โ”œโ”€ caveman.ts โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ 30+ regex rules via cavemanRules.ts + โ”‚ โ””โ”€ preservation.ts โ”€โ”€โ”€ Protects code blocks, URLs, JSON from compression + โ”œโ”€ engines/rtk/ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Command detection + JSON DSL filters + raw-output recovery + โ”œโ”€ engines/registry.ts โ”€โ”€โ”€ Shared engine registry for caveman, RTK, and stacked + โ”œโ”€ aggressive.ts โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Summarizer + tool result compressor + progressive aging + โ”‚ โ”œโ”€ summarizer.ts โ”€โ”€โ”€โ”€โ”€ Rule-based message summarization + โ”‚ โ”œโ”€ toolResultCompressor.ts โ”€โ”€ file/grep/shell/JSON/error compression + โ”‚ โ””โ”€ progressiveAging.ts โ”€โ”€โ”€โ”€ Older messages โ†’ shorter summaries + โ””โ”€ ultra.ts โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Heuristic token scoring + pruning + โ””โ”€ ultraHeuristic.ts โ”€ Stopword detection, score thresholds, force-preserve +``` + +### Configuration + +``` +Dashboard โ†’ Context & Cache โ†’ Caveman / RTK / Compression Combos +``` + +Or per-combo override: + +```json +{ + "comboOverrides": { + "my-coding-combo": "standard", + "my-cheap-combo": "ultra" + } +} +``` + +Auto-trigger: set `autoTriggerTokens` to automatically enable compression when a request exceeds a token threshold. + +Compression combos can also assign a named compression pipeline to routing combos, so a coding combo can use RTK + Caveman while a paid subscription combo stays on lite mode. + +> ๐Ÿชจ **Fun fact:** The standard/caveman mode is inspired by [Caveman](https://github.com/JuliusBrussee/caveman) โ€” the viral project that reports 65% average output-token savings while keeping technical accuracy. OmniRoute takes this further with a **7-option pipeline** and a default `RTK -> Caveman` combo that can reach ~89% average savings on eligible tool/context payloads. + +๐Ÿ“– **Full compression documentation:** [`docs/COMPRESSION_GUIDE.md`](docs/COMPRESSION_GUIDE.md) โ€ข [`docs/RTK_COMPRESSION.md`](docs/RTK_COMPRESSION.md) โ€ข [`docs/COMPRESSION_ENGINES.md`](docs/COMPRESSION_ENGINES.md) โ€ข [`docs/COMPRESSION_RULES_FORMAT.md`](docs/COMPRESSION_RULES_FORMAT.md) โ€ข [`docs/COMPRESSION_LANGUAGE_PACKS.md`](docs/COMPRESSION_LANGUAGE_PACKS.md) + +--- + +## ๐ŸŽฏ What OmniRoute Solves + +> **Every developer using AI tools faces these problems daily.** OmniRoute solves them all. + +| # | Problem | OmniRoute Solution | +| --- | ---------------------------------------- | ----------------------------------------------------------------------------------------------- | +| ๐Ÿ’ธ | Subscription quota expires mid-coding | **Smart 4-Tier Fallback** โ€” auto-routes Subscription โ†’ API Key โ†’ Cheap โ†’ Free | +| ๐Ÿ”Œ | Each provider has a different API format | **Format Translation** โ€” unified endpoint translates OpenAI โ†” Claude โ†” Gemini โ†” Responses | +| ๐ŸŒ | AI providers block my country/region | **3-Level Proxy** โ€” global, per-provider, and per-key proxy with TLS fingerprint spoofing | +| ๐Ÿ†“ | Can't afford AI subscriptions | **11 Free Providers** โ€” Kiro, Qoder, Pollinations, LongCat, Cloudflare AI, NVIDIA NIM... | +| ๐Ÿ”’ | Gateway is exposed without protection | **API Key Management** โ€” scoping, rotation, IP filtering, rate limiting, prompt injection guard | +| ๐Ÿ›‘ | Provider went down, lost coding flow | **Circuit Breakers** โ€” auto-failover with cooldown, retry, anti-thundering herd | +| ๐Ÿ”ง | Configuring each CLI tool is tedious | **CLI Tools Dashboard** โ€” one-click setup for Claude Code, Codex, Cursor, OpenClaw, Kilo | +| ๐Ÿ”‘ | Managing OAuth tokens is hell | **Auto Token Refresh** โ€” OAuth PKCE for 8 providers, multi-account, LAN/remote fix | +| ๐Ÿ“Š | Don't know how much I'm spending | **Cost Analytics** โ€” per-token tracking, budget limits, usage stats per API key | +| ๐Ÿ› | Can't diagnose errors in AI calls | **Unified Logs** โ€” 4-tab dashboard (request, proxy, audit, console) + p50/p95/p99 telemetry | + +
+๐Ÿ“– See all 31 problems OmniRoute solves + +| # | Problem | Solution | +| --- | --------------------------------------------- | -------------------------------------------------------------------------------------------------- | +| 11 | Deploying/maintaining is complex | npm global, Docker multi-arch, Electron, Termux โ€” deploy anywhere | +| 12 | Interface is English-only | 40+ languages with RTL support | +| 13 | Need more than chat (images, audio, video) | 10 multi-modal APIs: embeddings, images, video, music, TTS, STT, moderation, rerank, search, batch | +| 14 | No way to test/compare models | LLM Evals, Translator Playground, Chat Tester, Live Monitor | +| 15 | Need to scale without losing performance | Semantic cache, request dedup, rate limit detection, queue & pacing | +| 16 | Want to control model behavior globally | System prompt injection, thinking budget, wildcard routing | +| 17 | Need MCP tools as first-class features | 29 MCP tools, 3 transports (stdio/SSE/HTTP), 10 scopes, audit trail | +| 18 | Need A2A orchestration | JSON-RPC 2.0 + SSE streaming, task lifecycle, sync + stream paths | +| 19 | Need real MCP process health | Runtime heartbeat, PID tracking, UI status cards | +| 20 | Need auditable MCP execution | SQLite-backed audit with filters, pagination, stats | +| 21 | Need scoped MCP permissions | 10 granular scopes per integration | +| 22 | Need operational controls without redeploying | Combo switches, resilience tuning, breaker resets from dashboard | +| 23 | Need A2A task lifecycle visibility | Task listing/filtering, drill-down, cancellation | +| 24 | Need active stream metrics | Active stream counters, per-state counts, A2A dashboard cards | +| 25 | Need standard agent discovery | Agent Card at `/.well-known/agent.json` | +| 26 | Need protocol discoverability | Consolidated Endpoints page with Proxy, MCP, A2A, API tabs | +| 27 | Need E2E protocol validation | Real MCP SDK + A2A client flows in `test:protocols:e2e` | +| 28 | Need unified observability | Health + audit + telemetry across OpenAI, MCP, and A2A layers | +| 29 | Need one runtime for proxy + tools + agents | OpenAI proxy + MCP + A2A in one stack with shared auth/resilience | +| 30 | Need agentic workflows without glue-code | Unified endpoint, protocol UIs, production-ready foundations | +| 31 | Long sessions crash with context limits | Proactive context compression, structural integrity guards, multi-layer dropping | + +
+ +๐Ÿ“– **Deep dives:** [Resilience Guide](docs/RESILIENCE_GUIDE.md) โ€ข [Proxy Guide](docs/PROXY_GUIDE.md) โ€ข [Setup Guide](docs/SETUP_GUIDE.md) โ€ข [Compression Guide](docs/COMPRESSION_GUIDE.md) + +--- + +## ๐Ÿ†“ Start Free โ€” Zero Configuration Cost + +> Setup AI coding in minutes at **$0/month**. Connect these free accounts and use the built-in **Free Stack** combo. + +| Step | Action | Providers Unlocked | +| ---- | -------------------------------------------------- | ------------------------------------------------------------------ | +| 1 | Connect **Kiro** (AWS Builder ID OAuth) | Claude Sonnet 4.5, Haiku 4.5 โ€” **unlimited** | +| 2 | Connect **Qoder** (Google OAuth) | kimi-k2-thinking, qwen3-coder-plus, deepseek-r1... โ€” **unlimited** | +| 3 | Connect **Qwen** (Device Code) | qwen3-coder-plus, qwen3-coder-flash... โ€” **unlimited** | +| 4 | Connect **Gemini CLI** (Google OAuth) | gemini-3-flash, gemini-2.5-pro โ€” **180K/mo free** | +| 5 | `/dashboard/combos` โ†’ **Free Stack ($0)** template | Round-robin all free providers automatically | + +**Point any IDE/CLI to:** `http://localhost:20128/v1` ยท API Key: `any-string` ยท Done. + +> **Optional extra coverage (also free):** Groq API key (30 RPM free), NVIDIA NIM (40 RPM free, 70+ models), Cerebras (1M tok/day), LongCat API key (50M tokens/day!), Cloudflare Workers AI (10K Neurons/day, 50+ models). + ## โšก Quick Start +### 1) Install and run + ```bash -# Run instantly (npx โ€” no install needed) +npm install -g omniroute +omniroute +``` + +Dashboard opens at `http://localhost:20128` ยท API at `http://localhost:20128/v1`. + +### 2) Connect providers + +1. Dashboard โ†’ **Providers** โ†’ connect at least one provider (OAuth or API key) +2. Dashboard โ†’ **Endpoints** โ†’ create an API key +3. Dashboard โ†’ **Combos** โ†’ set your fallback chain (optional) + +### 3) Point your coding tool + +```txt +Base URL: http://localhost:20128/v1 +API Key: [copy from Endpoint page] +Model: if/kimi-k2-thinking (or any provider/model) +``` + +Works with Claude Code, Codex CLI, Gemini CLI, Cursor, Cline, OpenClaw, OpenCode, and any OpenAI-compatible tool. + +
+๐Ÿ“ฆ More install methods (Docker, source, Arch, Void, pnpm) + +**Docker:** + +```bash +docker run -d --name omniroute --restart unless-stopped -p 20128:20128 -v omniroute-data:/app/data diegosouzapw/omniroute:latest +``` + +**From source:** + +```bash +cp .env.example .env && npm install +PORT=20128 DASHBOARD_PORT=20129 NEXT_PUBLIC_BASE_URL=http://localhost:20129 npm run dev +``` + +**pnpm:** `pnpm install -g omniroute && pnpm approve-builds -g && omniroute` + +**Arch Linux (AUR):** `yay -S omniroute-bin && systemctl --user enable --now omniroute.service` + +**MCP:** `omniroute --mcp` (stdio transport) + +**CLI options:** `omniroute setup`, `omniroute doctor`, `omniroute providers available`, `omniroute providers list`, `omniroute --port 3000`, `omniroute --no-open`, `omniroute --help` + +**Split-port mode:** `PORT=20128 DASHBOARD_PORT=20129 omniroute` + +**Uninstall:** `npm run uninstall` (keeps data) or `npm run uninstall:full` (removes everything) + +๐Ÿ“– Full details: [Setup Guide](#-setup-guide) ยท [Docker](#-docker) ยท [Void Linux template](#-quick-start) + +
+ +--- + +## ๐Ÿณ Docker + +OmniRoute is available as a public Docker image on [Docker Hub](https://hub.docker.com/r/diegosouzapw/omniroute). + +**Quick run:** + +```bash +docker run -d \ + --name omniroute \ + --restart unless-stopped \ + --stop-timeout 40 \ + -p 20128:20128 \ + -v omniroute-data:/app/data \ + diegosouzapw/omniroute:latest +``` + +**With environment file:** + +```bash +# Copy and edit .env first +cp .env.example .env + +docker run -d \ + --name omniroute \ + --restart unless-stopped \ + --stop-timeout 40 \ + --env-file .env \ + -p 20128:20128 \ + -v omniroute-data:/app/data \ + diegosouzapw/omniroute:latest +``` + +**Using Docker Compose:** + +```bash +# Base profile (no CLI tools) +docker compose --profile base up -d + +# CLI profile (Claude Code, Codex, OpenClaw built-in) +docker compose --profile cli up -d +``` + +Dashboard support for Docker deployments now includes a one-click **Cloudflare Quick Tunnel** on `Dashboard โ†’ Endpoints`. The first enable downloads `cloudflared` only when needed, starts a temporary tunnel to your current `/v1` endpoint, and shows the generated `https://*.trycloudflare.com/v1` URL directly below your normal public URL. Endpoint tunnel panels, including Cloudflare, Tailscale, and ngrok, can be shown or hidden from `Settings โ†’ Appearance` without changing active tunnel state. + +Notes: + +- Quick Tunnel URLs are temporary and change after every restart. +- Quick Tunnels are not auto-restored after an OmniRoute or container restart. Re-enable them from the dashboard when needed. +- Managed install currently supports Linux, macOS, and Windows on `x64` / `arm64`. +- Managed Quick Tunnels default to HTTP/2 transport to avoid noisy QUIC UDP buffer warnings in constrained container environments. Set `CLOUDFLARED_PROTOCOL=quic` or `auto` if you want a different transport. +- Docker images bundle system CA roots and pass them to managed `cloudflared`, which avoids TLS trust failures when the tunnel bootstraps inside the container. +- SQLite runs in WAL mode. `docker stop` should be allowed to finish so OmniRoute can checkpoint the latest changes back into `storage.sqlite`. +- The bundled Compose files already set a 40s stop grace period. If you run the image directly, keep `--stop-timeout 40` (or similar) so manual stops do not cut off shutdown cleanup. +- Set `CLOUDFLARED_BIN=/absolute/path/to/cloudflared` if you want OmniRoute to use an existing binary instead of downloading one. + +**Using Docker Compose with Caddy (HTTPS Auto-TLS):** + +OmniRoute can be securely exposed using Caddy's automatic SSL provisioning. Ensure your domain's DNS A record points to your server's IP. + +```yaml +services: + omniroute: + image: diegosouzapw/omniroute:latest + container_name: omniroute + restart: unless-stopped + volumes: + - omniroute-data:/app/data + environment: + - PORT=20128 + - NEXT_PUBLIC_BASE_URL=https://your-domain.com + + caddy: + image: caddy:latest + container_name: caddy + restart: unless-stopped + ports: + - "80:80" + - "443:443" + command: caddy reverse-proxy --from https://your-domain.com --to http://omniroute:20128 + +volumes: + omniroute-data: +``` + +| Image | Tag | Size | Description | +| ------------------------ | -------- | ------ | --------------------- | +| `diegosouzapw/omniroute` | `latest` | ~250MB | Latest stable release | +| `diegosouzapw/omniroute` | `3.7.8` | ~250MB | Current version | + +๐Ÿ“– **Full Docker documentation:** [`docs/DOCKER_GUIDE.md`](docs/DOCKER_GUIDE.md) โ€” Compose profiles, Caddy HTTPS, Cloudflare tunnels, and more. + +--- + +## ๐Ÿ“ฑ Multi-Platform โ€” Run Anywhere + +> OmniRoute runs on **Web**, **Desktop (Electron)**, **Android (Termux)**, and as a **Progressive Web App (PWA)**. + +| Platform | Install | Highlights | +| -------------- | -------------------------------------------- | -------------------------------------------------------------------------- | +| ๐Ÿ–ฅ๏ธ **Desktop** | `npm run electron:build` | Native window, system tray, auto-start, offline mode โ€” Windows/macOS/Linux | +| ๐Ÿ“ฑ **Android** | `pkg install nodejs-lts && npx -y omniroute` | ARM native, no root, 24/7 via Termux:Boot โ€” your phone is an AI server | +| ๐Ÿ“ฒ **PWA** | "Add to Home Screen" in browser | Fullscreen, offline page, service worker caching โ€” Android/iOS/Desktop | + +
+๐Ÿ–ฅ๏ธ Desktop App details + +- Native Electron app with system tray, auto-start, native notifications +- One-click install: NSIS (Windows), DMG (macOS), AppImage (Linux) +- Dev: `npm run electron:dev` ยท Build: `npm run electron:build` +- ๐Ÿ“– Full docs: [`electron/README.md`](electron/README.md) + +
+ +
+๐Ÿ“ฑ Android (Termux) details + +```bash +pkg update && pkg install nodejs-lts python build-essential git npx -y omniroute@latest - -# Or install globally -npm install -g omniroute && omniroute - -# Or via Docker -docker run -d -p 20128:20128 diegosouzapw/omniroute:3.8.0 ``` -โ†’ Open **http://localhost:20128** โ†’ login with `admin` / `CHANGEME` โ†’ connect your first provider via OAuth or API key. +Access from any device on the same network: `http://PHONE_IP:20128/v1` -**Point any OpenAI-compatible client at OmniRoute:** +- ๐Ÿ“– Full guide: [`docs/TERMUX_GUIDE.md`](docs/TERMUX_GUIDE.md) + +
+ +
+๐Ÿ“ฒ PWA details + +- **Android (Chrome):** โ‹ฎ โ†’ "Add to Home screen" +- **iOS (Safari):** Share โ†’ "Add to Home Screen" +- **Desktop (Chrome/Edge):** Install icon in address bar +- ๐Ÿ“– Full docs: [`docs/PWA_GUIDE.md`](docs/PWA_GUIDE.md) + +
+ +--- + +## ๐ŸŒ Bypass Geographic Blocks โ€” Use AI From Any Country + +> ๐Ÿ‡ท๐Ÿ‡บ ๐Ÿ‡จ๐Ÿ‡ณ ๐Ÿ‡ฎ๐Ÿ‡ท ๐Ÿ‡จ๐Ÿ‡บ ๐Ÿ‡น๐Ÿ‡ท **In Russia, China, Iran, or any blocked region?** OmniRoute's 3-level proxy system solves this completely. + +| Level | Badge | Configure In | Use Case | +| ------------------ | ----- | ------------------ | ------------------------------- | +| **Global** | ๐ŸŸข | Settings โ†’ Proxy | All traffic through one proxy | +| **Per-Provider** | ๐ŸŸก | Provider โ†’ Proxy | Only specific providers proxied | +| **Per-Connection** | ๐Ÿ”ต | Connection โ†’ Proxy | Each API key uses its own proxy | + +**What gets proxied:** API requests โœ… โ€ข OAuth flows โœ… โ€ข Connection tests โœ… โ€ข Token refresh โœ… โ€ข Model sync โœ… + +**Protocols:** HTTP/HTTPS, SOCKS5 (`ENABLE_SOCKS5_PROXY=true`), Authenticated proxies + +### ๐Ÿ†“ 1proxy โ€” Free Proxy Marketplace + +> Contributed by [@oyi77](https://github.com/oyi77) โ€” [#1847](https://github.com/diegosouzapw/OmniRoute/pull/1847) + +No proxy? Use the built-in **1proxy** integration for **hundreds of free, validated proxies** worldwide: + +- One-click sync (up to 500 proxies) โ€ข Quality scores (0-100) โ€ข Country filter โ€ข Auto-rotation (quality/random/sequential) โ€ข Auto-degradation โ€ข Circuit breaker + +### Anti-Detection + +- ๐Ÿ”’ **TLS Fingerprint Spoofing** โ€” browser-like TLS via `wreq-js` +- ๐Ÿ” **CLI Fingerprint Matching** โ€” matches native CLI binary signatures +- ๐Ÿ  **Proxy IP Preservation** โ€” stealth + IP masking simultaneously + +๐Ÿ“– **Full proxy documentation:** [`docs/PROXY_GUIDE.md`](docs/PROXY_GUIDE.md) + +--- + +--- + +## ๐Ÿ’ฐ Pricing at a Glance + +| Tier | Provider | Cost | Quota Reset | Best For | +| ------------------- | --------------------------- | ------------------------- | ---------------- | --------------------------------- | +| **๐Ÿ’ณ SUBSCRIPTION** | Claude Code (Pro) | $20/mo | 5h + weekly | Already subscribed | +| | Codex (Plus/Pro) | $20-200/mo | 5h + weekly | OpenAI users | +| | Gemini CLI | **FREE** | 180K/mo + 1K/day | Everyone! | +| | GitHub Copilot | $10-19/mo | Monthly | GitHub users | +| **๐Ÿ”‘ API KEY** | NVIDIA NIM | **FREE** (dev forever) | ~40 RPM | 70+ open models | +| | Cerebras | **FREE** (1M tok/day) | 60K TPM / 30 RPM | World's fastest | +| | Groq | **FREE** (30 RPM) | 14.4K RPD | Ultra-fast Llama/Gemma | +| | DeepSeek V3.2 | $0.27/$1.10 per 1M | None | Best price/quality reasoning | +| | xAI Grok-4 Fast | **$0.20/$0.50 per 1M** ๐Ÿ†• | None | Fastest + tool calling, ultralow | +| | xAI Grok-4 (standard) | $0.20/$1.50 per 1M ๐Ÿ†• | None | Reasoning flagship from xAI | +| | Mistral | Free trial + paid | Rate limited | European AI | +| | OpenRouter | Pay-per-use | None | 100+ models aggr. | +| | AgentRouter ๐Ÿ†• | Pay-per-use | None | $200 free credits at signup | +| **๐Ÿ’ฐ CHEAP** | GLM-5 (via Z.AI) ๐Ÿ†• | $0.5/1M | Daily 10AM | 128K output, newest flagship | +| | GLM-4.7 | $0.6/1M | Daily 10AM | Budget backup | +| | MiniMax M2.5 ๐Ÿ†• | $0.3/1M input | 5-hour rolling | Reasoning + agentic tasks | +| | MiniMax M2.1 | $0.2/1M | 5-hour rolling | Cheapest option | +| | Kimi K2.5 (Moonshot API) ๐Ÿ†• | Pay-per-use | None | Direct Moonshot API access | +| | Kimi K2 | $9/mo flat | 10M tokens/mo | Predictable cost | +| **๐Ÿ†“ FREE** | Qoder | **$0** | Unlimited | 5 models unlimited | +| | Qwen | **$0** | Unlimited | 4 models unlimited | +| | Kiro | **$0** | Unlimited | Claude Sonnet/Haiku (AWS Builder) | +| | LongCat Flash-Lite ๐Ÿ†• | **$0** (50M tok/day ๐Ÿ”ฅ) | 1 RPS | Largest free quota on Earth | +| | Pollinations AI ๐Ÿ†• | **$0** (no key needed) | 1 req/15s | GPT-5, Claude, DeepSeek, Llama 4 | +| | Cloudflare Workers AI ๐Ÿ†• | **$0** (10K Neurons/day) | ~150 resp/day | 50+ models, global edge | +| | Scaleway AI ๐Ÿ†• | **$0** (1M tokens total) | Rate limited | EU/GDPR, Qwen3 235B, Llama 70B | + +> ๐Ÿ†• **New models added (Mar 2026):** Grok-4 Fast family at $0.20/$0.50/M (benchmarked at 1143ms โ€” 30% faster than Gemini 2.5 Flash), GLM-5 via Z.AI with 128K output, MiniMax M2.5 reasoning, DeepSeek V3.2 updated pricing, Kimi K2.5 via Moonshot direct API. + +**๐Ÿ’ก See the full [$0 Free Stack (11 providers)](#-free-models--11-providers-0-forever) below.** + +> ๐Ÿ’ก **Understanding Dashboard Costs:** +> +> The "cost" displayed in the Usage Analytics page is **for tracking and comparison purposes only**. +> OmniRoute itself **never charges you anything** โ€” it's free, open-source software running on your machine. +> If your dashboard shows "$290 total cost" while using free models, that's how much you **saved** compared to paid API pricing. +> Think of it as a **savings tracker**, not a bill. + +--- + +## ๐Ÿ†“ Free Models โ€” 11 Providers, $0 Forever + +> Combine all free providers into one unbreakable combo โ€” OmniRoute auto-routes between them when quota runs out. + +| Provider | Prefix | Free Models | Quota | +| ----------------- | ----------- | ------------------------------------------------------------- | -------------------- | +| **Kiro** | `kr/` | Claude Sonnet 4.5, Haiku 4.5, Opus 4.6 | 50 CREDITS per month | +| **Qoder** | `if/` | kimi-k2-thinking, qwen3-coder-plus, deepseek-r1, minimax-m2.1 | โ™พ๏ธ Unlimited | +| **Qwen** | `qw/` | qwen3-coder-plus, qwen3-coder-flash, qwen3-coder-next | โ™พ๏ธ Unlimited | +| **Pollinations** | `pol/` | GPT-5, Claude, Gemini, DeepSeek, Llama 4, Mistral | No key needed | +| **LongCat** | `lc/` | LongCat-Flash-Lite | 50M tokens/day ๐Ÿ”ฅ | +| **Gemini CLI** | `gc/` | gemini-3-flash, gemini-2.5-pro | 180K tok/mo | +| **Cloudflare AI** | `cf/` | 50+ models (Llama, Gemma, Mistral, Whisper) | 10K Neurons/day | +| **Groq** | `groq/` | Llama 3.3 70B, Qwen3 32B, Kimi K2 | 14.4K RPD | +| **NVIDIA NIM** | `nvidia/` | 129 models (DeepSeek, Llama, GLM, Kimi) | ~40 RPM | +| **Cerebras** | `cerebras/` | Qwen3 235B, GPT-OSS 120B, Llama 3.1 | 1M tok/day | +| **Scaleway** | `scw/` | Qwen3 235B, Llama 70B, DeepSeek V3 | 1M tokens (EU) | + +
+๐Ÿ“– 25+ more free providers โ€” Groq, Cerebras, Mistral, GitHub Models, OpenRouter, and more + +**Also free (API Key required):** +Mistral (1B tok/month) ยท OpenRouter (35+ `:free` models) ยท GitHub Models (GPT-5, 45+ models) ยท +Cohere (1K calls/month) ยท Z.AI/GLM (permanent free Flash models) ยท SiliconFlow (1K RPM, 50K TPM) ยท +Kilo Code (~200 req/hr auto-router) ยท HuggingFace ($0.10/mo credits) ยท Ollama Cloud (400+ models) ยท +LLM7.io (30+ models) ยท Kluster AI ยท IBM watsonx (300K tok/month) ยท OpenCode Zen ยท Vercel AI Gateway ($5/mo) + +**Trial credits (one-time):** +Baseten ($30) ยท NLP Cloud ($15) ยท AI21 ($10) ยท Upstage ($10) ยท SambaNova ($5) ยท Modal ($5/mo) ยท +Fireworks ($1) ยท Nebius ($1) ยท Inference.net ($1 + $25 survey) ยท Hyperbolic ($1) ยท Novita ($0.50) + +**China-based (free tiers):** +ModelScope ยท Tencent Hunyuan ยท Volcengine ยท ChatAnywhere ยท InternAI ยท Bigmodel + +**Combined capacity: ~31,000+ RPD ยท ~32B+ tokens/month ยท 500+ models ยท $0** + +
+ +๐Ÿ“– **Complete free provider directory:** [`docs/FREE_TIERS.md`](docs/FREE_TIERS.md) โ€” 25+ providers, quotas, base URLs, model tables, and OmniRoute combo setup. + +--- + +## ๐ŸŽ™๏ธ Free Transcription Combo + +> Transcribe any audio/video for **$0** โ€” Deepgram leads with $200 free, AssemblyAI $50 fallback, Groq Whisper as unlimited emergency backup. + +| Provider | Free Credits | Best Model | Rate Limit | +| ----------------- | ---------------------- | -------------------------------------------- | ---------------------------- | +| ๐ŸŸข **Deepgram** | **$200 free** (signup) | `nova-3` โ€” best accuracy, 30+ languages | No RPM limit on free credits | +| ๐Ÿ”ต **AssemblyAI** | **$50 free** (signup) | `universal-3-pro` โ€” chapters, sentiment, PII | No RPM limit on free credits | +| ๐Ÿ”ด **Groq** | **Free forever** | `whisper-large-v3` โ€” OpenAI Whisper | 30 RPM (rate limited) | + +--- + +**Suggested combo in `/dashboard/combos`:** + +``` +Name: free-transcription +Strategy: Priority +Nodes: + [1] deepgram/nova-3 โ†’ uses $200 free first + [2] assemblyai/universal-3-pro โ†’ fallback when Deepgram credits run out + [3] groq/whisper-large-v3 โ†’ free forever, emergency fallback +``` + +Then in `/dashboard/media` โ†’ **Transcription** tab: upload any audio or video file โ†’ select your combo endpoint โ†’ get transcription in supported formats. + +## ๐Ÿ’ก Key Features + +> **4,690+ automated tests** across 517 test files. Not just a relay โ€” a full operational platform. + +| Feature | Why It Matters | +| ---------------------------------------------------------------------------------------------------- | -------------------------------- | +| ๐Ÿง  **Smart 4-Tier Fallback** โ€” Subscription โ†’ API โ†’ Cheap โ†’ Free | Never stop coding, zero downtime | +| ๐Ÿ”„ **Format Translation** โ€” OpenAI โ†” Claude โ†” Gemini โ†” Responses API | Works with ANY CLI tool | +| ๐Ÿ—œ๏ธ **Prompt Compression** โ€” 7 options including Caveman, RTK, and stacked pipelines | Save 15-95% eligible tokens | +| ๐Ÿค– **MCP Server** โ€” 37 tools, 3 transports (stdio/SSE/HTTP), 10 scopes | IDE/agent tool integration | +| ๐Ÿ›ก๏ธ **Resilience Engine** โ€” circuit breakers, cooldowns, TLS spoofing, anti-thundering herd | Auto-recovery from any failure | +| ๐ŸŽต **10 Multi-Modal APIs** โ€” chat, embed, images, video, music, TTS, STT, moderation, rerank, search | One endpoint for everything | +| ๐ŸŒ **3-Level Proxy** โ€” global, per-provider, per-key + 1proxy free marketplace | Access AI from any country | +| ๐Ÿ“Š **Full Observability** โ€” unified logs, p50/p95/p99 telemetry, cost tracking, budget controls | Know exactly what's happening | + +
+๐Ÿ“‹ Complete feature list โ€” 30+ capabilities + +**Routing & Intelligence** + +- 13 balancing strategies (priority, weighted, round-robin, P2C, cost-optimized, context-relay...) +- Task-aware smart routing (coding/vision/analysis) ยท Context relay session handoffs +- Thinking budget controls (passthrough/auto/custom) ยท Wildcard routing ยท System prompt injection + +**Translation & Compatibility** + +- Auto token refresh (OAuth PKCE for 8 providers) ยท Multi-account round-robin +- Responses API โ€” full `/v1/responses` for Codex ยท Batch API with Files API +- OpenAPI 3.0 live spec + Try-It UI + +**Protocols** + +- A2A Server โ€” JSON-RPC 2.0, SSE streaming, task lifecycle, skills +- ACP โ€” CLI agent discovery (14 agents + custom) + +**Platform** + +- Desktop (Electron) ยท Android (Termux) ยท PWA ยท Docker (AMD64 + ARM64) +- Cloudflare / Tailscale / ngrok tunnels ยท 40+ languages with RTL +- Semantic + signature cache (two-tier) ยท Request idempotency + deduplication + +**Observability** + +- Health dashboard โ€” uptime, breakers, cache, lockouts +- Evaluation framework โ€” golden set testing ยท Webhooks ยท Compliance audit + +**v3.6+ Highlights:** +V1 WebSocket Bridge ยท Sync Tokens & Config Bundle ยท GLM Thinking (glmt) ยท Hybrid Token Counting ยท +Safe Outbound Fetch ยท Wait For Cooldown ยท Runtime Env Validation ยท Vision Bridge ยท +Grok-4 Fast ยท GLM-5 via Z.AI ยท MiniMax M2.5 ยท toolCalling flag ยท +Multilingual Intent Detection ยท Benchmark-Driven Fallbacks ยท Request Deduplication + +**Architecture Examples:** + +```txt +Combo: "my-coding-stack" Format Translation: + 1. cc/claude-opus-4-7 CLI โ†’ OpenAI format + 2. nvidia/llama-3.3-70b OmniRoute โ†’ translates + 3. glm/glm-4.7 Provider โ†’ native format + 4. if/kimi-k2-thinking +``` + +๐Ÿ“– [MCP Server README](open-sse/mcp-server/README.md) ยท [A2A Server README](src/lib/a2a/README.md) ยท [Resilience Guide](docs/RESILIENCE_GUIDE.md) ยท [Features Gallery](docs/FEATURES.md) + +
+ +--- + +## ๐ŸŽฏ Use Cases โ€” Ready-Made Combo Playbooks + +### Case 0: "I want zero-config, auto-routing NOW" + +**Problem:** Don't want to create combos manually. Just want AI routing to work immediately. ```bash -export OPENAI_BASE_URL=http://localhost:20128/v1 -export OPENAI_API_KEY=or_ +# No combo creation needed! Use auto/ prefix directly: +model: "auto" # Default LKGP routing across all connected providers +model: "auto/coding" # Quality-first weights for code generation +model: "auto/fast" # Low-latency routing (fastest provider first) +model: "auto/cheap" # Cost-optimized (cheapest per token) +model: "auto/offline" # High availability (most quota available) +model: "auto/smart" # Best discovery (10% exploration rate) ``` -That's it. Cursor, Cline, Codex, Continue, Aider, and any SDK now work. โ†’ Detailed setup: [`docs/guides/SETUP_GUIDE.md`](docs/guides/SETUP_GUIDE.md) +**How it works:** + +1. Add providers in Dashboard โ†’ Providers (OAuth or API key) +2. Use `auto/` prefix in any AI tool โ€” **no combo creation needed** +3. OmniRoute dynamically builds a virtual combo from your active connections +4. Routes using LKGP (Last Known Good Provider) + 6-factor scoring +5. Session stickiness ensures consistent provider selection + +**Dashboard indicator:** A blue banner at the top shows "Auto-Routing Active" with a link to `/dashboard/combos` for configuration. + +**Monthly cost:** $0 (uses your existing free providers) or whatever your connected providers cost --- -## ๐ŸŒŸ What's new in v3.8.0 +### Case 1: "I have a Claude Pro subscription" -- ๐Ÿค– **Auto-Combo zero-config routing** โ€” just use `auto/coding`, `auto/cheap`, `auto/fast`, `auto/offline`, `auto/smart`, `auto/lkgp` as model IDs -- ๐ŸŽฏ **Manifest-aware tier routing W1-W4** โ€” automatic tier prioritization -- ๐Ÿ†• **Command Code provider** + **Z.AI quota labels** + **KIE video expansion** -- ๐Ÿ” **Windsurf + Devin CLI + GitLab Duo OAuth** flows -- ๐Ÿ†“ **9 new free providers**: LLM7, Lepton, Kluster, UncloseAI, BazaarLink, Completions, Enally, FreeTheAi, AgentRouter ($200 credits) -- ๐Ÿฉบ **Model Cooldowns dashboard** with manual re-enable -- ๐ŸŽจ **Cursor full OpenAI parity** (tools, streaming, sessions) -- ๐Ÿ“Œ **Per-session sticky routing** for Codex -- ๐Ÿ”Š **Inworld TTS** enhancements -- ๐Ÿง  **Reasoning Replay Cache** โ€” fixes 400s on DeepSeek V4, Kimi K2, Qwen-Thinking, GLM -- ๐Ÿ”„ **Reset-aware routing** strategy (14th strategy) -- ๐Ÿ› ๏ธ **20+ new CLI commands** (`omniroute setup/doctor/providers/combos`) +**Problem:** Quota expires unused, rate limits during heavy coding sessions. -โ†’ Full changelog: [`CHANGELOG.md`](CHANGELOG.md) +``` +Combo: "maximize-claude" + 1. cc/claude-opus-4-7 (use subscription fully) + 2. glm/glm-5.1 (cheap backup when quota out โ€” $0.5/1M) + 3. kr/claude-sonnet-4.5 (free emergency fallback via Kiro) + +Compression: standard (caveman) โ€” saves 30% tokens = stretch quota further +Monthly cost: $20 (subscription) + ~$3 (backup) = $23 total +vs. $20 + hitting limits + lost productivity = frustration +``` + +### Case 2: "I want $0 forever" + +**Problem:** Can't afford subscriptions, need reliable AI for coding. + +``` +Combo: "free-forever" + 1. kr/claude-sonnet-4.5 (Claude 4.5 free unlimited via Kiro) + 2. if/kimi-k2-thinking (reasoning model free via Qoder) + 3. pol/gpt-5 (GPT-5 free via Pollinations โ€” no key) + 4. lc/longcat-flash-lite (50M tokens/day free backup) + +Compression: aggressive โ€” saves 50% tokens = double your free quota +Monthly cost: $0 +Quality: Production-ready models + 50% token savings +``` + +### Case 3: "I need 24/7 coding, no interruptions" + +**Problem:** Deadlines, can't afford any downtime. + +``` +Combo: "always-on" + 1. cc/claude-opus-4-7 (best quality โ€” subscription) + 2. cx/gpt-5.5 (second subscription โ€” OpenAI) + 3. glm/glm-5.1 (cheap, resets daily โ€” $0.5/1M) + 4. minimax/MiniMax-M2.5 (cheapest paid โ€” $0.3/1M) + 5. kr/claude-sonnet-4.5 (free unlimited โ€” never fails) + +Compression: lite โ€” saves 15% tokens passively, zero risk +Result: 5 layers of fallback = zero downtime +Monthly cost: $20-200 (subscriptions) + $5-10 (backup) +``` + +### Case 4: "I'm in a blocked region (Russia, China, Iran...)" + +**Problem:** AI providers block my country, VPNs are slow. + +``` +Combo: "unblocked-ai" + 1. kr/claude-sonnet-4.5 (free via Kiro + proxy) + 2. pol/deepseek-r1 (Pollinations โ€” no geo-block) + 3. groq/llama-3.3-70b (Groq + proxy) + +Proxy: Global proxy set in Settings โ†’ or per-provider proxy override +Result: Access ALL providers from ANY country +Monthly cost: $0 (free providers) + $0 (1proxy free marketplace) +``` + +### Case 5: "I want maximum token savings" + +**Problem:** Token costs are eating my budget, need to squeeze every token. + +``` +Combo: "ultra-saver" + 1. cc/claude-opus-4-7 (subscription โ€” best quality) + 2. glm/glm-5.1 (cheap backup) + +Compression: ultra โ€” saves 75% tokens +Result: 10K token prompt โ†’ 2.5K tokens sent +Montly savings: ~$150-300/month in token costs for heavy users +``` + +## ๐Ÿงช Evaluations (Evals) + +OmniRoute includes a built-in evaluation framework to test LLM response quality against a golden set. Access it via **Analytics โ†’ Evals** in the dashboard. + +### Built-in Golden Set + +The pre-loaded "OmniRoute Golden Set" contains test cases for: + +- Greetings, math, geography, code generation +- JSON format compliance, translation, markdown generation +- Safety refusal (harmful content), counting, boolean logic + +### Evaluation Strategies + +| Strategy | Description | Example | +| ---------- | ------------------------------------------------ | -------------------------------- | +| `exact` | Output must match exactly | `"4"` | +| `contains` | Output must contain substring (case-insensitive) | `"Paris"` | +| `regex` | Output must match regex pattern | `"1.*2.*3"` | +| `custom` | Custom JS function returns true/false | `(output) => output.length > 10` | --- -## ๐ŸŽฏ Why OmniRoute Wins +## ๐Ÿ“– Setup Guide -| | OmniRoute v3.8 | LiteLLM | OpenRouter | -| ------------------ | ------------------- | ---------------- | ------------- | -| Providers | **177+** | ~50 | ~50 | -| Free providers | **11** | 0 | 0 | -| OAuth providers | **14** | 0 | 1 | -| Routing strategies | **14** | 3 | 1 | -| Auto routing | โœ… 9-factor scoring | โŒ | โŒ | -| Prompt compression | โœ… RTK + Caveman | โŒ | โŒ | -| MCP server | โœ… 37 tools | โŒ | โŒ | -| A2A protocol | โœ… v0.3 + 5 skills | โŒ | โŒ | -| Desktop app | โœ… Electron 41 | โŒ | โŒ | -| PWA | โœ… | โŒ | โŒ | -| Self-hosted | โœ… MIT | Limited | โŒ (cloud) | -| Pricing | **$0 forever** | OSS / Cloud paid | 10% fee + API | +### Connect Your Coding Tool -โ†’ Detailed comparison: [`docs/guides/FEATURES.md`](docs/guides/FEATURES.md) +Point any OpenAI-compatible tool to OmniRoute: ---- +```txt +Base URL: http://localhost:20128/v1 +API Key: [from Dashboard โ†’ Endpoints] +``` -## ๐Ÿ› ๏ธ Compatible CLI Tools (17+) +| Tool | Config Location | +| --------------- | ----------------------------------------------------------------------------------------- | +| **Claude Code** | `claude mcp add-server omniroute --type http --url http://localhost:20128/api/mcp/stream` | +| **Codex CLI** | `OPENAI_BASE_URL=http://localhost:20128/v1 OPENAI_API_KEY=your-key codex` | +| **Cursor** | Settings โ†’ Models โ†’ Add Model โ†’ Override Base URL | +| **Cline** | Extension settings โ†’ Custom API Base URL | +| **OpenClaw** | `OPENAI_BASE_URL=http://localhost:20128/v1 openclaw` | +| **Gemini CLI** | Uses native OAuth via OmniRoute โ€” connect in Providers | -All work out-of-the-box once you point `OPENAI_BASE_URL` at OmniRoute: - -**Claude family:** Claude Code ยท Cline ยท Continue ยท Kilo Code ยท Kimi Coding -**OpenAI family:** Codex CLI ยท Cursor ยท Aider ยท OpenClaw ยท Droid ยท AMP -**Google family:** Gemini CLI ยท Antigravity ยท Jules -**Others:** Windsurf ยท GitLab Duo ยท Devin CLI ยท Hermes ยท Amazon Q ยท Kiro ยท Qoder ยท Custom - -โ†’ Full setup: [`docs/reference/CLI-TOOLS.md`](docs/reference/CLI-TOOLS.md) - ---- - -## ๐ŸŒ Providers (177+) - -### ๐Ÿ†“ Free providers (11 โ€” no API key or unlimited tier) - -| Provider | Highlight | -| --------------------------------------------------- | --------------------------------------- | -| **Kiro AI** | 50 credits/month (Claude Sonnet/Haiku) | -| **Qoder AI** | Unlimited (Kimi-K2, Qwen3, DeepSeek-R1) | -| **Gemini CLI** | 180K tokens/month | -| **Amazon Q** | AWS Builder ID OAuth | -| **LongCat** | 50M tokens/day | -| **Pollinations** | No API key, GPT-5 + Claude | -| **AgentRouter** | $200 free credits | -| **LLM7** ยท **Lepton** ยท **Kluster** ยท **UncloseAI** | New v3.8 free tiers | - -โš ๏ธ Qwen Code OAuth was **discontinued on 2026-04-15** (use API key with `alicode` provider instead). - -โ†’ Curated guide: [`docs/reference/FREE_TIERS.md`](docs/reference/FREE_TIERS.md) ยท Full catalog: [`docs/reference/PROVIDER_REFERENCE.md`](docs/reference/PROVIDER_REFERENCE.md) (auto-generated) - -### ๐Ÿ” OAuth providers (14) - -Claude Code ยท Codex ยท GitHub Copilot ยท Cursor ยท Antigravity ยท Gemini ยท Kimi Coding ยท Kilo Code ยท Cline ยท Qwen ยท Kiro ยท Qoder ยท Windsurf ยท GitLab Duo - -### ๐Ÿ”‘ API key providers (~123) - -OpenAI ยท Anthropic ยท Google ยท Mistral ยท Cohere ยท DeepSeek ยท Groq ยท Together ยท Fireworks ยท Cerebras ยท SambaNova ยท NVIDIA NIM ยท Bedrock ยท Vertex ยท Azure ยท Cloudflare AI ยท 100+ more. - -### ๐Ÿ  Self-hosted (10) - -Ollama ยท LM Studio ยท vLLM ยท Llamafile ยท Lemonade ยท Petals ยท Triton ยท Docker Model Runner ยท Xinference ยท Oobabooga - ---- - -## ๐Ÿค– Auto-Combo โ€” Zero-Config Routing - -Just use `auto/` as model ID. No combo setup needed. +### Protocols (MCP + A2A) ```bash -# 6 variants + plain `auto`: -auto/coding # โ†’ optimized for coding tasks -auto/cheap # โ†’ minimize cost -auto/fast # โ†’ minimize latency -auto/offline # โ†’ prefer local providers -auto/smart # โ†’ prefer top-tier models -auto/lkgp # โ†’ Last-Known-Good-Path (sticky) -auto # โ†’ balanced default +# MCP (stdio transport) +omniroute --mcp + +# A2A (JSON-RPC 2.0) +curl http://localhost:20128/.well-known/agent.json ``` -**How it picks:** 9-factor scoring (health ยท quota ยท cost ยท latency ยท taskFit ยท stability ยท tierPriority ยท tierAffinity ยท specificityMatch) over a virtual candidate pool built from all enabled providers. +### Key Environment Variables -โ†’ Full guide: [`docs/routing/AUTO-COMBO.md`](docs/routing/AUTO-COMBO.md) +| Variable | Default | Purpose | +| -------------------- | -------------- | ----------------------------------------- | +| `PORT` | `20128` | API and dashboard port | +| `DASHBOARD_PORT` | โ€” | Separate dashboard port (split-port mode) | +| `REQUIRE_API_KEY` | `false` | Require API key for all requests | +| `DATA_DIR` | `~/.omniroute` | Database and config storage | +| `REQUEST_TIMEOUT_MS` | `600000` | Upstream response timeout | + +
+๐Ÿ“– Full Setup Guide โ€” All CLI tools, protocols, and environment variables + +๐Ÿ“– **Complete documentation:** + +- [User Guide](docs/USER_GUIDE.md) โ€” Providers, combos, CLI integration +- [API Reference](docs/API_REFERENCE.md) โ€” All endpoints with examples +- [MCP Server](open-sse/mcp-server/README.md) โ€” 37 tools, IDE configs +- [A2A Server](src/lib/a2a/README.md) โ€” JSON-RPC, skills, streaming +- [Environment Config](docs/ENVIRONMENT.md) โ€” Complete `.env` reference +- [VM Deployment](docs/VM_DEPLOYMENT_GUIDE.md) โ€” VM + nginx + Cloudflare + +
--- -## ๐Ÿ—œ๏ธ Prompt Compression โ€” Save 15-95% Tokens +## โ“ Frequently Asked Questions -Two engines, stackable: +
+๐Ÿ“Š Why does my dashboard show high costs if I'm using free models? -- **Caveman** โ€” natural-language condensation (filler removal, hedging, repeated context). 30+ regex rules per language pack (en, es, pt-BR, de, fr, ja). -- **RTK** โ€” terminal/shell/git/test output. 49 declarative filters. +The dashboard tracks your token usage and displays **estimated costs** as if you were using paid APIs directly. This is **not actual billing** โ€” it's a reference to show how much you're saving. -**Modes:** `off` ยท `lite` ยท `standard` ยท `aggressive` ยท `ultra` ยท `rtk` ยท `stacked` (RTKโ†’Caveman, max savings). +**Example:** -โ†’ [`docs/compression/COMPRESSION_GUIDE.md`](docs/compression/COMPRESSION_GUIDE.md) ยท [`docs/compression/RTK_COMPRESSION.md`](docs/compression/RTK_COMPRESSION.md) ยท [`docs/compression/COMPRESSION_LANGUAGE_PACKS.md`](docs/compression/COMPRESSION_LANGUAGE_PACKS.md) +- **Dashboard shows:** "$290 total cost" +- **Reality:** You're using Kiro + Qoder (FREE unlimited) +- **Your actual cost:** **$0.00** +- **What $290 means:** Amount you **saved** by using free models instead of paid APIs! + +The cost display is a "savings tracker" to help you understand your usage patterns and optimization opportunities. + +
+ +
+๐Ÿ’ณ Will I be charged by OmniRoute? + +**No.** OmniRoute is free, open-source software that runs on your own computer. It never charges you anything. + +**You only pay:** + +- โœ… **Subscription providers** (Claude Code $20/mo, Codex $20-200/mo) โ†’ Pay them directly on their websites +- โœ… **API key providers** (DeepSeek, xAI, etc.) โ†’ Pay them directly, OmniRoute just routes your requests +- โŒ **OmniRoute itself** โ†’ **Never charges anything, ever** + +OmniRoute is a local proxy/router. It doesn't have your credit card, can't send invoices, and has no billing system. It's completely free software. + +
+ +
+๐Ÿ†“ Are FREE providers really unlimited? + +**Yes!** The current FREE providers are genuinely free with **no hidden charges**: + +- **Kiro AI**: Free unlimited Claude Sonnet/Haiku via AWS Builder ID / Google / GitHub OAuth +- **Qoder**: Free unlimited kimi-k2-thinking, qwen3-coder-plus, deepseek-r1 via PAT token +- **Pollinations AI**: No API key needed โ€” GPT-5, Claude, DeepSeek, Llama 4 +- **LongCat Flash-Lite**: 50M tokens/day โ€” largest free quota available +- **Cloudflare Workers AI**: 10K Neurons/day โ€” 50+ models at the edge + +OmniRoute just routes your requests to them โ€” there's no "catch" or future billing. + +
+ +
+๐Ÿ’ฐ How do I minimize my actual AI costs? + +**Free-First Strategy:** + +1. **Start with 100% free combo:** + + ``` + 1. kr/claude-sonnet-4.5 (Kiro โ€” unlimited free) + 2. if/kimi-k2-thinking (Qoder โ€” unlimited free) + 3. pol/gpt-5 (Pollinations โ€” no key needed) + ``` + + **Cost: $0/month** + +2. **Enable Prompt Compression** โ€” even `lite` mode saves ~15% passively + +3. **Add cheap backup** only if you need it: + + ``` + 4. glm/glm-5.1 ($0.5/1M tokens) + ``` + + **Additional cost: Only pay for what you actually use** + +4. **Use subscription providers last** โ€” only if you already have them. OmniRoute helps maximize their value through quota tracking. + +**Result:** Most users can operate at **$0/month** using only free tiers! + +
+ +
+๐Ÿ—œ๏ธ Will compression affect response quality? + +**No.** Compression only affects the **input** (your prompt), not the model's response. Each mode has been designed to preserve technical accuracy: + +- **Lite** (~15%): Only whitespace/formatting โ€” zero semantic change +- **Standard** (~30%): Removes filler words ("please", "I think", "basically") โ€” same meaning +- **Aggressive** (~50%): Summarizes old messages + compresses tool outputs โ€” core context preserved +- **Ultra** (~75%): Heuristic pruning โ€” use only when token budget is critical + +Code blocks, URLs, JSON, and structured data are **always protected** from compression via the preservation engine. + +
+ +
+๐ŸŒ Does OmniRoute work in countries where AI is blocked? + +**Yes!** OmniRoute has a 3-level proxy system: + +1. **Global proxy** โ€” all requests go through your proxy +2. **Per-provider proxy** โ€” different proxy per provider +3. **Per-API-key proxy** โ€” different proxy per key + +Plus the **1proxy free marketplace** for community-shared proxies. Users in Russia, China, Iran, and other restricted regions can access all 160+ providers through OmniRoute's proxy infrastructure. + +See the [Proxy Guide](docs/PROXY_GUIDE.md) for setup instructions. + +
--- -## ๐ŸŒ Bypass Geographic Blocks +## ๐Ÿ› Troubleshooting -For users in **Russia, China, Iran, Cuba, Turkey** and other regions: +| Problem | Quick Fix | +| --------------------------------------------- | ------------------------------------------------------------------------------------ | +| **"Language model did not provide messages"** | Provider quota exhausted โ†’ check quota tracker, use combo fallback | +| **Rate limiting (429)** | Add fallback combo: `cc/claude โ†’ glm/glm-4.7 โ†’ if/kimi-k2-thinking` | +| **OAuth token expired** | Auto-refreshed by OmniRoute. If stuck: delete + re-auth in Providers | +| **`unsupported_country_region_territory`** | Configure proxy in Settings โ†’ Proxy (see [Proxy Guide](docs/PROXY_GUIDE.md)) | +| **Docker SQLite locks** | Use `--stop-timeout 40` for clean WAL checkpoint on shutdown | +| **Node.js runtime errors** | Use Node.js `>=20.20.2 <21`, `>=22.22.2 <23`, or `>=24.0.0 <25` (24 LTS recommended) | +| **`system-info` for bug reports** | Run `npm run system-info` and attach `system-info.txt` to your issue | -- **4-level outbound proxy** โ€” account / provider / combo / global scopes -- **1proxy free marketplace** โ€” auto-syncs working HTTP/SOCKS5 proxies -- **Anti-detection** โ€” TLS fingerprinting (JA3/JA4), CCH headshakes, header sanitization -- **Public tunnels** โ€” Cloudflare (Quick or Named), ngrok, Tailscale Funnel for OAuth callbacks +๐Ÿ“– **Full troubleshooting guide:** [`docs/TROUBLESHOOTING.md`](docs/TROUBLESHOOTING.md) -โ†’ [`docs/ops/PROXY_GUIDE.md`](docs/ops/PROXY_GUIDE.md) ยท [`docs/ops/TUNNELS_GUIDE.md`](docs/ops/TUNNELS_GUIDE.md) ยท [`docs/security/STEALTH_GUIDE.md`](docs/security/STEALTH_GUIDE.md) +## ๐Ÿ› ๏ธ Tech Stack + +
+Click to expand tech stack details + +- **Runtime**: Node.js 20.20.2+, 22.22.2+, or 24.x LTS (24 LTS recommended) +- **Language**: TypeScript 5.9 โ€” **100% TypeScript** across `src/` and `open-sse/` (zero `any` in core modules since v2.0) +- **Framework**: Next.js 16 + React 19 + Tailwind CSS 4 +- **Database**: better-sqlite3 (SQLite) + LowDB (JSON legacy) โ€” domain state, proxy logs, MCP audit, routing decisions, memory, skills +- **Schemas**: Zod (MCP tool I/O validation, API contracts) +- **Protocols**: MCP (stdio/HTTP) + A2A v0.3 (JSON-RPC 2.0 + SSE) +- **Streaming**: Server-Sent Events (SSE) + WebSocket bridge (`/v1/ws`) +- **Auth**: OAuth 2.0 (PKCE) + JWT + API Keys + MCP Scoped Authorization +- **Testing**: Node.js test runner + Vitest (**4,690+ test cases** across 517 files โ€” unit, integration, E2E, security, ecosystem) +- **Platforms**: Desktop (Electron), Android (Termux), PWA (any browser) +- **CI/CD**: GitHub Actions (auto npm publish + Docker Hub on release) +- **Website**: [omniroute.online](https://omniroute.online) +- **Package**: [npmjs.com/package/omniroute](https://www.npmjs.com/package/omniroute) +- **Docker**: [hub.docker.com/r/diegosouzapw/omniroute](https://hub.docker.com/r/diegosouzapw/omniroute) +- **Resilience**: Circuit breaker, exponential backoff, anti-thundering herd, TLS spoofing, auto-combo self-healing + +
--- -## ๐Ÿ“ฑ Multi-Platform +## ๐Ÿ“– Documentation -| Platform | Install | Doc | -| --------------------------- | -------------------------------------------------------- | ------------------------------------------------------------------- | -| **CLI / Server** | `npm install -g omniroute` | [`SETUP_GUIDE.md`](docs/guides/SETUP_GUIDE.md) | -| **Desktop (Win/Mac/Linux)** | Electron installer from GitHub Releases | [`ELECTRON_GUIDE.md`](docs/guides/ELECTRON_GUIDE.md) | -| **PWA** | Install from any modern browser | [`PWA_GUIDE.md`](docs/guides/PWA_GUIDE.md) | -| **Android (Termux)** | `pkg install nodejs-lts && npm i -g omniroute` | [`TERMUX_GUIDE.md`](docs/guides/TERMUX_GUIDE.md) | -| **Docker** | `docker compose up` (base/cli/host/cliproxyapi profiles) | [`DOCKER_GUIDE.md`](docs/guides/DOCKER_GUIDE.md) | -| **VM / VPS** | Generic Ubuntu/Debian + nginx + systemd | [`VM_DEPLOYMENT_GUIDE.md`](docs/ops/VM_DEPLOYMENT_GUIDE.md) | -| **Fly.io** | `fly deploy` | [`FLY_IO_DEPLOYMENT_GUIDE.md`](docs/ops/FLY_IO_DEPLOYMENT_GUIDE.md) | +### ๐Ÿ“˜ Getting Started + +| Document | Description | +| ------------------------------------- | ----------------------------------------------------------------------------- | +| [User Guide](docs/USER_GUIDE.md) | Providers, combos, CLI integration, deployment | +| [Setup Guide](docs/SETUP_GUIDE.md) | Full install methods, CLI tool configs, protocol setup, timeout tuning | +| [CLI Tools Guide](docs/CLI-TOOLS.md) | Per-tool setup for Claude Code, Codex, Cursor, Cline, OpenClaw, Kilo, Copilot | +| [Quick Start](README.md#-quick-start) | 3-step install โ†’ connect โ†’ configure | + +### ๐Ÿ”ง Operations & Deployment + +| Document | Description | +| ---------------------------------------------------- | -------------------------------------------------------------- | +| [Docker Guide](docs/DOCKER_GUIDE.md) | Docker run, Compose profiles, Caddy HTTPS, tunnels, image tags | +| [VM Deployment](docs/VM_DEPLOYMENT_GUIDE.md) | Complete guide: VM + nginx + Cloudflare setup | +| [Fly.io Deployment](docs/FLY_IO_DEPLOYMENT_GUIDE.md) | Deploy to Fly.io with persistent storage | +| [Termux Guide](docs/TERMUX_GUIDE.md) | Run OmniRoute on Android via Termux | +| [PWA Guide](docs/PWA_GUIDE.md) | Progressive Web App install, caching, architecture | +| [Uninstall Guide](docs/UNINSTALL.md) | Clean removal for all install methods | +| [Environment Config](docs/ENVIRONMENT.md) | Complete `.env` variables and references | + +### ๐Ÿง  Features & Architecture + +| Document | Description | +| ---------------------------------------------------------------- | ----------------------------------------------------------------------------- | +| [Architecture](docs/ARCHITECTURE.md) | System architecture, data flow, and internals | +| [Compression Guide](docs/COMPRESSION_GUIDE.md) | 7-option pipeline: off / lite / standard / aggressive / ultra / RTK / stacked | +| [RTK Compression](docs/RTK_COMPRESSION.md) | Command-output compression, filters, trust, verify, raw-output recovery | +| [Compression Engines](docs/COMPRESSION_ENGINES.md) | Caveman, RTK, stacked pipelines, dashboard/API/MCP surfaces | +| [Compression Rules Format](docs/COMPRESSION_RULES_FORMAT.md) | JSON rule-pack schemas for Caveman and RTK filters | +| [Compression Language Packs](docs/COMPRESSION_LANGUAGE_PACKS.md) | Language detection and Caveman rule-pack authoring | +| [Resilience Guide](docs/RESILIENCE_GUIDE.md) | Circuit breakers, cooldowns, queue, anti-thundering herd, TLS spoofing | +| [Auto-Combo Engine](docs/AUTO-COMBO.md) | 6-factor scoring, mode packs, self-healing | +| [Proxy Guide](docs/PROXY_GUIDE.md) | 3-level proxy system, 1proxy marketplace, registry CRUD | +| [Free Tiers](docs/FREE_TIERS.md) | 25+ free API providers consolidated directory | +| [Features Gallery](docs/FEATURES.md) | Visual dashboard tour with screenshots | +| [Codebase Documentation](docs/CODEBASE_DOCUMENTATION.md) | Beginner-friendly codebase walkthrough | + +### ๐Ÿค– Protocols & APIs + +| Document | Description | +| ------------------------------------------- | --------------------------------------------------- | +| [API Reference](docs/API_REFERENCE.md) | All endpoints with examples | +| [OpenAPI Spec](docs/openapi.yaml) | OpenAPI 3.0 specification | +| [MCP Server](open-sse/mcp-server/README.md) | 29 MCP tools, IDE configs, Python/TS/Go clients | +| [MCP Server Guide](docs/MCP-SERVER.md) | MCP installation, transports, and tool reference | +| [A2A Server](src/lib/a2a/README.md) | JSON-RPC 2.0 protocol, skills, streaming, task mgmt | +| [A2A Server Guide](docs/A2A-SERVER.md) | A2A agent card, tasks, skills, and streaming | + +### ๐Ÿ“‹ Project & Quality + +| Document | Description | +| ---------------------------------------------- | ----------------------------------------------- | +| [Contributing](CONTRIBUTING.md) | Development setup and guidelines | +| [Security Policy](SECURITY.md) | Vulnerability reporting and security practices | +| [i18n Guide](docs/I18N.md) | 40+ language support, translation workflow, RTL | +| [Release Checklist](docs/RELEASE_CHECKLIST.md) | Pre-release validation steps | +| [Coverage Plan](docs/COVERAGE_PLAN.md) | Test coverage strategy and 4,690+ test suite | --- -## ๐Ÿงฉ Extensibility +## โญ Top Contributors -| System | What it does | Docs | -| ----------------------- | ----------------------------------------------------------------------- | ------------------------------------------------------------------ | -| ๐Ÿง  **Skills** | Built-in skills + marketplace + sandboxed custom skills (Docker) | [`docs/frameworks/SKILLS.md`](docs/frameworks/SKILLS.md) | -| ๐Ÿ’พ **Memory** | Persistent conversational memory (SQLite FTS5 + Qdrant vector) | [`docs/frameworks/MEMORY.md`](docs/frameworks/MEMORY.md) | -| โ˜๏ธ **Cloud Agents** | Submit long tasks to Codex Cloud / Devin / Jules | [`docs/frameworks/CLOUD_AGENT.md`](docs/frameworks/CLOUD_AGENT.md) | -| ๐Ÿช **Webhooks** | HMAC-signed event delivery (request.completed, quota.exceeded, etc.) | [`docs/frameworks/WEBHOOKS.md`](docs/frameworks/WEBHOOKS.md) | -| ๐Ÿ›ก๏ธ **Guardrails** | PII masker, prompt injection guard, vision bridge โ€” hot-reload | [`docs/security/GUARDRAILS.md`](docs/security/GUARDRAILS.md) | -| ๐Ÿงช **Evals** | Suite-based regression testing (combos/models/cases/rubrics) | [`docs/frameworks/EVALS.md`](docs/frameworks/EVALS.md) | -| ๐Ÿ” **Compliance/Audit** | `audit_log` table, retention, `noLog` opt-out, SSRF logging | [`docs/security/COMPLIANCE.md`](docs/security/COMPLIANCE.md) | -| ๐Ÿ›ก๏ธ **MCP Server** | 37 tools, 3 transports (stdio/SSE/Streamable HTTP), ~13 scopes | [`docs/frameworks/MCP-SERVER.md`](docs/frameworks/MCP-SERVER.md) | -| ๐Ÿค **A2A Protocol** | v0.3 JSON-RPC, 5 skills (smart-routing, quota, discovery, cost, health) | [`docs/frameworks/A2A-SERVER.md`](docs/frameworks/A2A-SERVER.md) | +> OmniRoute is shaped by a passionate open-source community. These individuals have made exceptional contributions that directly impact the quality, stability, and reach of the project. **Thank you.** + + + + + + + + + +
+ + oyi77
+ oyi77 +

+ ๐Ÿฅ‡ 190 commits โ€ข +72K lines
+ Analytics engine, SQL aggregations,
proxy marketplace, test coverage
+
+ + Chris Staley
+ Chris Staley +

+ ๐Ÿฅˆ 72 commits โ€ข +5.7K lines
+ SSE stream hardening, Responses API,
Gemini pagination, test regression fixes
+
+ + zenobit
+ zenobit +

+ ๐Ÿฅ‰ 62 commits โ€ข +24K lines
+ CI/CD pipeline, i18n for 33 languages,
Void Linux package, platform fixes
+
+ + R.D. & Randi
+ R.D. & Randi +

+ ๐Ÿ… 107 commits โ€ข +28K lines
+ Endpoints page, tunnel integrations,
Docker workflows, A2A status, compression UI
+
+ + benzntech
+ benzntech +

+ ๐Ÿ… 20 commits โ€ข +7.5K lines
+ Electron desktop app, auto-updater,
release build workflows, cross-platform CI
+
+ +> ๐Ÿ™ These contributors' features, bug fixes, and infrastructure improvements are a **core part** of what makes OmniRoute reliable and feature-rich. Every pull request, every test case, and every i18n translation file matters. Open source is built by people like them. --- -## ๐Ÿ“š Documentation +## ๐Ÿ‘ฅ Contributors -Everything you need, organized by area. +[![Contributors](https://contrib.rocks/image?repo=diegosouzapw/OmniRoute&max=100&columns=20&anon=1)](https://github.com/diegosouzapw/OmniRoute/graphs/contributors) -### ๐Ÿš€ Start here +### How to Contribute -- [`SETUP_GUIDE.md`](docs/guides/SETUP_GUIDE.md) โ€” install + connect first provider -- [`USER_GUIDE.md`](docs/guides/USER_GUIDE.md) โ€” end-user manual (modes, combos, CLIs, audio, ~1200 lines) -- [`FREE_TIERS.md`](docs/reference/FREE_TIERS.md) โ€” start free, no card -- [`TROUBLESHOOTING.md`](docs/guides/TROUBLESHOOTING.md) โ€” common issues + v3.8 known issues +1. Fork the repository +2. Create your feature branch (`git checkout -b feature/amazing-feature`) +3. Commit your changes (`git commit -m 'Add amazing feature'`) +4. Push to the branch (`git push origin feature/amazing-feature`) +5. Open a Pull Request -### ๐Ÿ›๏ธ Architecture +See [CONTRIBUTING.md](CONTRIBUTING.md) for detailed guidelines. -- [`ARCHITECTURE.md`](docs/architecture/ARCHITECTURE.md) โ€” high-level architecture -- [`CODEBASE_DOCUMENTATION.md`](docs/architecture/CODEBASE_DOCUMENTATION.md) โ€” engineering reference -- [`REPOSITORY_MAP.md`](docs/architecture/REPOSITORY_MAP.md) โ€” every directory and root file -- [`FEATURES.md`](docs/guides/FEATURES.md) โ€” full feature matrix +### Releasing a New Version -### ๐Ÿ”Œ API & contracts - -- [`API_REFERENCE.md`](docs/reference/API_REFERENCE.md) โ€” endpoint reference -- [`openapi.yaml`](docs/reference/openapi.yaml) โ€” OpenAPI 3.0 spec -- [`PROVIDER_REFERENCE.md`](docs/reference/PROVIDER_REFERENCE.md) โ€” full catalog (auto-generated) -- [`CLI-TOOLS.md`](docs/reference/CLI-TOOLS.md) โ€” CLI integrations + internal CLI -- [`ENVIRONMENT.md`](docs/reference/ENVIRONMENT.md) โ€” all env vars - -### ๐ŸŽฏ Routing & resilience - -- [`AUTO-COMBO.md`](docs/routing/AUTO-COMBO.md) โ€” Auto-Combo (9-factor scoring, 14 strategies) -- [`RESILIENCE_GUIDE.md`](docs/architecture/RESILIENCE_GUIDE.md) โ€” circuit breaker + cooldown + lockout -- [`REASONING_REPLAY.md`](docs/routing/REASONING_REPLAY.md) โ€” reasoning cache for DeepSeek/Kimi/Qwen -- [`STEALTH_GUIDE.md`](docs/security/STEALTH_GUIDE.md) โ€” TLS fingerprinting + obfuscation - -### ๐Ÿค– Agent protocols - -- [`AGENT_PROTOCOLS_GUIDE.md`](docs/frameworks/AGENT_PROTOCOLS_GUIDE.md) โ€” A2A vs ACP vs Cloud Agents -- [`MCP-SERVER.md`](docs/frameworks/MCP-SERVER.md) โ€” Model Context Protocol server -- [`A2A-SERVER.md`](docs/frameworks/A2A-SERVER.md) โ€” Agent-to-Agent protocol -- [`CLOUD_AGENT.md`](docs/frameworks/CLOUD_AGENT.md) โ€” Codex Cloud / Devin / Jules - -### ๐Ÿง  Extensions - -- [`SKILLS.md`](docs/frameworks/SKILLS.md) โ€” Skills framework -- [`MEMORY.md`](docs/frameworks/MEMORY.md) โ€” Memory system -- [`EVALS.md`](docs/frameworks/EVALS.md) โ€” Eval framework -- [`GUARDRAILS.md`](docs/security/GUARDRAILS.md) โ€” PII / injection / vision -- [`WEBHOOKS.md`](docs/frameworks/WEBHOOKS.md) โ€” Webhook delivery -- [`COMPLIANCE.md`](docs/security/COMPLIANCE.md) โ€” Audit + retention -- [`AUTHZ_GUIDE.md`](docs/architecture/AUTHZ_GUIDE.md) โ€” Authorization pipeline - -### ๐Ÿ—œ๏ธ Compression - -- [`COMPRESSION_GUIDE.md`](docs/compression/COMPRESSION_GUIDE.md) -- [`COMPRESSION_ENGINES.md`](docs/compression/COMPRESSION_ENGINES.md) -- [`COMPRESSION_RULES_FORMAT.md`](docs/compression/COMPRESSION_RULES_FORMAT.md) -- [`COMPRESSION_LANGUAGE_PACKS.md`](docs/compression/COMPRESSION_LANGUAGE_PACKS.md) -- [`RTK_COMPRESSION.md`](docs/compression/RTK_COMPRESSION.md) - -### ๐Ÿš€ Deployment - -- [`DOCKER_GUIDE.md`](docs/guides/DOCKER_GUIDE.md) -- [`VM_DEPLOYMENT_GUIDE.md`](docs/ops/VM_DEPLOYMENT_GUIDE.md) -- [`FLY_IO_DEPLOYMENT_GUIDE.md`](docs/ops/FLY_IO_DEPLOYMENT_GUIDE.md) -- [`ELECTRON_GUIDE.md`](docs/guides/ELECTRON_GUIDE.md) -- [`PWA_GUIDE.md`](docs/guides/PWA_GUIDE.md) -- [`TERMUX_GUIDE.md`](docs/guides/TERMUX_GUIDE.md) -- [`TUNNELS_GUIDE.md`](docs/ops/TUNNELS_GUIDE.md) -- [`PROXY_GUIDE.md`](docs/ops/PROXY_GUIDE.md) - -### ๐Ÿ“‹ Operations - -- [`RELEASE_CHECKLIST.md`](docs/ops/RELEASE_CHECKLIST.md) โ€” release flow with Claude Code skills -- [`COVERAGE_PLAN.md`](docs/ops/COVERAGE_PLAN.md) โ€” test coverage state (current: 82.58%/82.58%/84.23%/75.22%) -- [`I18N.md`](docs/guides/I18N.md) โ€” 30 supported locales -- [`UNINSTALL.md`](docs/guides/UNINSTALL.md) - -### ๐Ÿค Contributing & policy - -- [`CONTRIBUTING.md`](CONTRIBUTING.md) โ€” contributor guide -- [`SECURITY.md`](SECURITY.md) โ€” security policy -- [`CODE_OF_CONDUCT.md`](CODE_OF_CONDUCT.md) -- [`CLAUDE.md`](CLAUDE.md) โ€” rules for Claude Code agents -- [`AGENTS.md`](AGENTS.md) โ€” rules for non-Claude agents -- [`GEMINI.md`](GEMINI.md) โ€” rules for Gemini agents - ---- - -## ๐Ÿ’ก Use Cases - -| Scenario | Solution | -| --------------------------------- | ------------------------------------------------------------- | -| "Claude Pro user, hit rate limit" | Combo: Claude โ†’ GLM โ†’ DeepSeek (auto-fallback) | -| "Want $0 forever" | `auto/cheap` โ†’ Kiro/Qoder/Pollinations fallback chain | -| "24/7 coding, no interruptions" | `auto/lkgp` (sticky to last-good) + Resilience | -| "Blocked region" | 1proxy free marketplace + Cloudflare Quick Tunnel | -| "Max token savings" | Stacked compression: `rtk โ†’ caveman` (78-95% on logs) | -| "Multi-agent system" | Expose OmniRoute as A2A node, route via `smart-routing` skill | -| "Long-running coding task" | Cloud Agents โ†’ Devin/Jules with management auth | - -โ†’ Detailed playbooks: [`USER_GUIDE.md`](docs/guides/USER_GUIDE.md) ยท [`AUTO-COMBO.md`](docs/routing/AUTO-COMBO.md) - ---- - -## ๐Ÿ“ก Protocols supported - -OmniRoute speaks all major AI protocols โ€” clients don't need to change: - -- **OpenAI** (Chat Completions, Responses, Embeddings, Images, Audio, Files, Batches, Rerank, Moderations) -- **Anthropic Messages** (Claude format, with thinking blocks + reasoning replay) -- **Google Gemini** (generateContent + Vertex) -- **Claude Code** (CLI-specific format with CCH + fingerprinting) -- **Cursor** (proprietary format with tool calls) -- **Kiro** (AWS Builder ID OAuth) -- **MCP** (Model Context Protocol โ€” 37 tools, stdio/SSE/Streamable HTTP) -- **A2A** (Agent-to-Agent v0.3 JSON-RPC โ€” agent card at `/.well-known/agent.json`) - ---- - -## ๐Ÿ—๏ธ Architecture (10-second tour) - -``` -Client โ†’ /v1/chat/completions โ†’ [CORS โ†’ Zod โ†’ Auth โ†’ Authz โ†’ Guardrails] - โ†’ handleChatCore() โ†’ [Cache โ†’ Rate limit โ†’ Combo routing] - โ†’ translateRequest โ†’ getExecutor โ†’ fetch upstream (with retry) - โ†’ response translation โ†’ SSE stream or JSON - โ†’ [Compliance audit] โ†’ response +```bash +# Create a release โ€” npm publish happens automatically +gh release create v2.0.0 --title "v2.0.0" --generate-notes ``` -**Major pieces:** - -- **`src/app/`** โ€” Next.js 16 App Router (60+ API routes + 30 dashboard pages) -- **`src/lib/`** โ€” 50+ domain modules (db, a2a, memory, skills, guardrails, evals, โ€ฆ) -- **`open-sse/`** โ€” Streaming engine workspace (31 executors, 9+8+9 translators, 80+ services, 37-tool MCP server) -- **`src/domain/`** โ€” Pure business logic (policies, fallback, cost rules) -- **`src/server/`** โ€” Server-only (authz pipeline, cors) - -โ†’ Deep dive: [`docs/architecture/ARCHITECTURE.md`](docs/architecture/ARCHITECTURE.md) ยท [`docs/architecture/CODEBASE_DOCUMENTATION.md`](docs/architecture/CODEBASE_DOCUMENTATION.md) - --- -## ๐ŸŒ i18n +## ๐Ÿ“Š Star History -UI translated to **30 languages** with full RTL support for Arabic and Hebrew. + + + + + Star History Chart + + -๐ŸŒ [English](README.md) ยท [Portuguรชs](docs/i18n/pt-BR/README.md) ยท [Espaรฑol](docs/i18n/es/README.md) ยท [Franรงais](docs/i18n/fr/README.md) ยท [Deutsch](docs/i18n/de/README.md) ยท [ไธญๆ–‡](docs/i18n/zh-CN/README.md) ยท [ๆ—ฅๆœฌ่ชž](docs/i18n/ja/README.md) ยท [ํ•œ๊ตญ์–ด](docs/i18n/ko/README.md) ยท [ุงู„ุนุฑุจูŠุฉ](docs/i18n/ar/README.md) ยท [เคนเคฟเคจเฅเคฆเฅ€](docs/i18n/hi/README.md) ยท [ะ ัƒััะบะธะน](docs/i18n/ru/README.md) ยท [+ 19 more](docs/i18n/) +## ๐ŸŒ StarMapper -โ†’ Adding a language: [`docs/guides/I18N.md`](docs/guides/I18N.md) + + + + + StarMapper + + ---- +## ๐Ÿ™ Acknowledgments -## ๐Ÿค Community +Special thanks to **[9router](https://github.com/decolua/9router)** by **[decolua](https://github.com/decolua)** โ€” the original project that inspired this fork. OmniRoute builds upon that incredible foundation with additional features, multi-modal APIs, and a full TypeScript rewrite. -- ๐ŸŒ **Website:** [omniroute.online](https://omniroute.online) -- ๐Ÿ“ฆ **npm:** [omniroute](https://www.npmjs.com/package/omniroute) -- ๐Ÿณ **Docker Hub:** [diegosouzapw/omniroute](https://hub.docker.com/r/diegosouzapw/omniroute) -- ๐Ÿ’ฌ **WhatsApp (BR):** Brazilian community group โ€” see README link -- ๐Ÿ› **Issues:** [GitHub Issues](https://github.com/diegosouzapw/OmniRoute/issues) -- ๐Ÿ’ก **Discussions:** [GitHub Discussions](https://github.com/diegosouzapw/OmniRoute/discussions) +Special thanks to **[CLIProxyAPI](https://github.com/router-for-me/CLIProxyAPI)** by **[router-for-me](https://github.com/router-for-me)** โ€” the original Go implementation that inspired this JavaScript port. ---- +Special thanks to **[Caveman](https://github.com/JuliusBrussee/caveman)** by **[JuliusBrussee](https://github.com/JuliusBrussee)** (โญ 51K+) โ€” the viral "why use many token when few token do trick" project whose caveman-speak compression philosophy inspired OmniRoute's standard compression mode and 30+ filler/condensation regex rules. -## โค๏ธ Contributing - -We welcome PRs! Start with: - -1. Read [`CONTRIBUTING.md`](CONTRIBUTING.md) โ€” setup, conventional commits, testing -2. Pick an issue labeled [`good first issue`](https://github.com/diegosouzapw/OmniRoute/labels/good%20first%20issue) -3. Branch from `main` (`feat/*`, `fix/*`, `docs/*`, `refactor/*`, `test/*`, `chore/*`) -4. Hooks will run lint + test on commit/push - -**Adding a provider?** [`docs/architecture/ARCHITECTURE.md ยง Adding a New Provider`](docs/architecture/ARCHITECTURE.md) -**Adding an MCP tool?** [`docs/frameworks/MCP-SERVER.md`](docs/frameworks/MCP-SERVER.md) -**Adding an A2A skill?** [`docs/frameworks/A2A-SERVER.md ยง Adding a New Skill`](docs/frameworks/A2A-SERVER.md) - ---- - -## ๐Ÿ”’ Security - -- **Reporting:** see [`SECURITY.md`](SECURITY.md) for disclosure policy -- **Supported versions:** 3.8.x (Active), 3.7.x (Security only) -- **Secrets:** never commit. Use `.env` (auto-generated from `.env.example` on first install) or vaults -- **Encryption:** credentials at rest with AES-256-GCM -- **Authz:** route-aware classification (`src/server/authz/`) โ€” see [`docs/architecture/AUTHZ_GUIDE.md`](docs/architecture/AUTHZ_GUIDE.md) -- **Guardrails:** PII masking, prompt injection detection โ€” hot-reloadable +Special thanks to **[RTK - Rust Token Killer](https://github.com/rtk-ai/rtk)** by **[RTK AI](https://github.com/rtk-ai)** โ€” the high-performance command-output compression project whose terminal, build, test, git, and tool-output filtering model inspired OmniRoute's RTK engine, JSON filter DSL, raw-output recovery, and stacked RTK โ†’ Caveman compression pipeline. --- ## ๐Ÿ“„ License -[MIT](LICENSE) ยฉ 2025-2026 [Diego Souza](https://github.com/diegosouzapw) - -Free forever. Self-hosted. No tracking. No cloud lock-in. +MIT License - see [LICENSE](LICENSE) for details. ---
- -**[โฌ† Back to top](#-omniroute)** ยท Built with โค๏ธ for the open-source AI community. - -OmniRoute v3.8.0 ยท Node โ‰ฅ20.20.2 ยท MIT License - + Built with โค๏ธ for developers who code 24/7 +
+ omniroute.online
+ diff --git a/bin/cli/commands/config.mjs b/bin/cli/commands/config.mjs new file mode 100644 index 0000000000..32d61cba3f --- /dev/null +++ b/bin/cli/commands/config.mjs @@ -0,0 +1,182 @@ +import { parseArgs, getStringFlag, hasFlag } from "../args.mjs"; +import { printHeading, printInfo, printSuccess, printError } from "../io.mjs"; +import { resolveDataDir } from "../data-dir.mjs"; +import path from "node:path"; +import fs from "node:fs"; + +function printConfigHelp() { + console.log(` +Usage: + omniroute config list List all CLI tools and config status + omniroute config get Show current config for a tool + omniroute config set [options] Write config for a tool + omniroute config validate Validate config format without writing + +Options: + --base-url OmniRoute API base URL (default: http://localhost:20128/v1) + --api-key API key for the tool + --model Model identifier (where applicable) + --json Output as JSON + --non-interactive Do not prompt for confirmation + --yes Skip confirmation prompt + --help Show this help + +Tools: claude, codex, opencode, cline, kilocode, continue +`); +} + +function ensureBackup(configPath) { + if (!fs.existsSync(configPath)) return; + const backupDir = path.join(path.dirname(configPath), ".omniroute.bak"); + if (!fs.existsSync(backupDir)) fs.mkdirSync(backupDir, { recursive: true }); + const backupPath = path.join(backupDir, path.basename(configPath) + ".bak"); + fs.copyFileSync(configPath, backupPath); + return backupPath; +} + +export async function runConfigCommand(argv) { + const { flags, positionals } = parseArgs(argv); + + if (hasFlag(flags, "help") || hasFlag(flags, "h") || positionals.length === 0) { + printConfigHelp(); + return 0; + } + + const subcommand = positionals[0]; + const toolId = positionals[1]; + + if (subcommand === "list") { + const { detectAllTools } = await import("../../../src/lib/cli-helper/tool-detector.js"); + const tools = await detectAllTools(); + + if (hasFlag(flags, "json")) { + console.log(JSON.stringify(tools, null, 2)); + } else { + printHeading("CLI Tool Configuration Status"); + for (const t of tools) { + const status = t.configured + ? "โœ“ Configured" + : t.installed + ? "โœ— Not configured" + : "โœ— Not installed"; + console.log(` ${t.name.padEnd(14)} ${status}`); + if (t.version) console.log(` version: ${t.version}`); + console.log(` config: ${t.configPath}`); + } + } + return 0; + } + + if (subcommand === "get") { + if (!toolId) { + printError("Tool ID required. Usage: omniroute config get "); + return 1; + } + const { detectTool } = await import("../../../src/lib/cli-helper/tool-detector.js"); + const tool = await detectTool(toolId); + if (!tool) { + printError(`Unknown tool: ${toolId}`); + return 1; + } + if (hasFlag(flags, "json")) { + console.log(JSON.stringify(tool, null, 2)); + } else { + printHeading(`${tool.name} Configuration`); + console.log(` Installed: ${tool.installed ? "Yes" : "No"}`); + console.log(` Configured: ${tool.configured ? "Yes" : "No"}`); + console.log(` Config: ${tool.configPath}`); + if (tool.version) console.log(` Version: ${tool.version}`); + if (tool.configContents) { + console.log(`\n Contents:`); + console.log(tool.configContents); + } + } + return 0; + } + + if (subcommand === "set") { + if (!toolId) { + printError("Tool ID required. Usage: omniroute config set [options]"); + return 1; + } + + const baseUrl = + getStringFlag(flags, "base-url", "OMNIROUTE_BASE_URL") || "http://localhost:20128/v1"; + const apiKey = getStringFlag(flags, "api-key", "OMNIROUTE_API_KEY"); + const model = getStringFlag(flags, "model"); + + if (!apiKey) { + printError("API key required. Use --api-key or set OMNIROUTE_API_KEY."); + return 1; + } + + const { generateConfig } = + await import("../../../src/lib/cli-helper/config-generator/index.js"); + const result = await generateConfig(toolId, { baseUrl, apiKey, model }); + + if (!result.success) { + printError(result.error || "Failed to generate config"); + return 1; + } + + const nonInteractive = hasFlag(flags, "non-interactive") || hasFlag(flags, "yes"); + + if (!nonInteractive) { + console.log(`\n About to write config to: ${result.configPath}`); + console.log(` Content preview:\n`); + console.log(result.content); + console.log(""); + + const readline = await import("node:readline"); + const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); + const answer = await new Promise((resolve) => rl.question("Proceed? [y/N] ", resolve)); + rl.close(); + + if (!/^y(es)?$/i.test(answer)) { + console.log("Aborted."); + return 0; + } + } + + const dir = path.dirname(result.configPath); + if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); + + const backupPath = ensureBackup(result.configPath); + if (backupPath) printInfo(`Backup saved to: ${backupPath}`); + + fs.writeFileSync(result.configPath, result.content, "utf-8"); + printSuccess(`Config written to ${result.configPath}`); + return 0; + } + + if (subcommand === "validate") { + if (!toolId) { + printError("Tool ID required. Usage: omniroute config validate "); + return 1; + } + + const baseUrl = + getStringFlag(flags, "base-url", "OMNIROUTE_BASE_URL") || "http://localhost:20128/v1"; + const apiKey = getStringFlag(flags, "api-key", "OMNIROUTE_API_KEY") || "test-key"; + const model = getStringFlag(flags, "model"); + + const { generateConfig } = + await import("../../../src/lib/cli-helper/config-generator/index.js"); + const result = await generateConfig(toolId, { baseUrl, apiKey, model }); + + if (!result.success) { + printError(`Validation failed: ${result.error}`); + return 1; + } + + printSuccess(`Config for ${toolId} is valid`); + if (hasFlag(flags, "json")) { + console.log(JSON.stringify({ valid: true, content: result.content }, null, 2)); + } + return 0; + } + + printError(`Unknown subcommand: ${subcommand}`); + printConfigHelp(); + return 1; +} diff --git a/bin/cli/commands/doctor.mjs b/bin/cli/commands/doctor.mjs index 2f4543b6b9..3fe11b1a31 100644 --- a/bin/cli/commands/doctor.mjs +++ b/bin/cli/commands/doctor.mjs @@ -466,6 +466,15 @@ export async function collectDoctorChecks(context = {}, options = {}) { checks.push(await checkServerLiveness(options)); } + // CLI tool health checks + try { + const { collectCliToolChecks } = await import("../../../src/lib/cli-helper/doctor/checks.js"); + const cliChecks = await collectCliToolChecks(); + checks.push(...cliChecks); + } catch (err) { + checks.push(warn("CLI Tools", `Could not run CLI tool checks: ${err.message}`)); + } + return { dataDir, dbPath, @@ -493,7 +502,7 @@ Options: --liveness-url Full health endpoint URL override Checks: - config, database, storage/encryption, ports, Node runtime, native binary, memory, server liveness + config, database, storage/encryption, ports, Node runtime, native binary, memory, server liveness, CLI tools `); } diff --git a/bin/cli/commands/logs.mjs b/bin/cli/commands/logs.mjs new file mode 100644 index 0000000000..28763922b1 --- /dev/null +++ b/bin/cli/commands/logs.mjs @@ -0,0 +1,83 @@ +import { parseArgs, getStringFlag, hasFlag } from "../args.mjs"; +import { printHeading, printInfo, printError } from "../io.mjs"; + +function printLogsHelp() { + console.log(` +Usage: + omniroute logs [options] + +Options: + --follow Stream logs in real-time + --filter Filter by level (error, warn, info) โ€” comma-separated + --lines Number of lines to fetch (default: 100) + --timeout Connection timeout in ms (default: 30000) + --base-url OmniRoute API base URL (default: http://localhost:20128) + --json Output as JSON + --help Show this help +`); +} + +export async function runLogsCommand(argv) { + const { flags } = parseArgs(argv); + + if (hasFlag(flags, "help") || hasFlag(flags, "h")) { + printLogsHelp(); + return 0; + } + + const baseUrl = getStringFlag(flags, "base-url") || "http://localhost:20128"; + const follow = hasFlag(flags, "follow"); + const filter = getStringFlag(flags, "filter"); + const lines = getStringFlag(flags, "lines") || "100"; + const timeout = parseInt(getStringFlag(flags, "timeout") || "30000", 10); + + const filters = filter ? filter.split(",").map((f) => f.trim()) : []; + + const { createLogStream } = await import("../../../src/lib/cli-helper/log-streamer.js"); + const { stream, stop } = createLogStream({ baseUrl, filters, follow, timeout }); + + const reader = stream.getReader(); + const decoder = new TextDecoder(); + let buffer = ""; + + const processLine = (line) => { + if (!line.trim()) return; + if (hasFlag(flags, "json")) { + console.log(line); + return; + } + try { + const parsed = JSON.parse(line); + const level = parsed.level || "info"; + const ts = parsed.timestamp || new Date().toISOString(); + const msg = parsed.message || JSON.stringify(parsed); + const prefix = + { error: "\x1b[31m[ERR]", warn: "\x1b[33m[WRN]", info: "\x1b[36m[INF]" }[level] || "[INF]"; + console.log(`${prefix}\x1b[0m ${ts} ${msg}`); + } catch { + console.log(line); + } + }; + + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + buffer += decoder.decode(value, { stream: true }); + const lines = buffer.split("\n"); + buffer = lines.pop() || ""; + for (const line of lines) processLine(line); + } + if (buffer) processLine(buffer); + } catch (err) { + if (err.name === "AbortError") { + printInfo("Log stream stopped."); + } else { + printError(`Log stream error: ${err.message}`); + } + } finally { + stop(); + } + + return 0; +} diff --git a/bin/cli/commands/provider-cmd.mjs b/bin/cli/commands/provider-cmd.mjs new file mode 100644 index 0000000000..bbd977d8a3 --- /dev/null +++ b/bin/cli/commands/provider-cmd.mjs @@ -0,0 +1,278 @@ +import { parseArgs, getStringFlag, hasFlag } from "../args.mjs"; +import { printHeading, printInfo, printSuccess, printError } from "../io.mjs"; +import { resolveDataDir, resolveStoragePath } from "../data-dir.mjs"; +import path from "node:path"; +import fs from "node:fs"; + +function printProviderHelp() { + console.log(` +Usage: + omniroute provider add [options] Add a provider connection + omniroute provider list List configured providers + omniroute provider remove Remove a provider connection + omniroute provider test Test a provider connection + omniroute provider default Set default provider + +Options: + --provider Provider id (e.g., openai, anthropic, omniroute) + --api-key API key for the provider + --provider-name Display name for the connection + --default-model Default model to use + --base-url Custom base URL override + --json Output as JSON + --yes Skip confirmation + --help Show this help +`); +} + +export async function runProviderCommand(argv) { + const { flags, positionals } = parseArgs(argv); + + if (hasFlag(flags, "help") || hasFlag(flags, "h") || positionals.length === 0) { + printProviderHelp(); + return 0; + } + + const subcommand = positionals[0]; + + if (subcommand === "add") { + const providerName = positionals[1] || getStringFlag(flags, "provider"); + const apiKey = getStringFlag(flags, "api-key"); + const displayName = getStringFlag(flags, "provider-name"); + const defaultModel = getStringFlag(flags, "default-model"); + const baseUrl = getStringFlag(flags, "base-url"); + + if (!providerName) { + printError("Provider name required. Usage: omniroute provider add "); + return 1; + } + + if (providerName === "omniroute") { + // Special case: add OmniRoute as a provider in OpenCode config + const opencodePath = path.join( + process.env.HOME || os.homedir(), + ".config", + "opencode", + "opencode.json" + ); + const { generateConfig } = + await import("../../../src/lib/cli-helper/config-generator/index.js"); + const result = await generateConfig("opencode", { + baseUrl: baseUrl || "http://localhost:20128/v1", + apiKey: apiKey || "", + }); + + if (!result.success) { + printError(result.error || "Failed to generate config"); + return 1; + } + + if (!hasFlag(flags, "yes")) { + console.log(`\n About to write OpenCode config to: ${opencodePath}`); + console.log(` Content:\n`); + console.log(result.content); + console.log(""); + const readline = await import("node:readline"); + const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); + const answer = await new Promise((resolve) => rl.question("Proceed? [y/N] ", resolve)); + rl.close(); + if (!/^y(es)?$/i.test(answer)) { + printInfo("Aborted."); + return 0; + } + } + + const dir = path.dirname(opencodePath); + if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); + fs.writeFileSync(opencodePath, result.content, "utf-8"); + printSuccess(`OpenCode config written to ${opencodePath}`); + return 0; + } + + // Generic provider addition via SQLite + const dbPath = resolveStoragePath(resolveDataDir()); + if (!fs.existsSync(dbPath)) { + printError("Database not found. Run `omniroute setup` first."); + return 1; + } + + const { default: Database } = await import("better-sqlite3"); + const db = new Database(dbPath); + + try { + const stmt = db.prepare(` + INSERT INTO provider_connections (provider, name, api_key, default_model, provider_specific_data) + VALUES (?, ?, ?, ?, ?) + `); + const specificData = baseUrl ? JSON.stringify({ baseUrl }) : null; + stmt.run( + providerName, + displayName || providerName, + apiKey || "", + defaultModel || null, + specificData + ); + printSuccess(`Provider "${displayName || providerName}" added`); + } finally { + db.close(); + } + + return 0; + } + + if (subcommand === "list") { + const dbPath = resolveStoragePath(resolveDataDir()); + if (!fs.existsSync(dbPath)) { + if (isJson()) console.log(JSON.stringify([])); + else printInfo("No database found. Run `omniroute setup` first."); + return 0; + } + + const { default: Database } = await import("better-sqlite3"); + const db = new Database(dbPath); + try { + const rows = db + .prepare("SELECT id, provider, name, default_model FROM provider_connections") + .all(); + if (isJson()) { + console.log(JSON.stringify(rows, null, 2)); + } else { + printHeading("Configured Providers"); + for (const r of rows) { + console.log( + ` [${r.id}] ${r.name} (${r.provider})${r.default_model ? ` โ€” model: ${r.default_model}` : ""}` + ); + } + } + } finally { + db.close(); + } + return 0; + } + + if (subcommand === "remove") { + const target = positionals[1]; + if (!target) { + printError("Provider name or ID required. Usage: omniroute provider remove "); + return 1; + } + + const dbPath = resolveStoragePath(resolveDataDir()); + if (!fs.existsSync(dbPath)) { + printError("Database not found."); + return 1; + } + + const { default: Database } = await import("better-sqlite3"); + const db = new Database(dbPath); + try { + const isId = /^\d+$/.test(target); + const stmt = isId + ? db.prepare("DELETE FROM provider_connections WHERE id = ?") + : db.prepare("DELETE FROM provider_connections WHERE name = ? OR provider = ?"); + const result = stmt.run(isId ? parseInt(target, 10) : target); + if (result.changes > 0) { + printSuccess(`Removed ${result.changes} provider(s)`); + } else { + printError("Provider not found"); + } + } finally { + db.close(); + } + return 0; + } + + if (subcommand === "test") { + const target = positionals[1]; + if (!target) { + printError("Provider name or ID required. Usage: omniroute provider test "); + return 1; + } + + const dbPath = resolveStoragePath(resolveDataDir()); + if (!fs.existsSync(dbPath)) { + printError("Database not found."); + return 1; + } + + const { default: Database } = await import("better-sqlite3"); + const db = new Database(dbPath); + try { + const isId = /^\d+$/.test(target); + const row = isId + ? db.prepare("SELECT * FROM provider_connections WHERE id = ?").get(parseInt(target, 10)) + : db + .prepare("SELECT * FROM provider_connections WHERE name = ? OR provider = ?") + .get(target); + + if (!row) { + printError("Provider not found"); + return 1; + } + + const { testProviderApiKey } = await import("../provider-test.mjs"); + const result = await testProviderApiKey({ + provider: row.provider, + apiKey: row.api_key, + defaultModel: row.default_model, + baseUrl: row.provider_specific_data ? JSON.parse(row.provider_specific_data).baseUrl : null, + }); + + if (isJson()) { + console.log(JSON.stringify(result, null, 2)); + } else if (result.valid) { + printSuccess(`Provider "${row.name}" is reachable`); + } else { + printError(`Provider test failed: ${result.error || "unknown error"}`); + } + } finally { + db.close(); + } + return 0; + } + + if (subcommand === "default") { + const target = positionals[1]; + if (!target) { + printError("Provider name or ID required. Usage: omniroute provider default "); + return 1; + } + + const dbPath = resolveStoragePath(resolveDataDir()); + if (!fs.existsSync(dbPath)) { + printError("Database not found."); + return 1; + } + + const { default: Database } = await import("better-sqlite3"); + const db = new Database(dbPath); + try { + const isId = /^\d+$/.test(target); + const row = isId + ? db.prepare("SELECT * FROM provider_connections WHERE id = ?").get(parseInt(target, 10)) + : db + .prepare("SELECT * FROM provider_connections WHERE name = ? OR provider = ?") + .get(target); + + if (!row) { + printError("Provider not found"); + return 1; + } + + db.prepare("UPDATE provider_connections SET is_default = 0").run(); + db.prepare("UPDATE provider_connections SET is_default = 1 WHERE id = ?").run(row.id); + printSuccess(`Default provider set to "${row.name}"`); + } finally { + db.close(); + } + return 0; + } + + printError(`Unknown subcommand: ${subcommand}`); + printProviderHelp(); + return 1; +} + +function isJson() { + return process.argv.includes("--json"); +} diff --git a/bin/cli/commands/status.mjs b/bin/cli/commands/status.mjs new file mode 100644 index 0000000000..b7d3446aea --- /dev/null +++ b/bin/cli/commands/status.mjs @@ -0,0 +1,84 @@ +import { parseArgs, getStringFlag, hasFlag } from "../args.mjs"; +import { printHeading, printInfo, printSuccess } from "../io.mjs"; +import { resolveDataDir, resolveStoragePath } from "../data-dir.mjs"; +import path from "node:path"; +import fs from "node:fs"; +import os from "node:os"; + +function getPackageVersion() { + try { + const pkg = JSON.parse(fs.readFileSync(path.join(process.cwd(), "package.json"), "utf-8")); + return pkg.version || "unknown"; + } catch { + return "unknown"; + } +} + +function formatBytes(bytes) { + if (bytes < 1024) return `${bytes} B`; + if (bytes < 1048576) return `${(bytes / 1024).toFixed(1)} KB`; + return `${(bytes / 1048576).toFixed(1)} MB`; +} + +export async function runStatusCommand(argv) { + const { flags } = parseArgs(argv); + const isJson = hasFlag(flags, "json"); + const isVerbose = hasFlag(flags, "verbose"); + + const dataDir = resolveDataDir(); + const dbPath = resolveStoragePath(dataDir); + const version = getPackageVersion(); + + const status = { + version, + dataDir, + database: { + exists: fs.existsSync(dbPath), + path: dbPath, + size: fs.existsSync(dbPath) ? formatBytes(fs.statSync(dbPath).size) : null, + }, + configDir: path.join(dataDir, "config"), + configExists: fs.existsSync(path.join(dataDir, "config")), + }; + + if (isVerbose || !isJson) { + try { + const { detectAllTools } = await import("../../../src/lib/cli-helper/tool-detector.js"); + const tools = await detectAllTools(); + status.tools = tools.map((t) => ({ + id: t.id, + name: t.name, + installed: t.installed, + configured: t.configured, + version: t.version || null, + })); + } catch { + status.tools = "unavailable"; + } + } + + if (isJson) { + console.log(JSON.stringify(status, null, 2)); + return 0; + } + + printHeading("OmniRoute Status"); + console.log(` Version: ${status.version}`); + console.log(` Data Dir: ${status.dataDir}`); + console.log( + ` Database: ${status.database.exists ? "Found" : "Not found"} (${status.database.size || "N/A"})` + ); + console.log(` Config Dir: ${status.configExists ? "Exists" : "Not found"}`); + + if (status.tools) { + console.log("\n CLI Tools:"); + for (const t of status.tools) { + const icon = t.configured ? "โœ“" : t.installed ? "~" : "โœ—"; + console.log( + ` ${icon} ${t.name.padEnd(14)} ${t.installed ? "installed" : "not installed"}${t.version ? ` (${t.version})` : ""}` + ); + } + } + + return 0; +} diff --git a/bin/cli/commands/update.mjs b/bin/cli/commands/update.mjs new file mode 100644 index 0000000000..8b8a394090 --- /dev/null +++ b/bin/cli/commands/update.mjs @@ -0,0 +1,166 @@ +import { parseArgs, getStringFlag, hasFlag } from "../args.mjs"; +import { printHeading, printInfo, printSuccess, printError } from "../io.mjs"; +import { homedir } from "node:os"; +import path from "node:path"; +import { execFile } from "node:child_process"; +import { promisify } from "node:util"; + +const execFileAsync = promisify(execFile); + +function printUpdateHelp() { + console.log(` +Usage: + omniroute update [options] + +Options: + --check Check for available update without applying + --dry-run Show what would be updated without applying + --backup Create backup before updating (default: true) + --no-backup Skip backup creation + --help Show this help + +Environment: + OMNIRoute_AUTO_UPDATE Set to "true" to enable auto-update check on startup +`); +} + +async function getCurrentVersion() { + try { + const { readFileSync } = await import("node:fs"); + const pkg = JSON.parse(readFileSync(path.join(process.cwd(), "package.json"), "utf-8")); + return pkg.version; + } catch { + return null; + } +} + +async function getLatestVersion() { + try { + const { stdout } = await execFileAsync("npm", ["view", "omniroute", "version"], { + timeout: 15000, + }); + return stdout.trim(); + } catch { + return null; + } +} + +function compareVersions(a, b) { + const pa = a.split(".").map(Number); + const pb = b.split(".").map(Number); + for (let i = 0; i < 3; i++) { + if ((pa[i] || 0) > (pb[i] || 0)) return 1; + if ((pa[i] || 0) < (pb[i] || 0)) return -1; + } + return 0; +} + +async function createBackup() { + const binPath = path.join(process.cwd(), "bin"); + const backupDir = path.join(homedir(), ".omniroute", "backups", `omniroute-${Date.now()}`); + + try { + const { mkdirSync, copyFileSync, existsSync } = await import("node:fs"); + if (!existsSync(binPath)) return null; + + mkdirSync(backupDir, { recursive: true }); + const files = ["omniroute.mjs", "cli", "nodeRuntimeSupport.mjs", "mcp-server.mjs"]; + for (const f of files) { + const src = path.join(binPath, f); + if (existsSync(src)) { + copyFileSync(src, path.join(backupDir, f)); + } + } + return backupDir; + } catch { + return null; + } +} + +export async function runUpdateCommand(argv) { + const { flags } = parseArgs(argv); + + if (hasFlag(flags, "help") || hasFlag(flags, "h")) { + printUpdateHelp(); + return 0; + } + + const checkOnly = hasFlag(flags, "check"); + const dryRun = hasFlag(flags, "dry-run"); + const skipBackup = hasFlag(flags, "no-backup"); + + const current = await getCurrentVersion(); + const latest = await getLatestVersion(); + + if (!current) { + printError("Could not determine current version"); + return 1; + } + + if (!latest) { + printError("Could not check latest version. Is npm available?"); + return 1; + } + + printHeading("OmniRoute Update"); + console.log(` Current version: ${current}`); + console.log(` Latest version: ${latest}`); + + const cmp = compareVersions(current, latest); + if (cmp >= 0) { + printSuccess("You are running the latest version!"); + return 0; + } + + console.log(`\n Update available: ${current} โ†’ ${latest}`); + + if (checkOnly) { + console.log("\n Run `omniroute update` to apply the update."); + return 0; + } + + if (dryRun) { + console.log("\n [DRY RUN] Would run: npm install -g omniroute@latest"); + if (!skipBackup) console.log(" [DRY RUN] Would create backup in ~/.omniroute/backups/"); + return 0; + } + + if (!skipBackup) { + printInfo("Creating backup..."); + const backupPath = await createBackup(); + if (backupPath) { + printSuccess(`Backup created: ${backupPath}`); + } else { + printError("Failed to create backup. Aborting update."); + return 1; + } + } + + if (!hasFlag(flags, "yes")) { + const readline = await import("node:readline"); + const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); + const answer = await new Promise((resolve) => + rl.question(`Proceed with update to ${latest}? [y/N] `, resolve) + ); + rl.close(); + if (!/^y(es)?$/i.test(answer)) { + printInfo("Update aborted."); + return 0; + } + } + + printInfo("Updating OmniRoute..."); + try { + const { execSync } = await import("child_process"); + execSync("npm install -g omniroute@latest", { stdio: "inherit" }); + printSuccess(`Updated to version ${latest}`); + printInfo("Run `omniroute --version` to verify."); + return 0; + } catch (err) { + printError(`Update failed: ${err.message}`); + printInfo("Restore from backup:"); + const backupDir = path.join(homedir(), ".omniroute", "backups"); + printInfo(` ls ${backupDir}`); + return 1; + } +} diff --git a/bin/cli/index.mjs b/bin/cli/index.mjs index c9c8683016..a908b54745 100644 --- a/bin/cli/index.mjs +++ b/bin/cli/index.mjs @@ -1,6 +1,11 @@ import { runDoctorCommand } from "./commands/doctor.mjs"; import { runProvidersCommand } from "./commands/providers.mjs"; import { runSetupCommand } from "./commands/setup.mjs"; +import { runConfigCommand } from "./commands/config.mjs"; +import { runStatusCommand } from "./commands/status.mjs"; +import { runLogsCommand } from "./commands/logs.mjs"; +import { runUpdateCommand } from "./commands/update.mjs"; +import { runProviderCommand } from "./commands/provider-cmd.mjs"; export async function runCliCommand(command, argv, context = {}) { if (command === "doctor") { @@ -15,5 +20,25 @@ export async function runCliCommand(command, argv, context = {}) { return runSetupCommand(argv, context); } + if (command === "config") { + return runConfigCommand(argv); + } + + if (command === "status") { + return runStatusCommand(argv); + } + + if (command === "logs") { + return runLogsCommand(argv); + } + + if (command === "update") { + return runUpdateCommand(argv); + } + + if (command === "provider") { + return runProviderCommand(argv); + } + throw new Error(`Unknown CLI command: ${command}`); } diff --git a/bin/cli/io.mjs b/bin/cli/io.mjs index e131ba4ac0..97b419dc77 100644 --- a/bin/cli/io.mjs +++ b/bin/cli/io.mjs @@ -54,3 +54,7 @@ export function printSuccess(message) { export function printInfo(message) { console.log(`\x1b[2m${message}\x1b[0m`); } + +export function printError(message) { + console.log(`\x1b[31mโœ– ${message}\x1b[0m`); +} diff --git a/bin/omniroute.mjs b/bin/omniroute.mjs index 1ce5ca86f7..54febbffc1 100644 --- a/bin/omniroute.mjs +++ b/bin/omniroute.mjs @@ -79,7 +79,16 @@ loadEnvFile(); const args = process.argv.slice(2); const command = args[0]; -const CLI_COMMANDS = new Set(["doctor", "providers", "setup"]); +const CLI_COMMANDS = new Set([ + "doctor", + "providers", + "setup", + "config", + "status", + "logs", + "update", + "provider", +]); if (CLI_COMMANDS.has(command)) { try { @@ -196,11 +205,23 @@ if (args.includes("--help") || args.includes("-h")) { omniroute providers available --search openai omniroute providers available --category api-key omniroute providers list - omniroute providers test - omniroute providers test-all - omniroute providers validate +omniroute providers test + omniroute providers test-all + omniroute providers validate - \x1b[1mAfter starting:\x1b[0m + \x1b[1mCLI Tools:\x1b[0m + omniroute config list List CLI tool configuration status + omniroute config get Show config for a specific tool + omniroute config set Write config for a tool + omniroute config validate Validate config without writing + omniroute status Offline status dashboard + omniroute logs [--follow] [--filter] Stream usage logs + omniroute update [--check] [--dry-run] Check or apply OmniRoute update + omniroute provider add Add a provider connection + omniroute provider list List configured providers + omniroute provider test Test a provider connection + + \x1b[1mAfter starting:\x1b[0m Dashboard: http://localhost: API: http://localhost:/v1 diff --git a/docs/AUTO-COMBO.md b/docs/AUTO-COMBO.md new file mode 100644 index 0000000000..c042204b81 --- /dev/null +++ b/docs/AUTO-COMBO.md @@ -0,0 +1,134 @@ +# OmniRoute Auto-Combo Engine + +> Self-managing model chains with adaptive scoring + zero-config auto-routing + +## Zero-Config Auto-Routing (`auto/` prefix) + +> **NEW:** No combo creation required. Use `auto/` prefix directly in any client. + +### Quick Examples + +| Model ID | Variant | Behavior | +| -------------- | ------- | ------------------------------------------------------------------------ | +| `auto` | default | All connected providers, LKGP strategy, balanced weights | +| `auto/coding` | coding | Quality-first weights, suitable for code generation | +| `auto/fast` | fast | Low-latency weighted selection | +| `auto/cheap` | cheap | Cost-optimized routing (lowest cost first) | +| `auto/offline` | offline | Favors providers with highest quota availability | +| `auto/smart` | smart | Quality-first + higher exploration rate (10%) for better model discovery | +| `auto/lkgp` | lkgp | Explicit LKGP (same as default `auto`) | + +**How to use:** + +```bash +# Any IDE or CLI tool that supports OpenAI format +Base URL: http://localhost:20128/v1 +API Key: + +# In your code/config, set model to: +model: "auto" # balanced default +model: "auto/coding" # best for coding tasks +model: "auto/fast" # fastest available +model: "auto/cheap" # cheapest per token +``` + +**What happens:** + +1. OmniRoute detects `auto/` prefix in `src/sse/handlers/chat.ts` +2. Queries all **active provider connections** from the database +3. Filters to those with valid credentials (API key or OAuth token) +4. Determines the model per connection (`connection.defaultModel` or provider's first model) +5. Builds a **virtual combo** in-memory (not stored in DB) +6. Routes using the selected variant's weight profile + LKGP strategy + +**Key properties:** + +- โœ… **Always-on:** No toggle, no combo creation, no configuration needed +- โœ… **Dynamic:** Reflects current connected providers automatically +- โœ… **Session stickiness:** LKGP ensures last successful provider is prioritized +- โœ… **Multi-account aware:** Each provider connection becomes a separate candidate +- โœ… **No DB writes:** Virtual combo exists only for the request, zero persistence overhead + +**Behind the scenes:** + +```txt +Request: { model: "auto/coding" } + โ†“ +src/sse/handlers/chat.ts detects prefix + โ†“ +createVirtualAutoCombo('coding') โ†’ candidatePool from active connections + โ†“ +handleComboChat (same engine as persisted combos) + โ†“ +Auto-scoring selects best provider/model per request +``` + +**Implementation files:** + +| File | Purpose | +| --------------------------------------------------------- | ----------------------------------------- | +| `open-sse/services/autoCombo/autoPrefix.ts` | Prefix parser (`parseAutoPrefix`) | +| `open-sse/services/autoCombo/virtualFactory.ts` | Creates virtual `AutoComboConfig` objects | +| `open-sse/services/autoCombo/providerRegistryAccessor.ts` | Test hook for mocking provider registry | +| `src/sse/handlers/chat.ts` | Integration: auto prefix short-circuit | +| `src/shared/constants/providers.ts` | `SYSTEM_PROVIDERS.auto` system entry | + +## How It Works (Persisted Auto-Combos) + +The Auto-Combo Engine dynamically selects the best provider/model for each request using a **6-factor scoring function**: + +| Factor | Weight | Description | +| :--------- | :----- | :---------------------------------------------- | +| Quota | 0.20 | Remaining capacity [0..1] | +| Health | 0.25 | Circuit breaker: CLOSED=1.0, HALF=0.5, OPEN=0.0 | +| CostInv | 0.20 | Inverse cost (cheaper = higher score) | +| LatencyInv | 0.15 | Inverse p95 latency (faster = higher) | +| TaskFit | 0.10 | Model ร— task type fitness score | +| Stability | 0.10 | Low variance in latency/errors | + +## Mode Packs + +| Pack | Focus | Key Weight | +| :---------------------- | :----------- | :--------------- | +| ๐Ÿš€ **Ship Fast** | Speed | latencyInv: 0.35 | +| ๐Ÿ’ฐ **Cost Saver** | Economy | costInv: 0.40 | +| ๐ŸŽฏ **Quality First** | Best model | taskFit: 0.40 | +| ๐Ÿ“ก **Offline Friendly** | Availability | quota: 0.40 | + +## Self-Healing + +- **Temporary exclusion**: Score < 0.2 โ†’ excluded for 5 min (progressive backoff, max 30 min) +- **Circuit breaker awareness**: OPEN โ†’ auto-excluded; HALF_OPEN โ†’ probe requests +- **Incident mode**: >50% OPEN โ†’ disable exploration, maximize stability +- **Cooldown recovery**: After exclusion, first request is a "probe" with reduced timeout + +## Bandit Exploration + +5% of requests (configurable) are routed to random providers for exploration. Disabled in incident mode. + +## API + +```bash +# Create auto-combo +curl -X POST http://localhost:20128/api/combos/auto \ + -H "Content-Type: application/json" \ + -d '{"id":"my-auto","name":"Auto Coder","candidatePool":["anthropic","google","openai"],"modePack":"ship-fast"}' + +# List auto-combos +curl http://localhost:20128/api/combos/auto +``` + +## Task Fitness + +30+ models scored across 6 task types (`coding`, `review`, `planning`, `analysis`, `debugging`, `documentation`). Supports wildcard patterns (e.g., `*-coder` โ†’ high coding score). + +## Files + +| File | Purpose | +| :------------------------------------------- | :------------------------------------ | +| `open-sse/services/autoCombo/scoring.ts` | Scoring function & pool normalization | +| `open-sse/services/autoCombo/taskFitness.ts` | Model ร— task fitness lookup | +| `open-sse/services/autoCombo/engine.ts` | Selection logic, bandit, budget cap | +| `open-sse/services/autoCombo/selfHealing.ts` | Exclusion, probes, incident mode | +| `open-sse/services/autoCombo/modePacks.ts` | 4 weight profiles | +| `src/app/api/combos/auto/route.ts` | REST API | diff --git a/docs/CLI-TOOLS.md b/docs/CLI-TOOLS.md new file mode 100644 index 0000000000..b04aac3893 --- /dev/null +++ b/docs/CLI-TOOLS.md @@ -0,0 +1,492 @@ +# CLI Tools Setup Guide โ€” OmniRoute + +This guide explains how to install and configure all supported AI coding CLI tools +to use **OmniRoute** as the unified backend, giving you centralized key management, +cost tracking, model switching, and request logging across every tool. + +--- + +## How It Works + +``` +Claude / Codex / OpenCode / Cline / KiloCode / Continue / Kiro / Cursor / Copilot + โ”‚ + โ–ผ (all point to OmniRoute) + http://YOUR_SERVER:20128/v1 + โ”‚ + โ–ผ (OmniRoute routes to the right provider) + Anthropic / OpenAI / Gemini / DeepSeek / Groq / Mistral / ... +``` + +**Benefits:** + +- One API key to manage all tools +- Cost tracking across all CLIs in the dashboard +- Model switching without reconfiguring every tool +- Works locally and on remote servers (VPS) + +--- + +## Supported Tools (Dashboard Source of Truth) + +The dashboard cards in `/dashboard/cli-tools` are generated from `src/shared/constants/cliTools.ts`. +Current list (v3.0.0-rc.16): + +| Tool | ID | Command | Setup Mode | Install Method | +| ------------------ | ------------- | ---------- | ---------- | -------------- | +| **Claude Code** | `claude` | `claude` | env | npm | +| **OpenAI Codex** | `codex` | `codex` | custom | npm | +| **Factory Droid** | `droid` | `droid` | custom | bundled/CLI | +| **OpenClaw** | `openclaw` | `openclaw` | custom | bundled/CLI | +| **Cursor** | `cursor` | app | guide | desktop app | +| **Cline** | `cline` | `cline` | custom | npm | +| **Kilo Code** | `kilo` | `kilocode` | custom | npm | +| **Continue** | `continue` | extension | guide | VS Code | +| **Antigravity** | `antigravity` | internal | mitm | OmniRoute | +| **GitHub Copilot** | `copilot` | extension | custom | VS Code | +| **OpenCode** | `opencode` | `opencode` | guide | npm | +| **Kiro AI** | `kiro` | app/cli | mitm | desktop/CLI | +| **Qwen Code** | `qwen` | `qwen` | custom | npm | + +### CLI fingerprint sync (Agents + Settings) + +`/dashboard/agents` and `Settings > CLI Fingerprint` use `src/shared/constants/cliCompatProviders.ts`. +This keeps provider IDs aligned with CLI cards and legacy IDs. + +| CLI ID | Fingerprint Provider ID | +| ---------------------------------------------------------------------------------------------------- | ----------------------- | +| `kilo` | `kilocode` | +| `copilot` | `github` | +| `claude` / `codex` / `antigravity` / `kiro` / `cursor` / `cline` / `opencode` / `droid` / `openclaw` | same ID | + +Legacy IDs still accepted for compatibility: `copilot`, `kimi-coding`, `qwen`. + +--- + +## Step 1 โ€” Get an OmniRoute API Key + +1. Open the OmniRoute dashboard โ†’ **API Manager** (`/dashboard/api-manager`) +2. Click **Create API Key** +3. Give it a name (e.g. `cli-tools`) and select all permissions +4. Copy the key โ€” you'll need it for every CLI below + +> Your key looks like: `sk-xxxxxxxxxxxxxxxx-xxxxxxxxx` + +--- + +## Step 2 โ€” Install CLI Tools + +All npm-based tools require Node.js 18+: + +```bash +# Claude Code (Anthropic) +npm install -g @anthropic-ai/claude-code + +# OpenAI Codex +npm install -g @openai/codex + +# OpenCode +npm install -g opencode-ai + +# Cline +npm install -g cline + +# KiloCode +npm install -g kilocode + +# Kiro CLI (Amazon โ€” requires curl + unzip) +apt-get install -y unzip # on Debian/Ubuntu +curl -fsSL https://cli.kiro.dev/install | bash +export PATH="$HOME/.local/bin:$PATH" # add to ~/.bashrc +``` + +**Verify:** + +```bash +claude --version # 2.x.x +codex --version # 0.x.x +opencode --version # x.x.x +cline --version # 2.x.x +kilocode --version # x.x.x (or: kilo --version) +kiro-cli --version # 1.x.x +``` + +--- + +## Step 3 โ€” Set Global Environment Variables + +Add to `~/.bashrc` (or `~/.zshrc`), then run `source ~/.bashrc`: + +```bash +# OmniRoute Universal Endpoint +export OPENAI_BASE_URL="http://localhost:20128/v1" +export OPENAI_API_KEY="sk-your-omniroute-key" +export ANTHROPIC_BASE_URL="http://localhost:20128" +export ANTHROPIC_AUTH_TOKEN="sk-your-omniroute-key" +export GEMINI_BASE_URL="http://localhost:20128/v1" +export GEMINI_API_KEY="sk-your-omniroute-key" +``` + +> For a **remote server** replace `localhost:20128` with the server IP or domain, +> e.g. `http://192.168.0.15:20128`. + +--- + +## Step 4 โ€” Configure Each Tool + +### Claude Code + +```bash +# Create ~/.claude/settings.json: +mkdir -p ~/.claude && cat > ~/.claude/settings.json << EOF +{ + "env": { + "ANTHROPIC_BASE_URL": "http://localhost:20128", + "ANTHROPIC_AUTH_TOKEN": "sk-your-omniroute-key" + } +} +EOF +``` + +Use the unified Anthropic gateway root for Claude Code. Do not append `/v1` here. + +**Test:** `claude "say hello"` + +--- + +### OpenAI Codex + +```bash +mkdir -p ~/.codex && cat > ~/.codex/config.yaml << EOF +model: auto +apiKey: sk-your-omniroute-key +apiBaseUrl: http://localhost:20128/v1 +EOF +``` + +**Test:** `codex "what is 2+2?"` + +--- + +### OpenCode + +```bash +mkdir -p ~/.config/opencode && cat > ~/.config/opencode/config.toml << EOF +[provider.openai] +base_url = "http://localhost:20128/v1" +api_key = "sk-your-omniroute-key" +EOF +``` + +**Test:** `opencode` + +--- + +### Cline (CLI or VS Code) + +**CLI mode:** + +```bash +mkdir -p ~/.cline/data && cat > ~/.cline/data/globalState.json << EOF +{ + "apiProvider": "openai", + "openAiBaseUrl": "http://localhost:20128/v1", + "openAiApiKey": "sk-your-omniroute-key" +} +EOF +``` + +**VS Code mode:** +Cline extension settings โ†’ API Provider: `OpenAI Compatible` โ†’ Base URL: `http://localhost:20128/v1` + +Or use the OmniRoute dashboard โ†’ **CLI Tools โ†’ Cline โ†’ Apply Config**. + +--- + +### KiloCode (CLI or VS Code) + +**CLI mode:** + +```bash +kilocode --api-base http://localhost:20128/v1 --api-key sk-your-omniroute-key +``` + +**VS Code settings:** + +```json +{ + "kilo-code.openAiBaseUrl": "http://localhost:20128/v1", + "kilo-code.apiKey": "sk-your-omniroute-key" +} +``` + +Or use the OmniRoute dashboard โ†’ **CLI Tools โ†’ KiloCode โ†’ Apply Config**. + +--- + +### Continue (VS Code Extension) + +Edit `~/.continue/config.yaml`: + +```yaml +models: + - name: OmniRoute + provider: openai + model: auto + apiBase: http://localhost:20128/v1 + apiKey: sk-your-omniroute-key + default: true +``` + +Restart VS Code after editing. + +--- + +### Kiro CLI (Amazon) + +```bash +# Login to your AWS/Kiro account: +kiro-cli login + +# The CLI uses its own auth โ€” OmniRoute is not needed as backend for Kiro CLI itself. +# Use kiro-cli alongside OmniRoute for other tools. +kiro-cli status +``` + +--- + +### Qwen Code (Alibaba) + +Qwen Code supports OpenAI-compatible API endpoints via environment variables or `settings.json`. + +**Option 1: Environment variables (`~/.qwen/.env`)** + +```bash +mkdir -p ~/.qwen && cat > ~/.qwen/.env << EOF +OPENAI_API_KEY="sk-your-omniroute-key" +OPENAI_BASE_URL="http://localhost:20128/v1" +OPENAI_MODEL="auto" +EOF +``` + +**Option 2: `settings.json` with model providers** + +```json +// ~/.qwen/settings.json +{ + "env": { + "OPENAI_API_KEY": "sk-your-omniroute-key", + "OPENAI_BASE_URL": "http://localhost:20128/v1" + }, + "modelProviders": { + "openai": [ + { + "id": "omniroute-default", + "name": "OmniRoute (Auto)", + "envKey": "OPENAI_API_KEY", + "baseUrl": "http://localhost:20128/v1" + } + ] + } +} +``` + +**Option 3: Inline CLI flags** + +```bash +OPENAI_BASE_URL="http://localhost:20128/v1" \ +OPENAI_API_KEY="sk-your-omniroute-key" \ +OPENAI_MODEL="auto" \ +qwen +``` + +> For a **remote server** replace `localhost:20128` with the server IP or domain. + +**Test:** `qwen "say hello"` + +### Cursor (Desktop App) + +> **Note:** Cursor routes requests through its cloud. For OmniRoute integration, +> enable **Cloud Endpoint** in OmniRoute Settings and use your public domain URL. + +Via GUI: **Settings โ†’ Models โ†’ OpenAI API Key** + +- Base URL: `https://your-domain.com/v1` +- API Key: your OmniRoute key + +--- + +## Dashboard Auto-Configuration + +The OmniRoute dashboard automates configuration for most tools: + +1. Go to `http://localhost:20128/dashboard/cli-tools` +2. Expand any tool card +3. Select your API key from the dropdown +4. Click **Apply Config** (if tool is detected as installed) +5. Or copy the generated config snippet manually + +--- + +## Built-in Agents: Droid & OpenClaw + +**Droid** and **OpenClaw** are AI agents built directly into OmniRoute โ€” no installation needed. +They run as internal routes and use OmniRoute's model routing automatically. + +- Access: `http://localhost:20128/dashboard/agents` +- Configure: same combos and providers as all other tools +- No API key or CLI install required + +--- + +## Available API Endpoints + +| Endpoint | Description | Use For | +| -------------------------- | ----------------------------- | --------------------------- | +| `/v1/chat/completions` | Standard chat (all providers) | All modern tools | +| `/v1/responses` | Responses API (OpenAI format) | Codex, agentic workflows | +| `/v1/completions` | Legacy text completions | Older tools using `prompt:` | +| `/v1/embeddings` | Text embeddings | RAG, search | +| `/v1/images/generations` | Image generation | GPT-Image, Flux, etc. | +| `/v1/audio/speech` | Text-to-speech | ElevenLabs, OpenAI TTS | +| `/v1/audio/transcriptions` | Speech-to-text | Deepgram, AssemblyAI | + +### CLI Tools API (New in v3.8) + +| Endpoint | Method | Description | +| ------------------------------- | ------ | ------------------------------------------------ | +| `/api/cli-tools/detect` | GET | Detect all installed CLI tools and config status | +| `/api/cli-tools/detect?tool=ID` | GET | Detect a specific tool by ID | +| `/api/cli-tools/config` | GET | List generated configs for all tools | +| `/api/cli-tools/config` | POST | Generate config for a specific tool | +| `/api/cli-tools/apply` | POST | Apply config to a tool (with backup) | + +--- + +## CLI Commands Reference (New in v3.8) + +### `omniroute config` + +Manage CLI tool configurations directly from the terminal. + +```bash +omniroute config list # List all tools and config status +omniroute config get # Show config for a specific tool +omniroute config set \ # Generate and write config + --api-key sk-your-key \ + [--base-url http://localhost:20128/v1] \ + [--model auto] +omniroute config validate # Validate config without writing +``` + +**Options:** `--base-url`, `--api-key`, `--model`, `--json`, `--non-interactive`, `--yes`, `--help` + +### `omniroute status` + +Show offline status dashboard with version, database, and tool info. + +```bash +omniroute status # Human-readable status +omniroute status --json # JSON output +omniroute status --verbose # Include tool detection details +``` + +### `omniroute logs` + +Stream usage logs from the API endpoint. + +```bash +omniroute logs # Fetch last 100 log lines +omniroute logs --follow # Stream in real-time +omniroute logs --filter error,warn # Filter by level +omniroute logs --lines 500 # Fetch more lines +omniroute logs --base-url http://localhost:20128 +``` + +**Options:** `--follow`, `--filter`, `--lines`, `--timeout`, `--base-url`, `--json`, `--help` + +### `omniroute update` + +Check for or apply OmniRoute updates. + +```bash +omniroute update --check # Check for updates only +omniroute update --dry-run # Preview update without applying +omniroute update --yes # Apply update without prompt +omniroute update --no-backup # Skip backup creation +``` + +**Options:** `--check`, `--dry-run`, `--backup`, `--no-backup`, `--yes`, `--help` + +### `omniroute provider` + +Manage provider connections from the CLI. + +```bash +omniroute provider add openai --api-key sk-xxx # Add a provider +omniroute provider list # List all providers +omniroute provider remove # Remove a provider +omniroute provider test # Test connectivity +omniroute provider default # Set default provider +``` + +**Options:** `--provider`, `--api-key`, `--provider-name`, `--default-model`, `--base-url`, `--json`, `--yes`, `--help` + +--- + +## Quick Setup Script (One Command) + +Set up all CLI tools and configure for OmniRoute: + +```bash +OMNIROUTE_URL="http://localhost:20128/v1" +OMNIROUTE_ANTHROPIC_URL="http://localhost:20128" +OMNIROUTE_KEY="sk-your-omniroute-key" + +npm install -g @anthropic-ai/claude-code @openai/codex opencode-ai cline kilocode @qwen-code/qwen-code + +# Kiro CLI +apt-get install -y unzip 2>/dev/null; curl -fsSL https://cli.kiro.dev/install | bash + +# Write configs +mkdir -p ~/.claude ~/.codex ~/.config/opencode ~/.continue + +cat > ~/.claude/settings.json <<< "{\"env\":{\"ANTHROPIC_BASE_URL\":\"$OMNIROUTE_ANTHROPIC_URL\",\"ANTHROPIC_AUTH_TOKEN\":\"$OMNIROUTE_KEY\"}}" +cat > ~/.codex/config.yaml <<< "model: auto\napiKey: $OMNIROUTE_KEY\napiBaseUrl: $OMNIROUTE_URL" +cat >> ~/.bashrc << EOF +export OPENAI_BASE_URL="$OMNIROUTE_URL" +export OPENAI_API_KEY="$OMNIROUTE_KEY" +export ANTHROPIC_BASE_URL="$OMNIROUTE_ANTHROPIC_URL" +export ANTHROPIC_AUTH_TOKEN="$OMNIROUTE_KEY" +EOF + +source ~/.bashrc +echo "โœ… All CLIs installed and configured for OmniRoute" +``` + +```bash +# Install all CLIs and configure for OmniRoute (replace with your key and server URL) +OMNIROUTE_URL="http://localhost:20128/v1" +OMNIROUTE_ANTHROPIC_URL="http://localhost:20128" +OMNIROUTE_KEY="sk-your-omniroute-key" + +npm install -g @anthropic-ai/claude-code @openai/codex opencode-ai cline kilocode @qwen-code/qwen-code + +# Kiro CLI +apt-get install -y unzip 2>/dev/null; curl -fsSL https://cli.kiro.dev/install | bash + +# Write configs +mkdir -p ~/.claude ~/.codex ~/.config/opencode ~/.continue + +cat > ~/.claude/settings.json <<< "{\"env\":{\"ANTHROPIC_BASE_URL\":\"$OMNIROUTE_ANTHROPIC_URL\",\"ANTHROPIC_AUTH_TOKEN\":\"$OMNIROUTE_KEY\"}}" +cat > ~/.codex/config.yaml <<< "model: auto\napiKey: $OMNIROUTE_KEY\napiBaseUrl: $OMNIROUTE_URL" +cat >> ~/.bashrc << EOF +export OPENAI_BASE_URL="$OMNIROUTE_URL" +export OPENAI_API_KEY="$OMNIROUTE_KEY" +export ANTHROPIC_BASE_URL="$OMNIROUTE_ANTHROPIC_URL" +export ANTHROPIC_AUTH_TOKEN="$OMNIROUTE_KEY" +EOF + +source ~/.bashrc +echo "โœ… All CLIs installed and configured for OmniRoute" +``` diff --git a/docs/guides/SETUP_GUIDE.md b/docs/guides/SETUP_GUIDE.md index f3fb9af3ac..4031ea6160 100644 --- a/docs/guides/SETUP_GUIDE.md +++ b/docs/guides/SETUP_GUIDE.md @@ -91,16 +91,21 @@ Combined with env vars (`INITIAL_PASSWORD`, `OMNIROUTE_WS_BRIDGE_SECRET`, etc.), ### CLI Options -| Command | Description | -| ----------------------- | ----------------------------------------------------------- | -| `omniroute` | Start server (`PORT=20128`, API and dashboard on same port) | -| `omniroute setup` | Guided CLI onboarding for password and first provider | -| `omniroute doctor` | Run local health checks without starting the server | -| `omniroute providers` | Discover, list, validate, and test providers from CLI | -| `omniroute --port 3000` | Set canonical/API port to 3000 | -| `omniroute --mcp` | Start MCP server (stdio transport) | -| `omniroute --no-open` | Don't auto-open browser | -| `omniroute --help` | Show help | +| Command | Description | +| ----------------------- | -------------------------------------------------------------- | +| `omniroute` | Start server (`PORT=20128`, API and dashboard on same port) | +| `omniroute setup` | Guided CLI onboarding for password and first provider | +| `omniroute doctor` | Run local health checks without starting the server | +| `omniroute providers` | Discover, list, validate, and test providers from CLI | +| `omniroute config` | CLI tool configuration โ€” list, get, set, validate configs | +| `omniroute status` | Offline status dashboard โ€” version, DB, tools, config | +| `omniroute logs` | Stream usage logs from the API (supports `--follow`) | +| `omniroute update` | Check for or apply OmniRoute updates | +| `omniroute provider` | Manage provider connections โ€” add, list, remove, test, default | +| `omniroute --port 3000` | Set canonical/API port to 3000 | +| `omniroute --mcp` | Start MCP server (stdio transport) | +| `omniroute --no-open` | Don't auto-open browser | +| `omniroute --help` | Show help | Headless setup can be scripted with flags or environment variables: diff --git a/package.json b/package.json index d639f8853e..d9fc1076f4 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,8 @@ "files": [ "bin/", "app/", + "src/lib/cli-helper/", + "@omniroute/", "open-sse/mcp-server/index.ts", "open-sse/mcp-server/server.ts", "open-sse/mcp-server/httpTransport.ts", diff --git a/src/app/(dashboard)/dashboard/settings/components/RoutingTab.tsx b/src/app/(dashboard)/dashboard/settings/components/RoutingTab.tsx index 82f5110939..23b1c52259 100644 --- a/src/app/(dashboard)/dashboard/settings/components/RoutingTab.tsx +++ b/src/app/(dashboard)/dashboard/settings/components/RoutingTab.tsx @@ -296,8 +296,16 @@ export default function RoutingTab() { }} disabled={loading || forced} aria-pressed={checked} +<<<<<<< HEAD aria-disabled={forced || undefined} title={titleText} +======= + title={ + checked + ? t("disableFingerprintTitle", { provider: label }) + : t("enableFingerprintTitle", { provider: label }) + } +>>>>>>> a10ef5ee (feat(auto): complete zero-config auto-routing feature) className={`flex items-start gap-3 rounded-lg border p-3 text-left transition-all ${ checked ? "border-indigo-500/50 bg-indigo-500/5 ring-1 ring-indigo-500/20" diff --git a/src/app/api/cli-tools/apply/route.ts b/src/app/api/cli-tools/apply/route.ts new file mode 100644 index 0000000000..8547401b39 --- /dev/null +++ b/src/app/api/cli-tools/apply/route.ts @@ -0,0 +1,82 @@ +import { NextResponse } from "next/server"; +import { requireCliToolsAuth } from "@/lib/api/requireCliToolsAuth"; +import fs from "node:fs"; +import path from "node:path"; +import os from "node:os"; +import { generateConfig } from "@/lib/cli-helper/config-generator"; + +const TOOL_CONFIG_PATHS: Record = { + claude: path.join(os.homedir(), ".claude", "settings.json"), + codex: path.join(os.homedir(), ".codex", "config.yaml"), + opencode: path.join(os.homedir(), ".config", "opencode", "opencode.json"), + cline: path.join(os.homedir(), ".cline", "data", "globalState.json"), + kilocode: path.join(os.homedir(), ".config", "kilocode", "settings.json"), + continue: path.join(os.homedir(), ".continue", "config.yaml"), +}; + +function ensureBackup(configPath: string): string | null { + if (!fs.existsSync(configPath)) return null; + const backupDir = path.join(path.dirname(configPath), ".omniroute.bak"); + if (!fs.existsSync(backupDir)) fs.mkdirSync(backupDir, { recursive: true }); + const backupPath = path.join(backupDir, path.basename(configPath) + ".bak"); + fs.copyFileSync(configPath, backupPath); + return backupPath; +} + +// POST /api/cli-tools/apply - Apply config for a specific tool +export async function POST(request: Request) { + const authError = await requireCliToolsAuth(request); + if (authError) return authError; + + try { + const body = await request.json(); + const { toolId, baseUrl, apiKey, model, dryRun } = body; + + if (!toolId) { + return NextResponse.json({ error: "toolId is required" }, { status: 400 }); + } + if (!apiKey) { + return NextResponse.json({ error: "apiKey is required" }, { status: 400 }); + } + + const result = await generateConfig(toolId, { + baseUrl: baseUrl || "http://localhost:20128/v1", + apiKey, + model, + }); + + if (!result.success) { + return NextResponse.json({ error: result.error }, { status: 400 }); + } + + if (dryRun) { + return NextResponse.json({ + dryRun: true, + configPath: result.configPath, + content: result.content, + }); + } + + const configPath = TOOL_CONFIG_PATHS[toolId]; + if (!configPath) { + return NextResponse.json({ error: `Unknown tool: ${toolId}` }, { status: 400 }); + } + + const backupPath = ensureBackup(configPath); + + const dir = path.dirname(configPath); + if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); + + fs.writeFileSync(configPath, result.content!, "utf-8"); + + return NextResponse.json({ + success: true, + configPath, + backupPath, + content: result.content, + }); + } catch (error) { + console.log("Error applying config:", error); + return NextResponse.json({ error: "Failed to apply config" }, { status: 500 }); + } +} diff --git a/src/app/api/cli-tools/config/route.ts b/src/app/api/cli-tools/config/route.ts new file mode 100644 index 0000000000..35f74ef613 --- /dev/null +++ b/src/app/api/cli-tools/config/route.ts @@ -0,0 +1,61 @@ +import { NextResponse } from "next/server"; +import { requireCliToolsAuth } from "@/lib/api/requireCliToolsAuth"; +import { generateConfig, generateAllConfigs } from "@/lib/cli-helper/config-generator"; + +// GET /api/cli-tools/config - List generated configs for all tools +export async function GET(request: Request) { + const authError = await requireCliToolsAuth(request); + if (authError) return authError; + + const { searchParams } = new URL(request.url); + const baseUrl = searchParams.get("baseUrl") || "http://localhost:20128/v1"; + const apiKey = searchParams.get("apiKey") || ""; + + if (!apiKey) { + return NextResponse.json({ error: "API key is required" }, { status: 400 }); + } + + try { + const results = await generateAllConfigs({ baseUrl, apiKey }); + return NextResponse.json({ configs: results }); + } catch (error) { + console.log("Error generating configs:", error); + return NextResponse.json({ error: "Failed to generate configs" }, { status: 500 }); + } +} + +// POST /api/cli-tools/config - Generate config for a specific tool +export async function POST(request: Request) { + const authError = await requireCliToolsAuth(request); + if (authError) return authError; + + try { + const body = await request.json(); + const { toolId, baseUrl, apiKey, model } = body; + + if (!toolId) { + return NextResponse.json({ error: "toolId is required" }, { status: 400 }); + } + if (!apiKey) { + return NextResponse.json({ error: "apiKey is required" }, { status: 400 }); + } + + const result = await generateConfig(toolId, { + baseUrl: baseUrl || "http://localhost:20128/v1", + apiKey, + model, + }); + + if (!result.success) { + return NextResponse.json({ error: result.error }, { status: 400 }); + } + + return NextResponse.json({ + configPath: result.configPath, + content: result.content, + }); + } catch (error) { + console.log("Error generating config:", error); + return NextResponse.json({ error: "Failed to generate config" }, { status: 500 }); + } +} diff --git a/src/app/api/cli-tools/detect/route.ts b/src/app/api/cli-tools/detect/route.ts new file mode 100644 index 0000000000..58f0a28e97 --- /dev/null +++ b/src/app/api/cli-tools/detect/route.ts @@ -0,0 +1,28 @@ +import { NextResponse } from "next/server"; +import { requireCliToolsAuth } from "@/lib/api/requireCliToolsAuth"; +import { detectAllTools, detectTool } from "@/lib/cli-helper/tool-detector"; + +// GET /api/cli-tools/detect - Detect all installed CLI tools +export async function GET(request: Request) { + const authError = await requireCliToolsAuth(request); + if (authError) return authError; + + const { searchParams } = new URL(request.url); + const toolId = searchParams.get("tool"); + + try { + if (toolId) { + const tool = await detectTool(toolId); + if (!tool) { + return NextResponse.json({ error: `Unknown tool: ${toolId}` }, { status: 400 }); + } + return NextResponse.json(tool); + } + + const tools = await detectAllTools(); + return NextResponse.json({ tools }); + } catch (error) { + console.log("Error detecting tools:", error); + return NextResponse.json({ error: "Failed to detect tools" }, { status: 500 }); + } +} diff --git a/src/lib/cli-helper/config-generator/claude.ts b/src/lib/cli-helper/config-generator/claude.ts new file mode 100644 index 0000000000..488f72eeaa --- /dev/null +++ b/src/lib/cli-helper/config-generator/claude.ts @@ -0,0 +1,21 @@ +import path from "node:path"; +import os from "node:os"; + +const CONFIG_PATH = path.join(os.homedir(), ".claude", "settings.json"); + +export function generateClaudeConfig(options: { + baseUrl: string; + apiKey: string; + model?: string; +}): string { + const base = options.baseUrl.replace(/\/+$/, "").replace(/\/v1$/, ""); + const model = options.model || "claude-3-5-sonnet-20241022"; + + const config = { + baseUrl: `${base}/v1`, + authToken: options.apiKey, + models: [{ id: model }], + }; + + return JSON.stringify(config, null, 2); +} diff --git a/src/lib/cli-helper/config-generator/cline.ts b/src/lib/cli-helper/config-generator/cline.ts new file mode 100644 index 0000000000..5c5bc78765 --- /dev/null +++ b/src/lib/cli-helper/config-generator/cline.ts @@ -0,0 +1,19 @@ +import path from "node:path"; +import os from "node:os"; + +const CONFIG_PATH = path.join(os.homedir(), ".cline", "data", "globalState.json"); + +export function generateClineConfig(options: { + baseUrl: string; + apiKey: string; + model?: string; +}): string { + const base = options.baseUrl.replace(/\/+$/, "").replace(/\/v1$/, ""); + + const config = { + openAiBaseUrl: `${base}/v1`, + openAiApiKey: options.apiKey, + }; + + return JSON.stringify(config, null, 2); +} diff --git a/src/lib/cli-helper/config-generator/codex.ts b/src/lib/cli-helper/config-generator/codex.ts new file mode 100644 index 0000000000..4ec669e483 --- /dev/null +++ b/src/lib/cli-helper/config-generator/codex.ts @@ -0,0 +1,30 @@ +import path from "node:path"; +import os from "node:os"; + +let yaml: typeof import("js-yaml") | null = null; +async function loadYaml() { + if (!yaml) { + yaml = await import("js-yaml"); + } + return yaml; +} + +const CONFIG_PATH = path.join(os.homedir(), ".codex", "config.yaml"); + +export async function generateCodexConfig(options: { + baseUrl: string; + apiKey: string; + model?: string; +}): Promise { + const y = await loadYaml(); + const base = options.baseUrl.replace(/\/+$/, "").replace(/\/v1$/, ""); + + const config = { + openai: { + api_key: options.apiKey, + base_url: `${base}/v1`, + }, + }; + + return y.dump(config, { lineWidth: -1 }); +} diff --git a/src/lib/cli-helper/config-generator/continue.ts b/src/lib/cli-helper/config-generator/continue.ts new file mode 100644 index 0000000000..8eedb4afcf --- /dev/null +++ b/src/lib/cli-helper/config-generator/continue.ts @@ -0,0 +1,33 @@ +import path from "node:path"; +import os from "node:os"; + +let yaml: typeof import("js-yaml") | null = null; +async function loadYaml() { + if (!yaml) { + yaml = await import("js-yaml"); + } + return yaml; +} + +const CONFIG_PATH = path.join(os.homedir(), ".continue", "config.yaml"); + +export async function generateContinueConfig(options: { + baseUrl: string; + apiKey: string; + model?: string; +}): Promise { + const y = await loadYaml(); + const base = options.baseUrl.replace(/\/+$/, "").replace(/\/v1$/, ""); + + const config = { + models: [ + { + title: "OmniRoute", + apiKey: options.apiKey, + apiBase: `${base}/v1`, + }, + ], + }; + + return y.dump(config, { lineWidth: -1 }); +} diff --git a/src/lib/cli-helper/config-generator/index.ts b/src/lib/cli-helper/config-generator/index.ts new file mode 100644 index 0000000000..9431eaa12d --- /dev/null +++ b/src/lib/cli-helper/config-generator/index.ts @@ -0,0 +1,95 @@ +import path from "node:path"; +import os from "node:os"; + +export interface GenerateOptions { + baseUrl: string; + apiKey: string; + model?: string; +} + +export interface GenerateResult { + success: boolean; + configPath: string; + content?: string; + error?: string; +} + +function validateBaseUrl(url: string): boolean { + try { + const u = new URL(url); + return u.protocol === "http:" || u.protocol === "https:"; + } catch { + return false; + } +} + +function expandHome(p: string): string { + const home = os.homedir(); + return p.replace(/^~\//, home + "/"); +} + +const TOOL_CONFIG_PATHS: Record = { + claude: path.join(os.homedir(), ".claude", "settings.json"), + codex: path.join(os.homedir(), ".codex", "config.yaml"), + opencode: path.join(os.homedir(), ".config", "opencode", "opencode.json"), + cline: path.join(os.homedir(), ".cline", "data", "globalState.json"), + kilocode: path.join(os.homedir(), ".config", "kilocode", "settings.json"), + continue: path.join(os.homedir(), ".continue", "config.yaml"), +}; + +async function importGenerator(toolId: string) { + const generators: Record = { + claude: { module: "./claude.js", export: "generateClaudeConfig" }, + codex: { module: "./codex.js", export: "generateCodexConfig" }, + opencode: { module: "./opencode.js", export: "generateOpencodeConfig" }, + cline: { module: "./cline.js", export: "generateClineConfig" }, + kilocode: { module: "./kilocode.js", export: "generateKilocodeConfig" }, + continue: { module: "./continue.js", export: "generateContinueConfig" }, + }; + + const gen = generators[toolId]; + if (!gen) return null; + const mod = await import(gen.module); + return { generate: mod[gen.export] }; +} + +export async function generateConfig( + toolId: string, + options: GenerateOptions +): Promise { + if (!validateBaseUrl(options.baseUrl)) { + return { + success: false, + configPath: "", + error: "Invalid baseUrl: must be an absolute HTTP(S) URL", + }; + } + + if (!options.apiKey || options.apiKey.trim().length === 0) { + return { success: false, configPath: "", error: "API key is required" }; + } + + try { + const mod = await importGenerator(toolId); + if (!mod) { + return { success: false, configPath: "", error: `Unknown tool: ${toolId}` }; + } + const content = await mod.generate(options); + const configPath = TOOL_CONFIG_PATHS[toolId] || ""; + return { success: true, configPath, content }; + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + return { success: false, configPath: "", error: `Generation failed: ${msg}` }; + } +} + +export async function generateAllConfigs(options: GenerateOptions): Promise { + const toolIds = ["claude", "codex", "opencode", "cline", "kilocode", "continue"] as const; + const results = await Promise.allSettled(toolIds.map((id) => generateConfig(id, options))); + + return results.map((r) => + r.status === "fulfilled" + ? r.value + : { success: false, configPath: "", error: r.reason?.message || "Unknown error" } + ); +} diff --git a/src/lib/cli-helper/config-generator/kilocode.ts b/src/lib/cli-helper/config-generator/kilocode.ts new file mode 100644 index 0000000000..8c43dcd407 --- /dev/null +++ b/src/lib/cli-helper/config-generator/kilocode.ts @@ -0,0 +1,19 @@ +import path from "node:path"; +import os from "node:os"; + +const CONFIG_PATH = path.join(os.homedir(), ".config", "kilocode", "settings.json"); + +export function generateKilocodeConfig(options: { + baseUrl: string; + apiKey: string; + model?: string; +}): string { + const base = options.baseUrl.replace(/\/+$/, "").replace(/\/v1$/, ""); + + const config = { + apiKey: options.apiKey, + baseUrl: `${base}/v1`, + }; + + return JSON.stringify(config, null, 2); +} diff --git a/src/lib/cli-helper/config-generator/opencode.ts b/src/lib/cli-helper/config-generator/opencode.ts new file mode 100644 index 0000000000..b17214dac7 --- /dev/null +++ b/src/lib/cli-helper/config-generator/opencode.ts @@ -0,0 +1,21 @@ +import path from "node:path"; +import os from "node:os"; + +const CONFIG_PATH = path.join(os.homedir(), ".config", "opencode", "opencode.json"); + +export function generateOpencodeConfig(options: { + baseUrl: string; + apiKey: string; + model?: string; +}): string { + const base = options.baseUrl.replace(/\/+$/, "").replace(/\/v1$/, ""); + + const config = { + provider: "omniroute", + baseURL: `${base}/v1`, + apiKey: options.apiKey, + model: options.model || "opencode", + }; + + return JSON.stringify(config, null, 2); +} diff --git a/src/lib/cli-helper/doctor/checks.ts b/src/lib/cli-helper/doctor/checks.ts new file mode 100644 index 0000000000..353d0e6026 --- /dev/null +++ b/src/lib/cli-helper/doctor/checks.ts @@ -0,0 +1,41 @@ +import path from "node:path"; +import os from "node:os"; + +export interface DoctorCheckResult { + name: string; + status: "ok" | "warn" | "fail"; + message: string; + details: Record; +} + +export async function collectCliToolChecks(): Promise { + const { detectAllTools } = await import("../tool-detector.js"); + const tools = await detectAllTools(); + + return tools.map((tool) => { + if (!tool.installed) { + return { + name: `CLI: ${tool.name}`, + status: "warn" as const, + message: `${tool.name} not installed`, + details: { id: tool.id, installed: false }, + }; + } + + if (!tool.configured) { + return { + name: `CLI: ${tool.name}`, + status: "warn" as const, + message: `${tool.name} not configured for OmniRoute`, + details: { id: tool.id, configured: false }, + }; + } + + return { + name: `CLI: ${tool.name}`, + status: "ok" as const, + message: `${tool.name} configured`, + details: { id: tool.id, configured: true }, + }; + }); +} diff --git a/src/lib/cli-helper/log-streamer.ts b/src/lib/cli-helper/log-streamer.ts new file mode 100644 index 0000000000..9a4c237fed --- /dev/null +++ b/src/lib/cli-helper/log-streamer.ts @@ -0,0 +1,79 @@ +import os from "node:os"; +import path from "node:path"; + +export interface LogStreamOptions { + baseUrl?: string; + filters?: string[]; + follow?: boolean; + timeout?: number; +} + +export interface LogStream { + stream: ReadableStream; + stop: () => void; +} + +export function createLogStream(options: LogStreamOptions = {}): LogStream { + const baseUrl = options.baseUrl || "http://localhost:20128"; + const filters = options.filters || []; + const follow = options.follow ?? false; + const timeout = options.timeout || 30000; + + const controller = new AbortController(); + const { signal } = controller; + + const stream = new ReadableStream({ + async start(controller) { + let url = `${baseUrl}/api/cli-tools/logs?follow=${follow}`; + if (filters.length > 0) { + url += `&filter=${encodeURIComponent(filters.join(","))}`; + } + + const timeoutId = setTimeout(() => { + if (follow) return; // Don't timeout follow mode + controller.error(new Error(`Log stream timed out after ${timeout}ms`)); + }, timeout); + + try { + const response = await fetch(url, { signal }); + + if (!response.ok) { + controller.error(new Error(`HTTP ${response.status}: ${response.statusText}`)); + clearTimeout(timeoutId); + return; + } + + if (!response.body) { + controller.error(new Error("Response body is null")); + clearTimeout(timeoutId); + return; + } + + const reader = response.body.getReader(); + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + if (signal.aborted) break; + controller.enqueue(value); + } + + controller.close(); + clearTimeout(timeoutId); + } catch (err) { + if (signal.aborted) return; // Expected stop + controller.error(err instanceof Error ? err : new Error(String(err))); + clearTimeout(timeoutId); + } + }, + + cancel() { + controller.abort(); + }, + }); + + return { + stream, + stop: () => controller.abort(), + }; +} diff --git a/src/lib/cli-helper/tool-detector.ts b/src/lib/cli-helper/tool-detector.ts new file mode 100644 index 0000000000..7b48f46585 --- /dev/null +++ b/src/lib/cli-helper/tool-detector.ts @@ -0,0 +1,105 @@ +import os from "node:os"; +import path from "node:path"; +import { execFile } from "node:child_process"; +import { promisify } from "node:util"; + +const execFileAsync = promisify(execFile); + +export interface DetectedTool { + id: string; + name: string; + installed: boolean; + version?: string; + configPath: string; + configured: boolean; + configContents?: string; +} + +const TOOLS = [ + { id: "claude", name: "Claude Code", configPath: "~/.claude/settings.json" }, + { id: "codex", name: "Codex CLI", configPath: "~/.codex/config.yaml" }, + { id: "opencode", name: "OpenCode", configPath: "~/.config/opencode/opencode.json" }, + { id: "cline", name: "Cline", configPath: "~/.cline/data/globalState.json" }, + { id: "kilocode", name: "Kilo Code", configPath: "~/.config/kilocode/settings.json" }, + { id: "continue", name: "Continue", configPath: "~/.continue/config.yaml" }, +] as const; + +const BINARY_NAMES: Record = { + claude: "claude", + codex: "codex", + opencode: "opencode", + cline: "cline", + kilocode: "kilocode", + continue: "continue", +}; + +function expandHome(p: string): string { + const home = os.homedir(); + return p.replace(/^~\//, home + "/"); +} + +function isConfigured(content: string, baseUrl: string): boolean { + const normalized = baseUrl.replace(/\/+$/, ""); + return ( + content.includes(normalized) || + content.includes("localhost:20128") || + content.includes("OMNIROUTE_BASE_URL") + ); +} + +async function detectBinary(name: string): Promise<{ installed: boolean; version?: string }> { + const binary = BINARY_NAMES[name] || name; + try { + const { stdout } = await execFileAsync(binary, ["--version"], { timeout: 5000 }); + const version = stdout.trim().replace(/^v/, ""); + return { installed: true, version }; + } catch { + try { + // Try `which` as fallback + const { stdout } = await execFileAsync("which", [binary], { timeout: 5000 }); + if (stdout.trim()) { + return { installed: true }; + } + } catch {} + return { installed: false }; + } +} + +async function readConfigFile(configPath: string): Promise { + try { + const { readFileSync } = await import("node:fs"); + const expanded = expandHome(configPath); + if (!expanded) return null; + return readFileSync(expanded, "utf-8"); + } catch { + return null; + } +} + +export async function detectTool(id: string): Promise { + const tool = TOOLS.find((t) => t.id === id); + if (!tool) return null; + + const { installed, version } = await detectBinary(tool.id); + const configPath = expandHome(tool.configPath); + const configContents = await readConfigFile(tool.configPath); + const configured = !!configContents && isConfigured(configContents, "http://localhost:20128"); + + return { + id: tool.id, + name: tool.name, + installed, + version, + configPath, + configured, + configContents: configContents ?? undefined, + }; +} + +export async function detectAllTools(): Promise { + const results = await Promise.allSettled(TOOLS.map((t) => detectTool(t.id))); + + return results + .filter((r) => r.status === "fulfilled" && r.value !== null) + .map((r) => (r as PromiseFulfilledResult).value); +} diff --git a/src/shared/components/AutoRoutingBanner.test.tsx b/src/shared/components/AutoRoutingBanner.test.tsx new file mode 100644 index 0000000000..2249ef566e --- /dev/null +++ b/src/shared/components/AutoRoutingBanner.test.tsx @@ -0,0 +1,96 @@ +// @vitest-environment jsdom +import React from "react"; +import { act } from "react"; +import { createRoot } from "react-dom/client"; +import { afterEach, beforeEach, describe, expect, it } from "vitest"; + +const cleanupCallbacks: Array<() => void> = []; + +function makeContainer(): HTMLElement { + const container = document.createElement("div"); + document.body.appendChild(container); + cleanupCallbacks.push(() => { + container.remove(); + }); + return container; +} + +describe("AutoRoutingBanner", () => { + beforeEach(() => { + ( + globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean } + ).IS_REACT_ACT_ENVIRONMENT = true; + localStorage.clear(); + }); + + afterEach(() => { + while (cleanupCallbacks.length > 0) { + cleanupCallbacks.pop()?.(); + } + document.body.innerHTML = ""; + localStorage.clear(); + }); + + it("renders banner on first mount", async () => { + const { default: AutoRoutingBanner } = await import("./AutoRoutingBanner"); + const container = makeContainer(); + const root = createRoot(container); + await act(async () => { + root.render(); + }); + expect(container.querySelector('[role="banner"]')).toBeTruthy(); + expect(container.textContent).toContain("Auto-Routing Active"); + }); + + it("includes link to Combos page", async () => { + const { default: AutoRoutingBanner } = await import("./AutoRoutingBanner"); + const container = makeContainer(); + const root = createRoot(container); + await act(async () => { + root.render(); + }); + const link = container.querySelector('a[href="/dashboard/combos"]'); + expect(link).toBeTruthy(); + }); + + it("can be dismissed by clicking close button", async () => { + const { default: AutoRoutingBanner } = await import("./AutoRoutingBanner"); + const container = makeContainer(); + const root = createRoot(container); + await act(async () => { + root.render(); + }); + expect(container.querySelector('[role="banner"]')).toBeTruthy(); + const closeButton = container.querySelector('button[aria-label="Dismiss auto-routing banner"]'); + expect(closeButton).toBeTruthy(); + await act(async () => { + closeButton?.click(); + }); + expect(container.querySelector('[role="banner"]')).toBeFalsy(); + }); + + it("persists dismissal to localStorage", async () => { + const { default: AutoRoutingBanner } = await import("./AutoRoutingBanner"); + const container = makeContainer(); + const root = createRoot(container); + await act(async () => { + root.render(); + }); + const closeButton = container.querySelector('button[aria-label="Dismiss auto-routing banner"]'); + await act(async () => { + closeButton?.click(); + }); + expect(localStorage.getItem("auto-routing-banner-dismissed")).toBe("true"); + }); + + it("remains hidden after dismissal on remount", async () => { + localStorage.setItem("auto-routing-banner-dismissed", "true"); + const { default: AutoRoutingBanner } = await import("./AutoRoutingBanner"); + const container = makeContainer(); + const root = createRoot(container); + await act(async () => { + root.render(); + }); + expect(container.querySelector('[role="banner"]')).toBeFalsy(); + }); +}); diff --git a/src/sse/handlers/chat.ts b/src/sse/handlers/chat.ts index 9a9bb63a90..03d257337c 100644 --- a/src/sse/handlers/chat.ts +++ b/src/sse/handlers/chat.ts @@ -661,7 +661,7 @@ async function handleSingleModelChat( }); } - const { provider, model, sourceFormat, targetFormat, extendedContext } = resolved; + const { provider, model, sourceFormat, targetFormat, extendedContext, apiFormat } = resolved; const forceLiveComboTest = runtimeOptions.forceLiveComboTest === true; const hasForcedConnection = typeof runtimeOptions.forcedConnectionId === "string" && @@ -901,6 +901,7 @@ async function handleSingleModelChat( comboStepId: runtimeOptions.comboStepId ?? null, comboExecutionKey: runtimeOptions.comboExecutionKey ?? runtimeOptions.comboStepId ?? null, extendedContext, + modelApiFormat: apiFormat, providerProfile, cachedSettings: runtimeOptions.cachedSettings, });