Provider Routing
Onlist routes every request to a platform provider that has an active listing for the
model you asked for. By default Onlist picks for you. To take control, add an optional
provider object to the JSON request body. It is the one Onlist-specific extension to the
OpenAI/OpenRouter wire format: everything else in your request is unchanged.
curl https://onlist.io/v1/chat/completions \
-H "Authorization: Bearer YOUR_ONLIST_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"model": "openai/gpt-4o",
"messages": [{"role": "user", "content": "Hello from Onlist"}],
"provider": { "sort": "price" }
}'
The provider field
A bare string is shorthand for pinning a single provider:
"provider": "alice-shop"
is exactly equivalent to:
"provider": { "only": ["alice-shop"] }
Provider slugs are bare and must match ^[a-z0-9_-]{1,64}$. There is no sub-slug
addressing: a slug containing a / returns 400 provider_sub_slug_removed.
The provider field is read only on JSON endpoints (/v1/chat/completions,
/v1/completions, /v1/embeddings, /v1/messages, /v1/responses, /v1/rerank,
/v1/audio/speech, /v1/images/generations, /v1/videos). It is not read on multipart
endpoints (/v1/images/edits, audio transcription/translation) or on the Gemini /v1beta
path: only model is read there. This mirrors OpenRouter.
Field reference
Onlist honors a strict subset of OpenRouter's provider controls. Unsupported fields are
accepted and silently ignored (never a 400) and reported in the X-Onlist-Warnings
response header so you can detect them.
| Field | Type | Status | Behavior |
|---|---|---|---|
only | string[] | Honored | Single pin: only only[0] is used; the rest are dropped and reported as ignored: provider.only[1:N]. For multiple providers, use allow. |
sort | "price" | "throughput" | "latency" | Honored | price = lowest effective price first (list price adjusted by delivered cache hit rate, see below), throughput = fastest first, ranked by measured speed over the last 7 days, combining time to first token and throughput (see below). latency is accepted as an alias of throughput (same ordering, no warning). A :floor/:nitro model suffix overrides this and the field is reported as ignored: provider.sort due to model variant slug. Any other value is ignored + warned. Setting sort also turns off the Smart weighted draw. |
order | string[] | Honored | Provider try-order priority for failover. Suppresses sticky-channel affinity and the Smart weighted draw when set. Non-string elements are skipped + warned. |
allow | string[] | Honored | Request-level allowlist, merged with token/account preferences. Draining the pool returns 503 no_provider_after_allow_deny. |
ignore | string[] | Honored | Request-level denylist. Deny wins over allow. |
allow_fallbacks | boolean | Honored | Default true. false disables cross-provider failover (and the top-level models[] fallback list). |
max_price | object | Partial | prompt/completion enforced fail-closed (drained pool → 503 no_provider_in_max_price); image/request ignored + warned. |
zdr | boolean | Honored | true routes only to listings whose channel declares zero data retention on the full upstream path (seller-declared, not verified). Drained pool → 503 no_zdr_provider_available. Also skips platform-side retention for the request. |
data_collection | "allow" | "deny" | Honored | deny routes only to listings whose channel declares prompts are not used for training. Drained pool → 503 no_provider_without_data_collection; any other value → 422 invalid_data_collection. |
require_parameters | boolean | Not implemented | Silently ignored + warned. |
quantizations | string[] | Not implemented | Silently ignored + warned. |
variant | string | Not implemented | Deprecated. Any value is ignored + warned. |
preferred_max_latency | number | Not implemented | Silently ignored + warned. |
preferred_min_throughput | number | Not implemented | Silently ignored + warned. |
Default routing (Smart)
When you send no sort, no order and no routing suffix, Onlist spreads requests across the
eligible providers the way OpenRouter's default does: each request draws a provider with
probability proportional to the inverse square of its price. A provider at twice the
cheapest price gets a quarter of the weight, one at eight times the price gets 1/64. Smart
then adjusts for reliability and speed, and buyer ratings only break ties.
weight = (quality × delivery)^4 × (cheapest cost ÷ this provider's cost)^2
delivery = 0.8 × availability + 0.2 × speed
| Input | What it measures |
|---|---|
| Delivered price | This provider's cost per token, using the effective price (list price adjusted by delivered cache hit rate and the model's input/output mix). The cheapest provider's ratio is 1. |
| Availability | Success rate on this model over the last 24 hours, see below. |
| Speed | This provider's measured speed divided by the fastest provider's, using the same time-to-first-token plus throughput measurement as :nitro. Fastest = 1. |
Price counts as a ratio, so a 5% price gap costs about 10% of weight, not the full swing a min-max scale would give it. In price terms, the exchange rates to remember:
| Difference | Weighs about the same as being |
|---|---|
| One point of failure rate | 18% pricier |
| Five points of failure rate | 2.8 times the price |
| Success rate of 90% or below | out of the draw |
| Half the speed | 23% pricier |
Availability is the 24-hour success rate on this model, counting buyers' calls only and only the failures the provider is responsible for. A client that keeps retrying through an outage does not multiply it: in any minute, a buyer's failures count up to that buyer's usual number of successful calls per minute on this model (at least one), and the rest are treated as retries of the same work. A failure that happens after the buyer has already disconnected does not count either, since no buyer was waiting for that result. The rate is shrunk toward the pool average with a prior worth about 20 requests: a new listing starts at the typical level for that model and moves toward its own record as traffic arrives, and nothing scores as perfect just because it is untested. Every point of failure rate then costs 0.1, and 90% or below scores zero. Acute outages are handled separately by cooldown, which pulls a failing provider out of the pool within seconds.
The delivery score is multiplied by the same quality score the other orders use (authenticity inspections where the model has them, ratings otherwise). A provider caught serving a different model is excluded outright, whatever its price.
Smart spreads traffic; it is not a fixed first place. The cheapest reliable providers get
nearly all new traffic, a provider priced close to them gets a real share, and an expensive
one keeps a trickle that keeps its measurements current. Failover then walks the remaining
providers in the drawn order. A sticky session (see below) is unaffected: it stays on the
provider already serving it, and the draw only happens on a session's first request. Any
explicit preference turns the draw off and routes in strict ranked order, the same rule
OpenRouter applies: provider.sort, provider.order, or a :floor / :nitro suffix.
GET /api/mkt/routes/:route_id records how a request was placed: selection is
weighted_random (a Smart draw) or ranked (strict order), and selected_rank is the
position, counting from 1, the chosen provider held in the ranked order at the time.
Each model page shows exactly these orders. The providers table opens on the Smart order
(routing_order.smart in GET /api/mkt/models/:id) and can be switched to the price
(:floor) or speed (:nitro) order; providers currently cooling down or excluded are listed
after the routable ones.
Honored fields
only: pin one provider
only pins the request to a single provider. Only the first element (only[0]) is
honored. Any additional slugs are dropped and reported in X-Onlist-Warnings.
curl https://onlist.io/v1/chat/completions \
-H "Authorization: Bearer YOUR_ONLIST_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"model": "openai/gpt-4o",
"messages": [{"role": "user", "content": "Hi"}],
"provider": { "only": ["alice-shop"] }
}'Unlike OpenRouter, only is not a multi-provider allowlist: Onlist honors only[0]
and drops the rest. If you want to permit several providers, use allow instead. A pinned
slug that does not exist returns 404 provider_not_found; a pinned slug that exists but has
no listing for the model returns 422 no_listing_for_model_at_provider.
sort: price or throughput
sort chooses the ordering variant within the eligible pool. Both variants are strict
rankings (no weighted draw), and both are multiplied by the availability score described in
Default routing (Smart): a cheap or fast provider that is failing
sorts below a reliable one, and at 90% success or below it drops to the bottom.
"provider": { "sort": "price" } // lowest effective price first
"provider": { "sort": "throughput" } // fastest measured speed first
price ranks by effective price, not list price. Each provider's list price is adjusted by
the prompt-cache hit rate it actually delivered on that model over the last 7 days, and input
and output are weighted by the model's real traffic mix. A provider that lists a low price but
rarely hits the cache costs you more per request, and sorts accordingly. Providers without
enough recent traffic are ranked at their list price adjusted by the model-wide average, so a
new provider is neither punished nor favored. Each model page shows the effective input price
and the hit rate behind it. max_price still compares list prices, exactly as on OpenRouter.
throughput ranks by measured speed, not by throughput alone. Two measurements a provider
produced on that model over the last 7 days — its median time to first token, and the output
tokens per second it sustains after that first token — are combined into how long it takes to
deliver one typical-length answer for that model. Shorter sorts first. With equal time to first
token the faster writer wins; with equal writing speed the faster first token wins. Buffering
the whole answer and releasing it in one piece gains nothing: time to first token then equals
the whole call, which comes out slowest of all.
Both measurements count only streaming calls that Onlist routed to that provider first, and the writing speed is clocked after the first token and token-weighted, so a provider is not marked slow for writing long answers. A provider needs at least 10 qualifying calls before it is measured. Providers without enough recent traffic sort at the median of the measured providers for that model, so a new provider is neither punished nor favored; when no provider for the model has enough traffic yet, the ordering falls back to quality times availability. Each model page shows the time to first token and the throughput. Throughput there follows OpenRouter's definition, output tokens divided by the whole call time including the wait for the first token, so it usually ranks providers in the same order as this sort.
A model-slug suffix overrides this field: gpt-4o:floor forces price-first and
gpt-4o:nitro forces speed-first, regardless of any sort value you send. When you
send both, the dropped field is reported as ignored: provider.sort due to model variant slug. See Model variant suffixes below.
OpenRouter's latency is accepted as an alias of throughput: both map to the same
speed ordering, and no warning is emitted. Any other sort value is ignored and
reported in X-Onlist-Warnings.
Session stickiness
Onlist keeps a healthy conversation on the provider that is already serving it, so the provider's prompt cache keeps hitting. The session key is read from the request body:
| Endpoint | Session key |
|---|---|
/v1/chat/completions, /v1/responses | prompt_cache_key |
/v1/messages | metadata.user_id (what Claude Code sends) |
Requests without a session key still stick, at the granularity of your API key, the model
and the effective sort: everything that key sends for a model stays with one provider
until the traffic pauses for ten minutes, which is about how long upstream prompt caches
live anyway. Session-keyed stickiness lasts one hour after the last successful call. It follows the provider that actually delivered after a failover, and a stream that
fails mid-way is not remembered. Hard constraints always win: allow, ignore,
max_price, zdr and data_collection are re-checked on every request, and order or
only bypass stickiness entirely. Changing sort starts a new session. Requests that
start a session at the same moment share one choice: the first one placed claims the
session and the others follow it, so a burst of parallel calls lands on one provider
instead of being split across several. When the sticky
provider cannot be reused (cooling down, paused, or failing a constraint) the response
carries X-Onlist-Cache-Continuity: broken and the warning prompt_cache_continuity_lost.
order: try-order priority
order is an ordered list of provider slugs. Failover advances through them in the order you
give. Setting order also suppresses sticky-channel affinity, so each request re-evaluates
from the top of your list.
"provider": { "order": ["alice-shop", "bob-relay", "carol-ai"] }
allow and ignore: allowlist and denylist
allow is a request-level allowlist (merged with any token/account preferences). ignore
is a request-level denylist. When a slug appears in both, deny wins.
"provider": {
"allow": ["alice-shop", "bob-relay"],
"ignore": ["carol-ai"]
}
If your allow/ignore rules exclude every viable provider, the request returns
503 no_provider_after_allow_deny. To see which models are still callable under the
allow/ignore preferences stored on a key, call
GET /v1/models/user.
allow_fallbacks: control failover
Failover across providers is on by default. Set allow_fallbacks: false to require the first
selected provider and disable cross-provider failover. This also disables the top-level
models[] fallback list (see below).
"provider": { "only": ["alice-shop"], "allow_fallbacks": false }
max_price: USD-per-token caps
max_price filters the pool to providers priced at or below your caps, in OpenRouter's native
USD-per-token format (for example 0.0000025 = $2.50 per million tokens).
"provider": {
"max_price": { "prompt": 0.0000025, "completion": 0.00001 }
}
promptandcompletionare enforced fail-closed: if no provider fits, the request returns 503no_provider_in_max_pricerather than silently overspending.imageandrequestare not implemented (the scope is chat-only): they are ignored and reported inX-Onlist-Warnings.
Because prompt/completion fail closed, an aggressive cap can drain the pool and return a 503. If you would rather get a result than a hard failure, widen the caps or omit them.
zdr and data_collection: privacy constraints
Both fields filter the pool on the seller's own data policy declaration for that listing, which covers the full path to that seller's upstream and is not verified by Onlist.
"provider": {
"zdr": true,
"data_collection": "deny"
}
zdr: truekeeps only listings whose channel is declared zero data retention. Nothing eligible left returns 503no_zdr_provider_available. The request is also excluded from Onlist's own temporary retention, which means it leaves no platform-side record to use as dispute evidence.data_collection: "deny"keeps only listings whose channel is declared to withhold prompts from training. A channel that retains prompts but does not train on them is still eligible, matching OpenRouter. Nothing eligible left returns 503no_provider_without_data_collection.zdr: falseanddata_collection: "allow"are the same as omitting the field. Any otherdata_collectionvalue returns 422invalid_data_collection.- The filter runs after
max_priceand beforeallow/ignore, so a 503 here means nothing survived your other constraints too, not necessarily that the market has no such seller.
Read Privacy & data before relying on either field: it spells out
what the declaration covers, who stands behind it, and the evidence trade-off of zdr: true.
Model variant suffixes
OpenRouter splits a model:suffix id into two kinds, and Onlist reads them the same way.
Only the last : segment is inspected.
Routing shortcuts do not change which model you get. They are removed from the id
before routing and only affect how the eligible providers are ordered, except :online,
which turns on web search:
| Suffix | Effect |
|---|---|
:floor | Lowest effective price first (same as provider.sort: "price"). |
:nitro | Fastest first by measured speed over the last 7 days, combining time to first token and throughput (same as provider.sort: "throughput"). |
:exacto | Honored as the default ordering, which is already a composite quality score. No warning. |
:online | Turns on web search, the same as plugins: [{"id": "web"}]. Chat Completions only; on other endpoints it is dropped and reported as ignored: plugins (web search is only available on /v1/chat/completions). Provider order is unchanged. |
Static variants are separate models, not options. Every other : suffix, including
:free, :batch, :thinking and anything Onlist does not recognize, stays in the model id
and is routed as its own listing. deepseek/deepseek-v4:thinking and
deepseek/deepseek-v4 are two different products, priced and sold separately.
The two kinds compose: in llama-3.3-70b:free:floor the :floor shortcut is stripped for
ordering and llama-3.3-70b:free is the model that gets routed. Two shortcuts do not stack,
because only the last segment is read. To search the web and sort by price, send the web
plugin with provider.sort: "price" instead of chaining :floor and :online.
If no provider sells that exact id, the request returns 404 no_listing_for_model.
Onlist does not quietly drop the suffix and serve you the base model, so a
:thinking id can never come back as the non-thinking model. Call GET /v1/models for
the ids that actually exist. Free variants (:free) cannot be published yet, so those ids
normally have nothing behind them.
The models[] fallback list
Independently of the provider object, you can send a top-level models array: the
OpenRouter-compatible fallback list. Onlist tries each model in order, rewriting the body
model for each attempt, until one succeeds.
{
"model": "openai/gpt-4o",
"models": ["qwen/qwen3.8-27b", "deepseek/deepseek-v4-flash"],
"messages": [{ "role": "user", "content": "Hi" }]
}
This list is gated by allow_fallbacks: with provider.allow_fallbacks: false, only the
primary model is attempted and models[] is not used.
The X-Onlist-Warnings header
Whenever Onlist ignores a field or fires a routing notice, it reports it in the
X-Onlist-Warnings response header. The value is a de-duplicated list joined by a comma and
a space (, ). Parse it by splitting on , .
X-Onlist-Warnings: ignored: provider.sort, ignored: provider.quantizations, ignored: provider.only[1:3]
Entries take the form ignored: <field> (a dropped field) or a notice such as
cool_pool_exhausted_fallback_used. The header is present only when something was ignored or
a notice fired: a clean request returns no warnings header. Always check it in development
to confirm Onlist honored what you intended. The notices are a closed set, listed in
Limits & headers.
Next steps
- Privacy & data handling: What the zdr and data_collection declarations actually mean (seller-declared, not platform-verified) and what they do to platform-side retention.
- Errors: The 503 routing codes: no_provider_in_max_price, no_provider_after_allow_deny.
- Limits & headers: X-Onlist-Warnings, X-Onlist-Route-Id, Retry-After, and why there are no X-RateLimit-* headers.
- Models: Model id forms, onlist/auto, and a short version of the routing shortcut vs static variant suffix rules.
- Web search: The
webplugin and:online, which work with every routing control on this page.