Load balancer for ChatGPT and Claude accounts. Pool multiple accounts, track usage, manage API keys, view everything in a dashboard.
Humans: don't read further — paste this into your coding agent (Claude Code, Codex CLI, Cursor, …) and let it drive. Agents: your entrypoints are GETTING-STARTED.md (setup runbook) and AGENTS.md (working on the codebase); setup and operations never require the dashboard.
Clone https://github.com/aneym/agent-lb into ~/repos/agent-lb, cd into it, and read
GETTING-STARTED.md. Follow it top to bottom to set me up:
1. Install dependencies and start the local agent-lb service.
2. Walk me through connecting my Claude/ChatGPT accounts ONE AT A TIME — for each
account give me the browser URL, wait for me to approve (and paste back the code
where required), confirm it connected, then move to the next.
3. When wiring Claude Code, never set ANTHROPIC_AUTH_TOKEN or ANTHROPIC_API_KEY;
use the agent-lb base URL or launcher only so subscription billing stays intact.
4. Wire up my CLI clients (show exact dotfile edits and ask before applying them).
5. Finish by probing every account with a real request and telling me each account's
state per the "Account health model" (usable / unsubscribed / disconnected),
plus a short status summary of service, accounts, and wired clients.
If anything is already installed or running, detect it and skip ahead.
The full walkthrough lives in GETTING-STARTED.md. If your agent is
Claude Code or Codex launched inside the repo, saying "get started" triggers the
get-started skill, which follows the same runbook.
For post-setup account-specific work — quota reset checks, stuck or rate-limited
account triage, billing/subscription changes, pause/reactivate routing,
verification, or browser-profile work — say "account operator". Repo-aware
agents should use the agent-lb-account-operator skill and local
.agent-lb/account-profiles.json registry for that work.
![]() |
![]() |
|---|
| Account Pooling Load balance across multiple ChatGPT and Claude accounts |
Usage Tracking Per-account tokens, cost, 28-day trends |
API Keys Per-key rate limits by token, cost, window, model |
| Dashboard Auth Password + optional TOTP |
Client-compatible Codex CLI, Claude Code, OpenCode, OpenClaw, and SDKs |
Auto Model Sync Available models fetched from upstream |
This always works, including before PyPI/GHCR artifacts for a prerelease have been published:
git clone https://github.com/aneym/agent-lb.git
cd agent-lb
uv sync
scripts/install-service.shSource checkout starts the API/service. A git clone does not include built
dashboard assets; connect accounts from the CLI with
GETTING-STARTED.md step 4:
scripts/anthropic-auth.sh start
scripts/openai-auth.sh startTo use the dashboard from a source checkout, build the frontend and restart the service:
cd frontend && bun install && bun run build
cd ..
scripts/install-service.shAfter the v1.20.0-beta.3 release workflow has published artifacts:
# Docker
docker volume create agent-lb-data
docker run -d --name agent-lb \
-p 2455:2455 -p 1455:1455 \
-v agent-lb-data:/var/lib/agent-lb \
ghcr.io/aneym/agent-lb:1.20.0-beta.3
# or uvx
uvx --from "agent-lb==1.20.0b3" agent-lbOpen localhost:2455 → Add account → Done.
When accessing the dashboard remotely for the first time, a bootstrap token is required to set the initial password.
Auto-generated (default): On first startup (no password configured), the server generates a one-time token and prints it to logs:
docker logs agent-lb
# ============================================
# Dashboard bootstrap token (first-run):
# <token>
# ============================================Open the dashboard → enter the token + new password → done. The token is shared across replicas and remains valid until a password is set. In multi-replica setups, replicas must share the same encryption key (the Helm chart default) for restart recovery to work.
Manual token: To use a fixed token instead, set the env var before starting:
docker run -d --name agent-lb \
-e AGENT_LB_DASHBOARD_BOOTSTRAP_TOKEN=your-secret-token \
-p 2455:2455 -p 1455:1455 \
-v agent-lb-data:/var/lib/agent-lb \
ghcr.io/aneym/agent-lb:1.20.0-beta.3Local access (localhost) bypasses bootstrap entirely — no token needed.
Point each client at the matching agent-lb surface. OpenAI-compatible clients use /v1, Codex uses /backend-api/codex, and Claude Code uses the Anthropic-compatible base URL. Anthropic-compatible SDKs use the same root Messages API surface. If API key auth is enabled, pass a key from the dashboard as a Bearer token for HTTP clients that support it.
Use 127.0.0.1 only for clients running on the same machine as agent-lb.
Server-side app integrations such as the Vercel AI SDK or OpenAI SDK can set
their OpenAI-compatible base URL to /v1; browser-direct code and deployed
loopback URLs cannot reach or spend a user's local subscription accounts.
Model availability is discovered from the upstream Codex model catalog and can vary by account plan, workspace, rollout, and upstream deprecation state. Prefer the live GET /v1/models or GET /backend-api/codex/models response over a copied static table when configuring clients or API-key model allowlists.
Codex CLI / IDE Extension
~/.codex/config.toml:
model = "gpt-5.3-codex"
model_reasoning_effort = "xhigh"
model_provider = "agent-lb"
[model_providers.agent-lb]
name = "openai" # required — enables remote /responses/compact. Lowercase since Codex 2026-05-23; older "OpenAI" stops resolving gpt-5.5
base_url = "http://127.0.0.1:2455/backend-api/codex"
wire_api = "responses"
supports_websockets = true
requires_openai_auth = true # required for codex appOptional: enable native upstream WebSockets for Codex streaming while keeping agent-lb pooling:
export AGENT_LB_UPSTREAM_STREAM_TRANSPORT=websocketauto is the default and uses native WebSockets for native Codex headers or models that prefer them.
You can also switch this in the dashboard under Settings -> Routing -> Upstream stream transport.
Note: Codex itself does not currently expose a stable documented wire_api = "websocket" provider mode.
If you want to experiment on the Codex side, the current CLI exposes under-development feature flags:
[features]
responses_websockets = true
# or
responses_websockets_v2 = trueThese flags are experimental and do not replace wire_api = "responses".
Upstream websocket handshakes automatically honor standard proxy environment variables when they are
present. wss:// handshakes check wss_proxy, socks_proxy, https_proxy, and all_proxy;
plain ws:// handshakes also check ws_proxy and http_proxy. Set
AGENT_LB_UPSTREAM_WEBSOCKET_TRUST_ENV=false only when websocket handshakes must bypass those
environment proxies and connect directly.
With API key auth:
[model_providers.agent-lb]
name = "openai"
base_url = "http://127.0.0.1:2455/backend-api/codex"
wire_api = "responses"
env_key = "AGENT_LB_API_KEY"
supports_websockets = true
requires_openai_auth = true # required for codex appexport AGENT_LB_API_KEY="sk-clb-..." # key from dashboard
codexVerify WebSocket transport
Use a one-off debug run:
RUST_LOG=debug codex exec "Reply with OK only."Healthy websocket signals:
- CLI logs contain
connecting to websocketandsuccessfully connected to websocket agent-lblogs showWebSocket /backend-api/codex/responsesagent-lblogs do not show fallbackPOST /backend-api/codex/responsesfor the same run
If you run agent-lb behind a reverse proxy, make sure it forwards WebSocket upgrades.
Migrating from direct OpenAI — codex resume filters by model_provider;
old sessions won't appear until you re-tag them. Use the built-in retag command
instead of editing Codex files by hand; see
Codex session retagging for backups, Docker, WSL,
and rollback details.
# Preview what will change first.
agent-lb codex-sessions retag --from openai --to agent-lb --dry-run
# Then close Codex/Codex CLI and apply the retag.
agent-lb codex-sessions retag --from openai --to agent-lb --yes
Claude Code
Connect Claude (Anthropic) accounts first — scripts/anthropic-auth.sh or step 4 of
GETTING-STARTED.md. Then either launch with the base URL only:
ANTHROPIC_BASE_URL=http://127.0.0.1:2455 claudeor install the vendored cc launcher and canonical routing policy. Preview the exact
changes first; installation preserves existing regular client/hook files, checkpoints
changed global configuration, and removes retired CCDEX artifacts when present:
scripts/install-claude-clients.sh --print
scripts/install-claude-clients.shcc defaults to Fable/high. GPT compatibility seats run inside the Claude Code harness
through agent-lb's server-side model aliases. The installer semantically updates only
routing-owned Markdown blocks and the Claude model field; unrelated global
configuration remains intact.
For Claude Desktop's embedded Code runtime on macOS, install the dedicated shared loopback proxy after the main service is healthy:
scripts/install-claude-desktop-proxy.sh --print
scripts/install-claude-desktop-proxy.shIt runs as the KeepAlive LaunchAgent
com.aneyman.agent-lb-claude-desktop-proxy, verifies the proxied Anthropic health
endpoint before atomically updating ~/.claude/settings.json, and preserves unrelated
settings. Fully quit and reopen Claude Desktop after install. This routes the embedded
Code runtime; ordinary Claude Desktop chat is not a supported or claimed surface.
Within api.anthropic.com, only Messages API calls route to agent-lb. OAuth, telemetry,
feature, and Code worker traffic keeps its caller credentials and goes directly to
Anthropic; unknown future paths default direct.
Verify the integration by running a Desktop Code task and correlating it with a fresh
agent-lb request/session record. Logs are written to
~/.agent-lb/claude-desktop-proxy.err.log. To remove it and conditionally restore only
installer-owned settings:
scripts/install-claude-desktop-proxy.sh --uninstallImportant: do not set
ANTHROPIC_AUTH_TOKENorANTHROPIC_API_KEYwhen pointing Claude Code at agent-lb. Setting either flips Claude Code from subscription ("Claude Max") billing to per-token API billing. The launcher strips both defensively and falls back to plainclaudeif the load balancer is down (CLAUDE_LB_DISABLE=1forces the bypass).
Sticky routing without the launcher: claim a route via POST /api/anthropic/session-route
with {"sessionId", "model", "quotaKey"} and send x-claude-session-id: <sessionId> on
requests.
Anthropic Python SDK
Install the official Anthropic SDK:
pip install anthropicUse this from server-side code or a trusted local script:
import os
from anthropic import Anthropic
client = Anthropic(
base_url=os.environ.get("AGENT_LB_ANTHROPIC_BASE_URL", "http://127.0.0.1:2455"),
auth_token=os.environ.get("AGENT_LB_API_KEY", "sk-local"),
)
message = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=64,
messages=[{"role": "user", "content": "Hello!"}],
)
print(message.content[0].text)auth_token makes the SDK send the Agent LB key as Authorization: Bearer ...,
which is required when API key auth is enabled. Use a
dashboard key in AGENT_LB_API_KEY; if auth is disabled and the script runs on
the same machine, any non-empty local placeholder is fine.
Do not export ANTHROPIC_AUTH_TOKEN or ANTHROPIC_API_KEY as Agent LB
placeholders. Those names are first-party Anthropic/Claude credentials; keep LB
credentials in AGENT_LB_API_KEY and pass them explicitly in SDK code.
Like the OpenAI-compatible SDK examples, keep this server-side or local. Browser-direct code and deployed loopback URLs cannot reach or spend local subscription accounts through the LB.
OpenCode
Important: Use the built-in
openaiprovider withbaseURLoverride — not a custom provider with@ai-sdk/openai-compatible. Custom providers use the Chat Completions API which drops reasoning/thinking content. The built-inopenaiprovider uses the Responses API, which properly preservesencrypted_contentand multi-turn reasoning state.
Before starting, make sure all existing OpenAI credentials are cleared from ~/.local/share/opencode/auth.json.
You can clean the config by using this one-liner
jq 'del(.openai)' ~/.local/share/opencode/auth.json > auth.json.tmp && mv auth.json.tmp ~/.local/share/opencode/auth.json
~/.config/opencode/opencode.json:
This overrides the built-in openai provider's endpoint to point at agent-lb while keeping the Responses API code path that handles reasoning properly.
export AGENT_LB_API_KEY="sk-clb-..." # key from dashboard
opencode
OpenClaw
~/.openclaw/openclaw.json:
{
"agents": {
"defaults": {
"model": { "primary": "agent-lb/gpt-5.4" },
"models": {
"agent-lb/gpt-5.4": { "params": { "cacheRetention": "short" } },
"agent-lb/gpt-5.4-mini": { "params": { "cacheRetention": "short" } },
"agent-lb/gpt-5.3-codex": { "params": { "cacheRetention": "short" } },
},
},
},
"models": {
"mode": "merge",
"providers": {
"agent-lb": {
"baseUrl": "http://127.0.0.1:2455/v1",
"apiKey": "${AGENT_LB_API_KEY}", // or "dummy" if API key auth is disabled
"api": "openai-responses",
"models": [
{
"id": "gpt-5.4",
"name": "gpt-5.4 (agent-lb)",
"contextWindow": 1050000,
"contextTokens": 272000,
"maxTokens": 4096,
"input": ["text"],
"reasoning": false,
},
{
"id": "gpt-5.4-mini",
"name": "gpt-5.4-mini (agent-lb)",
"contextWindow": 400000,
"contextTokens": 272000,
"maxTokens": 4096,
"input": ["text"],
"reasoning": false,
},
{
"id": "gpt-5.3-codex",
"name": "gpt-5.3-codex (agent-lb)",
"contextWindow": 400000,
"contextTokens": 272000,
"maxTokens": 4096,
"input": ["text"],
"reasoning": false,
},
],
},
},
},
}Set the env var or replace ${AGENT_LB_API_KEY} with a key from the dashboard. If API key auth is disabled,
local requests can omit the key, but non-local requests are still rejected until proxy authentication is configured.
The /v1 route is the simplest OpenAI-compatible setup. If your OpenClaw build uses a Codex-native provider path such as openai-codex-responses and needs Codex-style usage/accounting behavior, point that provider at http://127.0.0.1:2455/backend-api/codex instead. For third-party Codex-compatible backends, the client must allow opaque bearer-token passthrough and should only send chatgpt-account-id when it actually decoded one from an official ChatGPT/Codex token.
Vercel AI SDK
Install the OpenAI provider in the app:
pnpm add ai @ai-sdk/openaiUse this only from server-side code, such as a route handler or server action:
import { createOpenAI } from "@ai-sdk/openai";
import { generateText } from "ai";
const agentLB = createOpenAI({
baseURL: process.env.AGENT_LB_BASE_URL ?? "http://127.0.0.1:2455/v1",
apiKey: process.env.AGENT_LB_API_KEY ?? "sk-local",
});
export async function POST(req: Request) {
const { prompt } = await req.json();
const { text } = await generateText({
model: agentLB.responses("gpt-5.3-codex"),
prompt,
});
return Response.json({ text });
}127.0.0.1:2455 works only when that server route runs on the same machine as
agent-lb. Deployed apps need AGENT_LB_BASE_URL set to a reachable Agent LB URL
and should not route through Vercel AI Gateway if the goal is to spend local
subscription accounts through this proxy.
OpenAI Python SDK
from openai import OpenAI
client = OpenAI(
base_url="http://127.0.0.1:2455/v1",
api_key="sk-clb-...", # from dashboard, or any non-empty string if auth is disabled
)
response = client.chat.completions.create(
model="gpt-5.3-codex",
messages=[{"role": "user", "content": "Hello!"}],
)
print(response.choices[0].message.content)A native macOS 26 menu bar companion (clients/macos-menubar/) puts the
dashboard's vitals one click away: a status-bar ring gauge that drains with
the pool's 5-hour window, separated 5-hour / weekly limit cards with
next reset in … · +n cr recovery, a provider scope control (All / Codex /
Claude) that filters every stat exactly like the dashboard, an account list
with status/sort/search filtering and pause/reactivate, recent requests, and
service start/restart/stop via launchd.
cd clients/macos-menubar
make install # build, bundle, and register as a login LaunchAgent (starts now + at every login)make install is the supported way to run it: it registers the app in the
GUI launchd domain, which makes startup automatic at login. (Launching the
bundle with open from an SSH session silently fails to register the menu
bar item on macOS 26 — use the LaunchAgent.) make uninstall removes it;
make test runs the unit suite.
Remote machines (Tailscale): copy AgentLB.app anywhere (e.g.
~/Applications), point it at the service host, and register the agent for
the copied path:
defaults write com.aneyman.agentlb.menubar baseURL "https://<host>.<tailnet>.ts.net:2455"
make install-agent APP_PATH=$HOME/Applications/AgentLB.appWhen the base URL isn't local the app runs in remote mode: the header shows the host and the launchd service controls hide. If your menu bar is full (notched MacBooks hide overflow items), nudge the icon right of the notch:
defaults write com.aneyman.agentlb.menubar "NSStatusItem Preferred Position Item-0" -float 220API key auth is disabled by default. In that mode, only local requests to the protected proxy routes can proceed without a key; non-local requests are rejected until proxy authentication is configured. Enable it in Settings → API Key Auth on the dashboard when clients connect remotely or through Docker, VM, or container networking that appears non-local to the service.
When enabled, clients must pass a valid API key as a Bearer token:
Authorization: Bearer sk-clb-...
The protected proxy routes covered by this setting are:
/v1/*(except/v1/usage, which always requires a valid key)/backend-api/codex/*/backend-api/transcribe
Creating keys: Dashboard → API Keys → Create. The full key is shown only once at creation. Keys support optional expiration, model restrictions, and rate limits (tokens / cost per day / week / month).
Environment variables with AGENT_LB_ prefix or .env.local. See .env.example.
SQLite is the default database backend; PostgreSQL is optional via AGENT_LB_DATABASE_URL (for example postgresql+asyncpg://...).
The Docker Compose postgres profile uses the Postgres 18 image and mounts the named data volume at
/var/lib/postgresql, the parent of the image's versioned PGDATA directory.
Existing Postgres 16 compose volumes must be upgraded before the Postgres 18 container starts:
docker compose --profile postgres stop postgres
docker run --rm -v agent-lb-postgres-data:/var/lib/postgresql -v "$PWD:/backup" alpine \
tar -C /var/lib/postgresql -czf /backup/agent-lb-postgres-data-before-pg18.tgz .
docker compose --profile postgres-upgrade run --rm postgres-upgrade
docker compose --profile postgres up -d postgresThe postgres-upgrade profile runs pg_upgrade in one-shot mode against the same named volume and exits after the
data directory has been upgraded to the Postgres 18 layout. Because that helper mounts and rewrites the operator's
database volume, Compose pins the helper image by digest; refresh and review the digest deliberately when changing the
helper image tag. Keep the backup until the application has started and agent-lb-db check succeeds against the
upgraded database.
The normal postgres service refuses to start when it detects the old root-level PG_VERSION file from a pre-18
Compose volume. If that guard fires, run the postgres-upgrade profile above before starting Postgres again.
It also refuses nested /var/lib/postgresql/data directories that still report a pre-18 major version, because those
layouts need an explicit pg_upgrade before the Postgres 18 container can safely open them.
agent-lb supports three dashboard auth modes via environment variables:
AGENT_LB_DASHBOARD_AUTH_MODE=standard— built-in dashboard password with optional TOTP from the Settings page.AGENT_LB_DASHBOARD_AUTH_MODE=trusted_header— trust a reverse-proxy auth header such as Authelia'sRemote-User, but only fromAGENT_LB_FIREWALL_TRUSTED_PROXY_CIDRS. Built-in password/TOTP remain available as an optional fallback, and password/TOTP management still requires a fallback password session.AGENT_LB_DASHBOARD_AUTH_MODE=disabled— fully bypass dashboard auth. Use only behind network restrictions or external auth. Built-in password/TOTP management is disabled in this mode.
trusted_header mode also requires:
AGENT_LB_FIREWALL_TRUST_PROXY_HEADERS=true
AGENT_LB_FIREWALL_TRUSTED_PROXY_CIDRS=172.18.0.0/16
AGENT_LB_DASHBOARD_AUTH_PROXY_HEADER=Remote-UserIf the trusted header is missing and no fallback password is configured, the dashboard fails closed and shows a reverse-proxy-required message instead of loading the UI.
Authelia / trusted header
docker run -d --name agent-lb \
-p 2455:2455 -p 1455:1455 \
-e AGENT_LB_DASHBOARD_AUTH_MODE=trusted_header \
-e AGENT_LB_DASHBOARD_AUTH_PROXY_HEADER=Remote-User \
-e AGENT_LB_FIREWALL_TRUST_PROXY_HEADERS=true \
-e AGENT_LB_FIREWALL_TRUSTED_PROXY_CIDRS=172.18.0.0/16 \
-v agent-lb-data:/var/lib/agent-lb \
ghcr.io/aneym/agent-lb:1.20.0-beta.3Hard override / no app-level dashboard auth
docker run -d --name agent-lb \
-p 2455:2455 -p 1455:1455 \
-e AGENT_LB_DASHBOARD_AUTH_MODE=disabled \
-v agent-lb-data:/var/lib/agent-lb \
ghcr.io/aneym/agent-lb:1.20.0-beta.3For Helm, pass the same values through extraEnv.
| Environment | Path |
|---|---|
| Local / uvx | ~/.agent-lb/ |
| Docker | /var/lib/agent-lb/ |
Backup this directory to preserve your data.
The OCI chart command below requires the approval-gated beta release workflow to
publish the chart artifact first. Before that artifact is public, install from
the local chart source after helm dependency build deploy/helm/agent-lb/; the
Helm chart README includes matching source commands for each install mode.
helm install agent-lb oci://ghcr.io/aneym/charts/agent-lb \
--version 1.20.0-beta.3 \
--devel \
--set postgresql.auth.password=changeme \
--set config.databaseMigrateOnStartup=true \
--set migration.schemaGate.enabled=false
kubectl port-forward svc/agent-lb 2455:2455Open localhost:2455 → Add account → Done.
The Helm chart auto-configures HTTP /responses owner handoff for multi-replica installs using a headless-service DNS name per pod. The default cluster domain is cluster.local; set Helm clusterDomain if your cluster uses a different suffix. Override config.sessionBridgeAdvertiseBaseUrl only if pods must be reached through a different internal address.
For external database, production config, ingress, observability, and more see the Helm chart README.
Fast Mode and service-tier behavior is documented in Responses API compatibility context.
# Docker
docker compose watch
# Local
uv sync && cd frontend && bun install && cd ..
uv run fastapi run app/main.py --reload # backend :2455
cd frontend && bun run dev # frontend :5173Thanks goes to these wonderful people (emoji key):
This project follows the all-contributors specification. Contributions of any kind welcome!







{ "$schema": "https://opencode.ai/config.json", "provider": { "openai": { "options": { "baseURL": "http://127.0.0.1:2455/v1", "apiKey": "{env:AGENT_LB_API_KEY}", }, "models": { "gpt-5.4": { "name": "GPT-5.4", "reasoning": true, "options": { "reasoningEffort": "high", "reasoningSummary": "detailed", }, "limit": { "context": 1050000, "output": 128000 }, }, "gpt-5.3-codex": { "name": "GPT-5.3 Codex", "reasoning": true, "options": { "reasoningEffort": "high", "reasoningSummary": "detailed", }, "limit": { "context": 272000, "output": 65536 }, }, "gpt-5.1-codex-mini": { "name": "GPT-5.1 Codex Mini", "reasoning": true, "options": { "reasoningEffort": "high", "reasoningSummary": "detailed", }, "limit": { "context": 272000, "output": 65536 }, }, "gpt-5.3-codex-spark": { "name": "GPT-5.3 Codex Spark", "reasoning": true, "options": { "reasoningEffort": "xhigh", "reasoningSummary": "detailed", }, "limit": { "context": 128000, "output": 65536 }, }, }, }, }, "model": "openai/gpt-5.3-codex", }