cp .env.example .env # set PROXY_API_KEY
docker compose up -d --build # or: go run ./cmd/apiExposes OpenAI and Anthropic endpoints, translates each request into the private
chatgpt.com/backend-api/codex/* format, and routes it across your logged-in
Codex accounts.
The upstream API is private and undocumented, so it can change without notice. Built for local and small-scale use.
- Two API surfaces, one backend — OpenAI Chat Completions, Responses, and Images plus Anthropic Messages.
- Streaming everywhere — SSE, a persistent WebSocket for Responses, or plain JSON.
- Multi-account rotation — least-used, round-robin, or sticky, with cooldowns and quota awareness.
- Device login — add an account by opening a URL. No cookie scraping, no pasted tokens.
- Tools and structured output — custom tools, legacy
functions,json_schema,json_object.
Needs Docker or Go 1.26.x, and a long random PROXY_API_KEY.
export PROXY_URL=http://localhost:8080
export PROXY_API_KEY=change-me-to-a-long-random-stringAdd an account — start a device login, open the returned auth_url, then poll
until status is ready:
curl -sS -X POST "${PROXY_URL}/admin/accounts/device-login/start" \
-H "Authorization: Bearer ${PROXY_API_KEY}"
curl -sS "${PROXY_URL}/admin/accounts/device-login/<login_id>" \
-H "Authorization: Bearer ${PROXY_API_KEY}"Then:
curl -sS "${PROXY_URL}/v1/chat/completions" \
-H "Authorization: Bearer ${PROXY_API_KEY}" \
-H "Content-Type: application/json" \
-d '{"model":"gpt-5.6-sol","messages":[{"role":"user","content":"hello"}]}'Point any OpenAI client at http://localhost:8080/v1 using PROXY_API_KEY as
the API key. For Claude Code or another Anthropic client:
export ANTHROPIC_BASE_URL=http://localhost:8080
export ANTHROPIC_API_KEY="${PROXY_API_KEY}"
export ANTHROPIC_MODEL=gpt-5.6-solUse a model ID from GET /v1/models — there are no claude-* aliases.
Every route except GET /health/live needs the key, as either
Authorization: Bearer <key> or X-API-Key: <key>.
POST /v1/completions POST /v1/messages
POST /v1/chat/completions POST /v1/messages/count_tokens
POST /v1/responses GET /v1/models
GET /v1/responses (WebSocket) GET /v1/models/:model_id
POST /v1/responses/compact GET /health
POST /v1/images/generations GET /health/live
POST /v1/images/edits
Text, image, and file inputs, reasoning, hosted web search, streaming and non-streaming. The model catalog comes from upstream at runtime.
Gotchas:
GET /v1/responsesis a WebSocket, not SSE. First message must beresponse.create; later turns useresponse.createorresponse.append. No[DONE]marker./v1/chat/completionsalso accepts a Responses-shaped body whenmessagesis omitted.- Images default to
gpt-image-2. If the native endpoint returns404,405, or501, it falls back to the Responses image tool — one image, data URL. - Anthropic requests need
Anthropic-Version: 2023-06-01. Sampling controls, stop sequences,max_tokenstruncation, and thinking budgets are accepted but advisory; Codex has no equivalent. - Audio input is rejected. Request bodies must be identity or zstd.
Exact rules: docs/TRANSLATION.md, docs/ANTHROPIC.md.
GET /admin/accounts
POST /admin/accounts/device-login/start
GET /admin/accounts/device-login/:login_id
DELETE /admin/accounts/:account_id
PATCH /admin/accounts/:account_id
GET /admin/accounts/:account_id/usage
POST /admin/accounts/:account_id/refresh
GET /admin/rotation
PUT /admin/rotation
Rotation is least_used, round_robin, or sticky. An account is skipped when
its status is disabled, expired, or banned, a cooldown is active, its
token is missing, or its quota is spent. code_review_rate_limit is tracked but
does not affect routing.
A failed OAuth refresh only expires an account on invalid_grant. Anything else
keeps it active behind a 60-second cooldown.
Details: docs/MULTI_ACCOUNT_ROTATION_STRATEGY.md.
compose.yaml persists state in the chatgpt-codex-proxy-data volume and runs
with basic hardening. Dockerfile builds the API server alone.
docker compose up -d --build
docker compose logs -fConfig is environment-only: PROXY_API_KEY (required), PORT (8080),
DATA_DIR (data, or /app/data in Docker), DEBUG_LOG_PAYLOADS (false).
${DATA_DIR} holds accounts.json — accounts, OAuth tokens, labels, status,
quota, cooldowns — and models-cache.json. Continuation state and in-flight
device logins are memory-only and do not survive a restart.
- Gin accepts an OpenAI- or Anthropic-shaped request.
- Adapters normalize it into one internal turn model.
- The proxy picks a ready account, translates, and calls Codex over SSE or WebSocket.
- Upstream events convert back to whichever protocol the client asked for.
POST https://chatgpt.com/backend-api/codex/responses
POST https://chatgpt.com/backend-api/codex/responses/compact
GET https://chatgpt.com/backend-api/codex/usage
GET https://chatgpt.com/backend-api/codex/models
WSS https://chatgpt.com/backend-api/codex/responses
https://auth.openai.com/api/accounts/deviceauth/*
https://auth.openai.com/oauth/token
chatgpt-codex-proxy/
├── cmd/api/ # server entrypoint
├── internal/
│ ├── server/ # Gin routing and handlers
│ ├── openai/ anthropic/ # public protocol adapters
│ ├── turn/ translate/ # internal turn model and translation
│ ├── codex/ codexauth/ # private upstream client and OAuth
│ ├── accounts/ # account store
│ ├── accountmanager/ # rotation, cooldowns, quota routing
│ ├── devicelogin/ # device-auth onboarding
│ ├── conversation/ # continuation state and affinity
│ ├── models/ # runtime model catalog
│ ├── admin/ middleware/ # admin API and auth
│ └── store/ config/ # persistence and configuration
├── test/integration/ # live compatibility suite
└── docs/ # upstream and translation references
go test ./...Live tests, against a proxy you already have running:
OPENAI_API_KEY=change-me-to-a-long-random-string \
OPENAI_MODEL=gpt-5.6-sol \
OPENAI_BASE_URL="${PROXY_URL}/v1" \
go test -tags=live ./test/integration -v -count=1- docs/TRANSLATION.md — OpenAI-to-Codex translation and compatibility rules
- docs/ANTHROPIC.md — Anthropic mapping, streaming, token counting, limits
- docs/MULTI_ACCOUNT_ROTATION_STRATEGY.md — account selection and quota routing
- docs/CODEX_API_DOCS.md — private upstream behavior, inferred from this codebase
- Upstream is private and may change without notice.
- Device auth is the only onboarding flow.
- Continuation state is in memory and expires with its TTL.
- Deliberately small; it does not chase every edge of the public OpenAI platform.