Subrouter is a local AI coding-agent proxy. It routes traffic across Codex accounts with sticky conversation-to-account assignment so cached context stays useful.
- Run fast on a Mac Mini.
- Forward requests with normal Go reverse-proxy behavior, including headers and streaming responses.
- Support subscription accounts first, API keys second.
- Keep each conversation pinned to one account.
- Pick a fresh account for a new conversation based on available rate-limit headroom.
- Provide the Codex account manager and daemon in one Go binary.
Paste this into Claude, Codex, or another coding agent that has SSH access to your server and a local browser for OAuth:
Set up Subrouter as a shared production service.
Inputs:
- Server SSH target: <user@host>
- Server URL reachable from my machine: http://<tailnet-ip-or-dns>:31415
- Local server nickname: team
Rules:
- Do not copy ~/.codex/auth.json or local ~/.subrouter/codex/accounts/*.json to the server.
- Server OAuth accounts must be created with fresh server-owned login flows.
- Do not print access tokens, refresh tokens, API keys, id tokens, or admin tokens.
- Keep the listener private to Tailscale/VPC. Do not expose it to the public internet.
- Use the released Subrouter binary unless I explicitly ask you to build from source.
Steps:
1. On this local machine, create an admin token variable without printing it:
TOKEN="$(openssl rand -hex 32)"
2. Install the release on the server:
ssh <user@host> 'curl -fsSL https://github.com/manaflow-ai/subrouter/releases/latest/download/install.sh | sudo sh'
3. Install the systemd service with the local admin token and verify it over loopback:
ssh <user@host> "sudo sr install-systemd --addr 0.0.0.0:31415 --admin-token '$TOKEN'"
ssh <user@host> 'curl -fsS http://127.0.0.1:31415/_subrouter/health'
ssh <user@host> 'curl -fsS http://127.0.0.1:31415/_subrouter/ready'
4. Save the same admin token locally in Subrouter server config without printing it. This also writes Codex routing defaults to `CODEX_HOME/config.toml` or `~/.codex/config.toml`:
sr server add team --url http://<tailnet-ip-or-dns>:31415 --admin-token "$TOKEN" --default
5. Create server-owned Codex OAuth chains:
sr server sync team --device-auth
Follow each OAuth flow. Do not upload local refresh tokens.
6. Verify:
sr server status team
curl -fsS http://<tailnet-ip-or-dns>:31415/_subrouter/health
curl -fsS http://<tailnet-ip-or-dns>:31415/_subrouter/ready
ssh <user@host> 'journalctl -u subrouter --since "30 min ago" --no-pager | grep -Ei "WARN|ERROR|failed|401|502|503|no usable|refresh_token" | tail -n 200 || true'
7. Report:
- systemd active/running status
- health and readiness result
- number of registered Codex OAuth accounts
- any warning/error log lines, without secrets
- the exact command I should use for Codex through Subrouter
For local-only use on macOS, paste this instead:
Set up Subrouter locally for Codex.
Rules:
- Do not print tokens.
- Do not edit Codex config by hand unless Subrouter docs say so.
Steps:
1. Install:
curl -fsSL https://github.com/manaflow-ai/subrouter/releases/latest/download/install.sh | sh
2. Install and verify the LaunchAgent:
sr install-daemon
curl -fsS http://127.0.0.1:31415/_subrouter/health
curl -fsS http://127.0.0.1:31415/_subrouter/ready
3. Add Codex accounts:
sr add
Repeat as needed.
4. Verify:
sr status
5. Report the command I should use:
sr codex
Install the released Go binary directly:
curl -fsSL https://github.com/manaflow-ai/subrouter/releases/latest/download/install.sh | shOn a Linux server, install to /usr/local/bin:
curl -fsSL https://github.com/manaflow-ai/subrouter/releases/latest/download/install.sh | sudo shInstall with npm:
npm install -g subrouterInstall with Python:
pipx install subrouterAll install paths provide subrouter, sr, and cx. The npm and Python wrappers download the matching Go release binary for macOS, Linux, Windows, FreeBSD, OpenBSD, or NetBSD on amd64, arm64, or supported 32-bit variants. Set SUBROUTER_BIN to use a local binary instead.
On macOS, install Subrouter as a localhost-only LaunchAgent:
make build
./bin/subrouter install-daemonThis installs the binary to ~/bin/subrouter, installs ~/bin/sr and ~/bin/cx as symlinks to the same Go binary, writes ~/Library/LaunchAgents/ai.manaflow.subrouter.plist, creates ~/.subrouter/transcripts, starts the service, and runs:
~/bin/subrouter serve --addr 127.0.0.1:31415 --transcripts ~/.subrouter/transcripts --sr-switch-interval 10mThe 10 minute sr auto-switch interval is the default. Override it with subrouter install-daemon --sr-switch-interval 5m, or disable it with --sr-switch-interval 0. The old --cx-switch-interval flag remains a compatibility alias.
On a Linux server, install the binary and service:
curl -fsSL https://github.com/manaflow-ai/subrouter/releases/latest/download/install.sh | sudo sh
sudo sr install-systemd --addr 0.0.0.0:31415This creates a subrouter system user, stores state under /var/lib/subrouter, writes /etc/systemd/system/subrouter.service, installs subrouter, sr, and cx in /usr/local/bin, and starts:
/usr/local/bin/subrouter serve --addr 0.0.0.0:31415 --sessions /var/lib/subrouter/sessions.json --transcripts /var/lib/subrouter/transcripts --sr-switch-interval 10mIf legacy switchboard or gateway services exist, sr install-systemd stops and disables them, merges their /var/lib/... state into /var/lib/subrouter, and preserves their extra service args.
Useful endpoints:
GET /_subrouter/health
GET /_subrouter/ready
POST /_subrouter/drain
GET /_subrouter/drain-status
GET /_subrouter/accounts
GET /_subrouter/account-status
POST /_subrouter/account-status
GET /_subrouter/usage-status
GET /_subrouter/sessions
GET /_subrouter/dashboard
GET /_subrouter/transcripts
/_subrouter/health is liveness. /_subrouter/ready returns 503 while the process is draining. /_subrouter/drain is loopback-only and tells the process to reject new proxy sessions while allowing active sessions to continue. GET /_subrouter/account-status validates only expired OAuth tokens; POST /_subrouter/account-status force-refreshes token chains and should be reserved for explicit diagnostics. GET /_subrouter/usage-status returns the read-only account usage data rendered by sr server status <name>.
For servers that listen on a non-loopback address, set an admin token before exposing account, session, dashboard, or transcript endpoints:
TOKEN="$(openssl rand -hex 32)"
sudo sr install-systemd --addr 0.0.0.0:31415 --admin-token "$TOKEN"
sr server add team --url http://100.64.0.1:31415 --admin-token "$TOKEN" --defaultWhen SUBROUTER_ADMIN_TOKEN or --admin-token is set, non-loopback requests to sensitive /_subrouter/* endpoints must send Authorization: Bearer <token> or X-Subrouter-Admin-Token: <token>. Loopback stays trusted so server-local hot reloads continue to work.
See deploy/gcp/README.md for the small GCP + Tailscale Subrouter deployment flow. See docs/production.md for the production checklist before running a shared server.
To persist raw Subrouter transcripts, pass a transcript directory:
subrouter serve --transcripts ~/.subrouter/transcriptsTranscripts are JSONL files keyed by agent type and session id under by-agent/<agent-type>/by-session/<agent-session-id>.jsonl. They include Subrouter metadata, redacted headers, HTTP/SSE body chunks, HTTP/SSE body summaries, and WebSocket message payloads as base64 with byte counts and SHA-256 hashes. Each event includes agent_type and agent_session_id; Codex events also include codex_session_id for matching ~/.codex/sessions JSONL files. This is intentionally storage-heavy and can contain sensitive request/response payloads. Authorization-style headers are redacted, but bodies are stored in full.
When transcript recording is enabled, /_subrouter/dashboard serves an internal HTML dashboard over the same Subrouter listener. It shows token usage over time, usage by user email, usage by selected account, session assignments, transcript summaries, and links to sanitized transcript event JSON under /_subrouter/transcripts/<agent-type>/<session-id>. Raw internal trajectory JSON with decoded body text is available under /_subrouter/transcripts/<agent-type>/<session-id>/raw.
To mirror transcripts to GCS without blocking proxy requests, also pass a gs:// destination:
subrouter serve --transcripts ~/.subrouter/transcripts --transcript-gcs-uri gs://bucket/prefixThe daemon shells out to gsutil -m rsync -r on a background interval. Local transcript writes stay on the request path; GCS upload failures are logged and retried later.
For best cache behavior, clients should send a stable header per conversation:
X-Subrouter-Session: <conversation-or-thread-id>
If that header is missing, Subrouter checks Codex headers such as x-codex-window-id and x-codex-turn-state, common session headers, query params, and small JSON bodies for session_id, conversation_id, or thread_id.
Subrouter scopes sticky assignments and transcript files by agent type. It infers codex, claude, or gemini from provider session headers, and clients can set an explicit namespace:
X-Subrouter-Agent: codex
For teammate-level graphs, clients can also send a self-reported user header:
X-Subrouter-User-Email: alice@example.com
Subrouter stores the normalized email on the session assignment, includes it in proxy logs as user, and exposes it in GET /_subrouter/sessions. This is observability metadata, not authentication. To force a selected account, send X-Subrouter-Account-ID; API-key labels can omit the apikey: prefix. Subrouter strips X-Subrouter-Session, X-Subrouter-Agent, X-Subrouter-User-Email, X-Subrouter-User, X-User-Email, X-Subrouter-Account-ID, and X-Subrouter-Account before forwarding upstream.
subrouter codex is a direct Codex wrapper. Use it anywhere you would use codex:
subrouter codex
subrouter codex exec "your prompt"
subrouter codex --versionThe wrapper injects this config override into the child Codex process:
openai_base_url = "http://127.0.0.1:31415/v1"It does not edit Codex config or set auth environment variables. Do not set a dummy OPENAI_API_KEY for normal subscription routing. Leave Codex logged in the same way it already is. If Codex is in ChatGPT auth mode, /model keeps the subscription model picker. Subrouter replaces outbound credentials with the selected sr account before forwarding. Responses and realtime WebSocket requests are proxied through the same route.
Override the subrouter URL with SUBROUTER_CODEX_BASE_URL if needed. See docs/codex.md for details and the custom-provider fallback.
If SUBROUTER_CODEX_BASE_URL is not set, the wrapper uses local 127.0.0.1:31415/v1. To make sr codex, Codex Desktop's app-server, and the default sr usage view use a remote Subrouter, register and select a named server:
sr server add team --url http://100.64.0.1:31415 --defaultsr server add --default and sr server use <name> write these top-level keys in CODEX_HOME/config.toml, or ~/.codex/config.toml when CODEX_HOME is unset:
openai_base_url = "http://100.64.0.1:31415/v1"
chatgpt_base_url = "http://100.64.0.1:31415/backend-api"
experimental_realtime_ws_base_url = "http://100.64.0.1:31415/v1"Use --no-codex-config to change only Subrouter's selected server. Use sr server use local or sr server clear-default to return to the local daemon and rewrite Codex config to 127.0.0.1:31415.
The server name is only a local nickname. Use whatever matches your setup, such as team, prod, or staging. For a one-off command, set SUBROUTER_CODEX_SERVER=team.
Rename a local server nickname with sr server rename <old> <new>.
Top-level sr account commands follow the selected target. If sr server use team is active, sr add, sr add-key, sr list, sr status, sr usage, and sr pick talk to that server. If the selected target is local, those same commands use the local account store. Commands without a remote-safe implementation fail before editing local auth when a server is selected. Use SUBROUTER_CODEX_SERVER=local sr <command> for a one-off local command.
Set SUBROUTER_CODEX_USER_EMAIL to attribute Codex traffic to a teammate:
SUBROUTER_CODEX_USER_EMAIL=alice@example.com subrouter codex exec "your prompt"Force a specific Subrouter account, including an API-key account, with SUBROUTER_CODEX_ACCOUNT_ID:
SUBROUTER_CODEX_ACCOUNT_ID=team-codex-1 subrouter codex exec "your prompt"
SUBROUTER_CODEX_ACCOUNT_ID=apikey:team-codex-1 subrouter codex exec "your prompt"When either variable is set, the wrapper uses a custom subrouter provider with WebSockets enabled so Codex can send X-Subrouter-User-Email and X-Subrouter-Account-ID. Subrouter still replaces outbound credentials before forwarding upstream. SUBROUTER_CODEX_USER_EMAIL is only teammate observability metadata; account selection belongs in SUBROUTER_CODEX_ACCOUNT_ID.
Codex Desktop is separate from the CLI wrapper. Its app-server reads CODEX_HOME/config.toml, and its Electron shell reads CODEX_API_BASE_URL at process start. See docs/codex.md for the desktop routing setup.
Subrouter has a native Go implementation of the Codex account manager. It reads and writes its account store under Subrouter's data directory:
~/.subrouter/codex/accounts/*.json
On first run, Subrouter migrates legacy ~/.codex-accounts state into ~/.subrouter/codex. Codex's own active auth file remains ~/.codex/auth.json.
Server-owned OAuth accounts must be created with fresh logins because Codex refresh tokens rotate. Do not copy local OAuth account files to a server. To compare local OAuth emails with a configured server, validate server refresh-token chains, and reauth missing or invalid accounts on the server, run:
sr server sync team --device-authTo only show the diff:
sr server diff teamsr server sync prints the plan and asks before opening login. Use --yes for unattended sync, --email you@example.com to reauth one email, or --all to replace every local OAuth email on the server with a new server-owned refresh-token chain. The server status check may refresh valid server-owned OAuth chains in place because Codex refresh tokens rotate.
Account uploads hot-reload the live server process after writing the new server-owned account file. Existing proxy and WebSocket connections keep running.
Account-management commands are built into the subrouter binary:
go run ./cmd/subrouter add
go run ./cmd/subrouter import
go run ./cmd/subrouter list
go run ./cmd/subrouter status
sr statusThe supported Codex commands include add, add-key, import, list, switch, g, gui, gui-switch, remove, status, usage, server, add-admin-key, admin-keys, remove-admin-key, attach-project, claude, and gemini. The older subrouter cx <command> form remains as a compatibility alias.
sr switch also syncs compatible ChatGPT Codex credentials into:
~/.codex/auth.json
~/.local/share/opencode/auth.json # provider key: openai
~/.pi/agent/auth.json # provider key: openai-codex
OpenCode uses XDG data home, so XDG_DATA_HOME changes its auth path. pi uses PI_CODING_AGENT_DIR when set. Existing unrelated provider credentials in those files are preserved.
Claude profiles are also native Go and use the same Subrouter store:
sr claude list
sr claude switch <profile>
sr claude env
sr claude run <profile>Claude Code can also proxy through Subrouter with Claude Code OAuth tokens. Generate a long-lived token with claude setup-token, then configure the Claude user settings env:
{
"env": {
"ANTHROPIC_BASE_URL": "http://127.0.0.1:31415",
"CLAUDE_CODE_OAUTH_TOKEN": "sk-ant-oat01-...",
"ANTHROPIC_AUTH_TOKEN": "sk-ant-oat01-..."
}
}For a shared server, replace 127.0.0.1 with the server URL. Subrouter recognizes Claude Code traffic, selects a Claude OAuth account from its own store, strips API-key auth, and forwards to Anthropic with the OAuth beta header.
Gemini has its own sr gemini namespace and store scaffold so future routing cannot collide with Codex or Claude state.
On startup, Subrouter fetches current Codex usage for OAuth accounts and scores each account by its most constrained usage window. The scheduler keeps existing sessions sticky. For a new session it protects low-headroom accounts, spends healthy quota that resets soonest, then breaks ties by live assigned-session counts. If all else ties, subscription OAuth accounts are preferred before API-key accounts.
The daemon also refreshes usage and updates Codex, OpenCode, and pi auth every 10 minutes by default so local agents follow the same OAuth-only policy. Configure it with subrouter serve --sr-switch-interval 5m, or disable it with --sr-switch-interval 0. If --fetch-usage=false, auto-switch is disabled because fresh usage is required.
By default, OAuth accounts are forwarded to https://chatgpt.com/backend-api/codex and API-key accounts are forwarded to https://api.openai.com. Subrouter accepts either /v1/responses or /responses from clients and normalizes the path for the selected account type.
Live headroom comes from Codex subscription usage. API-key spend comes from the OpenAI organization usage endpoints through stored sk-admin-* keys. Claude profile usage comes from the Anthropic OAuth usage endpoint when profile credentials are readable.
See docs/saturation.md for the 5h/7d placement strategy and simulation tests.
- Bind to
127.0.0.1unless explicitly exposed. - Do not log tokens, refresh tokens, API keys, request bodies, or full Authorization headers.
- Keep Subrouter-managed credentials under
~/.subrouter/codexlocally and/var/lib/subrouter/codexon systemd servers.