From d05b84d377b1d674517ca9831e4d6afd31f28435 Mon Sep 17 00:00:00 2001 From: "MUH. IQRAM BAHRING" Date: Thu, 6 Aug 2026 03:11:31 +0800 Subject: [PATCH] chore: prepare fork docker deployment --- .github/workflows/docker-publish.yml | 2 +- README.md | 1547 +---------------- docker-compose.yml | 2 +- open-sse/providers/registry/freebuff.js | 2 +- .../dashboard/providers/[id]/ConnectionRow.js | 251 ++- .../dashboard/providers/[id]/page.js | 28 +- .../api/oauth/[provider]/[action]/route.js | 1 + src/app/api/providers/[id]/models/route.js | 2 +- src/app/api/providers/[id]/route.js | 41 +- src/app/api/providers/[id]/test/testUtils.js | 2 +- src/app/api/usage/[connectionId]/route.js | 2 +- src/app/api/v1/models/route.js | 2 +- src/lib/db/repos/connectionsRepo.js | 1 + src/lib/network/connectionProxy.js | 108 +- src/shared/components/OAuthModal.js | 1 + src/sse/services/auth.js | 2 +- 16 files changed, 397 insertions(+), 1597 deletions(-) diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index e8ef957e..270f71b6 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -8,7 +8,7 @@ on: env: GHCR_IMAGE: ghcr.io/${{ github.repository }} - DOCKERHUB_IMAGE: decolua/9router + DOCKERHUB_IMAGE: mhiqrambhrng/9router-mibp-version jobs: build-and-push: diff --git a/README.md b/README.md index d46d0819..cb7a48e7 100644 --- a/README.md +++ b/README.md @@ -1,124 +1,23 @@ -
- 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 - FREE AI Router & Token Saver -decolua%2F9router | Trendshift +9Router is a local AI routing gateway with provider fallback and token-saving features. -[🚀 Quick Start](#-quick-start) • [💡 Features](#-key-features) • [📖 Setup](#-setup-guide) • [🌐 Website](https://9router.com) +## Quick Start -[🇻🇳 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) +### Install -
- ---- - -## 🤔 Why 9Router? - -**Stop wasting money, tokens and hitting limits:** - -- ❌ Subscription quota expires unused every month -- ❌ Rate limits stop you mid-coding -- ❌ Tool outputs (git diff, grep, ls...) burn tokens fast -- ❌ Expensive APIs ($20-50/month per provider) -- ❌ Manual switching between providers - -**9Router solves this:** - -- ✅ **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 - ---- - -## 🔄 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:** +#### Docker ```bash -npm install -g 9router -9router +cp .env.example .env +docker compose up -d ``` -🎉 Dashboard opens at `http://localhost:20128` +Dashboard: `http://localhost:20128/dashboard` -**2. Connect a FREE provider (no signup needed):** +API endpoint: `http://localhost:20128/v1` -Dashboard → Providers → Connect **Kiro AI** (~50 credits/month free: Claude 4.5 + GLM-5 + MiniMax) or **OpenCode Free** (no auth) → Done! - -**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 -``` - -**That's it!** Start coding with FREE AI models. - ---- - -## 🔄 Keep Your Fork Updated - -If you forked this repository from `decolua/9router`, sync with upstream updates: - -```bash -cd /path/to/your/9router-fork - -# Fetch and rebase with upstream -git fetch upstream -git rebase upstream/master -git push origin master --force-with-lease -``` - -If conflicts occur during rebase, resolve them manually then run `git rebase --continue`. - ---- - -**Alternative: run from source (this repository):** - -This repository package is private (`9router-app`), so source/Docker execution is the expected local development path. +#### From Source ```bash cp .env.example .env @@ -126,1420 +25,44 @@ npm install PORT=20128 NEXT_PUBLIC_BASE_URL=http://localhost:20128 npm run dev ``` -Production mode: +Before starting, set at least `JWT_SECRET` and `INITIAL_PASSWORD` in `.env`. + +### Update + +This fork follows `decolua/9router` through the `upstream` remote. ```bash -npm run build -PORT=20128 HOSTNAME=0.0.0.0 NEXT_PUBLIC_BASE_URL=http://localhost:20128 npm run start +git fetch upstream +git checkout master +git merge upstream/master +git push origin master ``` -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: +To build and publish this fork's Docker image after updating the source: ```bash -pip install "headroom-ai[proxy]" -headroom proxy --port 8787 +git tag v1.0.0 +git push origin v1.0.0 ``` -Enable in Dashboard → Endpoint → Token Saver → Headroom. Default URL: `http://localhost:8787`. +The GitHub Actions workflow builds and publishes: -Docker examples: +```text +mhiqrambhrng/9router-mibp-version +``` + +On the server, update the running container with: ```bash -# Headroom service in same Docker network -http://headroom:8787 - -# Headroom running on host machine -http://host.docker.internal:8787 +docker compose pull 9router +docker compose up -d ``` -If Headroom is down or returns an error, 9Router fails open and sends the original request. +## Forks (9Router) -### 🐴 Ponytail (Lazy Senior Dev) +- [9Router upstream](https://github.com/decolua/9router) +- [9Router MIBP Version](https://github.com/mhiqrambg/9router-mibp-version) -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). +## License -- **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 -
+MIT License. See [LICENSE](LICENSE) for details. diff --git a/docker-compose.yml b/docker-compose.yml index 8331b260..081fab8b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,6 @@ services: 9router: - image: decolua/9router:latest + image: mhiqrambhrng/9router-mibp-version:latest container_name: 9router restart: always ports: diff --git a/open-sse/providers/registry/freebuff.js b/open-sse/providers/registry/freebuff.js index a3431050..78e8df3c 100644 --- a/open-sse/providers/registry/freebuff.js +++ b/open-sse/providers/registry/freebuff.js @@ -34,7 +34,7 @@ export default { website: "https://freebuff.com", notice: { signupUrl: "https://freebuff.com", - text: "Free ad-supported coding agent by Codebuff. Sign in with your Freebuff/Codebuff account via browser login. Free tier is ad-supported and limited in some regions (limited mode: 6 x 1-hour sessions/day); full mode runs in select countries.", + text: "Free ad-supported coding agent by Codebuff. Sign in with your Freebuff/Codebuff account via browser login. Free tier is ad-supported and limited in some regions (limited mode: 6 x 1-hour sessions/day); full mode runs in select countries. ⚠️ One account has ONE active session locked to ONE model — requesting a different model while a session is active returns 'model_locked' (409); use a separate account per model, or wait for the session to expire.", }, }, category: "free", diff --git a/src/app/(dashboard)/dashboard/providers/[id]/ConnectionRow.js b/src/app/(dashboard)/dashboard/providers/[id]/ConnectionRow.js index 0b5bea19..51c8e2a5 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/ConnectionRow.js +++ b/src/app/(dashboard)/dashboard/providers/[id]/ConnectionRow.js @@ -9,27 +9,72 @@ import CooldownTimer from "./CooldownTimer"; export default function ConnectionRow({ connection, proxyPools, isOAuth, isFirst, isLast, onMoveUp, onMoveDown, onToggleActive, onUpdateProxy, onEdit, onDelete, oneByOneStatus = null, autoPing = null }) { const [showProxyDropdown, setShowProxyDropdown] = useState(false); const [updatingProxy, setUpdatingProxy] = useState(false); + const [selectedProxyIds, setSelectedProxyIds] = useState([]); + const [rotationStrategy, setRotationStrategy] = useState("none"); const proxyDropdownRef = useRef(null); + // Initialize proxy state from connection + useEffect(() => { + const proxyPoolIds = connection.providerSpecificData?.proxyPoolIds || []; + const legacyProxyPoolId = connection.providerSpecificData?.proxyPoolId; + + // Migrate legacy single proxy to array format + if (legacyProxyPoolId && proxyPoolIds.length === 0) { + setSelectedProxyIds([legacyProxyPoolId]); + } else { + setSelectedProxyIds(proxyPoolIds); + } + + setRotationStrategy(connection.providerSpecificData?.proxyRotationStrategy || "none"); + }, [connection]); + const proxyPoolMap = new Map((proxyPools || []).map((pool) => [pool.id, pool])); - const boundProxyPoolId = connection.providerSpecificData?.proxyPoolId || null; - const boundProxyPool = boundProxyPoolId ? proxyPoolMap.get(boundProxyPoolId) : null; + + // Display logic - support both new (multi-proxy) and legacy (single proxy) formats const hasLegacyProxy = connection.providerSpecificData?.connectionProxyEnabled === true && !!connection.providerSpecificData?.connectionProxyUrl; - const hasAnyProxy = !!boundProxyPoolId || hasLegacyProxy; - const proxyDisplayText = boundProxyPool - ? `Pool: ${boundProxyPool.name}` - : boundProxyPoolId - ? `Pool: ${boundProxyPoolId} (inactive/missing)` - : hasLegacyProxy - ? `Legacy: ${connection.providerSpecificData?.connectionProxyUrl}` - : ""; + const hasAnyProxy = selectedProxyIds.length > 0 || hasLegacyProxy; + + const getProxyDisplayText = () => { + if (selectedProxyIds.length === 0 && !hasLegacyProxy) return ""; + + if (selectedProxyIds.length === 1) { + const pool = proxyPoolMap.get(selectedProxyIds[0]); + return pool ? `Pool: ${pool.name}` : `Pool: ${selectedProxyIds[0]} (inactive/missing)`; + } + + if (selectedProxyIds.length > 1) { + const strategyLabel = rotationStrategy === "random" ? "Random" : rotationStrategy === "round-robin" ? "Round Robin" : rotationStrategy === "failover" ? "Failover" : "Multiple"; + return `${selectedProxyIds.length} pools (${strategyLabel})`; + } + + if (hasLegacyProxy) { + return `Legacy: ${connection.providerSpecificData?.connectionProxyUrl}`; + } + + return ""; + }; + + const proxyDisplayText = getProxyDisplayText(); const autoPingTooltip = autoPing?.provider === "codex" ? "Auto-starts the next 5h Codex window after reset by sending a tiny gpt-5.5 request. Consumes a small amount of quota." : "When your 5h quota runs out, auto-sends a request the moment it resets so a new window starts right away."; let maskedProxyUrl = ""; - if (boundProxyPool?.proxyUrl || connection.providerSpecificData?.connectionProxyUrl) { - const rawProxyUrl = boundProxyPool?.proxyUrl || connection.providerSpecificData?.connectionProxyUrl; + if (selectedProxyIds.length > 0) { + const selectedPools = selectedProxyIds.map(id => proxyPoolMap.get(id)).filter(Boolean); + if (selectedPools.length > 0) { + try { + const parsed = new URL(selectedPools[0].proxyUrl); + maskedProxyUrl = `${parsed.protocol}//${parsed.hostname}${parsed.port ? `:${parsed.port}` : ""}`; + if (selectedPools.length > 1) { + maskedProxyUrl += ` (+${selectedPools.length - 1} more)`; + } + } catch { + maskedProxyUrl = selectedPools[0].proxyUrl; + } + } + } else if (connection.providerSpecificData?.connectionProxyUrl) { + const rawProxyUrl = connection.providerSpecificData?.connectionProxyUrl; try { const parsed = new URL(rawProxyUrl); maskedProxyUrl = `${parsed.protocol}//${parsed.hostname}${parsed.port ? `:${parsed.port}` : ""}`; @@ -38,12 +83,15 @@ export default function ConnectionRow({ connection, proxyPools, isOAuth, isFirst } } - const noProxyText = boundProxyPool?.noProxy || connection.providerSpecificData?.connectionNoProxy || ""; + const noProxyText = selectedProxyIds.length > 0 + ? proxyPoolMap.get(selectedProxyIds[0])?.noProxy || "" + : connection.providerSpecificData?.connectionNoProxy || ""; let proxyBadgeVariant = "default"; - if (boundProxyPool?.isActive === true) { - proxyBadgeVariant = "success"; - } else if (boundProxyPoolId || hasLegacyProxy) { + if (selectedProxyIds.length > 0) { + const allActive = selectedProxyIds.every(id => proxyPoolMap.get(id)?.isActive === true); + proxyBadgeVariant = allActive ? "success" : "error"; + } else if (hasLegacyProxy) { proxyBadgeVariant = "error"; } @@ -59,10 +107,54 @@ export default function ConnectionRow({ connection, proxyPools, isOAuth, isFirst return () => document.removeEventListener("mousedown", handler); }, [showProxyDropdown]); - const handleSelectProxy = async (poolId) => { + const handleToggleProxySelection = (poolId) => { + setSelectedProxyIds(prev => { + if (prev.includes(poolId)) { + return prev.filter(id => id !== poolId); + } else { + return [...prev, poolId]; + } + }); + }; + + const handleStrategyChange = (strategy) => { + setRotationStrategy(strategy); + + // Auto-select all active proxy pools when switching to rotation strategies + if (strategy !== "none" && selectedProxyIds.length === 0) { + const activePoolIds = (proxyPools || []) + .filter(pool => pool.isActive === true) + .map(pool => pool.id); + setSelectedProxyIds(activePoolIds); + } + + // Reset to single proxy when switching to "none" + if (strategy === "none" && selectedProxyIds.length > 1) { + setSelectedProxyIds(selectedProxyIds.slice(0, 1)); + } + }; + + const handleApplyProxyChanges = async () => { setUpdatingProxy(true); try { - await onUpdateProxy(poolId === "__none__" ? null : poolId); + await onUpdateProxy({ + proxyPoolIds: selectedProxyIds, + proxyRotationStrategy: rotationStrategy, + }); + setShowProxyDropdown(false); + } finally { + setUpdatingProxy(false); + } + }; + + const handleSelectProxy = async (poolId) => { + // Legacy single-proxy mode (backwards compatibility) + setUpdatingProxy(true); + try { + await onUpdateProxy({ + proxyPoolIds: poolId === "__none__" ? [] : [poolId], + proxyRotationStrategy: "none", + }); } finally { setUpdatingProxy(false); setShowProxyDropdown(false); @@ -226,22 +318,117 @@ export default function ConnectionRow({ connection, proxyPools, isOAuth, isFirst Proxy {showProxyDropdown && ( -
- - {(proxyPools || []).map((pool) => ( - - ))} + + {/* Select All Button (only for rotation strategies) */} + {rotationStrategy !== "none" && ( + + )} + + {(proxyPools || []).map((pool) => { + const isSelected = selectedProxyIds.includes(pool.id); + const isActive = pool.isActive === true; + return ( + + ); + })} +
+ + {/* Apply Button */} +
+ +
)} diff --git a/src/app/(dashboard)/dashboard/providers/[id]/page.js b/src/app/(dashboard)/dashboard/providers/[id]/page.js index 8ca07ae4..5eb92c34 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/page.js +++ b/src/app/(dashboard)/dashboard/providers/[id]/page.js @@ -968,17 +968,39 @@ export default function ProviderDetailPage() { onToggle: (on) => handleAutoPingConnection(conn.id, on), provider: providerId, } : null} - onUpdateProxy={async (proxyPoolId) => { + onUpdateProxy={async (proxyConfig) => { try { + // Support both new format (object) and legacy format (string/null) + const updatePayload = typeof proxyConfig === 'object' && proxyConfig !== null + ? { + proxyPoolIds: proxyConfig.proxyPoolIds || [], + proxyRotationStrategy: proxyConfig.proxyRotationStrategy || "none", + } + : { + // Legacy single-proxy format + proxyPoolId: proxyConfig || null, + }; + const res = await fetch(`/api/providers/${conn.id}`, { method: "PUT", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ proxyPoolId: proxyPoolId || null }), + body: JSON.stringify(updatePayload), }); if (res.ok) { setConnections(prev => prev.map(c => c.id === conn.id - ? { ...c, providerSpecificData: { ...c.providerSpecificData, proxyPoolId: proxyPoolId || null } } + ? { + ...c, + providerSpecificData: { + ...c.providerSpecificData, + ...(updatePayload.proxyPoolIds !== undefined ? { + proxyPoolIds: updatePayload.proxyPoolIds, + proxyRotationStrategy: updatePayload.proxyRotationStrategy, + } : { + proxyPoolId: updatePayload.proxyPoolId, + }) + } + } : c )); } diff --git a/src/app/api/oauth/[provider]/[action]/route.js b/src/app/api/oauth/[provider]/[action]/route.js index 824cad8b..5b86f013 100644 --- a/src/app/api/oauth/[provider]/[action]/route.js +++ b/src/app/api/oauth/[provider]/[action]/route.js @@ -215,6 +215,7 @@ export async function GET(request, { params }) { "codebuddy-intl", "qoder", "grok-cli", + "freebuff", ]; let deviceData; if (noPkceDeviceProviders.includes(provider)) { diff --git a/src/app/api/providers/[id]/models/route.js b/src/app/api/providers/[id]/models/route.js index d73ec11d..8a422b58 100644 --- a/src/app/api/providers/[id]/models/route.js +++ b/src/app/api/providers/[id]/models/route.js @@ -391,7 +391,7 @@ const PROVIDER_MODELS_CONFIG = { }, "grok-cli": { customResolver: async (connection) => { - const proxy = await resolveConnectionProxyConfig(connection.providerSpecificData || {}); + const proxy = await resolveConnectionProxyConfig(connection.providerSpecificData || {}, connection.id); const result = await resolveGrokCliModels({ ...connection, connectionId: connection.id, diff --git a/src/app/api/providers/[id]/route.js b/src/app/api/providers/[id]/route.js index 6ab51797..77e9a5a4 100644 --- a/src/app/api/providers/[id]/route.js +++ b/src/app/api/providers/[id]/route.js @@ -33,7 +33,31 @@ function normalizeProxyConfig(body = {}) { }; } -async function normalizeProxyPoolUpdate(proxyPoolIdInput) { +async function normalizeProxyPoolUpdate(body) { + // Handle new multi-proxy format + if (body.proxyPoolIds !== undefined) { + const proxyPoolIds = Array.isArray(body.proxyPoolIds) ? body.proxyPoolIds : []; + const proxyRotationStrategy = body.proxyRotationStrategy || "none"; + + // Validate all proxy pool IDs exist + for (const poolId of proxyPoolIds) { + if (poolId) { + const pool = await getProxyPoolById(poolId); + if (!pool) { + return { hasProxyPoolField: true, error: `Proxy pool ${poolId} not found` }; + } + } + } + + return { + hasProxyPoolField: true, + proxyPoolIds: proxyPoolIds.filter(Boolean), + proxyRotationStrategy, + }; + } + + // Handle legacy single proxy format + const proxyPoolIdInput = body.proxyPoolId; if (proxyPoolIdInput === undefined) { return { hasProxyPoolField: false, proxyPoolId: null }; } @@ -111,7 +135,7 @@ export async function PUT(request, { params }) { return NextResponse.json({ error: proxyConfig.error }, { status: 400 }); } - const proxyPoolResult = await normalizeProxyPoolUpdate(body.proxyPoolId); + const proxyPoolResult = await normalizeProxyPoolUpdate(body); if (proxyPoolResult.error) { return NextResponse.json({ error: proxyPoolResult.error }, { status: 400 }); } @@ -147,10 +171,19 @@ export async function PUT(request, { params }) { } if (proxyPoolResult.hasProxyPoolField) { - if (proxyPoolResult.proxyPoolId === null) { + // Handle new multi-proxy format + if (proxyPoolResult.proxyPoolIds !== undefined) { + updateData.providerSpecificData.proxyPoolIds = proxyPoolResult.proxyPoolIds; + updateData.providerSpecificData.proxyRotationStrategy = proxyPoolResult.proxyRotationStrategy; + // Clear legacy field delete updateData.providerSpecificData.proxyPoolId; } else { - updateData.providerSpecificData.proxyPoolId = proxyPoolResult.proxyPoolId; + // Handle legacy single-proxy format + if (proxyPoolResult.proxyPoolId === null) { + delete updateData.providerSpecificData.proxyPoolId; + } else { + updateData.providerSpecificData.proxyPoolId = proxyPoolResult.proxyPoolId; + } } } } diff --git a/src/app/api/providers/[id]/test/testUtils.js b/src/app/api/providers/[id]/test/testUtils.js index 0ea46b2d..96fffd2a 100644 --- a/src/app/api/providers/[id]/test/testUtils.js +++ b/src/app/api/providers/[id]/test/testUtils.js @@ -803,7 +803,7 @@ export async function testSingleConnection(id) { const connection = await getProviderConnectionById(id); if (!connection) return { valid: false, error: "Connection not found", latencyMs: 0, testedAt: new Date().toISOString() }; - const effectiveProxy = await resolveConnectionProxyConfig(connection.providerSpecificData || {}); + const effectiveProxy = await resolveConnectionProxyConfig(connection.providerSpecificData || {}, connection.id); if (effectiveProxy.connectionProxyEnabled && effectiveProxy.connectionProxyUrl && !effectiveProxy.vercelRelayUrl) { const proxyResult = await testProxyUrl({ proxyUrl: effectiveProxy.connectionProxyUrl }); diff --git a/src/app/api/usage/[connectionId]/route.js b/src/app/api/usage/[connectionId]/route.js index 8ccdc015..3550c2b8 100644 --- a/src/app/api/usage/[connectionId]/route.js +++ b/src/app/api/usage/[connectionId]/route.js @@ -145,7 +145,7 @@ export async function GET(request, { params }) { } // Resolve connection proxy config; force strictProxy=false so quota/refresh fall back to direct on failure - const proxyConfig = await resolveConnectionProxyConfig(connection.providerSpecificData); + const proxyConfig = await resolveConnectionProxyConfig(connection.providerSpecificData, connection.id); const proxyOptions = { connectionProxyEnabled: proxyConfig.connectionProxyEnabled === true, connectionProxyUrl: proxyConfig.connectionProxyUrl || "", diff --git a/src/app/api/v1/models/route.js b/src/app/api/v1/models/route.js index 26c9d010..a5826545 100644 --- a/src/app/api/v1/models/route.js +++ b/src/app/api/v1/models/route.js @@ -77,7 +77,7 @@ const LIVE_MODEL_RESOLVERS = { return result?.models?.length ? { models: result.models } : null; }, "grok-cli": async (conn) => { - const proxy = await resolveConnectionProxyConfig(conn.providerSpecificData || {}); + const proxy = await resolveConnectionProxyConfig(conn.providerSpecificData || {}, conn.id); const result = await resolveGrokCliModels({ ...conn, connectionId: conn.id, diff --git a/src/lib/db/repos/connectionsRepo.js b/src/lib/db/repos/connectionsRepo.js index 4181843f..3b510965 100644 --- a/src/lib/db/repos/connectionsRepo.js +++ b/src/lib/db/repos/connectionsRepo.js @@ -8,6 +8,7 @@ const OPTIONAL_FIELDS = [ "scope", "projectId", "apiKey", "testStatus", "lastTested", "lastError", "lastErrorAt", "rateLimitedUntil", "expiresIn", "errorCode", "consecutiveUseCount", "idToken", "lastRefreshAt", + "proxyRotationStrategy", "proxyPoolIds", ]; function rowToConn(row) { diff --git a/src/lib/network/connectionProxy.js b/src/lib/network/connectionProxy.js index 9ecd2535..135859f7 100644 --- a/src/lib/network/connectionProxy.js +++ b/src/lib/network/connectionProxy.js @@ -59,75 +59,107 @@ function normalizeLegacyProxy(providerSpecificData = {}) { * Resolve final proxy configuration. * * Priority: - * 1. Proxy Pool - * 2. Legacy Proxy - * 3. No Proxy + * 1. Multi-Proxy Pool (new format with rotation) + * 2. Single Proxy Pool (legacy) + * 3. Legacy Proxy + * 4. No Proxy */ export async function resolveConnectionProxyConfig( - providerSpecificData = {} + providerSpecificData = {}, + connectionId = null ) { try { - const proxyPoolIdRaw = normalizeString( - providerSpecificData?.proxyPoolId - ); - - // "__none__" means explicitly disabled - const proxyPoolId = - proxyPoolIdRaw === "__none__" ? "" : proxyPoolIdRaw; + // Handle new multi-proxy format + const proxyPoolIds = providerSpecificData?.proxyPoolIds || []; + const proxyRotationStrategy = providerSpecificData?.proxyRotationStrategy || "none"; + + // Handle legacy single-proxy format + const legacyProxyPoolId = normalizeString(providerSpecificData?.proxyPoolId); + const proxyPoolIdRaw = legacyProxyPoolId === "__none__" ? "" : legacyProxyPoolId; const legacy = normalizeLegacyProxy(providerSpecificData); /** * ----------------------------- - * Proxy Pool Resolution + * Multi-Proxy Pool Resolution (NEW) * ----------------------------- */ - if (proxyPoolId) { - const proxyPool = await getProxyPoolById(proxyPoolId); + if (proxyPoolIds.length > 0) { + const selectedPoolId = pickProxyPoolId(proxyPoolIds, proxyRotationStrategy, connectionId); + + if (selectedPoolId) { + const proxyPool = await getProxyPoolById(selectedPoolId); + const proxyUrl = normalizeString(proxyPool?.proxyUrl); + const noProxy = normalizeString(proxyPool?.noProxy); + const isValidPool = proxyPool && proxyPool.isActive === true && proxyUrl; + + if (isValidPool) { + /** + * Vercel/Cloudflare relay proxies use base URL rewriting + * instead of HTTP_PROXY environment variables. + */ + if (proxyPool.type === "vercel" || proxyPool.type === "cloudflare" || proxyPool.type === "deno") { + return { + source: proxyPool.type, + proxyPoolId: selectedPoolId, + proxyPool, + connectionProxyEnabled: false, + connectionProxyUrl: "", + connectionNoProxy: noProxy, + strictProxy: proxyPool.strictProxy === true, + vercelRelayUrl: proxyUrl, + }; + } + + /** + * Standard proxy pool + */ + return { + source: "pool", + proxyPoolId: selectedPoolId, + proxyPool, + connectionProxyEnabled: true, + connectionProxyUrl: proxyUrl, + connectionNoProxy: noProxy, + strictProxy: proxyPool.strictProxy === true, + }; + } + } + } + + /** + * ----------------------------- + * Single Proxy Pool Resolution (LEGACY) + * ----------------------------- + */ + if (proxyPoolIdRaw) { + const proxyPool = await getProxyPoolById(proxyPoolIdRaw); const proxyUrl = normalizeString(proxyPool?.proxyUrl); const noProxy = normalizeString(proxyPool?.noProxy); - - const isValidPool = - proxyPool && - proxyPool.isActive === true && - proxyUrl; + const isValidPool = proxyPool && proxyPool.isActive === true && proxyUrl; if (isValidPool) { - /** - * Vercel/Cloudflare relay proxies use base URL rewriting - * instead of HTTP_PROXY environment variables. - */ if (proxyPool.type === "vercel" || proxyPool.type === "cloudflare" || proxyPool.type === "deno") { return { source: proxyPool.type, - - proxyPoolId, + proxyPoolId: proxyPoolIdRaw, proxyPool, - connectionProxyEnabled: false, connectionProxyUrl: "", connectionNoProxy: noProxy, - strictProxy: proxyPool.strictProxy === true, - - vercelRelayUrl: proxyUrl, // Still mapped to vercelRelayUrl in the unified payload since they use the exact same header spec + vercelRelayUrl: proxyUrl, }; } - /** - * Standard proxy pool - */ return { source: "pool", - - proxyPoolId, + proxyPoolId: proxyPoolIdRaw, proxyPool, - connectionProxyEnabled: true, connectionProxyUrl: proxyUrl, connectionNoProxy: noProxy, - strictProxy: proxyPool.strictProxy === true, }; } @@ -145,7 +177,7 @@ export async function resolveConnectionProxyConfig( return { source: "legacy", - proxyPoolId: proxyPoolId || null, + proxyPoolId: proxyPoolIdRaw || null, proxyPool: null, ...legacy, @@ -160,7 +192,7 @@ export async function resolveConnectionProxyConfig( return { source: "none", - proxyPoolId: proxyPoolId || null, + proxyPoolId: proxyPoolIdRaw || null, proxyPool: null, ...legacy, diff --git a/src/shared/components/OAuthModal.js b/src/shared/components/OAuthModal.js index 805301da..85ca8622 100644 --- a/src/shared/components/OAuthModal.js +++ b/src/shared/components/OAuthModal.js @@ -234,6 +234,7 @@ export default function OAuthModal({ isOpen, provider, providerInfo, onSuccess, "codebuddy-intl", "qoder", "grok-cli", + "freebuff", ]; if (deviceCodeProviders.includes(provider)) { setIsDeviceCode(true); diff --git a/src/sse/services/auth.js b/src/sse/services/auth.js index feaaa2ab..8eb0545c 100644 --- a/src/sse/services/auth.js +++ b/src/sse/services/auth.js @@ -172,7 +172,7 @@ export async function getProviderCredentials(provider, excludeConnectionIds = nu connection = availableConnections[0]; } - const resolvedProxy = await resolveConnectionProxyConfig(connection.providerSpecificData || {}); + const resolvedProxy = await resolveConnectionProxyConfig(connection.providerSpecificData || {}, connection.id); return { authType: connection.authType,