Skip to content

OpenAI

Route mapped models to the OpenAI Responses API with an OPENAI_API_KEY — translation, effort, token counting, and tool search.

Updated View as Markdown

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 | claude

Or 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 counting

Ordered [[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" and stop_sequence naming 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.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close