TUTORIAL T-002 · AI ROUTER · INFRA

Cara Memakai 9Router

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.

PORT: 20128 VERSION: 0.4.71 STEPS: 09 LEVEL: Intermediate
— YANG LO DAPET DI AKHIR

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.

— TL;DR

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.

STEP 01

Setup & Install di Server

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
— PITFALL: Process exit "Exiting..."

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.

— PITFALL: 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
— PITFALL: Symptom "9router mati" disamarkan jadi error connection

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
— NOTE

Di bawah systemd, lo gak butuh -tRestart=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
— PITFALL: 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+')).

STEP 02

Setup Proxy di 9Router

Proxy dipake biar request ke provider LLM lewat IP residential (anti rate-limit, bypass geo-block). Pake hasil dari tutorial T-001.

  1. Dashboard → Settings → Proxy
  2. Enable "Use proxy for outbound requests"
  3. Pilih protocol: HTTP atau SOCKS5
  4. Paste proxy URL format: http://user:pass@gateway:port
  5. Test connection → Save
# 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
— TIPS

Pake sticky session ID per provider biar IP konsisten, less ban risk. Untuk rotation aggressive, ganti session di setiap restart 9router.

STEP 03

Import API Key (Single Provider)

9Router support 40+ provider. Setiap provider butuh API key sendiri. Mulai dari satu dulu untuk test.

Add provider via dashboard:

  1. Dashboard → Providers+ Add Provider
  2. Pilih provider (e.g. Xiaomi MiMo, Kiro AI, OpenRouter, dll)
  3. Pilih connection type: API Key atau OAuth
  4. Paste API key, beri name (label), save
— PITFALL: Xiaomi MiMo

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.

— PITFALL: Kiro AI auth

Kiro punya 2 auth yang BEDA total dan gak interchangeable:

  • API Key (ksk_*) — buat CLI headless mode ONLY. ksk_* TIDAK bisa dipake di 9router.
  • OAuth Refresh Token — dari Kiro IDE desktop (Electron). Ini yang dipake 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".

— KIRO: 3 auth method di dashboard

Dashboard → Providers → Kiro AI → Add Connection punya 3 opsi:

  • AWS Builder ID — free tier, bikin akun baru via OAuth device flow
  • AWS IAM Identity Center — enterprise SSO, butuh start URL + region
  • Import Token — paste refresh_token dari Kiro IDE desktop (Electron cookies)
STEP 04

Bikin Command Setup untuk AI Agent

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"
— MODEL PREFIX

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
STEP 05

Import Bulk Proxy

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.

  1. Dashboard → Proxy PoolsImport
  2. Paste list proxy (satu URL per baris): http://user:pass@host:port
  3. Save → test connection per proxy (status kelihatan di list)

Cara 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")
— PITFALL: Direct write di-ignore tanpa restart

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.

— ROLLBACK kalau inject corrupt

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
STEP 06

Setup Connections & Combos

Penting dipisah dua konsep yang sering ketuker:

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.

— PITFALL: Combo ≠ Connection

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.

STEP 07

Rotate Proxy & Round-Robin Strategy

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:

  1. fallback sebagai primary: subscription dulu, drop ke cheap/free kalau kena limit
  2. round-robin kalau punya banyak akun setara & mau spread rata (anti-rate-limit)
  3. sticky per chat session biar context konsisten dari satu provider

Per-provider bisa di-override sendiri lewat providerStrategies (tiap provider punya fallbackStrategy masing-masing).

— RTK TOKEN SAVER

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
STEP 08

API Surface & HTTP Method Matrix

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
— PITFALL: PATCH vs PUT

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
STEP 09

Docker Alternative

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}
— TIPS

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.

— COMMON PITFALLS

Things That Break

Mostly aku belajar from production scars. Lo gak perlu repeat.

Process exit langsung

Tanpa -t flag, 9router exit dalam detik di headless server. Selalu pake: 9router -p 20128 --no-browser -t -l

Auth token salah

Auth token yang dipake AI tools BUKAN dashboard password (123456). Token hex ada di ~/.9router/auth/, panjang ~64 chars.

OAuth connection "hilang" setelah restart

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;"

Port 20128 conflict dengan OmniRoute

Default port sama persis. Gak bisa run dua-duanya. Uninstall OmniRoute dulu atau pakai port berbeda: 9router -p 20129 ...

Kiro CLI refresh_token ≠ IDE refresh_token

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.

429 rate-limit dari upstream

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.

Direct SQLite write di-ignore tanpa restart

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.

Initial password ≠ new password (AWS IAM IDC)

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).

"Bad credentials" waktu Import Token (Kiro)

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.