# 9router-go โ FREE AI Router & Token Saver (Single Binary)
**Never stop coding. Save 20-40% tokens with RTK + auto-fallback to FREE & cheap AI models.**
**Connect Claude Code, Cursor, Antigravity, Codex, Gemini, OpenCode, Cline, OpenClaw... to 40+ AI providers & 100+ models โ no Node.js needed at runtime.**
[](https://github.com/luqman-v1/9router-go/actions/workflows/ci.yml)
[](https://github.com/luqman-v1/9router-go/actions/workflows/release.yml)
[](https://github.com/luqman-v1/9router-go/releases/latest)
[](https://github.com/luqman-v1/9router-go/blob/main/LICENSE)
[](https://github.com/luqman-v1/9router-go/releases)
[](https://luqman-v1.github.io/9router-go/tg)
[๐ Quick Start](#-quick-start) โข [๐ก Features](#-key-features) โข [โ๏ธ Setup](#-setup-guide) โข [๐ Share DB with 9Router](#-sharing-a-database-with-9router) โข [๐ Upstream](https://github.com/decolua/9router)
---
## ๐ค Why 9router-go?
Same idea as [9Router](https://github.com/decolua/9router), minus the Node.js runtime: **one Go binary** serves the proxy APIs + an embedded Svelte dashboard.
**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
- โ Manual switching between providers
**9router-go solves this:**
- โ
**RTK Token Saver** โ auto-compress tool_result content, save 20-40% tokens
- โ
**Auto fallback** โ Subscription โ Cheap โ Free, zero downtime
- โ
**Extended combo routing** โ per-combo strategy: fallback, round-robin, sticky, capacity, or fusion
- โ
**Multi-account** โ round-robin between accounts per provider
- โ
**Proxy per API key** โ bind a proxy pool to each provider API key (connection), one-by-one or via **Apply Proxy** across many at once
- โ
**Custom headers** โ add your own request headers to custom compatible providers
- โ
**Unified custom provider add** โ one shared dialog adds OpenAI-/Anthropic-compatible endpoints
- โ
**Custom usage ranges** โ analyze any window (`14d`, `12h`), not just fixed presets
- โ
**Extended logging** โ live console log with level filters & search, plus per-request payload inspector
- โ
**Single binary** โ Go + embedded dashboard, works with Claude Code, Codex, Cursor, Cline, any CLI tool
---
## ๐ How It Works
```text
โโโโโโโโโโโโโโโ
โ Your CLI โ (Claude Code, Codex, OpenClaw, Cursor, Cline...)
โ Tool โ
โโโโโโโโฌโโโโโโโ
โ http://localhost:20130/v1
โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ 9router-go (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 (one line):**
```bash
# macOS / Linux
curl -fsSL https://raw.githubusercontent.com/luqman-v1/9router-go/main/install.sh | bash
```
```powershell
# Windows (PowerShell โ installer writes to %LOCALAPPDATA% + user PATH, no admin needed)
irm https://raw.githubusercontent.com/luqman-v1/9router-go/main/install.ps1 | iex
```
๐ Then start it (defaults: port `20130`, data `~/.9router` โ no flags needed):
```bash
9router-go
# Dashboard: http://localhost:20130 (Default password: 123456)
```
> **Already using upstream [9Router](https://github.com/decolua/9router)?** Point 9router-go at the same data dir โ it opens the **same `DATA_DIR/db/data.sqlite`**. Your providers, connections, combos, API keys, and usage history carry over as-is. No import step, no migration. See [Sharing a database with 9Router](#-sharing-a-database-with-9router) for the caveats.
**Keep the terminal free (background mode):**
```bash
9router-go start # same as: 9router-go --background (or -d)
9router-go status # is it running? (pid, dashboard, log)
9router-go restart # replace the running daemon
9router-go logs -n 100 # tail the background log
9router-go stop # stop it
```
The detached process records itself in `DATA_DIR/run/gateway.pid` and writes its
output to `DATA_DIR/run/gateway.log`. A second `start` while one is running
refuses instead of fighting over the port, and a daemon killed from Task Manager
leaves nothing behind: the stale pid file is cleaned on the next command.
> Windows stops the process with `TerminateProcess` instead of `SIGTERM`, so
> stopping skips the graceful drain; use the dashboard's Shutdown button or
> `9router-go restart` when you want in-flight streams to finish first.
**Start on boot (autostart):**
```bash
9router-go service install # systemd user unit, launchd agent, OpenRC/runit service, cron @reboot, or a Windows logon task
9router-go service status # which backend owns the entry, and whether it is enabled/active
9router-go service start # start the supervised service now (falls back to the background daemon)
9router-go service restart # restart it
9router-go service stop # stop it now but keep it enabled for boot
9router-go service uninstall # remove the autostart entry
```
`install` picks the native backend for the host. systemd, launchd, and cron all
install into the **user** scope and need no root; runit's `~/service` path too.
Two exceptions: OpenRC is a system service and needs root (`service install`
refuses without it โ run with `sudo`, or pick `--backend cron`), and on Windows
install registers a scheduled task via `schtasks`, which requires an
Administrator console. The Windows task runs as the current user at logon, so it
reads that user's data dir rather than `SYSTEM`'s. Force a backend with
`--backend systemd|launchd|openrc|runit|cron`.
**2. Connect a FREE provider (no signup needed):**
Dashboard โ Providers โ Connect **Kiro AI** (~50 credits/month free) or **OpenCode Free** (no auth) โ Done!
**3. Use in your CLI tool:**
```text
Claude Code / Codex / OpenClaw / Cursor / Cline Settings:
Endpoint: http://localhost:20130/v1
API Key: [Settings โ API Keys in the dashboard]
Model: kr/claude-sonnet-4.5
```
**That's it!** Start coding with FREE AI models.
**Alternatives:**
```bash
# Docker โ no build needed
docker run -d --name 9router-go --restart unless-stopped \
-p 20130:20130 -v "$HOME/.9router:/data" \
-e PORT=20130 -e DATA_DIR=/data \
-e INITIAL_PASSWORD=your-secure-password \
luqmenul/9router-go:latest
# Manual download โ pick your file, no command line guesswork:
# Windows โ .exe | Mac M1+ โ darwin-arm64 | Mac Intel โ darwin-amd64
# Linux VPS โ linux-amd64 | Raspberry Pi โ linux-arm64
```
๐ฆ [All release binaries](https://github.com/luqman-v1/9router-go/releases/latest) โข ๐จ [Build from source](#-setup-guide)
---
## ๐ก Key Features
- ๐ฅ๏ธ Native Svelte 5 dashboard: providers, OAuth, combos, proxy pools, API keys, usage, quota, settings, plans, per-entry sidebar visibility
- ๐ฑ Cost display in USD, IDR, EUR and ten more currencies โ editable rate table, dual display, one-click refresh from a public provider
- ๐ OpenAI Chat, Claude Messages, Gemini, Ollama-compatible, Responses, embeddings, media, search, web tools
- ๐ Combos with fallback, round-robin, sticky routing, fusion, capability-aware reordering
- ๐ฅ Per-provider executors, OAuth refresh, reactive 401 retry
- ๐ก Live usage + console-log SSE streams, stall detection
- ๐พ SQLite WAL persistence, outbound proxy support, self-update, MITM commands, Docker, cross-compilation
For sequential combo routing, set **Model response timeout** in seconds (decimals
such as `5.3` are fine) in the combo's Edit dialog, or from the compact field on the
combo card. Empty or `0` disables it. The timer covers each model
candidate except the last, including connection selection and waiting for a
usable response. If it expires before output is committed, the router cancels
that candidate and tries the next. The last candidate keeps the gateway's
normal deadline, so the setting never makes a combo fail sooner than it would
without it (#744). An active stream is not cut off by this setting. For
non-streaming requests, the timer covers the wait for the complete response.
Fusion panel timing is separate. A model that times out is left out of that
combo for a
grace period (default 5 minutes, set per combo under the timeout in the Edit
dialog; `0` re-includes it on the next request) and then rejoins the chain. If
every model in the chain is inside its grace period, the full chain is used. The
pause is held in memory and resets on restart. Stored as
`firstTokenGraceMs` next to the timeout. The dashboard shows seconds; the setting is stored in
milliseconds as `comboStrategies[comboName].firstTokenTimeoutMs`.
---
## ๐ Sharing a database with 9Router
**Already running [9Router](https://github.com/decolua/9router) (the Next.js version)? 9router-go reads that exact database. There is no import, no export, and no migration step.**
Both projects default to the same file and speak the same schema:
| | Path |
|---|---|
| macOS / Linux | `~/.9router/db/data.sqlite` |
| Windows | `%APPDATA%\9router\db\data.sqlite` |
| Docker | `/data/db/data.sqlite` inside the container |
Stop 9Router, start 9router-go, and log in with your existing password. Your provider connections, proxy pools, combos, API keys, model aliases, and usage history are all there โ 9router-go's own dashboard renders them, because it *is* the same data.
**You don't even have to stop 9Router** if you want to keep the Node.js dashboard around. Run 9router-go on a different port against the same `DATA_DIR`:
```bash
# 9Router on :20128 (Next.js) ยท 9router-go on :20130 (Go, same DB)
PORT=20130 DATA_DIR=/home/you/.9router ./9router-go
```
Both read the same tables, so a connection you add in either dashboard shows up in the other. SQLite serialises the writes; the supported setup is one active writer, so avoid editing the same combo or settings entry in both at the same moment.
**Running it the other way round** (9router-go first, then pointing 9Router at the same directory) works the same way. 9router-go will have added two columns to `providerConnections` (`lastUsedAt`, `consecutiveUseCount`) and one extra table (`upstream_leases`). Upstream ignores both โ its schema sync is additive and doesn't drop unknown columns โ so nothing breaks.
What 9router-go will **not** do:
- **Import a legacy JSON export.** If your 9Router data still lives in old JSON files, start upstream once to convert them, then switch.
- **Run destructive migrations or pre-migration backups.** Its bootstrap only ever *adds* โ create missing tables, add missing columns, seed missing rows. It never drops or retypes.
- **Interpret `_meta.schemaVersion`.** It's upstream's migration bookkeeping. A database written by a much newer 9Router release should be checked before you rely on it.
Take a backup first if the database matters:
```bash
cp -r ~/.9router/db ~/9router-db-backup # stop the daemon first โ see DATABASE.md
```
Full schema, operator contract, and multi-process limits: [`DATABASE.md`](DATABASE.md). Routing internals: [`ARCHITECTURE.md`](ARCHITECTURE.md).
---
## ๐ธ Screenshots


---
## โ๏ธ Setup Guide
### Release binary
Download from [GitHub Releases](https://github.com/luqman-v1/9router-go/releases/latest), verify against `SHA256SUMS.txt`.
### ๐งช Experimental builds
Unreleased work also ships as **experimental** builds. They are published
separately and are never offered to a normal install:
- GitHub Releases marks them **Pre-release**, so `releases/latest` โ the URL
`9router-go update` falls back to โ keeps pointing at the stable version.
- Docker Hub tags them `exp` (`luqmenul/9router-go:exp`), never `latest`.
```bash
docker pull luqmenul/9router-go:exp
```
Opt in only if you want to help shake out bugs. `9router-go update` will not
install one over a stable release โ grab the asset for your platform from the
[pre-releases page](https://github.com/luqman-v1/9router-go/releases) and
replace the running binary with it.
### Build from source
Prerequisites: Go 1.27 and Bun 1.x (dashboard is embedded into the binary, so build web first):
```bash
git clone https://github.com/luqman-v1/9router-go.git
cd 9router-go
make web-build # bun install --frozen-lockfile && bun run build
make build # embeds VERSION into the Go binary
```
### Run
Defaults are enough for most people โ plain `9router-go` listens on port `20130` with data in `~/.9router`:
```bash
9router-go
curl http://localhost:20130/health
./9router-go version
```
Only override when you need something different (`PORT`, `DATA_DIR`/`DB_PATH` โ there are no `--port` flags):
```bash
PORT=20129 ./9router-go # different port
DATA_DIR=/srv/9router ./9router-go # different data dir
DB_PATH=/srv/9router/data.sqlite ./9router-go # explicit SQLite file
HOST=127.0.0.1 ./9router-go # localhost only, behind a reverse proxy
```
### ๐ Dashboard Login & Fresh Install
- **Localhost (`localhost` / `127.0.0.1`)**: First login uses the default compatibility password `123456`. Once logged in, change your password in **Settings โ Profile**.
- **Remote / VPS / Docker / LAN**: For security (preventing public takeover of fresh installs with known defaults, CVE-2026-56679), remote access blocks the default `123456` password. You **must** either:
1. **Set `INITIAL_PASSWORD` on launch (Recommended)**:
```bash
INITIAL_PASSWORD="your-secure-password" ./9router-go
# Or in your .env file:
# INITIAL_PASSWORD=your-secure-password
```
2. **Or access via SSH port-forwarding first**:
```bash
ssh -L 20130:127.0.0.1:20130 user@remote-host
# Open http://localhost:20130, login with 123456, then change password in Settings
```
### Client example
```bash
curl http://localhost:20130/v1/chat/completions \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer sk-your-api-key' \
-d '{"model":"ag/gemini-3.8-flash-high","messages":[{"role":"user","content":"Hello"}],"stream":true}'
```
For Claude Messages clients: `ANTHROPIC_BASE_URL=http://localhost:20130/v1`.
Advanced: environment variables, API surface, auth, database
### Environment
| Variable | Default | Purpose |
| --- | --- | --- |
| `PORT` | `20130` | HTTP port |
| `HOST` / `BIND_ADDR` | all interfaces | Listener address |
| `DATA_DIR` | `~/.9router` (`%APPDATA%/9router` on Windows) | Data root |
| `DB_PATH` | `$DATA_DIR/db/data.sqlite` | SQLite file |
| `INITIAL_PASSWORD` | unset (fallback `123456` locally) | First dashboard password |
| `RTK_ENABLED` | `true` | RTK input compression |
| `CAVEMAN_ENABLED` / `PONYTAIL_ENABLED` | `false` | Style savers |
| `AUTO_UPDATE` | `false` | Background self-update |
| `HTTP_PROXY` / `HTTPS_PROXY` | Go defaults | Upstream egress proxy |
| `PPROF_ENABLED` | `false` | Expose `/debug/pprof/*` (keep off on untrusted networks) |
| `TRUST_PROXY` / `TRUST_CLOUDFLARE` | unset | Trust forwarded client-IP headers |
| `TRUSTED_PROXIES` | unset | CIDR list of the reverse proxies whose forwarded client-IP headers are trusted |
| `GEMINI_WEB_ROTATION` | `on` | Renew the Gemini Web session cookie in the background (see below) |
#### Gemini Web session renewal
The `gemini-web` provider reaches `gemini.google.com` with a session cookie,
and Google rotates the session's timestamp cookie (`__Secure-1PSIDTS`) every
few minutes. The router renews it in the background, so a signed-in account
keeps reaching the models it pays for instead of falling back to one model
after a few minutes.
**There are three ways to get a session, and the dashboard offers two of
them as a button.**
1. *A throwaway browser* โ the safest. The button opens a browser with a
profile of its own, waits for the Google sign-in, reads the session out of
it, then closes the browser and deletes the profile. Nothing is left
running that could rotate the session, so the router's renewal is the
only one.
2. *A browser that is already signed in* โ the convenient one. The button
opens the browser's own cookie database, takes the key that seals it from
the platform's secret store (the login keyring on Linux, the Keychain on
macOS, the profile's "Local State" file on Windows), and unseals the
cookies. **A browser that is still open keeps rotating the same session**
and the router renews it from the other side, and the two cancel each
out โ which signs the account out. Close the browser first, or use the
throwaway browser above. The dashboard says so when it finds a browser
that is running.
3. *A cookie you paste* โ the fallback. Copy the cookie header from an
Incognito window that you close right after, not from a normal tab, and
paste it into the connection form.
Set `GEMINI_WEB_ROTATION=off` to run the router without renewing anything at
all. That is the right choice when the credential belongs to a browser you
want to keep using.
`.env.example` documents the security-sensitive subset and OAuth overrides.
A forwarded client-IP header is only read when the TCP peer is inside the
trusted set โ otherwise a client could rotate `X-Forwarded-For` and give every
login attempt its own rate-limit bucket. `TRUST_PROXY=true` trusts loopback and
the RFC1918/link-local ranges; `TRUSTED_PROXIES` takes a comma-separated list
of CIDRs or single IPs and **replaces** those defaults, so set it when the
proxy connects from an address outside them.
Behind a proxy that is *not* in the trusted set, every request is bucketed by
the proxy's own socket address, so all users of that proxy share one login
bucket: five failed attempts lock them all out. `TRUSTED_PROXIES` is the knob
that separates them again.
### API surface
```text
POST /v1/chat/completions OpenAI Chat Completions
POST /v1/messages Claude Messages
POST /v1/responses Responses API
POST /v1/embeddings Embeddings
POST /api/chat Ollama-compatible chat
GET /v1/models Model catalog
POST /v1/images/generations Image generation
POST /v1/videos/generations Video generation
POST /v1/audio/speech Text to speech
POST /v1/audio/transcriptions Speech to text
POST /v1/search Web search
GET /api/usage/stream Live usage SSE
GET /health Liveness
GET /api/version Version metadata
```
### Authentication
- Public: `/health`, dashboard HTML/assets, `/login`, OAuth callbacks.
- Proxy routes need an active client API key (`Authorization: Bearer` / `X-API-Key`).
- Dashboard APIs need a session cookie, CLI token, or API key; destructive ops (shutdown, update, DB import) need a session or CLI token.
- The API server defaults to all interfaces โ bind localhost or protect the port outside trusted machines.
### Database compatibility
Go reads/writes the upstream 9router table/JSON shapes and bootstraps the core schema on startup (creates the 11 tables when absent, backfills missing columns, seeds an empty settings row) โ a fresh `DATA_DIR` just works, no upstream install needed. Existing databases are never modified beyond additive backfills. Full contract in [`DATABASE.md`](DATABASE.md), routing internals in [`ARCHITECTURE.md`](ARCHITECTURE.md).
---
## ๐ค Contributing
Issues and pull requests use templates, so pick the right one rather than opening a blank report:
- **Bug report** โ something behaves incorrectly. Include a reproduction, `9router-go version`, OS, and the log excerpt around the failure.
- **Feature request** โ the problem you cannot solve today, the surface it touches, and whether upstream already has it.
- **Upstream parity** โ a behaviour `decolua/9router` has and this gateway does not; link the upstream commit or PR.
- **Question** โ configuration and usage help.
PRs follow the checks CI runs: `go vet ./...`, `go test -count=1 ./...`, `make test-integration`, plus `cd web && bun test` and `make vet-svelte` for anything touching the dashboard. Before writing code, skim [`AGENTS.md`](AGENTS.md) โ provider isolation and the Go/Svelte conventions there are enforced by review.
## ๐ Docs
- [`ARCHITECTURE.md`](ARCHITECTURE.md) โ routing, providers, runtime layout
- [`DATABASE.md`](DATABASE.md) โ SQLite schema & operator contract
- [`ROADMAP.md`](ROADMAP.md) โ proposals only, not current behavior
- [`CHANGELOG.md`](CHANGELOG.md) โ release history (Go **v1.9.9**, upstream baseline `decolua/9router` v0.5.85). Work merged since the last tag lives in [`.changes/`](.changes/) and is folded in at the next release; the dashboard's changelog modal shows both.
- [`docs/TELEGRAM_RELEASE_NOTIFICATIONS.md`](docs/TELEGRAM_RELEASE_NOTIFICATIONS.md) โ one-time setup for the release notice posted to Telegram
## Credits
- [9Router](https://github.com/decolua/9router) โ original Next.js gateway & dashboard this Go port preserves compatibility with
## ๐ License
MIT License โ lihat [`LICENSE`](https://github.com/luqman-v1/9router-go/blob/main/LICENSE).