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. The body is
otherwise forwarded byte for byte; this is the one field Perdurance changes.
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.
Because the rewrite happens after the body is hashed, two requests that differ only in the rewritten name are still two different requests. See Idempotency.
Getting it wrong
| Symptom | Cause |
|---|---|
422, nothing stored | The model matched a rule whose backend speaks the other dialect |
403 naming a rule | A rule refused the request; the message says which |
| Requests reaching the wrong provider | A catch-all at a lower priority than the specific rule you expected to win |
Agent SDK retries
What an agent framework's retry actually re-runs, why it costs more than a retried API call, and how to make every model call in the loop idempotent.
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.

