Skip to content

Connect the Codex CLI

Point the OpenAI Codex CLI at shunt and have it load-balanced across a ChatGPT/Codex OAuth account pool.

Updated View as Markdown

Every other connection guide routes Claude Code to a backend. This one runs the opposite direction: point the OpenAI Codex CLI at shunt and let shunt load-balance it across a pool of ChatGPT/Codex OAuth accounts. shunt relays the Codex CLI’s OpenAI Responses traffic verbatim — no Anthropic translation — so the CLI talks the same wire protocol to shunt that it would talk directly to chatgpt.com.

This is the Inbound Codex Endpoint in practice; that page is the behavior spec (failover table, exhaustion semantics, reload rules). The steps below are the end-to-end connection walkthrough. Codex CLI config keys quoted here come from OpenAI’s configuration reference and authentication docs.

1. Enable the endpoint on shunt

The inbound endpoint is opt-in. Add the table and point it at a chatgpt_oauth provider (the default provider name is codex):

# shunt config.toml
[server.codex_endpoint]
provider = "codex"        # must be a chatgpt_oauth provider; "codex" is the built-in default

codex is a built-in chatgpt_oauth provider, so no [providers.codex] block is required to enable the endpoint. To point it at a differently-named provider, declare one as a [providers.<name>] table (see the configuration reference) and set provider = "<name>" above.

shunt check                # validates the endpoint's provider exists + is chatgpt_oauth
shunt run

When [server.codex_endpoint] is absent, none of the routes are registered and shunt’s default HTTP surface is unchanged. When present, startup validation rejects an unknown provider or one whose auth isn’t chatgpt_oauth — the endpoint injects the operator’s Codex bearer, so only a chatgpt_oauth provider qualifies. See the configuration reference for every key.

2. Point the Codex CLI at shunt

The Codex CLI appends /responses to whatever base URL it uses (Codex speaks the OpenAI Responses wire protocol — wire_api = "responses" is the only value it supports), so shunt registers three routes and any client shape below lands on one:

Codex CLI ~/.codex/config.toml shunt route it hits
custom provider base_url = ".../v1" POST /v1/responses
openai_base_url = ".../v1" POST /v1/responses
chatgpt_base_url = ".../backend-api/codex" POST /backend-api/codex/responses

Recommended — a custom model provider. With requires_openai_auth = false it is unauthenticated from the CLI’s view, so it needs no local codex login at all — shunt supplies the account — and it supports either way of presenting the shunt token (step 3):

# ~/.codex/config.toml
model_provider = "shunt"          # select it as the active provider
model = "gpt-5.6-sol"             # an entitled slug — see step 5

[model_providers.shunt]
name = "shunt"
base_url = "http://127.0.0.1:3001/v1"
wire_api = "responses"            # the only supported value; also the default
requires_openai_auth = false      # shunt handles auth; the CLI needs no ChatGPT/API login here

Simpler — override a base URL (no provider block):

# ~/.codex/config.toml — pick one
openai_base_url  = "http://127.0.0.1:3001/v1"                 # + OPENAI_API_KEY = your shunt token
chatgpt_base_url = "http://127.0.0.1:3001/backend-api/codex"  # loopback only (ChatGPT login mode)

openai_base_url + OPENAI_API_KEY=<shunt-token> is the LiteLLM/llmgateway idiom — the CLI sends the token as Authorization: Bearer, which shunt accepts (step 3), so this shape works even with [server.auth]. chatgpt_base_url keeps the CLI in ChatGPT-login mode and sends its own ChatGPT token, which shunt can’t accept as a client token — use it only on an ungated (loopback) shunt. Both run the CLI in its own OpenAI/ChatGPT auth mode, so both need a local codex login; the custom provider above avoids even that.

The CLI’s own local ~/.codex/auth.json login is irrelevant to which account answers — every request draws an account from shunt’s pool. A loopback base_url may stay plain http://; use https:// for anything remote. Do not set supports_websockets = true on the shunt provider — this endpoint is HTTP/SSE-only (see below).

3. Present the shunt client token (when [server.auth] is set)

If shunt has [server.auth] configured — recommended for anything beyond loopback — present the shunt client token one of two ways; shunt accepts either.

A. As an OpenAI-style Bearer key — the LiteLLM/llmgateway idiom. Set the shunt token as the API key and the CLI sends it as Authorization: Bearer <shunt-token>:

# ~/.codex/config.toml — built-in openai provider (no provider block needed).
# Set the base URL here, NOT via the OPENAI_BASE_URL env var (see the caution in step 2).
openai_base_url = "http://127.0.0.1:3001/v1"
# then present only the token via env — the CLI sends it as Authorization: Bearer
export OPENAI_API_KEY="<shunt-token>"
# or a custom provider — env_key becomes the Bearer
[model_providers.shunt]
base_url = "http://127.0.0.1:3001/v1"
wire_api = "responses"
env_key = "SHUNT_TOKEN"      # reads $SHUNT_TOKEN, sent as Authorization: Bearer

B. As the x-shunt-token header (or whatever [server.auth].header names). Only a custom provider can attach a header; keep the secret out of config.toml with env_http_headers:

[model_providers.shunt]
name = "shunt"
base_url = "http://127.0.0.1:3001/v1"
wire_api = "responses"
requires_openai_auth = false
env_http_headers = { "x-shunt-token" = "SHUNT_TOKEN" }   # reads $SHUNT_TOKEN
export SHUNT_TOKEN="<token>"

(Or hardcode http_headers = { "x-shunt-token" = "<token>" }.) Either way the token is checked against [server.auth]’s list; a missing or wrong token returns 401 authentication_error. Without [server.auth] at all the endpoint is open to anyone who can reach it — fine for loopback, not for a shared gateway. The client’s bearer/header is used only to authenticate to shunt — it is stripped and never forwarded to the Codex backend.

4. Provision the account pool on shunt

The endpoint reuses the Codex Multi-Account pool unchanged — provision accounts on shunt’s host, not the CLI’s:

codex login                       # sign in to a ChatGPT account (browser flow)
shunt login codex --name main     # capture it into shunt's store
# Each additional pool account needs its own login + capture (a separate ChatGPT
# account): `codex login` then `shunt login codex --name backup`, and so on.
# shunt config.toml
[[providers.codex.accounts]]
name = "main"

[[providers.codex.accounts]]
name = "backup"     # each declared name must be provisioned above; an unprovisioned
                    # account is skipped on failover (its credential fails to resolve)

Selection is session-sticky: the Codex CLI’s own session-id request header keys the account, so one conversation stays on one account for as long as it stays healthy, then fails over (429 → rotate, 401 → refresh + retry, 5xx → cool down + rotate). A successful pooled response carries an x-shunt-account: <name> header. On a shared gateway with [server.auth] enabled, the sticky key is scoped per authenticated client, so different clients that happen to send the same session-id use distinct sticky keys — preventing cross-client sticky-key collisions. (The keys are distinct; two distinct keys can still hash to the same account.)

With no [[providers.codex.accounts]] configured and an empty store, the endpoint falls back to the single default ~/.codex/auth.json credential — no pool, no failover — so one Codex login works the moment [server.codex_endpoint] is set.

5. Pick an entitled model

6. Verify

A raw Responses request against any of the three routes should relay verbatim and (when pooled) return an x-shunt-account header:

curl -N -i -X POST http://127.0.0.1:3001/v1/responses \
  -H "content-type: application/json" \
  -H "x-shunt-token: <token>" \
  -d '{"model":"gpt-5.6-sol","input":"say hi","stream":true}'
  • A 200 with content-type: text/event-stream and an x-shunt-account: header ⇒ the pool served the request and the SSE is relayed unchanged.
  • A 401 authentication_error ⇒ missing/invalid x-shunt-token (step 3).
  • A 400 naming the model ⇒ the account isn’t entitled to that slug (step 5).

Then run the real Codex CLI a turn — it round-trips through the pool with no code change on the CLI side beyond the base URL. See Inbound Codex Endpoint for how this differs from the /v1/messages path and HTTP Endpoints for the registered routes.

HTTP/SSE only

Even when the target provider has websocket = true, this endpoint always uses the HTTP transport, so leave supports_websockets off (its default) on the Codex CLI’s shunt provider. The experimental Codex WebSocket v2 transport is out of scope for this endpoint and tracked as a follow-up.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close