9Router adalah local proxy yang sit between AI coding tools (Claude Code, Cursor, Codex) dan provider LLM. Dengan token saver, auto-fallback, multi-account round-robin. Tutorial ini cover full lifecycle dari install sampai production rotation.
Setelah selesai tutorial ini: (1) 9router jalan sebagai systemd service di server lo, auto-restart, (2) minimal 1 provider terhubung (Kiro/OpenRouter/Xiaomi), (3) Hermes Agent atau tool lain sudah route request lewat 9router, (4) ngerti cara tambah provider baru, rotasi credential, dan troubleshoot kalau ada yang mati.
Install via npm → jalanin di server pakai 9router -p 20128 --no-browser -t -l → dashboard di localhost:20128/dashboard (password default 123456) → add provider (Kiro, Xiaomi, dll) → setup proxy if needed → connect AI tools ke localhost:20128/v1. Multi-account = sticky session + round-robin via dashboard.
9Router butuh Node.js 18+. Install via npm sebagai global package, jalanin sebagai background daemon biar tetap up.
# Prerequisite: Node.js 18+
$ node --version
v20.10.0
# Install 9router globally
$ npm install -g 9router
# Verify install
$ 9router --version
0.4.71
Jalankan sebagai daemon (headless mode wajib di server, biar gak exit langsung):
# Production: tray mode + log + skip auto-update
$ 9router -p 20128 --no-browser -t -l --skip-update
# Alternative (launcher dengan spinner — bisa hang di non-TTY)
$ 9router serve --port 20128 --no-open --daemon
Tanpa flag -t (tray mode), proses 9router exit dalam beberapa detik dengan pesan "Exiting...". Di server headless WAJIB pake -t. 9router start juga BUKAN subcommand valid — flag-only.
serve --daemon hang
Subcommand 9router serve --daemon pake interactive spinner — bisa hang kalau dijalanin di non-TTY (CI, ssh tanpa pty). Gunakan flag mode (9router -p ... -t) atau systemd unit (lihat bawah). Server forknya tetep jalan walaupun launcher exit 137.
CLI flags reference (verified v0.4.71):
-p, --port <port> # Port (default: 20128)
-H, --host <host> # Bind host (default: 0.0.0.0)
-n, --no-browser # Don't auto-open browser
-l, --log # Show server logs
-t, --tray # Tray/background mode (REQUIRED for headless)
--skip-update # Skip auto-update check
-v, --version # Show version
Verify running:
$ ss -tlnp | grep 20128
LISTEN 0 511 0.0.0.0:20128 *:* users:(("node",pid=12345))
# Health check (no auth needed)
$ curl -s http://localhost:20128/api/health
{"ok":true}
# Models endpoint (no auth, list available models)
$ curl -s -o /dev/null -w "HTTP %{http_code}\n" http://localhost:20128/v1/models
HTTP 200
Kalau klien return "HTTP 404: No active credentials for provider" atau model_not_found walaupun connection udah ada — biasanya 9router-nya yang mati. Check dulu ss -tlnp | grep 20128. Kalau gak listening, start service. Jangan langsung utak-atik connection.
Buka dashboard: http://your-server:20128/dashboard (default password: 123456). Ganti password di Settings.
Troubleshooting install (kalau npm install -g gagal):
# Error: EACCES permission denied → npm prefix gak writable
$ mkdir -p ~/.npm-global
$ npm config set prefix ~/.npm-global
$ echo 'export PATH=~/.npm-global/bin:$PATH' >> ~/.bashrc
$ source ~/.bashrc
# Error: node version < 18 → upgrade via nvm
$ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
$ nvm install 20 && nvm use 20
# Error: package not found → registry config corrupted
$ npm config set registry https://registry.npmjs.org/
$ npm cache clean --force
Production deployment via systemd (auto-restart, log managed):
# /etc/systemd/system/9router.service
[Unit]
Description=9Router AI Router
After=network.target
[Service]
Type=simple
User=root
Environment=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
ExecStart=/usr/bin/9router -p 20128 -n --skip-update
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target
Di bawah systemd, lo gak butuh -t — Restart=always yang jaga proses hidup. Flag --skip-update penting biar gak kena auto-update glitch di production. Kalau 9router binary lo bukan di /usr/bin/, cari dulu: which 9router dan adjust ExecStart.
$ systemctl daemon-reload
$ systemctl enable --now 9router
$ sleep 5
$ curl -s -o /dev/null -w "HTTP %{http_code}\n" http://localhost:20128/v1/models
HTTP 200 # confirm alive
pkill -f 9router kills SSH session
Kalau lo mau clean up proses manual sebelum start systemd, pkill -f '9router' bisa match argv SSH session lo sendiri dan abort koneksi (exit 255). Solusi: systemctl start 9router di SSH session terpisah setelah pkill, atau kill by PID spesifik (kill $(ss -tlnp | grep 20128 | grep -oP 'pid=\K\d+')).
Proxy dipake biar request ke provider LLM lewat IP residential (anti rate-limit, bypass geo-block). Pake hasil dari tutorial T-001.
http://user:pass@gateway:port# Format yang accepted di 9router proxy field
http://user:pass@rp.scrapegw.com:PORT
http://user-session-abc123-lifetime-10:pass@rp.scrapegw.com:PORT
socks5://user:pass@rp.scrapegw.com:PORT
Pake sticky session ID per provider biar IP konsisten, less ban risk. Untuk rotation aggressive, ganti session di setiap restart 9router.
9Router support 40+ provider. Setiap provider butuh API key sendiri. Mulai dari satu dulu untuk test.
Add provider via dashboard:
Kalau API key lo prefix tp- (token plan), pake provider type "Xiaomi MiMo (Token Plan)", BUKAN regular "Xiaomi MiMo". Salah pilih = 401 Invalid API Key.
Kiro punya 2 auth yang BEDA total dan gak interchangeable:
ksk_*) — buat CLI headless mode ONLY. ksk_* TIDAK bisa dipake di 9router.Cara dapet refresh token: buka Kiro IDE desktop → Inspect Element → Application → Cookies → kiro.dev → copy value refresh_token. Web portal (app.kiro.dev) TIDAK simpan refresh_token di cookies — cuma session cookies. Kiro CLI (~/.local/share/kiro-cli/data.sqlite3) simpan token tapi itu SSO DeviceCode flow — BERBEDA, 9router reject dengan "Bad credentials".
Dashboard → Providers → Kiro AI → Add Connection punya 3 opsi:
Setelah 9router jalan, configure AI tool (Claude Code, Cursor, Codex, Hermes Agent) buat point ke 9router, BUKAN langsung ke provider.
Get auth token:
$ cat ~/.9router/auth/* | head -c 64
a3f2b1c9d8e7f6a5b4c3d2e1f0a9b8c7d6e5f4a3b2c1d0e9f8a7b6c5d4e3f2a1
Hermes Agent (~/.hermes/config.yaml):
model:
default: kr/claude-sonnet-4
provider: 9router
providers:
9router:
api_key: your-9router-auth-token # dari ~/.9router/auth/
api_mode: chat_completions
base_url: http://localhost:20128/v1
default_model: kr/claude-sonnet-4
Claude Code / Cursor / Codex: set environment variable atau config file:
$ export ANTHROPIC_BASE_URL="http://localhost:20128/v1"
$ export ANTHROPIC_API_KEY="your-9router-auth-token"
Setiap provider pake prefix model: kr/ Kiro, xmtp/ Xiaomi (Token Plan), cc/ Claude Code, or/ OpenRouter. Cek list lengkap real-time di dashboard → Models.
Available Kiro models (verified Jun 2026, prefix kr/):
# Opus tier (2.2x credit cost)
kr/claude-opus-4.8
kr/claude-opus-4.7
kr/claude-opus-4.6
kr/claude-opus-4.5
# Sonnet tier (default)
kr/claude-sonnet-4.6
kr/claude-sonnet-4
kr/claude-sonnet-4.5
# Haiku (fastest, cheapest)
kr/claude-haiku-4.5
# Non-Anthropic Kiro routes
kr/deepseek-3.2
kr/qwen3-coder-next
kr/glm-5
kr/minimax-m2.5
# Variants tersedia untuk semua model di atas:
# -thinking → reasoning mode
# -agentic → tool-calling optimized
# -thinking-agentic → both
Available Xiaomi MiMo models (prefix xmtp/ untuk Token Plan):
xmtp/mimo-v2.5-pro
xmtp/mimo-v2.5-pro-claude
xmtp/mimo-v2-pro
xmtp/mimo-v2.5
Untuk multi-account ops, lo butuh banyak proxy yang assigned ke setiap connection. Pake proxies.txt dari tutorial T-001.
Cara 1 · Dashboard (recommended): 9router punya import proxy bawaan, gak perlu sentuh DB.
http://user:pass@host:portCara 2 · Inject SQLite (advanced/bulk): kalau butuh ratusan sekaligus, inject langsung ke tabel proxyPools. Tiap row nyimpen konfigurasi sebagai JSON di kolom data.
# WAJIB backup DB dulu sebelum modify
$ cp ~/.9router/db/data.sqlite ~/.9router/db/data.sqlite.bak
$ python3 inject_proxies.py
# inject_proxies.py — schema asli 9router v0.4.71
import sqlite3, json, uuid
from pathlib import Path
from datetime import datetime, timezone
db = sqlite3.connect(Path.home() / ".9router/db/data.sqlite")
proxies = Path("proxies.txt").read_text().strip().split("\n")
now = datetime.now(timezone.utc).isoformat()
for proxy in proxies:
# proxyPools: id, isActive, testStatus, data(JSON), createdAt, updatedAt
data = json.dumps({
"name": f"Imported {proxy.split('@')[-1]}",
"proxyUrl": proxy,
"noProxy": "",
"type": "http",
"strictProxy": False,
})
db.execute(
"INSERT INTO proxyPools (id, isActive, data, createdAt, updatedAt) VALUES (?, 1, ?, ?, ?)",
(str(uuid.uuid4()), data, now, now)
)
db.commit()
print(f"Injected {len(proxies)} proxies")
9router cache config di memory. INSERT ke SQLite gak langsung kebaca. WAJIB restart biar DB di-reload: systemctl restart 9router (atau kill PID lalu start ulang). Verifikasi schema asli dulu sebelum inject: sqlite3 ~/.9router/db/data.sqlite ".schema proxyPools". Kolom bisa beda antar versi.
Kalau setelah inject 9router gagal start atau proxy gak kebaca, restore dari backup yang lo bikin tadi — jangan panik edit manual:
# Stop dulu biar gak nulis ke DB pas restore
$ systemctl stop 9router
# Restore backup (overwrite DB corrupt)
$ cp ~/.9router/db/data.sqlite.bak ~/.9router/db/data.sqlite
# Verify integritas DB sebelum start lagi
$ sqlite3 ~/.9router/db/data.sqlite "PRAGMA integrity_check;"
ok
$ systemctl start 9router
Penting dipisah dua konsep yang sering ketuker:
providerConnections.combos (id, name, kind, models, ...). Combo gak nyimpen API key/proxy/quota. Itu murni model grouping.Step 1 · Add Connection per akun (Dashboard → Providers → [Provider] → Add Connection):
# Multi-akun Xiaomi MiMo: 3 connection di provider yang sama
Connection #1
Name: "luke-account-1"
API Key: tp-xxxxxxxxxxxx # cluster-specific key
Proxy: http://user-s1:pass@gateway:6060 # optional, per-connection
Priority: 1
isActive: true
Connection #2
Name: "luke-account-2"
API Key: tp-yyyyyyyyyyyy
Proxy: http://user-s2:pass@gateway:6060
Priority: 2
Connection #3
Name: "luke-account-3"
API Key: tp-zzzzzzzzzzzz
Proxy: http://user-s3:pass@gateway:6060
Priority: 3
Combo strategy (STEP 07) yang nentuin gimana 3 connection ini diputerin: fallback = primary dulu, drop kalau limit. round-robin = spread rata. sticky = session nempel.
Step 2 · Create Combo (Dashboard → Combos → New Combo): grouping model yang mau dipake klien.
# Schema asli: combos (id, name, kind, models)
# Data real dari DB live:
Combo: "KIRO"
models: [
"kr/claude-opus-4.8",
"kr/claude-opus-4.7",
"xmtp/mimo-v2.5-pro-claude"
]
Combo: "XIAOMI"
models: [
"xmtp/mimo-v2.5-pro",
"xmtp/mimo-v2-pro",
"xmtp/mimo-v2.5"
]
Klien (Claude Code, Cursor, dll) tinggal hit nama combo sebagai model. 9router otomatis pilih connection & model dari list itu sesuai strategy yang lo set.
Banyak yang nyari "field API key di combo", itu gak ada. API key & proxy di-attach ke connection, bukan combo. Combo cuma nge-list model. Quota tracking pun di-track per connection (ada di providerConnections.data field modelLock_*, backoffLevel, lastUsedAt), bukan per combo.
9Router punya 3 combo strategy buat distribute traffic antar connection (Settings → Combo Strategy):
# Settings → Combo Strategy (nilai asli di dashboard)
1. fallback → Pake combo utama, pindah ke berikutnya kalau gagal/limit (default)
2. round-robin → Cycle equal antar combo tiap request
3. sticky → Satu session nempel ke satu combo terus, anti-fingerprint
Pattern recommended untuk production:
Per-provider bisa di-override sendiri lewat providerStrategies (tiap provider punya fallbackStrategy masing-masing).
Aktifin di Settings → RTK. 9router bakal auto-compress tool_result output (git diff, grep, ls) sebelum kirim ke provider. Saving 20-40% token, free win.
Monitoring rotation: endpoint /api/* butuh session login (bukan curl polos, bakal 401). Buat health check pake /v1/models yang open, dan liat stats per-combo di dashboard → Usage.
# Health check (open, no auth) — konfirmasi router hidup + list model
$ curl -s -o /dev/null -w "HTTP %{http_code}\n" http://localhost:20128/v1/models
HTTP 200
# Stats detail (requests per combo, token saved, latency)
# → buka dashboard: http://localhost:20128/dashboard → tab Usage
# /api/usage/stats butuh cookie session (login dulu), gak bisa curl langsung
Buat lo yang mau bikin tools, dashboard, atau toggle automation di atas 9router — endpoint dan HTTP method-nya gak konsisten. Salah method = HTTP 405 diam-diam (toggle muncul gak respond, gak ada error message yang jelas).
Auth matrix (verified v0.4.71):
GET /api/health → no auth, returns {"ok":true}
GET /v1/models → no auth, list available models
POST /v1/chat/completions → Bearer token (dari ~/.9router/auth/)
/api/* (semua lain) → session cookie (dashboard login required)
HTTP method per endpoint (toggle/CRUD):
# Resources — pake PUT (BUKAN PATCH!)
PUT /api/providers/:id # PATCH → 405
PUT /api/keys/:id # PATCH → 405
PUT /api/proxy-pools/:id # PATCH → 405
# Settings — pengecualian, pake PATCH
PATCH /api/settings # POST → 405
# Combos — full RESTful
GET/PUT/POST/DELETE /api/combos/:id
Symptom paling sering: toggle switch di custom dashboard gak jalan, error toast nampilin HTTP 405. Penyebab 9 dari 10 kali = pake api.patch() padahal harus api.put(). PUT {isActive: false} body-nya di-MERGE bukan replace, jadi aman buat partial update — gak perlu round-trip full record.
Quick verification sebelum wiring UI baru — selalu probe method dulu:
$ TOKEN=*** < ~/.9router/auth/$(ls ~/.9router/auth/ | head -1))
$ curl -s -o /dev/null -w "%{http_code}\n" \
-X PATCH http://localhost:20128/api/providers/<id> \
-H "Cookie: auth_token=$TOKEN" \
-H "Content-Type: application/json" \
-d '{"isActive":false}'
405
$ curl ... -X PUT ... # 200
Kalau lo prefer container atau lagi setup di environment yang gak ada Node.js, pake Docker image resmi:
$ docker run -d --name 9router \
-p 20128:20128 \
-v "$HOME/.9router:/app/data" \
-e DATA_DIR=/app/data \
decolua/9router:latest
# Verify
$ docker logs 9router --tail 20
$ curl -s http://localhost:20128/api/health
{"ok":true}
Volume ~/.9router di-mount biar config (DB, auth tokens, OAuth connections) persist across container restart. Untuk update: docker pull decolua/9router:latest lalu docker rm -f 9router dan rerun. Data aman selama volume gak ke-delete.
Mostly aku belajar from production scars. Lo gak perlu repeat.
Tanpa -t flag, 9router exit dalam detik di headless server. Selalu pake: 9router -p 20128 --no-browser -t -l
Auth token yang dipake AI tools BUKAN dashboard password (123456). Token hex ada di ~/.9router/auth/, panjang ~64 chars.
Sejak v0.4.71, OAuth connection (Kiro, dll) persisten di tabel providerConnections. refreshToken/clientId/clientSecret survive restart, dan accessToken auto-refresh waktu expired. Kalau connection beneran hilang setelah restart, kemungkinan besar ke-delete (bukan lost). Verify langsung di DB: sqlite3 ~/.9router/db/data.sqlite "SELECT id, providerName FROM providerConnections;"
Default port sama persis. Gak bisa run dua-duanya. Uninstall OmniRoute dulu atau pakai port berbeda: 9router -p 20129 ...
Token dari ~/.local/share/kiro-cli/data.sqlite3 GAK akan work di 9router Import Token. CLI pake AWS SSO DeviceCode flow, 9router butuh desktop IDE refresh token. Solusi: pake AWS Builder ID OAuth flow di dashboard.
Multi-account masih kena 429 dari Kiro/provider kalau ramean. Set delay antar request (Settings → Throttle) atau gunakan sticky session lebih lama biar kelihatan natural.
9router cache config di memory. Setelah INSERT/UPDATE/DELETE langsung ke SQLite, WAJIB systemctl restart 9router biar memory cache reload. Path zero-restart yang supported: dashboard UI atau API call lewat session cookie/JWT.
AWS IAM Identity Center pake initial password buat first login → AWS minta change password → password baru WAJIB beda dari initial. Reuse = "Password not valid". Subsequent login pake new password + TOTP. Kalau add multiple Kiro accounts, sign out AWS SSO portal antar akun (cookie session HTTPOnly persist dan diam-diam approve under wrong user).
Token dari Kiro CLI sqlite3 atau dari web portal (app.kiro.dev) GAK valid buat "Import Token" di 9router. Cuma refresh_token dari Kiro IDE desktop Electron yang work. Open Kiro IDE → Inspect → Application → Cookies → kiro.dev → copy refresh_token.