The codex provider routes a mapped model’s inference to your ChatGPT / Codex
subscription instead of an API key. It reuses the credential the Codex CLI already wrote to
~/.codex/auth.json, so there’s nothing to paste and no per-token billing — the request is
authenticated as your ChatGPT account and answered by the same backend the codex CLI talks to.
This page is the end-to-end setup. It links out to the deeper topic pages (Effort & Context, Model Discovery, Providers) rather than repeating them. To pool several ChatGPT accounts behind this provider instead of a single login, see Codex Multi-Account.
Quick start
Let a coding agent wire it up for you — shunt add prints this provider’s setup blueprint
(offline and read-only; the agent edits the config, the command never does):
shunt add upstream codex --print | claudeOr follow the manual steps below.
How it works
codex is a built-in kind = "responses" provider: shunt translates Claude Code’s Anthropic
Messages request into the OpenAI Responses API, sends it to the ChatGPT-account Codex backend,
and translates the streamed reply back. Three things make it “Codex” rather than plain OpenAI:
| Aspect | Value |
|---|---|
| Endpoint | <base_url>/codex/responses |
| Auth | ChatGPT OAuth from ~/.codex/auth.json, auto-refreshed |
| Responses dialect | Chatgpt flavor — drops params codex never sends (e.g. max_output_tokens), sends store: false, round-trips encrypted reasoning |
The dialect is keyed on auth = "chatgpt_oauth", not the provider name.
Request bodies on this flavor are zstd-compressed (content-encoding: zstd), matching what the Codex CLI itself sends to the same backend. A long agentic turn re-sends its whole history every request, so this cuts the uploaded bytes substantially — most on the largest turns. It is on by default and applies only to the ChatGPT/Codex flavor; set request_compression = false on the provider to send plain JSON.
When Codex accounts are pooled, successful backend responses also populate the admin Pool health dashboard from the reported x-codex-* rate-limit windows. A roughly 5-hour window appears in 5h, a roughly 7-day window appears in 7d, and unsupported daily/monthly windows are ignored. Codex has no 7d_oi analog. The recorded windows also feed quota-aware pool selection — see Codex multi-account.
1. Log in
Log in once with the Codex CLI. shunt reads and refreshes the file it writes — it does not run its own login for Codex.
codex loginThis creates ~/.codex/auth.json. If that file is missing, has no tokens, or its refresh token
is gone, shunt returns an authentication_error telling you to run codex login again.
2. The provider block (optional)
codex is built in — you don’t need to declare it. This is the full default; a partial table
overrides only the keys you set (config maps deep-merge):
[providers.codex]
kind = "responses"
base_url = "https://chatgpt.com/backend-api" # shunt appends /codex/responses
auth = "chatgpt_oauth" # read + auto-refresh ~/.codex/auth.json
# effort = "high" # optional default reasoning effort (§4)
# count_tokens = "tiktoken" # default; "estimate" opts outCommon overrides: pin a default effort for all Codex traffic, or set
count_tokens = "estimate". api_key_env / api_key_header don’t apply to chatgpt_oauth —
the credential comes from the auth file. See the Configuration Reference
for every key.
3. Route a model to codex
A request’s model id picks the provider. Precedence: matching [models.upstream_model] entry →
exact [[routes]] → [[route_prefixes]] → server.default_provider.
[[routes]]
model = "gpt-5.6-sol" # the id Claude Code sends (see §4 below)
provider = "codex"
# upstream_model = "gpt-5.6-sol" # optional: forward a different slug upstream
# effort = "high" # optional: pin effort for this routeupstream_model lets the id Claude Code sends differ from the slug the backend receives — the
mechanism behind discovery aliases and a way to swap the real slug
without touching your Claude Code env.
4. Select the model in Claude Code
Claude Code’s /model picker only honors discovery ids beginning with claude/anthropic, so a
raw gpt-* id needs one of two paths — they split on the claude- prefix and don’t overlap:
claude-… discovery alias |
non-claude- id (gpt-5.6-sol) |
|
|---|---|---|
/model picker via discovery |
✅ auto-listed, many models | ❌ dropped by Claude Code |
ANTHROPIC_CUSTOM_MODEL_OPTION |
❌ not honored | ✅ adds to picker (one id) |
CLAUDE_CODE_MAX_CONTEXT_TOKENS window |
❌ ignored → 200k | ✅ real window |
Primary path — add the slug to the picker directly:
export ANTHROPIC_CUSTOM_MODEL_OPTION="gpt-5.6-sol"That id is exactly what shunt routes on, so it must resolve through a matching
[models.upstream_model] entry, [[routes]], or [[route_prefixes]] rule. This is the recommended
path — it’s the only one that also lets you set an accurate context window. For auto-listing several
Codex models in the picker instead, use a claude--named discovery alias
(accepting the 200k window trade-off).
Put a subagent on a Codex slug
A subagent can run on a Codex slug while the main session stays on Claude. The model: frontmatter
field accepts any string (unlike the Agent/Task tool’s model parameter, which only takes the
built-in aliases). To point an existing subagent at gpt-5.6-sol, edit its
.claude/agents/<name>.md and set model::
---
name: researcher
description: Deep research agent.
model: gpt-5.6-sol # was: sonnet (or absent → inherited)
---
<the agent's system prompt — unchanged>Spawn it without a model override (the tool parameter outranks frontmatter). Resolution order:
CLAUDE_CODE_SUBAGENT_MODEL > tool model > frontmatter > inherit. To force every subagent
onto one slug, set export CLAUDE_CODE_SUBAGENT_MODEL="gpt-5.6-sol".
Either way the slug needs explicit routing through a matching [models.upstream_model] entry,
[[routes]], or [[route_prefixes]] rule and, being non-claude-, obeys
CLAUDE_CODE_MAX_CONTEXT_TOKENS — the window follows the id automatically.
Remap the tier aliases to Codex
Instead of adding one custom id, repoint Claude Code’s built-in tier aliases at Codex slugs, so the whole session’s tier system resolves to your ChatGPT subscription (model-config env vars).
| Env var | Controls |
|---|---|
ANTHROPIC_DEFAULT_HAIKU_MODEL |
the haiku alias and the background “small-fast” model |
ANTHROPIC_DEFAULT_SONNET_MODEL |
the sonnet alias |
ANTHROPIC_DEFAULT_OPUS_MODEL / ANTHROPIC_DEFAULT_FABLE_MODEL |
the opus / fable aliases |
A two-tier setup — haiku → gpt-5.6-luna, sonnet → gpt-5.6-sol:
export ANTHROPIC_DEFAULT_HAIKU_MODEL="gpt-5.6-luna"
export ANTHROPIC_DEFAULT_SONNET_MODEL="gpt-5.6-sol"
# nicer picker labels (the _NAME/_DESCRIPTION companions work on a gateway)
export ANTHROPIC_DEFAULT_SONNET_MODEL_NAME="GPT-5.6-Sol"
export ANTHROPIC_DEFAULT_SONNET_MODEL_DESCRIPTION="ChatGPT/Codex Sol via shunt"
export ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME="GPT-5.6-Luna"
export ANTHROPIC_DEFAULT_HAIKU_MODEL_DESCRIPTION="ChatGPT/Codex Luna via shunt (background tier)"# shunt.toml — both resolved ids need a route
[[routes]]
model = "gpt-5.6-luna"
provider = "codex"
[[routes]]
model = "gpt-5.6-sol"
provider = "codex"Selecting Sonnet in /model now runs gpt-5.6-sol via Codex, and every background/haiku task
runs gpt-5.6-luna — the resolved id is exactly what shunt routes on, so no
ANTHROPIC_CUSTOM_MODEL_OPTION is needed.
5. Reasoning effort
Set the effort with Claude Code’s usual controls (/effort, the /model slider, --effort).
shunt maps it to the Responses reasoning.effort, folding max → xhigh for slugs that don’t
support max (only the gpt-5.6 and gpt-6 families do).
Full precedence and the effort table: Effort & Context.
Fast mode
Codex CLI’s /fast toggle isn’t a model or effort change — it sends the Responses API top-level
field service_tier: "priority", branded “Fast” (faster responses at increased usage). shunt
exposes the same knob as a config key, at both provider and route level, exactly parallel to
effort:
[[routes]]
model = "gpt-5.6-sol"
provider = "codex"
# service_tier = "priority" # optional: opt in to Codex's "Fast" modeAccepted values: fast (legacy alias, normalized to priority), priority, flex, or default
(a client-only sentinel — it’s never sent on the wire). It’s off by default and sent only when
explicitly configured; a route-level value wins over the provider-level default, same as effort —
including a route explicitly set to default, which opts that route out of a provider-level
priority/flex tier instead of inheriting it. Full key reference:
Configuration.
Whatever tier ends up on the wire is also echoed to the backend in the Codex CLI’s
x-codex-routing-hint header, as model=<upstream model>;tier=<tier> (just model=<upstream model>
when no tier is sent). shunt sends that header only on the ChatGPT OAuth path, exactly as the Codex
CLI does — on HTTP requests, and on the WebSocket transport as a handshake header. On a prefix route
or via default_provider that upstream model is the client’s own model string, so the header is
built defensively: it goes out only for a plausible slug (non-empty, at most 128 bytes, ASCII
letters/digits and - _ . : / +), and is otherwise dropped while the request still goes
out.
6. Context window
Claude Code sizes its context bar at a fixed 200k for mapped ids. gpt-5.6-sol’s real window
is 372k (gpt-5.5 is 272k), so raise it for a non-claude- id:
export CLAUDE_CODE_MAX_CONTEXT_TOKENS=372000It’s global (one value per session) and setting it larger than the real window causes
prompt is too long overflow churn — match it to the smallest real window among your mapped
models. shunt rewrites that overflow so Claude Code auto-compacts and retries, but each round-trip
is wasted latency. Details, the live-verified boundary, and count_tokens behavior:
Effort & Context.
Full example
shunt.toml:
[server]
bind = "127.0.0.1:3001"
default_provider = "anthropic"
[providers.codex]
effort = "high" # optional: pin high effort for all Codex traffic
# service_tier = "fast" # optional: opt in to Codex's "Fast" mode
[[routes]]
model = "gpt-5.6-sol"
provider = "codex"Shell (both shunt and Claude Code run with these):
codex login # one-time
./target/release/shunt run # start the gateway
export ANTHROPIC_BASE_URL=http://127.0.0.1:3001
export ANTHROPIC_CUSTOM_MODEL_OPTION="gpt-5.6-sol" # add to /model picker
export CLAUDE_CODE_MAX_CONTEXT_TOKENS=372000 # gpt-5.6-sol's real windowPick gpt-5.6-sol from /model. Everything else in the session still flows to Anthropic
unchanged; only the mapped model’s inference is answered by your ChatGPT/Codex subscription.
Web search
Claude Code’s built-in web search works through the Codex path with no extra setup. When you
enable it, Claude Code sends the hosted web_search_20250305 tool; shunt registers it as the
Responses API’s hosted web_search tool, so the backend actually performs the search instead
of handing it back as an unfulfilled tool call.
- Domain filters carry over — Claude Code’s
allowed_domains/blocked_domainsbecome the Responsesweb_searchfilters. - Applies to the
codex(ChatGPT) andopenai(stock Responses) providers. - xAI / Grok routes don’t support it — Grok’s Responses API only accepts function tools, so
shunt drops the hosted web-search tool there; use a
codexoropenairoute for web search.
Tool search
Claude Code’s tool search — deferring MCP / LSP tool schemas and revealing them on demand via a
ToolSearch tool, so the model’s context isn’t spent on tools it never calls — works through the
Codex path. Enable it client-side:
export ENABLE_TOOL_SEARCH=trueClaude Code disables its optimistic tool search whenever the base URL is not a first-party Anthropic
host — shunt isn’t one — so without this flag every tool’s full schema is sent upstream from turn 1
and the feature is inert (it still works, it just reclaims nothing). The client’s own contract is to
set ENABLE_TOOL_SEARCH=true if your proxy forwards tool_reference blocks — shunt does.
With it on, shunt maps tool search onto the OpenAI Responses API’s own native, client-executed
tool_search protocol for upstreams already known to implement it, and keeps the #43 text shim
everywhere else: Claude Code’s ToolSearch tool becomes a tool_search (execution: "client")
tool, its tool_use becomes a tool_search_call, and the tool_reference result becomes a
tool_search_output item carrying the loaded tools’ full schemas as structured JSON, appended at
the tail of input. That matters because the alternative — folding a revealed tool’s schema back
into the request’s tools array — invalidates the cached prompt prefix on every reveal: tools is
part of that cacheable prefix, shunt re-derives it from client history each turn, and a growing
array forces the backend to re-process the conversation from the tool boundary onward. The native
protocol’s append-only input history keeps tools untouched, so the cached prefix survives each
reveal.
- No
shunt.tomlchange is needed for the default (native) behavior on the built-incodexandopenaiproviders — it’s purely a Claude Code environment variable at theENABLE_TOOL_SEARCHlevel. - Applies to any
kind = "responses"provider; whether a given one gets the native protocol or the shim by default depends on the upstream host — see Native protocol below. - Non-deferred tools (and the hosted
web_searchtool above) are always forwarded; only deferrable tools are progressively revealed.
Native protocol
Requirements — unsupported combinations silently fall back to the #43 text shim, never error. The
shim still withholds each not-yet-revealed deferred tool from tools, but once Claude Code reveals
one, the shim renders its full schema as tool_reference text and re-adds the tool to tools so
the model can call it — sending the schema twice from then on and paying the cache-invalidation cost
described above on every reveal:
- The upstream must be a stock OpenAI or ChatGPT/Codex-flavored Responses backend. xAI/Grok routes always keep the shim.
- The routed model must be gpt-5.4 or later (
gpt-5.4,gpt-5.5, or thegpt-5.6family). Earlier slugs (gpt-5.2and below) keep the shim regardless oftool_search. ENABLE_TOOL_SEARCH=trueis still required on the Claude Code side — this flag only changes how shunt translates that feature upstream, not whether Claude Code defers tools at all.
tool_search under [providers.<name>] is a three-state setting. Left unset (the default,
“auto”), it resolves to native only for a host already verified to implement tool_search items —
the ChatGPT/Codex backend (the built-in codex provider) and api.openai.com (the built-in
openai provider) — and to the text shim for every other OpenAI-compatible endpoint, including a
custom kind = "responses" provider pointed at LiteLLM, vLLM, OpenRouter, or a self-hosted proxy:
most third-party Responses backends don’t implement tool_search items, so auto-enabling native
there would fail the turn instead of degrading gracefully. The native shapes were live-probe
verified against the ChatGPT/Codex backend on gpt-5.6 (2026-07-13,
pleaseai/shunt#86), and tool_search is documented
on the public OpenAI Responses API for gpt-5.4+ models.
Set tool_search = true to opt a custom endpoint into the native path once you’ve confirmed it
implements tool_search items — still subject to the flavor and model requirements above:
[providers.my-litellm]
kind = "responses"
base_url = "https://litellm.internal/v1"
tool_search = trueSet tool_search = false to force the text shim on a provider that would otherwise default to (or
was opted into) the native path:
[providers.codex]
tool_search = falseTroubleshooting
| Symptom | Cause / Fix |
|---|---|
ChatGPT auth not found; run codex login |
No ~/.codex/auth.json (or wrong $CODEX_AUTH_FILE). Run codex login. |
ChatGPT auth tokens missing |
Auth file is in ApiKey mode — that’s the openai provider. Re-codex login with a ChatGPT account. |
400 … not supported when using Codex with a ChatGPT account |
You used a gpt-*-codex slug. Use an entitled non--codex slug. |
Model not found <slug> |
Client-version gating or an unentitled slug — confirm via models.json. |
Effort slider ignored on a gpt-* id |
A route/provider effort override is winning, or no effort level is set. Verified against v2.1.224: a gpt-* id already sends output_config.effort without CLAUDE_CODE_ALWAYS_ENABLE_EFFORT, so that flag is not the fix. |
| Context bar over-reports / compacts early | Set CLAUDE_CODE_MAX_CONTEXT_TOKENS; a discovery alias can’t take it — use a non-claude- id. |
| Web search returns nothing on a Grok route | xAI/Grok’s Responses API doesn’t support web search; shunt drops the tool. Use a codex or openai route. |
| Tool search does nothing / all tool schemas sent every turn | Set ENABLE_TOOL_SEARCH=true — Claude Code disables tool search by default behind a non-Anthropic base URL. shunt forwards tool_reference blocks and reveals deferred schemas on demand. |
| Tool search reclaims no context (still paying the shim’s cache-invalidation cost) | Check the flavor/model gates — native tool_search needs a stock OpenAI/ChatGPT-Codex flavor and a gpt-5.4+ model; unsupported combinations silently fall back to the shim. If both gates pass and you’re on a custom OpenAI-compatible endpoint (LiteLLM, vLLM, OpenRouter, self-hosted), it isn’t auto-enabled — set tool_search = true once you’ve confirmed it implements tool_search items. On a known host (codex/openai), confirm you haven’t set tool_search = false; see Tool search → Native protocol above. |
See the full Troubleshooting reference for more.