Claude Code and the Agent SDK
Point Claude Code or the Agent SDK at Perdurance with two variables, what is forwarded, what an error looks like, and where subscriptions stand.
Claude Code runs through Perdurance on two environment variables and no code change. It
treats whatever ANTHROPIC_BASE_URL names as the Anthropic API, so a namespace URL and a
Perdurance key are the whole of the configuration. Every turn is then a stored request: the body
you sent, the answer, every stream chunk, and the token counts.
The Agent SDK is the same configuration, because it passes its environment to the Claude Code process it spawns.
Two variables
export ANTHROPIC_BASE_URL='https://api.perdurance.dev/acme/prod'
export ANTHROPIC_AUTH_TOKEN='sar_ab12cd34_…'The base URL is your tenancy and your namespace, and it stops there. Claude Code appends
/v1/messages itself, so a base URL that already ends in /v1 asks for /v1/v1/messages and
nothing answers there. This is the one place the address differs from the one an SDK is given in
Getting started.
ANTHROPIC_AUTH_TOKEN is your Perdurance key, not your Anthropic key. Claude Code prefixes
it with Bearer and sends it in Authorization, which is where Perdurance reads a key from.
The Anthropic key lives on the backend, encrypted, and Perdurance substitutes it on the way out.
Two things have to exist in the namespace before this works: a backend of kind anthropic
holding your Anthropic key, and a routing rule matching the model names Claude Code asks for.
/v1/messages refuses a rule that resolves to an OpenAI-dialect backend with 422, because
nothing here translates between dialects. Routing has the patterns.
export ANTHROPIC_BASE_URL='https://api.perdurance.dev/acme/prod'
export ANTHROPIC_AUTH_TOKEN='sar_ab12cd34_…'
claudeLasts for one terminal session.
What reaches Anthropic
The body. Every field, every content block, every tool definition, the system array in the
order it arrived, and every cache_control marker. Only the model id changes, and only when a
routing rule rewrites it. Object keys are re-serialised in a canonical order, which is what makes
an identical retry hash to the same request. Array order is never touched, so the attribution
block Claude Code writes as the first entry of system is still the first entry when the call
goes out.
The headers. Every request header whose name starts with anthropic- is forwarded, as an
open list rather than a fixed one, so a capability Anthropic ships as a new header works on the
release that introduces it. openai-beta, openai-organization and openai-project are
forwarded too. These are not:
| Not forwarded | Why |
|---|---|
authorization, x-api-key | They carry the key you send Perdurance. The backend's own credential goes out instead |
anthropic-version | Perdurance sets it, at 2023-06-01. A forwarded copy would be a second header |
x-perdurance-key, last-event-id | They address this service, and mean nothing upstream |
| Everything else | Including x-claude-code-session-id and the agent id headers, which Anthropic's gateway protocol says a gateway may consume rather than forward |
A forwarded value may be up to 4,096 bytes, and a value carrying a control character is dropped rather than sent, because a newline in a header would become a second header at the provider.
The stream. Chunks are stored and forwarded as they arrive. Nothing waits for the whole answer, which matters because Claude Code reads the stream as it comes and stalls against a gateway that buffers.
Token counting falls back on its own. There is no /v1/messages/count_tokens route here.
Anthropic's gateway protocol makes that endpoint the only optional one, and Claude Code counts
context through the messages endpoint instead when it is absent.
Model discovery is off by default. There is no /v1/models route here either. Claude Code
only asks for one when a developer sets CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY=1, and when
the request fails the model picker falls back to its built-in list. Anthropic's protocol
reference calls the startup probe at HEAD /api/hello best-effort traffic a gateway may reject
without breaking anything, and Perdurance has no route for it.
What an error looks like
Anthropic's refusal is Anthropic's. The two dialect routes answer it with Anthropic's own
status, Anthropic's body byte for byte as application/json, and retry-after when Anthropic
sent one. Nothing is wrapped. This is deliberate: Claude Code matches the provider's error
wording to retry a rejected capability and to turn it off for the rest of the conversation, and
an envelope around the body breaks that recovery even when the status survives it.
Perdurance's own envelope, {"error": {"code", "message"}}, is left for Perdurance's own
outcomes.
| Status | code | When |
|---|---|---|
502 | upstream_failed | The provider could not be reached, or refused in a way that was not an HTTP answer |
504 | held_too_long | The connection was held past the synchronous hold. The execution is still running |
4xx | various | Every refusal before the call reaches the provider: no key, no grant, no matching rule, a dialect mismatch, no credit |
Errors and status codes has the full table.
A stream decides on its first item. A streaming call waits up to the deployment's
synchronous hold, five minutes by default, for its first chunk. A provider that refuses before
that first chunk is answered with the provider's status and body, not with an event stream that
reports a failure inside a 200. Once the first chunk has gone out the status line is spent, so
a failure after it arrives as event: failed instead.
Retries
Claude Code and the Agent SDK re-send the identical body when a call fails. Inside the idempotency window, ten minutes from submission, those bytes hash to the record already there and the retry attaches to the execution already running rather than starting and paying for a second one. Idempotency and resume covers what counts as identical.
That is also the way back from a 504 held_too_long. The connection was let go, the execution
was not cancelled, and the answer is stored when it finishes. Re-send the same body to attach to
it, or read it by id.
What attaches is a retry of one turn. The next turn of a conversation carries a longer transcript, so it is a different body and a new request, which is the point of the section below.
Subscriptions
By default Perdurance carries no subscription credential, and asks for a Perdurance key
instead. A bearer token starting sk-ant-, which both an Anthropic API key and a claude.ai
session token carry, is refused with 401 and code provider_credential. The message names
ANTHROPIC_AUTH_TOKEN and X-Perdurance-Key, because a bare unauthorised would send you to
check a key you typed correctly.
The reason this comes up at all is what Claude Code does with a base URL and nothing else. Anthropic's gateway documentation, read on 2026-09-02:
Setting only that variable, without a gateway credential, doesn't replace the subscription. Requests still route through the gateway, but a saved claude.ai login remains the active credential, so its usage limits and billing apply. Gateways that pass this traffic on to Anthropic must forward the OAuth capability in
anthropic-beta.
So a developer signed in with a claude.ai login who sets only ANTHROPIC_BASE_URL sends their
subscription's own bearer token to the gateway. Anthropic's legal page on using the Developer
Platform, read the same day, sets the limit on who may carry it:
Anthropic does not permit third-party developers to offer Claude.ai login into their own applications, or to route requests through Free, Pro, or Max plan credentials on behalf of their users. Moreover, developers may not collect, store, or intermediate Claude.ai credentials or session tokens …
Anthropic does not permit a third party to route requests through those credentials or to
intermediate session tokens. Perdurance's shared service carries none. Setting
ANTHROPIC_AUTH_TOKEN to a Perdurance key is what this page opened with, and it is the
configuration this product is built for.
A dedicated deployment can be asked to carry one
The switch is off by default, and Perdurance's shared service does not pass it. The two quotations above are Anthropic's own words on who may carry a subscription credential, and they are what to read before turning it on anywhere.
A deployment you run for your own developers is the case Anthropic's gateway documentation
describes, and it is yours to decide. Perdurance can be started with --credential-passthrough
on a dedicated deployment at the customer's request. The customer's own agreement with Anthropic
is what settles whose terms that traffic runs under. Perdurance states Anthropic's position and
does not interpret it.
With the switch on, a backend can be created with credential_source: "caller". Such a backend
holds no key of its own and forwards the one each call brought.
export ANTHROPIC_BASE_URL='https://gateway.example.com/acme/prod'
export ANTHROPIC_CUSTOM_HEADERS='X-Perdurance-Key: sar_ab12cd34_…'
# ANTHROPIC_AUTH_TOKEN is deliberately unset, so the claude.ai login stays activeANTHROPIC_CUSTOM_HEADERS takes Name: Value pairs, newline-separated for more than one. The
Perdurance key moves into X-Perdurance-Key, which wins over the bearer when both are present,
and that leaves Authorization free to carry what Claude Code put there. The OAuth capability
in anthropic-beta is forwarded by the open-list rule above, which is what Anthropic's gateway
documentation asks of a gateway passing this traffic on.
Six things bound it:
- The credential is sealed with the deployment's cipher inside the submission transaction, kept for the life of the attempt, and destroyed by the completion that writes a terminal status. A deferred retry keeps it, because the retry has to make the same call.
- No route returns it, no listing selects it, and no log line names it.
- The idempotency claim is taken over the body and the credential. Two callers who send one body under two credentials run two executions, because a shared one would run a request on somebody else's subscription. The same credential re-sent inside the ten minutes attaches as usual.
- A submission that brought no
Authorizationand nox-api-keyof its own is refused with401 caller_credential_missing, and no request row is written. POST /requests, the fire-and-forget route, refuses a caller backend with422 passthrough_disabled. Nobody is holding the connection for the answer, and the row would hold a token until a worker got to it.- Without the switch, creating a caller backend and submitting to one are both
422 passthrough_disabled.
Storage
A Claude Code session stores more than it looks like. Every turn sends the whole conversation, so every turn is a new request row holding the whole conversation. Ten turns of a long session are ten copies of a transcript that got longer each time, not ten short messages.
That is what makes a session the fastest way to spend a namespace's storage allowance. Check what
one is holding with GET /storage, and shorten the retention horizon on a namespace you use for
this if the answer is more than you want to keep. Usage and storage has both.
Getting started
The namespace, the backend, the routing rule and the key this page assumes.
Agent SDK retries
What a retry re-runs in an agent loop, and why it costs more than a retried API call.
Errors and status codes
Every status this API returns, and the ones that are the provider's own.
Idempotency and resume
The ten-minute window, and what counts as identical.

