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

P5 - Ultimate OpenClaw CLIProxy Setup (Free Multi API)

🐍
filename.py
Step 0) Objectives

• Run CLIProxyAPI locally on VPS: 127.0.0.1:8317

• Add multiple Codex/ChatGPT accounts (device login) → proxy automatically rotates accounts

• OpenClaw points the openai-codex provider to CLIProxyAPI (instead of chatgpt.com)

• CLIProxyAPI runs as a service (systemd user), automatically starts on reboot

Step 1) Install Go 1.22.5

cd /tmp
wget https://go.dev/dl/go1.22.5.linux-amd64.tar.gz

sudo rm -rf /usr/local/go
sudo tar -C /usr/local -xzf go1.22.5.linux-amd64.tar.gz

sudo ln -sf /usr/local/go/bin/go /usr/local/bin/go
sudo ln -sf /usr/local/go/bin/gofmt /usr/local/bin/gofmt

Check the version

which go
go version

Step 2) Clone CLIProxyAPI and build the binary

cd ~
rm -rf CLIProxyAPI
git clone https://github.com/router-for-me/CLIProxyAPI.git

cd ~/CLIProxyAPI
go build -o cli-proxy-api ./cmd/server

./cli-proxy-api -h

Step 3) Create the CLIProxyAPI configuration

Suggested way to create a proxy API key (create using Python):

python3 - <<'PY'
import secrets
print(secrets.token_urlsafe(32))
PY

Key: sMPIdzRqbikhhHZ04QD9DHhvwKo32SbinpkkvVNQ9Mo

nano ~/CLIProxyAPI/config.yaml

Content

host: "127.0.0.1"
port: 8317

tls:
  enable: false
  cert: ""
  key: ""

remote-management:
  allow-remote: false
  secret-key: ""
  disable-control-panel: true
  panel-github-repository: "https://github.com/router-for-me/Cli-Proxy-API-Management-Center"

auth-dir: "/home/bao/.cli-proxy-api"

api-keys:
  - "sMPIdzRqbikhhHZ04QD9DHhvwKo32SbinpkkvVNQ9Mo"

debug: true

usage-statistics-enabled: true

routing:
  strategy: "round-robin"

Save file : Ctrl + O enter, Ctrl X exit
Note: must be in the correct format


Step 5) Add Codex/ChatGPT account Go to CLIProxyAPI (device login)

Enable Codex in Chatgpt settings for accounts
Setting => Security => Enable device code author...

Add account #1
cd ~/CLIProxyAPI
./cli-proxy-api -codex-device-login -config ./config.yaml -no-browser


It will print out:
• URL: https://auth.openai.com/codex/device
• CODE: XXXX-XXXXX

Open the URL on the local computer → enter code → login account #1.

Add account #2 (repeat)

Run the correct command again:
cd ~/CLIProxyAPI

./cli-proxy-api -codex-device-login -config ./config.yaml -no-browser

After success, the proxy will create the auth file at:
ls -la ~/.cli-proxy-api

# There will be 2 codex-*.json files

# Test model list

cd ~/CLIProxyAPI
nohup ./cli-proxy-api -config ./config.yaml > proxy.log 2>&1 &

curl -sS -H "Authorization: Bearer sMPIdzRqbikhhHZ04QD9DHhvwKo32SbinpkkvVNQ9Mo" http://127.0.0.1:8317/v1/models

Step 6) Point OpenClaw to CLIProxyAPI

OpenClaw is using the openai-codex provider. We edit the conf file
Use python to insert files

python3 - <<'PY'
import json

p = "/home/bao/.openclaw/openclaw.json"
obj = json.load(open(p))

obj.setdefault("models", {})
obj["models"]["mode"] = "merge"

providers = obj["models"].setdefault("providers", {})
providers["openai-codex"] = {
  "baseUrl": "http://127.0.0.1:8317/v1",
  "api": "openai-completions",
  "headers": {
    "Authorization": "Bearer sMPIdzRqbikhhHZ04QD9DHhvwKo32SbinpkkvVNQ9Mo"
  },
  "models": []
}

with open(p, "w") as f:
  json.dump(obj, f, indent=2)
  f.write("\n")

print("updated", p)
PY

Validate config
openclaw config validate --json

Should output {"valid":true,...}.

Restart the gateway to regenerate agents/main/agent/models.json
systemctl --user restart openclaw-gateway

Check

cat ~/.openclaw/agents/main/agent/models.json


Step 7) Restart OpenClaw Gateway

systemctl --user restart openclaw-gateway

Then test normal Telegram messaging.

Step 8) Run CLIProxyAPI as a service (run automatically after reboot)

Create service file:
sudo nano ~/.config/systemd/user/cliproxyapi.service

Content

[Unit]
Description=CLIProxyAPI (local OpenAI-compatible proxy)
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
WorkingDirectory=%h/CLIProxyAPI
ExecStart=%h/CLIProxyAPI/cli-proxy-api -config %h/CLIProxyAPI/config.yaml
Restart=on-failure
RestartSec=2
NoNewPrivileges=true
PrivateTmp=true

[Install]
WantedBy=default.target


Enable + start:
systemctl --user daemon-reload
systemctl --user enable --now cliproxyapi.service


Log Follow:

journalctl --user -u cliproxyapi -f

Step 9) Option Note

routing.strategy: What is round-robin, and where is it located?

It's a strategy for selecting accounts/credentials when a proxy has multiple valid accounts for the same model.

• round-robin = alternating (A → B → A → B…)

• fill-first = use up the priority account first, then move on to the next account

It's located in the CLIProxyAPI configuration file:

nano ~/CLIProxyAPI/config.yaml

# Restart proxy
systemctl --user restart cliproxyapi

# Restart gateway
systemctl --user restart openclaw-gateway

Step 10) Enable usage quota checking

#0 Create secret key

Create a secret key (you can use py)
(python creates plaintext key)

python3 - <<'PY'
import secrets
print(secrets.token_urlsafe(32))
PY

Key: rDdnKJu4JYNMnlVp8WRr-7tGgXSBtzMib2c_KQqzs4I

nano ~/CLIProxyAPI/config.yaml

Then restart the service:
systemctl --user restart cliproxyapi

When the service starts, it will automatically hash the secret (bcrypt) and overwrite the secret key in the file.

Restart service:

systemctl --user restart cliproxyapi

#1 Create key management file (to avoid copying the key every time)

echo 'rDdnKJu4JYNMnlVp8WRr-7tGgXSBtzMib2c_KQqzs4I' > ~/.cli-proxy-api/.mgmt_key

chmod 600 ~/.cli-proxy-api/.mgmt_key

#2 Create main Python script:

Create folder

mkdir -p ~/bin

Run python command to write file

cat > ~/bin/cliproxy_stats.py <<'PY'
#!/usr/bin/env python3
import argparse
import json
from datetime import datetime, timezone
try:
    from zoneinfo import ZoneInfo
except Exception:
    ZoneInfo = None
from urllib.request import Request, urlopen

DEFAULT_URL = "http://127.0.0.1:8317/v0/management/usage"
DEFAULT_AUTHFILES_URL = "http://127.0.0.1:8317/v0/management/auth-files"
DEFAULT_KEY_FILE = "/home/bao/.cli-proxy-api/.mgmt_key"

def fetch_json(url: str, key: str) -> dict:
    req = Request(url, headers={"X-Management-Key": key})
    with urlopen(req, timeout=10) as resp:
        body = resp.read().decode("utf-8", errors="replace")
    return json.loads(body)

def iter_details(usage_json: dict):
    apis = usage_json.get("usage", {}).get("apis", {})
    for _api_key, api in apis.items():
        for model, model_info in (api.get("models", {}) or {}).items():
            for det in (model_info.get("details", []) or []):
                yield model, det

def auth_status_map(auth_files_json: dict) -> dict:
    out = {}
    files = auth_files_json.get("files", []) or []
    for f in files:
        email = f.get("email") or f.get("account") or f.get("label")
        if not email:
            continue
        out[email] = {
            "status": f.get("status", ""),
            "disabled": bool(f.get("disabled", False)),
        }
    return out

def print_table(rows, headers, limit=None):
    if limit is not None:
        rows = rows[-limit:]
    widths = []
    for i, h in enumerate(headers):
        w = len(h)
        for r in rows:
            w = max(w, len(str(r[i])))
        widths.append(w)
    fmt = "  ".join("{:<%d}" % w for w in widths)
    print(fmt.format(*headers))
    print(fmt.format(*["-" * w for w in widths]))
    for r in rows:
        print(fmt.format(*[str(x) for x in r]))

def cmd_recent(usage: dict, n: int):
    rows = []
    for model, det in iter_details(usage):
        t = det.get("tokens", {}) or {}
        rows.append([
            det.get("timestamp", ""),
            model,
            det.get("source", ""),
            det.get("latency_ms", ""),
            t.get("input_tokens", 0) or 0,
            t.get("output_tokens", 0) or 0,
            t.get("cached_tokens", 0) or 0,
            t.get("total_tokens", 0) or 0,
            "Y" if det.get("failed") else "N",
        ])
    rows.sort(key=lambda r: r[0])
    headers = ["time", "model", "account", "lat_ms", "in_tok", "out_tok", "cached", "total", "fail"]
    print_table(rows, headers, limit=n)

def cmd_today(usage: dict, authfiles: dict, tz_name: str = "Asia/Ho_Chi_Minh"):
    tz = ZoneInfo(tz_name) if ZoneInfo else timezone.utc
    today = datetime.now(tz).date().isoformat()
    status = auth_status_map(authfiles)

    agg = {}
    for model, det in iter_details(usage):
        ts = det.get("timestamp", "")
        if not ts.startswith(today):
            continue
        acct = det.get("source", "unknown")
        t = det.get("tokens", {}) or {}
        a = agg.setdefault(acct, {"req": 0, "in": 0, "out": 0, "cached": 0, "total": 0, "fail": 0})
        a["req"] += 1
        a["in"] += int(t.get("input_tokens", 0) or 0)
        a["out"] += int(t.get("output_tokens", 0) or 0)
        a["cached"] += int(t.get("cached_tokens", 0) or 0)
        a["total"] += int(t.get("total_tokens", 0) or 0)
        a["fail"] += 1 if det.get("failed") else 0

    print(f"date({tz_name}) = {today}")
    accounts = sorted(status.keys() | agg.keys())
    rows = []
    for acct in accounts:
        v = agg.get(acct, {"req": 0, "in": 0, "out": 0, "cached": 0, "total": 0, "fail": 0})
        s = status.get(acct, {})
        st = s.get("status", "")
        dis = "Y" if s.get("disabled") else "N"
        rows.append([acct, st, dis, v["req"], v["in"], v["out"], v["cached"], v["total"], v["fail"]])

    headers = ["account", "status", "disabled", "req", "in_tok", "out_tok", "cached", "total", "fail"]
    print_table(rows, headers)

def main():
    ap = argparse.ArgumentParser(description="CLIProxyAPI management usage viewer")
    ap.add_argument("--url", default=DEFAULT_URL)
    ap.add_argument("--key-file", default=DEFAULT_KEY_FILE)
    sub = ap.add_subparsers(dest="cmd", required=True)

    p1 = sub.add_parser("recent", help="show last N requests")
    p1.add_argument("-n", type=int, default=40)

    p2 = sub.add_parser("today", help="show today's totals per account")
    p2.add_argument("--tz", default="Asia/Ho_Chi_Minh")

    args = ap.parse_args()
    with open(args.key_file, "r", encoding="utf-8") as f:
        key = f.read().strip()

    usage = fetch_json(args.url, key)
    authfiles = fetch_json(DEFAULT_AUTHFILES_URL, key)

    if args.cmd == "recent":
        cmd_recent(usage, args.n)
    else:
        cmd_today(usage, authfiles, tz_name=args.tz)

if __name__ == "__main__":
    main()
PY

Permissions

chmod +x ~/bin/cliproxy_stats.py

#3 Create a short bash wrapper: ~/bin/cliproxy-stats

cat > ~/bin/cliproxy-stats << 'SH'
#!/usr/bin/env bash
set -euo pipefail
exec /home/bao/bin/cliproxy_stats.py "$@"

SH

Permissions

chmod +x ~/bin/cliproxy-stats

#4 Execute

• View today's total + which account is active/disabled:

~/bin/cliproxy-stats today

• View recent requests running on which account (to ensure it "fill-first" correctly):

~/bin/cliproxy-stats recent -n 30

In conclusion: If you want it to be faster, just copy and paste my steps and let the AI ​​do it automatically.

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