Provider Selection
When more than one configured provider declares the same model, Routeplane has to pick which endpoint to try first — and in what order to fall through the rest.
The answer is deliberately boring: the order is the priority you declared in routeplane.yaml. Routeplane does not score, rank, or predict. It filters the eligible set, sorts by your declared priority, and lets observed failures push a bad target later.
[!IMPORTANT] There is no
providerrequest-body field. Earlier drafts of these docs described aprovider.sortknob withcost/latency/throughputpolicies. It was never implemented and is not accepted on any surface. Routing preferences are declared in config and applied inline with a:variantor@presettoken in themodelstring.
How a model resolves
Section titled “How a model resolves”Resolution runs in stages and stops at the first that matches.
- Stage 0 — token stripping. A leading
@presetand a trailing:variantare stripped off themodelstring and turned into routing preferences (filters, and asortthat currently does nothing — see below). - Strategy 1 — direct route.
provider:model_id(e.g.openai:gpt-5) routes straight to that provider. Chain length 1, no cascade. - Strategy 2 — explicit virtual model. A model you defined under
models:uses exactly the endpoint chain you listed. - Strategy 3 — auto-cascade. Every active provider that declares the model contributes a candidate, and the chain is ordered as below.
If nothing matches, the request fails at resolve time with a structured error — including a “did you mean” suggestion when one is close — and zero upstream egress. There is no default-provider fallback.
The cascade order
Section titled “The cascade order”For Strategy 3 (and for a cascade-strategy virtual model), candidates are sorted by:
- Priority rank, ascending. A provider’s explicit
priority:wins outright. Otherwise its rank is the position of itsclassinregistry.provider_priority. A provider with no class, or a class absent from that list, ranks last. - Provider id, alphabetically.
- Native protocol as a tie-break — a provider that already speaks the inbound protocol sorts ahead of one that would need translation. Because provider id is a total order within a rank, this key is currently inert.
The default class ladder, highest priority first:
| Class | Meaning |
|---|---|
first-party-subscription |
An official upstream billed as a flat-rate subscription. |
gateway-subscription |
A gateway or aggregator billed by subscription. |
first-party-api |
An official upstream billed pay-as-you-go per token. |
routeplane-cloud |
A rung reserved for a Routeplane hosted gateway. Nothing fills it. |
third-party-api |
A third-party or community reseller, pay-as-you-go. |
routeplane-cloud is a real variant of the class enum and a real rung on the ladder — a provider named routeplane, or a registry entry declaring kind: cloud, is auto-assigned to it. In practice the rung stays empty, because there is no Routeplane hosted gateway to point such a provider at: no account, no endpoint, no service. It is listed here so the table matches the code, not because you can route to it.
Override the ladder with registry.provider_priority, or pin one provider with a per-provider priority:. A plain config that sets neither leaves every provider unclassed, which means the order is simply alphabetical by provider id — deterministic and easy to reason about.
The sort is stable, so account-expanded targets within one provider keep their configured build order.
Which providers are eligible
Section titled “Which providers are eligible”Ordering only ever applies to candidates that survive filtering. A provider is dropped when:
- it is not
active; - it fails the request’s
only/ignore/require_tagsfilters (set by a variant or preset); - it is
require_freeand the requested id doesn’t follow the:freeconvention; - it is a subscription provider and the request didn’t reach it explicitly. Subscription providers are explicit-route-only: a
provider:modelroute or anonlypreference reaches them, but they never join the auto-cascade — so a plain canonical request never silently bills a personal subscription.
These are all observations about eligibility, not guesses about performance.
Ordering by cost or latency is not implemented
Section titled “Ordering by cost or latency is not implemented”routing.sort accepts three values. Only one has an effect:
| Value | Status |
|---|---|
alphabetical |
The default. The order described above. |
latency |
Not implemented. No metrics source. Produces the identical order to alphabetical. |
cost |
Not implemented. No pricing-driven ordering. Produces the identical order to alphabetical. |
There is no throughput axis at all — no throughput value exists, and nothing measures output tokens per second. There is no balanced blend. There is no rolling telemetry window feeding routing decisions, and no routing_profile recorded on requests.
Why: filter is craft, score is theater
Section titled “Why: filter is craft, score is theater”This is a design position, not a gap in the roadmap.
Only negative evidence is allowed to change routing order. A target that has actually failed gets deprioritized; a target that succeeds gets restored. Nothing is predicted, and nothing is ranked on a belief about the future.
A cost or latency ranker would break three things worth keeping:
- Determinism. Two identical requests would route differently depending on a moving average. Debugging a gateway whose order shifts under you is miserable.
- Operator authority. You declared a priority ladder because you know which subscription you want burned first. A scorer would quietly overrule it.
- Honesty. A p50 TTFT from a rolling window is a prediction about the next request. Dressing that up as a routing guarantee is the kind of sophistication that looks impressive in a feature table and doesn’t survive contact with a real workload.
SortOrder::Latency exists in the source as a dead stub. It stays dead on purpose.
What does change the order: Influence
Section titled “What does change the order: Influence”The one sanctioned reordering is the Influence circuit breaker, which is off by default. When enabled, it reads per-attempt outcomes and:
- deprioritizes a seat or model that ran out of credit, got rate-limited, or has a
5xx/timeout streak — moving it later in the chain, never removing it; - recovers it after a cooldown, with a single probe request.
That is negative evidence: a recorded failure, not a forecast. See Influence for the thresholds and the recovery behavior.
How selection composes with fallback
Section titled “How selection composes with fallback”Model fallback and provider selection are independent layers:
- For each model in your
modelslist (or the singlemodelif no fallback), Routeplane builds the ordered provider chain described above. - If the chosen provider fails in a way that doesn’t surface to the caller (rate limit,
5xx), Routeplane retries on the next-ranked provider of the same model before falling through to the next model in the list. - The same routing preferences apply to every model in the fallback list — you cannot specify different preferences per model.
Pinning and excluding
Section titled “Pinning and excluding”- Pin to a specific provider — use the
provider:model_iddirect route, e.g.openai:gpt-5. Or declare a variant withrouting.only. - Exclude a provider — declare a variant with
routing.ignore, mark the provideractive: false, or drop it from your config. - Prefer a provider — give it an explicit
priority:, or reorderregistry.provider_priority.
Ties are broken by provider id, which means Routeplane does not load balance across equal providers. That is intentional for auditability. If even spend distribution across tied providers matters for your workload, email contact@routeplane.app with the concrete use case.