diff --git a/CHANGELOG.md b/CHANGELOG.md index e62edded..7cb1a186 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,89 @@ +# v0.5.55 (2026-08-14) + +## Features +- **Auth**: native SAML 2.0 SSO alongside OIDC — AuthnRequest generation, ACS + assertion handling, SP metadata export, admin config test, replay-protected + via a `saml_state` cookie matched against `InResponseTo` +- **Providers**: add Alibaba Token Plan (`token-plan.ap-southeast-1`) — the + fourth Alibaba key type, Singapore-only and OpenAI-compatible transport only +- **Providers**: add `glm-5.3` to GLM Coding and GLM (China) +- **Providers**: Kimchi accepts API keys as well as OAuth (dual auth), with a + working Test Connection for both modes +- **Antigravity**: add Gemini 3.7 Flash and its tiered high/medium/low variants + (also in the Gemini registry) with pricing and quota tracking +- **TTS**: add Fish Audio — model id travels in an HTTP `model` header, voice + is a `reference_id` (preset or cloned voice model) +- **OpenCode-Go**: route by request format via declared transports instead of + forcing every client into `/messages` — Codex/OpenAI clients no longer pay a + lossy Responses→OpenAI→Claude double translation. Per-model `supportedFormats` + guard; the bespoke executor is gone (its shared `_lastModel` cache could cross + auth headers between concurrent requests) +- **Usage**: dedup + cache Claude quota calls (120s TTL keyed by access token, + in-flight promise dedup, last-good read on soft failure) to stop multiple + tabs tripping 429; manual refresh (↻) sends `force=1` to bypass the cache + +## Fixes +- **Docker**: ship `sql.js` in the image so the pure-JS DB fallback can start — + file tracing carried the package's JS without `dist/sql-wasm.wasm`, so a + container with no native driver aborted with ENOENT and never got a database + (#3248) +- **Usage**: read Gemini `usageMetadata` out of the antigravity `{ response }` + envelope — every non-streaming antigravity request logged `IN 0 | OUT 0` + (#3260) +- **Claude**: re-anchor passthrough cache breakpoints — the client's own + `cache_control` markers point at pre-normalization offsets, so the tail was + re-cached every request. Last system block and last tool pinned at 1h TTL, + last assistant turn at 5m, mid-conversation system messages folded into the + neighbouring user turn instead of hoisted into `body.system` +- **Combos**: detect images from Hermes and attachment payloads (`images[]`, + `experimental_attachments`, message-level `image_url`/`audio_url`, inline + `data:` URIs) so the Vision Adapter auto-switch fires for Hermes/Ollama/ + Vercel AI SDK shapes +- **Kiro**: intercept chat via `x-amz-target` — Kiro IDE 1.0.228+ moved + `GenerateAssistantResponse` to `POST /` + header, bypassing MITM. Also emit + the now-mandatory initial-response frame and map the `auto` model slot +- **Kiro**: report real output tokens and stop discarding usable turns +- **Qoder**: detect billing blocks at stream start and return a synthetic 403 + so combo/account fallback triggers instead of leaking the error into chat +- **Antigravity**: strip competitive system prompts (Zed IDE's Claude-agent + prompt) that Antigravity flags with a 429 Quota Exhausted +- **OpenCode**: send the official client fingerprint on free-tier requests so + the Console stops classifying traffic as unidentified and rate-limiting it; + session id resolves conversation-stable to preserve prompt caching +- **Responses**: don't close the message on an empty `tool_calls` array — some + providers attach one to every chunk, and the truthy check ended the message + on the first content token (#3234) +- **Translator**: preserve `prompt_cache_key` when converting chat to responses +- **Models**: expose snake_case token limits on `/v1/models` +- **Combos**: strip `stream_options` from the Fusion panel fan-out to avoid a + DeepSeek 400 (#3024); raise the dashboard model-test probe budget to 1024 and + soft-pass reasoning-only responses (#3010) +- **Headroom**: the toggle reflects the `headroomEnabled` setting even when the + proxy is down — it previously showed OFF while the engine kept calling + `/v1/compress`; proxy status stays visible via the status chip +- **Hermes**: add the `api_key` parameter to the model block in YAML config +- **Providers**: add llm7 to provider test support + +## Docs +- **i18n**: add Spanish, French, and Brazilian Portuguese README translations + +## Security +- **Real IP**: `x-9r-real-ip` and the Host fallback were trusted from + client-controlled headers whenever `custom-server.js` was not in the request + path (`npm run start`, `start:bun`), letting a remote caller pose as local to + skip API key auth and reach `LOCAL_ONLY_PATHS` (`/api/mcp/*`, + `/api/tunnel/enable`, `/api/auth/reset-password`). The server now stamps a + per-process `x-9r-peer-token` on every request it sanitizes and only trusts + `x-9r-real-ip` behind it — falling back to Host in development and failing + closed in production (GHSA-pjm4-8fpg-f9p6). Also fixes IPv6 loopback + detection (`::1`, `::ffff:127.0.0.1`) and routes `npm run start` / + `start:bun` through `custom-server.js` +- **Search**: `resolveBaseUrl()` rejects client-supplied non-public baseUrls + (SSRF guard on `/v1/search`) +- **Login**: fresh-install remote login with the default password returns 403 + without issuing a JWT +- **Usage**: `/api/usage/request-details` redacts request/response payloads + # v0.5.50 (2026-08-05) ## Features diff --git a/Dockerfile b/Dockerfile index da8eaacb..ad373f8a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -37,6 +37,9 @@ COPY --from=builder /app/src/mitm ./src/mitm COPY --from=builder /app/node_modules/node-forge ./node_modules/node-forge # Ensure `next` is available at runtime in case tracing did not include it. COPY --from=builder /app/node_modules/next ./node_modules/next +# sql.js loads dist/sql-wasm.wasm by path at runtime; tracing only follows JS imports, +# so the last-resort DB driver would abort with ENOENT on the missing binary. +COPY --from=builder /app/node_modules/sql.js ./node_modules/sql.js RUN mkdir -p /app/data && chown -R node:node /app && \ mkdir -p /app/data-home && chown node:node /app/data-home && \ diff --git a/README.md b/README.md index 553aa3cb..4c8e4f94 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,27 @@ -# 9Router - FREE AI Router & Token Saver +
+ 9Router Dashboard + + # 9Router - FREE AI Router & Token Saver + + **Never stop coding. Save 20-40% tokens with RTK + auto-fallback to FREE & cheap AI models.** + + **Connect All AI Code Tools (Claude Code, Cursor, Antigravity, Copilot, Codex, Gemini, OpenCode, Cline, OpenClaw...) to 40+ AI Providers & 100+ Models.** + + [![npm](https://img.shields.io/npm/v/9router.svg)](https://www.npmjs.com/package/9router) + [![Downloads](https://img.shields.io/npm/dm/9router.svg)](https://www.npmjs.com/package/9router) + [![Docker Pulls](https://img.shields.io/docker/pulls/decolua/9router.svg?logo=docker&label=Docker%20pulls)](https://hub.docker.com/r/decolua/9router) + [![GHCR](https://img.shields.io/badge/GHCR-decolua%2F9router-blue?logo=github)](https://github.com/decolua/9router/pkgs/container/9router) + [![License](https://img.shields.io/npm/l/9router.svg)](https://github.com/decolua/9router/blob/main/LICENSE) -9Router is a local AI routing gateway with provider fallback and token-saving features. +decolua%2F9router | Trendshift -![9Router](images/9router.png) +[🚀 Quick Start](#-quick-start) • [💡 Features](#-key-features) • [📖 Setup](#-setup-guide) • [🌐 Website](https://9router.com) + +[🇧🇷 Português (Brasil)](./i18n/README.pt-BR.md) • [🇻🇳 Tiếng Việt](./i18n/README.vi.md) • [🇨🇳 中文](./i18n/README.zh-CN.md) • [🇯🇵 日本語](./i18n/README.ja-JP.md) • [🇷🇺 Русский](./i18n/README.ru.md) • [🇹🇭 ไทย](./i18n/README.th.md) • [🇮🇷 فارسی](./i18n/README.fa_IR.md) • [🇮🇩 Indonesia](./i18n/README.id-ID.md) • [🇪🇸 Español](./i18n/README.es.md) • [🇫🇷 Français](./i18n/README.fr.md) + +
+ +--- ## New Features @@ -18,143 +37,83 @@ Automatically rotate proxy pools and provider connections for better availabilit ![Smart Rotation](images/smart-rotation.png) -## Installation for Beginners +## 🤔 Why 9Router? -The easiest installation method is Docker. Docker runs 9Router in a container, so you do not need to install Node.js, npm, or the application dependencies manually. +**Stop wasting money, tokens and hitting limits:** -### 1. Install Docker +- ❌ 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 -Install Docker Desktop on Windows or macOS, or install Docker Engine and Docker Compose on Linux: +**9Router solves this:** -- [Docker Desktop](https://www.docker.com/products/docker-desktop/) -- [Docker Engine for Linux](https://docs.docker.com/engine/install/) +- ✅ **RTK Token Saver** - Auto-compress tool_result content, save 20-40% tokens per request +- ✅ **Maximize subscriptions** - Track quota, use every bit before reset +- ✅ **Auto fallback** - Subscription → Cheap → Free, zero downtime +- ✅ **Multi-account** - Round-robin between accounts per provider +- ✅ **Universal** - Works with Claude Code, Codex, Cursor, Cline, any CLI tool -After installation, open a terminal and check that Docker is available: +--- + +## 🔄 How It Works + +``` +┌─────────────┐ +│ Your CLI │ (Claude Code, Codex, OpenClaw, Cursor, Cline...) +│ Tool │ +└──────┬──────┘ + │ http://localhost:20128/v1 + ↓ +┌─────────────────────────────────────────────┐ +│ 9Router (Smart Router) │ +│ • RTK Token Saver (cut tool_result tokens) │ +│ • Format translation (OpenAI ↔ Claude) │ +│ • Quota tracking │ +│ • Auto token refresh │ +└──────┬──────────────────────────────────────┘ + │ + ├─→ [Tier 1: SUBSCRIPTION] Claude Code, Codex, GitHub Copilot + │ ↓ quota exhausted + ├─→ [Tier 2: CHEAP] GLM ($0.6/1M), MiniMax ($0.2/1M) + │ ↓ budget limit + └─→ [Tier 3: FREE] Kiro, OpenCode Free, Vertex ($300 credits) + +Result: Never stop coding, minimal cost + 20-40% token savings via RTK +``` + +--- + +## ⚡ Quick Start + +**1. Install globally:** ```bash -docker --version -docker compose version +npm install -g 9router +9router ``` -Both commands should print a version number. +🎉 Dashboard opens at `http://localhost:20128` -### 2. Download the project +**2. Connect a FREE provider (no signup needed):** -Clone this repository, then enter its folder: +Dashboard → Providers → Connect **Kiro AI** (~50 credits/month free: Claude 4.5 + GLM-5 + MiniMax) or **OpenCode Free** (no auth) → Done! -```bash -git clone https://github.com/mhiqrambg/9router-mibp-version.git -cd 9router-mibp-version +**3. Use in your CLI tool:** + +``` +Claude Code/Codex/OpenClaw/Cursor/Cline Settings: + Endpoint: http://localhost:20128/v1 + API Key: [copy from dashboard] + Model: kr/claude-sonnet-4.5 ``` -If Git is not installed, download the repository as a ZIP file from GitHub and open the extracted folder in a terminal. +**That's it!** Start coding with FREE AI models. -### 3. Create the environment file +**Alternative: run from source (this repository):** -Create `.env` from the example file: - -```bash -cp .env.example .env -``` - -Open `.env` in a text editor and change at least these values: - -```env -JWT_SECRET=replace-with-a-long-random-secret -INITIAL_PASSWORD=replace-with-your-dashboard-password -API_KEY_SECRET=replace-with-another-long-random-secret -MACHINE_ID_SALT=replace-with-another-random-secret -REQUIRE_API_KEY=true -``` - -Do not share `.env` or commit it to Git. It contains the dashboard password and security secrets. On Linux or macOS, you can generate random values with: - -```bash -openssl rand -hex 32 -``` - -Use a different generated value for each secret. - -### 4. Start 9Router - -Run this command in the project folder: - -```bash -docker compose up -d -``` - -The first run downloads the 9Router and Headroom images. The `-d` option keeps them running in the background. - -Check the containers: - -```bash -docker compose ps -``` - -Both `9router` and `headroom` should have a running status. - -### 5. Open the dashboard - -Open this address in your browser: - -```text -http://localhost:20128/dashboard -``` - -Log in with the `INITIAL_PASSWORD` value from `.env`. - -### 6. Create an API key - -In the dashboard, open **Endpoint & Key** and create a key. Keep the complete key that starts with `sk-`. - -Use it when calling the API: - -```bash -curl http://localhost:20128/v1/models \ - -H "Authorization: Bearer YOUR_API_KEY" -``` - -For a server exposed through a domain, replace the URL with your domain: - -```bash -curl https://your-domain.example/v1/models \ - -H "Authorization: Bearer YOUR_API_KEY" -``` - -Remote requests without a valid API key are rejected. The API key is stored in the application database, not in `.env`. - -### Useful Docker commands - -View live logs: - -```bash -docker compose logs -f 9router -``` - -Restart the application: - -```bash -docker compose restart 9router -``` - -Stop the services without deleting data: - -```bash -docker compose down -``` - -Update to the latest image: - -```bash -docker compose pull -docker compose up -d -``` - -The Docker volume named `9router-data` keeps your settings, providers, and API keys when the container is recreated. - -### Install from source - -This option is intended for development. Install Node.js 22 or newer, then run: +This repository package is private (`9router-app`), so source/Docker execution is the expected local development path. ```bash cp .env.example .env @@ -162,9 +121,1427 @@ npm install PORT=20128 NEXT_PUBLIC_BASE_URL=http://localhost:20128 npm run dev ``` -Open `http://localhost:20128/dashboard` after the development server starts. +Production mode: -### Update +```bash +npm run build +PORT=20128 HOSTNAME=0.0.0.0 NEXT_PUBLIC_BASE_URL=http://localhost:20128 npm run start +``` + +Default URLs: + +- Dashboard: `http://localhost:20128/dashboard` +- OpenAI-compatible API: `http://localhost:20128/v1` + +--- + +## Video Guides + +
+ + + + + + + + + + + + + + + + + + + + + + + +
+ + Tiết kiệm chi phí LLM với 9Router +
+ 🇻🇳 Tiếng Việt
+ Tiết kiệm chi phí LLM cho OpenClaw với 9Router
by Mì AI
+
+ + 9Router + Claude Code FREE Unlimited Setup +
+ 🇵🇰 اردو / हिन्दी
+ 9Router + Claude Code FREE Unlimited Setup
by Build AI With Hamid
+
+ + 9Router Setup Tutorial +
+ 🇺🇸 English
+ 9Router + Claude Code FREE Setup
by Build AI With Hamid
+
+ + 9Router Setup Tutorial +
+ 🇺🇸 English
+ 9Router + Claude Code FREE Setup
by Build AI With Hamid
+
+ + Claude Code FREE Forever +
+ 🇺🇸 English
+ Claude Code FREE Forever — Unlimited Models
by Build AI With Hamid
+
+ + Claude CLI Free Setup +
+ 🇺🇸 English
+ Claude CLI Free Setup with 9Router 🚀
by CodeVerse Soban
+
+ + Cài đặt OpenClaw Free A-Z +
+ 🇻🇳 Tiếng Việt
+ Cài Đặt OpenClaw Free Từ A-Z + 9Router
by Mai Gia
+
+ + FREE OpenClaw with Claude Opus +
+ 🇺🇸 English
+ FREE OpenClaw + Claude Opus 4.6
by Build AI With Hamid
+
+ + Claude CLI Free Setup +
+ 🇮🇩 Indonesia
+ Koding 24 Jam Anti Rate Limit! Hemat Token AI 65% | Tutorial Quick Setup 9Router 🚀
by Krisswuh
+
+ + Cara Deploy 9Router di Hugging Face GRATIS Non-Stop! | Alternatif VPS RAM 16GB +
+ 🇮🇩 Indonesia
+ Cara Deploy 9Router di Hugging Face GRATIS Non-Stop! | Alternatif VPS RAM 16GB
by Krisswuh
+
+ + این شکلی از هر API ای استفاده کن برای هوش مصنوعی +
+ 🇮🇷 Persian-فارسی
+ این شکلی از هر API ای استفاده کن برای هوش مصنوعی
by Matin SenPai
+
+ + Hướng Dẫn Setup OpenClaw + 9Router: Tạo Bot Zalo AI Tự Động Từ A-Z +
+ 🇻🇳 Tiếng Việt
+ Hướng Dẫn Setup OpenClaw + 9Router: Tạo Bot Zalo AI Tự Động Từ A-Z
by tuanminhhole
+
+ +
+ +> 🎬 **Made a video about 9Router?** Submit a [Pull Request](https://github.com/decolua/9router/pulls) adding your video to this section — we'll merge it! + +--- + +## 🛠️ Supported CLI Tools + +9Router works seamlessly with all major AI coding tools: + +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+ Claude Code
+ Claude-Code +
+ OpenClaw
+ OpenClaw +
+ Codex
+ Codex +
+ OpenCode
+ OpenCode +
+ Cursor
+ Cursor +
+ Antigravity
+ Antigravity +
+ Cline
+ Cline +
+ Continue
+ Continue +
+ Droid
+ Droid +
+ Roo
+ Roo +
+ Copilot
+ Copilot +
+ Kilo Code
+ Kilo Code +
+ OpenDesign
+ OpenDesign +
+ jcode
+ jcode +
+ Grok Build
+ Grok Build +
+ Devin CLI
+ Devin CLI +
+ DeepSeek TUI
+ DeepSeek TUI +
+ Qwen Code
+ Qwen Code +
+
+ +--- + +## 🌐 Supported Providers + +### 🔐 OAuth Providers + +
+ + + + + + + + + +
+ Claude Code
+ Claude-Code +
+ Antigravity
+ Antigravity +
+ Codex
+ Codex +
+ GitHub
+ GitHub +
+ Cursor
+ Cursor +
+ Kimchi
+ Kimchi +
+
+ +### 🆓 Free Providers + +
+ + + + + + +
+ Kiro
+ Kiro AI
+ Claude 4.5 + GLM-5 + MiniMax
50 credits/month free
+
+ OpenCode Free
+ OpenCode Free
+ No auth • Auto-fetch models
Free (model list varies)
+
+ Vertex AI
+ Vertex AI
+ Gemini 3 Pro + GLM-5 + DeepSeek
$300 credits free
+
+
+ +> **Note:** iFlow, Qwen Code and Gemini CLI free tiers were discontinued in 2026. Use Kiro / OpenCode Free / Vertex instead. +> +> **Kiro AI** moved to a paid model in Sep 2025 — the free tier is now capped at **50 credits/month** (plus 500 trial credits for new accounts in the first 30 days). Paid tiers: Pro $20/mo (1,000 credits), Pro+ $40/mo (2,000), Pro Max $100/mo (5,000), Power $200/mo (10,000). +> **OpenCode Free** model list fluctuates over time (some models free only for limited promos) — subject to change without notice. +> **Vertex AI**: the $300 free credit for new GCP accounts is still valid, but since Mar 2026 the **Gemini API endpoint no longer consumes these credits** — call the **Vertex AI Studio** endpoint instead. + +### 🔑 API Key Providers (40+) + +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+ OpenRouter
+ OpenRouter +
+ GLM
+ GLM +
+ Kimi
+ Kimi +
+ MiniMax
+ MiniMax +
+ OpenAI
+ OpenAI +
+ Anthropic
+ Anthropic +
+ Gemini
+ Gemini +
+ DeepSeek
+ DeepSeek +
+ Groq
+ Groq +
+ xAI
+ xAI +
+ Mistral
+ Mistral +
+ Perplexity
+ Perplexity +
+ Together
+ Together AI +
+ Fireworks
+ Fireworks +
+ Cerebras
+ Cerebras +
+ Cohere
+ Cohere +
+ NVIDIA
+ NVIDIA +
+ SiliconFlow
+ SiliconFlow +
+

...and 20+ more providers including Nebius, Chutes, Hyperbolic, and custom OpenAI/Anthropic compatible endpoints

+
+ +### 🏠 Self-hosted Providers + +For speech and embeddings served from **your own** machine — whisper.cpp, +faster-whisper, Speaches, Kokoro-FastAPI, openedai-speech, llama.cpp/llama-server, +vLLM, Infinity, text-embeddings-inference, or anything else that speaks the OpenAI +shape. + +| Provider | Endpoint used | Typical server | +| --- | --- | --- | +| **Self-hosted STT** | `/v1/audio/transcriptions` | whisper.cpp, faster-whisper | +| **Self-hosted TTS** | `/v1/audio/speech` | Kokoro-FastAPI, openedai-speech | +| **Self-hosted Embedding** | `/v1/embeddings` | llama-server, vLLM, Infinity | + +Every other speech provider is a named cloud service with a fixed endpoint. These +three read their address from **each connection**, so one provider can front +several machines and load-balance across them like any other. + +Set it on the connection as `providerSpecificData.baseUrl`: + +| Provider | Give it | Result | +| --- | --- | --- | +| Self-hosted STT | the full URL — `http://host:8080/v1/audio/transcriptions` | used as-is | +| Self-hosted TTS | the server root — `http://host:8880` | `+ /v1/audio/speech` | +| Self-hosted Embedding | the **OpenAI base**, `/v1` included — `http://host:8080/v1` | `+ /embeddings` | + +> **Mind the `/v1` on embeddings.** The adapter appends `/embeddings`, so +> `http://host:8080` resolves to `http://host:8080/embeddings` and misses the +> OpenAI route — llama-server answers **501**. Give it the same base URL an OpenAI +> client would use. A full `.../v1/embeddings` is also accepted, so a value pasted +> from a `curl` example works too. + +The API key is not checked by most local servers, but the field must be non-empty: +it is what gives the connection a credentials record, and `baseUrl` lives there. +Any placeholder works. + +Self-hosted Embedding has **no cloud fallback by design** — a connection saved +without a `baseUrl` is reported as a configuration error rather than quietly +falling back to `api.openai.com`, which would send your input text and API key to +a third party under a provider named "Self-hosted". + +--- + +## 💡 Key Features + +| Feature | What It Does | Why It Matters | +| --------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | ------------------------------------------------- | +| 🚀 **RTK Token Saver** ([RTK](https://github.com/rtk-ai/rtk) ⭐40K) | Compress tool outputs (`git diff`, `grep`, `ls`, `tree`...) before sending to LLM | Save **20-40% input tokens** per request | +| 🧠 **Headroom Token Saver** ([Headroom](https://github.com/chopratejas/headroom)) | Optional external `/v1/compress` proxy before provider routing | Save more context tokens without changing clients | +| 🪨 **Caveman Mode** ([Caveman](https://github.com/JuliusBrussee/caveman) ⭐52K) | Inject caveman-speak prompt → LLM replies terse, technical substance preserved | Save **up to 65% output tokens** | +| 🐴 **Ponytail** ([Ponytail](https://github.com/DietrichGebert/ponytail)) | Inject "lazy senior dev" prompt → LLM writes minimal, YAGNI-first code (Lite/Full/Ultra) | **Fewer output tokens, less refactoring** | +| 🎯 **Smart 3-Tier Fallback** | Auto-route: Subscription → Cheap → Free | Never stop coding, zero downtime | +| 📊 **Real-Time Quota Tracking** | Live token count + reset countdown | Maximize subscription value | +| 🔄 **Format Translation** | OpenAI ↔ Claude ↔ Gemini ↔ Cursor ↔ Kiro ↔ Vertex | Works with any CLI tool | +| 👥 **Multi-Account Support** | Multiple accounts per provider | Load balancing + redundancy | +| 🔄 **Auto Token Refresh** | OAuth tokens refresh automatically | No manual re-login needed | +| 🎨 **Custom Combos** | Create unlimited model combinations | Tailor fallback to your needs | +| 📝 **Request Logging** | Debug mode with full request/response logs | Troubleshoot issues easily | +| 💾 **Cloud Sync** | Sync config across devices | Same setup everywhere | +| 📊 **Usage Analytics** | Track tokens, cost, trends over time | Optimize spending | +| 🌐 **Deploy Anywhere** | Localhost, VPS, Docker, Cloudflare Workers | Flexible deployment options | + +Set `X-9Router-Token-Saver: off` to bypass all token savers for one chat request. + +
+📖 Feature Details + +### 🚀 RTK Token Saver + +Tool outputs (`git diff`, `grep`, `find`, `ls`, `tree`, log dumps...) often eat 30-50% of your prompt budget. RTK detects them and applies smart, lossless compression **before** the request hits the LLM: + +- **Filters:** `git-diff`, `git-status`, `grep`, `find`, `ls`, `tree`, `dedup-log`, `smart-truncate`, `read-numbered`, `search-list` +- **Auto-detect:** No config needed — RTK peeks the first 1KB of each `tool_result` and picks the right filter. +- **Safe by design:** If a filter fails, throws, or makes output bigger, RTK silently keeps the original text. Errors never break your request. +- **Universal:** Works across all formats (OpenAI, Claude, Gemini, Cursor, Kiro, OpenAI Responses) because it runs **before** any format translation. +- **Default ON:** Toggle anytime in Dashboard → Endpoint settings. + +``` +Without RTK: 47K tokens sent to LLM +With RTK: 28K tokens sent to LLM (40% saved · same context · same answer) +``` + +### 🧠 Headroom Token Saver + +Headroom is optional and runs separately. 9Router calls Headroom's local `/v1/compress` endpoint, then keeps normal routing, fallback, auth, and usage tracking: + +``` +Client → 9Router → Headroom /v1/compress → 9Router → provider +``` + +Local setup: + +```bash +pip install "headroom-ai[proxy]" +headroom proxy --port 8787 +``` + +Enable in Dashboard → Endpoint → Token Saver → Headroom. Default URL: `http://localhost:8787`. + +Docker examples: + +```bash +# Headroom service in same Docker network +http://headroom:8787 + +# Headroom running on host machine +http://host.docker.internal:8787 +``` + +If Headroom is down or returns an error, 9Router fails open and sends the original request. + +### 🐴 Ponytail (Lazy Senior Dev) + +Ponytail injects a _"lazy senior dev"_ system prompt into every request, biasing the LLM toward minimal, YAGNI-first code — deletion over addition, stdlib over new deps, one-liners over abstractions. Adapted from [DietrichGebert/ponytail](https://github.com/DietrichGebert/ponytail). + +- **Lite** — Build what's asked, name the lazier alternative. +- **Full** — YAGNI ladder enforced: stdlib → native → existing deps → one-liner → minimal code. +- **Ultra** — YAGNI extremist: deletion first, ship the one-liner, challenge the rest of the requirement in the same response. + +``` +Without Ponytail: verbose code, extra abstractions, "just in case" scaffolding +With Ponytail: shortest working diff, no unrequested abstractions, fewer tokens +``` + +Never trades away: input validation, error handling that prevents data loss, security, accessibility, or anything explicitly requested. Enable in Dashboard → Endpoint → Ponytail. Stacks with Caveman (output terseness) and RTK (input compression). + +### 🎯 Smart 3-Tier Fallback + +Create combos with automatic fallback: + +``` +Combo: "my-coding-stack" + 1. cc/claude-opus-4-6 (your subscription) + 2. glm/glm-4.7 (cheap backup, $0.6/1M) + 3. if/kimi-k2-thinking (free fallback) + +→ Auto switches when quota runs out or errors occur +``` + +### 📊 Real-Time Quota Tracking + +- Token consumption per provider +- Reset countdown (5-hour, daily, weekly) +- Cost estimation for paid tiers +- Monthly spending reports + +### 🔄 Format Translation + +Seamless translation between formats: + +- **OpenAI** ↔ **Claude** ↔ **Gemini** ↔ **Cursor** ↔ **Kiro** ↔ **Vertex** ↔ **Antigravity** ↔ **Ollama** ↔ **OpenAI Responses** +- Your CLI tool sends OpenAI format → 9Router translates → Provider receives native format +- Works with any tool that supports custom OpenAI endpoints + +### 👥 Multi-Account Support + +- Add multiple accounts per provider +- Auto round-robin or priority-based routing +- Fallback to next account when one hits quota + +### 🔄 Auto Token Refresh + +- OAuth tokens automatically refresh before expiration +- No manual re-authentication needed +- Seamless experience across all providers + +### 🎨 Custom Combos + +- Create unlimited model combinations +- Mix subscription, cheap, and free tiers +- Name your combos for easy access +- Share combos across devices with Cloud Sync + +### 📝 Request Logging + +- Enable debug mode for full request/response logs +- Track API calls, headers, and payloads +- Troubleshoot integration issues +- Export logs for analysis + +### 💾 Cloud Sync + +- Sync providers, combos, and settings across devices +- Automatic background sync +- Secure encrypted storage +- Access your setup from anywhere + +#### Cloud Runtime Notes + +- Prefer server-side cloud variables in production: + - `BASE_URL` (internal callback URL used by sync scheduler) + - `CLOUD_URL` (cloud sync endpoint base) +- `NEXT_PUBLIC_BASE_URL` and `NEXT_PUBLIC_CLOUD_URL` are still supported for compatibility/UI, but server runtime now prioritizes `BASE_URL`/`CLOUD_URL`. +- Cloud sync requests now use timeout + fail-fast behavior to avoid UI hanging when cloud DNS/network is unavailable. + +### 📊 Usage Analytics + +- Track token usage per provider and model +- Cost estimation and spending trends +- Monthly reports and insights +- Optimize your AI spending + +> **💡 IMPORTANT - Understanding Dashboard Costs:** +> +> The "cost" displayed in Usage Analytics is **for tracking and comparison purposes only**. +> 9Router itself **never charges** you anything. You only pay providers directly (if using paid services). +> +> **Example:** If your dashboard shows "$290 total cost" while using Kiro free models, this represents +> what you would have paid using paid APIs directly. Your actual cost = **$0** (Kiro free tier: ~50 credits/mo). +> +> Think of it as a "savings tracker" showing how much you're saving by using free models or +> routing through 9Router! + +### 🌐 Deploy Anywhere + +- 💻 **Localhost** - Default, works offline +- ☁️ **VPS/Cloud** - Share across devices +- 🐳 **Docker** - One-command deployment +- 🚀 **Cloudflare Workers** - Global edge network + +
+ +--- + +## 💰 Pricing at a Glance + +| Tier | Provider | Cost | Quota Reset | Best For | +| ------------------- | --------------------- | ------------ | ---------------- | --------------------------------------- | +| **🚀 TOKEN SAVER** | **RTK (built-in)** | **FREE** | Always on | **Save 20-40% tokens on EVERY request** | +| **💳 SUBSCRIPTION** | Claude Code (Pro/Max) | $20-200/mo | 5h + weekly | Already subscribed | +| | Codex (Plus/Pro) | $20-200/mo | 5h + weekly | OpenAI users | +| | GitHub Copilot | $10-19/mo | Monthly | GitHub users | +| | Cursor IDE | $20/mo | Monthly | Cursor users | +| **💰 CHEAP** | GLM-5.1 / GLM-4.7 | $0.6/1M | Daily 10AM | Budget backup | +| | MiniMax M2.7 | $0.2/1M | 5-hour rolling | Cheapest option | +| | Kimi K2.5 | $9/mo flat | 10M tokens/mo | Predictable cost | + | **🆓 FREE** | Kiro AI | $0 | 50 credits/mo | Claude 4.5 + GLM-5 + MiniMax free (paid tiers above) | + | | OpenCode Free | $0 | Varies* | No auth, auto-fetch models (list changes over time) | + | | Vertex AI | $300 credits | New GCP accounts | Gemini 3 Pro + DeepSeek + GLM-5 (use Vertex AI Studio endpoint for free credits) | + +**💡 Pro Tip:** RTK + Kiro AI + OpenCode Free combo = **$0 cost + 20-40% token savings**! + +--- + +### 📊 Understanding 9Router Costs & Billing + +**9Router Billing Reality:** + +✅ **9Router software = FREE forever** (open source, never charges) +✅ **Dashboard "costs" = Display/tracking only** (not actual bills) +✅ **You pay providers directly** (subscriptions or API fees) +✅ **FREE providers stay FREE** (Kiro ~50 credits/mo, OpenCode Free, Vertex $300 credits = $0 within free-tier limits) — note iFlow/Qwen/Gemini CLI free tiers were discontinued in 2026 +❌ **9Router never sends invoices** or charges your card + +**How Cost Display Works:** + +The dashboard shows **estimated costs** as if you were using paid APIs directly. This is **not billing** - it's a comparison tool to show your savings. + +**Example Scenario:** + +``` +Dashboard Display: +• Total Requests: 1,662 +• Total Tokens: 47M +• Display Cost: $290 + +Reality Check: +• Provider: Kiro (free tier: ~50 credits/mo) +• Actual Payment: $0.00 +• What $290 Means: Amount you SAVED by using free models! +``` + +**Payment Rules:** + +- **Subscription providers** (Claude Code, Codex): Pay them directly via their websites +- **Cheap providers** (GLM, MiniMax): Pay them directly, 9Router just routes +- **FREE providers** (iFlow, Kiro, Qwen): Genuinely free forever, no hidden charges +- **9Router**: Never charges anything, ever + +--- + +## 🎯 Use Cases + +### Case 1: "I have Claude Pro subscription" + +**Problem:** Quota expires unused, rate limits during heavy coding + +**Solution:** + +``` +Combo: "maximize-claude" + 1. cc/claude-opus-4-7 (use subscription fully) + 2. glm/glm-5.1 (cheap backup when quota out) + 3. kr/claude-sonnet-4.5 (free emergency fallback) + +Monthly cost: $20 (subscription) + ~$5 (backup) = $25 total +vs. $20 + hitting limits = frustration +``` + +### Case 2: "I want zero cost" + +**Problem:** Can't afford subscriptions, need reliable AI coding + +**Solution:** + +``` +Combo: "free-forever" + 1. kr/claude-sonnet-4.5 (Claude 4.5 free via Kiro, ~50 credits/mo) + 2. kr/glm-5 (GLM-5 free via Kiro) + 3. oc/ (OpenCode Free, no auth) + +Monthly cost: $0 +Quality: Production-ready models + RTK saves 20-40% tokens +``` + +### Case 3: "I need 24/7 coding, no interruptions" + +**Problem:** Deadlines, can't afford downtime + +**Solution:** + +``` +Combo: "always-on" + 1. cc/claude-opus-4-7 (best quality) + 2. cx/gpt-5.5 (second subscription) + 3. glm/glm-5.1 (cheap, resets daily) + 4. minimax/MiniMax-M2.7 (cheapest, 5h reset) + 5. kr/claude-sonnet-4.5 (free via Kiro, ~50 credits/mo) + +Result: 5 layers of fallback = zero downtime +Monthly cost: $20-200 (subscriptions) + $10-20 (backup) +``` + +### Case 4: "I want FREE AI in OpenClaw" + +**Problem:** Need AI assistant in messaging apps (WhatsApp, Telegram, Slack...), completely free + +**Solution:** + +``` +Combo: "openclaw-free" + 1. kr/claude-sonnet-4.5 (Claude 4.5 free) + 2. kr/glm-5 (GLM-5 free) + 3. kr/MiniMax-M2.5 (MiniMax free) + +Monthly cost: $0 +Access via: WhatsApp, Telegram, Slack, Discord, iMessage, Signal... +``` + +--- + +## ❓ Frequently Asked Questions + +
+📊 Why does my dashboard show high costs? + +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 by using free models or existing subscriptions through 9Router. + +**Example:** + +- **Dashboard shows:** "$290 total cost" +- **Reality:** You're using Kiro free models (~50 credits/mo) +- **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 9Router? + +**No.** 9Router 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 +- ✅ **Cheap providers** (GLM, MiniMax) → Pay them directly, 9Router just routes your requests +- ❌ **9Router itself** → **Never charges anything, ever** + +9Router 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? + +**Mostly!** The current FREE providers (Kiro, OpenCode Free, Vertex) are genuinely free, but free tiers have limits: + +These are free services offered by those respective companies: + +- **Kiro AI**: ~50 credits/month free (plus 500 trial credits for new accounts in the first 30 days) via AWS Builder ID / Google / GitHub OAuth. Paid tiers available above that. +- **OpenCode Free**: No-auth passthrough proxy, models auto-fetched from `opencode.ai/zen/v1/models`. The free model list fluctuates over time (some models free only for limited promos) — subject to change without notice. +- **Vertex AI**: $300 free credits for new Google Cloud accounts (90 days). Since Mar 2026 the Gemini API endpoint no longer consumes these credits — use the **Vertex AI Studio** endpoint instead. + +9Router just routes your requests to them - there's no "catch" or future billing from 9Router itself. They're truly free services, and 9Router makes them easy to use with fallback support. + +**Discontinued free tiers (no longer recommended):** + +- ❌ **iFlow**: Was free unlimited, now changed to paid (2026) +- ❌ **Qwen Code**: Free OAuth tier fully discontinued by Alibaba on 2026-04-15 +- ❌ **Gemini CLI**: Service fully shut down by Google on 2026-06-18 (replaced by the closed-source Antigravity CLI). Discontinued — do not use. + +
+ +
+💰 How do I minimize my actual AI costs? + +**Free-First Strategy:** + +1. **Start with 100% free combo:** + + ``` + 1. kr/glm-5 (GLM-5 free via Kiro, ~50 credits/mo) + 2. OpenCode Free models (no auth, auto-fetched) + 3. Vertex AI Gemini 3 Pro (using the Vertex AI Studio endpoint with $300 credits) + ``` + + **Cost: $0/month** (within Kiro's free credit cap; OpenCode/Vertex subject to their free-tier limits) + +2. **Add cheap backup** only if you need it: + + ``` + 4. glm/glm-4.7 ($0.6/1M tokens) + ``` + + **Additional cost: Only pay for what you actually use** + +3. **Use subscription providers last:** + - Only if you already have them + - 9Router helps maximize their value through quota tracking + +**Result:** Most users can operate at $0/month using only free tiers! + +
+ +
+📈 What if my usage suddenly spikes? + +9Router's smart fallback prevents surprise charges: + +**Scenario:** You're on a coding sprint and blow through your quotas + +**Without 9Router:** + +- ❌ Hit rate limit → Work stops → Frustration +- ❌ Or: Accidentally rack up huge API bills + +**With 9Router:** + +- ✅ Subscription hits limit → Auto-fallback to cheap tier +- ✅ Cheap tier gets expensive → Auto-fallback to free tier +- ✅ Never stop coding → Predictable costs + +**You're in control:** Set spending limits per provider in dashboard, and 9Router respects them. + +
+ +--- + +## 📖 Setup Guide + +
+🔐 Subscription Providers (Maximize Value) + +### Claude Code (Pro/Max) + +```bash +Dashboard → Providers → Connect Claude Code +→ OAuth login → Auto token refresh +→ 5-hour + weekly quota tracking + +Models: + cc/claude-opus-4-7 + cc/claude-opus-4-6 + cc/claude-sonnet-4-6 + cc/claude-haiku-4-5-20251001 +``` + +**Pro Tip:** Use Opus for complex tasks, Sonnet for speed. 9Router tracks quota per model! + +### OpenAI Codex (Plus/Pro) + +```bash +Dashboard → Providers → Connect Codex +→ OAuth login (port 1455) +→ 5-hour + weekly reset + +Models: + cx/gpt-5.5 + cx/gpt-5.4 + cx/gpt-5.3-codex + cx/gpt-5.2-codex +``` + +### GitHub Copilot + +```bash +Dashboard → Providers → Connect GitHub +→ OAuth via GitHub +→ Monthly reset (1st of month) + +Models: + gh/gpt-5.4 + gh/claude-opus-4.7 + gh/claude-sonnet-4.6 + gh/gemini-3.1-pro-preview + gh/grok-code-fast-1 +``` + +### Cursor IDE + +```bash +Dashboard → Providers → Connect Cursor +→ OAuth login +→ Monthly subscription + +Models: + cu/claude-4.6-opus-max + cu/claude-4.5-sonnet-thinking + cu/gpt-5.3-codex +``` + +
+ +
+💰 Cheap Providers (Backup) + +### GLM-5.1 / GLM-4.7 (Daily reset, $0.6/1M) + +1. Sign up: [Zhipu AI](https://open.bigmodel.cn/) +2. Get API key from Coding Plan +3. Dashboard → Add API Key: + - Provider: `glm` + - API Key: `your-key` + +**Use:** `glm/glm-5.1`, `glm/glm-5`, `glm/glm-4.7` + +**Pro Tip:** Coding Plan offers 3× quota at 1/7 cost! Reset daily 10:00 AM. + +### MiniMax M2.7 (5h reset, $0.20/1M) + +1. Sign up: [MiniMax](https://www.minimax.io/) +2. Get API key +3. Dashboard → Add API Key + +**Use:** `minimax/MiniMax-M2.7`, `minimax/MiniMax-M2.5` + +**Pro Tip:** Cheapest option for long context (1M tokens)! + +### Kimi K2.5 ($9/month flat) + +1. Subscribe: [Moonshot AI](https://platform.moonshot.ai/) +2. Get API key +3. Dashboard → Add API Key + +**Use:** `kimi/kimi-k2.5`, `kimi/kimi-k2.5-thinking` + +**Pro Tip:** Fixed $9/month for 10M tokens = $0.90/1M effective cost! + +
+ +
+🆓 FREE Providers (Recommended) + +### Kiro AI (Claude 4.5 + GLM-5 + MiniMax FREE) + +```bash +Dashboard → Connect Kiro +→ AWS Builder ID, AWS IAM Identity Center, Google, or GitHub +→ Unlimited usage + +Models: + kr/claude-sonnet-4.5 + kr/claude-haiku-4.5 + kr/glm-5 + kr/MiniMax-M2.5 + kr/qwen3-coder-next + kr/deepseek-3.2 +``` + +**Pro Tip:** Best free option for Claude. No API key, no payment, fully unlimited. + +### OpenCode Free (No auth, auto-fetch models) + +```bash +Dashboard → Connect OpenCode Free +→ No login required (passthrough proxy) +→ Models auto-fetched from opencode.ai/zen/v1/models +``` + +**Pro Tip:** Fastest setup. Just connect and start coding. + +### Vertex AI ($300 free credits for new GCP accounts) + +```bash +Dashboard → Connect Vertex AI +→ Upload Google Cloud Service Account JSON +→ Enable Vertex AI API in your GCP project + +Models: + vertex/gemini-3.1-pro-preview + vertex/gemini-3-flash-preview + vertex/gemini-2.5-flash + +Vertex Partner (Anthropic / DeepSeek / GLM / Qwen via Vertex): + vertex-partner/glm-5-maas + vertex-partner/deepseek-v3.2-maas + vertex-partner/qwen3-next-80b-a3b-thinking-maas +``` + +**Pro Tip:** New Google Cloud accounts get $300 credits free for 90 days. Plenty for daily coding. + +
+ +
+🎨 Create Combos + +### Example 1: Maximize Subscription → Cheap Backup + +``` +Dashboard → Combos → Create New + +Name: premium-coding +Models: + 1. cc/claude-opus-4-7 (Subscription primary) + 2. glm/glm-5.1 (Cheap backup, $0.6/1M) + 3. minimax/MiniMax-M2.7 (Cheapest fallback, $0.20/1M) + +Use in CLI: premium-coding + +Monthly cost example (100M tokens): + 80M via Claude (subscription): $0 extra + 15M via GLM: $9 + 5M via MiniMax: $1 + Total: $10 + your subscription +``` + +### Example 2: Free-Only (Zero Cost) + +``` +Name: free-combo +Models: + 1. kr/claude-sonnet-4.5 (Claude 4.5 free via Kiro, ~50 credits/mo) + 2. kr/glm-5 (GLM-5 free via Kiro) + 3. vertex/gemini-3.1-pro-preview ($300 free credits) + +Cost: $0 forever (+ 20-40% token savings via RTK)! +``` + +
+ +
+🔧 CLI Integration + +### Cursor IDE + +``` +Settings → Models → Advanced: + OpenAI API Base URL: http://localhost:20128/v1 + OpenAI API Key: [from 9router dashboard] + Model: cc/claude-opus-4-7 +``` + +Or use combo: `premium-coding` + +### Claude Code + +Edit `~/.claude/config.json`: + +```json +{ + "anthropic_api_base": "http://localhost:20128/v1", + "anthropic_api_key": "your-9router-api-key" +} +``` + +### Codex CLI + +```bash +export OPENAI_BASE_URL="http://localhost:20128" +export OPENAI_API_KEY="your-9router-api-key" + +codex "your prompt" +``` + +### OpenClaw + +**Option 1 — Dashboard (recommended):** + +``` +Dashboard → CLI Tools → OpenClaw → Select Model → Apply +``` + +**Option 2 — Manual:** Edit `~/.openclaw/openclaw.json`: + +```json +{ + "agents": { + "defaults": { + "model": { + "primary": "9router/kr/claude-sonnet-4.5" + } + } + }, + "models": { + "providers": { + "9router": { + "baseUrl": "http://127.0.0.1:20128/v1", + "apiKey": "sk_9router", + "api": "openai-completions", + "models": [ + { + "id": "kr/claude-sonnet-4.5", + "name": "Claude Sonnet 4.5 (Kiro Free)" + } + ] + } + } + } +} +``` + +> **Note:** OpenClaw only works with local 9Router. Use `127.0.0.1` instead of `localhost` to avoid IPv6 resolution issues. + +### Cline / Continue / RooCode + +``` +Provider: OpenAI Compatible +Base URL: http://localhost:20128/v1 +API Key: [from dashboard] +Model: cc/claude-opus-4-7 +``` + +
+ +
+🚀 Deployment + +### VPS Deployment + +```bash +# Clone and install +git clone https://github.com/decolua/9router.git +cd 9router +npm install +npm run build + +# Configure +export JWT_SECRET="your-secure-secret-change-this" +export INITIAL_PASSWORD="your-password" +export DATA_DIR="/var/lib/9router" +export PORT="20128" +export HOSTNAME="0.0.0.0" +export NODE_ENV="production" +export NEXT_PUBLIC_BASE_URL="http://localhost:20128" +export NEXT_PUBLIC_CLOUD_URL="https://9router.com" +export API_KEY_SECRET="endpoint-proxy-api-key-secret" +export MACHINE_ID_SALT="endpoint-proxy-salt" + +# Start +npm run start + +# Or use PM2 +npm install -g pm2 +pm2 start npm --name 9router -- start +pm2 save +pm2 startup +``` + +### Docker + +Published images (multi-platform `linux/amd64` + `linux/arm64`): + +- Docker Hub: [`decolua/9router`](https://hub.docker.com/r/decolua/9router) +- GHCR: [`ghcr.io/decolua/9router`](https://github.com/decolua/9router/pkgs/container/9router) + +**Quick start (use published image):** + +```bash +docker run -d \ + --name 9router \ + -p 20128:20128 \ + -v "$HOME/.9router:/app/data" \ + -e DATA_DIR=/app/data \ + decolua/9router:latest +``` + +→ Open http://localhost:20128 + +**Build from source (dev):** + +```bash +git clone https://github.com/decolua/9router.git +cd 9router/app +docker build -t 9router . +docker run -d --name 9router -p 20128:20128 \ + -v "$HOME/.9router:/app/data" -e DATA_DIR=/app/data 9router +``` + +**Container defaults:** + +- `PORT=20128` +- `HOSTNAME=0.0.0.0` + +**Useful commands:** + +```bash +docker logs -f 9router +docker restart 9router +docker stop 9router && docker rm 9router +docker pull decolua/9router:latest # update to latest +``` + +**Data persistence:** `$HOME/.9router/db/data.sqlite` on host ↔ `/app/data/db/data.sqlite` in container. + +### Environment Variables + +| Variable | Default | Description | +| ---------------------------------------------------- | ---------------------------------------- | ----------------------------------------------------------------------------------- | +| `JWT_SECRET` | Auto-generated (`~/.9router/jwt-secret`) | JWT signing secret for dashboard auth cookie (override to share across instances) | +| `INITIAL_PASSWORD` | `123456` | First login password when no saved hash exists | +| `DATA_DIR` | `~/.9router` | Main app data location (SQLite at `$DATA_DIR/db/data.sqlite`) | +| `PORT` | framework default | Service port (`20128` in examples) | +| `HOSTNAME` | framework default | Bind host (Docker defaults to `0.0.0.0`) | +| `NODE_ENV` | runtime default | Set `production` for deploy | +| `BASE_URL` | `http://localhost:20128` | Server-side internal base URL used by cloud sync jobs | +| `CLOUD_URL` | `https://9router.com` | Server-side cloud sync endpoint base URL | +| `NEXT_PUBLIC_BASE_URL` | `http://localhost:3000` | Backward-compatible/public base URL (prefer `BASE_URL` for server runtime) | +| `NEXT_PUBLIC_CLOUD_URL` | `https://9router.com` | Backward-compatible/public cloud URL (prefer `CLOUD_URL` for server runtime) | +| `API_KEY_SECRET` | `endpoint-proxy-api-key-secret` | HMAC secret for generated API keys | +| `MACHINE_ID_SALT` | `endpoint-proxy-salt` | Salt for stable machine ID hashing | +| `ENABLE_REQUEST_LOGS` | `false` | Enables request/response logs under `logs/` | +| `AUTH_COOKIE_SECURE` | `false` | Force `Secure` auth cookie (set `true` behind HTTPS reverse proxy) | +| `REQUIRE_API_KEY` | `false` | Enforce Bearer API key on `/v1/*` routes (recommended for internet-exposed deploys) | +| `HTTP_PROXY`, `HTTPS_PROXY`, `ALL_PROXY`, `NO_PROXY` | empty | Optional outbound proxy for upstream provider calls | +| `SEARXNG_URL` | `http://localhost:8888/search` | Endpoint for the built-in unauthenticated SearXNG web-search provider | + +Notes: + +- Lowercase proxy variables are also supported: `http_proxy`, `https_proxy`, `all_proxy`, `no_proxy`. +- `.env` is not baked into Docker image (`.dockerignore`); inject runtime config with `--env-file` or `-e`. +- On Windows, `APPDATA` can be used for local storage path resolution. +- `INSTANCE_NAME` appears in older docs/env templates, but is currently not used at runtime. + +### Runtime Files and Storage + +- Main app state: `${DATA_DIR}/db/data.sqlite` (SQLite — providers, combos, aliases, keys, settings, usage history) +- Auto backups: `${DATA_DIR}/db/backups/` +- Optional request/translator logs: `/logs/...` when `ENABLE_REQUEST_LOGS=true` +- Both `${DATA_DIR}` and `~/.9router` resolve to the same location in a Docker container — the symlink `/root/.9router -> /app/data` is created at build time. + +
+ +--- + +## 📊 Available Models + +
+View all available models + +**Claude Code (`cc/`)** - Pro/Max: + +- `cc/claude-opus-4-7` +- `cc/claude-opus-4-6` +- `cc/claude-sonnet-4-6` +- `cc/claude-sonnet-4-5-20250929` +- `cc/claude-haiku-4-5-20251001` + +**Codex (`cx/`)** - Plus/Pro: + +- `cx/gpt-5.5` +- `cx/gpt-5.4` +- `cx/gpt-5.3-codex` +- `cx/gpt-5.2-codex` +- `cx/gpt-5.1-codex-max` + +**GitHub Copilot (`gh/`)**: + +- `gh/gpt-5.4` +- `gh/claude-opus-4.7` +- `gh/claude-sonnet-4.6` +- `gh/gemini-3.1-pro-preview` +- `gh/grok-code-fast-1` + +**Cursor (`cu/`)** - Subscription: + +- `cu/claude-4.6-opus-max` +- `cu/claude-4.5-sonnet-thinking` +- `cu/gpt-5.3-codex` +- `cu/kimi-k2.5` + +**GLM (`glm/`)** - $0.6/1M: + +- `glm/glm-5.1` +- `glm/glm-5` +- `glm/glm-4.7` + +**MiniMax (`minimax/`)** - $0.2/1M: + +- `minimax/MiniMax-M2.7` +- `minimax/MiniMax-M2.5` + +**Kimi (`kimi/`)** - $9/mo flat: + +- `kimi/kimi-k2.5` +- `kimi/kimi-k2.5-thinking` + +**Kiro (`kr/`)** - Free (~50 credits/month, paid tiers above): + +- `kr/claude-sonnet-4.5` +- `kr/claude-haiku-4.5` +- `kr/glm-5` +- `kr/MiniMax-M2.5` +- `kr/qwen3-coder-next` +- `kr/deepseek-3.2` + +**OpenCode Free (`oc/`)** - FREE no-auth: + +- Auto-fetched from `opencode.ai/zen/v1/models` + +**Vertex AI (`vertex/`)** - $300 free credits: + +- `vertex/gemini-3.1-pro-preview` +- `vertex/gemini-3-flash-preview` +- `vertex/gemini-2.5-flash` +- `vertex-partner/glm-5-maas` +- `vertex-partner/deepseek-v3.2-maas` + +
+ +--- + +## 🐛 Troubleshooting + +**"Language model did not provide messages"** + +- Provider quota exhausted → Check dashboard quota tracker +- Solution: Use combo fallback or switch to cheaper tier + +**Rate limiting** + +- Subscription quota out → Fallback to GLM/MiniMax +- Add combo: `cc/claude-opus-4-7 → glm/glm-5.1 → kr/claude-sonnet-4.5` + +**OAuth token expired** + +- Auto-refreshed by 9Router +- If issues persist: Dashboard → Provider → Reconnect + +**High costs** + +- Enable RTK in Dashboard → Endpoint settings (default ON, saves 20-40% tokens) +- Check usage stats in Dashboard +- Switch primary model to GLM/MiniMax +- Use free tier (Kiro, OpenCode Free, Vertex) for non-critical tasks + +**Dashboard opens on wrong port** + +- Set `PORT=20128` and `NEXT_PUBLIC_BASE_URL=http://localhost:20128` + +**First login not working** + +- Check `INITIAL_PASSWORD` in `.env` +- If unset, fallback password is `123456` + +**No request logs under `logs/`** + +- Set `ENABLE_REQUEST_LOGS=true` + +--- + +## 🛠️ Tech Stack + +- **Runtime**: Node.js 20+ +- **Framework**: Next.js 16 +- **UI**: React 19 + Tailwind CSS 4 +- **Database**: SQLite (better-sqlite3 / node:sqlite / sql.js fallback) +- **Streaming**: Server-Sent Events (SSE) +- **Auth**: OAuth 2.0 (PKCE) + JWT + API Keys + +--- + +## 📝 API Reference + +### Chat Completions + +```bash +POST http://localhost:20128/v1/chat/completions +Authorization: Bearer your-api-key +Content-Type: application/json + +{ + "model": "cc/claude-opus-4-6", + "messages": [ + {"role": "user", "content": "Write a function to..."} + ], + "stream": true +} +``` + +### List Models + +```bash +GET http://localhost:20128/v1/models +Authorization: Bearer your-api-key + +→ Returns all models + combos in OpenAI format +``` + +## 📧 Support + +- **Website**: [9router.com](https://9router.com) +- **GitHub**: [github.com/decolua/9router](https://github.com/decolua/9router) +- **Issues**: [github.com/decolua/9router/issues](https://github.com/decolua/9router/issues) + +--- + +## 👥 Contributors + +Thanks to all contributors who helped make 9Router better! + +[![Contributors](https://contrib.rocks/image?repo=decolua/9router&max=150&columns=15&anon=1&v=20260309)](https://github.com/decolua/9router/graphs/contributors) + +--- + +## 📊 Star Chart + +[![Star Chart](https://starchart.cc/decolua/9router.svg?variant=adaptive)](https://starchart.cc/decolua/9router) + +## 🔀 Forks + +**[OmniRoute](https://github.com/diegosouzapw/OmniRoute)** — A full-featured TypeScript fork of 9Router. Adds 36+ providers, 4-tier auto-fallback, multi-modal APIs (images, embeddings, audio, TTS), circuit breaker, semantic cache, LLM evaluations, and a polished dashboard. 368+ unit tests. Available via npm and Docker. + +--- + +## 🙏 Acknowledgments + +Built on the shoulders of giants: + +- **[CLIProxyAPI](https://github.com/router-for-me/CLIProxyAPI)** — original Go implementation that inspired this JavaScript port. +- **[RTK](https://github.com/rtk-ai/rtk)** ![Stars](https://img.shields.io/github/stars/rtk-ai/rtk?style=flat&color=yellow) — Rust token-saver. 9Router ports its compression pipeline to JS → **−20-40% input tokens** on every request. +- **[Caveman](https://github.com/JuliusBrussee/caveman)** ![Stars](https://img.shields.io/github/stars/JuliusBrussee/caveman?style=flat&color=yellow) by **[@JuliusBrussee](https://github.com/JuliusBrussee)** — viral _"why use many token when few token do trick"_. 9Router adapts its prompt → **−65% output tokens**. +- **[Ponytail](https://github.com/DietrichGebert/ponytail)** ![Stars](https://img.shields.io/github/stars/DietrichGebert/ponytail?style=flat&color=yellow) by **[@DietrichGebert](https://github.com/DietrichGebert)** — _"lazy senior dev"_ skill. 9Router injects its YAGNI-first ladder → **fewer tokens, less code, shorter diffs**. + +Huge thanks to these authors — without their work, 9Router's token-saving features wouldn't exist. ⭐ them on GitHub! + +--- + +## 📄 License + +MIT License - see [LICENSE](LICENSE) for details. + +--- + +
+ Built with ❤️ for developers who code 24/7 +
+ +--- + +## Update This Fork This fork follows `decolua/9router` through the `upstream` remote. @@ -199,7 +1576,3 @@ docker compose up -d - [9Router upstream](https://github.com/decolua/9router) - [9Router MIBP Version](https://github.com/mhiqrambg/9router-mibp-version) - -## License - -MIT License. See [LICENSE](LICENSE) for details. diff --git a/cli/package.json b/cli/package.json index 98ad8cdc..2fe55c9c 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "9router", - "version": "0.5.50", + "version": "0.5.55", "description": "9Router CLI - Start and manage 9Router server", "bin": { "9router": "./cli.js" diff --git a/cli/scripts/build-cli.js b/cli/scripts/build-cli.js index d4e4d279..2c23d1e7 100644 --- a/cli/scripts/build-cli.js +++ b/cli/scripts/build-cli.js @@ -216,7 +216,9 @@ function buildCliPackage() { fs.copyFileSync(customServerSrc, path.join(cliAppDir, "custom-server.js")); console.log("✅ Copied custom-server.js\n"); } else { - console.warn("⚠️ custom-server.js not found — server will run without real-IP injection\n"); + console.error("❌ custom-server.js not found — without it no request can be proven local,"); + console.error(" so the packaged CLI would demand an API key for its own dashboard and /v1."); + process.exit(1); } // Step 3b: Ensure sql.js (pure JS fallback) bundled in app/cli/app/node_modules. diff --git a/cli/src/cli/menus/providers.js b/cli/src/cli/menus/providers.js index 7e28ec64..92d10a04 100644 --- a/cli/src/cli/menus/providers.js +++ b/cli/src/cli/menus/providers.js @@ -53,6 +53,9 @@ const PROVIDER_MODELS = { { id: "glm-4.7" }, ], ag: [ + { id: "gemini-3.7-flash-high" }, + { id: "gemini-3.7-flash-medium" }, + { id: "gemini-3.7-flash-low" }, { id: "gemini-3.6-flash-high" }, { id: "gemini-3.6-flash-medium" }, { id: "gemini-3.6-flash-low" }, diff --git a/custom-server.js b/custom-server.js index cf824ba4..ba092136 100644 --- a/custom-server.js +++ b/custom-server.js @@ -1,9 +1,18 @@ const http = require("http"); const path = require("path"); +const fs = require("fs"); +const crypto = require("crypto"); const { pathToFileURL } = require("url"); const origCreate = http.createServer.bind(http); +// Per-process secret proving x-9r-real-ip was stamped below rather than sent by the client. +// A bare `next start` / `next dev` never loads this file, so it cannot produce a matching +// header even though the env var is inherited by child processes. Named like x-9r-cli-token +// so the request-detail header sanitizer redacts it too. +const PEER_TOKEN = crypto.randomBytes(24).toString("hex"); +process.env.NINEROUTER_PEER_TOKEN = PEER_TOKEN; + let backgroundRefreshStarted = false; function startBackgroundTokenRefreshFromCustomServer() { @@ -57,7 +66,9 @@ http.createServer = (...args) => { delete req.headers["x-9r-real-ip"]; delete req.headers["x-forwarded-for"]; delete req.headers["x-9r-via-proxy"]; + delete req.headers["x-9r-peer-token"]; req.headers["x-9r-real-ip"] = ip; + req.headers["x-9r-peer-token"] = PEER_TOKEN; if (viaProxy) req.headers["x-9r-via-proxy"] = "1"; return handler(req, res); }; @@ -114,4 +125,15 @@ http.createServer = (...args) => { return server; }; -if (require.main === module) require("./server.js"); +if (require.main === module) { + const standalone = path.join(__dirname, "server.js"); + if (fs.existsSync(standalone)) { + require(standalone); + } else { + // Repo checkout has no standalone build next to us. `next start` builds its HTTP + // server in-process, so the wrapper above still sanitizes every request. + const nextBin = require.resolve("next/dist/bin/next"); + process.argv = [process.argv[0], nextBin, "start", ...process.argv.slice(2)]; + require(nextBin); + } +} diff --git a/docs/images/saml-admin-dashboard.png b/docs/images/saml-admin-dashboard.png new file mode 100644 index 00000000..53d0466e Binary files /dev/null and b/docs/images/saml-admin-dashboard.png differ diff --git a/docs/images/saml-login-screen.png b/docs/images/saml-login-screen.png new file mode 100644 index 00000000..b1995af1 Binary files /dev/null and b/docs/images/saml-login-screen.png differ diff --git a/i18n/README.es.md b/i18n/README.es.md new file mode 100644 index 00000000..5469fcb7 --- /dev/null +++ b/i18n/README.es.md @@ -0,0 +1,1445 @@ +
+ Panel de control de 9Router + + # 9Router - Enrutador de IA GRATUITO y ahorrador de tokens + + **Nunca dejes de programar. Ahorra entre 20-40% de tokens con RTK + reserva automática hacia modelos de IA GRATUITOS y económicos.** + + **Conecta todas tus herramientas de código con IA (Claude Code, Cursor, Antigravity, Copilot, Codex, Gemini, OpenCode, Cline, OpenClaw...) a más de 40 proveedores de IA y más de 100 modelos.** + + [![npm](https://img.shields.io/npm/v/9router.svg)](https://www.npmjs.com/package/9router) + [![Downloads](https://img.shields.io/npm/dm/9router.svg)](https://www.npmjs.com/package/9router) + [![Docker Pulls](https://img.shields.io/docker/pulls/decolua/9router.svg?logo=docker&label=Docker%20pulls)](https://hub.docker.com/r/decolua/9router) + [![GHCR](https://img.shields.io/badge/GHCR-decolua%2F9router-blue?logo=github)](https://github.com/decolua/9router/pkgs/container/9router) + [![License](https://img.shields.io/npm/l/9router.svg)](https://github.com/decolua/9router/blob/main/LICENSE) + +decolua%2F9router | Trendshift + +[🚀 Inicio rápido](#-inicio-rápido) • [💡 Características](#-características-principales) • [📖 Configuración](#-guía-de-instalación) • [🌐 Sitio web](https://9router.com) + +[🇻🇳 Tiếng Việt](./README.vi.md) • [🇨🇳 中文](./README.zh-CN.md) • [🇯🇵 日本語](./README.ja-JP.md) • [🇷🇺 Русский](./README.ru.md) • [🇹🇭 ไทย](./README.th.md) • [🇮🇷 فارسی](./README.fa_IR.md) • [🇮🇩 Indonesia](./README.id-ID.md) • [🇪🇸 Español](./README.es.md) • [🇫🇷 Français](./README.fr.md) + +
+ +--- + +## 🤔 ¿Por qué 9Router? + +**Deja de perder dinero, tokens y de chocar contra los límites:** + +- ❌ La cuota de la suscripción caduca sin usar cada mes +- ❌ Los límites de velocidad te interrumpen a mitad de la programación +- ❌ Las salidas de las herramientas (git diff, grep, ls...) consumen tokens rápidamente +- ❌ APIs caras ($20-50/mes por proveedor) +- ❌ Cambio manual entre proveedores + +**9Router resuelve esto:** + +- ✅ **Ahorrador de tokens RTK** - Comprime automáticamente el contenido de tool_result y ahorra entre 20-40% de tokens por solicitud +- ✅ **Maximiza las suscripciones** - Realiza el seguimiento de la cuota y usa cada bit antes del restablecimiento +- ✅ **Reserva automática** - Suscripción → Económico → Gratuito, sin tiempos de inactividad +- ✅ **Multi-cuenta** - Round-robin entre cuentas de cada proveedor +- ✅ **Universal** - Funciona con Claude Code, Codex, Cursor, Cline y cualquier herramienta CLI + +--- + +## 🔄 Cómo funciona + +``` +┌─────────────┐ +│ Your CLI │ (Claude Code, Codex, OpenClaw, Cursor, Cline...) +│ Tool │ +└──────┬──────┘ + │ http://localhost:20128/v1 + ↓ +┌─────────────────────────────────────────────┐ +│ 9Router (Smart Router) │ +│ • RTK Token Saver (cut tool_result tokens) │ +│ • Format translation (OpenAI ↔ Claude) │ +│ • Quota tracking │ +│ • Auto token refresh │ +└──────┬──────────────────────────────────────┘ + │ + ├─→ [Tier 1: SUBSCRIPTION] Claude Code, Codex, GitHub Copilot + │ ↓ quota exhausted + ├─→ [Tier 2: CHEAP] GLM ($0.6/1M), MiniMax ($0.2/1M) + │ ↓ budget limit + └─→ [Tier 3: FREE] Kiro, OpenCode Free, Vertex ($300 credits) + +Result: Never stop coding, minimal cost + 20-40% token savings via RTK +``` + +--- + +## ⚡ Inicio rápido + +**1. Instálalo globalmente:** + +```bash +npm install -g 9router +9router +``` + +🎉 El panel de control se abre en `http://localhost:20128` + +**2. Conecta un proveedor GRATUITO (no requiere registro):** + +Panel de control → Providers → Conecta **Kiro AI** (Claude gratuito e ilimitado) o **OpenCode Free** (sin autenticación) → ¡Listo! + +**3. Úsalo en tu herramienta CLI:** + +``` +Ajustes de Claude Code/Codex/OpenClaw/Cursor/Cline: + Endpoint: http://localhost:20128/v1 + API Key: [copia desde el panel de control] + Model: kr/claude-sonnet-4.5 +``` + +**¡Eso es todo!** Empieza a programar con modelos de IA GRATUITOS. + +**Alternativa: ejecutar desde el código fuente (este repositorio):** + +El paquete de este repositorio es privado (`9router-app`), por lo que ejecutar desde el código fuente/Docker es la ruta de desarrollo local prevista. + +```bash +cp .env.example .env +npm install +PORT=20128 NEXT_PUBLIC_BASE_URL=http://localhost:20128 npm run dev +``` + +Modo de producción: + +```bash +npm run build +PORT=20128 HOSTNAME=0.0.0.0 NEXT_PUBLIC_BASE_URL=http://localhost:20128 npm run start +``` + +URLs por defecto: + +- Panel de control: `http://localhost:20128/dashboard` +- API compatible con OpenAI: `http://localhost:20128/v1` + +--- + +## 🎥 Guías en video + +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+ + Tiết kiệm chi phí LLM với 9Router +
+ 🇻🇳 Tiếng Việt
+ Tiết kiệm chi phí LLM cho OpenClaw với 9Router
by Mì AI
+
+ + 9Router + Claude Code FREE Unlimited Setup +
+ 🇵🇰 اردو / हिन्दी
+ 9Router + Claude Code FREE Unlimited Setup
by Build AI With Hamid
+
+ + 9Router Setup Tutorial +
+ 🇺🇸 English
+ 9Router + Claude Code FREE Setup
by Build AI With Hamid
+
+ + 9Router Setup Tutorial +
+ 🇺🇸 English
+ 9Router + Claude Code FREE Setup
by Build AI With Hamid
+
+ + Claude Code FREE Forever +
+ 🇺🇸 English
+ Claude Code FREE Forever — Unlimited Models
by Build AI With Hamid
+
+ + Claude CLI Free Setup +
+ 🇺🇸 English
+ Claude CLI Free Setup with 9Router 🚀
by CodeVerse Soban
+
+ + Cài đặt OpenClaw Free A-Z +
+ 🇻🇳 Tiếng Việt
+ Cài Đặt OpenClaw Free Từ A-Z + 9Router
by Mai Gia
+
+ + FREE OpenClaw with Claude Opus +
+ 🇺🇸 English
+ FREE OpenClaw + Claude Opus 4.6
by Build AI With Hamid
+
+ + Claude CLI Free Setup +
+ 🇮🇩 Indonesia
+ Koding 24 Jam Anti Rate Limit! Hemat Token AI 65% | Tutorial Quick Setup 9Router 🚀
by Krisswuh
+
+ + Cara Deploy 9Router di Hugging Face GRATIS Non-Stop! | Alternatif VPS RAM 16GB +
+ 🇮🇩 Indonesia
+ Cara Deploy 9Router di Hugging Face GRATIS Non-Stop! | Alternatif VPS RAM 16GB
by Krisswuh
+
+ +
+ +> 🎬 **¿Has hecho un video sobre 9Router?** Envía una [Pull Request](https://github.com/decolua/9router/pulls) añadiendo tu video a esta sección — ¡lo fusionaremos! + +--- + +## 🛠️ Herramientas CLI compatibles + +9Router funciona a la perfección con todas las principales herramientas de código con IA: + +
+ + + + + + + + + + + + + + + + + +
+ Claude Code
+ Claude-Code +
+ OpenClaw
+ OpenClaw +
+ Codex
+ Codex +
+ OpenCode
+ OpenCode +
+ Cursor
+ Cursor +
+ Antigravity
+ Antigravity +
+ Cline
+ Cline +
+ Continue
+ Continue +
+ Droid
+ Droid +
+ Roo
+ Roo +
+ Copilot
+ Copilot +
+ Kilo Code
+ Kilo Code +
+
+ +--- + +## 🌐 Proveedores compatibles + +### 🔐 Proveedores OAuth + +
+ + + + + + + + + +
+ Claude Code
+ Claude-Code +
+ Antigravity
+ Antigravity +
+ Codex
+ Codex +
+ GitHub
+ GitHub +
+ Cursor
+ Cursor +
+ Kimchi
+ Kimchi +
+
+ +### 🆓 Proveedores gratuitos + +
+ + + + + + +
+ Kiro
+ Kiro AI
+ Claude 4.5 + GLM-5 + MiniMax
GRATUITO e ilimitado
+
+ OpenCode Free
+ OpenCode Free
+ Sin autenticación • Modelos automáticos
GRATUITO e ilimitado
+
+ Vertex AI
+ Vertex AI
+ Gemini 3 Pro + GLM-5 + DeepSeek
$300 de crédito gratuito
+
+
+ +> **Nota:** Los niveles gratuitos de iFlow, Qwen y Gemini CLI se suspendieron en 2026. Usa Kiro / OpenCode Free / Vertex en su lugar. + +### 🔑 Proveedores con clave API (más de 40) + +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+ OpenRouter
+ OpenRouter +
+ GLM
+ GLM +
+ Kimi
+ Kimi +
+ MiniMax
+ MiniMax +
+ OpenAI
+ OpenAI +
+ Anthropic
+ Anthropic +
+ Gemini
+ Gemini +
+ DeepSeek
+ DeepSeek +
+ Groq
+ Groq +
+ xAI
+ xAI +
+ Mistral
+ Mistral +
+ Perplexity
+ Perplexity +
+ Together
+ Together AI +
+ Fireworks
+ Fireworks +
+ Cerebras
+ Cerebras +
+ Cohere
+ Cohere +
+ NVIDIA
+ NVIDIA +
+ SiliconFlow
+ SiliconFlow +
+

...y más de 20 proveedores adicionales, incluyendo Nebius, Chutes, Hyperbolic y endpoints personalizados compatibles con OpenAI/Anthropic

+
+ +--- + +## 💡 Características principales + +| Característica | Qué hace | Por qué importa | +| ---------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | ------------------------------------------------------- | +| 🚀 **Ahorrador de tokens RTK** ([RTK](https://github.com/rtk-ai/rtk) ⭐40K) | Comprime las salidas de las herramientas (`git diff`, `grep`, `ls`, `tree`...) antes de enviarlas al LLM | Ahorra **20-40% de tokens de entrada** por solicitud | +| 🧠 **Ahorrador de tokens Headroom** ([Headroom](https://github.com/chopratejas/headroom)) | Proxy externo opcional `/v1/compress` antes del enrutamiento al proveedor | Ahorra más tokens de contexto sin cambiar los clientes | +| 🪨 **Modo cavernícola** ([Caveman](https://github.com/JuliusBrussee/caveman) ⭐52K) | Inyecta un prompt de lenguaje cavernícola → el LLM responde de forma concisa, se conserva el contenido técnico | Ahorra **hasta un 65% de tokens de salida** | +| 🐴 **Ponytail** ([Ponytail](https://github.com/DietrichGebert/ponytail)) | Inyecta un prompt de "dev sénior perezoso" → el LLM escribe código mínimo, primero YAGNI (Lite/Full/Ultra) | **Menos tokens de salida, menos refactorización** | +| 🎯 **Reserva inteligente de 3 niveles** | Enrutado automático: Suscripción → Económico → Gratuito | Nunca dejes de programar, cero tiempos de inactividad | +| 📊 **Seguimiento de cuota en tiempo real** | Recuento de tokens en vivo + cuenta atrás de restablecimiento | Maximiza el valor de tu suscripción | +| 🔄 **Traducción de formatos** | OpenAI ↔ Claude ↔ Gemini ↔ Cursor ↔ Kiro ↔ Vertex | Funciona con cualquier herramienta CLI | +| 👥 **Compatibilidad con varias cuentas** | Varias cuentas por proveedor | Balance de carga + redundancia | +| 🔄 **Renovación automática de tokens** | Los tokens OAuth se renuevan automáticamente | Sin necesidad de volver a iniciar sesión | +| 🎨 **Combos personalizados** | Crea combinaciones de modelos ilimitadas | Adapta la reserva a tus necesidades | +| 📝 **Registro de solicitudes** | Modo de depuración con registros completos de solicitudes/respuestas | Soluciona problemas fácilmente | +| 💾 **Sincronización en la nube** | Sincroniza la configuración entre dispositivos | La misma configuración en todas partes | +| 📊 **Analítica de uso** | Realiza el seguimiento de tokens, costes y tendencias a lo largo del tiempo | Optimiza el gasto | +| 🌐 **Implementa en cualquier lugar** | Localhost, VPS, Docker, Cloudflare Workers | Opciones de implementación flexibles | + +
+📖 Detalles de las características + +### 🚀 Ahorrador de tokens RTK + +Las salidas de las herramientas (`git diff`, `grep`, `find`, `ls`, `tree`, volcados de registros...) suelen consumir entre 30-50% del presupuesto de tu prompt. RTK las detecta y aplica una compresión inteligente y sin pérdidas **antes** de que la solicitud llegue al LLM: + +- **Filtros:** `git-diff`, `git-status`, `grep`, `find`, `ls`, `tree`, `dedup-log`, `smart-truncate`, `read-numbered`, `search-list` +- **Detección automática:** No requiere configuración — RTK inspecciona el primer 1KB de cada `tool_result` y elige el filtro correcto. +- **Seguro por diseño:** Si un filtro falla, lanza un error o hace la salida más grande, RTK conserva silenciosamente el texto original. Los errores nunca interrumpen tu solicitud. +- **Universal:** Funciona en todos los formatos (OpenAI, Claude, Gemini, Cursor, Kiro, OpenAI Responses) porque se ejecuta **antes** de cualquier traducción de formato. +- **Activado por defecto:** Actívalo o desactívalo en cualquier momento en Panel de control → Ajustes de Endpoint. + +``` +Sin RTK: se envían 47K tokens al LLM +Con RTK: se envían 28K tokens al LLM (40% ahorrado · mismo contexto · misma respuesta) +``` + +### 🧠 Ahorrador de tokens Headroom + +Headroom es opcional y se ejecuta por separado. 9Router llama al endpoint local `/v1/compress` de Headroom y luego mantiene el enrutamiento, la reserva, la autenticación y el seguimiento de uso normales: + +``` +Client → 9Router → Headroom /v1/compress → 9Router → provider +``` + +Configuración local: + +```bash +pip install "headroom-ai[proxy]" +headroom proxy --port 8787 +``` + +Actívalo en Panel de control → Endpoint → Token Saver → Headroom. URL por defecto: `http://localhost:8787`. + +Ejemplos con Docker: + +```bash +# Servicio de Headroom en la misma red de Docker +http://headroom:8787 + +# Headroom ejecutándose en la máquina host +http://host.docker.internal:8787 +``` + +Si Headroom está caído o devuelve un error, 9Router falla de forma segura y envía la solicitud original. + +### 🐴 Ponytail (Dev sénior perezoso) + +Ponytail inyecta un prompt de sistema de _"dev sénior perezoso"_ en cada solicitud, orientando al LLM hacia un código mínimo y primero YAGNI — eliminación antes que adición, librería estándar antes que nuevas dependencias, una línea antes que abstracciones. Adaptado de [DietrichGebert/ponytail](https://github.com/DietrichGebert/ponytail). + +- **Lite** — Construye lo que se pide, nombra la alternativa más perezosa. +- **Full** — Jerarquía YAGNI aplicada: stdlib → nativo → dependencias existentes → una línea → código mínimo. +- **Ultra** — Extremista del YAGNI: eliminación primero, entrega la solución de una línea, cuestiona el resto del requisito en la misma respuesta. + +``` +Sin Ponytail: código verboso, abstracciones extra, andamiaje "por si acaso" +Con Ponytail: diff funcional más corto, sin abstracciones no solicitadas, menos tokens +``` + +Nunca sacrifica: validación de entrada, manejo de errores que evita pérdida de datos, seguridad, accesibilidad ni nada solicitado explícitamente. Actívalo en Panel de control → Endpoint → Ponytail. Se combina con Caveman (concisión de salida) y RTK (compresión de entrada). + +### 🎯 Reserva inteligente de 3 niveles + +Crea combos con reserva automática: + +``` +Combo: "my-coding-stack" + 1. cc/claude-opus-4-6 (tu suscripción) + 2. glm/glm-4.7 (respaldo económico, $0.6/1M) + 3. if/kimi-k2-thinking (reserva gratuita) + +→ Cambia automáticamente cuando la cuota se agota o se producen errores +``` + +### 📊 Seguimiento de cuota en tiempo real + +- Consumo de tokens por proveedor +- Cuenta atrás de restablecimiento (5 horas, diaria, semanal) +- Estimación de costes para niveles de pago +- Informes de gasto mensual + +### 🔄 Traducción de formatos + +Traducción fluida entre formatos: + +- **OpenAI** ↔ **Claude** ↔ **Gemini** ↔ **Cursor** ↔ **Kiro** ↔ **Vertex** ↔ **Antigravity** ↔ **Ollama** ↔ **OpenAI Responses** +- Tu herramienta CLI envía el formato OpenAI → 9Router lo traduce → El proveedor recibe el formato nativo +- Funciona con cualquier herramienta que admita endpoints OpenAI personalizados + +### 👥 Compatibilidad con varias cuentas + +- Añade varias cuentas por proveedor +- Enrutado automático round-robin o basado en prioridades +- Reserva a la siguiente cuenta cuando una alcanza su cuota + +### 🔄 Renovación automática de tokens + +- Los tokens OAuth se renuevan automáticamente antes de caducar +- Sin necesidad de reautenticación manual +- Experiencia fluida en todos los proveedores + +### 🎨 Combos personalizados + +- Crea combinaciones de modelos ilimitadas +- Mezcla niveles de suscripción, económicos y gratuitos +- Pon nombre a tus combos para acceder fácilmente +- Comparte combos entre dispositivos con Sincronización en la nube + +### 📝 Registro de solicitudes + +- Activa el modo de depuración para registros completos de solicitudes/respuestas +- Realiza el seguimiento de llamadas API, cabeceras y cargas útiles +- Soluciona problemas de integración +- Exporta los registros para su análisis + +### 💾 Sincronización en la nube + +- Sincroniza proveedores, combos y ajustes entre dispositivos +- Sincronización automática en segundo plano +- Almacenamiento cifrado y seguro +- Accede a tu configuración desde cualquier lugar + +#### Notas del runtime en la nube + +- Prefiere las variables de nube del lado del servidor en producción: + - `BASE_URL` (URL de retorno interno usada por el planificador de sincronización) + - `CLOUD_URL` (base del endpoint de sincronización en la nube) +- `NEXT_PUBLIC_BASE_URL` y `NEXT_PUBLIC_CLOUD_URL` siguen siendo compatibles para compatibilidad/UI, pero el runtime del servidor ahora prioriza `BASE_URL`/`CLOUD_URL`. +- Las solicitudes de sincronización en la nube ahora usan tiempo de espera + comportamiento de fallo rápido para evitar que la UI se cuelgue cuando el DNS/la red de la nube no está disponible. + +### 📊 Analítica de uso + +- Realiza el seguimiento del uso de tokens por proveedor y modelo +- Estimación de costes y tendencias de gasto +- Informes e información mensual +- Optimiza tu gasto en IA + +> **💡 IMPORTANTE - Comprender los costes del panel de control:** +> +> El "coste" que se muestra en Analítica de uso es **solo para seguimiento y comparación**. +> 9Router por sí mismo **nunca te cobra** nada. Solo pagas a los proveedores directamente (si usas servicios de pago). +> +> **Ejemplo:** Si tu panel muestra "coste total de $290" mientras usas modelos iFlow, esto representa +> lo que habrías pagado usando APIs de pago directamente. Tu coste real = **$0** (iFlow es gratis e ilimitado). +> +> ¡Piénsalo como un "rastreador de ahorros" que muestra cuánto estás ahorrando al usar modelos gratuitos o +> enrutar a través de 9Router! + +### 🌐 Implementa en cualquier lugar + +- 💻 **Localhost** - Por defecto, funciona sin conexión +- ☁️ **VPS/Nube** - Comparte entre dispositivos +- 🐳 **Docker** - Implementación con un solo comando +- 🚀 **Cloudflare Workers** - Red perimetral global + +
+ +--- + +## 💰 Precios de un vistazo + +| Nivel | Proveedor | Coste | Restablecimiento de cuota | Mejor para | +| --------------------- | ---------------------- | -------------- | ------------------------- | ------------------------------------------- | +| **🚀 AHORRO DE TOKENS** | **RTK (integrado)** | **GRATUITO** | Siempre activo | **Ahorra 20-40% de tokens en CADA solicitud** | +| **💳 SUSCRIPCIÓN** | Claude Code (Pro/Max) | $20-200/mes | 5 h + semanal | Ya suscrito | +| | Codex (Plus/Pro) | $20-200/mes | 5 h + semanal | Usuarios de OpenAI | +| | GitHub Copilot | $10-19/mes | Mensual | Usuarios de GitHub | +| | Cursor IDE | $20/mes | Mensual | Usuarios de Cursor | +| **💰 ECONÓMICO** | GLM-5.1 / GLM-4.7 | $0.6/1M | Diario 10:00 | Respaldo económico | +| | MiniMax M2.7 | $0.2/1M | Ventana de 5 horas | La opción más barata | +| | Kimi K2.5 | $9/mes fijos | 10M tokens/mes | Coste predecible | +| **🆓 GRATUITO** | Kiro AI | $0 | Ilimitado | Claude 4.5 + GLM-5 + MiniMax gratis | +| | OpenCode Free | $0 | Ilimitado | Sin autenticación, modelos automáticos | +| | Vertex AI | $300 de crédito | Cuentas GCP nuevas | Gemini 3 Pro + DeepSeek + GLM-5 | + +**💡 Consejo profesional:** Combo RTK + Kiro AI + OpenCode Free = **$0 de coste + 20-40% de ahorro de tokens**! + +--- + +### 📊 Cómo entender los costes y la facturación de 9Router + +**Realidad de la facturación de 9Router:** + +✅ **El software de 9Router = GRATUITO para siempre** (código abierto, nunca cobra) +✅ **Los "costes" del panel = Solo visualización/seguimiento** (no son facturas reales) +✅ **Pagas a los proveedores directamente** (suscripciones o tarifas de API) +✅ **Los proveedores GRATUITOS siguen siendo GRATUITOS** (iFlow, Kiro, Qwen = $0 ilimitado) +❌ **9Router nunca envía facturas** ni carga tu tarjeta + +**Cómo funciona la visualización de costes:** + +El panel muestra **costes estimados** como si estuvieras usando APIs de pago directamente. Esto **no es facturación** — es una herramienta de comparación para mostrar tus ahorros. + +**Escenario de ejemplo:** + +``` +Visualización del panel: +• Total de solicitudes: 1,662 +• Tokens totales: 47M +• Coste mostrado: $290 + +Comprobación de la realidad: +• Proveedor: iFlow (GRATUITO e ilimitado) +• Pago real: $0.00 +• Qué significa $290: ¡La cantidad que AHORRASTE usando modelos gratuitos! +``` + +**Reglas de pago:** + +- **Proveedores de suscripción** (Claude Code, Codex): Págales directamente a través de sus sitios web +- **Proveedores económicos** (GLM, MiniMax): Págales directamente, 9Router solo enruta +- **Proveedores GRATUITOS** (iFlow, Kiro, Qwen): Realmente gratuitos para siempre, sin cargos ocultos +- **9Router**: Nunca cobra nada, jamás + +--- + +## 🎯 Casos de uso + +### Caso 1: "Tengo una suscripción a Claude Pro" + +**Problema:** La cuota caduca sin usar, límites de velocidad durante programación intensa + +**Solución:** + +``` +Combo: "maximize-claude" + 1. cc/claude-opus-4-7 (usa la suscripción al máximo) + 2. glm/glm-5.1 (respaldo económico cuando se agota la cuota) + 3. kr/claude-sonnet-4.5 (reserva gratuita de emergencia) + +Coste mensual: $20 (suscripción) + ~$5 (respaldo) = $25 en total +vs. $20 + chocar contra límites = frustración +``` + +### Caso 2: "Quiero coste cero" + +**Problema:** No puedo permitirme suscripciones, necesito programación con IA fiable + +**Solución:** + +``` +Combo: "free-forever" + 1. kr/claude-sonnet-4.5 (Claude 4.5 gratuito e ilimitado) + 2. kr/glm-5 (GLM-5 gratis a través de Kiro) + 3. oc/ (OpenCode Free, sin autenticación) + +Coste mensual: $0 +Calidad: Modelos listos para producción + RTK ahorra 20-40% de tokens +``` + +### Caso 3: "Necesito programar 24/7, sin interrupciones" + +**Problema:** Plazos de entrega, no puedo permitirme tiempos de inactividad + +**Solución:** + +``` +Combo: "always-on" + 1. cc/claude-opus-4-7 (la mejor calidad) + 2. cx/gpt-5.5 (segunda suscripción) + 3. glm/glm-5.1 (económico, se restablece diariamente) + 4. minimax/MiniMax-M2.7 (el más barato, restablecimiento cada 5 h) + 5. kr/claude-sonnet-4.5 (gratuito e ilimitado) + +Resultado: 5 capas de reserva = cero tiempos de inactividad +Coste mensual: $20-200 (suscripciones) + $10-20 (respaldo) +``` + +### Caso 4: "Quiero IA GRATUITA en OpenClaw" + +**Problema:** Necesito un asistente de IA en aplicaciones de mensajería (WhatsApp, Telegram, Slack...), completamente gratis + +**Solución:** + +``` +Combo: "openclaw-free" + 1. kr/claude-sonnet-4.5 (Claude 4.5 gratuito) + 2. kr/glm-5 (GLM-5 gratuito) + 3. kr/MiniMax-M2.5 (MiniMax gratuito) + +Coste mensual: $0 +Acceso mediante: WhatsApp, Telegram, Slack, Discord, iMessage, Signal... +``` + +--- + +## ❓ Preguntas frecuentes + +
+📊 ¿Por qué mi panel muestra costes elevados? + +El panel realiza el seguimiento de tu uso de tokens y muestra **costes estimados** como si estuvieras usando APIs de pago directamente. Esto **no es facturación real** — es una referencia para mostrar cuánto estás ahorrando al usar modelos gratuitos o suscripciones existentes a través de 9Router. + +**Ejemplo:** + +- **El panel muestra:** "$290 de coste total" +- **La realidad:** Estás usando iFlow (GRATUITO e ilimitado) +- **Tu coste real:** **$0.00** +- **Qué significa $290:** ¡La cantidad que **ahorraste** al usar modelos gratuitos en lugar de APIs de pago! + +La visualización de costes es un "rastreador de ahorros" que te ayuda a comprender tus patrones de uso y las oportunidades de optimización. + +
+ +
+💳 ¿9Router me cobrará algo? + +**No.** 9Router es un software gratuito y de código abierto que se ejecuta en tu propio ordenador. Nunca te cobra nada. + +**Solo pagas:** + +- ✅ **Proveedores de suscripción** (Claude Code $20/mes, Codex $20-200/mes) → Págales directamente en sus sitios web +- ✅ **Proveedores económicos** (GLM, MiniMax) → Págales directamente, 9Router solo enruta tus solicitudes +- ❌ **El propio 9Router** → **Nunca cobra nada, jamás** + +9Router es un proxy/enrutador local. No tiene tu tarjeta de crédito, no puede enviar facturas y no tiene sistema de facturación. Es software totalmente gratuito. + +
+ +
+🆓 ¿Los proveedores GRATUITOS son realmente ilimitados? + +**¡Sí!** Los proveedores GRATUITOS actuales (Kiro, OpenCode Free, Vertex) son realmente gratuitos y **no tienen cargos ocultos**. + +Estos son servicios gratuitos ofrecidos por esas respectivas empresas: + +- **Kiro AI**: Claude 4.5 + GLM-5 + MiniMax gratuitos e ilimitados mediante AWS Builder ID / Google / GitHub OAuth +- **OpenCode Free**: Proxy de paso sin autenticación, modelos obtenidos automáticamente de `opencode.ai/zen/v1/models` +- **Vertex AI**: $300 de crédito gratuito para cuentas nuevas de Google Cloud (90 días) + +9Router solo enruta tus solicitudes hacia ellos — no hay "trampa" ni facturación futura. Son servicios realmente gratuitos y 9Router los hace fáciles de usar con soporte de reserva. + +**Niveles gratuitos suspendidos (ya no recomendados):** + +- ❌ **iFlow**: Era gratuito e ilimitado, ahora es de pago (2026) +- ❌ **Qwen Code**: El nivel gratuito de OAuth fue suspendido por Alibaba el 15-04-2026 +- ❌ **Gemini CLI**: Sigue funcionando, pero usarlo con herramientas que no son CLI (Claude, Codex, Cursor...) puede resultar en baneo de cuentas — úsalo solo si te quedas con el propio Gemini CLI + +
+ +
+💰 ¿Cómo minimizo mis costes reales de IA? + +**Estrategia gratis-primero:** + +1. **Empieza con un combo 100% gratuito:** + + ``` + 1. gc/gemini-3-flash (180K/mes gratis de Google) + 2. if/kimi-k2-thinking (gratuito e ilimitado de iFlow) + 3. qw/qwen3-coder-plus (gratuito e ilimitado de Qwen) + ``` + + **Coste: $0/mes** + +2. **Añade un respaldo económico** solo si lo necesitas: + + ``` + 4. glm/glm-4.7 ($0.6/1M de tokens) + ``` + + **Coste adicional: Solo pagas por lo que realmente usas** + +3. **Usa los proveedores de suscripción al final:** + - Solo si ya los tienes + - 9Router te ayuda a maximizar su valor mediante el seguimiento de cuota + +**Resultado:** ¡La mayoría de los usuarios pueden operar con $0/mes usando solo niveles gratuitos! + +
+ +
+📈 ¿Qué pasa si mi uso se dispara de repente? + +La reserva inteligente de 9Router evita cargos sorpresa: + +**Escenario:** Estás en un sprint de programación y agotas tus cuotas + +**Sin 9Router:** + +- ❌ Chocas contra el límite de velocidad → El trabajo se detiene → Frustración +- ❌ O: Acumulas accidentalmente facturas enormes de API + +**Con 9Router:** + +- ✅ La suscripción alcanza su límite → Reserva automática al nivel económico +- ✅ El nivel económico se vuelve caro → Reserva automática al nivel gratuito +- ✅ Nunca dejas de programar → Costes predecibles + +**Tú tienes el control:** Establece límites de gasto por proveedor en el panel y 9Router los respeta. + +
+ +--- + +## 📖 Guía de instalación + +
+🔐 Proveedores de suscripción (maximiza el valor) + +### Claude Code (Pro/Max) + +```bash +Panel de control → Providers → Conecta Claude Code +→ Inicio de sesión OAuth → Renovación automática de tokens +→ Seguimiento de cuota de 5 horas + semanal + +Modelos: + cc/claude-opus-4-7 + cc/claude-opus-4-6 + cc/claude-sonnet-4-6 + cc/claude-haiku-4-5-20251001 +``` + +**Consejo profesional:** Usa Opus para tareas complejas y Sonnet para velocidad. ¡9Router realiza el seguimiento de la cuota por modelo! + +### OpenAI Codex (Plus/Pro) + +```bash +Panel de control → Providers → Conecta Codex +→ Inicio de sesión OAuth (puerto 1455) +→ Restablecimiento de 5 horas + semanal + +Modelos: + cx/gpt-5.5 + cx/gpt-5.4 + cx/gpt-5.3-codex + cx/gpt-5.2-codex +``` + +### GitHub Copilot + +```bash +Panel de control → Providers → Conecta GitHub +→ OAuth a través de GitHub +→ Restablecimiento mensual (día 1 de cada mes) + +Modelos: + gh/gpt-5.4 + gh/claude-opus-4.7 + gh/claude-sonnet-4.6 + gh/gemini-3.1-pro-preview + gh/grok-code-fast-1 +``` + +### Cursor IDE + +```bash +Panel de control → Providers → Conecta Cursor +→ Inicio de sesión OAuth +→ Suscripción mensual + +Modelos: + cu/claude-4.6-opus-max + cu/claude-4.5-sonnet-thinking + cu/gpt-5.3-codex +``` + +
+ +
+💰 Proveedores económicos (respaldo) + +### GLM-5.1 / GLM-4.7 (restablecimiento diario, $0.6/1M) + +1. Regístrate: [Zhipu AI](https://open.bigmodel.cn/) +2. Obtén la clave API del Coding Plan +3. Panel de control → Add API Key: + - Proveedor: `glm` + - API Key: `tu-clave` + +**Uso:** `glm/glm-5.1`, `glm/glm-5`, `glm/glm-4.7` + +**Consejo profesional:** ¡El Coding Plan ofrece cuota 3× por 1/7 del coste! Restablecimiento diario a las 10:00. + +### MiniMax M2.7 (restablecimiento cada 5 h, $0.20/1M) + +1. Regístrate: [MiniMax](https://www.minimax.io/) +2. Obtén la clave API +3. Panel de control → Add API Key + +**Uso:** `minimax/MiniMax-M2.7`, `minimax/MiniMax-M2.5` + +**Consejo profesional:** ¡La opción más barata para contexto largo (1M de tokens)! + +### Kimi K2.5 ($9/mes fijos) + +1. Suscríbete: [Moonshot AI](https://platform.moonshot.ai/) +2. Obtén la clave API +3. Panel de control → Add API Key + +**Uso:** `kimi/kimi-k2.5`, `kimi/kimi-k2.5-thinking` + +**Consejo profesional:** ¡$9/mes fijos por 10M de tokens = coste efectivo de $0.90/1M! + +
+ +
+🆓 Proveedores GRATUITOS (recomendados) + +### Kiro AI (Claude 4.5 + GLM-5 + MiniMax GRATIS) + +```bash +Panel de control → Conecta Kiro +→ AWS Builder ID, AWS IAM Identity Center, Google o GitHub +→ Uso ilimitado + +Modelos: + kr/claude-sonnet-4.5 + kr/claude-haiku-4.5 + kr/glm-5 + kr/MiniMax-M2.5 + kr/qwen3-coder-next + kr/deepseek-3.2 +``` + +**Consejo profesional:** La mejor opción gratuita para Claude. Sin clave API, sin pago, totalmente ilimitado. + +### OpenCode Free (sin autenticación, modelos automáticos) + +```bash +Panel de control → Conecta OpenCode Free +→ No requiere inicio de sesión (proxy de paso) +→ Modelos obtenidos automáticamente de opencode.ai/zen/v1/models +``` + +**Consejo profesional:** La configuración más rápida. Solo conéctate y empieza a programar. + +### Vertex AI ($300 de crédito gratuito para cuentas GCP nuevas) + +```bash +Panel de control → Conecta Vertex AI +→ Sube el JSON de la cuenta de servicio de Google Cloud +→ Habilita la API de Vertex AI en tu proyecto de GCP + +Modelos: + vertex/gemini-3.1-pro-preview + vertex/gemini-3-flash-preview + vertex/gemini-2.5-flash + +Vertex Partner (Anthropic / DeepSeek / GLM / Qwen a través de Vertex): + vertex-partner/glm-5-maas + vertex-partner/deepseek-v3.2-maas + vertex-partner/qwen3-next-80b-a3b-thinking-maas +``` + +**Consejo profesional:** Las cuentas nuevas de Google Cloud obtienen $300 de crédito gratis durante 90 días. Más que suficiente para programar a diario. + +
+ +
+🎨 Crea combos + +### Ejemplo 1: Maximiza la suscripción → Respaldo económico + +``` +Panel de control → Combos → Create New + +Nombre: premium-coding +Modelos: + 1. cc/claude-opus-4-7 (Suscripción principal) + 2. glm/glm-5.1 (Respaldo económico, $0.6/1M) + 3. minimax/MiniMax-M2.7 (Reserva más barata, $0.20/1M) + +Uso en CLI: premium-coding + +Ejemplo de coste mensual (100M de tokens): + 80M vía Claude (suscripción): $0 extra + 15M vía GLM: $9 + 5M vía MiniMax: $1 + Total: $10 + tu suscripción +``` + +### Ejemplo 2: Solo gratis (coste cero) + +``` +Nombre: free-combo +Modelos: + 1. kr/claude-sonnet-4.5 (Claude 4.5 gratuito e ilimitado) + 2. kr/glm-5 (GLM-5 gratis a través de Kiro) + 3. vertex/gemini-3.1-pro-preview ($300 de crédito gratuito) + +Coste: $0 para siempre (+ 20-40% de ahorro de tokens vía RTK)! +``` + +
+ +
+🔧 Integración con CLI + +### Cursor IDE + +``` +Settings → Models → Advanced: + OpenAI API Base URL: http://localhost:20128/v1 + OpenAI API Key: [desde el panel de 9router] + Model: cc/claude-opus-4-7 +``` + +O usa el combo: `premium-coding` + +### Claude Code + +Edita `~/.claude/config.json`: + +```json +{ + "anthropic_api_base": "http://localhost:20128/v1", + "anthropic_api_key": "tu-clave-api-de-9router" +} +``` + +### Codex CLI + +```bash +export OPENAI_BASE_URL="http://localhost:20128" +export OPENAI_API_KEY="tu-clave-api-de-9router" + +codex "tu prompt" +``` + +### OpenClaw + +**Opción 1 — Panel de control (recomendada):** + +``` +Panel de control → CLI Tools → OpenClaw → Selecciona el modelo → Aplicar +``` + +**Opción 2 — Manual:** Edita `~/.openclaw/openclaw.json`: + +```json +{ + "agents": { + "defaults": { + "model": { + "primary": "9router/kr/claude-sonnet-4.5" + } + } + }, + "models": { + "providers": { + "9router": { + "baseUrl": "http://127.0.0.1:20128/v1", + "apiKey": "sk_9router", + "api": "openai-completions", + "models": [ + { + "id": "kr/claude-sonnet-4.5", + "name": "Claude Sonnet 4.5 (Kiro Free)" + } + ] + } + } + } +} +``` + +> **Nota:** OpenClaw solo funciona con 9Router local. Usa `127.0.0.1` en lugar de `localhost` para evitar problemas de resolución IPv6. + +### Cline / Continue / RooCode + +``` +Provider: OpenAI Compatible +Base URL: http://localhost:20128/v1 +API Key: [desde el panel] +Model: cc/claude-opus-4-7 +``` + +
+ +
+🚀 Implementación + +### Implementación en VPS + +```bash +# Clona e instala +git clone https://github.com/decolua/9router.git +cd 9router +npm install +npm run build + +# Configura +export JWT_SECRET="tu-secreto-seguro-cámbialo" +export INITIAL_PASSWORD="tu-contraseña" +export DATA_DIR="/var/lib/9router" +export PORT="20128" +export HOSTNAME="0.0.0.0" +export NODE_ENV="production" +export NEXT_PUBLIC_BASE_URL="http://localhost:20128" +export NEXT_PUBLIC_CLOUD_URL="https://9router.com" +export API_KEY_SECRET="endpoint-proxy-api-key-secret" +export MACHINE_ID_SALT="endpoint-proxy-salt" + +# Inicia +npm run start + +# O usa PM2 +npm install -g pm2 +pm2 start npm --name 9router -- start +pm2 save +pm2 startup +``` + +### Docker + +Imágenes publicadas (multi-plataforma `linux/amd64` + `linux/arm64`): + +- Docker Hub: [`decolua/9router`](https://hub.docker.com/r/decolua/9router) +- GHCR: [`ghcr.io/decolua/9router`](https://github.com/decolua/9router/pkgs/container/9router) + +**Inicio rápido (usa la imagen publicada):** + +```bash +docker run -d \ + --name 9router \ + -p 20128:20128 \ + -v "$HOME/.9router:/app/data" \ + -e DATA_DIR=/app/data \ + decolua/9router:latest +``` + +→ Abre http://localhost:20128 + +**Compilar desde el código fuente (dev):** + +```bash +git clone https://github.com/decolua/9router.git +cd 9router/app +docker build -t 9router . +docker run -d --name 9router -p 20128:20128 \ + -v "$HOME/.9router:/app/data" -e DATA_DIR=/app/data 9router +``` + +**Valores por defecto del contenedor:** + +- `PORT=20128` +- `HOSTNAME=0.0.0.0` + +**Comandos útiles:** + +```bash +docker logs -f 9router +docker restart 9router +docker stop 9router && docker rm 9router +docker pull decolua/9router:latest # actualiza a la última versión +``` + +**Persistencia de datos:** `$HOME/.9router/db/data.sqlite` en el host ↔ `/app/data/db/data.sqlite` en el contenedor. + +### Variables de entorno + +| Variable | Por defecto | Descripción | +| ---------------------------------------------------- | ------------------------------------------ | ------------------------------------------------------------------------------------ | +| `JWT_SECRET` | Generado automáticamente (`~/.9router/jwt-secret`) | Secreto de firma JWT para la cookie de autenticación del panel (sobreescríbelo para compartir entre instancias) | +| `INITIAL_PASSWORD` | `123456` | Contraseña del primer inicio de sesión cuando no existe un hash guardado | +| `DATA_DIR` | `~/.9router` | Ubicación principal de datos de la app (SQLite en `$DATA_DIR/db/data.sqlite`) | +| `PORT` | por defecto del framework | Puerto del servicio (`20128` en los ejemplos) | +| `HOSTNAME` | por defecto del framework | Host de enlace (Docker usa `0.0.0.0` por defecto) | +| `NODE_ENV` | por defecto del runtime | Establece `production` para el despliegue | +| `BASE_URL` | `http://localhost:20128` | URL base interna del lado del servidor usada por los trabajos de sincronización en la nube | +| `CLOUD_URL` | `https://9router.com` | URL base del endpoint de sincronización en la nube del lado del servidor | +| `NEXT_PUBLIC_BASE_URL` | `http://localhost:3000` | URL base pública/compatible con versiones anteriores (prefiere `BASE_URL` para el runtime del servidor) | +| `NEXT_PUBLIC_CLOUD_URL` | `https://9router.com` | URL de nube pública/compatible (prefiere `CLOUD_URL` para el runtime del servidor) | +| `API_KEY_SECRET` | `endpoint-proxy-api-key-secret` | Secreto HMAC para las claves API generadas | +| `MACHINE_ID_SALT` | `endpoint-proxy-salt` | Sal para el hash estable del ID de máquina | +| `ENABLE_REQUEST_LOGS` | `false` | Habilita los registros de solicitudes/respuestas en `logs/` | +| `AUTH_COOKIE_SECURE` | `false` | Fuerza la cookie `Secure` de autenticación (establece `true` detrás de un proxy HTTPS inverso) | +| `REQUIRE_API_KEY` | `false` | Aplica clave API Bearer en las rutas `/v1/*` (recomendado para despliegues expuestos a Internet) | +| `HTTP_PROXY`, `HTTPS_PROXY`, `ALL_PROXY`, `NO_PROXY` | vacío | Proxy saliente opcional para las llamadas a proveedores externos | +| `SEARXNG_URL` | `http://localhost:8888/search` | Endpoint del proveedor de búsqueda web SearXNG integrado sin autenticación | + +Notas: + +- También se admiten variables de proxy en minúsculas: `http_proxy`, `https_proxy`, `all_proxy`, `no_proxy`. +- `.env` no se incluye en la imagen de Docker (`.dockerignore`); inyecta la configuración del runtime con `--env-file` o `-e`. +- En Windows, se puede usar `APPDATA` para resolver la ruta de almacenamiento local. +- `INSTANCE_NAME` aparece en documentación/plantillas de entorno antiguas, pero actualmente no se usa en el runtime. + +### Archivos en tiempo de ejecución y almacenamiento + +- Estado principal de la app: `${DATA_DIR}/db/data.sqlite` (SQLite — proveedores, combos, alias, claves, ajustes, historial de uso) +- Copias de seguridad automáticas: `${DATA_DIR}/db/backups/` +- Registros opcionales de solicitudes/translator: `/logs/...` cuando `ENABLE_REQUEST_LOGS=true` +- Tanto `${DATA_DIR}` como `~/.9router` resuelven a la misma ubicación en un contenedor de Docker — el enlace simbólico `/root/.9router -> /app/data` se crea en tiempo de compilación. + +
+ +--- + +## 📊 Modelos disponibles + +
+Ver todos los modelos disponibles + +**Claude Code (`cc/`)** - Pro/Max: + +- `cc/claude-opus-4-7` +- `cc/claude-opus-4-6` +- `cc/claude-sonnet-4-6` +- `cc/claude-sonnet-4-5-20250929` +- `cc/claude-haiku-4-5-20251001` + +**Codex (`cx/`)** - Plus/Pro: + +- `cx/gpt-5.5` +- `cx/gpt-5.4` +- `cx/gpt-5.3-codex` +- `cx/gpt-5.2-codex` +- `cx/gpt-5.1-codex-max` + +**GitHub Copilot (`gh/`)**: + +- `gh/gpt-5.4` +- `gh/claude-opus-4.7` +- `gh/claude-sonnet-4.6` +- `gh/gemini-3.1-pro-preview` +- `gh/grok-code-fast-1` + +**Cursor (`cu/`)** - Suscripción: + +- `cu/claude-4.6-opus-max` +- `cu/claude-4.5-sonnet-thinking` +- `cu/gpt-5.3-codex` +- `cu/kimi-k2.5` + +**GLM (`glm/`)** - $0.6/1M: + +- `glm/glm-5.1` +- `glm/glm-5` +- `glm/glm-4.7` + +**MiniMax (`minimax/`)** - $0.2/1M: + +- `minimax/MiniMax-M2.7` +- `minimax/MiniMax-M2.5` + +**Kimi (`kimi/`)** - $9/mes fijos: + +- `kimi/kimi-k2.5` +- `kimi/kimi-k2.5-thinking` + +**Kiro (`kr/`)** - GRATUITO e ilimitado: + +- `kr/claude-sonnet-4.5` +- `kr/claude-haiku-4.5` +- `kr/glm-5` +- `kr/MiniMax-M2.5` +- `kr/qwen3-coder-next` +- `kr/deepseek-3.2` + +**OpenCode Free (`oc/`)** - GRATUITO sin autenticación: + +- Obtenidos automáticamente de `opencode.ai/zen/v1/models` + +**Vertex AI (`vertex/`)** - $300 de crédito gratuito: + +- `vertex/gemini-3.1-pro-preview` +- `vertex/gemini-3-flash-preview` +- `vertex/gemini-2.5-flash` +- `vertex-partner/glm-5-maas` +- `vertex-partner/deepseek-v3.2-maas` + +
+ +--- + +## 🐛 Solución de problemas + +**"Language model did not provide messages"** + +- Cuota del proveedor agotada → Revisa el rastreador de cuota del panel +- Solución: usa la reserva del combo o cambia a un nivel más barato + +**Límites de velocidad** + +- Cuota de suscripción agotada → Reserva a GLM/MiniMax +- Añade el combo: `cc/claude-opus-4-7 → glm/glm-5.1 → kr/claude-sonnet-4.5` + +**Token OAuth caducado** + +- 9Router lo renueva automáticamente +- Si los problemas persisten: Panel de control → Provider → Reconectar + +**Costes elevados** + +- Habilita RTK en Panel de control → Ajustes de Endpoint (activado por defecto, ahorra 20-40% de tokens) +- Revisa las estadísticas de uso en el panel +- Cambia el modelo principal a GLM/MiniMax +- Usa el nivel gratuito (Kiro, OpenCode Free, Vertex) para tareas no críticas + +**El panel se abre en el puerto equivocado** + +- Establece `PORT=20128` y `NEXT_PUBLIC_BASE_URL=http://localhost:20128` + +**El primer inicio de sesión no funciona** + +- Revisa `INITIAL_PASSWORD` en `.env` +- Si no está configurada, la contraseña de respaldo es `123456` + +**No hay registros de solicitudes en `logs/`** + +- Establece `ENABLE_REQUEST_LOGS=true` + +--- + +## 🛠️ Pila tecnológica + +- **Runtime**: Node.js 20+ +- **Framework**: Next.js 16 +- **UI**: React 19 + Tailwind CSS 4 +- **Base de datos**: SQLite (better-sqlite3 / node:sqlite / sql.js como respaldo) +- **Streaming**: Server-Sent Events (SSE) +- **Autenticación**: OAuth 2.0 (PKCE) + JWT + claves API + +--- + +## 📝 Referencia de la API + +### Chat Completions + +```bash +POST http://localhost:20128/v1/chat/completions +Authorization: Bearer tu-clave-api +Content-Type: application/json + +{ + "model": "cc/claude-opus-4-6", + "messages": [ + {"role": "user", "content": "Escribe una función para..."} + ], + "stream": true +} +``` + +### Listar modelos + +```bash +GET http://localhost:20128/v1/models +Authorization: Bearer tu-clave-api + +→ Devuelve todos los modelos y combos en formato OpenAI +``` + +## 📧 Soporte + +- **Sitio web**: [9router.com](https://9router.com) +- **GitHub**: [github.com/decolua/9router](https://github.com/decolua/9router) +- **Problemas**: [github.com/decolua/9router/issues](https://github.com/decolua/9router/issues) + +--- + +## 👥 Colaboradores + +¡Gracias a todos los colaboradores que ayudaron a mejorar 9Router! + +[![Contributors](https://contrib.rocks/image?repo=decolua/9router&max=150&columns=15&anon=1&v=20260309)](https://github.com/decolua/9router/graphs/contributors) + +--- + +## 📊 Gráfico de estrellas + +[![Star Chart](https://starchart.cc/decolua/9router.svg?variant=adaptive)](https://starchart.cc/decolua/9router) + +## 🔀 Forks + +**[OmniRoute](https://github.com/diegosouzapw/OmniRoute)** — Un fork de 9Router en TypeScript con todas las funciones. Añade más de 36 proveedores, reserva automática de 4 niveles, APIs multimodales (imágenes, embeddings, audio, TTS), interruptor de circuitos, caché semántica, evaluaciones de LLM y un panel pulido. Más de 368 pruebas unitarias. Disponible a través de npm y Docker. + +--- + +## 🙏 Agradecimientos + +Construido sobre los hombros de gigantes: + +- **[CLIProxyAPI](https://github.com/router-for-me/CLIProxyAPI)** — implementación original en Go que inspiró este port a JavaScript. +- **[RTK](https://github.com/rtk-ai/rtk)** ![Stars](https://img.shields.io/github/stars/rtk-ai/rtk?style=flat&color=yellow) — ahorrador de tokens en Rust. 9Router porta su pipeline de compresión a JS → **−20-40% de tokens de entrada** en cada solicitud. +- **[Caveman](https://github.com/JuliusBrussee/caveman)** ![Stars](https://img.shields.io/github/stars/JuliusBrussee/caveman?style=flat&color=yellow) por **[@JuliusBrussee](https://github.com/JuliusBrussee)** — viral _"why use many token when few token do trick"_. 9Router adapta su prompt → **−65% de tokens de salida**. +- **[Ponytail](https://github.com/DietrichGebert/ponytail)** ![Stars](https://img.shields.io/github/stars/DietrichGebert/ponytail?style=flat&color=yellow) por **[@DietrichGebert](https://github.com/DietrichGebert)** — habilidad del _"dev sénior perezoso"_. 9Router inyecta su jerarquía primero-YAGNI → **menos tokens, menos código, diffs más cortos**. + +Un enorme agradecimiento a estos autores — sin su trabajo, las funciones de ahorro de tokens de 9Router no existirían. ¡Dales una ⭐ en GitHub! + +--- + +## 📄 Licencia + +Licencia MIT - consulta [LICENSE](LICENSE) para más detalles. + +--- + +
+ Hecho con ❤️ para desarrolladores que programan 24/7 +
+ diff --git a/i18n/README.fr.md b/i18n/README.fr.md new file mode 100644 index 00000000..d93df014 --- /dev/null +++ b/i18n/README.fr.md @@ -0,0 +1,1445 @@ +
+ Tableau de bord 9Router + + # 9Router - Routeur IA GRATUIT et économiseur de tokens + + **N'arrêtez jamais de coder. Économisez 20 à 40 % de tokens avec RTK + bascule automatique vers des modèles d'IA GRATUITS et bon marché.** + + **Connectez tous vos outils de codage IA (Claude Code, Cursor, Antigravity, Copilot, Codex, Gemini, OpenCode, Cline, OpenClaw...) à plus de 40 fournisseurs d'IA et plus de 100 modèles.** + + [![npm](https://img.shields.io/npm/v/9router.svg)](https://www.npmjs.com/package/9router) + [![Downloads](https://img.shields.io/npm/dm/9router.svg)](https://www.npmjs.com/package/9router) + [![Docker Pulls](https://img.shields.io/docker/pulls/decolua/9router.svg?logo=docker&label=Docker%20pulls)](https://hub.docker.com/r/decolua/9router) + [![GHCR](https://img.shields.io/badge/GHCR-decolua%2F9router-blue?logo=github)](https://github.com/decolua/9router/pkgs/container/9router) + [![License](https://img.shields.io/npm/l/9router.svg)](https://github.com/decolua/9router/blob/main/LICENSE) + +decolua%2F9router | Trendshift + +[🚀 Démarrage rapide](#-démarrage-rapide) • [💡 Fonctionnalités](#-fonctionnalités-clés) • [📖 Configuration](#-guide-dinstallation) • [🌐 Site web](https://9router.com) + +[🇻🇳 Tiếng Việt](./README.vi.md) • [🇨🇳 中文](./README.zh-CN.md) • [🇯🇵 日本語](./README.ja-JP.md) • [🇷🇺 Русский](./README.ru.md) • [🇹🇭 ไทย](./README.th.md) • [🇮🇷 فارسی](./README.fa_IR.md) • [🇮🇩 Indonesia](./README.id-ID.md) • [🇪🇸 Español](./README.es.md) • [🇫🇷 Français](./README.fr.md) + +
+ +--- + +## 🤔 Pourquoi 9Router ? + +**Arrêtez de gaspiller de l'argent, des tokens et de buter contre les limites :** + +- ❌ Le quota de votre abonnement expire inutilisé chaque mois +- ❌ Les limites de débit vous interrompent en plein codage +- ❌ Les sorties des outils (git diff, grep, ls...) consomment vos tokens rapidement +- ❌ Des API coûteuses (20 à 50 $/mois par fournisseur) +- ❌ Changement manuel entre les fournisseurs + +**9Router résout tout cela :** + +- ✅ **Économiseur de tokens RTK** - Compresse automatiquement le contenu de tool_result et économise 20 à 40 % de tokens par requête +- ✅ **Maximise les abonnements** - Suit le quota et utilise chaque bit avant sa réinitialisation +- ✅ **Bascule automatique** - Abonnement → Bon marché → Gratuit, zéro temps d'arrêt +- ✅ **Multi-comptes** - Round-robin entre les comptes de chaque fournisseur +- ✅ **Universel** - Fonctionne avec Claude Code, Codex, Cursor, Cline et tout outil CLI + +--- + +## 🔄 Comment ça marche + +``` +┌─────────────┐ +│ Your CLI │ (Claude Code, Codex, OpenClaw, Cursor, Cline...) +│ Tool │ +└──────┬──────┘ + │ http://localhost:20128/v1 + ↓ +┌─────────────────────────────────────────────┐ +│ 9Router (Smart Router) │ +│ • RTK Token Saver (cut tool_result tokens) │ +│ • Format translation (OpenAI ↔ Claude) │ +│ • Quota tracking │ +│ • Auto token refresh │ +└──────┬──────────────────────────────────────┘ + │ + ├─→ [Tier 1: SUBSCRIPTION] Claude Code, Codex, GitHub Copilot + │ ↓ quota exhausted + ├─→ [Tier 2: CHEAP] GLM ($0.6/1M), MiniMax ($0.2/1M) + │ ↓ budget limit + └─→ [Tier 3: FREE] Kiro, OpenCode Free, Vertex ($300 credits) + +Result: Never stop coding, minimal cost + 20-40% token savings via RTK +``` + +--- + +## ⚡ Démarrage rapide + +**1. Installez globalement :** + +```bash +npm install -g 9router +9router +``` + +🎉 Le tableau de bord s'ouvre sur `http://localhost:20128` + +**2. Connectez un fournisseur GRATUIT (aucune inscription requise) :** + +Tableau de bord → Providers → Connectez **Kiro AI** (Claude gratuit et illimité) ou **OpenCode Free** (sans authentification) → C'est tout ! + +**3. Utilisez-le dans votre outil CLI :** + +``` +Paramètres de Claude Code/Codex/OpenClaw/Cursor/Cline : + Endpoint : http://localhost:20128/v1 + API Key : [copiez depuis le tableau de bord] + Model : kr/claude-sonnet-4.5 +``` + +**Et voilà !** Commencez à coder avec des modèles d'IA GRATUITS. + +**Alternative : exécuter depuis le code source (ce dépôt) :** + +Le paquet de ce dépôt est privé (`9router-app`), donc l'exécution depuis le code source/Docker est le chemin de développement local attendu. + +```bash +cp .env.example .env +npm install +PORT=20128 NEXT_PUBLIC_BASE_URL=http://localhost:20128 npm run dev +``` + +Mode production : + +```bash +npm run build +PORT=20128 HOSTNAME=0.0.0.0 NEXT_PUBLIC_BASE_URL=http://localhost:20128 npm run start +``` + +URL par défaut : + +- Tableau de bord : `http://localhost:20128/dashboard` +- API compatible OpenAI : `http://localhost:20128/v1` + +--- + +## 🎥 Guides vidéo + +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+ + Tiết kiệm chi phí LLM với 9Router +
+ 🇻🇳 Tiếng Việt
+ Tiết kiệm chi phí LLM cho OpenClaw với 9Router
by Mì AI
+
+ + 9Router + Claude Code FREE Unlimited Setup +
+ 🇵🇰 اردو / हिन्दी
+ 9Router + Claude Code FREE Unlimited Setup
by Build AI With Hamid
+
+ + 9Router Setup Tutorial +
+ 🇺🇸 English
+ 9Router + Claude Code FREE Setup
by Build AI With Hamid
+
+ + 9Router Setup Tutorial +
+ 🇺🇸 English
+ 9Router + Claude Code FREE Setup
by Build AI With Hamid
+
+ + Claude Code FREE Forever +
+ 🇺🇸 English
+ Claude Code FREE Forever — Unlimited Models
by Build AI With Hamid
+
+ + Claude CLI Free Setup +
+ 🇺🇸 English
+ Claude CLI Free Setup with 9Router 🚀
by CodeVerse Soban
+
+ + Cài đặt OpenClaw Free A-Z +
+ 🇻🇳 Tiếng Việt
+ Cài Đặt OpenClaw Free Từ A-Z + 9Router
by Mai Gia
+
+ + FREE OpenClaw with Claude Opus +
+ 🇺🇸 English
+ FREE OpenClaw + Claude Opus 4.6
by Build AI With Hamid
+
+ + Claude CLI Free Setup +
+ 🇮🇩 Indonesia
+ Koding 24 Jam Anti Rate Limit! Hemat Token AI 65% | Tutorial Quick Setup 9Router 🚀
by Krisswuh
+
+ + Cara Deploy 9Router di Hugging Face GRATIS Non-Stop! | Alternatif VPS RAM 16GB +
+ 🇮🇩 Indonesia
+ Cara Deploy 9Router di Hugging Face GRATIS Non-Stop! | Alternatif VPS RAM 16GB
by Krisswuh
+
+ +
+ +> 🎬 **Vous avez réalisé une vidéo sur 9Router ?** Envoyez une [Pull Request](https://github.com/decolua/9router/pulls) en ajoutant votre vidéo à cette section — nous la fusionnerons ! + +--- + +## 🛠️ Outils CLI pris en charge + +9Router fonctionne parfaitement avec tous les principaux outils de codage IA : + +
+ + + + + + + + + + + + + + + + + +
+ Claude Code
+ Claude-Code +
+ OpenClaw
+ OpenClaw +
+ Codex
+ Codex +
+ OpenCode
+ OpenCode +
+ Cursor
+ Cursor +
+ Antigravity
+ Antigravity +
+ Cline
+ Cline +
+ Continue
+ Continue +
+ Droid
+ Droid +
+ Roo
+ Roo +
+ Copilot
+ Copilot +
+ Kilo Code
+ Kilo Code +
+
+ +--- + +## 🌐 Fournisseurs pris en charge + +### 🔐 Fournisseurs OAuth + +
+ + + + + + + + + +
+ Claude Code
+ Claude-Code +
+ Antigravity
+ Antigravity +
+ Codex
+ Codex +
+ GitHub
+ GitHub +
+ Cursor
+ Cursor +
+ Kimchi
+ Kimchi +
+
+ +### 🆓 Fournisseurs gratuits + +
+ + + + + + +
+ Kiro
+ Kiro AI
+ Claude 4.5 + GLM-5 + MiniMax
GRATUIT illimité
+
+ OpenCode Free
+ OpenCode Free
+ Sans authentification • Modèles automatiques
GRATUIT illimité
+
+ Vertex AI
+ Vertex AI
+ Gemini 3 Pro + GLM-5 + DeepSeek
300 $ de crédit gratuit
+
+
+ +> **Remarque :** Les niveaux gratuits d'iFlow, Qwen et Gemini CLI ont été interrompus en 2026. Utilisez plutôt Kiro / OpenCode Free / Vertex. + +### 🔑 Fournisseurs avec clé API (40+) + +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+ OpenRouter
+ OpenRouter +
+ GLM
+ GLM +
+ Kimi
+ Kimi +
+ MiniMax
+ MiniMax +
+ OpenAI
+ OpenAI +
+ Anthropic
+ Anthropic +
+ Gemini
+ Gemini +
+ DeepSeek
+ DeepSeek +
+ Groq
+ Groq +
+ xAI
+ xAI +
+ Mistral
+ Mistral +
+ Perplexity
+ Perplexity +
+ Together
+ Together AI +
+ Fireworks
+ Fireworks +
+ Cerebras
+ Cerebras +
+ Cohere
+ Cohere +
+ NVIDIA
+ NVIDIA +
+ SiliconFlow
+ SiliconFlow +
+

...et plus de 20 autres fournisseurs, dont Nebius, Chutes, Hyperbolic et des endpoints personnalisés compatibles OpenAI/Anthropic

+
+ +--- + +## 💡 Fonctionnalités clés + +| Fonctionnalité | Ce qu'elle fait | Pourquoi c'est important | +| -------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | --------------------------------------------------------- | +| 🚀 **Économiseur de tokens RTK** ([RTK](https://github.com/rtk-ai/rtk) ⭐40K) | Compresse les sorties des outils (`git diff`, `grep`, `ls`, `tree`...) avant de les envoyer au LLM | Économise **20 à 40 % de tokens d'entrée** par requête | +| 🧠 **Économiseur de tokens Headroom** ([Headroom](https://github.com/chopratejas/headroom)) | Proxy externe optionnel `/v1/compress` avant le routage vers le fournisseur | Économise plus de tokens de contexte sans changer les clients | +| 🪨 **Mode cavernicole** ([Caveman](https://github.com/JuliusBrussee/caveman) ⭐52K) | Injecte un prompt en langage cavernicole → le LLM répond de manière concise, le contenu technique est conservé | Économise **jusqu'à 65 % de tokens de sortie** | +| 🐴 **Ponytail** ([Ponytail](https://github.com/DietrichGebert/ponytail)) | Injecte un prompt de "dev sénior fainéant" → le LLM écrit un code minimal, YAGNI d'abord (Lite/Full/Ultra) | **Moins de tokens de sortie, moins de refactorisation** | +| 🎯 **Repli intelligent à 3 niveaux** | Routage automatique : Abonnement → Bon marché → Gratuit | N'arrêtez jamais de coder, zéro temps d'arrêt | +| 📊 **Suivi de quota en temps réel** | Compteur de tokens en direct + compte à rebours de réinitialisation | Maximisez la valeur de votre abonnement | +| 🔄 **Traduction de formats** | OpenAI ↔ Claude ↔ Gemini ↔ Cursor ↔ Kiro ↔ Vertex | Fonctionne avec n'importe quel outil CLI | +| 👥 **Prise en charge multi-comptes** | Plusieurs comptes par fournisseur | Équilibrage de charge + redondance | +| 🔄 **Renouvellement automatique des tokens** | Les tokens OAuth se renouvellent automatiquement | Plus besoin de se reconnecter manuellement | +| 🎨 **Combos personnalisés** | Créez des combinaisons de modèles illimitées | Adaptez le repli à vos besoins | +| 📝 **Journalisation des requêtes** | Mode débogage avec journaux complets de requêtes/réponses | Résolvez facilement les problèmes | +| 💾 **Synchronisation cloud** | Synchronisez la configuration entre vos appareils | La même configuration partout | +| 📊 **Analytique d'utilisation** | Suivez tokens, coûts et tendances au fil du temps | Optimisez vos dépenses | +| 🌐 **Déployez partout** | Localhost, VPS, Docker, Cloudflare Workers | Options de déploiement flexibles | + +
+📖 Détails des fonctionnalités + +### 🚀 Économiseur de tokens RTK + +Les sorties des outils (`git diff`, `grep`, `find`, `ls`, `tree`, vidages de journaux...) consomment souvent 30 à 50 % de votre budget de prompt. RTK les détecte et applique une compression intelligente et sans perte **avant** que la requête n'atteigne le LLM : + +- **Filtres :** `git-diff`, `git-status`, `grep`, `find`, `ls`, `tree`, `dedup-log`, `smart-truncate`, `read-numbered`, `search-list` +- **Détection automatique :** Aucune configuration requise — RTK inspecte le premier 1 Ko de chaque `tool_result` et choisit le bon filtre. +- **Sûr par conception :** Si un filtre échoue, génère une erreur ou rend la sortie plus volumineuse, RTK conserve silencieusement le texte original. Les erreurs ne cassent jamais votre requête. +- **Universel :** Fonctionne dans tous les formats (OpenAI, Claude, Gemini, Cursor, Kiro, OpenAI Responses) car il s'exécute **avant** toute traduction de format. +- **Activé par défaut :** Activez-le ou désactivez-le à tout moment dans Tableau de bord → Paramètres d'Endpoint. + +``` +Sans RTK : 47K tokens envoyés au LLM +Avec RTK : 28K tokens envoyés au LLM (40 % économisés · même contexte · même réponse) +``` + +### 🧠 Économiseur de tokens Headroom + +Headroom est optionnel et s'exécute séparément. 9Router appelle l'endpoint local `/v1/compress` de Headroom, puis conserve le routage, le repli, l'authentification et le suivi d'utilisation normaux : + +``` +Client → 9Router → Headroom /v1/compress → 9Router → fournisseur +``` + +Configuration locale : + +```bash +pip install "headroom-ai[proxy]" +headroom proxy --port 8787 +``` + +Activez-le dans Tableau de bord → Endpoint → Token Saver → Headroom. URL par défaut : `http://localhost:8787`. + +Exemples Docker : + +```bash +# Service Headroom dans le même réseau Docker +http://headroom:8787 + +# Headroom exécuté sur la machine hôte +http://host.docker.internal:8787 +``` + +Si Headroom est indisponible ou renvoie une erreur, 9Router bascule en mode dégradé et envoie la requête originale. + +### 🐴 Ponytail (Dev sénior fainéant) + +Ponytail injecte un prompt système de _"dev sénior fainéant"_ dans chaque requête, orientant le LLM vers un code minimal et YAGNI d'abord — la suppression plutôt que l'ajout, la bibliothèque standard plutôt que de nouvelles dépendances, une ligne plutôt que des abstractions. Adapté de [DietrichGebert/ponytail](https://github.com/DietrichGebert/ponytail). + +- **Lite** — Construisez ce qui est demandé, nommez l'alternative plus fainéante. +- **Full** — Hiérarchie YAGNI appliquée : stdlib → natif → dépendances existantes → une ligne → code minimal. +- **Ultra** — Extrémiste du YAGNI : suppression d'abord, livrez la solution d'une ligne, remettez en question le reste de l'exigence dans la même réponse. + +``` +Sans Ponytail : code verbeux, abstractions superflues, échafaudage "au cas où" +Avec Ponytail : diff fonctionnel le plus court, aucune abstraction non demandée, moins de tokens +``` + +Ne sacrifie jamais : la validation des entrées, la gestion des erreurs qui évite la perte de données, la sécurité, l'accessibilité ni rien d'explicitement demandé. Activez-le dans Tableau de bord → Endpoint → Ponytail. Se cumule avec Caveman (concision de sortie) et RTK (compression d'entrée). + +### 🎯 Repli intelligent à 3 niveaux + +Créez des combos avec repli automatique : + +``` +Combo : "my-coding-stack" + 1. cc/claude-opus-4-6 (votre abonnement) + 2. glm/glm-4.7 (sauvegarde économique, $0.6/1M) + 3. if/kimi-k2-thinking (repli gratuit) + +→ Bascule automatiquement quand le quota est épuisé ou en cas d'erreur +``` + +### 📊 Suivi de quota en temps réel + +- Consommation de tokens par fournisseur +- Compte à rebours de réinitialisation (5 heures, quotidien, hebdomadaire) +- Estimation des coûts pour les niveaux payants +- Rapports de dépenses mensuels + +### 🔄 Traduction de formats + +Traduction transparente entre les formats : + +- **OpenAI** ↔ **Claude** ↔ **Gemini** ↔ **Cursor** ↔ **Kiro** ↔ **Vertex** ↔ **Antigravity** ↔ **Ollama** ↔ **OpenAI Responses** +- Votre outil CLI envoie le format OpenAI → 9Router le traduit → Le fournisseur reçoit le format natif +- Fonctionne avec tout outil prenant en charge les endpoints OpenAI personnalisés + +### 👥 Prise en charge multi-comptes + +- Ajoutez plusieurs comptes par fournisseur +- Routage automatique round-robin ou basé sur la priorité +- Repli vers le compte suivant quand un compte atteint son quota + +### 🔄 Renouvellement automatique des tokens + +- Les tokens OAuth se renouvellent automatiquement avant expiration +- Plus besoin de réauthentification manuelle +- Expérience transparente avec tous les fournisseurs + +### 🎨 Combos personnalisés + +- Créez des combinaisons de modèles illimitées +- Mélangez niveaux d'abonnement, économiques et gratuits +- Nommez vos combos pour un accès facile +- Partagez vos combos entre appareils grâce à la Synchronisation cloud + +### 📝 Journalisation des requêtes + +- Activez le mode débogage pour des journaux complets de requêtes/réponses +- Suivez les appels API, les en-têtes et les charges utiles +- Résolvez les problèmes d'intégration +- Exportez les journaux pour analyse + +### 💾 Synchronisation cloud + +- Synchronisez fournisseurs, combos et paramètres entre appareils +- Synchronisation automatique en arrière-plan +- Stockage chiffré et sécurisé +- Accédez à votre configuration depuis n'importe où + +#### Remarques sur le runtime cloud + +- Privilégiez les variables cloud côté serveur en production : + - `BASE_URL` (URL de rappel interne utilisée par le planificateur de synchronisation) + - `CLOUD_URL` (base de l'endpoint de synchronisation cloud) +- `NEXT_PUBLIC_BASE_URL` et `NEXT_PUBLIC_CLOUD_URL` restent prises en charge pour la compatibilité/UI, mais le runtime serveur privilégie désormais `BASE_URL`/`CLOUD_URL`. +- Les requêtes de synchronisation cloud utilisent désormais un délai d'expiration + un comportement de défaillance rapide pour éviter que l'UI ne se bloque quand le DNS/réseau cloud est indisponible. + +### 📊 Analytique d'utilisation + +- Suivez l'utilisation des tokens par fournisseur et modèle +- Estimation des coûts et tendances de dépenses +- Rapports et informations mensuels +- Optimisez vos dépenses IA + +> **💡 IMPORTANT - Comprendre les coûts du tableau de bord :** +> +> Le "coût" affiché dans l'Analytique d'utilisation est **uniquement à des fins de suivi et de comparaison**. +> 9Router lui-même **ne vous facture jamais** quoi que ce soit. Vous ne payez que les fournisseurs directement (si vous utilisez des services payants). +> +> **Exemple :** Si votre tableau de bord affiche "coût total de 290 $" alors que vous utilisez les modèles iFlow, cela représente +> ce que vous auriez payé en utilisant des API payantes directement. Votre coût réel = **0 $** (iFlow est gratuit et illimité). +> +> Considérez-le comme un "suivi d'économies" qui montre combien vous économisez en utilisant des modèles gratuits ou +> en routant via 9Router ! + +### 🌐 Déployez partout + +- 💻 **Localhost** - Par défaut, fonctionne hors ligne +- ☁️ **VPS/Cloud** - Partagez entre appareils +- 🐳 **Docker** - Déploiement en une commande +- 🚀 **Cloudflare Workers** - Réseau mondial de périphérie + +
+ +--- + +## 💰 Tarifs en un coup d'œil + +| Niveau | Fournisseur | Coût | Réinitialisation du quota | Idéal pour | +| ---------------------- | --------------------- | --------------- | ------------------------- | ----------------------------------------- | +| **🚀 ÉCONOMIE DE TOKENS** | **RTK (intégré)** | **GRATUIT** | Toujours actif | **Économisez 20 à 40 % de tokens sur CHAQUE requête** | +| **💳 ABONNEMENT** | Claude Code (Pro/Max) | 20 à 200 $/mois | 5 h + hebdomadaire | Déjà abonné | +| | Codex (Plus/Pro) | 20 à 200 $/mois | 5 h + hebdomadaire | Utilisateurs d'OpenAI | +| | GitHub Copilot | 10 à 19 $/mois | Mensuelle | Utilisateurs de GitHub | +| | Cursor IDE | 20 $/mois | Mensuelle | Utilisateurs de Cursor | +| **💰 BON MARCHÉ** | GLM-5.1 / GLM-4.7 | 0,6 $/1M | Quotidienne 10 h | Sauvegarde économique | +| | MiniMax M2.7 | 0,2 $/1M | Fenêtre de 5 heures | L'option la moins chère | +| | Kimi K2.5 | 9 $/mois forfait | 10M tokens/mois | Coût prévisible | +| **🆓 GRATUIT** | Kiro AI | 0 $ | Illimité | Claude 4.5 + GLM-5 + MiniMax gratuits | +| | OpenCode Free | 0 $ | Illimité | Sans authentification, modèles automatiques | +| | Vertex AI | 300 $ de crédit | Nouveaux comptes GCP | Gemini 3 Pro + DeepSeek + GLM-5 | + +**💡 Astuce pro :** Le combo RTK + Kiro AI + OpenCode Free = **0 $ de coût + 20 à 40 % d'économie de tokens** ! + +--- + +### 📊 Comprendre les coûts et la facturation de 9Router + +**La réalité de la facturation de 9Router :** + +✅ **Le logiciel 9Router = GRATUIT pour toujours** (open source, ne facture jamais) +✅ **Les "coûts" du tableau de bord = Affichage/suivi uniquement** (pas de vraies factures) +✅ **Vous payez les fournisseurs directement** (abonnements ou frais d'API) +✅ **Les fournisseurs GRATUITS restent GRATUITS** (iFlow, Kiro, Qwen = 0 $ illimité) +❌ **9Router n'envoie jamais de factures** et ne débite pas votre carte + +**Comment fonctionne l'affichage des coûts :** + +Le tableau de bord affiche des **coûts estimés** comme si vous utilisiez des API payantes directement. Ce n'est **pas une facturation** — c'est un outil de comparaison pour montrer vos économies. + +**Scénario d'exemple :** + +``` +Affichage du tableau de bord : +• Total des requêtes : 1 662 +• Total des tokens : 47M +• Coût affiché : 290 $ + +Vérification de la réalité : +• Fournisseur : iFlow (GRATUIT illimité) +• Paiement réel : 0,00 $ +• Ce que signifie 290 $ : Le montant que vous avez ÉCONOMISÉ en utilisant des modèles gratuits ! +``` + +**Règles de paiement :** + +- **Fournisseurs par abonnement** (Claude Code, Codex) : Payez-les directement via leurs sites web +- **Fournisseurs bon marché** (GLM, MiniMax) : Payez-les directement, 9Router se contente de router +- **Fournisseurs GRATUITS** (iFlow, Kiro, Qwen) : Vraiment gratuits pour toujours, sans frais cachés +- **9Router** : Ne facture jamais rien, jamais + +--- + +## 🎯 Cas d'utilisation + +### Cas 1 : "J'ai un abonnement Claude Pro" + +**Problème :** Le quota expire inutilisé, limites de débit pendant un codage intensif + +**Solution :** + +``` +Combo : "maximize-claude" + 1. cc/claude-opus-4-7 (utilisez l'abonnement à fond) + 2. glm/glm-5.1 (sauvegarde économique quand le quota est épuisé) + 3. kr/claude-sonnet-4.5 (repli gratuit d'urgence) + +Coût mensuel : 20 $ (abonnement) + ~5 $ (sauvegarde) = 25 $ au total +vs. 20 $ + buter sur les limites = frustration +``` + +### Cas 2 : "Je veux un coût zéro" + +**Problème :** Impossible de payer un abonnement, besoin d'un codage IA fiable + +**Solution :** + +``` +Combo : "free-forever" + 1. kr/claude-sonnet-4.5 (Claude 4.5 gratuit illimité) + 2. kr/glm-5 (GLM-5 gratuit via Kiro) + 3. oc/ (OpenCode Free, sans authentification) + +Coût mensuel : 0 $ +Qualité : Modèles prêts pour la production + RTK économise 20 à 40 % de tokens +``` + +### Cas 3 : "J'ai besoin de coder 24h/24 et 7j/7, sans interruptions" + +**Problème :** Délais à respecter, pas de temps d'arrêt possible + +**Solution :** + +``` +Combo : "always-on" + 1. cc/claude-opus-4-7 (meilleure qualité) + 2. cx/gpt-5.5 (deuxième abonnement) + 3. glm/glm-5.1 (bon marché, réinitialisation quotidienne) + 4. minimax/MiniMax-M2.7 (le moins cher, réinitialisation 5 h) + 5. kr/claude-sonnet-4.5 (gratuit illimité) + +Résultat : 5 couches de repli = zéro temps d'arrêt +Coût mensuel : 20 à 200 $ (abonnements) + 10 à 20 $ (sauvegarde) +``` + +### Cas 4 : "Je veux une IA GRATUITE dans OpenClaw" + +**Problème :** Besoin d'un assistant IA dans les applications de messagerie (WhatsApp, Telegram, Slack...), entièrement gratuit + +**Solution :** + +``` +Combo : "openclaw-free" + 1. kr/claude-sonnet-4.5 (Claude 4.5 gratuit) + 2. kr/glm-5 (GLM-5 gratuit) + 3. kr/MiniMax-M2.5 (MiniMax gratuit) + +Coût mensuel : 0 $ +Accès via : WhatsApp, Telegram, Slack, Discord, iMessage, Signal... +``` + +--- + +## ❓ Questions fréquemment posées + +
+📊 Pourquoi mon tableau de bord affiche-t-il des coûts élevés ? + +Le tableau de bord suit votre utilisation de tokens et affiche des **coûts estimés** comme si vous utilisiez des API payantes directement. Ce n'est **pas une facturation réelle** — c'est une référence pour montrer combien vous économisez en utilisant des modèles gratuits ou des abonnements existants via 9Router. + +**Exemple :** + +- **Le tableau de bord affiche :** "290 $ de coût total" +- **La réalité :** Vous utilisez iFlow (GRATUIT illimité) +- **Votre coût réel :** **0,00 $** +- **Ce que signifie 290 $ :** Le montant que vous avez **économisé** en utilisant des modèles gratuits au lieu d'API payantes ! + +L'affichage des coûts est un "suivi d'économies" qui vous aide à comprendre vos habitudes d'utilisation et vos opportunités d'optimisation. + +
+ +
+💳 9Router me facturera-t-il ? + +**Non.** 9Router est un logiciel gratuit et open source qui s'exécute sur votre propre ordinateur. Il ne vous facture jamais rien. + +**Vous ne payez que :** + +- ✅ **Fournisseurs par abonnement** (Claude Code 20 $/mois, Codex 20 à 200 $/mois) → Payez-les directement sur leurs sites web +- ✅ **Fournisseurs bon marché** (GLM, MiniMax) → Payez-les directement, 9Router route simplement vos requêtes +- ❌ **9Router lui-même** → **Ne facture jamais rien, jamais** + +9Router est un proxy/routeur local. Il n'a pas votre carte bancaire, ne peut pas envoyer de factures et ne possède aucun système de facturation. C'est un logiciel entièrement gratuit. + +
+ +
+🆓 Les fournisseurs GRATUITS sont-ils vraiment illimités ? + +**Oui !** Les fournisseurs GRATUITS actuels (Kiro, OpenCode Free, Vertex) sont réellement gratuits et **sans frais cachés**. + +Ce sont des services gratuits offerts par ces entreprises respectives : + +- **Kiro AI** : Claude 4.5 + GLM-5 + MiniMax gratuits et illimités via AWS Builder ID / Google / GitHub OAuth +- **OpenCode Free** : Proxy de passage sans authentification, modèles récupérés automatiquement depuis `opencode.ai/zen/v1/models` +- **Vertex AI** : 300 $ de crédit gratuit pour les nouveaux comptes Google Cloud (90 jours) + +9Router se contente de router vos requêtes vers eux — il n'y a pas de "piège" ni de facturation future. Ce sont de véritables services gratuits, et 9Router les rend faciles à utiliser avec le support du repli. + +**Niveaux gratuits interrompus (non recommandés) :** + +- ❌ **iFlow** : Était gratuit et illimité, désormais payant (2026) +- ❌ **Qwen Code** : Le niveau gratuit OAuth a été interrompu par Alibaba le 15/04/2026 +- ❌ **Gemini CLI** : Fonctionne toujours, mais l'utiliser avec des outils non-CLI (Claude, Codex, Cursor...) peut entraîner des bannissements de compte — utilisez-le uniquement si vous restez sur le Gemini CLI lui-même + +
+ +
+💰 Comment minimiser mes vrais coûts d'IA ? + +**Stratégie gratuit-d'abord :** + +1. **Commencez avec un combo 100 % gratuit :** + + ``` + 1. gc/gemini-3-flash (180K/mois gratuit de Google) + 2. if/kimi-k2-thinking (gratuit illimité d'iFlow) + 3. qw/qwen3-coder-plus (gratuit illimité de Qwen) + ``` + + **Coût : 0 $/mois** + +2. **Ajoutez une sauvegarde économique** uniquement si nécessaire : + + ``` + 4. glm/glm-4.7 (0,6 $/1M de tokens) + ``` + + **Coût supplémentaire : Vous ne payez que ce que vous utilisez réellement** + +3. **Utilisez les fournisseurs par abonnement en dernier :** + - Uniquement si vous les possédez déjà + - 9Router vous aide à maximiser leur valeur grâce au suivi de quota + +**Résultat :** La plupart des utilisateurs peuvent fonctionner à 0 $/mois en utilisant uniquement les niveaux gratuits ! + +
+ +
+📈 Et si mon utilisation explose soudainement ? + +Le repli intelligent de 9Router évite les frais surprises : + +**Scénario :** Vous êtes en sprint de codage et vous épuisez vos quotas + +**Sans 9Router :** + +- ❌ Limite de débit atteinte → Le travail s'arrête → Frustration +- ❌ Ou : Vous accumulez accidentellement d'énormes factures d'API + +**Avec 9Router :** + +- ✅ L'abonnement atteint sa limite → Repli automatique vers le niveau économique +- ✅ Le niveau économique devient coûteux → Repli automatique vers le niveau gratuit +- ✅ N'arrêtez jamais de coder → Coûts prévisibles + +**Vous gardez le contrôle :** Définissez des limites de dépenses par fournisseur dans le tableau de bord, et 9Router les respecte. + +
+ +--- + +## 📖 Guide d'installation + +
+🔐 Fournisseurs par abonnement (maximisez la valeur) + +### Claude Code (Pro/Max) + +```bash +Tableau de bord → Providers → Connectez Claude Code +→ Connexion OAuth → Renouvellement automatique des tokens +→ Suivi de quota de 5 heures + hebdomadaire + +Modèles : + cc/claude-opus-4-7 + cc/claude-opus-4-6 + cc/claude-sonnet-4-6 + cc/claude-haiku-4-5-20251001 +``` + +**Astuce pro :** Utilisez Opus pour les tâches complexes et Sonnet pour la vitesse. 9Router suit le quota par modèle ! + +### OpenAI Codex (Plus/Pro) + +```bash +Tableau de bord → Providers → Connectez Codex +→ Connexion OAuth (port 1455) +→ Réinitialisation de 5 heures + hebdomadaire + +Modèles : + cx/gpt-5.5 + cx/gpt-5.4 + cx/gpt-5.3-codex + cx/gpt-5.2-codex +``` + +### GitHub Copilot + +```bash +Tableau de bord → Providers → Connectez GitHub +→ OAuth via GitHub +→ Réinitialisation mensuelle (le 1er du mois) + +Modèles : + gh/gpt-5.4 + gh/claude-opus-4.7 + gh/claude-sonnet-4.6 + gh/gemini-3.1-pro-preview + gh/grok-code-fast-1 +``` + +### Cursor IDE + +```bash +Tableau de bord → Providers → Connectez Cursor +→ Connexion OAuth +→ Abonnement mensuel + +Modèles : + cu/claude-4.6-opus-max + cu/claude-4.5-sonnet-thinking + cu/gpt-5.3-codex +``` + +
+ +
+💰 Fournisseurs bon marché (sauvegarde) + +### GLM-5.1 / GLM-4.7 (réinitialisation quotidienne, 0,6 $/1M) + +1. Inscrivez-vous : [Zhipu AI](https://open.bigmodel.cn/) +2. Obtenez la clé API depuis le Coding Plan +3. Tableau de bord → Add API Key : + - Fournisseur : `glm` + - API Key : `votre-clé` + +**Utilisation :** `glm/glm-5.1`, `glm/glm-5`, `glm/glm-4.7` + +**Astuce pro :** Le Coding Plan offre un quota 3× pour 1/7 du coût ! Réinitialisation quotidienne à 10 h. + +### MiniMax M2.7 (réinitialisation 5 h, 0,20 $/1M) + +1. Inscrivez-vous : [MiniMax](https://www.minimax.io/) +2. Obtenez la clé API +3. Tableau de bord → Add API Key + +**Utilisation :** `minimax/MiniMax-M2.7`, `minimax/MiniMax-M2.5` + +**Astuce pro :** L'option la moins chère pour les longs contextes (1M de tokens) ! + +### Kimi K2.5 (9 $/mois forfaitaires) + +1. Abonnez-vous : [Moonshot AI](https://platform.moonshot.ai/) +2. Obtenez la clé API +3. Tableau de bord → Add API Key + +**Utilisation :** `kimi/kimi-k2.5`, `kimi/kimi-k2.5-thinking` + +**Astuce pro :** 9 $/mois forfaitaires pour 10M de tokens = coût effectif de 0,90 $/1M ! + +
+ +
+🆓 Fournisseurs GRATUITS (recommandés) + +### Kiro AI (Claude 4.5 + GLM-5 + MiniMax GRATUIT) + +```bash +Tableau de bord → Connectez Kiro +→ AWS Builder ID, AWS IAM Identity Center, Google ou GitHub +→ Utilisation illimitée + +Modèles : + kr/claude-sonnet-4.5 + kr/claude-haiku-4.5 + kr/glm-5 + kr/MiniMax-M2.5 + kr/qwen3-coder-next + kr/deepseek-3.2 +``` + +**Astuce pro :** La meilleure option gratuite pour Claude. Sans clé API, sans paiement, entièrement illimité. + +### OpenCode Free (sans authentification, modèles automatiques) + +```bash +Tableau de bord → Connectez OpenCode Free +→ Aucune connexion requise (proxy de passage) +→ Modèles récupérés automatiquement depuis opencode.ai/zen/v1/models +``` + +**Astuce pro :** La configuration la plus rapide. Connectez-vous et commencez à coder. + +### Vertex AI (300 $ de crédit gratuit pour les nouveaux comptes GCP) + +```bash +Tableau de bord → Connectez Vertex AI +→ Importez le JSON du compte de service Google Cloud +→ Activez l'API Vertex AI dans votre projet GCP + +Modèles : + vertex/gemini-3.1-pro-preview + vertex/gemini-3-flash-preview + vertex/gemini-2.5-flash + +Vertex Partner (Anthropic / DeepSeek / GLM / Qwen via Vertex) : + vertex-partner/glm-5-maas + vertex-partner/deepseek-v3.2-maas + vertex-partner/qwen3-next-80b-a3b-thinking-maas +``` + +**Astuce pro :** Les nouveaux comptes Google Cloud reçoivent 300 $ de crédit gratuit pendant 90 jours. Largement suffisant pour coder tous les jours. + +
+ +
+🎨 Créez des combos + +### Exemple 1 : Maximisez l'abonnement → Sauvegarde économique + +``` +Tableau de bord → Combos → Create New + +Nom : premium-coding +Modèles : + 1. cc/claude-opus-4-7 (Abonnement principal) + 2. glm/glm-5.1 (Sauvegarde économique, 0,6 $/1M) + 3. minimax/MiniMax-M2.7 (Repli le moins cher, 0,20 $/1M) + +Utilisation en CLI : premium-coding + +Exemple de coût mensuel (100M de tokens) : + 80M via Claude (abonnement) : 0 $ supplémentaire + 15M via GLM : 9 $ + 5M via MiniMax : 1 $ + Total : 10 $ + votre abonnement +``` + +### Exemple 2 : Gratuit uniquement (coût zéro) + +``` +Nom : free-combo +Modèles : + 1. kr/claude-sonnet-4.5 (Claude 4.5 gratuit illimité) + 2. kr/glm-5 (GLM-5 gratuit via Kiro) + 3. vertex/gemini-3.1-pro-preview (300 $ de crédit gratuit) + +Coût : 0 $ pour toujours (+ 20 à 40 % d'économie de tokens via RTK) ! +``` + +
+ +
+🔧 Intégration CLI + +### Cursor IDE + +``` +Settings → Models → Advanced : + OpenAI API Base URL : http://localhost:20128/v1 + OpenAI API Key : [depuis le tableau de bord 9router] + Model : cc/claude-opus-4-7 +``` + +Ou utilisez le combo : `premium-coding` + +### Claude Code + +Modifiez `~/.claude/config.json` : + +```json +{ + "anthropic_api_base": "http://localhost:20128/v1", + "anthropic_api_key": "votre-clé-api-9router" +} +``` + +### Codex CLI + +```bash +export OPENAI_BASE_URL="http://localhost:20128" +export OPENAI_API_KEY="votre-clé-api-9router" + +codex "votre prompt" +``` + +### OpenClaw + +**Option 1 — Tableau de bord (recommandée) :** + +``` +Tableau de bord → CLI Tools → OpenClaw → Sélectionnez le modèle → Appliquer +``` + +**Option 2 — Manuel :** Modifiez `~/.openclaw/openclaw.json` : + +```json +{ + "agents": { + "defaults": { + "model": { + "primary": "9router/kr/claude-sonnet-4.5" + } + } + }, + "models": { + "providers": { + "9router": { + "baseUrl": "http://127.0.0.1:20128/v1", + "apiKey": "sk_9router", + "api": "openai-completions", + "models": [ + { + "id": "kr/claude-sonnet-4.5", + "name": "Claude Sonnet 4.5 (Kiro Free)" + } + ] + } + } + } +} +``` + +> **Remarque :** OpenClaw ne fonctionne qu'avec un 9Router local. Utilisez `127.0.0.1` au lieu de `localhost` pour éviter les problèmes de résolution IPv6. + +### Cline / Continue / RooCode + +``` +Provider : OpenAI Compatible +Base URL : http://localhost:20128/v1 +API Key : [depuis le tableau de bord] +Model : cc/claude-opus-4-7 +``` + +
+ +
+🚀 Déploiement + +### Déploiement VPS + +```bash +# Clonez et installez +git clone https://github.com/decolua/9router.git +cd 9router +npm install +npm run build + +# Configuration +export JWT_SECRET="votre-secret-sécurisé-changez-le" +export INITIAL_PASSWORD="votre-mot-de-passe" +export DATA_DIR="/var/lib/9router" +export PORT="20128" +export HOSTNAME="0.0.0.0" +export NODE_ENV="production" +export NEXT_PUBLIC_BASE_URL="http://localhost:20128" +export NEXT_PUBLIC_CLOUD_URL="https://9router.com" +export API_KEY_SECRET="endpoint-proxy-api-key-secret" +export MACHINE_ID_SALT="endpoint-proxy-salt" + +# Démarrage +npm run start + +# Ou utilisez PM2 +npm install -g pm2 +pm2 start npm --name 9router -- start +pm2 save +pm2 startup +``` + +### Docker + +Images publiées (multi-plateformes `linux/amd64` + `linux/arm64`) : + +- Docker Hub : [`decolua/9router`](https://hub.docker.com/r/decolua/9router) +- GHCR : [`ghcr.io/decolua/9router`](https://github.com/decolua/9router/pkgs/container/9router) + +**Démarrage rapide (utilisez l'image publiée) :** + +```bash +docker run -d \ + --name 9router \ + -p 20128:20128 \ + -v "$HOME/.9router:/app/data" \ + -e DATA_DIR=/app/data \ + decolua/9router:latest +``` + +→ Ouvrez http://localhost:20128 + +**Compiler depuis le code source (dev) :** + +```bash +git clone https://github.com/decolua/9router.git +cd 9router/app +docker build -t 9router . +docker run -d --name 9router -p 20128:20128 \ + -v "$HOME/.9router:/app/data" -e DATA_DIR=/app/data 9router +``` + +**Valeurs par défaut du conteneur :** + +- `PORT=20128` +- `HOSTNAME=0.0.0.0` + +**Commandes utiles :** + +```bash +docker logs -f 9router +docker restart 9router +docker stop 9router && docker rm 9router +docker pull decolua/9router:latest # mise à jour vers la dernière version +``` + +**Persistance des données :** `$HOME/.9router/db/data.sqlite` sur l'hôte ↔ `/app/data/db/data.sqlite` dans le conteneur. + +### Variables d'environnement + +| Variable | Par défaut | Description | +| ---------------------------------------------------- | ------------------------------------------- | -------------------------------------------------------------------------------------------- | +| `JWT_SECRET` | Généré automatiquement (`~/.9router/jwt-secret`) | Secret de signature JWT pour le cookie d'authentification du tableau de bord (remplacez-le pour partager entre instances) | +| `INITIAL_PASSWORD` | `123456` | Mot de passe de première connexion quand aucun hash n'est enregistré | +| `DATA_DIR` | `~/.9router` | Emplacement principal des données de l'app (SQLite dans `$DATA_DIR/db/data.sqlite`) | +| `PORT` | défaut du framework | Port du service (`20128` dans les exemples) | +| `HOSTNAME` | défaut du framework | Hôte de liaison (Docker utilise `0.0.0.0` par défaut) | +| `NODE_ENV` | défaut du runtime | Définissez `production` pour le déploiement | +| `BASE_URL` | `http://localhost:20128` | URL de base interne côté serveur utilisée par les tâches de synchronisation cloud | +| `CLOUD_URL` | `https://9router.com` | URL de base de l'endpoint de synchronisation cloud côté serveur | +| `NEXT_PUBLIC_BASE_URL` | `http://localhost:3000` | URL de base publique/rétrocompatible (préférez `BASE_URL` pour le runtime serveur) | +| `NEXT_PUBLIC_CLOUD_URL` | `https://9router.com` | URL cloud publique/rétrocompatible (préférez `CLOUD_URL` pour le runtime serveur) | +| `API_KEY_SECRET` | `endpoint-proxy-api-key-secret` | Secret HMAC pour les clés API générées | +| `MACHINE_ID_SALT` | `endpoint-proxy-salt` | Sel pour le hachage stable de l'ID machine | +| `ENABLE_REQUEST_LOGS` | `false` | Active les journaux de requêtes/réponses dans `logs/` | +| `AUTH_COOKIE_SECURE` | `false` | Force le cookie d'authentification `Secure` (mettez `true` derrière un proxy inverse HTTPS) | +| `REQUIRE_API_KEY` | `false` | Applique la clé API Bearer sur les routes `/v1/*` (recommandé pour les déploiements exposés à Internet) | +| `HTTP_PROXY`, `HTTPS_PROXY`, `ALL_PROXY`, `NO_PROXY` | vide | Proxy sortant optionnel pour les appels aux fournisseurs externes | +| `SEARXNG_URL` | `http://localhost:8888/search` | Endpoint du fournisseur de recherche web SearXNG intégré, sans authentification | + +Remarques : + +- Les variables de proxy en minuscules sont également prises en charge : `http_proxy`, `https_proxy`, `all_proxy`, `no_proxy`. +- `.env` n'est pas intégré à l'image Docker (`.dockerignore`) ; injectez la configuration du runtime avec `--env-file` ou `-e`. +- Sous Windows, `APPDATA` peut être utilisé pour résoudre le chemin de stockage local. +- `INSTANCE_NAME` apparaît dans l'ancienne documentation/les anciens modèles d'environnement, mais n'est actuellement pas utilisé au runtime. + +### Fichiers d'exécution et stockage + +- État principal de l'app : `${DATA_DIR}/db/data.sqlite` (SQLite — fournisseurs, combos, alias, clés, paramètres, historique d'utilisation) +- Sauvegardes automatiques : `${DATA_DIR}/db/backups/` +- Journaux optionnels de requêtes/translator : `/logs/...` quand `ENABLE_REQUEST_LOGS=true` +- `${DATA_DIR}` et `~/.9router` résolvent tous deux le même emplacement dans un conteneur Docker — le lien symbolique `/root/.9router -> /app/data` est créé au moment de la compilation. + +
+ +--- + +## 📊 Modèles disponibles + +
+Afficher tous les modèles disponibles + +**Claude Code (`cc/`)** - Pro/Max : + +- `cc/claude-opus-4-7` +- `cc/claude-opus-4-6` +- `cc/claude-sonnet-4-6` +- `cc/claude-sonnet-4-5-20250929` +- `cc/claude-haiku-4-5-20251001` + +**Codex (`cx/`)** - Plus/Pro : + +- `cx/gpt-5.5` +- `cx/gpt-5.4` +- `cx/gpt-5.3-codex` +- `cx/gpt-5.2-codex` +- `cx/gpt-5.1-codex-max` + +**GitHub Copilot (`gh/`)** : + +- `gh/gpt-5.4` +- `gh/claude-opus-4.7` +- `gh/claude-sonnet-4.6` +- `gh/gemini-3.1-pro-preview` +- `gh/grok-code-fast-1` + +**Cursor (`cu/`)** - Abonnement : + +- `cu/claude-4.6-opus-max` +- `cu/claude-4.5-sonnet-thinking` +- `cu/gpt-5.3-codex` +- `cu/kimi-k2.5` + +**GLM (`glm/`)** - 0,6 $/1M : + +- `glm/glm-5.1` +- `glm/glm-5` +- `glm/glm-4.7` + +**MiniMax (`minimax/`)** - 0,2 $/1M : + +- `minimax/MiniMax-M2.7` +- `minimax/MiniMax-M2.5` + +**Kimi (`kimi/`)** - 9 $/mois forfaitaires : + +- `kimi/kimi-k2.5` +- `kimi/kimi-k2.5-thinking` + +**Kiro (`kr/`)** - GRATUIT illimité : + +- `kr/claude-sonnet-4.5` +- `kr/claude-haiku-4.5` +- `kr/glm-5` +- `kr/MiniMax-M2.5` +- `kr/qwen3-coder-next` +- `kr/deepseek-3.2` + +**OpenCode Free (`oc/`)** - GRATUIT sans authentification : + +- Récupérés automatiquement depuis `opencode.ai/zen/v1/models` + +**Vertex AI (`vertex/`)** - 300 $ de crédit gratuit : + +- `vertex/gemini-3.1-pro-preview` +- `vertex/gemini-3-flash-preview` +- `vertex/gemini-2.5-flash` +- `vertex-partner/glm-5-maas` +- `vertex-partner/deepseek-v3.2-maas` + +
+ +--- + +## 🐛 Dépannage + +**"Language model did not provide messages"** + +- Quota du fournisseur épuisé → Consultez le suivi de quota du tableau de bord +- Solution : utilisez le repli du combo ou passez à un niveau moins cher + +**Limites de débit** + +- Quota d'abonnement épuisé → Repli vers GLM/MiniMax +- Ajoutez le combo : `cc/claude-opus-4-7 → glm/glm-5.1 → kr/claude-sonnet-4.5` + +**Token OAuth expiré** + +- Renouvelé automatiquement par 9Router +- Si les problèmes persistent : Tableau de bord → Provider → Reconnecter + +**Coûts élevés** + +- Activez RTK dans Tableau de bord → Paramètres d'Endpoint (activé par défaut, économise 20 à 40 % de tokens) +- Consultez les statistiques d'utilisation dans le tableau de bord +- Changez le modèle principal pour GLM/MiniMax +- Utilisez le niveau gratuit (Kiro, OpenCode Free, Vertex) pour les tâches non critiques + +**Le tableau de bord s'ouvre sur le mauvais port** + +- Définissez `PORT=20128` et `NEXT_PUBLIC_BASE_URL=http://localhost:20128` + +**La première connexion ne fonctionne pas** + +- Vérifiez `INITIAL_PASSWORD` dans `.env` +- S'il n'est pas défini, le mot de passe de secours est `123456` + +**Pas de journaux de requêtes dans `logs/`** + +- Définissez `ENABLE_REQUEST_LOGS=true` + +--- + +## 🛠️ Pile technologique + +- **Runtime** : Node.js 20+ +- **Framework** : Next.js 16 +- **UI** : React 19 + Tailwind CSS 4 +- **Base de données** : SQLite (better-sqlite3 / node:sqlite / sql.js en secours) +- **Streaming** : Server-Sent Events (SSE) +- **Authentification** : OAuth 2.0 (PKCE) + JWT + clés API + +--- + +## 📝 Référence de l'API + +### Chat Completions + +```bash +POST http://localhost:20128/v1/chat/completions +Authorization: Bearer votre-clé-api +Content-Type: application/json + +{ + "model": "cc/claude-opus-4-6", + "messages": [ + {"role": "user", "content": "Écrivez une fonction pour..."} + ], + "stream": true +} +``` + +### Lister les modèles + +```bash +GET http://localhost:20128/v1/models +Authorization: Bearer votre-clé-api + +→ Renvoie tous les modèles et combos au format OpenAI +``` + +## 📧 Support + +- **Site web** : [9router.com](https://9router.com) +- **GitHub** : [github.com/decolua/9router](https://github.com/decolua/9router) +- **Problèmes** : [github.com/decolua/9router/issues](https://github.com/decolua/9router/issues) + +--- + +## 👥 Contributeurs + +Merci à tous les contributeurs qui ont contribué à améliorer 9Router ! + +[![Contributors](https://contrib.rocks/image?repo=decolua/9router&max=150&columns=15&anon=1&v=20260309)](https://github.com/decolua/9router/graphs/contributors) + +--- + +## 📊 Graphique d'étoiles + +[![Star Chart](https://starchart.cc/decolua/9router.svg?variant=adaptive)](https://starchart.cc/decolua/9router) + +## 🔀 Forks + +**[OmniRoute](https://github.com/diegosouzapw/OmniRoute)** — Un fork TypeScript complet de 9Router. Ajoute plus de 36 fournisseurs, un repli automatique à 4 niveaux, des API multimodales (images, embeddings, audio, TTS), un disjoncteur, un cache sémantique, des évaluations de LLM et un tableau de bord soigné. Plus de 368 tests unitaires. Disponible via npm et Docker. + +--- + +## 🙏 Remerciements + +Construit sur les épaules de géants : + +- **[CLIProxyAPI](https://github.com/router-for-me/CLIProxyAPI)** — implémentation originale en Go qui a inspiré ce port JavaScript. +- **[RTK](https://github.com/rtk-ai/rtk)** ![Stars](https://img.shields.io/github/stars/rtk-ai/rtk?style=flat&color=yellow) — économiseur de tokens en Rust. 9Router porte son pipeline de compression en JS → **−20 à 40 % de tokens d'entrée** sur chaque requête. +- **[Caveman](https://github.com/JuliusBrussee/caveman)** ![Stars](https://img.shields.io/github/stars/JuliusBrussee/caveman?style=flat&color=yellow) par **[@JuliusBrussee](https://github.com/JuliusBrussee)** — viral _"why use many token when few token do trick"_. 9Router adapte son prompt → **−65 % de tokens de sortie**. +- **[Ponytail](https://github.com/DietrichGebert/ponytail)** ![Stars](https://img.shields.io/github/stars/DietrichGebert/ponytail?style=flat&color=yellow) par **[@DietrichGebert](https://github.com/DietrichGebert)** — compétence de _"dev sénior fainéant"_. 9Router injecte sa hiérarchie YAGNI-d'abord → **moins de tokens, moins de code, des diffs plus courts**. + +Un immense merci à ces auteurs — sans leur travail, les fonctionnalités d'économie de tokens de 9Router n'existeraient pas. Mettez-leur une ⭐ sur GitHub ! + +--- + +## 📄 Licence + +Licence MIT - voir [LICENSE](LICENSE) pour plus de détails. + +--- + +
+ Conçu avec ❤️ pour les développeurs qui codent 24h/24 et 7j/7 +
+ diff --git a/i18n/README.pt-BR.md b/i18n/README.pt-BR.md new file mode 100644 index 00000000..c141d679 --- /dev/null +++ b/i18n/README.pt-BR.md @@ -0,0 +1,1526 @@ +
+ Painel do 9Router + + # 9Router — roteador de IA GRATUITO e economizador de tokens + + **Nunca pare de codificar. Economize 20-40% de tokens com RTK + fallback automático para modelos de IA GRATUITOS e baratos.** + + **Conecte todas as ferramentas de código de IA (Claude Code, Cursor, Antigravity, Copilot, Codex, Gemini, OpenCode, Cline, OpenClaw...) a mais de 40 provedores de IA e mais de 100 modelos.** + + [![npm](https://img.shields.io/npm/v/9router.svg)](https://www.npmjs.com/package/9router) + [![Downloads](https://img.shields.io/npm/dm/9router.svg)](https://www.npmjs.com/package/9router) + [![Docker Pulls](https://img.shields.io/docker/pulls/decolua/9router.svg?logo=docker&label=Docker%20pulls)](https://hub.docker.com/r/decolua/9router) + [![GHCR](https://img.shields.io/badge/GHCR-decolua%2F9router-blue?logo=github)](https://github.com/decolua/9router/pkgs/container/9router) + [![Licença](https://img.shields.io/npm/l/9router.svg)](https://github.com/decolua/9router/blob/main/LICENSE) + +decolua%2F9router | Trendshift + +[🚀 Início rápido](#-início-rápido) • [💡 Recursos](#-principais-recursos) • [📖 Configuração](#-guia-de-configuração) • [🌐 Site](https://9router.com) + +[🇺🇸 English](../README.md) • [🇻🇳 Tiếng Việt](./README.vi.md) • [🇨🇳 中文](./README.zh-CN.md) • [🇯🇵 日本語](./README.ja-JP.md) • [🇷🇺 Русский](./README.ru.md) • [🇹🇭 ไทย](./README.th.md) • [🇮🇷 فارسی](./README.fa_IR.md) • [🇮🇩 Indonesia](./README.id-ID.md) + +
+ +--- + +## 🤔 Por que 9Router? + +**Pare de desperdiçar dinheiro, tokens e atingir limites:** + +- ❌ A cota de assinatura expira sem ser utilizada todos os meses +- ❌ Os limites de requisições interrompem você durante a programação +- ❌ Saídas de ferramentas (`git diff`, `grep`, `ls`...) consomem tokens rapidamente +- ❌ APIs caras ($20–50/mês por provedor) +- ❌ Troca manual entre provedores + +**9Router resolve isso:** + +- ✅ **RTK Token Saver** - Compactação automática do conteúdo de `tool_result`, economize 20-40% de tokens por solicitação +- ✅ **Maximize as assinaturas** - Rastreie a cota, aproveite toda a cota antes da renovação +- ✅ **Auto fallback** - Assinatura → Econômico → Gratuito, sem tempo de inatividade +- ✅ **Múltiplas contas** - Round-robin entre contas por provedor +- ✅ **Universal** - Funciona com Claude Code, Codex, Cursor, Cline, qualquer ferramenta CLI + +--- + +## 🔄 Como funciona + +``` +┌─────────────┐ +│ Sua CLI │ (Claude Code, Codex, OpenClaw, Cursor, Cline...) +│ Ferramenta│ +└──────┬──────┘ + │ http://localhost:20128/v1 + ↓ +┌─────────────────────────────────────────────┐ +│ 9Router (Roteador inteligente) │ +│ • RTK Token Saver (reduz tokens de `tool_result`) │ +│ • Tradução de formatos (OpenAI ↔ Claude) │ +│ • Acompanhamento de cotas │ +│ • Renovação automática de token │ +└──────┬──────────────────────────────────────┘ + │ + ├─→ [Nível 1: ASSINATURA] Claude Code, Codex, GitHub Copilot + │ ↓ cota esgotada + ├─→ [Nível 2: ECONÔMICO] GLM ($0.6/1M), MiniMax ($0.2/1M) + │ ↓ limite de orçamento + └─→ [Nível 3: GRATUITO] Kiro, OpenCode Free, Vertex ($300 em créditos) + +Resultado: programe sem interrupções, com custo mínimo e economia de 20% a 40% dos tokens via RTK +``` + +--- + +## ⚡ Início rápido + +**1. Instale globalmente:** + +```bash +npm install -g 9router +9router +``` + +🎉 O painel abre em `http://localhost:20128` + +**2. Conecte um provedor GRATUITO (sem necessidade de inscrição):** + +Painel → Provedores → Conecte **Kiro AI** (~50 créditos/mês grátis: Claude 4.5 + GLM-5 + MiniMax) ou **OpenCode Free** (sem autenticação) → Pronto! + +**3. Use em sua ferramenta CLI:** + +``` +Configurações do Claude Code/Codex/OpenClaw/Cursor/Cline: + Endpoint: http://localhost:20128/v1 + API Key: [copie do painel] + Model: kr/claude-sonnet-4.5 +``` + +**É isso aí!** Comece a codificar com modelos de IA GRATUITOS. + +**Alternativa: executar a partir do código-fonte (este repositório):** + +Este pacote de repositório é privado (`9router-app`), portanto, a execução pelo código-fonte/Docker é o caminho de desenvolvimento local esperado. + +```bash +cp .env.example .env +npm install +PORT=20128 NEXT_PUBLIC_BASE_URL=http://localhost:20128 npm run dev +``` + +Modo de produção: + +```bash +npm run build +PORT=20128 HOSTNAME=0.0.0.0 NEXT_PUBLIC_BASE_URL=http://localhost:20128 npm run start +``` + +URLs padrão: + +- Painel: `http://localhost:20128/dashboard` +- API compatível com OpenAI: `http://localhost:20128/v1` + +--- + +## Guias de vídeo + +
+ + + + + + + + + + + + + + + + + + + + + + + +
+ + Tiết kiệm chi phí LLM với 9Router +
+ 🇻🇳 Tiếng Việt
+ Tiết kiệm chi phí LLM cho OpenClaw no 9Router
por Mì AI
+
+ + 9Router + Claude Code FREE Unlimited Setup +
+ 🇵🇰 اردو / हिन्दी
+ 9Router + Claude Code Configuração ilimitada GRATUITA
por Construir IA com Hamid
+
+ + 9Router Setup Tutorial +
+ 🇺🇸 Inglês
+ 9Router + Claude Code Configuração GRATUITA
por Construir IA com Hamid
+
+ + 9Router Setup Tutorial +
+ 🇺🇸 Inglês
+ 9Router + Claude Code Configuração GRATUITA
por Construir IA com Hamid
+
+ + Claude Code FREE Forever +
+ 🇺🇸 Inglês
+ Claude Code FREE Forever - Modelos ilimitados
por Construa IA com Hamid
+
+ +Claude CLI Free Setup +
+ 🇺🇸 Inglês
+ Configuração gratuita Claude CLI com 9Router 🚀
por CodeVerse Soban
+
+ + Cài đặt OpenClaw Free A-Z +
+ 🇻🇳 Tiếng Việt
+ Cài Đặt OpenClaw Free Từ A-Z + 9Router
por Mai Gia
+
+ + FREE OpenClaw with Claude Opus +
+ 🇺🇸 Inglês
+ FREE OpenClaw + Claude Opus 4.6
por Construir IA com Hamid
+
+ + Claude CLI Free Setup +
+ 🇮🇩 Indonésia
+Koding 24 Jam Anti Rate Limit! Hemat Token AI 65% | Tutorial Configuração Rápida 9Router 🚀
por Krisswuh
+
+ + Cara Deploy 9Router di Hugging Face GRATIS Non-Stop! | Alternatif VPS RAM 16GB +
+ 🇮🇩 Indonésia
+ Cara Implante 9Router no Hugging Face GRATUITAMENTE sem parar! | Alternativa VPS RAM 16GB
por Krisswuh
+
+ + این شکلی از هر API ای استفاده کن برای هوش مصنوعی +
+ 🇮🇷 Persa-فارسی
+ این شکلی از هر API ای استفاده کن برای هوش مصنوعی
por Matin SenPai
+
+ + Hướng Dẫn Setup OpenClaw + 9Router: Tạo Bot Zalo AI Tự Động Từ A-Z +
+ 🇻🇳 Tiếng Việt
+ Hướng Dẫn Setup OpenClaw + 9Router: Tạo Bot Zalo AI Tự Động Từ A-Z
por tuanminhhole
+
+ +
+ +> 🎬 **Fez um vídeo sobre o 9Router?** Envie um [Pull Request](https://github.com/decolua/9router/pulls) adicionando seu vídeo a esta seção - nós o mesclaremos! + +--- + +## 🛠️ Ferramentas CLI suportadas + +9Router funciona perfeitamente com todas as principais ferramentas de codificação de IA: + +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+ Claude Code
+ Código-Claude +
+ OpenClaw
+ OpenClaw +
+ Codex
+ Codex +
+ OpenCode
+ OpenCode +
+ Cursor
+ Cursor +
+ Antigravity
+ antigravidade +
+ Cline
+ Cline +
+ Continue
+ Continuar +
+Droid
+ Droid +
+ Roo
+ Roo +
+ Copilot
+ Copilot +
+Kilo Code
+ Código Kilo +
+ OpenDesign
+ OpenDesign +
+ jcode
+ jcódigo +
+ Grok Build
+ Grok Build +
+ Devin CLI
+ Devin CLI +
+ DeepSeek TUI
+ DeepSeek TUI +
+ Qwen Code
+ Código Qwen +
+
+ +--- + +## 🌐 Provedores compatíveis + +### 🔐 Provedores OAuth + +
+ + + + + + + + + +
+ Claude Code
+ Código-Claude +
+ Antigravity
+ antigravidade +
+ Codex
+ Codex +
+ GitHub
+ GitHub +
+ Cursor
+ Cursor +
+ Kimchi
+ Kimchi +
+
+ +### 🆓 Provedores gratuitos + +
+ + + + + + +
+ Kiro
+ Kiro AI
+ Claude 4.5 + GLM-5 + MiniMax
50 créditos/mês grátis
+
+ OpenCode Free
+ OpenCode grátis
+ Sem autenticação • Modelos de busca automática
Grátis (a lista de modelos varia)
+
+ Vertex AI
+ Vertex AI
+ Gemini 3 Pro + GLM-5 + DeepSeek
$300 créditos grátis
+
+
+ +> **Observação:** Os níveis gratuitos iFlow, Qwen Code e Gemini CLI foram descontinuados em 2026. Em vez disso, use Kiro / OpenCode Free / Vertex. +> +> **Kiro AI** mudou para um modelo pago em setembro de 2025 — o nível gratuito agora é limitado a **50 créditos/mês** (mais 500 créditos de avaliação para novas contas nos primeiros 30 dias). Camadas pagas: Pro $20/mo (1.000 créditos), Pro+ $40/mo (2.000), Pro Max $100/mo (5.000), Power $200/mo (10.000). +> A lista de modelos **OpenCode Free** varia com o tempo (alguns modelos são gratuitos apenas para promoções limitadas) — sujeito a alterações sem aviso prévio. +> **Vertex AI**: o crédito gratuito $300 para novas contas do GCP ainda é válido, mas desde março de 2026 o **endpoint da API Gemini não consome mais esses créditos**. Em vez disso, chame o endpoint **Vertex AI Studio**. + +### 🔑 Provedores de chaves de API (40+) + +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+ OpenRouter
+OpenRouter +
+ GLM
+ GLM +
+ Kimi
+ Kimi +
+ MiniMax
+MiniMax +
+ OpenAI
+ OpenAI +
+ Anthropic
+ Antrópico +
+ Gemini
+ Gemini +
+ DeepSeek
+ DeepSeek +
+ Groq
+ Groq +
+ xAI
+ xAI +
+ Mistral
+ Mistral +
+ Perplexity
+ Perplexidade +
+ Together
+ Juntos AI +
+ Fireworks
+ Fogos de artifício +
+ Cerebras
+Cerebras +
+ Cohere
+ Cohere +
+ NVIDIA
+ NVIDIA +
+ SiliconFlow
+SiliconFlow +
+

...e mais de 20 provedores, incluindo Nebius, Chutes, Hyperbolic e endpoints personalizados compatíveis com OpenAI/Anthropic

+
+ +### 🏠 Provedores auto-hospedados + +Para fala e incorporações veiculadas em **sua própria** máquina — whisper.cpp, +faster-whisper, Speaches, Kokoro-FastAPI, openai-speech, llama.cpp/llama-server, +vLLM, Infinity, text-embeddings-inference ou qualquer outro serviço compatível com o formato OpenAI. + +| Provedor | Ponto final usado | Servidor típico | +| --- | --- | --- | +| **STT auto-hospedado** | `/v1/audio/transcriptions` | whisper.cpp, faster-whisper | +| **TTS auto-hospedado** | `/v1/audio/speech` | Kokoro-FastAPI, openedai-speech | +| **Incorporação auto-hospedada** | `/v1/embeddings` | llama-server, vLLM, Infinity | + +Todos os outros provedores de voz são um serviço de nuvem nomeado com um endpoint fixo. Estes +três leem seus endereços de **cada conexão**, para que um provedor possa apontar para +várias máquinas e balanceamento de carga entre elas como qualquer outra. + +Defina-o na conexão como `providerSpecificData.baseUrl`: + +| Provedor | Informe | Resultado | +| --- | --- | --- | +| STT auto-hospedado | o URL completo — `http://host:8080/v1/audio/transcriptions` | usado como está | +| TTS auto-hospedado | a raiz do servidor — `http://host:8880` | `+ /v1/audio/speech` | +| Incorporação auto-hospedada | a **base OpenAI**, `/v1` incluída — `http://host:8080/v1` | `+ /embeddings` | + +> **Cuidado com `/v1` em embeddings.** O adaptador anexa `/embeddings`, então +> `http://host:8080` resolve para `http://host:8080/embeddings` e perde o +> rota OpenAI – o llama-server responde **501**. Dê a ele o mesmo URL base de um OpenAI +> o cliente usaria. Um `.../v1/embeddings` completo também é aceito, então um valor colado +> de um exemplo `curl` também funciona. + +A chave API não é verificada pela maioria dos servidores locais, mas o campo não deve estar vazio: +é o que dá à conexão um registro de credenciais, e `baseUrl` reside lá. +Qualquer espaço reservado funciona. + +A incorporação auto-hospedada **não tem fallback na nuvem por design** — uma conexão salva +sem um `baseUrl` é relatado como um erro de configuração, em vez de silenciosamente +voltando para `api.openai.com`, que enviaria seu texto de entrada e chave de API para +terceiros por meio de um provedor chamado "Auto-hospedado". + +--- + +## 💡 Principais recursos + +| Recurso | O que faz | Por que é importante | +| --------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | ------------------------------------------------- | +| 🚀 **RTK Token Saver** ([RTK](https://github.com/rtk-ai/rtk) ⭐40K) | Compactar saídas de ferramentas (`git diff`, `grep`, `ls`, `tree`...) antes de enviar para LLM | Economize **20-40% de tokens de entrada** por solicitação | +| 🧠 **Headroom Token Saver** ([Headroom](https://github.com/chopratejas/headroom)) | Proxy `/v1/compress` externo opcional antes do roteamento do provedor | Economize mais tokens de contexto sem alterar clientes | +| 🪨 **Modo Caveman** ([Caveman](https://github.com/JuliusBrussee/caveman) ⭐52K) | Injetar prompt de fala do homem das cavernas → Respostas do LLM concisas, substância técnica preservada | Economize **até 65% de tokens de produção** | +| 🐴 **Ponytail** ([Ponytail](https://github.com/DietrichGebert/ponytail)) | Injetar prompt "lazy senior dev" → LLM escreve código mínimo YAGNI primeiro (Lite/Full/Ultra) | **Menos tokens de saída, menos refatoração** | +| 🎯 **Fallback inteligente de 3 camadas** | Rota automática: Assinatura → Barato → Grátis | Nunca pare de codificar, tempo de inatividade zero | +| 📊 **Acompanhamento de cotas em tempo real** | Contagem de tokens ao vivo + contagem regressiva de redefinição | Maximizar o valor da assinatura | +| 🔄 **Tradução de formato** | OpenAI ↔ Claude ↔ Gemini ↔ Cursor ↔ Kiro ↔ Vertex | Funciona com qualquer ferramenta CLI | +| 👥 **Suporte para múltiplas contas** | Várias contas por provedor | Balanceamento de carga + redundância | +| 🔄 **Atualização automática de token** | Os tokens OAuth são atualizados automaticamente | Não é necessário fazer novo login manual | +| 🎨 **Combos Personalizados** | Crie combinações ilimitadas de modelos | Adapte o fallback às suas necessidades | +| 📝 **Registro de requisições** | Modo de depuração com logs completos de solicitação/resposta | Solucione problemas facilmente | +| 💾 **Sincronização na nuvem** | Sincronizar configuração entre dispositivos | Mesma configuração em todos os lugares | +| 📊 **Análise de uso** | Acompanhe tokens, custos e tendências ao longo do tempo | Otimizar gastos | +| 🌐 **Implante em qualquer lugar** | Localhost, VPS, Docker, Cloudflare Workers | Opções flexíveis de implantação | + +Configure `X-9Router-Token-Saver: off` para ignorar todos os economizadores de tokens para uma solicitação de chat. + +
+📖 Detalhes do recurso + +### 🚀 RTK Token Saver + +As saídas da ferramenta (`git diff`, `grep`, `find`, `ls`, `tree`, despejos de log...) geralmente consomem de 30 a 50% do seu orçamento de contexto. O RTK os detecta e aplica compactação inteligente e sem perdas **antes** que a solicitação chegue ao LLM: + +- **Filtros:** `git-diff`, `git-status`, `grep`, `find`, `ls`, `tree`, `dedup-log`, `smart-truncate`, `read-numbered`, `search-list` +- **Detecção automática:** Não é necessária configuração — o RTK inspeciona o primeiro 1 KB de cada `tool_result` e escolhe o filtro correto. +- **Seguro por design:** Se um filtro falhar, gerar uma exceção ou aumentar a saída, o RTK mantém silenciosamente o texto original. Erros nunca interrompem sua solicitação. +- **Universal:** Funciona em todos os formatos (OpenAI, Claude, Gemini, Cursor, Kiro, OpenAI Responses) porque é executado **antes** de qualquer tradução de formato. +- **Padrão ATIVADO:** Alterne a qualquer momento em Painel → Configurações de endpoint. + +``` +Sem RTK: 47 mil tokens enviados ao LLM +Com RTK: 28 mil tokens enviados ao LLM (40% de economia · mesmo contexto · mesma resposta) +``` + +### 🧠 Headroom Token Saver + +O headroom é opcional e funciona separadamente. 9Router chama o endpoint `/v1/compress` local do Headroom e, em seguida, mantém roteamento normal, fallback, autenticação e rastreamento de uso: + +``` +Cliente → 9Router → Headroom /v1/compress → 9Router → provedor +``` + +Configuração local: + +```bash +pip install "headroom-ai[proxy]" +headroom proxy --port 8787 +``` + +Habilite em Painel → Endpoint → Economia de tokens → Headroom. URL padrão: `http://localhost:8787`. + +Exemplos de Docker: + +```bash +# Serviço Headroom na mesma rede Docker +http://headroom:8787 + +# Headroom em execução na máquina host +http://host.docker.internal:8787 +``` + +Se o Headroom estiver inativo ou retornar um erro, o o 9Router seguirá em modo aberto e enviará a solicitação original. + +### 🐴 Ponytail (desenvolvedor sênior preguiçoso) + +O Ponytail injeta um prompt do sistema _"lazy senior dev"_ em cada solicitação, direcionando o LLM para o código mínimo, primeiro YAGNI - preferindo exclusão a adição, stdlib sobre novos deps, one-liners sobre abstrações. Adaptado de [DietrichGebert/ponytail](https://github.com/DietrichGebert/ponytail). + +- **Lite** — Construa o que for solicitado, nomeie a alternativa mais preguiçosa. +- **Full** — escada YAGNI aplicada: stdlib → nativo → dependências existentes → one-liner → código mínimo. +- **Ultra** — Extremista YAGNI: exclua primeiro, envie a linha única, desafie o restante do requisito na mesma resposta. + +``` +Sem Ponytail: código verboso, abstrações extras e estrutura "por precaução" +Com Ponytail: menor diff funcional, sem abstrações não solicitadas e menos tokens +``` + +Nunca negocie: validação de entrada, tratamento de erros que evita perda de dados, segurança, acessibilidade ou qualquer coisa explicitamente solicitada. Ative em Painel → Endpoint → Ponytail. Pode ser combinado com Caveman (concisão de saída) e RTK (compressão de entrada). + +### 🎯 Fallback inteligente de 3 camadas + +Crie combos com fallback automático: + +``` +Combo: "my-coding-stack" + 1. cc/claude-opus-4-6 (sua assinatura) + 2. glm/glm-4.7 (backup econômico, $0,60/1M) + 3. if/kimi-k2-thinking (fallback gratuito) + +→ Alterna automaticamente quando a cota acaba ou ocorre um erro +``` + +### 📊 Rastreamento de cotas em tempo real + +- Consumo de token por provedor +- Redefinir contagem regressiva (5 horas, diariamente, semanalmente) +- Estimativa de custos para níveis pagos +- Relatórios mensais de gastos + +### 🔄 Tradução de formato + +Tradução perfeita entre formatos: + +- **OpenAI** ↔ **Claude** ↔ **Gemini** ↔ **Cursor** ↔ **Kiro** ↔ **Vertex** ↔ **Antigravity** ↔ **Ollama** ↔ **OpenAI Responses** +- Sua ferramenta CLI envia formato OpenAI → 9Router traduz → Provedor recebe formato nativo +- Funciona com qualquer ferramenta que suporte endpoints OpenAI personalizados + +### 👥 Suporte para múltiplas contas + +- Adicione várias contas por provedor +- Round-robin automático ou roteamento baseado em prioridade +- Fallback para a próxima conta quando uma delas atingir a cota + +### 🔄 Atualização automática de token + +- Os tokens OAuth são atualizados automaticamente antes da expiração +- Não é necessária reautenticação manual +- Experiência perfeita em todos os provedores + +### 🎨 Combos Personalizados + +- Crie combinações ilimitadas de modelos +- Combine níveis de assinatura, baratos e gratuitos +- Nomeie seus combos para facilitar o acesso +- Compartilhe combos entre dispositivos com Cloud Sync + +### 📝 Registro de requisições + +- Habilite o modo de depuração para logs completos de solicitação/resposta +- Rastreie chamadas de API, cabeçalhos e payloads +- Solucionar problemas de integração +- Exportar logs para análise + +### 💾 Sincronização na nuvem + +- Sincronize provedores, combos e configurações entre dispositivos +- Sincronização automática em segundo plano +- Armazenamento criptografado seguro +- Acesse sua configuração de qualquer lugar + +#### Notas de tempo de execução na nuvem + +- Prefira variáveis de nuvem do lado do servidor na produção: + - `BASE_URL` (URL interna de callback usado pelo agendador de sincronização) + - `CLOUD_URL` (base de endpoint de sincronização em nuvem) +- `NEXT_PUBLIC_BASE_URL` e `NEXT_PUBLIC_CLOUD_URL` ainda são suportados para compatibilidade/UI, mas o tempo de execução do servidor agora prioriza `BASE_URL`/`CLOUD_URL`. +- As solicitações de sincronização na nuvem agora usam o comportamento de tempo limite + falha rápida para evitar o travamento da interface do usuário quando o DNS/rede da nuvem estiver indisponível. + +### 📊 Análise de uso + +- Rastreie o uso de token por provedor e modelo +- Estimativa de custos e tendências de gastos +- Relatórios e insights mensais +- Otimize seus gastos com IA + +> **💡 IMPORTANTE - Compreendendo os custos do painel:** +> +> O "custo" exibido no Usage Analytics é **apenas para fins de rastreamento e comparação**. +> O próprio 9Router **nunca cobra** nada. Você paga apenas aos provedores diretamente (se usar serviços pagos). +> +> **Exemplo:** Se o seu painel mostrar "Custo total $290" ao usar modelos gratuitos do Kiro, isso representa +> o que você pagaria usando APIs pagas diretamente. Seu custo real = **$0** (nível gratuito do Kiro: ~50 créditos/mês). +> +> Pense nisso como um "rastreador de economia" que mostra quanto você está economizando usando modelos gratuitos ou +> roteamento através do 9Router! + +### 🌐 Implante em qualquer lugar + +- 💻 **Localhost** - Padrão, funciona offline +- ☁️ **VPS/Cloud** - Compartilhe entre dispositivos +- 🐳 **Docker** – Implantação com um comando +- 🚀 **Cloudflare Workers** - Rede global de edge + +
+ +--- + +## 💰 Visão geral dos preços + +| Nível | Provedor | Custo | Redefinição de cota | Melhor para | +| ------------------- | --------------------- | ------------ | ---------------- | --------------------------------------- | +| **🚀 ECONOMIA DE TOKEN** | **RTK (integrado)** | **GRÁTIS** | Sempre ligado | **Economize 20-40% de tokens em CADA solicitação** | +| **💳 ASSINATURA** | Claude Code (Pro/Max) | $20-200/mo | 5h + semanalmente | Já inscrito | +| | Codex (Plus/Pro) | $20-200/mo | 5h + semanalmente | Usuários OpenAI | +| | GitHub Copilot | $10-19/mo | Mensalmente | Usuários do GitHub | +| | Cursor IDE | $20/mo | Mensalmente | Usuários de cursor | +| **💰 BARATO** | GLM-5.1/GLM-4.7 | $0.6/1M | Diariamente 10h | Backup de orçamento | +| | MiniMax M2.7 | $0.2/1M | Rolamento de 5 horas | Opção mais barata | +| | Kimi K2.5 | $9/mo plano | 10 milhões de tokens/mês | Custo previsível | + | **🆓 GRÁTIS** | Kiro AI | $0 | 50 créditos/mês | Claude 4.5 + GLM-5 + MiniMax grátis (níveis pagos acima) | + | | OpenCode Free | $0 | Varia* | Sem autenticação, modelos de busca automática (a lista muda ao longo do tempo) | +| | Vertex AI | Créditos $300 | Novas contas do GCP | Gemini 3 Pro + DeepSeek + GLM-5 (use o endpoint Vertex AI Studio para obter créditos gratuitos) | + +**💡 Dica profissional:** combinação RTK + Kiro AI + OpenCode grátis = **custo $0 + economia de token de 20-40%**! + +--- + +### 📊 Compreendendo os custos e faturamento do 9Router + +**Como funciona o faturamento do 9Router:** + +✅ **Software 9Router = GRATUITO para sempre** (código aberto, nunca cobra) +✅ **Painel "custos" = Somente exibição/rastreamento** (não são cobranças reais) +✅ **Você paga diretamente aos provedores** (assinaturas ou taxas de API) +✅ **Provedores GRATUITOS permanecem GRATUITOS** (Kiro ~50 créditos/mês, OpenCode Free, créditos do Vertex $300 = $0 dentro dos limites do nível gratuito) — observe que os níveis gratuitos do iFlow/Qwen/Gemini CLI foram descontinuados em 2026 +❌ **9Router nunca envia faturas** ou cobra seu cartão + +**Como funciona a exibição de custos:** + +O painel mostra **custos estimados** como se você estivesse usando APIs pagas diretamente. Isto **não é cobrança** - é uma ferramenta de comparação para mostrar suas economias. + +**Cenário de exemplo:** + +``` +Exibição no painel: +• Total de requisições: 1,662 +• Total de tokens: 47M +• Custo exibido: $290 + +Custo real: +• Provedor: Kiro (nível gratuito: ~50 créditos/mês) +• Pagamento real: $0.00 +• O que $290 significa: valor ECONOMIZADO ao usar modelos gratuitos! +``` + +**Regras de pagamento:** + +- **Provedores de assinatura** (Claude Code, Codex): pague diretamente por meio de seus sites +- **Provedores baratos** (GLM, MiniMax): pague diretamente, 9Router apenas roteia +- **Provedores GRATUITOS** (iFlow, Kiro, Qwen): Genuinamente grátis para sempre, sem taxas ocultas +- **9Router**: Nunca cobra nada + +--- + +## 🎯 Casos de uso + +### Caso 1: "Tenho assinatura do Claude Pro" + +**Problema:** A cota expira sem ser utilizada, limites de taxa durante codificação pesada + +**Solução:** + +``` +Combo: "maximize-claude" + 1. cc/claude-opus-4-7 (use toda a assinatura) + 2. glm/glm-5.1 (backup econômico quando a cota acabar) + 3. kr/claude-sonnet-4.5 (fallback gratuito de emergência) + +Custo mensal: $20 (assinatura) + ~$5 (backup) = $25 total +versus $20 + atingir limites = frustração +``` + +### Caso 2: "Quero custo zero" + +**Problema:** Não posso pagar assinaturas, preciso de codificação de IA confiável + +**Solução:** + +``` +Combo: "free-forever" + 1. kr/claude-sonnet-4.5 (Claude 4.5 gratuito via Kiro, ~50 créditos/mês) + 2. kr/glm-5 (GLM-5 gratuito via Kiro) + 3. oc/ (OpenCode Free, sem autenticação) + +Custo mensal: $0 +Qualidade: modelos prontos para produção + economia de 20–40% com RTK +``` + +### Caso 3: "Preciso de codificação 24 horas por dia, 7 dias por semana, sem interrupções" + +**Problema:** Prazos, não podemos arcar com o tempo de inatividade + +**Solução:** + +``` +Combo: "always-on" + 1. cc/claude-opus-4-7 (melhor qualidade) + 2. cx/gpt-5.5 (segunda assinatura) + 3. glm/glm-5.1 (econômico, renova diariamente) + 4. minimax/MiniMax-M2.7 (mais econômico, renovação em 5h) + 5. kr/claude-sonnet-4.5 (gratuito via Kiro, ~50 créditos/mês) + +Resultado: 5 camadas de fallback = nenhuma interrupção +Custo mensal: $20-200 (subscriptions) + $10-20 (backup) +``` + +### Caso 4: "Quero IA GRATUITA no OpenClaw" + +**Problema:** Precisa de assistente de IA em aplicativos de mensagens (WhatsApp, Telegram, Slack...), totalmente gratuito + +**Solução:** + +``` +Combo: "openclaw-free" + 1. kr/claude-sonnet-4.5 (Claude 4.5 gratuito) + 2. kr/glm-5 (GLM-5 gratuito) + 3. kr/MiniMax-M2.5 (MiniMax gratuito) + +Custo mensal: $0 +Acesso por: WhatsApp, Telegram, Slack, Discord, iMessage, Signal... +``` + +--- + +## ❓ Perguntas frequentes + +
+📊 Por que meu painel mostra custos altos? + +O painel rastreia o uso do token e exibe **custos estimados** como se você estivesse usando APIs pagas diretamente. Este **não é faturamento real** - é uma referência para mostrar quanto você está economizando usando modelos gratuitos ou assinaturas existentes através do 9Router. + +**Exemplo:** + +- **O painel mostra:** "Custo total $290" +- **Realidade:** Você está usando modelos gratuitos do Kiro (~50 créditos/mês) +- **Seu custo real:** **$0.00** +- **O que significa $290:** Valor que você **economizou** usando modelos gratuitos em vez de APIs pagas! + +A exibição de custos é um "rastreador de economia" para ajudá-lo a entender seus padrões de uso e oportunidades de otimização. + +
+ +
+💳 Serei cobrado pelo 9Router? + +**Não.** 9Router é um software gratuito e de código aberto que roda em seu próprio computador. Nunca cobra nada. + +**Você só paga:** + +- ✅ **Provedores de assinatura** (Claude Code $20/mo, Codex $20-200/mo) → Pague-os diretamente em seus sites +- ✅ **Provedores baratos** (GLM, MiniMax) → Pague diretamente, 9Router apenas encaminha suas solicitações +- ❌ **9Router em si** → **Nunca carrega nada, nunca** + +9Router é um proxy/roteador local. Não possui cartão de crédito, não pode enviar faturas e não possui sistema de cobrança. É um software totalmente gratuito. + +
+ +
+🆓 Os provedores GRATUITOS são realmente ilimitados? + +**Principalmente!** Os provedores GRATUITOS atuais (Kiro, OpenCode Free, Vertex) são genuinamente gratuitos, mas os níveis gratuitos têm limites: + +Estes são serviços gratuitos oferecidos pelas respectivas empresas: + +- **Kiro AI**: ~50 créditos/mês grátis (mais 500 créditos de avaliação para novas contas nos primeiros 30 dias) via AWS Builder ID / Google / GitHub OAuth. Níveis pagos disponíveis acima disso. +- **OpenCode Free**: proxy de passagem sem autenticação, modelos buscados automaticamente em `opencode.ai/zen/v1/models`. A lista de modelos gratuitos varia com o tempo (alguns modelos são gratuitos apenas para promoções limitadas) — sujeita a alterações sem aviso prévio. +- **Vertex AI**: créditos gratuitos $300 para novas contas do Google Cloud (90 dias). Desde março de 2026, o endpoint da API Gemini não consome mais esses créditos. Em vez disso, use o endpoint **Vertex AI Studio**. + +O 9Router apenas encaminha suas solicitações para eles - não há pegadinhas ou cobrança futura do próprio 9Router. Eles são serviços verdadeiramente gratuitos e o 9Router os torna fáceis de usar com suporte alternativo. + +**Níveis gratuitos descontinuados (não mais recomendados):** + +- ❌ **iFlow**: era gratuito e ilimitado, agora alterado para pago (2026) +- ❌ **Qwen Code**: nível OAuth gratuito totalmente descontinuado pelo Alibaba em 15/04/2026 +- ❌ **Gemini CLI**: Serviço totalmente encerrado pelo Google em 18/06/2026 (substituído pelo Antigravity CLI de código fechado). Descontinuado – não use. + +
+ +
+💰 Como posso minimizar meus custos reais de IA? + +**Estratégia Grátis primeiro:** + +1. **Comece com um combo 100% grátis:** + + ``` + 1. kr/glm-5 (GLM-5 gratuito via Kiro, ~50 créditos/mês) + 2. Modelos do OpenCode Free (sem autenticação, obtidos automaticamente) + 3. Vertex AI Gemini 3 Pro (usando o endpoint do Vertex AI Studio com $300 em créditos) + ``` + + **Custo: $0/mês** (dentro do limite de crédito gratuito do Kiro; OpenCode/Vertex sujeito aos limites de nível gratuito) + +2. **Adicione backup barato** apenas se precisar: + + ``` + 4. glm/glm-4.7 ($0,60/1M tokens) + ``` + + **Custo adicional: pague apenas pelo que você realmente usa** + +3. **Use os provedores de assinatura por último:** + - Somente se você já os tiver +- 9Router ajuda a maximizar seu valor por meio do rastreamento de cotas + +**Resultado:** A maioria dos usuários pode operar em $0/mês usando apenas níveis gratuitos! + +
+ +
+📈 E se meu uso aumentar repentinamente? + +O fallback inteligente do 9Router evita cobranças surpresa: + +**Cenário:** você está em um sprint de codificação e ultrapassa suas cotas + +**Sem 9Router:** + +- ❌ Limite de requisições atingido → Paradas de trabalho → Frustração +- ❌ Ou: acidentalmente acumular enormes contas de API + +**Com 9Router:** + +- ✅ Assinatura atinge o limite → Retorno automático para nível barato +- ✅ O nível barato fica caro → Retorno automático para o nível gratuito +- ✅ Nunca pare de codificar → Custos previsíveis + +**Você está no controle:** Defina limites de gastos por provedor no painel e o 9Router os respeita. + +
+ +--- + +## 📖 Guia de configuração + +
+🔐 Provedores de assinatura (maximizar valor) + +### Claude Code (Pro/Max) + +```bash +Painel → Provedores → Conectar Claude Code +→ OAuth login → Renovação automática de token +→ Acompanhamento de cota de 5 horas + semanal + +Modelos: + cc/claude-opus-4-7 + cc/claude-opus-4-6 + cc/claude-sonnet-4-6 + cc/claude-haiku-4-5-20251001 +``` + +**Dica profissional:** Use o Opus para tarefas complexas e o Sonnet para velocidade. 9Router rastreia cota por modelo! + +### OpenAI Codex (Plus/Pro) + +```bash +Painel → Provedores → Conectar Codex +→ OAuth login (port 1455) +→ Renovação de 5 horas + semanal + +Modelos: + cx/gpt-5.5 + cx/gpt-5.4 + cx/gpt-5.3-codex + cx/gpt-5.2-codex +``` + +### GitHub Copilot + +```bash +Painel → Provedores → Conectar GitHub +→ OAuth via GitHub +→ Renovação mensal (dia 1º) + +Modelos: + gh/gpt-5.4 + gh/claude-opus-4.7 + gh/claude-sonnet-4.6 + gh/gemini-3.1-pro-preview + gh/grok-code-fast-1 +``` + +### Cursor IDE + +```bash +Painel → Provedores → Conectar Cursor +→ OAuth login +→ Assinatura mensal + +Modelos: + cu/claude-4.6-opus-max + cu/claude-4.5-sonnet-thinking + cu/gpt-5.3-codex +``` + +
+ +
+💰 Provedores baratos (backup) + +### GLM-5.1 / GLM-4.7 (redefinição diária, $0.6/1M) + +1. Inscreva-se: [Zhipu AI](https://open.bigmodel.cn/) +2. Obtenha a chave API do plano de codificação +3. Painel → Adicionar chave API: + - Provedor: `glm` + - Chave API: `your-key` + +**Usar:** `glm/glm-5.1`, `glm/glm-5`, `glm/glm-4.7` + +**Dica profissional:** O plano de codificação oferece cota 3× com custo de 1/7! Redefinir diariamente às 10h. + +### MiniMax M2.7 (redefinição de 5h, $0.20/1M) + +1. Cadastre-se: [MiniMax](https://www.minimax.io/) +2. Obtenha a chave API +3. Painel → Adicionar chave API + +**Usar:** `minimax/MiniMax-M2.7`, `minimax/MiniMax-M2.5` + +**Dica profissional:** Opção mais barata para contexto longo (1 milhão de tokens)! + +### Kimi K2.5 ($9/mês plano) + +1. Inscreva-se: [Moonshot AI](https://platform.moonshot.ai/) +2. Obtenha a chave API +3. Painel → Adicionar chave API + +**Usar:** `kimi/kimi-k2.5`, `kimi/kimi-k2.5-thinking` + +**Dica profissional:** $9/mês corrigido para 10 milhões de tokens = custo efetivo de $0.90/1M! + +
+ +
+🆓 Provedores GRATUITOS (recomendado) + +### Kiro AI (Claude 4.5 + GLM-5 + MiniMax GRATUITO) + +```bash +Painel → Conectar Kiro +→ AWS Builder ID, AWS IAM Identity Center, Google ou GitHub +→ Uso conforme a cota do plano + +Modelos: + kr/claude-sonnet-4.5 + kr/claude-haiku-4.5 + kr/glm-5 + kr/MiniMax-M2.5 + kr/qwen3-coder-next + kr/deepseek-3.2 +``` + +**Dica profissional:** Melhor opção gratuita para Claude. Sem chave API, sem pagamento, totalmente ilimitado. + +### OpenCode Free (sem autenticação, modelos de busca automática) + +```bash +Painel → Conectar OpenCode Free +→ Login não necessário (proxy de passagem) +→ Modelos obtidos automaticamente de opencode.ai/zen/v1/models +``` + +**Dica profissional:** Configuração mais rápida. Basta conectar e começar a codificar. + +### Vertex AI (créditos gratuitos $300 para novas contas do GCP) + +```bash +Painel → Conectar Vertex AI +→ Envie o JSON da conta de serviço do Google Cloud +→ Ative a API Vertex AI no projeto GCP + +Modelos: + vertex/gemini-3.1-pro-preview + vertex/gemini-3-flash-preview + vertex/gemini-2.5-flash + +Parceiros do Vertex (Anthropic / DeepSeek / GLM / Qwen via Vertex): + vertex-partner/glm-5-maas + vertex-partner/deepseek-v3.2-maas + vertex-partner/qwen3-next-80b-a3b-thinking-maas +``` + +**Dica profissional:** novas contas do Google Cloud recebem créditos $300 gratuitamente por 90 dias. Bastante para codificação diária. + +
+ +
+🎨 Criar Combos + +### Exemplo 1: Maximize a assinatura → Backup barato + +``` +Painel → Combos → Criar novo + +Nome: premium-coding +Modelos: + 1. cc/claude-opus-4-7 (assinatura principal) + 2. glm/glm-5.1 (backup econômico, $0,60/1M) + 3. minimax/MiniMax-M2.7 (fallback mais econômico, $0,20/1M) + +Use na CLI: premium-coding + +Exemplo de custo mensal (100M de tokens): + 80M via Claude (assinatura): $0 extra + 15M via GLM: $9 + 5M via MiniMax: $1 + Total: $10 + sua assinatura +``` + +### Exemplo 2: somente gratuito (custo zero) + +``` +Nome: free-combo +Modelos: + 1. kr/claude-sonnet-4.5 (Claude 4.5 gratuito via Kiro, ~50 créditos/mês) + 2. kr/glm-5 (GLM-5 gratuito via Kiro) + 3. vertex/gemini-3.1-pro-preview ($300 free credits) + +Custo: $0 (+ economia de 20–40% de tokens com RTK)! +``` + +
+ +
+🔧 Integração CLI + +### Cursor IDE + +``` +Configurações → Modelos → Avançado: + OpenAI API Base URL: http://localhost:20128/v1 + OpenAI API Key: [copie do painel do 9Router] + Model: cc/claude-opus-4-7 +``` + +Ou use o combo: `premium-coding` + +### Claude Code + +Editar `~/.claude/config.json`: + +```json +{ + "anthropic_api_base": "http://localhost:20128/v1", + "anthropic_api_key": "your-9router-api-key" +} +``` + +### CLI do Codex + +```bash +export OPENAI_BASE_URL="http://localhost:20128" +export OPENAI_API_KEY="your-9router-api-key" + +codex "seu prompt" +``` + +### OpenClaw + +**Opção 1 — Painel (recomendado):** + +``` +Painel → Ferramentas CLI → OpenClaw → Selecionar modelo → Aplicar +``` + +**Opção 2 — Manual:** Editar `~/.openclaw/openclaw.json`: + +```json +{ + "agents": { + "defaults": { + "model": { + "primary": "9router/kr/claude-sonnet-4.5" + } + } + }, + "models": { + "providers": { + "9router": { + "baseUrl": "http://127.0.0.1:20128/v1", + "apiKey": "sk_9router", + "api": "openai-completions", + "models": [ + { + "id": "kr/claude-sonnet-4.5", + "name": "Claude Sonnet 4.5 (Kiro Free)" + } + ] + } + } + } +} +``` + +> **Nota:** OpenClaw só funciona com 9Router local. Use `127.0.0.1` em vez de `localhost` para evitar problemas de resolução IPv6. + +### Cline / Continue / RooCode + +``` +Provedor: compatível com OpenAI +Base URL: http://localhost:20128/v1 +API Key: [copie do painel] +Model: cc/claude-opus-4-7 +``` + +
+ +
+🚀 Implantação + +### Implantação VPS + +```bash +# Clonar e instalar +git clone https://github.com/decolua/9router.git +cd 9router +npm install +npm run build + +# Configurar +export JWT_SECRET="your-secure-secret-change-this" +export INITIAL_PASSWORD="your-password" +export DATA_DIR="/var/lib/9router" +export PORT="20128" +export HOSTNAME="0.0.0.0" +export NODE_ENV="production" +export NEXT_PUBLIC_BASE_URL="http://localhost:20128" +export NEXT_PUBLIC_CLOUD_URL="https://9router.com" +export API_KEY_SECRET="endpoint-proxy-api-key-secret" +export MACHINE_ID_SALT="endpoint-proxy-salt" + +# Iniciar +npm run start + +# Ou usar PM2 +npm install -g pm2 +pm2 start npm --name 9router -- start +pm2 save +pm2 startup +``` + +### Docker + +Imagens publicadas (multiplataforma `linux/amd64` + `linux/arm64`): + +- Hub Docker: [`decolua/9router`](https://hub.docker.com/r/decolua/9router) +- GHCR: [`ghcr.io/decolua/9router`](https://github.com/decolua/9router/pkgs/container/9router) + +**Início rápido (use imagem publicada):** + +```bash +docker run -d \ + --name 9router \ + -p 20128:20128 \ + -v "$HOME/.9router:/app/data" \ + -e DATA_DIR=/app/data \ + decolua/9router:latest +``` + +→ Abra http://localhost:20128 + +**Compilar a partir do código-fonte (desenvolvedor):** + +```bash +git clone https://github.com/decolua/9router.git +cd 9router/app +docker build -t 9router . +docker run -d --name 9router -p 20128:20128 \ + -v "$HOME/.9router:/app/data" -e DATA_DIR=/app/data 9router +``` + +**Padrões do contêiner:** + +- `PORT=20128` +- `HOSTNAME=0.0.0.0` + +**Comandos úteis:** + +```bash +docker logs -f 9router +docker restart 9router +docker stop 9router && docker rm 9router +docker pull decolua/9router:latest # atualizar para a versão mais recente +``` + +**Persistência de dados:** `$HOME/.9router/db/data.sqlite` no host ↔ `/app/data/db/data.sqlite` no contêiner. + +### Variáveis de Ambiente + +| Variável | Padrão | Descrição | +| ---------------------------------------------------- | ---------------------------------------- | ----------------------------------------------------------------------------------- | +| `JWT_SECRET` | Gerado automaticamente (`~/.9router/jwt-secret`) | Segredo de assinatura JWT para cookie de autenticação do painel (substituir para compartilhar entre instâncias) | +| `INITIAL_PASSWORD` | `123456` | Primeira senha de login quando não existe hash salvo | +| `DATA_DIR` | `~/.9router` | Localização principal dos dados do aplicativo (SQLite em `$DATA_DIR/db/data.sqlite`) | +| `PORT` | padrão da estrutura | Porta de serviço (`20128` nos exemplos) | +| `HOSTNAME` | padrão da estrutura | Host de vinculação (o padrão do Docker é `0.0.0.0`) | +| `NODE_ENV` | padrão de tempo de execução | Definir `production` para implantação | +| `BASE_URL` | `http://localhost:20128` | URL base interna do lado do servidor usada por trabalhos de sincronização na nuvem | +| `CLOUD_URL` | `https://9router.com` | URL base do endpoint de sincronização na nuvem do lado do servidor | +| `NEXT_PUBLIC_BASE_URL` | `http://localhost:3000` | URL de base pública/compatível com versões anteriores (prefira `BASE_URL` para tempo de execução do servidor) | +| `NEXT_PUBLIC_CLOUD_URL` | `https://9router.com` | URL de nuvem pública/compatível com versões anteriores (prefira `CLOUD_URL` para tempo de execução do servidor) | +| `API_KEY_SECRET` | `endpoint-proxy-api-key-secret` | Segredo HMAC para chaves de API geradas | +| `MACHINE_ID_SALT` | `endpoint-proxy-salt` | Salt para hashing estável de ID de máquina | +| `ENABLE_REQUEST_LOGS` | `false` | Habilita logs de solicitação/resposta em `logs/` | +| `AUTH_COOKIE_SECURE` | `false` | Forçar cookie de autenticação `Secure` (definir `true` atrás do proxy reverso HTTPS) | +| `REQUIRE_API_KEY` | `false` | Aplicar chave de API Bearer em rotas `/v1/*` (recomendado para implantações expostas à Internet) | +| `HTTP_PROXY`, `HTTPS_PROXY`, `ALL_PROXY`, `NO_PROXY` | vazio | Proxy de saída opcional para chamadas de provedor upstream | +| `SEARXNG_URL` | `http://localhost:8888/search` | Endpoint para o provedor de pesquisa na web SearXNG não autenticado integrado | + +Notas: + +- Variáveis de proxy em letras minúsculas também são suportadas: `http_proxy`, `https_proxy`, `all_proxy`, `no_proxy`. +- `.env` não está incluído na imagem Docker (`.dockerignore`); injete configuração de tempo de execução com `--env-file` ou `-e`. +- No Windows, `APPDATA` pode ser usado para resolução de caminho de armazenamento local. +- `INSTANCE_NAME` aparece em modelos de documentos/env mais antigos, mas atualmente não é usado em tempo de execução. + +### Arquivos e armazenamento em tempo de execução + +- Estado principal do aplicativo: `${DATA_DIR}/db/data.sqlite` (SQLite — provedores, combos, aliases, chaves, configurações, histórico de uso) +- Backups automáticos: `${DATA_DIR}/db/backups/` +- Logs opcionais de solicitação/tradutor: `/logs/...` quando `ENABLE_REQUEST_LOGS=true` +- `${DATA_DIR}` e `~/.9router` resolvem para o mesmo local em um contêiner Docker — o link simbólico `/root/.9router -> /app/data` é criado no momento da construção. + +
+ +--- + +## 📊 Modelos Disponíveis + +
+Ver todos os modelos disponíveis + +**Claude Code (`cc/`)** - Pro/Máx: + +-`cc/claude-opus-4-7` +-`cc/claude-opus-4-6` +-`cc/claude-sonnet-4-6` +-`cc/claude-sonnet-4-5-20250929` +-`cc/claude-haiku-4-5-20251001` + +**Codex (`cx/`)** - Plus/Pro: + +-`cx/gpt-5.5` +-`cx/gpt-5.4` +-`cx/gpt-5.3-codex` +-`cx/gpt-5.2-codex` +-`cx/gpt-5.1-codex-max` + +**GitHub Copilot (`gh/`)**: + +-`gh/gpt-5.4` +-`gh/claude-opus-4.7` +-`gh/claude-sonnet-4.6` +-`gh/gemini-3.1-pro-preview` +-`gh/grok-code-fast-1` + +**Cursor (`cu/`)** - Assinatura: + +-`cu/claude-4.6-opus-max` +-`cu/claude-4.5-sonnet-thinking` +-`cu/gpt-5.3-codex` +-`cu/kimi-k2.5` + +**GLM (`glm/`)** - $0.6/1M: + +-`glm/glm-5.1` +-`glm/glm-5` +-`glm/glm-4.7` + +**MiniMax (`minimax/`)** - $0.2/1M: + +-`minimax/MiniMax-M2.7` +-`minimax/MiniMax-M2.5` + +**Kimi (`kimi/`)** - $9/mo plana: + +-`kimi/kimi-k2.5` +-`kimi/kimi-k2.5-thinking` + +**Kiro (`kr/`)** - Gratuito (~50 créditos/mês, níveis pagos acima): + +-`kr/claude-sonnet-4.5` +-`kr/claude-haiku-4.5` +-`kr/glm-5` +-`kr/MiniMax-M2.5` +-`kr/qwen3-coder-next` +-`kr/deepseek-3.2` + +**OpenCode grátis (`oc/`)** - GRATUITO sem autenticação: + +- Obtido automaticamente de `opencode.ai/zen/v1/models` + +**Vertex AI (`vertex/`)** - Créditos gratuitos $300: + +-`vertex/gemini-3.1-pro-preview` +-`vertex/gemini-3-flash-preview` +-`vertex/gemini-2.5-flash` +-`vertex-partner/glm-5-maas` +-`vertex-partner/deepseek-v3.2-maas` + +
+ +--- + +## 🐛 Solução de problemas + +**"O modelo de linguagem não forneceu mensagens"** + +- Cota do provedor esgotada → Verifique o rastreador de cota do painel +- Solução: use o combo substituto ou mude para um nível mais barato + +**Limitação de requisições** + +- Cota de assinatura esgotada → Fallback para GLM/MiniMax +- Adicionar combo: `cc/claude-opus-4-7 → glm/glm-5.1 → kr/claude-sonnet-4.5` + +**O token OAuth expirou** + +- Atualizado automaticamente pelo 9Router +- Se os problemas persistirem: Painel → Provedor → Reconectar + +**Custos elevados** + +- Habilite RTK no Dashboard → Configurações de endpoint (padrão LIGADO, economiza 20-40% de tokens) +- Verifique as estatísticas de uso no Dashboard +- Mude o modelo primário para GLM/MiniMax +- Use o nível gratuito (Kiro, OpenCode Free, Vertex) para tarefas não críticas + +**Painel abre na porta errada** + +- Definir `PORT=20128` e `NEXT_PUBLIC_BASE_URL=http://localhost:20128` + +**Primeiro login não funciona** + +- Verifique `INITIAL_PASSWORD` em `.env` +- Se não definida, a senha substituta é `123456` + +**Nenhum registro de solicitação em `logs/`** + +- Definir `ENABLE_REQUEST_LOGS=true` + +--- + +## 🛠️ Pilha de tecnologia + +- **Tempo de execução**: Node.js 20+ +- **Estrutura**: Next.js 16 +- **IU**: React 19 + Tailwind CSS 4 +- **Banco de dados**: SQLite (better-sqlite3 / node:sqlite / fallback para sql.js) +- **Streaming**: eventos enviados pelo servidor (SSE) +- **Autenticação**: OAuth 2.0 (PKCE) + JWT + chaves de API + +--- + +## 📝 Referência da API + +### Chat Completions + +```bash +POST http://localhost:20128/v1/chat/completions +Authorization: Bearer your-api-key +Content-Type: application/json + +{ + "model": "cc/claude-opus-4-6", + "messages": [ + {"role": "user", "content": "Escreva uma função para..."} + ], + "stream": true +} +``` + +### Listar modelos + +```bash +GET http://localhost:20128/v1/models +Authorization: Bearer your-api-key + +→ Retorna todos os modelos e combos no formato OpenAI +``` + +## 📧 Suporte + +- **Site**: [9router.com](https://9router.com) +- **GitHub**: [github.com/decolua/9router](https://github.com/decolua/9router) +- **Issues**: [github.com/decolua/9router/issues](https://github.com/decolua/9router/issues) + +--- + +## 👥 Colaboradores + +Obrigado a todos os colaboradores que ajudaram a tornar o 9Router melhor! + +[![Contribuidores](https://contrib.rocks/image?repo=decolua/9router&max=150&columns=15&anon=1&v=20260309)](https://github.com/decolua/9router/graphs/contributors) + +--- + +## 📊 Gráfico de estrelas + +[![Gráfico de estrelas](https://starchart.cc/decolua/9router.svg?variant=adaptive)](https://starchart.cc/decolua/9router) + +## 🔀 Forks + +**[OmniRoute](https://github.com/diegosouzapw/OmniRoute)** — Um fork TypeScript completo do 9Router. Adiciona mais de 36 provedores, fallback automático de 4 camadas, APIs multimodais (imagens, incorporações, áudio, TTS), disjuntor, cache semântico, avaliações LLM e um painel sofisticado. Mais de 368 testes de unidade. Disponível via npm e Docker. + +--- + +## 🙏 Agradecimentos + +Construído sobre ombros de gigantes: + +- **[CLIProxyAPI](https://github.com/router-for-me/CLIProxyAPI)** — implementação Go original que inspirou esta versão JavaScript. +- **[RTK](https://github.com/rtk-ai/rtk)** ![Stars](https://img.shields.io/github/stars/rtk-ai/rtk?style=flat&color=yellow) — Economizador de tokens em Rust. O 9Router porta seu pipeline de compactação para JS → **−20-40% de tokens de entrada** em cada solicitação. +- **[Caveman](https://github.com/JuliusBrussee/caveman)** ![Stars](https://img.shields.io/github/stars/JuliusBrussee/caveman?style=flat&color=yellow) por **[@JuliusBrussee](https://github.com/JuliusBrussee)** — viral _"por que usar muitos tokens quando poucos tokens resolvem"_. 9Router adapta seu prompt → **−65% tokens de saída**. +- **[Ponytail](https://github.com/DietrichGebert/ponytail)** ![Estrelas](https://img.shields.io/github/stars/DietrichGebert/ponytail?style=flat&color=yellow) por **[@DietrichGebert](https://github.com/DietrichGebert)** — _"desenvolvedor sênior preguiçoso"_ habilidade. 9Router injeta sua abordagem YAGNI-first → **menos tokens, menos código, diferenças mais curtas**. + +Um grande agradecimento a esses autores – sem o trabalho deles, os recursos de economia de tokens do 9Router não existiriam. Dê uma ⭐ a eles no GitHub! + +--- + +## 📄 Licença + +Licença MIT - consulte [LICENSE](../LICENSE) para obter detalhes. + +--- + +
+ Construído com ❤️ para desenvolvedores que codificam 24/7 +
+ diff --git a/open-sse/config/providerModels.js b/open-sse/config/providerModels.js index 860f7097..300fef5a 100644 --- a/open-sse/config/providerModels.js +++ b/open-sse/config/providerModels.js @@ -2,7 +2,7 @@ import { PROVIDERS } from "./providers.js"; import REGISTRY from "../providers/registry/index.js"; // PROVIDER_MODELS now built from providers/registry (transport + models co-located) import { PROVIDER_MODELS } from "../providers/index.js"; -import { modelQuotaFamily, modelStrip, modelTargetFormat, normalizeModelId } from "../providers/models/schema.js"; +import { modelQuotaFamily, modelStrip, modelTargetFormat, modelSupportedFormats, normalizeModelId } from "../providers/models/schema.js"; import { CODEX_REVIEW_SUFFIX } from "../providers/models/helpers.js"; export { PROVIDER_MODELS }; @@ -54,6 +54,14 @@ export function getModelTargetFormat(aliasOrId, modelId) { return modelTargetFormat(findModel(models, modelId, aliasOrId)); } +// Declared upstream formats for a model (registry `supportedFormats`). Drives the +// per-model guard on the sourceFormat-matched transport; null when undeclared. +export function getModelSupportedFormats(aliasOrId, modelId) { + const models = PROVIDER_MODELS[aliasOrId]; + if (!models) return null; + return modelSupportedFormats(findModel(models, modelId, aliasOrId)); +} + export function getModelType(aliasOrId, modelId) { const models = PROVIDER_MODELS[aliasOrId]; if (!models) return null; diff --git a/open-sse/executors/antigravity.js b/open-sse/executors/antigravity.js index 7f24fd85..07bbb4fc 100644 --- a/open-sse/executors/antigravity.js +++ b/open-sse/executors/antigravity.js @@ -245,6 +245,18 @@ export class AntigravityExecutor extends BaseExecutor { // Strip tools/toolConfig (handled separately) and blacklisted fields that Google rejects const { tools: _originalTools, toolConfig: _originalToolConfig, ...requestWithoutTools } = body.request || {}; stripBlacklisted(requestWithoutTools); + + // Rewrite competitive system prompts (e.g. Zed IDE's Claude prompt) to prevent Antigravity from + // flagging the request and immediately blocking it with a 429 Quota Exhausted response. + if (requestWithoutTools.systemInstruction?.parts) { + const oldText = "You are a Claude agent, built on Anthropic's Claude Agent SDK."; + for (const part of requestWithoutTools.systemInstruction.parts) { + if (typeof part.text === "string" && part.text.includes(oldText)) { + part.text = part.text.split(oldText).join(""); + } + } + } + const generationConfig = { ...(requestWithoutTools.generationConfig || {}) }; if (generationConfig.maxOutputTokens > MAX_ANTIGRAVITY_OUTPUT_TOKENS) { generationConfig.maxOutputTokens = MAX_ANTIGRAVITY_OUTPUT_TOKENS; diff --git a/open-sse/executors/index.js b/open-sse/executors/index.js index 4ebd145e..a2d0fd68 100644 --- a/open-sse/executors/index.js +++ b/open-sse/executors/index.js @@ -10,7 +10,6 @@ import { CodexExecutor } from "./codex.js"; import { CursorExecutor } from "./cursor.js"; import { VertexExecutor } from "./vertex.js"; import { OpenCodeExecutor } from "./opencode.js"; -import { OpenCodeGoExecutor } from "./opencode-go.js"; import { GrokWebExecutor } from "./grok-web.js"; import { GrokCliExecutor } from "./grok-cli.js"; import { PerplexityWebExecutor } from "./perplexity-web.js"; @@ -42,7 +41,6 @@ const executors = { vertex: new VertexExecutor("vertex"), "vertex-partner": new VertexExecutor("vertex-partner"), opencode: new OpenCodeExecutor(), - "opencode-go": new OpenCodeGoExecutor(), "grok-web": new GrokWebExecutor(), "grok-cli": new GrokCliExecutor(), gcli: new GrokCliExecutor(), // Alias @@ -88,7 +86,6 @@ export { CursorExecutor } from "./cursor.js"; export { VertexExecutor } from "./vertex.js"; export { DefaultExecutor } from "./default.js"; export { OpenCodeExecutor } from "./opencode.js"; -export { OpenCodeGoExecutor } from "./opencode-go.js"; export { GrokWebExecutor } from "./grok-web.js"; export { GrokCliExecutor } from "./grok-cli.js"; export { PerplexityWebExecutor } from "./perplexity-web.js"; diff --git a/open-sse/executors/kiro.js b/open-sse/executors/kiro.js index 1205522b..77616618 100644 --- a/open-sse/executors/kiro.js +++ b/open-sse/executors/kiro.js @@ -144,6 +144,12 @@ function normalizeStopReason(value) { return reason || null; } +// Of the reasons stopDisposition() folds into "terminal_incomplete", only these +// mean "usable as far as it got, then the budget ran out" -- the case +// finish_reason "length" exists for. cancelled / pause_turn are abandoned turns +// whose partial content must stay private, so they are deliberately absent. +const KIRO_TRUNCATION_STOP_REASONS = new Set(["model_context_window_exceeded", "max_tokens"]); + function stopDisposition(stopReason, hasToolCalls) { if (["malformed_model_output", "invalid_model_output"].includes(stopReason)) return "retryable_protocol_failure"; if (["cancelled", "pause_turn", "model_context_window_exceeded"].includes(stopReason)) return "terminal_incomplete"; @@ -711,14 +717,25 @@ export class KiroExecutor extends BaseExecutor { }; const emitTools = (controller) => { for (const tool of state.tools.values()) { - const input = parsedToolInput(tool); - if (tool.name === "tool_call") { - if (typeof input.name !== "string" || !input.name.trim()) { - throw new Error("Invalid Kiro tool_call payload: missing nested MCP tool name"); - } - if (!Object.prototype.hasOwnProperty.call(input, "arguments")) { - throw new Error("Invalid Kiro tool_call payload: missing nested MCP tool arguments"); + // Validate per tool, not per turn: one unusable fragment used to throw out + // of emitTools and take every other complete tool call in the same turn + // with it, which the client saw as a turn that answered nothing. + let input; + try { + input = parsedToolInput(tool); + if (tool.name === "tool_call") { + if (typeof input.name !== "string" || !input.name.trim()) { + throw new Error("Invalid Kiro tool_call payload: missing nested MCP tool name"); + } + if (!Object.prototype.hasOwnProperty.call(input, "arguments")) { + throw new Error("Invalid Kiro tool_call payload: missing nested MCP tool arguments"); + } } + } catch (error) { + state.droppedTools = (state.droppedTools || 0) + 1; + state.toolValidationError ||= error.message; + console.error(`[Kiro] dropping unusable tool call ${tool.id} (${tool.name}): ${error.message}`); + continue; } const index = state.toolCounter++; emitDelta(controller, { @@ -729,14 +746,26 @@ export class KiroExecutor extends BaseExecutor { function: { name: tool.name, arguments: "" } }] }); + const serializedInput = JSON.stringify(input); emitDelta(controller, { - tool_calls: [{ index, function: { arguments: JSON.stringify(input) } }] + tool_calls: [{ index, function: { arguments: serializedInput } }] }); + // Tool arguments are billed output like any other completion bytes. They + // were never added to totalContentLength, so the /4 estimator in finish() + // reported OUT 0 -- or the Math.max floor of 1 -- for every turn whose + // entire answer was a tool call. + state.totalContentLength += tool.name.length + serializedInput.length; state.hasToolCalls = true; } state.tools.clear(); state.bufferedToolBytes = 0; - if (state.stopReason === "tool_use" && !state.hasToolCalls) { + // A declared tool turn that emitted no usable call is only fatal when the + // turn produced nothing else. Throwing unconditionally here escaped + // emitTools() with provenance "invalid_tool_call", which the integrity gate + // re-derived into a repair retry -- discarding text the client had already + // been promised. + if (state.stopReason === "tool_use" && !state.hasToolCalls && + !state.hasText && !state.hasReasoning && !state.hasCode) { throw new Error("Kiro tool_use stop reason did not include a complete tool call"); } }; @@ -796,7 +825,6 @@ export class KiroExecutor extends BaseExecutor { emitDelta(controller, { content: event.payload.content }); } else if (eventType === "toolUseEvent") { state.sawToolUse = true; - if (state.toolValidationError) return true; const values = Array.isArray(event.payload) ? event.payload : [event.payload]; if (!values[0]) throw new Error("Kiro toolUseEvent is empty"); for (const value of values) { @@ -924,9 +952,10 @@ export class KiroExecutor extends BaseExecutor { } catch (error) { const bufferExceeded = error.code === "KIRO_BUFFER_EXCEEDED"; if (!bufferExceeded) { + // Keep whatever is already buffered: the rejected fragment belongs to + // one tool, and clearing the map dropped the complete calls too. state.toolValidationError ||= error.message; - state.tools.clear(); - state.bufferedToolBytes = 0; + console.error(`[Kiro] tool fragment rejected, keeping ${state.tools.size} buffered tool(s): ${error.message}`); continue; } fail( @@ -958,7 +987,16 @@ export class KiroExecutor extends BaseExecutor { } state.transportState = "clean_eof"; const declaredDisposition = stopDisposition(state.stopReason, state.sawToolUse); - if (["retryable_protocol_failure", "terminal_incomplete", "terminal_refusal", "unknown_failure"].includes(declaredDisposition)) { + // model_context_window_exceeded / max_tokens map to terminal_incomplete. When + // they arrive after the model already streamed content, fail() threw away a + // complete-enough answer; a truncated turn is what finish_reason "length" is + // for. chunkIndex > 0 means at least one delta already reached the client. + const declaredTruncatedAfterOutput = declaredDisposition === "terminal_incomplete" && + KIRO_TRUNCATION_STOP_REASONS.has(state.stopReason) && state.chunkIndex > 0; + if (declaredTruncatedAfterOutput) { + console.error(`[Kiro] truncated after ${state.chunkIndex} chunk(s) (stop_reason=${state.stopReason}); keeping output`); + } + if (!declaredTruncatedAfterOutput && ["retryable_protocol_failure", "terminal_incomplete", "terminal_refusal", "unknown_failure"].includes(declaredDisposition)) { const code = declaredDisposition === "retryable_protocol_failure" ? "kiro_retryable_protocol_failure" : declaredDisposition === "terminal_refusal" @@ -975,16 +1013,6 @@ export class KiroExecutor extends BaseExecutor { ); return; } - if (state.toolValidationError) { - fail( - controller, - "invalid_tool_call", - "invalid_kiro_tool_call", - state.toolValidationError, - { transport_state: state.transportState, stop_disposition: "retryable_protocol_failure" } - ); - return; - } try { emitTools(controller); } catch (error) { @@ -997,6 +1025,22 @@ export class KiroExecutor extends BaseExecutor { ); return; } + // Fail only when the turn has nothing usable left. emitTools() validates + // per tool and drops just the unusable ones, so this has to run AFTER it: + // before, the rejected tool was still buffered and tools.size was never 0. + // A turn that also produced text keeps that text -- the dropped call is + // logged, not fatal. + if (state.toolValidationError && !state.hasToolCalls && + !state.hasText && !state.hasReasoning && !state.hasCode) { + fail( + controller, + "invalid_tool_call", + "invalid_kiro_tool_call", + state.toolValidationError, + { transport_state: state.transportState, stop_disposition: "retryable_protocol_failure" } + ); + return; + } const hasOutput = state.hasText || state.hasReasoning || state.hasCode || state.hasToolCalls; if (!hasOutput && !state.explicitStop) { @@ -1011,7 +1055,13 @@ export class KiroExecutor extends BaseExecutor { } const disposition = stopDisposition(state.stopReason, state.hasToolCalls); - if (["retryable_protocol_failure", "terminal_incomplete", "terminal_refusal", "unknown_failure"].includes(disposition)) { + // Same reasoning as declaredTruncatedAfterOutput above. + const truncatedAfterOutput = disposition === "terminal_incomplete" && + KIRO_TRUNCATION_STOP_REASONS.has(state.stopReason) && state.chunkIndex > 0; + if (truncatedAfterOutput) { + console.error(`[Kiro] truncated after ${state.chunkIndex} chunk(s) (stop_reason=${state.stopReason}); closing as length`); + } + if (!truncatedAfterOutput && ["retryable_protocol_failure", "terminal_incomplete", "terminal_refusal", "unknown_failure"].includes(disposition)) { const code = disposition === "retryable_protocol_failure" ? "kiro_retryable_protocol_failure" : disposition === "terminal_refusal" @@ -1041,18 +1091,24 @@ export class KiroExecutor extends BaseExecutor { total_tokens: prompt + completion }; } - const finishReason = state.hasToolCalls - ? "tool_calls" - : disposition === "length" - ? "length" - : "stop"; + const finishReason = truncatedAfterOutput + ? "length" + : state.hasToolCalls + ? "tool_calls" + : disposition === "length" + ? "length" + : "stop"; controller.enqueue(sseChunk({}, finishReason, state.usage)); controller.enqueue(encoder.encode(SSE_DONE)); state.finished = true; options.onTerminalState?.(diagnostics({ terminal_provenance: state.terminalProvenance || "clean_eventstream_eof", transport_state: state.transportState, - stop_disposition: disposition + // Report what this exit actually did, not the raw disposition. The + // integrity gate re-derives its verdict from stop_disposition, so + // reporting "terminal_incomplete" for a turn we deliberately kept made + // it discard the very bytes we just released to the client. + stop_disposition: truncatedAfterOutput ? "length" : disposition })); }; diff --git a/open-sse/executors/opencode-go.js b/open-sse/executors/opencode-go.js deleted file mode 100644 index 7bf47edb..00000000 --- a/open-sse/executors/opencode-go.js +++ /dev/null @@ -1,49 +0,0 @@ -import { BaseExecutor } from "./base.js"; -import { PROVIDERS } from "../config/providers.js"; -import { injectReasoningContent } from "../utils/reasoningContentInjector.js"; -import { ANTHROPIC_API_VERSION } from "../providers/shared.js"; - -// Models that use /zen/go/v1/messages (Anthropic/Claude format + x-api-key auth) -const MESSAGES_FORMAT_MODELS = new Set([ - "minimax-m3", - "minimax-m2.7", - "minimax-m2.5", - "qwen3.7-max", - "qwen3.7-plus", - "qwen3.6-plus", -]); - -const BASE = "https://opencode.ai/zen/go/v1"; - -export class OpenCodeGoExecutor extends BaseExecutor { - constructor() { - super("opencode-go", PROVIDERS["opencode-go"]); - } - - // buildUrl runs before buildHeaders in BaseExecutor.execute, cache model here - buildUrl(model) { - this._lastModel = model; - return MESSAGES_FORMAT_MODELS.has(model) - ? `${BASE}/messages` - : `${BASE}/chat/completions`; - } - - buildHeaders(credentials, stream = true) { - const key = credentials?.apiKey || credentials?.accessToken; - const headers = { "Content-Type": "application/json" }; - - if (MESSAGES_FORMAT_MODELS.has(this._lastModel)) { - headers["x-api-key"] = key; - headers["anthropic-version"] = ANTHROPIC_API_VERSION; - } else { - headers["Authorization"] = `Bearer ${key}`; - } - - if (stream) headers["Accept"] = "text/event-stream"; - return headers; - } - - transformRequest(model, body) { - return injectReasoningContent({ provider: this.provider, model, body }); - } -} diff --git a/open-sse/executors/opencode.js b/open-sse/executors/opencode.js index f1af1ebd..1bdeb318 100644 --- a/open-sse/executors/opencode.js +++ b/open-sse/executors/opencode.js @@ -1,10 +1,35 @@ +import crypto from "crypto"; import { BaseExecutor } from "./base.js"; import { PROVIDERS } from "../config/providers.js"; import { injectReasoningContent } from "../utils/reasoningContentInjector.js"; +import { resolveSessionId } from "../utils/sessionManager.js"; -// Models that use /zen/v1/messages (claude format) +const OPENCODE_UA = "opencode"; const MESSAGES_MODELS = new Set(); +function generateRequestId() { + return `msg_${crypto.randomUUID().replace(/-/g, "")}`; +} + +function generateSessionId() { + return `ses_${crypto.randomUUID().replace(/-/g, "")}`; +} + +// Normalize any resolved id into opencode's ses_ format (stable per-conversation) +function toOpencodeSession(id) { + const stripped = String(id || "").replace(/^ses_/, "").replace(/-/g, ""); + return stripped ? `ses_${stripped}` : null; +} + +function resolveOpencodeSession(body, credentials) { + return toOpencodeSession(resolveSessionId({ + headers: credentials?.rawHeaders, + body, + connectionId: credentials?.connectionId, + scope: "opencode", + })); +} + // OpenCode free tier is limited per egress IP — a 429/403 with a limit-ish // body means the POOL's IP is exhausted, not the account. Declare it // pool-scoped so chatCore marks the pool unfit, retries via another pool, and @@ -14,9 +39,11 @@ const IP_LIMIT_BODY = /limit|rate|quota|exhausted|capacity|too many|retry/i; export class OpenCodeExecutor extends BaseExecutor { constructor() { super("opencode", PROVIDERS.opencode); + this._currentSessionId = null; } - transformRequest(model, body) { + transformRequest(model, body, stream, credentials) { + this._currentSessionId = resolveOpencodeSession(body, credentials); return injectReasoningContent({ provider: this.provider, model, body }); } @@ -27,12 +54,23 @@ export class OpenCodeExecutor extends BaseExecutor { : `${base}/zen/v1/chat/completions`; } - buildHeaders() { + buildHeaders(credentials, stream = true) { + const raw = credentials?.rawHeaders || {}; + const lower = {}; + for (const [k, v] of Object.entries(raw)) lower[k.toLowerCase()] = v; + + const downstreamUa = lower["user-agent"] || ""; + const isOpencodeDownstream = downstreamUa.toLowerCase().includes("opencode"); + return { "Content-Type": "application/json", "Authorization": "Bearer public", - "x-opencode-client": "desktop", - "Accept": "text/event-stream" + "User-Agent": isOpencodeDownstream ? downstreamUa : OPENCODE_UA, + "x-opencode-client": lower["x-opencode-client"] || "desktop", + "x-opencode-session": lower["x-opencode-session"] || this._currentSessionId || generateSessionId(), + "x-opencode-request": lower["x-opencode-request"] || generateRequestId(), + "x-opencode-project": lower["x-opencode-project"] || "global", + "Accept": stream ? "text/event-stream" : "*/*", }; } diff --git a/open-sse/executors/qoder.js b/open-sse/executors/qoder.js index f8691ef5..e52a00df 100644 --- a/open-sse/executors/qoder.js +++ b/open-sse/executors/qoder.js @@ -215,6 +215,52 @@ async function buildQoderRequestBody({ model, body, credentials, log, proxyOptio }; } +/** + * Check if a qoder error message indicates a billing/quota block. + * Signatures: code 112 (quota exhausted), code 10605 (queue throttle), pricingUrl field. + */ +function isBillingBlock(inner) { + if (!inner || typeof inner !== "string") return false; + const lowerMsg = inner.toLowerCase(); + // Match: {"code":"112",...}, {"code":"10605",...}, or pricingUrl field + return /\"code\"\s*:\s*\"(112|10605)\"/.test(inner) || lowerMsg.includes("pricingurl"); +} + +/** + * Peek the first SSE frame to detect billing errors before piping. + * Returns { isBilling, statusVal, message, consumed } — `consumed` is every + * byte read so far (including the peeked line) so the caller can re-process + * it and nothing is dropped from the stream. + */ +async function peekFirstQoderFrame(reader, decoder) { + let consumed = ""; + while (true) { + const { done, value } = await reader.read(); + if (done) return { isBilling: false, consumed, upstreamDone: true }; + + consumed += decoder.decode(value, { stream: true }); + const nl = consumed.indexOf("\n"); + if (nl === -1) continue; // need a full line first + + const line = consumed.slice(0, nl).replace(/\r$/, "").trim(); + if (!line.startsWith("data:")) continue; + + const data = line.slice(5).trimStart(); + if (data === "[DONE]") return { isBilling: false, consumed }; + + let envelope; + try { envelope = JSON.parse(data); } catch { return { isBilling: false, consumed }; } + + const statusVal = typeof envelope.statusCodeValue === "number" ? envelope.statusCodeValue : 200; + const inner = typeof envelope.body === "string" ? envelope.body : ""; + + if (statusVal !== 200 && isBillingBlock(inner)) { + return { isBilling: true, statusVal, message: inner || `qoder billing block (${statusVal})` }; + } + return { isBilling: false, consumed }; + } +} + /** * Wrap the upstream's `{statusCodeValue, body}` SSE envelope into plain * OpenAI SSE chunks the rest of the chatCore pipeline understands. @@ -229,16 +275,34 @@ async function buildQoderRequestBody({ model, body, credentials, log, proxyOptio * [DONE]/error frame (agent keepalive). Non-streaming clients drain via * response.text() which hangs until the socket closes — so on terminal * events we cancel the upstream reader and close our stream immediately. + * + * NEW: Peek first frame to detect billing blocks (code 112/10605/pricingUrl). + * If detected, return 403 response so chatCore marks connection unavailable + * and triggers combo fallback instead of leaking error text into chat. */ -function wrapQoderSSE(response, model) { +async function wrapQoderSSE(response, model) { if (!response.ok || !response.body) return response; const decoder = new TextDecoder(); - const encoder = new TextEncoder(); - let buffer = ""; - let doneEmitted = false; const reader = response.body.getReader(); + // Peek first frame to detect billing block + const peek = await peekFirstQoderFrame(reader, decoder); + if (peek?.isBilling) { + // Billing block detected — return 403 so chatCore fails this connection + await reader.cancel().catch(() => {}); + return new Response( + JSON.stringify({ error: { message: peek.message, code: peek.statusVal } }), + { status: 403, headers: { "Content-Type": "application/json" } } + ); + } + + // Normal flow: re-process every byte the peek consumed, then continue. + let buffer = peek.consumed || ""; + const upstreamDrained = peek.upstreamDone === true; + const encoder = new TextEncoder(); + let doneEmitted = false; + // Process one already-extracted SSE line (no trailing newline). const processLine = (line, controller) => { const trimmed = line.replace(/\r$/, "").trim(); @@ -287,7 +351,28 @@ function wrapQoderSSE(response, model) { // enqueueing would never be re-invoked, hanging consumers like .text(). async start(controller) { try { - while (!doneEmitted) { + // Drain whatever the peek already pulled off the socket first. + let nlSeed; + while ((nlSeed = buffer.indexOf("\n")) !== -1) { + const line = buffer.slice(0, nlSeed); + buffer = buffer.slice(nlSeed + 1); + processLine(line, controller); + if (doneEmitted) { + await reader.cancel().catch(() => {}); + controller.close(); + return; + } + } + if (upstreamDrained) { + // Peek hit end-of-stream: flush any trailing partial line. + buffer += decoder.decode(); + if (buffer.length > 0) { + processLine(buffer, controller); + buffer = ""; + } + } + + while (!doneEmitted && !upstreamDrained) { const { done, value } = await reader.read(); if (done) { buffer += decoder.decode(); @@ -472,7 +557,7 @@ export class QoderExecutor extends BaseExecutor { return { response, url, headers, transformedBody: payload }; } - const wrapped = wrapQoderSSE(response, `qoder/${qoderKey}`); + const wrapped = await wrapQoderSSE(response, `qoder/${qoderKey}`); return { response: wrapped, url, headers, transformedBody: payload }; } @@ -496,4 +581,5 @@ export const __test__ = { normalizeMessages, wrapQoderSSE, buildQoderRequestBody, + isBillingBlock, }; diff --git a/open-sse/handlers/chatCore.js b/open-sse/handlers/chatCore.js index 8a097a1f..c41d2d15 100644 --- a/open-sse/handlers/chatCore.js +++ b/open-sse/handlers/chatCore.js @@ -2,11 +2,11 @@ import { detectFormat, getTargetFormat, resolveTransport } from "../services/pro import { translateRequest } from "../translator/index.js"; import { applyThinking, extractThinking, stripThinkingSuffix } from "../translator/concerns/thinkingUnified.js"; import { FORMATS } from "../translator/formats.js"; -import { normalizeClaudePassthrough } from "../translator/formats/claude.js"; +import { normalizeClaudePassthrough, anchorClaudeCache } from "../translator/formats/claude.js"; import { createStreamController } from "../utils/streamHandler.js"; import { refreshWithRetry } from "../services/tokenRefresh.js"; import { createRequestLogger } from "../utils/requestLogger.js"; -import { getModelTargetFormat, getModelStrip, getModelUpstreamId, getModelType, PROVIDER_ID_TO_ALIAS } from "../config/providerModels.js"; +import { getModelTargetFormat, getModelSupportedFormats, getModelStrip, getModelUpstreamId, getModelType, PROVIDER_ID_TO_ALIAS } from "../config/providerModels.js"; import { PROVIDERS } from "../config/providers.js"; import { createErrorResult, parseUpstreamError, formatProviderError } from "../utils/error.js"; import { HTTP_STATUS, TOKEN_SAVER_HEADER } from "../config/runtimeConfig.js"; @@ -84,10 +84,20 @@ export async function handleChatCore({ body, modelInfo, credentials, log, onCred const alias = PROVIDER_ID_TO_ALIAS[provider] || provider; const modelTargetFormat = getModelTargetFormat(alias, model); - // Multi-endpoint providers: pick transport matching sourceFormat → zero translation + // Multi-endpoint providers: pick transport matching sourceFormat → zero translation. + // Per-model guard: only use the transport when the model declares support for that + // sourceFormat — opencode-go models differ in endpoint support (kimi/glm only do + // /chat/completions), so without this guard a claude-format request would wrongly + // route kimi to /messages. + const modelSupportedFormats = getModelSupportedFormats(alias, model); const runtimeTransport = resolveTransport(provider, sourceFormat); - const targetFormat = modelTargetFormat || runtimeTransport?.format || getTargetFormat(provider, credentials); - if (runtimeTransport && credentials) credentials.runtimeTransport = runtimeTransport; + // Per-model guard: when a model declares supportedFormats, only use the + // sourceFormat-matched transport if that format is declared (opencode-go models + // differ — kimi/glm only do /chat/completions). Undeclared models keep the + // upstream default (use the transport), preserving behavior for glm/deepseek/... + const useTransport = (!modelSupportedFormats || modelSupportedFormats.includes(sourceFormat)) ? runtimeTransport : null; + const targetFormat = modelTargetFormat || useTransport?.format || getTargetFormat(provider, credentials); + if (useTransport && credentials) credentials.runtimeTransport = useTransport; const stripList = getModelStrip(alias, model); const upstreamModel = getModelUpstreamId(alias, model); @@ -281,6 +291,10 @@ export async function handleChatCore({ body, modelInfo, credentials, log, onCred if (xf.length && log?.line) log.line(reqTag, "⚙", xf.join(" · ")); + // Pin cache breakpoints to the final body — every saver above can reshape + // system/tools/messages, and a stale anchor costs a full prefix rewrite. + if (passthrough && clientTool === "claude") anchorClaudeCache(translatedBody); + const executor = getExecutor(provider); trackPendingRequest(model, provider, connectionId, true); appendRequestLog({ model, provider, connectionId, status: "PENDING" }).catch(() => { }); diff --git a/open-sse/handlers/chatCore/requestDetail.js b/open-sse/handlers/chatCore/requestDetail.js index b3d24115..376d5f90 100644 --- a/open-sse/handlers/chatCore/requestDetail.js +++ b/open-sse/handlers/chatCore/requestDetail.js @@ -44,13 +44,14 @@ export function extractUsageFromResponse(responseBody) { }; } - // Gemini format - if (responseBody.usageMetadata) { + // Gemini format. Antigravity / gemini-cli wrap the payload in { response: {...} }. + const usageMetadata = responseBody.usageMetadata || responseBody.response?.usageMetadata; + if (usageMetadata) { return { - prompt_tokens: responseBody.usageMetadata.promptTokenCount || 0, - completion_tokens: responseBody.usageMetadata.candidatesTokenCount || 0, - cached_tokens: responseBody.usageMetadata.cachedContentTokenCount || 0, - reasoning_tokens: responseBody.usageMetadata.thoughtsTokenCount || 0 + prompt_tokens: usageMetadata.promptTokenCount || 0, + completion_tokens: usageMetadata.candidatesTokenCount || 0, + cached_tokens: usageMetadata.cachedContentTokenCount || 0, + reasoning_tokens: usageMetadata.thoughtsTokenCount || 0 }; } diff --git a/open-sse/handlers/search/callers.js b/open-sse/handlers/search/callers.js index e32d93ed..3c02828e 100644 --- a/open-sse/handlers/search/callers.js +++ b/open-sse/handlers/search/callers.js @@ -29,6 +29,8 @@ * @property {Record} [providerSpecificData] */ +import { assertPublicUrl } from "../../../src/shared/utils/ssrfGuard.js"; + // ── Helpers ───────────────────────────────────────────────────────────── /** @@ -63,12 +65,31 @@ export function getProviderSetting(params, key) { /** * Resolve base URL with optional override from providerOptions.baseUrl. + * + * The override is client-controlled and therefore SSRF-hardened: only public + * http(s) URLs are accepted (internal/private/loopback/metadata addresses are + * rejected via assertPublicUrl). The provider's own configured baseUrl is + * trusted as-is (admin-controlled). + * * @param {SearchProviderConfig} config * @param {SearchRequestParams} params * @returns {string} */ export function resolveBaseUrl(config, params) { const override = getProviderSetting(params, "baseUrl"); + if (override) { + // SSRF guard: client-supplied base URLs must be public http(s) only. + let parsed; + try { + parsed = new URL(override); + } catch { + throw new Error(`Invalid baseUrl: ${override}`); + } + if (parsed.protocol !== "http:" && parsed.protocol !== "https:") { + throw new Error(`Invalid baseUrl protocol: ${parsed.protocol}`); + } + assertPublicUrl(override); + } return (override || config.baseUrl).replace(/\/+$/, ""); } diff --git a/open-sse/handlers/ttsProviders/genericFormats.js b/open-sse/handlers/ttsProviders/genericFormats.js index 2f27f9ea..a5c0cda3 100644 --- a/open-sse/handlers/ttsProviders/genericFormats.js +++ b/open-sse/handlers/ttsProviders/genericFormats.js @@ -51,6 +51,25 @@ async function huggingface({ baseUrl, apiKey, text, modelId }) { return responseToBase64(res, "wav"); } +// Fish Audio: model travels in an HTTP header, the voice is a reference_id, returns binary +async function fishAudio({ baseUrl, apiKey, text, modelId, voiceId }) { + const res = await fetch(baseUrl, { + method: "POST", + headers: { + "Content-Type": "application/json", + "Authorization": `Bearer ${apiKey}`, + "model": modelId || "s2.1-pro-free", + }, + body: JSON.stringify({ + text, + format: "mp3", + ...(voiceId ? { reference_id: voiceId } : {}), + }), + }); + if (!res.ok) await throwUpstreamError(res); + return responseToBase64(res, "mp3"); +} + // Inworld: Basic auth, JSON { audioContent } async function inworld({ baseUrl, apiKey, text, modelId, voiceId }) { const res = await fetch(baseUrl, { @@ -166,4 +185,5 @@ export const FORMAT_HANDLERS = { tortoise, openai: openaiCompat, "minimax-tts": minimaxTts, + "fish-audio": fishAudio, }; diff --git a/open-sse/providers/capabilities.js b/open-sse/providers/capabilities.js index e91ad1f2..24a03b04 100644 --- a/open-sse/providers/capabilities.js +++ b/open-sse/providers/capabilities.js @@ -205,6 +205,7 @@ export const PATTERN_CAPABILITIES = [ // ── Gemini (all 2.0+ multimodal + google_search grounding, 1M ctx) ─ { pattern: "*gemini*image*", caps: { vision: true, imageOutput: true, contextWindow: 1048576 } }, + { pattern: "*gemini-3.7*", caps: { vision: true, audioInput: true, videoInput: true, reasoning: true, search: true, thinkingFormat: "gemini-level", thinkingCanDisable: false, contextWindow: 1048576, maxOutput: 65536 } }, { pattern: "*gemini-3*pro*", caps: { vision: true, audioInput: true, videoInput: true, reasoning: true, search: true, thinkingFormat: "gemini-level", thinkingCanDisable: false, contextWindow: 1048576, maxOutput: 65535 } }, { pattern: "*gemini-3*", caps: { vision: true, audioInput: true, videoInput: true, reasoning: true, search: true, thinkingFormat: "gemini-level", thinkingCanDisable: false, contextWindow: 1048576, maxOutput: 65536 } }, { pattern: "*gemini-2.5*", caps: { vision: true, audioInput: true, videoInput: true, reasoning: true, search: true, thinkingFormat: "gemini-budget", thinkingRange: { min: 0, max: 24576 }, contextWindow: 1048576, maxOutput: 65536 } }, diff --git a/open-sse/providers/models/schema.js b/open-sse/providers/models/schema.js index c14a3d58..73bf7139 100644 --- a/open-sse/providers/models/schema.js +++ b/open-sse/providers/models/schema.js @@ -38,3 +38,11 @@ export function modelStrip(model) { export function modelTargetFormat(model) { return model?.targetFormat || MODEL_DEFAULTS.targetFormat; } + +// Per-model declared upstream formats (e.g. ["openai", "claude"]). Guards the +// sourceFormat-matched transport for multi-endpoint providers whose models differ +// in endpoint support (opencode-go: kimi/glm only do /chat/completions, minimax/qwen +// also do /messages, deepseek also does /responses). +export function modelSupportedFormats(model) { + return model?.supportedFormats || null; +} diff --git a/open-sse/providers/pricing.js b/open-sse/providers/pricing.js index e9f2b612..cf6ffd55 100644 --- a/open-sse/providers/pricing.js +++ b/open-sse/providers/pricing.js @@ -57,6 +57,10 @@ export const MODEL_PRICING = { "o1-mini": { input: 3.00, output: 12.00, cached: 1.50, reasoning: 18.00, cache_creation: 3.00 }, // === Gemini === + "gemini-3.7-flash": { input: 1.50, output: 7.50, cached: 0.15, reasoning: 11.25, cache_creation: 1.875 }, + "gemini-3.7-flash-high": { input: 1.50, output: 7.50, cached: 0.15, reasoning: 11.25, cache_creation: 1.875 }, + "gemini-3.7-flash-medium": { input: 1.50, output: 7.50, cached: 0.15, reasoning: 11.25, cache_creation: 1.875 }, + "gemini-3.7-flash-low": { input: 1.50, output: 7.50, cached: 0.15, reasoning: 11.25, cache_creation: 1.875 }, "gemini-3.6-flash": { input: 1.50, output: 7.50, cached: 0.15, reasoning: 11.25, cache_creation: 1.875 }, "gemini-3.6-flash-high": { input: 1.50, output: 7.50, cached: 0.15, reasoning: 11.25, cache_creation: 1.875 }, "gemini-3.6-flash-medium": { input: 1.50, output: 7.50, cached: 0.15, reasoning: 11.25, cache_creation: 1.875 }, diff --git a/open-sse/providers/registry/alitp-intl.js b/open-sse/providers/registry/alitp-intl.js new file mode 100644 index 00000000..bebc4d7f --- /dev/null +++ b/open-sse/providers/registry/alitp-intl.js @@ -0,0 +1,35 @@ +// Token Plan — credit subscription keys on token-plan..maas.aliyuncs.com. +// Fourth Alibaba key type: Coding Plan (alicode/alicode-intl) and Model Studio +// (alims-intl) both reject these keys, and they reject Model Studio keys back. +// Singapore is the only region that serves the plan; eu-central-1 answers +// IllegalEndpoint. The Anthropic surface (/apps/anthropic/v1/messages) is not +// authorized for this plan, so OpenAI-compatible mode is the only transport. +export default { + id: "alitp-intl", + priority: 11, + alias: "alitp-intl", + display: { + name: "Alibaba Token Plan", + icon: "cloud", + color: "#FF6A00", + textIcon: "ATP", + website: "https://www.alibabacloud.com/campaign/ai-landing-page-token", + notice: { + apiKeyUrl: "https://modelstudio.console.alibabacloud.com/?apiKey=1", + }, + }, + category: "apikey", + transport: { + baseUrl: "https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1/chat/completions", + headers: {}, + quirks: { preserveCacheControl: true }, + }, + models: [ + { id: "qwen3.8-max-preview", name: "Qwen3.8 Max Preview" }, + { id: "qwen3.7-max", name: "Qwen3.7 Max" }, + { id: "qwen3.7-plus", name: "Qwen3.7 Plus" }, + { id: "qwen3.6-flash", name: "Qwen3.6 Flash" }, + { id: "glm-5.2", name: "GLM 5.2" }, + { id: "deepseek-v4-pro", name: "DeepSeek V4 Pro" }, + ], +}; diff --git a/open-sse/providers/registry/antigravity.js b/open-sse/providers/registry/antigravity.js index 1a7c4347..2666552b 100644 --- a/open-sse/providers/registry/antigravity.js +++ b/open-sse/providers/registry/antigravity.js @@ -45,6 +45,9 @@ export default { clientSecret: "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", }, models: [ + { id: "gemini-3.7-flash-high", name: "Gemini 3.7 Flash (High)", upstreamModelId: "gemini-3.7-flash-tiered(high)" }, + { id: "gemini-3.7-flash-medium", name: "Gemini 3.7 Flash (Medium)", upstreamModelId: "gemini-3.7-flash-tiered(medium)" }, + { id: "gemini-3.7-flash-low", name: "Gemini 3.7 Flash (Low)", upstreamModelId: "gemini-3.7-flash-tiered(low)" }, { id: "gemini-3.6-flash-high", name: "Gemini 3.6 Flash (High)", upstreamModelId: "gemini-3.6-flash-tiered(high)" }, { id: "gemini-3.6-flash-medium", name: "Gemini 3.6 Flash (Medium)", upstreamModelId: "gemini-3.6-flash-tiered(medium)" }, { id: "gemini-3.6-flash-low", name: "Gemini 3.6 Flash (Low)", upstreamModelId: "gemini-3.6-flash-tiered(low)" }, diff --git a/open-sse/providers/registry/fish-audio.js b/open-sse/providers/registry/fish-audio.js new file mode 100644 index 00000000..f016a247 --- /dev/null +++ b/open-sse/providers/registry/fish-audio.js @@ -0,0 +1,31 @@ +// Fish Audio TTS — the model id travels in an HTTP `model` header rather than the +// JSON body, and the voice is a reference_id (a cloned or preset voice model). +export default { + id: "fish-audio", + alias: "fish", + display: { + name: "Fish Audio", + icon: "record_voice_over", + color: "#1E9BF0", + textIcon: "FA", + website: "https://fish.audio", + notice: { + apiKeyUrl: "https://fish.audio/app/api-keys/", + }, + }, + category: "apikey", + authType: "apikey", + serviceKinds: ["tts"], + ttsConfig: { + baseUrl: "https://api.fish.audio/v1/tts", + authType: "apikey", + authHeader: "bearer", + format: "fish-audio", + models: [ + { id: "s2.1-pro-free", name: "S2.1 Pro Free" }, + { id: "s2.1-pro", name: "S2.1 Pro" }, + { id: "s2-pro", name: "S2 Pro" }, + { id: "s1", name: "S1" }, + ], + }, +}; diff --git a/open-sse/providers/registry/gemini.js b/open-sse/providers/registry/gemini.js index c9b0de9b..df643556 100644 --- a/open-sse/providers/registry/gemini.js +++ b/open-sse/providers/registry/gemini.js @@ -36,6 +36,7 @@ export default { }, }, models: [ + { id: "gemini-3.7-flash", name: "Gemini 3.7 Flash" }, { id: "gemini-3.6-flash", name: "Gemini 3.6 Flash" }, { id: "gemini-3.5-flash-lite", name: "Gemini 3.5 Flash Lite" }, { id: "gemini-3.1-pro-preview", name: "Gemini 3.1 Pro Preview" }, diff --git a/open-sse/providers/registry/glm-cn.js b/open-sse/providers/registry/glm-cn.js index 90a71b4c..cff9eb92 100644 --- a/open-sse/providers/registry/glm-cn.js +++ b/open-sse/providers/registry/glm-cn.js @@ -21,6 +21,7 @@ export default { }, }, models: [ + { id: "glm-5.3", name: "GLM 5.3" }, { id: "glm-5.2", name: "GLM 5.2" }, { id: "glm-5.1", name: "GLM 5.1" }, { id: "glm-5", name: "GLM 5" }, diff --git a/open-sse/providers/registry/glm.js b/open-sse/providers/registry/glm.js index 95e11533..9bc099b2 100644 --- a/open-sse/providers/registry/glm.js +++ b/open-sse/providers/registry/glm.js @@ -45,6 +45,7 @@ export default { }, ], models: [ + { id: "glm-5.3", name: "GLM 5.3" }, { id: "glm-5.2", name: "GLM 5.2" }, { id: "glm-5.1", name: "GLM 5.1" }, { id: "glm-5", name: "GLM 5" }, diff --git a/open-sse/providers/registry/index.js b/open-sse/providers/registry/index.js index 4a1ef56f..1e102b7f 100644 --- a/open-sse/providers/registry/index.js +++ b/open-sse/providers/registry/index.js @@ -118,6 +118,8 @@ import p115 from "./tokenrouter.js"; import p116 from "./selfhosted-stt.js"; import p117 from "./selfhosted-tts.js"; import p118 from "./selfhosted-embedding.js"; +import p119 from "./fish-audio.js"; +import p120 from "./alitp-intl.js"; export default [ p0, @@ -239,4 +241,6 @@ export default [ p116, p117, p118, + p119, + p120, ]; diff --git a/open-sse/providers/registry/kimchi.js b/open-sse/providers/registry/kimchi.js index c2ec22b1..1fe0b706 100644 --- a/open-sse/providers/registry/kimchi.js +++ b/open-sse/providers/registry/kimchi.js @@ -14,7 +14,7 @@ export default { }, }, category: "freeTier", - authModes: ["oauth"], + authModes: ["oauth", "apikey"], hasOAuth: true, transport: { baseUrl: "https://llm.kimchi.dev/openai/v1/chat/completions", diff --git a/open-sse/providers/registry/opencode-go.js b/open-sse/providers/registry/opencode-go.js index c980cc85..4b189ba8 100644 --- a/open-sse/providers/registry/opencode-go.js +++ b/open-sse/providers/registry/opencode-go.js @@ -22,20 +22,28 @@ export default { baseUrl: "https://opencode.ai/zen/go/v1/chat/completions", headers: {}, }, + // Multi-endpoint: pick the transport matching the client sourceFormat to skip + // translation. Guarded per-model by `supportedFormats` (see chatCore) because + // opencode-go models differ in endpoint support. + transports: [ + { format: "openai", baseUrl: "https://opencode.ai/zen/go/v1/chat/completions", auth: { combined: true, header: "Authorization", scheme: "bearer" } }, + { format: "claude", baseUrl: "https://opencode.ai/zen/go/v1/messages", auth: { combined: true, header: "x-api-key", scheme: "raw", anthropicVersion: true } }, + { format: "openai-responses", baseUrl: "https://opencode.ai/zen/go/v1/responses", auth: { combined: true, header: "Authorization", scheme: "bearer" } }, + ], models: [ - { id: "glm-5.2", name: "GLM 5.2" }, - { id: "glm-5.1", name: "GLM 5.1" }, - { id: "kimi-k2.7-code", name: "Kimi K2.7 Code" }, - { id: "kimi-k2.6", name: "Kimi K2.6" }, - { id: "deepseek-v4-pro", name: "DeepSeek V4 Pro" }, - { id: "deepseek-v4-flash", name: "DeepSeek V4 Flash" }, - { id: "mimo-v2.5", name: "MiMo V2.5" }, - { id: "mimo-v2.5-pro", name: "MiMo V2.5 Pro" }, - { id: "minimax-m3", name: "MiniMax M3", targetFormat: "claude" }, - { id: "minimax-m2.7", name: "MiniMax M2.7", targetFormat: "claude" }, - { id: "minimax-m2.5", name: "MiniMax M2.5", targetFormat: "claude" }, - { id: "qwen3.7-max", name: "Qwen 3.7 Max", targetFormat: "claude" }, - { id: "qwen3.7-plus", name: "Qwen 3.7 Plus", targetFormat: "claude" }, - { id: "qwen3.6-plus", name: "Qwen 3.6 Plus", targetFormat: "claude" }, + { id: "glm-5.2", name: "GLM 5.2", supportedFormats: ["openai"] }, + { id: "glm-5.1", name: "GLM 5.1", supportedFormats: ["openai"] }, + { id: "kimi-k2.7-code", name: "Kimi K2.7 Code", supportedFormats: ["openai"] }, + { id: "kimi-k2.6", name: "Kimi K2.6", supportedFormats: ["openai"] }, + { id: "deepseek-v4-pro", name: "DeepSeek V4 Pro", supportedFormats: ["openai", "claude", "openai-responses"] }, + { id: "deepseek-v4-flash", name: "DeepSeek V4 Flash", supportedFormats: ["openai", "claude", "openai-responses"] }, + { id: "mimo-v2.5", name: "MiMo V2.5", supportedFormats: ["openai"] }, + { id: "mimo-v2.5-pro", name: "MiMo V2.5 Pro", supportedFormats: ["openai"] }, + { id: "minimax-m3", name: "MiniMax M3", supportedFormats: ["openai", "claude"] }, + { id: "minimax-m2.7", name: "MiniMax M2.7", supportedFormats: ["openai", "claude"] }, + { id: "minimax-m2.5", name: "MiniMax M2.5", supportedFormats: ["openai", "claude"] }, + { id: "qwen3.7-max", name: "Qwen 3.7 Max", supportedFormats: ["openai", "claude"] }, + { id: "qwen3.7-plus", name: "Qwen 3.7 Plus", supportedFormats: ["openai", "claude"] }, + { id: "qwen3.6-plus", name: "Qwen 3.6 Plus", supportedFormats: ["openai", "claude"] }, ], }; diff --git a/open-sse/services/combo.js b/open-sse/services/combo.js index fdfc189a..e3b9a8e6 100644 --- a/open-sse/services/combo.js +++ b/open-sse/services/combo.js @@ -138,8 +138,42 @@ export function detectRequiredCapabilities(body) { if (Array.isArray(content)) for (const b of content) scanBlock(b); }; + const scanMessage = (m) => { + if (!m || typeof m !== "object") return; + + // Ollama / Hermes images array (strings or objects) + if (Array.isArray(m.images) && m.images.length > 0) { + required.add("vision"); + } + + // Vercel AI SDK / Hermes attachments / experimental_attachments + const attachments = m.experimental_attachments || m.attachments; + if (Array.isArray(attachments)) { + for (const att of attachments) { + if (!att) continue; + const mime = att.contentType || att.mediaType || (typeof att.url === "string" && att.url.match(/^data:([^;,]+)/)?.[1]); + if (mime) addByMime(mime); + else if (att.url || att.data) required.add("vision"); + } + } + + // Direct message-level modality properties + if (m.image_url || m.image) required.add("vision"); + if (m.audio_url || m.audio) required.add("audioInput"); + + // Scan array content blocks + scanContent(m.content); + + // Scan string content for embedded data URIs + if (typeof m.content === "string") { + if (m.content.includes("data:image/")) required.add("vision"); + else if (m.content.includes("data:audio/")) required.add("audioInput"); + else if (m.content.includes("data:application/pdf")) required.add("pdf"); + } + }; + // Modalities: current user turn only (trailing user run across each known shape). - for (const m of trailingUserItems(body.messages)) scanContent(m.content); // openai / claude + for (const m of trailingUserItems(body.messages)) scanMessage(m); // openai / claude / hermes / ollama for (const it of trailingUserItems(body.input)) scanContent(it.content); // responses const contents = body.contents || body.request?.contents; // gemini / antigravity for (const c of trailingUserItems(contents)) scanContent(c.parts); @@ -530,7 +564,10 @@ export async function handleFusionChat({ body, models, handleSingleModel, log, c log.info("FUSION", `Combo "${comboName}" | panel=${panel.length} [${panel.join(", ")}] | judge=${judge} | quorum=${minPanel}`); // 1. Fan out to the panel in parallel: non-streaming, tools stripped (we want prose). - const { tools, tool_choice, ...rest } = body; + const { tools, tool_choice, stream_options, ...rest } = body; + // Fusion runs panel models non-streaming; drop stream_options too, or providers + // like DeepSeek reject it with "stream_options should be set along with stream = true". + // See issue #3024. const panelBody = { ...rest, stream: false }; // Flatten tool turns to prose so panel models keep context without emitting tool_calls. diff --git a/open-sse/services/usage.js b/open-sse/services/usage.js index 370583db..10bb4bdc 100644 --- a/open-sse/services/usage.js +++ b/open-sse/services/usage.js @@ -34,7 +34,7 @@ const USAGE_HANDLERS = { github: (c) => getGitHubUsage(c.accessToken, c.providerSpecificData, c.proxyOptions), "gemini-cli": (c) => getGeminiUsage(c.accessToken, c.providerDataWithProjectId, c.proxyOptions), antigravity: (c) => getAntigravityUsage(c.accessToken, c.providerSpecificData, c.proxyOptions), - claude: (c) => getClaudeUsage(c.accessToken, c.proxyOptions), + claude: (c) => getClaudeUsage(c.accessToken, c.proxyOptions, { force: c.force }), codex: (c) => getCodexUsage(c.accessToken, c.proxyOptions), kiro: (c) => getKiroUsage(c.accessToken, c.providerSpecificData, c.proxyOptions), qoder: async (c) => { @@ -58,7 +58,7 @@ const USAGE_HANDLERS = { freebuff: (c) => getFreebuffUsage(c.accessToken, c.providerSpecificData, c.proxyOptions), }; -export async function getUsageForProvider(connection, proxyOptions = null) { +export async function getUsageForProvider(connection, proxyOptions = null, options = {}) { const { provider, accessToken, apiKey, providerSpecificData, projectId } = connection; const providerDataWithProjectId = { ...(providerSpecificData || {}), @@ -67,5 +67,13 @@ export async function getUsageForProvider(connection, proxyOptions = null) { const handler = USAGE_HANDLERS[provider]; if (!handler) return { message: `Usage API not implemented for ${provider}` }; - return await handler({ provider, accessToken, apiKey, providerSpecificData, providerDataWithProjectId, proxyOptions }); + return await handler({ + provider, + accessToken, + apiKey, + providerSpecificData, + providerDataWithProjectId, + proxyOptions, + force: options.force === true, + }); } diff --git a/open-sse/services/usage/claude.js b/open-sse/services/usage/claude.js index 85ab8e6f..ce3e01a0 100644 --- a/open-sse/services/usage/claude.js +++ b/open-sse/services/usage/claude.js @@ -19,7 +19,43 @@ const CLAUDE_CONFIG = { const OAUTH_429_COOLDOWN_MS = 180000; const oauthCooldown = new Map(); -export async function getClaudeUsage(accessToken, proxyOptions = null) { +// Dedup + short TTL cache per access token. Many tabs / many accounts / auto-refresh +// all funnel through here; without this each call hits Anthropic and triggers 429. +const USAGE_CACHE_TTL_MS = 300000; +const usageCache = new Map(); // token -> { promise } | { result, expiresAt } + +export async function getClaudeUsage(accessToken, proxyOptions = null, options = {}) { + const force = options?.force === true; + + // Serve in-flight or fresh cached result (skip on manual force) + if (!force && accessToken) { + const hit = usageCache.get(accessToken); + if (hit?.promise) return hit.promise; + if (hit && hit.expiresAt > Date.now()) return hit.result; + } + + const stale = (!force && accessToken && usageCache.get(accessToken)?.result) || null; + + const promise = (async () => { + const result = await fetchClaudeUsageRaw(accessToken, proxyOptions); + // Only cache real quota data, not soft-failure {message: ...} payloads + if (accessToken && result?.quotas) { + usageCache.set(accessToken, { + result, + expiresAt: Date.now() + USAGE_CACHE_TTL_MS, + }); + return result; + } + // Soft failure (429/error): prefer the last good read over a transient error + if (stale) return stale; + return result; + })(); + + if (accessToken) usageCache.set(accessToken, { promise }); + return promise; +} + +async function fetchClaudeUsageRaw(accessToken, proxyOptions = null) { try { // Skip OAuth usage call while this token is cooling down from a recent 429 const cooldownUntil = oauthCooldown.get(accessToken); diff --git a/open-sse/services/usage/google.js b/open-sse/services/usage/google.js index e79c018e..c7564c00 100644 --- a/open-sse/services/usage/google.js +++ b/open-sse/services/usage/google.js @@ -161,6 +161,9 @@ export async function getAntigravityUsage(accessToken, providerSpecificData, pro if (data.models) { // Filter only recommended/important models (must match PROVIDER_MODELS ag ids) const importantModels = [ + 'gemini-3.7-flash-high', + 'gemini-3.7-flash-medium', + 'gemini-3.7-flash-low', 'gemini-3.6-flash-high', 'gemini-3.6-flash-medium', 'gemini-3.6-flash-low', diff --git a/open-sse/translator/concerns/modality.js b/open-sse/translator/concerns/modality.js index b3bba2de..d2d62f4e 100644 --- a/open-sse/translator/concerns/modality.js +++ b/open-sse/translator/concerns/modality.js @@ -62,6 +62,19 @@ function stripOpenAI(body, caps) { if (!Array.isArray(body.messages)) return; const last = body.messages.length - 1; body.messages.forEach((msg, i) => { + if (caps.vision === false) { + if (Array.isArray(msg.images)) delete msg.images; + if (Array.isArray(msg.experimental_attachments)) { + msg.experimental_attachments = msg.experimental_attachments.filter( + (a) => !(a?.contentType?.startsWith("image/") || (typeof a?.url === "string" && a.url.startsWith("data:image/"))) + ); + } + if (Array.isArray(msg.attachments)) { + msg.attachments = msg.attachments.filter( + (a) => !(a?.contentType?.startsWith("image/") || (typeof a?.url === "string" && a.url.startsWith("data:image/"))) + ); + } + } if (!Array.isArray(msg.content)) return; const removed = new Set(); msg.content = filterBlocks(msg.content, capForOpenAIBlock, caps, removed, i === last); diff --git a/open-sse/translator/formats/claude.js b/open-sse/translator/formats/claude.js index 128917ec..81e72a44 100644 --- a/open-sse/translator/formats/claude.js +++ b/open-sse/translator/formats/claude.js @@ -9,6 +9,9 @@ import { PROVIDERS } from "../../providers/index.js"; import { getCapabilitiesForModel } from "../../providers/capabilities.js"; import { DEFAULT_MAX_TOKENS } from "../../config/runtimeConfig.js"; +const CACHE_CONTROL_5M = { type: "ephemeral" }; +const CACHE_CONTROL_1H = { type: "ephemeral", ttl: "1h" }; + // Check if message has valid non-empty content export function hasValidContent(msg) { if (typeof msg.content === "string" && msg.content.trim()) return true; @@ -124,32 +127,38 @@ export function normalizeClaudePassthrough(body, model = "") { if (Object.keys(body.output_config).length === 0) delete body.output_config; } - // 2. Hoist mid-conversation system messages into the top-level system field + // 2. Fold mid-conversation system messages into the neighbouring turn. + // Hoisting them into body.system would insert volatile content (token counters, + // reminders) ahead of the whole conversation and invalidate the prefix cache on + // every request. Folding in place keeps the cached prefix stable. if (Array.isArray(body.messages)) { - const systemBlocks = []; const messages = []; for (const msg of body.messages) { - if (msg.role === ROLE.SYSTEM) { - const text = typeof msg.content === "string" - ? msg.content - : Array.isArray(msg.content) - ? msg.content.map(b => (typeof b === "string" ? b : b?.text || "")).join("\n") - : ""; - if (text.trim()) systemBlocks.push({ type: CLAUDE_BLOCK.TEXT, text }); + if (msg.role !== ROLE.SYSTEM) { + messages.push(msg); continue; } - messages.push(msg); - } + const text = typeof msg.content === "string" + ? msg.content + : Array.isArray(msg.content) + ? msg.content.map(b => (typeof b === "string" ? b : b?.text || "")).join("\n") + : ""; + if (!text.trim()) continue; - if (systemBlocks.length > 0) { - const existing = Array.isArray(body.system) - ? body.system - : typeof body.system === "string" && body.system.trim() - ? [{ type: "text", text: body.system }] - : []; - body.system = [...existing, ...systemBlocks]; - body.messages = messages; + // Copy-on-write: the caller's body is reused across account-fallback + // attempts, so folding must never mutate the original message. + const block = { type: CLAUDE_BLOCK.TEXT, text }; + const prev = messages[messages.length - 1]; + if (prev?.role === ROLE.USER) { + const content = typeof prev.content === "string" + ? [{ type: CLAUDE_BLOCK.TEXT, text: prev.content }] + : Array.isArray(prev.content) ? [...prev.content] : []; + messages[messages.length - 1] = { ...prev, content: [...content, block] }; + continue; + } + messages.push({ role: ROLE.USER, content: [block] }); } + body.messages = messages; } // 3. Drop thinking blocks whose signature is not Claude's (combo mixes models, @@ -182,6 +191,70 @@ export function normalizeClaudePassthrough(body, model = "") { return body; } +// Put a 5m breakpoint on the last cache-eligible block of a message. +// thinking/redacted_thinking blocks do not accept cache_control. +function markLastCacheableBlock(msg) { + if (!Array.isArray(msg?.content)) return false; + for (let i = msg.content.length - 1; i >= 0; i--) { + const block = msg.content[i]; + if (typeof block !== "object" || block === null) continue; + if (block.type === CLAUDE_BLOCK.THINKING || block.type === CLAUDE_BLOCK.REDACTED_THINKING) continue; + block.cache_control = { ...CACHE_CONTROL_5M }; + return true; + } + return false; +} + +// Re-anchor cache breakpoints on a Claude passthrough body (same policy as +// prepareClaudeRequest): last tool + last system block at 1h, last assistant at 5m. +// The client's own markers point at pre-normalization offsets, so they are dropped. +// Must run LAST, after every step that can reshape system/tools/messages +// (normalize, tool dedupe, token savers) — otherwise the anchor drifts off the tail. +export function anchorClaudeCache(body) { + if (!body || typeof body !== "object") return body; + + if (Array.isArray(body.system)) { + const last = body.system.length - 1; + body.system.forEach((block, i) => { + if (typeof block !== "object" || block === null) return; + if (i === last) block.cache_control = { ...CACHE_CONTROL_1H }; + else delete block.cache_control; + }); + } + + if (Array.isArray(body.tools)) { + const last = body.tools.length - 1; + body.tools.forEach((tool, i) => { + if (i === last) tool.cache_control = { ...CACHE_CONTROL_1H }; + else delete tool.cache_control; + }); + } + + if (Array.isArray(body.messages)) { + let anchored = null; + for (let i = body.messages.length - 1; i >= 0; i--) { + const msg = body.messages[i]; + if (!Array.isArray(msg.content)) continue; + for (const block of msg.content) delete block.cache_control; + + // Prefer the last assistant turn: it ends a completed exchange, so the + // prefix up to it stays byte-stable across the following requests. + if (anchored || msg.role !== ROLE.ASSISTANT) continue; + anchored = markLastCacheableBlock(msg); + } + + // First turn of a conversation has no assistant yet — anchor the final + // message instead, so the opening prompt is cached rather than paid twice. + if (!anchored) { + for (let i = body.messages.length - 1; i >= 0 && !anchored; i--) { + anchored = markLastCacheableBlock(body.messages[i]); + } + } + } + + return body; +} + // Prepare request for Claude format endpoints // - Cleanup cache_control // - Filter empty messages diff --git a/open-sse/translator/request/claude-to-kiro.js b/open-sse/translator/request/claude-to-kiro.js index 8651e819..972b0610 100644 --- a/open-sse/translator/request/claude-to-kiro.js +++ b/open-sse/translator/request/claude-to-kiro.js @@ -287,6 +287,18 @@ export function claudeToKiroRequest(model, body, stream, credentials) { toolSpecs, nameMap, }); + // canonicalizeKiroConversation() already ran its second-chance repair (flatten + // every structured tool turn to text, then re-validate). A body that is STILL + // invalid here cannot be made shippable, and Kiro answers it with + // 400 {"message":"Improperly formed request.","reason":"REQUEST_BODY_INVALID"}. + // Fail locally instead: chatCore turns a falsy return into a 400 without + // spending an upstream call or a per-account cooldown. The taxonomy + // (role:N | pair:N | id:N | spec:N | orphan:0 | current) names the offending + // turn so the shape can be diagnosed from the log alone. + if (!canonical.valid) { + console.error(`[Kiro] refusing invalid conversation (claude → kiro): ${(canonical.errors || []).join(", ") || "unknown"} | turns=${(canonical.history || []).length + 1}`); + return null; + } const replayCurrent = canonical.currentMessage.userInputMessage; const userInputMessage = { content: replayCurrent.content || "", diff --git a/open-sse/translator/request/openai-responses.js b/open-sse/translator/request/openai-responses.js index b821f3a4..29e43152 100644 --- a/open-sse/translator/request/openai-responses.js +++ b/open-sse/translator/request/openai-responses.js @@ -421,6 +421,7 @@ export function openaiToOpenAIResponsesRequest(model, body, stream, credentials) if (body.reasoning !== undefined) result.reasoning = body.reasoning; if (body.reasoning_effort !== undefined) result.reasoning = { effort: body.reasoning_effort, summary: "auto" }; if (body.service_tier !== undefined) result.service_tier = body.service_tier; + if (body.prompt_cache_key !== undefined) result.prompt_cache_key = body.prompt_cache_key; return result; } diff --git a/open-sse/translator/request/openai-to-kiro.js b/open-sse/translator/request/openai-to-kiro.js index aa776949..3e30d2f5 100644 --- a/open-sse/translator/request/openai-to-kiro.js +++ b/open-sse/translator/request/openai-to-kiro.js @@ -379,6 +379,18 @@ export function openaiToKiroRequest(model, body, stream, credentials) { toolSpecs, nameMap, }); + // canonicalizeKiroConversation() already ran its second-chance repair (flatten + // every structured tool turn to text, then re-validate). A body that is STILL + // invalid here cannot be made shippable, and Kiro answers it with + // 400 {"message":"Improperly formed request.","reason":"REQUEST_BODY_INVALID"}. + // Fail locally instead: chatCore turns a falsy return into a 400 without + // spending an upstream call or a per-account cooldown. The taxonomy + // (role:N | pair:N | id:N | spec:N | orphan:0 | current) names the offending + // turn so the shape can be diagnosed from the log alone. + if (!canonical.valid) { + console.error(`[Kiro] refusing invalid conversation (openai → kiro): ${(canonical.errors || []).join(", ") || "unknown"} | turns=${(canonical.history || []).length + 1}`); + return null; + } const replayCurrent = canonical.currentMessage.userInputMessage; const payload = { diff --git a/open-sse/translator/response/kiro-to-claude.js b/open-sse/translator/response/kiro-to-claude.js index 1c9ece5b..455672b1 100644 --- a/open-sse/translator/response/kiro-to-claude.js +++ b/open-sse/translator/response/kiro-to-claude.js @@ -75,6 +75,15 @@ export function kiroToClaudeResponse(chunk, state) { ? data.usage.completion_tokens : 0; state.usage = { input_tokens: promptTokens, output_tokens: outputTokens }; + // Claude clients read cache_read/cache_creation to price a turn and to size + // their prompt cache. Both spellings are accepted because the Kiro executor + // emits the Chat shape and passthrough responses use the nested details form. + const cacheRead = data.usage.cache_read_input_tokens + ?? data.usage.prompt_tokens_details?.cached_tokens; + const cacheCreation = data.usage.cache_creation_input_tokens + ?? data.usage.prompt_tokens_details?.cache_creation_tokens; + if (typeof cacheRead === "number") state.usage.cache_read_input_tokens = cacheRead; + if (typeof cacheCreation === "number") state.usage.cache_creation_input_tokens = cacheCreation; } // First chunk → emit message_start. @@ -254,6 +263,13 @@ export function kiroToClaudeNonStreaming(data) { usage: { input_tokens: usage.prompt_tokens || 0, output_tokens: usage.completion_tokens || 0, + // Same cache preservation as the streaming path above. + ...(typeof (usage.cache_read_input_tokens ?? usage.prompt_tokens_details?.cached_tokens) === "number" + ? { cache_read_input_tokens: usage.cache_read_input_tokens ?? usage.prompt_tokens_details.cached_tokens } + : {}), + ...(typeof (usage.cache_creation_input_tokens ?? usage.prompt_tokens_details?.cache_creation_tokens) === "number" + ? { cache_creation_input_tokens: usage.cache_creation_input_tokens ?? usage.prompt_tokens_details.cache_creation_tokens } + : {}), }, }; } diff --git a/open-sse/translator/response/openai-responses.js b/open-sse/translator/response/openai-responses.js index 92e3cb52..ff55bb4e 100644 --- a/open-sse/translator/response/openai-responses.js +++ b/open-sse/translator/response/openai-responses.js @@ -99,8 +99,8 @@ export function openaiToOpenAIResponsesResponse(chunk, state) { } } - // Handle tool_calls - if (delta.tool_calls) { + // Handle tool_calls (empty array is truthy; require a real call) + if (delta.tool_calls && delta.tool_calls.length) { closeMessage(state, emit, idx); for (const tc of delta.tool_calls) { emitToolCall(state, emit, tc); diff --git a/package.json b/package.json index 8661b19b..f931b4ea 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "9router-app", - "version": "0.5.50", + "version": "0.5.55", "description": "9Router web dashboard", "private": true, "scripts": { @@ -9,10 +9,10 @@ "build": "next build --webpack", "postbuild": "node scripts/copy-standalone-assets.mjs", "postbuild:bun": "node scripts/copy-standalone-assets.mjs", - "start": "next start --port 20127", + "start": "node custom-server.js --port 20127", "dev:bun": "bun --bun next dev --webpack --port 20127", "build:bun": "bun --bun next build --webpack", - "start:bun": "bun ./.next/standalone/server.js", + "start:bun": "bun ./.next/standalone/custom-server.js", "cli:pack": "npm --prefix cli run pack:cli", "cli:publish": "npm --prefix cli run publish:cli" }, @@ -23,8 +23,10 @@ "@dnd-kit/utilities": "^3.2.2", "@monaco-editor/react": "^4.7.0", "@next/third-parties": "^16.2.9", + "@node-saml/node-saml": "^5.1.0", "@xyflow/react": "^12.10.1", "bcryptjs": "^3.0.3", + "chalk": "^5.6.2", "confbox": "^0.2.4", "dompurify": "^3.4.13", "express": "^5.2.1", @@ -38,6 +40,7 @@ "node-machine-id": "^1.1.12", "open": "^11.0.0", "ora": "^9.1.0", + "prop-types": "^15.8.1", "react": "19.2.4", "react-dom": "19.2.4", "react-is": "^16.13.1", diff --git a/scripts/copy-standalone-assets.mjs b/scripts/copy-standalone-assets.mjs index bfaf6e0d..c0bc0a5a 100644 --- a/scripts/copy-standalone-assets.mjs +++ b/scripts/copy-standalone-assets.mjs @@ -29,6 +29,14 @@ export function copyStandaloneAssets({ projectRoot = process.cwd(), distDir = pr cpSync(publicSource, publicDestination, { recursive: true, force: true }); console.log(`[standalone-assets] Copied public assets to ${publicDestination}`); } + + // Without it beside server.js the standalone build serves requests unsanitized. + const serverWrapperSource = resolve(projectRoot, "custom-server.js"); + const serverWrapperDestination = resolve(standaloneDir, "custom-server.js"); + if (existsSync(serverWrapperSource)) { + cpSync(serverWrapperSource, serverWrapperDestination, { force: true }); + console.log(`[standalone-assets] Copied custom-server.js to ${serverWrapperDestination}`); + } } if (process.argv[1] && resolve(process.argv[1]) === resolve(dirname(fileURLToPath(import.meta.url)), "copy-standalone-assets.mjs")) { diff --git a/src/app/(dashboard)/dashboard/cli-tools/components/HermesToolCard.js b/src/app/(dashboard)/dashboard/cli-tools/components/HermesToolCard.js index 974372d7..9ef6cddf 100644 --- a/src/app/(dashboard)/dashboard/cli-tools/components/HermesToolCard.js +++ b/src/app/(dashboard)/dashboard/cli-tools/components/HermesToolCard.js @@ -170,7 +170,7 @@ export default function HermesToolCard({ ? selectedApiKey : (!cloudEnabled ? "sk_9router" : ""); - const yamlContent = `model:\n default: "${selectedModel || "provider/model-id"}"\n provider: "custom"\n base_url: "${getEffectiveBaseUrl()}"\n`; + const yamlContent = `model:\n default: "${selectedModel || "provider/model-id"}"\n provider: "custom"\n base_url: "${getEffectiveBaseUrl()}"\n api_key: \${OPENAI_API_KEY}\n`; const envContent = `OPENAI_API_KEY=${keyToUse}\n`; return [ diff --git a/src/app/(dashboard)/dashboard/profile/page.js b/src/app/(dashboard)/dashboard/profile/page.js index 806b85ac..39d15686 100644 --- a/src/app/(dashboard)/dashboard/profile/page.js +++ b/src/app/(dashboard)/dashboard/profile/page.js @@ -31,7 +31,7 @@ function getLocaleFromCookie() { export default function ProfilePage() { const { theme, setTheme, isDark } = useTheme(); - const [locale, setLocale] = useState("en"); + const [locale, setLocale] = useState(() => getLocaleFromCookie()); const [langOpen, setLangOpen] = useState(false); const [shutdownOpen, setShutdownOpen] = useState(false); const [isShuttingDown, setIsShuttingDown] = useState(false); @@ -56,8 +56,31 @@ export default function ProfilePage() { const [oidcLoading, setOidcLoading] = useState(false); const [oidcTestLoading, setOidcTestLoading] = useState(false); const [oidcTestStatus, setOidcTestStatus] = useState({ type: "", message: "" }); - const [oidcRedirectUri, setOidcRedirectUri] = useState("/api/auth/oidc/callback"); const [oidcExpanded, setOidcExpanded] = useState(false); + + const origin = typeof window !== "undefined" ? window.location.origin : ""; + const oidcRedirectUri = origin ? `${origin}/api/auth/oidc/callback` : "/api/auth/oidc/callback"; + const samlAcsUrl = origin ? `${origin}/api/auth/saml/acs` : "/api/auth/saml/acs"; + const samlMetadataUrl = origin ? `${origin}/api/auth/saml/metadata` : "/api/auth/saml/metadata"; + + // SAML State + const [ssoTypeTab, setSsoTypeTab] = useState("saml"); + const [samlForm, setSamlForm] = useState({ + samlEntryPoint: "", + samlIssuer: "urn:9router:sp", + samlCert: "", + samlLoginLabel: "Sign in with SAML SSO", + samlAttributeEmail: "email", + samlAttributeName: "name", + }); + const [samlStatus, setSamlStatus] = useState({ type: "", message: "" }); + const [samlLoading, setSamlLoading] = useState(false); + const [samlTestLoading, setSamlTestLoading] = useState(false); + const [samlTestStatus, setSamlTestStatus] = useState({ type: "", message: "" }); + const [showSamlGuide, setShowSamlGuide] = useState(false); + const idpMetadataFileRef = useRef(null); + const certFileRef = useRef(null); + const importFileRef = useRef(null); const [memoryInfo, setMemoryInfo] = useState(null); const [memoryLoading, setMemoryLoading] = useState(false); @@ -70,10 +93,6 @@ export default function ProfilePage() { const [proxyLoading, setProxyLoading] = useState(false); const [proxyTestLoading, setProxyTestLoading] = useState(false); - useEffect(() => { - setLocale(getLocaleFromCookie()); - }, [langOpen]); - useEffect(() => { fetch("/api/settings") .then((res) => res.json()) @@ -87,7 +106,23 @@ export default function ProfilePage() { oidcLoginLabel: data?.oidcLoginLabel || "Sign in with OIDC", }); setOidcClientSecret(""); - if (data?.authMode === "oidc" || data?.authMode === "both") setOidcExpanded(true); + setSsoTypeTab(data?.ssoType || "saml"); + setSamlForm({ + samlEntryPoint: data?.samlEntryPoint || "", + samlIssuer: data?.samlIssuer || "urn:9router:sp", + samlCert: data?.samlCert || "", + samlLoginLabel: data?.samlLoginLabel || "Sign in with SAML SSO", + samlAttributeEmail: data?.samlAttributeEmail || "email", + samlAttributeName: data?.samlAttributeName || "name", + }); + if ( + data?.authMode === "sso" || + data?.authMode === "saml" || + data?.authMode === "oidc" || + data?.authMode === "both" + ) { + setOidcExpanded(true); + } setProxyForm({ outboundProxyEnabled: data?.outboundProxyEnabled === true, outboundProxyUrl: data?.outboundProxyUrl || "", @@ -101,12 +136,6 @@ export default function ProfilePage() { }); }, []); - useEffect(() => { - if (typeof window !== "undefined") { - setOidcRedirectUri(`${window.location.origin}/api/auth/oidc/callback`); - } - }, []); - const updateOutboundProxy = async (e) => { e.preventDefault(); if (settings.outboundProxyEnabled !== true) return; @@ -343,6 +372,7 @@ export default function ProfilePage() { try { const payload = { authMode, + ssoType: "oidc", oidcIssuerUrl: issuerUrl, oidcClientId: clientId, oidcScopes: scopes || "openid profile email", @@ -457,6 +487,159 @@ export default function ProfilePage() { } }; + const updateSamlForm = (field, value) => { + setSamlForm((prev) => ({ ...prev, [field]: value })); + }; + + const handleIdpMetadataUpload = (event) => { + const file = event.target.files?.[0]; + if (idpMetadataFileRef.current) idpMetadataFileRef.current.value = ""; + if (!file) return; + + const reader = new FileReader(); + reader.onload = (e) => { + try { + const xmlText = e.target?.result || ""; + const parser = new DOMParser(); + const doc = parser.parseFromString(xmlText, "text/xml"); + const parserError = doc.querySelector("parsererror"); + if (parserError) { + setSamlStatus({ type: "error", message: "Unable to parse valid SAML IdP metadata from XML file" }); + return; + } + + const entityID = doc.documentElement.getAttribute("entityID") || ""; + const ssoNodes = Array.from(doc.querySelectorAll("SingleSignOnService, *|SingleSignOnService")); + let ssoUrl = ""; + for (const node of ssoNodes) { + const binding = node.getAttribute("Binding") || ""; + const location = node.getAttribute("Location") || ""; + if (location) { + ssoUrl = location; + if (binding.includes("HTTP-Redirect")) break; + } + } + + const certNodes = Array.from(doc.querySelectorAll("X509Certificate, *|X509Certificate")); + let certStr = ""; + if (certNodes.length > 0) { + certStr = certNodes[0].textContent.trim(); + } + + setSamlForm((prev) => ({ + ...prev, + samlEntryPoint: ssoUrl || prev.samlEntryPoint, + samlIssuer: prev.samlIssuer || "urn:9router:sp", + samlCert: certStr || prev.samlCert, + })); + + setSamlStatus({ + type: "success", + message: `IdP Metadata imported! (SSO URL: ${ssoUrl ? "found" : "not found"}, EntityID: ${entityID ? "found" : "not found"}, Cert: ${certStr ? "found" : "not found"})`, + }); + } catch (err) { + setSamlStatus({ type: "error", message: "Error reading IdP Metadata XML file" }); + } + }; + reader.readAsText(file); + }; + + const handleCertFileUpload = (event) => { + const file = event.target.files?.[0]; + if (certFileRef.current) certFileRef.current.value = ""; + if (!file) return; + + const reader = new FileReader(); + reader.onload = (e) => { + const text = e.target?.result || ""; + setSamlForm((prev) => ({ ...prev, samlCert: text.trim() })); + setSamlStatus({ type: "success", message: "Certificate file loaded into configuration." }); + }; + reader.readAsText(file); + }; + + const saveSamlSettings = async (targetAuthMode = oidcForm.authMode || "password") => { + setSamlLoading(true); + setSamlStatus({ type: "", message: "" }); + setSamlTestStatus({ type: "", message: "" }); + + try { + const payload = { + authMode: targetAuthMode, + ssoType: "saml", + samlEntryPoint: samlForm.samlEntryPoint.trim(), + samlIssuer: samlForm.samlIssuer.trim() || "urn:9router:sp", + samlCert: samlForm.samlCert.trim(), + samlLoginLabel: samlForm.samlLoginLabel.trim() || "Sign in with SAML SSO", + samlAttributeEmail: samlForm.samlAttributeEmail.trim() || "email", + samlAttributeName: samlForm.samlAttributeName.trim() || "name", + }; + + const res = await fetch("/api/settings", { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload), + }); + + const data = await res.json(); + if (res.ok) { + setSettings((prev) => ({ ...prev, ...data })); + setSamlForm({ + samlEntryPoint: data?.samlEntryPoint || payload.samlEntryPoint, + samlIssuer: data?.samlIssuer || payload.samlIssuer, + samlCert: data?.samlCert || payload.samlCert, + samlLoginLabel: data?.samlLoginLabel || payload.samlLoginLabel, + samlAttributeEmail: data?.samlAttributeEmail || payload.samlAttributeEmail, + samlAttributeName: data?.samlAttributeName || payload.samlAttributeName, + }); + setSamlStatus({ + type: "success", + message: + targetAuthMode === "sso" || targetAuthMode === "saml" + ? "SAML SSO login enabled" + : targetAuthMode === "both" + ? "Password and SAML SSO login enabled" + : "SAML 2.0 settings saved", + }); + } else { + setSamlStatus({ type: "error", message: data.error || "Failed to save SAML settings" }); + } + } catch { + setSamlStatus({ type: "error", message: "An error occurred while saving SAML settings" }); + } finally { + setSamlLoading(false); + } + }; + + const testSamlConnection = async () => { + setSamlTestLoading(true); + setSamlStatus({ type: "", message: "" }); + setSamlTestStatus({ type: "", message: "" }); + + try { + const res = await fetch("/api/auth/saml/test", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + samlEntryPoint: samlForm.samlEntryPoint.trim(), + samlIssuer: samlForm.samlIssuer.trim(), + samlCert: samlForm.samlCert.trim(), + }), + }); + + const data = await res.json(); + if (res.ok && data.ok) { + setSamlTestStatus({ type: "success", message: data.message || "SAML configuration verified!" }); + } else { + setSamlTestStatus({ type: "error", message: data.error || "SAML configuration test failed" }); + } + } catch { + setSamlTestStatus({ type: "error", message: "An error occurred while testing SAML configuration" }); + } finally { + setSamlTestLoading(false); + } + }; + const updateObservabilityEnabled = async (enabled) => { try { const res = await fetch("/api/settings", { @@ -793,7 +976,7 @@ export default function ProfilePage() { - {/* OIDC */} + {/* Single Sign-On (SSO) */} {oidcExpanded && ( -
-

- Use Authentik or any OIDC provider to sign in to the dashboard. You can enable password-only, OIDC-only, or both for the dashboard; model API access still uses API keys. -

+
+

+ Configure enterprise Single Sign-On (SSO) for dashboard access using SAML 2.0 or OIDC. +

-
- -
- {[ - { - value: "password", - title: "Password only", - desc: "Keep the legacy password login.", - }, - { - value: "oidc", - title: "OIDC only", - desc: "Require OIDC for dashboard access.", - }, - { - value: "both", - title: "Both", - desc: "Allow either password or OIDC.", - }, - ].map((option) => { - const active = oidcForm.authMode === option.value; - return ( + {/* SSO Protocol Switcher Tabs */} +
+ +
+ + +
+
+ + {/* Auth Mode selection */} +
+ +
+ {[ + { + value: "password", + title: "Password only", + desc: "Keep legacy password login.", + }, + { + value: "sso", + title: `${ssoTypeTab === "saml" ? "SAML" : "OIDC"} only`, + desc: "Require SSO for dashboard access.", + }, + { + value: "both", + title: "Both", + desc: "Allow password or SSO login.", + }, + ].map((option) => { + const currentMode = oidcForm.authMode; + const active = + option.value === "password" + ? currentMode === "password" + : option.value === "sso" + ? currentMode === "sso" || currentMode === "saml" || currentMode === "oidc" + : currentMode === "both"; + return ( + + ); + })} +
+
+ + {ssoTypeTab === "saml" ? ( + /* SAML Configuration Panel */ +
+ {/* IdP Setup Guidelines Banner & Collapsible Drawer */} +
- ); - })} -
+ + {showSamlGuide && ( +
+
+

🔑 Required Service Provider (SP) Values for your IdP Setup:

+
    +
  • + Assertion Consumer Service (ACS) URL:{" "} + {samlAcsUrl} +
  • +
  • + SP Entity ID / Audience URI:{" "} + {samlForm.samlIssuer || "urn:9router:sp"} +
  • +
  • + NameID Format:{" "} + EmailAddress or Unspecified +
  • +
+
+ +
+
+

+ ☁️ AWS IAM Identity Center +

+
    +
  1. Applications → Add application → Select Add custom SAML 2.0 application.
  2. +
  3. Set Application ACS URL to {samlAcsUrl}.
  4. +
  5. Set Application SAML audience to {samlForm.samlIssuer || "urn:9router:sp"}.
  6. +
  7. Under Attribute mappings, map Subject or email to ${`{user:email}`}.
  8. +
  9. Download IAM Identity Center SAML metadata XML file and use 1-Click Import below!
  10. +
+
+ +
+

+ 🔷 Microsoft Entra ID (Azure AD) +

+
    +
  1. Enterprise Applications → New application → Create your own application.
  2. +
  3. Select Single sign-on → SAML.
  4. +
  5. Identifier (Entity ID): {samlForm.samlIssuer || "urn:9router:sp"}
  6. +
  7. Reply URL (ACS): {samlAcsUrl}
  8. +
  9. Download Federation Metadata XML and import or copy X.509 Certificate.
  10. +
+
+ +
+

+ 🟢 Okta / Auth0 +

+
    +
  1. Applications → Create App Integration → Select SAML 2.0.
  2. +
  3. Single Sign-On URL: {samlAcsUrl}
  4. +
  5. Audience URI (SP Entity ID): {samlForm.samlIssuer || "urn:9router:sp"}
  6. +
  7. Name ID format: EmailAddress.
  8. +
  9. Download Identity Provider metadata XML or copy the X.509 cert.
  10. +
+
+ +
+

+ 🛡️ Keycloak / Authentik +

+
    +
  1. Clients → Create client → Select SAML.
  2. +
  3. Client ID: {samlForm.samlIssuer || "urn:9router:sp"}
  4. +
  5. Master SAML Processing URL: {samlAcsUrl}
  6. +
  7. Export SAML Descriptor XML or copy IDP Certificate PEM.
  8. +
+
+
+
+ )} +
+ + {/* Quick Import Card */} +
+
+

1-Click IdP Metadata XML Import

+

Auto-fill SSO URL, Issuer & Cert from XML metadata

+
+ + +
+ +
+
+ + updateSamlForm("samlEntryPoint", e.target.value)} + disabled={loading || samlLoading} + /> +
+ +
+ + updateSamlForm("samlIssuer", e.target.value)} + disabled={loading || samlLoading} + /> +
+ +
+
+ + + +
+