Every other guide on this site routes Claude Code to another backend. shunt can also run the opposite direction: an opt-in raw OpenAI Responses passthrough that lets the Codex CLI point its own base_url at shunt and be load-balanced across a ChatGPT/Codex OAuth account pool. It is opt-in: when [server.codex_endpoint] is absent, none of those routes are registered and shunt’s default HTTP surface is unchanged.
This builds on the same account pool as Codex Multi-Account — selection, cooldowns, and refresh are shared unchanged. See the M11 behavior specification for the full spec, including the exact failover table and reload semantics.
For the end-to-end setup walkthrough — enabling the endpoint, pointing the Codex CLI at shunt, client auth, account provisioning, and picking an entitled model — follow Connect the Codex CLI. This page focuses on what the endpoint does; that guide is the how to connect checklist.
Enable the endpoint
[server.codex_endpoint] # all keys optional; default shown
provider = "codex" # must be a chatgpt_oauth providershunt check
shunt runStartup validation rejects an unknown provider or one that doesn’t use auth = "chatgpt_oauth" — the endpoint injects the operator’s Codex bearer, so only a chatgpt_oauth provider qualifies. See the configuration reference for every key and default, and HTTP Endpoints for the registered routes.
Client analytics sink
The Codex CLI also posts product analytics to the base URL. shunt accepts both paths the CLI can produce:
POST /backend-api/codex/analytics-events/eventsPOST /codex/analytics-events/events
These routes use the same [server.auth] policy as the Responses routes but never forward telemetry upstream, because choosing one pooled account would misattribute the client event to that account. They always return 200 {} after authentication, including for malformed, unreadable, or oversized bodies.
The payload and event properties are neither logged nor exported. shunt records only the sanitized event_type as the event attribute on the opt-in shunt.codex_client_events counter: names may contain lowercase ASCII letters, digits, ., _, and -, up to 64 bytes; invalid names become other, and unrecognized batches become unparsed. Without Sentry or OpenTelemetry metrics enabled, this is a pure discard sink.
Point the Codex CLI at shunt
The Codex CLI always appends /responses to whatever base URL it uses, so either ~/.codex/config.toml shape works:
Mirror the ChatGPT backend’s base URL:
chatgpt_base_url = "http://127.0.0.1:3001/backend-api/codex"Or a custom model provider (the top-level model_provider must select it, or the CLI keeps its built-in provider):
model_provider = "shunt"
[model_providers.shunt]
base_url = "http://127.0.0.1:3001/v1"
wire_api = "responses"With the custom provider (add requires_openai_auth = false so the CLI needs no local login), the Codex CLI’s own ~/.codex/auth.json becomes irrelevant once pointed at shunt — the account comes from shunt’s pool on every request. The chatgpt_base_url shape instead keeps the CLI in ChatGPT-login mode, so it still needs its local login and works only against an ungated endpoint: its ChatGPT bearer is not the configured shunt token, so [server.auth] rejects it.
Client authentication
If shunt has [server.auth] configured — recommended for anything beyond loopback — present the client token either as an OpenAI-style Bearer key (OPENAI_API_KEY / a custom provider’s env_key, the LiteLLM/llmgateway idiom) or as the x-shunt-token header:
# A. Bearer — built-in openai provider. Set the base URL in ~/.codex/config.toml,
# NOT via the OPENAI_BASE_URL env var: the env var leaves the CLI's Responses
# WebSocket pointed at wss://api.openai.com, so it bypasses shunt. See
# "Point the Codex CLI at shunt" in the connect guide.
openai_base_url = "http://127.0.0.1:3001/v1"export OPENAI_API_KEY="<shunt-token>" # sent as Authorization: Bearer# B. Header — a custom provider carries it (use env_http_headers to keep it out of the file):
[model_providers.shunt]
base_url = "http://127.0.0.1:3001/v1"
wire_api = "responses"
http_headers = { "x-shunt-token" = "<token>" }Without [server.auth], the endpoint is open to anyone who can reach it — acceptable for loopback or personal use, not for a shared gateway. The client’s presented credential is used only to authenticate to shunt: it (and any Authorization the CLI happens to send) is stripped and never forwarded upstream. Because the inbound client is a real Codex CLI, the passthrough forwards its request headers verbatim (version, originator, OpenAI-Beta, x-codex-*, …) and swaps in only the selected pool account’s Authorization bearer + chatgpt-account-id. See Connect the Codex CLI for the full auth walkthrough.
Account provisioning
Reuses the same pool as Codex Multi-Account:
codex login
shunt login codex --name main[[providers.codex.accounts]]
name = "main"With no [[providers.codex.accounts]] configured and an empty shunt account store, the endpoint falls back to the single default ~/.codex/auth.json credential — no pooling, no failover — so a single Codex login works the moment [server.codex_endpoint] is set. (The handler first scans the account store and pools any accounts it discovers, so imported store accounts still enable pooling.)
What’s different from /v1/messages
- No translation. The inbound Responses body is forwarded upstream byte-for-byte, and the upstream response — SSE or JSON, success or error — is relayed back verbatim (status and
content-typepreserved). There is no Anthropic Messages ⇄ Responses translation step at all. - No model-based routing. Every request goes to the one provider named in
[server.codex_endpoint]; the body’smodelfield forwards through as-is and never selects a provider. - Exhaustion relays verbatim. If every pooled account is tried and at least one upstream response came back, shunt relays that last response unchanged rather than re-shaping it into an Anthropic-style error, since a Responses client expects the raw shape it would have gotten from the real ChatGPT backend.
- Gateway-owned errors are OpenAI-shaped. When the failure is shunt’s own — a bad or missing client token (
401), an unresolvable pool with no upstream response (502), an oversized request body, or an unconfigured endpoint — shunt returns it in the OpenAI Responses error shape ({"error":{"message":…,"type":…,"code":null}}) with the same status code, so the Codex CLI parses it through its own error path instead of the Anthropic{"type":"error",…}envelope. Relayed upstream errors (429/4xx/5xx from the backend) still pass through verbatim. - HTTP/SSE only. Even when the target provider has
websocket = true, this endpoint always uses the HTTP transport.
Security
- Gate this endpoint with
[server.auth]on anything beyond loopback — the provider injects a real Codex bearer on every request. - Nothing about the client’s own credential reaches the Codex backend; the passthrough forwards the Codex CLI’s own request headers verbatim and swaps in only the selected pool account’s bearer +
chatgpt-account-id(the shunt client-token header is stripped, never forwarded). - The route set is decided once at boot. Toggling
[server.codex_endpoint]on or off at runtime logs a warning that a restart is required; a reload can still change which provider it targets.