Skip to main content

Routes AI coding-agent traffic across subscription accounts and API keys.

Project description

Subrouter

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.

Goals

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

Current shape

make accounts
make run

This repo sets CGO_ENABLED=0 in Makefile because the local macOS Go 1.22 toolchain is currently producing cgo test binaries that fail before startup with missing LC_UUID load command.

Install

Install the released Go binary directly:

curl -fsSL https://github.com/manaflow-ai/subrouter/releases/latest/download/install.sh | sh

On a Linux server, install to /usr/local/bin:

curl -fsSL https://github.com/manaflow-ai/subrouter/releases/latest/download/install.sh | sudo sh

Install with npm:

npm install -g subrouter

Install with Python:

pipx install subrouter

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

Local macOS daemon

On macOS, install Subrouter as a localhost-only LaunchAgent:

make build
./bin/subrouter install-daemon

This installs the binary to ~/bin/subrouter, installs ~/bin/cx as a symlink 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 --cx-switch-interval 10m

The 10 minute cx auto-switch interval is the default. Override it with subrouter install-daemon --cx-switch-interval 5m, or disable it with --cx-switch-interval 0.

Linux systemd service

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:31415

This 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 --cx-switch-interval 10m

If 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/accounts
POST /_subrouter/account-status
GET /_subrouter/sessions
GET /_subrouter/dashboard
GET /_subrouter/transcripts

GCP deployment

See deploy/gcp/README.md for the small GCP + Tailscale Subrouter deployment flow.

To persist raw Subrouter transcripts, pass a transcript directory:

subrouter serve --transcripts ~/.subrouter/transcripts

Transcripts 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 bodies, SSE bodies, 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/prefix

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

Codex CLI

subrouter codex is a direct Codex wrapper. Use it anywhere you would use codex:

subrouter codex
subrouter codex exec "your prompt"
subrouter codex --version

The 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 cx account before forwarding.

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 use a remote Subrouter by default, register and select a named server:

sr server add team --url http://100.64.0.1:31415 --default

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

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 accounts

Subrouter has a native Go implementation of the Codex account manager. It reads and writes the existing cx store format:

~/.codex-accounts/accounts/*.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-auth

To only show the diff:

sr server diff team

sr 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-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 status

The supported Codex commands include add, add-key, import, list, switch, 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.

cx 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 existing ~/.codex-accounts/claude.json format:

cx claude list
cx claude switch <profile>
cx claude env
cx claude run <profile>

Gemini has its own cx gemini namespace and store scaffold so future routing cannot collide with Codex or Claude state.

Selection policy

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 --cx-switch-interval 5m, or disable it with --cx-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.

Security defaults

  • Bind to 127.0.0.1 unless explicitly exposed.
  • Do not log tokens, refresh tokens, API keys, request bodies, or full Authorization headers.
  • Keep ~/.codex-accounts credentials as the canonical local store.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

subrouter-0.1.9.tar.gz (107.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

subrouter-0.1.9-py3-none-any.whl (8.3 kB view details)

Uploaded Python 3

File details

Details for the file subrouter-0.1.9.tar.gz.

File metadata

  • Download URL: subrouter-0.1.9.tar.gz
  • Upload date:
  • Size: 107.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for subrouter-0.1.9.tar.gz
Algorithm Hash digest
SHA256 d7d42090055add947a029f374c8fc7940acdf5099110122cfeab09d121bcc63f
MD5 8b7fcb845344e6ffe53b7fcdf8017d79
BLAKE2b-256 d860b6afde3277bb10d357f208f0b5c128a87d656424438ed2609b02bdec317f

See more details on using hashes here.

Provenance

The following attestation bundles were made for subrouter-0.1.9.tar.gz:

Publisher: release.yml on manaflow-ai/subrouter

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file subrouter-0.1.9-py3-none-any.whl.

File metadata

  • Download URL: subrouter-0.1.9-py3-none-any.whl
  • Upload date:
  • Size: 8.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for subrouter-0.1.9-py3-none-any.whl
Algorithm Hash digest
SHA256 e6946972275725a3d314b366a5cdb92d58c6e28bf119c0bf149726b765235183
MD5 8b712e4a70e20541d7c4d8c043ae6f5a
BLAKE2b-256 74aa77f02ce3fbf975f68d30d418dfcd30b1d385e5eb0c8d0f3ec272c1b0baea

See more details on using hashes here.

Provenance

The following attestation bundles were made for subrouter-0.1.9-py3-none-any.whl:

Publisher: release.yml on manaflow-ai/subrouter

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page