The built-in anthropic provider is shunt’s default destination: any model with no matching
route is forwarded to api.anthropic.com unchanged (configurable via server.default_provider).
It is a kind = "anthropic" provider — the upstream already speaks the Anthropic Messages
API, so shunt passes the request through without translation and only decides which credential
goes upstream.
One request field is the exception. A thinking block whose signature shunt minted for another
provider — the Responses, Gemini, and Cursor paths each mint one, because a thinking block needs
the field to be well-formed — is dropped before the request is forwarded, along with an assistant
turn left holding nothing else. Anthropic decrypts that field and rejects a value it did not issue,
so forwarding it would fail the request. A signature Anthropic issued is opaque to shunt and is
never touched. The strip does not read conversation history, so it is not confined to sessions that
used a mapped provider: a thinking block carrying no signature at all is dropped too, since
shunt’s own Responses path emits one when the upstream returned no signature, and Anthropic rejects
a thinking block whose signature it cannot verify either way.
A third-party Anthropic-kind provider — any kind = "anthropic" upstream that is not on
api.anthropic.com and not a claude_oauth pool, such as MiniMax, Kimi, or Zhipu — may put a
signature on its thinking blocks that Anthropic never issued. On the way to the client, streamed or not,
shunt wraps that signature in an envelope of its own carrying a digest of the provider’s base_url (trailing / trimmed), never the URL itself, so the URL and any credentials in it never reach the client. The digest is unsalted and stable, though, so it confirms a guess: anyone who can guess the whole base_url — a well-known endpoint, or a weak credential embedded in one — can check the guess against it. When a later
turn goes back to a provider with the same base_url, shunt unwraps it and forwards the original
signature byte-for-byte, so that provider’s extended-thinking continuation keeps working; sent
anywhere else, Anthropic included, the envelope counts as shunt-minted and the block is dropped as
above. Switching between a third-party Anthropic-kind provider and Anthropic therefore drops that
provider’s thinking blocks rather than failing the request, and returning to the same provider
restores them. Responses from api.anthropic.com and from claude_oauth pools are never wrapped.
| Auth mode | Credential | Use case |
|---|---|---|
passthrough (default) |
the caller’s own x-api-key / Authorization |
transparent proxy — each client brings its own key or OAuth token |
api_key |
a server-side key from an env var | re-key every request with one operator-owned API key |
claude_oauth |
one or more shunt-managed Claude subscription accounts | spend Claude subscriptions, with pooling and quota-aware rotation |
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 anthropic --print | claudeOr follow the manual steps below.
Configure the upstream
anthropic is built in — with no config at all, shunt already forwards unrouted models to it
as passthrough. To declare it explicitly (for ordered failover or to change auth), use the
preset, which supplies kind = "anthropic", base_url = "https://api.anthropic.com", and
auth = "passthrough":
[[upstreams]]
name = "anthropic"
provider = "anthropic"Explicit fields override preset defaults. The legacy [providers.anthropic] table form remains
supported — but do not mix [[upstreams]] and [providers.*] in one file.
Passthrough (default)
Nothing else to configure: shunt forwards the client’s own x-api-key or Authorization
header. No server-side secret belongs in the config. Which credential the client should send
is covered in Connect Claude Code.
Server-side API key
Inject an operator-owned key instead of trusting callers to bring one:
[[upstreams]]
name = "anthropic"
provider = "anthropic"
auth = { mode = "api_key", env = "ANTHROPIC_API_KEY", header = "x_api_key" }If you inject a server-side credential, gate the gateway with
[server.auth] before exposing it beyond loopback.
Claude subscription accounts (claude_oauth)
Create a refreshable shunt-managed account once, then scope the upstream to it:
shunt login claude --name primary --mode oauth[[upstreams]]
name = "anthropic"
provider = "anthropic"
auth = { mode = "claude_oauth", account = "primary" }shunt login claude has three modes: --mode oauth runs shunt’s own refreshable OAuth flow
(recommended), --mode import copies the current Claude Code login, and --mode setup-token
creates a one-year inference-only token. Omit account (and accounts) to scan the whole
shunt-managed account store — with several accounts in the store this becomes a load-balanced
pool with session stickiness and quota-aware rotation. The full pooling behavior — explicit
account entries, per-account thresholds, [server.pool] tuning, and failover — is documented in
Anthropic Multi-Account.
Models & routing
As the default provider, anthropic needs no routes — everything you did not map elsewhere
lands here. To pin a model to it explicitly inside an ordered failover chain:
[[models]]
id = "claude-sonnet-5"
display_name = "Claude Sonnet 5"
[models.upstream_model]
anthropic = "claude-sonnet-5"The map key is the [[upstreams]].name, not the preset id. With
auto_include_builtin_models = true (the default), GET /v1/models first tries the live model
list from server.default_provider when it is Anthropic-kind; otherwise, it falls back to shunt’s
builtin Claude catalog snapshot — see Model Discovery.
Auto-mode classifier
When Claude Code runs in auto mode, every action that leaves the workspace or touches the network
goes through a permission classifier — a separate Messages request whose system prompt asks the
model for a safety verdict on the pending action. It omits the Claude Code identity block
api.anthropic.com looks for on a subscription-OAuth credential, as do side queries such as mods’
$.model.complete with CLAUDE_CODE_ATTRIBUTION_HEADER=0. shunt restores the identity on the
auto-mode classifier and on any Claude Code request (identified by x-claude-code-session-id, a
claude-cli/ user agent, or x-app: cli) that carries no accepted marker. The repair applies only when the outbound bearer is a
subscription OAuth token, never to count_tokens (which upstream does not gate), and an empty
x-claude-code-session-id does not count. Other clients, and requests on an API key, are
left alone by this repair. The 429 troubleshooting row describes the failure that
repairs.
The classifier does not use the session’s model by default. Measured on Claude Code 2.1.226, it
takes a server-supplied model if the client was given one, otherwise the Sonnet tier alias, and
only then the session’s own model — which is why ANTHROPIC_DEFAULT_SONNET_MODEL moves the
classifier too, as seen on the wire. That was read out of the distributed client, so it describes
the version measured rather than a contract.
Set classifier_model to pin the classifier request to one model from the gateway side, without
touching what the session’s own turns run on:
[providers.anthropic]
kind = "anthropic"
base_url = "https://api.anthropic.com"
classifier_model = "claude-sonnet-5"The key is accepted on kind = "anthropic" providers only, and it is a model remap inside that
provider, never a route to a different one. The classifier request depends on stop_sequences:
while shunt’s Responses translation still dropped that field, pointing the classifier at a Codex
upstream through the Sonnet alias made it retry its first stage, and every permission check took
5–7 s longer. shunt now emulates stop_sequences on the Responses path,
so that particular failure no longer applies — the key stays an in-provider remap regardless. An
unset key (the default) leaves the classifier on whatever model the client asked for.
On a gateway-login deployment you can push the alias to the client instead, through managed settings:
[server.gateway.policies.cli.env]
ANTHROPIC_DEFAULT_SONNET_MODEL = "claude-sonnet-5"That moves the classifier the same way, but it changes what sonnet resolves to for the whole
session, not just the classifier — and the id must also appear in availableModels when that list
is set.
Verify
shunt check # -> config ok
shunt run
curl -sS http://127.0.0.1:3001/v1/modelsThen send one minimal request (with passthrough, include your own credential; with
claude_oauth, omit it):
curl -sS http://127.0.0.1:3001/v1/messages \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H 'anthropic-version: 2023-06-01' \
-H 'content-type: application/json' \
-d '{"model":"claude-sonnet-5","max_tokens":16,"messages":[{"role":"user","content":"Reply with OK."}]}'Related
- Anthropic Multi-Account — account pool, quota-aware selection,
[server.pool] - Admin & Remote Provisioning — provision accounts from a browser
- Configuration Reference — every account key