A native Claude (Anthropic) + OpenAI Codex API proxy. Fans out client requests across multiple upstream credentials (OAuth + API keys) on two independent HTTP endpoints — one per provider, shared pool infrastructure — with per-user sticky slot allocation, per-credential proxies, persistent usage tracking, and automatic API-key fallback. Admin panel, client tokens, request log, pricing, and stats are shared across providers; credentials are strictly separated per upstream.
Codex OAuth validation pending. The ChatGPT-backend proxy (
/v1/responsespath) is implemented end-to-end — header set, body sanitization, usage extraction, plan-tier model listing — mirroring CLIProxyAPI's upstream implementation. It has not yet been validated in production with a real ChatGPT Plus/Pro subscription token; the auth-layer test paths (token exchange, refresh, JWT parsing) are exercised, but full request/response parity against chatgpt.com/backend-api is pending a real-token smoke. If you hit an unexpected 400, please open an issue with the error body.
Credit. This project is a derivative of CLIProxyAPI (MIT). The original supports many AI providers (Gemini, Codex/OpenAI, Qwen, Kimi, iFlow, Antigravity, Vertex, Claude, …) and ships a management UI, TUI, translator layer, and multi-protocol entry points. CPA-Claude keeps the Claude + Codex passthroughs and adds slot-based concurrency, per-credential SOCKS/HTTP proxies, persistent usage tracking, and per-provider endpoint routing. The Anthropic OAuth refresh flow, the Codex OAuth flow + JWT parsing, and the uTLS Chrome transport were borrowed from the upstream project — huge thanks to its authors.
- Two endpoints, one fleet — Claude on
:8317(/v1/messages) and Codex on:8318(/v1/chat/completions,/v1/responses). Each can be enabled/disabled independently; the admin panel mounts on whichever is "primary" (Claude by default). - Native Claude passthrough — no protocol translation for the
Anthropic path, pure passthrough to
api.anthropic.com/v1/messages(SSE streaming preserved). - Slot-based concurrency per OAuth file — each OAuth credential has a
max_concurrentcap. A client session that makes a request within the last 10 minutes (configurable) occupies one slot. Idle clients release their slot automatically. - Per-credential upstream proxy — every OAuth file may set its own
proxy_url, useful when different accounts must egress from different IPs. - Persistent usage tracking — input/output/cache-read/cache-write token
counts per credential survive restarts in
state.json. - API-key fallback — when every OAuth credential is saturated, quota- exceeded, or dead, requests fall through to the unlimited API-key pool.
- Codex passthrough — OpenAI-format
/v1/chat/completionsand/v1/responseson the Codex endpoint. API-key credentials forward toapi.openai.com; OAuth (ChatGPT Plus/Pro/Team) credentials forward tochatgpt.com/backend-api/codex/responseswith the session/account headers the Codex CLI sends./v1/modelssynthesizes the plan-tier catalog per subscription. - Per-client weekly budgets — each access token can have a
weekly_usdcap that applies across both providers. Spend is tracked by ISO week and costed with a built-in pricing table covering Claude (Haiku 4.5, Opus 4.6/4.7, Sonnet 4.6) and OpenAI (gpt-5, gpt-5-mini, gpt-5-nano, gpt-5.x codex tiers, gpt-4o/mini) — cache-read and cache-create priced separately. Exceeded → 429 until the next Monday. - Per-request JSONL log — one line per terminal request (who, which credential, model, tokens, cost, status, duration). Daily-rotated file, default 30-day retention.
- uTLS (Chrome) — bypasses Anthropic's TLS fingerprinting.
- Proxy schemes —
http://,https://,socks5://,socks5h://all supported, both with and without uTLS.
One-liner (Linux/macOS, amd64/arm64) — installs to /usr/local/bin:
curl -fsSL https://raw.githubusercontent.com/wjsoj/CPA-Claude/main/install.sh | bashPin a version or change prefix:
curl -fsSL https://raw.githubusercontent.com/wjsoj/CPA-Claude/main/install.sh \
| bash -s -- --version v0.1.0 --prefix ~/.localRe-run the same command to upgrade.
From source:
go install github.com/wjsoj/CPA-Claude/cmd/server@latest
# binary will be named "server"; rename to cpa-claude or adjust PATH usage.cp config.example.yaml config.yaml
# edit config.yaml, populate ./auths/*.json, then:
cpa-claude -config config.yaml./auths/<any-name>.json:
{
"type": "claude",
"access_token": "sk-ant-oat01-...",
"refresh_token": "sk-ant-ort01-...",
"expired": "2026-05-01T12:00:00Z",
"email": "alice@example.com",
"label": "alice",
"proxy_url": "http://10.0.0.1:3128",
"max_concurrent": 5,
"disabled": false
}access_tokenis auto-refreshed on every request if within 5 minutes ofexpired. The refreshed token (and newexpired) is written back to the file atomically.max_concurrent: 0means unlimited for this OAuth (not usually what you want — set it to match the account's practical parallelism).
| Method | Path | Notes |
|---|---|---|
| POST | /v1/messages |
Streaming (stream:true) OK |
| POST | /v1/messages/count_tokens |
Pass-through |
| GET | /status |
Auth pool + usage snapshot |
| GET | /healthz |
Liveness |
| GET | /admin/ |
Web admin panel (if configured) |
Set admin_token in config.yaml and open http://<host>:<port>/admin/.
The panel is a single embedded HTML page (Preact + Tailwind via CDN) that
lets you:
- view every OAuth / API-key credential with live slot usage, quota status, expiry, and accumulated token usage;
- toggle disabled, edit
max_concurrentandproxy_url, rename labels; - force-refresh an OAuth token, clear a quota-exceeded flag;
- upload a new OAuth JSON file or delete one.
- Sign in with Claude — initiates the Anthropic OAuth flow (PKCE) in a new tab, lets you paste the callback URL back, exchanges the code for tokens, and saves a new credential file. Optional proxy for the token exchange.
API keys are read-only in v1 — edit config.yaml and restart to change
them.
Each line in logs/requests-YYYY-MM-DD.jsonl:
{"ts":"2026-04-14T10:23:45.123Z","client":"alice-laptop",
"client_token":"sk-cli…yyyy","auth_id":"alice.json","auth_label":"alice",
"auth_kind":"oauth","model":"claude-sonnet-4-6",
"input_tokens":1234,"output_tokens":567,
"cache_read_tokens":100,"cache_create_tokens":50,
"cost_usd":0.01155,"status":200,"duration_ms":1842,
"stream":true,"path":"/v1/messages","attempts":1}clientis thenamefromaccess_tokens(or the budgetlabelas fallback), empty if neither set.client_tokenis masked (first 6 + last 4 chars).attempts> 1 when an initial credential was quota-flagged and the request fell through to another one.- One line per terminal outcome (success or final 4xx/5xx). Intermediate retries are not logged as separate entries.
Parse with e.g. jq -c 'select(.client=="alice-laptop" and .model!="unknown") | [.ts,.model,.cost_usd] | @tsv' logs/requests-*.jsonl.
OAuth JSON files produced by the original
CLIProxyAPI — both the
manual claude setup-token output and files created by its own login
flow — drop into auth_dir unchanged. Extra keys (id_token,
last_refresh, etc.) are preserved on save. The only caveat: files
imported from upstream have no max_concurrent, so they load as
unlimited (∞). Set a cap in the admin panel if you want slot-based
routing to take effect.
Clients authenticate with Authorization: Bearer <token> matching one of the
access_tokens in config.yaml. Each distinct token is one "client session"
and occupies one OAuth slot while active.
On each request for a client token:
- If that client already has a sticky OAuth assignment and that OAuth is healthy (not disabled, not quota-exceeded), reuse it.
- Otherwise pick the healthy OAuth with the fewest active sessions that
still has spare
max_concurrentcapacity. - If no OAuth has capacity, pick any usable API key.
- If the upstream returns 401/403/429/529, the credential is flagged (quota-exceeded where applicable) and the request is retried on a different credential (up to 4 attempts total).
MIT. See LICENSE for the full text and the attribution note to CLIProxyAPI.