Deploy https://github.com/router-for-me/CLIProxyAPI to a remote Linux server and expose an OpenAI-compatible interface (/v1/chat/completions、/v1/models). Compatible with OAuth subscription accounts for Codex / Claude / Gemini / Qwen / iFlow / Antigravity.
Every "must X" clause comes with a fallback path; every "done" conclusion must be based onthis session'scommand output (W-16).
0. Pre-flight confirmation (must ask)
Must confirm before starting:
- SSH target:
root@HOST, whether passwordless login works - Deployment scope: allowed directories for writing to disk (default
/root/CLIProxyAPI+/root/.cli-proxy-api) - Service port: default
8317 - Access policy:
localhost only(host=127.0.0.1, via SSH tunnel)公网直连(requires UFW allow rule + must change to a random key)
- Login path:
- A. Remote OAuth (requires SSH port forwarding + cooperation from a local browser, 5-minute timeout window)
- B. Log in locally then sync
~/.cli-proxy-api/*.jsonto the VPS (more reliable, recommended)
Fallback path: if the user does not specify, use 公网直连 + 本地登录同步by default, and rotate to a random key immediately after startup.
1. Remote environment check (read-only)
ssh -i <KEY> <SSH_TARGET> "uname -a && . /etc/os-release 2>/dev/null; \
command -v go && go version || echo 'NO_GO'; \
command -v git || echo 'NO_GIT'; \
command -v ufw && ufw status | head -n 20 || echo 'NO_UFW'; \
ss -ltn | grep ':<PORT>' || echo 'PORT_FREE'"
Determine:
- No Go → install in step 2
- Port in use → confirm whether it is an old process, do not blindly kill
- UFW active → record current rules (to avoid changing other ports after adding the allow rule)
2. Install Go + Git (Ubuntu/Debian)
ssh -i <KEY> <SSH_TARGET> "apt-get update && apt-get install -y golang-go git"
Note:
- The project
go.modmay require Go 1.26+, and the Go version in the system apt is too oldThat is fine ——go runwill automatically download the toolchain. - If you do not want GOTOOLCHAIN to auto-download, install the official tarball to
/usr/local/go, andexport PATH=/usr/local/go/bin:$PATH。
3. Clone the repository
ssh -i <KEY> <SSH_TARGET> "rm -rf /root/CLIProxyAPI && \
git clone https://github.com/router-for-me/CLIProxyAPI.git /root/CLIProxyAPI && \
cd /root/CLIProxyAPI && git rev-parse --short HEAD"
4. Minimal configuration
Copy config.example.yaml → config.yaml, adjust only these items:
host: "" # 公网直连;仅 localhost 改 "127.0.0.1"
port: 8317
auth-dir: "~/.cli-proxy-api"
api-keys:
- "PLACEHOLDER_WILL_ROTATE" # 启动前后立即用第 8 步轮换
debug: false
proxy-url: ""
Run remotely:
ssh -i <KEY> <SSH_TARGET> "cd /root/CLIProxyAPI && \
test -f config.yaml || cp config.example.yaml config.yaml"
5. Obtain authentication credentials (choose one)
5A: Local login → sync credentials (recommended)
Run locally (requires local source code or an already compiled cli-proxy-api):
go run ./cmd/server -codex-login -config config.yaml
# 或 -claude-login / -login(Gemini) / -qwen-login / -iflow-login / -antigravity-login
On success, credentials land in ~/.cli-proxy-api/<provider>-<email>.json。
Sync to the VPS:
ssh -i <KEY> <SSH_TARGET> 'mkdir -p /root/.cli-proxy-api'
scp -i <KEY> ~/.cli-proxy-api/<provider>-*.json <SSH_TARGET>:/root/.cli-proxy-api/
ssh -i <KEY> <SSH_TARGET> 'ls -l /root/.cli-proxy-api/'
5B: Remote OAuth (alternative when nothing is available locally)
Initiate remotely:
ssh -tt -i <KEY> <SSH_TARGET> \
"cd /root/CLIProxyAPI && /usr/bin/go run ./cmd/server -<provider>-login -no-browser -config config.yaml"
Key points:
- Callback port is fixed:Codex=1455,Claude=54545,iFlow=11451,Gemini=8085
- Open a tunnel in a new local terminal (same port):
ssh -i <KEY> -L <CALLBACK_PORT>:127.0.0.1:<CALLBACK_PORT> <SSH_TARGET> - Open the authorization URL printed by the remote in the local browser, complete authorization → automatically call back to the remote
- The program has a 5-minute timeout, so have both the tunnel and the browser ready before starting
- If the automatic callback fails: paste the entire
http://localhost:<PORT>/...URL that the browser finally redirects to back into the remote interactive prompt
Fallback path: if 5B fails twice, switch to 5A and stop trying.
6. Start the service (in the background)
ssh -i <KEY> <SSH_TARGET> "cd /root/CLIProxyAPI && \
nohup /usr/bin/go run ./cmd/server -config config.yaml \
>/root/CLIProxyAPI/cli-proxy-api.log 2>&1 </dev/null & \
echo \$! > /root/CLIProxyAPI/cli-proxy-api.pid"
Verify listening and logs:
ssh -i <KEY> <SSH_TARGET> "lsof -i:<PORT>; tail -n 40 /root/CLIProxyAPI/cli-proxy-api.log"
Must see:API server started successfully on: :<PORT> + N auth files ≥ 1。
7. Allow through the firewall (for direct public access)
UFW denies by default deny incoming, even if the port is listening, the public network will still Empty reply from server。This is the most common root cause of direct public access failure。
ssh -i <KEY> <SSH_TARGET> "ufw allow <PORT>/tcp && ufw status | grep <PORT>"
For localhost-only mode,skipthis step and keep host: "127.0.0.1"。
8. Rotate the access key (strongly recommended)
Never keep the your-api-key-1 default value. Generate a random key:
python3 -c "import secrets; print('cpa_' + secrets.token_urlsafe(24))"
On the remote side, use Python to replace the api-keys: section in place (to avoid sed multi-line pitfalls):
ssh -i <KEY> <SSH_TARGET> "python3 - <<'PY'
from pathlib import Path
cfg = Path('/root/CLIProxyAPI/config.yaml')
new_key = '<NEW_KEY>'
lines = cfg.read_text().splitlines()
out, in_api = [], False
for line in lines:
s = line.strip()
if s.startswith('api-keys:'):
out += ['api-keys:', f' - \"{new_key}\"']
in_api = True
continue
if in_api:
if line.startswith(' - ') or not s or line.startswith(' '):
continue
in_api = False
out.append(line)
cfg.write_text('\n'.join(out) + '\n')
PY"
CLIProxyAPI supports config hot reload (see service.go file watcher), the log will show config file changed, reloading,no restart needed. If hot reload does not succeed, then manually pkill -f 'cmd/server -config' and restart per step 6.
9. Health check (must have command evidence from this session)
9a. Internal network (not affected by UFW)
ssh -i <KEY> <SSH_TARGET> "curl -fsS -H 'Authorization: Bearer <NEW_KEY>' \
http://127.0.0.1:<PORT>/v1/models | head -c 400"
9b. Direct public access
curl -sS -i --max-time 30 http://<HOST>:<PORT>/v1/chat/completions \
-H 'Authorization: Bearer <NEW_KEY>' -H 'Content-Type: application/json' \
--data '{"model":"<MODEL>","messages":[{"role":"user","content":"请只回复 OK"}],"stream":false}'
Must satisfy:
- HTTP 200
choices[0].message.contentcontains readable characters (non-empty, non-error)
If any condition is not met, you must not claim "deployment complete".
10. Client environment variables
Append to ~/.zshrc(or ~/.bashrc):
# CLIProxyAPI
export BASE_URL="http://<HOST>:<PORT>/v1" # localhost 模式改 http://127.0.0.1:<PORT>/v1
export BASE_API_KEY="<NEW_KEY>"
export BASE_MODEL="<MODEL>" # 如 gpt-5.4 / claude-sonnet-4-5-...
Then:source ~/.zshrc。
Troubleshooting quick reference
| Symptom | Most likely root cause | Verification / fix |
|---|---|---|
Public Empty reply from server, but remote 127.0.0.1 is reachable |
UFW denies by default, port not allowed | ufw status → ufw allow <PORT>/tcp |
Public Connection refused |
service host bound to 127.0.0.1, or the process is not running |
Check whether lsof -i:<PORT> is *:<PORT>, or change the host and restart |
OAuth Authentication timed out |
Browser authorization + tunnel not completed within 5 minutes | Switch to 5A local login sync |
OAuth stuck at exchanging for tokens |
Local machine to auth.openai.com / claude.ai is proxied or blocked |
Clear proxy-url,curl -I https://auth.openai.com/oauth/token and verify |
Startup log 0 auth files |
auth-dir path is wrong or file permissions are insufficient |
Check ~/.cli-proxy-api actual resolved path and file owner |
/v1/models 401 |
key is not using the current config.yaml or hot reload did not take effect |
grep -nA2 '^api-keys:' config.yaml, restart if necessary |
| Port is already in use | Old process has not exited | lsof -i:<PORT> → kill it specifically, do not pkill -9 go |
Things not to do
- ❌ Go live with the default example key (
your-api-key-1) - ❌ Announce "publicly available" without allowing it through UFW
- ❌ Directly
pkill -9 go(will accidentally kill other Go processes) - ❌ Use
sed -ito modify theapi-keys:multi-line section, old lines are likely to remain - ❌ Paste the full key back into chat (unless the user explicitly asks to "just give me the full value" once)
- ❌ Claim "already deployed" across sessions — you must re-run step 9 to get current evidence
Reference facts (code anchors, 2026-04)
- Startup entry point:
cmd/server/main.go:53main(); login parameter-login/-codex-login/-claude-login/-qwen-login/-iflow-login/-antigravity-login/-no-browser/-config - Default auth directory:
config.example.yamlauth-dir: "~/.cli-proxy-api" - Default port:
config.example.yamlport: 8317 - Callback port: Codex
sdk/auth/codex.go:27= 1455;Claudesdk/auth/claude.go:27= 54545;iFlowinternal/auth/iflow/iflow_auth.go:42= 11451;Geminiinternal/auth/gemini/gemini_auth.go:280= 8085 - Credential file name:
codex-<email>.json/claude-<email>.json/qwen-<email>.json/iflow-<email>-<ts>.json - Hot reload:
internal/watcher/*(modifyconfig.yamlor triggered by JSON under auth-dir) - Route:
internal/api/server.go:321setupRoutes()(/v1/*requiresAuthMiddleware)