Skip to content

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 provider request-body field. Earlier drafts of these docs described a provider.sort knob with cost / latency / throughput policies. It was never implemented and is not accepted on any surface. Routing preferences are declared in config and applied inline with a :variant or @preset token in the model string.

Resolution runs in stages and stops at the first that matches.

  1. Stage 0 — token stripping. A leading @preset and a trailing :variant are stripped off the model string and turned into routing preferences (filters, and a sort that currently does nothing — see below).
  2. Strategy 1 — direct route. provider:model_id (e.g. openai:gpt-5) routes straight to that provider. Chain length 1, no cascade.
  3. Strategy 2 — explicit virtual model. A model you defined under models: uses exactly the endpoint chain you listed.
  4. 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.

For Strategy 3 (and for a cascade-strategy virtual model), candidates are sorted by:

  1. Priority rank, ascending. A provider’s explicit priority: wins outright. Otherwise its rank is the position of its class in registry.provider_priority. A provider with no class, or a class absent from that list, ranks last.
  2. Provider id, alphabetically.
  3. 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.

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_tags filters (set by a variant or preset);
  • it is require_free and the requested id doesn’t follow the :free convention;
  • it is a subscription provider and the request didn’t reach it explicitly. Subscription providers are explicit-route-only: a provider:model route or an only preference 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.

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.

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.

Model fallback and provider selection are independent layers:

  1. For each model in your models list (or the single model if no fallback), Routeplane builds the ordered provider chain described above.
  2. 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.
  3. The same routing preferences apply to every model in the fallback list — you cannot specify different preferences per model.
  • Pin to a specific provider — use the provider:model_id direct route, e.g. openai:gpt-5. Or declare a variant with routing.only.
  • Exclude a provider — declare a variant with routing.ignore, mark the provider active: false, or drop it from your config.
  • Prefer a provider — give it an explicit priority:, or reorder registry.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.