PerdurancePerdurance

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

KindDialect it speaksTypical use
openai_compatOpenAIOpenAI itself, and the many servers that copy its API
anthropicAnthropicAnthropic's Messages API
openrouterOpenAIOpenRouter, 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:

PriorityPatternBackend
10claude-*The anthropic backend
20gpt-*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-latestclaude-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

SymptomCause
422, nothing storedThe model matched a rule whose backend speaks the other dialect
403 naming a ruleA rule refused the request; the message says which
Requests reaching the wrong providerA catch-all at a lower priority than the specific rule you expected to win

On this page