The built-in openai provider routes a mapped model’s inference to the OpenAI platform
API (api.openai.com), billed per token against an OPENAI_API_KEY. It is a
kind = "responses" provider: shunt translates Claude Code’s Anthropic Messages request into
the OpenAI Responses API, streams the reply back, and translates it again — tools, images,
and streaming included.
Quick start
Let a coding agent wire it up for you — shunt add prints an embedded setup blueprint
(offline and read-only; the agent edits the config, the command never does):
shunt add upstream openai --print | claudeOr follow the manual steps below.
Configure the upstream
The openai preset supplies kind = "responses", base_url = "https://api.openai.com/v1"
(shunt appends /responses), and API-key auth from OPENAI_API_KEY:
[[upstreams]]
name = "anthropic"
provider = "anthropic" # keep Anthropic as the default for unrouted models (e.g. claude-*)
[[upstreams]]
name = "openai"
provider = "openai"
# effort = "high" # optional default reasoning effort
# count_tokens = "tiktoken" # default; "estimate" opts out of local countingOrdered [[upstreams]] replace shunt’s built-in providers, so the config must declare the
anthropic default it still falls back to (server.default_provider defaults to anthropic).
Explicit fields override preset defaults. The legacy [providers.openai] table form remains
supported — but do not mix [[upstreams]] and [providers.*] in one file.
Credentials
Export the key in the environment that launches shunt — never write it into the config:
export OPENAI_API_KEY='...'shunt check validates the config’s structure but does not read the key’s value — if
OPENAI_API_KEY is unset, the first request routed to openai returns an authentication error.
Models & routing
Route ids exactly, by prefix, or advertise a Claude-named alias to model discovery:
# Recommended: advertise + route + translate in one declaration
[[models]]
id = "claude-gpt-5-4-via-openai"
display_name = "GPT-5.4 (via OpenAI)"
[models.upstream_model]
openai = "gpt-5.4"
# Or catch every gpt-* id a client sends
[[route_prefixes]]
prefix = "gpt-"
provider = "openai"Verify current model availability on your OpenAI account before selecting a slug. Unlike the ChatGPT-account Codex backend, the platform API accepts its regular public slugs.
Reasoning effort, context, and token counting
The Responses translation supports a per-provider or per-route reasoning-effort dial
(low … max) and local tiktoken-based count_tokens so Claude Code’s context accounting keeps
working. Both are shared with the other Responses providers and documented in
Effort & Context.
Tool search. With Claude Code’s ENABLE_TOOL_SEARCH=true, shunt maps deferred tool discovery on
the Responses path onto the native client-executed tool_search protocol on GPT-5.4+ models. This
provider targets api.openai.com, one of the two hosts the unset (“auto”) default already trusts
to implement tool_search items — alongside the ChatGPT/Codex backend — so it gets native for free;
a custom OpenAI-compatible endpoint (LiteLLM, vLLM, OpenRouter, self-hosted) isn’t auto-enabled the
same way and needs tool_search = true to opt in. Set tool_search = false here to force the text
shim instead. See Tool search.
Stop sequences
The Responses API has no stop parameter — Chat Completions has one, Responses does not — so
a client’s Anthropic stop_sequences cannot be forwarded upstream. shunt emulates them
gateway-side in the Responses translation instead, on every Responses provider and every
transport (HTTP and WebSocket, streaming and non-streaming):
- assistant text is truncated at the first occurrence of any stop sequence, and the text after it never reaches the client;
- the turn ends with
stop_reason: "stop_sequence"andstop_sequencenaming the match, exactly as a native Anthropic upstream would report it; - the upstream request is aborted at the match rather than left running — on streaming HTTP and on both WebSocket paths. Non-streaming HTTP is the exception: it reads the whole response body before translating any of it, so it truncates like the others but lets the upstream finish. Tokens the upstream generated between the match and the abort are still billed by the upstream — negligible for the short completions stop sequences are typically used for.
One consequence to be aware of: the stop ends the turn before the upstream’s response.completed
usage can be applied, so shunt substitutes its own local input estimate and reports
output_tokens: 0 — in message_delta.usage on a streaming turn, in the response’s usage on a
non-streaming one. The estimate needs the provider’s count_tokens = "tiktoken" opt-in; without
it, a stopped turn reports input_tokens: 0.
Verify
shunt check # -> config ok
shunt run
curl -sS http://127.0.0.1:3001/v1/models
curl -sS http://127.0.0.1:3001/v1/messages \
-H 'anthropic-version: 2023-06-01' \
-H 'content-type: application/json' \
-d '{"model":"claude-gpt-5-4-via-openai","max_tokens":16,"messages":[{"role":"user","content":"Reply with OK."}]}'Confirm the response’s x-gateway-upstream header names openai, then
point Claude Code at shunt.