PerdurancePerdurance

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_…'

claude

Lasts 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 forwardedWhy
authorization, x-api-keyThey carry the key you send Perdurance. The backend's own credential goes out instead
anthropic-versionPerdurance sets it, at 2023-06-01. A forwarded copy would be a second header
x-perdurance-key, last-event-idThey address this service, and mean nothing upstream
Everything elseIncluding 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.

StatuscodeWhen
502upstream_failedThe provider could not be reached, or refused in a way that was not an HTTP answer
504held_too_longThe connection was held past the synchronous hold. The execution is still running
4xxvariousEvery 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 active

ANTHROPIC_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 Authorization and no x-api-key of its own is refused with 401 caller_credential_missing, and no request row is written.
  • POST /requests, the fire-and-forget route, refuses a caller backend with 422 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.

On this page