TSF – Giải pháp IT toàn diện cho doanh nghiệp SMB | HCM

P6 - Ultimate 9Router OpenClaw Setup

💻
filename.bash
Step 0:Enable Codex in Chatgpt settings for accounts

Setting => Security => Enable device code author...

Step 1: Install Node/npm and 9router

node -v
npm -v
npm install -g 9router

If node/npm/git is not installed, then:
Install Node 22 LTS:

curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -

sudo apt install nodejs -y

sudo apt install curl git -y

Step 2: Run 9Router

On the VM, execute the command:

9router

Dashboard (for Windows, open directly from this link): http://localhost:20128/dashboard

Since I'm using Ubuntu on a different machine, I will SSH into this Ubuntu AI machine from my machine.
Client: Open CMD and SSH into the AI ​​VM.

ssh -N -L 20128:127.0.0.1:20128 bao@192.168.16.253 (IP VM AI)
Enter pass SSH

Step 3: Connect to the provider in the dashboard

Go to:

• Dashboard → Providers → select Codex / ChatGPT (Codex) (or “Codex”)

• Click Connect → perform OAuth/device login as instructed



It will show the localhost link, copy that link and paste it into the terminal/SSH on the AI ​​machine.

Add the second account 2

Repeat “Connect” again (or “Add account”) to log in with the second account.

Similarly

enable round robin

Create combos : name openai

Step 4: Create the 9router API key

Name the key; it will generate the key automatically.
Key: sk-ccf64851645b8890-g9lfc4-4241aeb4

Test the key on the VM
KEY='sk-ccf64851645b8890-g9lfc4-4241aeb4'
curl -sS -H "Authorization: Bearer $KEY" http://127.0.0.1:20128/v1/models | head -c 400; echo

Step 5: Change the OpenClaw provider openai-codex to the 9Router endpoint

Run the batch command

openclaw config set --batch-json "[
  {\"path\":\"models.mode\",\"value\":\"merge\"},
  {\"path\":\"models.providers.openai-codex\",\"value\":{
    \"baseUrl\":\"http://127.0.0.1:20128/v1\",
    \"api\":\"openai-completions\",
    \"headers\":{\"Authorization\":\"Bearer $KEY\"},
    \"models\":[]
  }}
]"
 
Restart service

systemctl --user restart openclaw-gateway 
 
Validate + restart gateway

openclaw config validate --json
valid: true

systemctl --user restart openclaw-gateway
 
Verify OpenClaw to see the model via 9Router:

openclaw models status –plain

Edit the config file again:
nano ~/.openclaw/openclaw.json

Add /cx to the configuration file.
Save file: Ctrl O enter, Ctrl X exit

If you need to check the regenerated models file:
cat ~/.openclaw/agents/main/agent/models.json | head -n 60

Disabling round robin will automatically switch to the opanai1 account.


Step 6: Create a systemd user service.

#0: Enable CLI Tools Openclaw

#1 Create a unit file:
mkdir -p ~/.config/systemd/user
nano ~/.config/systemd/user/9router.service

Paste the following content (recommended to bind locally-only for safety):

[Unit]
Description=9Router (OpenAI-compatible router dashboard)
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
Environment=PATH=%h/.npm-global/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
WorkingDirectory=%h
ExecStart=%h/.npm-global/bin/9router --host 127.0.0.1 --port 20128 --no-browser --log --skip-update
Restart=on-failure
RestartSec=2

[Install]
WantedBy=default.target
 
Save file

#2 Reload systemd user + enable + start
systemctl --user daemon-reload
systemctl --user enable --now 9router
 
#3 Make openclaw-gateway wait for 9router
mkdir -p ~/.config/systemd/user/openclaw-gateway.service.d

Paste the command exactly as is

cat > ~/.config/systemd/user/openclaw-gateway.service.d/override.conf <<'EOF'
[Unit]
Wants=9router.service
After=9router.service
EOF

Restart service

systemctl --user daemon-reload
systemctl --user restart 9router
systemctl --user restart openclaw-gateway
 
 
#4 Check status + view real-time logs
systemctl --user status 9router --no-pager -l
journalctl --user -u 9router -f
 

#5 Quick Management Commands
systemctl --user restart 9router
systemctl --user stop 9router
systemctl --user start 9router
 

5) Test live endpoint

curl -sS http://127.0.0.1:20128/v1/models | head -c 300; echo

6) (Important) Run automatically after reboot

User services sometimes only run when the user has a session. If you want the service to boot up and run automatically even without SSH access, you need to enable linger (requires sudo):

sudo loginctl enable-linger bao

Step 7: Set up a script to monitor quota by user account

1. Create a bin directory (if it doesn't exist)

mkdir -p ~/bin

2. Create a Python file
nano ~/bin/9router-account-usage.py

(Content: parse ~/.9router/log.txt, remove the PENDING line, add by account, print the column table)

#!/usr/bin/env python3
"""Summarize 9Router usage per account from ~/.9router/log.txt.

Expected log format (from 9Router):
  DD-MM-YYYY HH:MM:SS | <model> | CODEX | <account> | <prompt_tokens or -> | <completion_tokens or -> | <status>

We count only completed (non-PENDING) rows.
"""

from __future__ import annotations

import argparse
import datetime as dt
import os
import re
import sys
from collections import defaultdict

LOG_PATH_DEFAULT = os.path.expanduser("~/.9router/log.txt")

LINE_RE = re.compile(
    r"^(?P<date>\d{2}-\d{2}-\d{4})\s+(?P<time>\d{2}:\d{2}:\d{2})\s+\|\s+"
    r"(?P<model>[^|]+?)\s+\|\s+"
    r"(?P<provider>[^|]+?)\s+\|\s+"
    r"(?P<account>[^|]+?)\s+\|\s+"
    r"(?P<prompt>[^|]+?)\s+\|\s+"
    r"(?P<completion>[^|]+?)\s+\|\s+"
    r"(?P<status>.+?)\s*$"
)


def parse_int(s: str) -> int:
    s = s.strip()
    if s == "-" or s == "":
        return 0
    try:
        return int(s)
    except ValueError:
        return 0


def parse_args() -> argparse.Namespace:
    ap = argparse.ArgumentParser(description="9Router per-account usage table")
    ap.add_argument("--log", default=LOG_PATH_DEFAULT, help=f"Log file path (default: {LOG_PATH_DEFAULT})")
    ap.add_argument("--date", default="today", help="Filter date: YYYY-MM-DD or 'today' (default: today)")
    ap.add_argument("--tz", default=None, help="Timezone name (best-effort). Default: local time on server")
    ap.add_argument("--top", type=int, default=50, help="Max accounts to show (default: 50)")
    return ap.parse_args()


def resolve_date(date_arg: str) -> dt.date:
    if date_arg == "today":
        return dt.date.today()
    return dt.date.fromisoformat(date_arg)


def main() -> int:
    args = parse_args()
    path = os.path.expanduser(args.log)
    if not os.path.exists(path):
        print(f"Log not found: {path}", file=sys.stderr)
        return 2

    target_date = resolve_date(args.date)

    # account -> metrics
    m = defaultdict(lambda: {
        "requests": 0,
        "ok": 0,
        "err": 0,
        "prompt_tokens": 0,
        "completion_tokens": 0,
        "models": defaultdict(int),
        "last": None,
    })

    with open(path, "r", encoding="utf-8", errors="replace") as f:
        for line in f:
            line = line.rstrip("\n")
            if not line.strip():
                continue
            mo = LINE_RE.match(line)
            if not mo:
                continue

            status = mo.group("status").strip()
            if status.upper() == "PENDING":
                continue

            # parse date in log: DD-MM-YYYY
            d_str = mo.group("date")
            t_str = mo.group("time")
            d = dt.datetime.strptime(d_str, "%d-%m-%Y").date()
            if d != target_date:
                continue

            account = mo.group("account").strip()
            model = mo.group("model").strip()
            prompt = parse_int(mo.group("prompt"))
            completion = parse_int(mo.group("completion"))

            rec = m[account]
            rec["requests"] += 1
            if "200" in status or status.upper().startswith("OK") or "OK" in status.upper():
                rec["ok"] += 1
            else:
                rec["err"] += 1
            rec["prompt_tokens"] += prompt
            rec["completion_tokens"] += completion
            rec["models"][model] += 1
            rec["last"] = f"{d_str} {t_str}"

    rows = []
    for acct, rec in m.items():
        total_tokens = rec["prompt_tokens"] + rec["completion_tokens"]
        top_models = ",".join([f"{k}:{v}" for k, v in sorted(rec["models"].items(), key=lambda kv: (-kv[1], kv[0]))[:3]])
        rows.append((
            acct,
            rec["requests"],
            rec["ok"],
            rec["err"],
            rec["prompt_tokens"],
            rec["completion_tokens"],
            total_tokens,
            top_models,
            rec["last"] or "-",
        ))

    rows.sort(key=lambda r: (-r[1], r[0]))
    rows = rows[: args.top]

    # print table
    header = [
        "ACCOUNT",
        "REQ",
        "OK",
        "ERR",
        "PROMPT_TOK",
        "COMP_TOK",
        "TOTAL_TOK",
        "TOP_MODELS",
        "LAST",
    ]

    # compute widths
    widths = [len(h) for h in header]
    for r in rows:
        for i, val in enumerate(r):
            widths[i] = max(widths[i], len(str(val)))

    def fmt_row(vals):
        return "  ".join(str(v).ljust(widths[i]) for i, v in enumerate(vals))

    print(fmt_row(header))
    print("  ".join("-" * w for w in widths))
    for r in rows:
        print(fmt_row(r))

    # totals
    tot_req = sum(r[1] for r in rows)
    tot_prompt = sum(r[4] for r in rows)
    tot_comp = sum(r[5] for r in rows)
    print()
    print(f"TOTAL requests={tot_req} prompt_tokens={tot_prompt} completion_tokens={tot_comp} total_tokens={tot_prompt+tot_comp}")

    return 0


if __name__ == "__main__":
    raise SystemExit(main())
 
Save file: Ctrl O enter, Ctrl X exit

3. Permission
chmod +x ~/bin/9router-account-usage.py 

4. Create a wrapper ~/bin/9router-account-usage for a shorter call.

cat > ~/bin/9router-account-usage <<'SH'
#!/usr/bin/env bash
set -euo pipefail
exec "$HOME/bin/9router-account-usage.py" "$@"
SH
 
Permission
chmod +x ~/bin/9router-account-usage 

5. Test

~/bin/9router-account-usage --date today | head -n 60 

~/bin/9router-account-usage --date today

See also related articles

P10 – Uninstall OpenClaw Windows Fast

P10 – Uninstall OpenClaw Windows Fast https://youtu.be/1ljEMzohiSY 🚀 AI Tutorial – P10: Uninstall OpenClaw on Windows (Clean Removal & Fix Issues) If you’re facing issues with OpenClaw or simply want to remove it completely, performing a proper Uninstall OpenClaw Windows process is essential. A partial uninstall may leave behind background...

Read More

P9 – Build Local AI Telegram Bot Fast (Ollama Guide)

P9 – Build Local AI Telegram Bot Fast (Ollama Guide) https://youtu.be/YuiLJDLIVr0 🚀 AI Tutorial – P9: Create a Local AI Telegram Bot with Ollama in Minutes Building a Local AI Telegram Bot is one of the fastest ways to bring AI into real-world usage. By combining Ollama with a simple...

Read More

P8 – Ultimate OpenClaw Local AI Setup

P8 – Ultimate OpenClaw Local AI Setup 🚀 AI Tutorial – P8: Complete Guide to Running Local AI with Ollama, Qwen & Open WebUI Running openclaw local AI is one of the most powerful ways to build a private, fast, and cost-efficient AI system. By combining Ollama, Qwen models, and...

Read More