Model routing
How a model name in a request body resolves to a provider: backends, glob patterns, priority order, and model rewrites.
Perdurance never guesses where a request should go. Two things decide it, and both live in the namespace the request was addressed to.
A backend is a connection to a provider: an address, a dialect, and your credential for it. A routing rule maps model names onto one of those backends.
A request whose model matches no rule is refused. That is deliberate — a router that guessed would send your prompt, and your money, somewhere you did not choose.
Backends
| Kind | Dialect it speaks | Typical use |
|---|---|---|
openai_compat | OpenAI | OpenAI itself, and the many servers that copy its API |
anthropic | Anthropic | Anthropic's Messages API |
openrouter | OpenAI | OpenRouter, including its Anthropic-family models |
The kind fixes the dialect, and the dialect has to match the route the request arrived on.
A body sent to /chat/completions that a rule resolves to an anthropic backend is refused
with 422, and no record is written.
Perdurance does not translate between dialects. A translation layer owns a mapping it must keep
current with two vendors, and every field it fails to understand is a corrupted upstream call.
To reach an Anthropic model from an OpenAI-shaped client, use an openrouter or
openai_compat backend that serves it.
A backend is created in a namespace and can be attached to others, so one provider connection — one credential — can serve several namespaces without being pasted twice.
The credential
The provider key you give a backend is encrypted with the deployment's credential-key before
it is stored, and it is never returned by any route. Not to the console, not to the CLI, not to
you. If you lose it, you replace it.
Routing rules
A rule is a pattern, a priority, a backend, and optionally a model rewrite.
{
"priority": 100,
"pattern": "gpt-*",
"backend_id": "…",
"model_rewrite": null
}Patterns are globs
The pattern is a glob matched against the model name in the request body — * matches
everything, gpt-* matches gpt-4.1 and gpt-4o-mini, claude-*-haiku-* matches by shape.
An invalid glob is refused when the rule is created rather than when a request arrives.
Priority decides, lowest first
Rules are taken in ascending priority order and the first one that matches wins. So a narrow rule needs a lower number than the catch-all it should beat:
| Priority | Pattern | Backend |
|---|---|---|
10 | claude-* | The anthropic backend |
20 | gpt-* | The openai_compat backend |
1000 | * | Wherever everything else should go |
Ties are broken by rule id, which is time-ordered — so two rules at the same priority resolve the same way on every process in the deployment, rather than depending on which one answered.
Model rewriting
model_rewrite replaces the model name before the request reaches the provider. It is the one
field Perdurance changes. Every other field is forwarded with the value it was sent, and every
array keeps its order. Object keys are re-serialised in a canonical order, which is what lets an
identical retry hash to the same request.
It is how one backend answers for several names, and how a name your code uses stops being the name your provider uses:
- Pin a moving name.
claude-sonnet-latest→claude-sonnet-4-20250514, so a provider's alias moving under you is a rule you change rather than a deploy. - Give a name to a choice.
our-summariser→ whatever model currently does that job, changed in one place instead of everywhere it is called. - Point staging at something cheaper without touching staging's code.
The rewrite is applied before the body is hashed, so routing is part of a request's identity. Two names that rewrite to the same model on the same backend are one request, and the provider is called once for both. The other side of that: repointing a rule makes the next submission of an unchanged body a new request. See Idempotency.
Getting it wrong
| Symptom | Cause |
|---|---|
422, nothing stored | No rule matched the model name |
422, nothing stored | The model matched a rule whose backend speaks the other dialect |
| Requests reaching the wrong provider | A catch-all at a lower priority than the specific rule you expected to win |
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.
Idempotency and resume
Idempotent LLM requests by body hash: why re-sending the same bytes never double-bills, what the ten-minute window is, and how a dropped stream is resumed.

