Which pricing key your model lands under
Predict the catalog key a request resolves to, why one model carries seven prices, and the six typed reasons a row has none.
On this page
A model id is not a price. deepseek-r1 appears in the committed price table seven times, at rates
that differ by a factor of nearly seven, because it is sold through seven different doors:
| Catalog key | Input, per million |
|---|---|
deepseek/deepseek/deepseek-r1 |
$0.55 |
openrouter/openrouter/deepseek/deepseek-r1 |
$0.55 |
vercel_ai_gateway/vercel_ai_gateway/deepseek/deepseek-r1 |
$0.55 |
azure_ai/azure_ai/deepseek-r1 |
$1.35 |
snowflake/snowflake/deepseek-r1 |
$1.35 |
fireworks_ai/fireworks_ai/accounts/fireworks/models/deepseek-r1 |
$3.00 |
replicate/replicate/deepseek-ai/deepseek-r1 |
$3.75 |
So a gateway asked “what does deepseek-r1 cost?” has to answer “through which upstream?” first,
or it is guessing. This page is how that question gets answered, so you can predict your key before
you send anything, and diagnose it when a row comes back with no dollar at all.
The rule in one sentence
The catalog namespace comes from the upstream’s descriptor, not from the wire dialect.
78 descriptors ride the openai wire. Eight of them declare no catalog namespace at all, because
their zero is asserted rather than looked up; the other 68 bind 68 distinct namespaces, no two
sharing one. Two Azure products ride that same driver and bind azure/ and azure_ai/ separately.
The wire says how to talk; the descriptor says whose price list applies.
A binding is a claim about which prices apply, and it is not the same claim as a tested cell. All
but ten of the 95 built-in provider descriptors carry at least one endpoint the generated matrix
gates with a named test, asserting the ledger row’s own provider and endpoint stamps against a mock
shaped like that provider; the rest print declared (no e2e gate), which the matrix calls not a
support claim rather than hiding. The verification is against mocks, not live endpoints.
The consequence you can check:
PREFIX MODEL RESULT KEY / REASON
deepseek deepseek-r1 PRICED deepseek/deepseek/deepseek-r1 in=$0.55/M out=$2.19/M
azure_ai deepseek-r1 PRICED azure_ai/azure_ai/deepseek-r1 in=$1.35/M out=$5.40/M
openai deepseek-r1 UNPRICED model_unknown
The third line is the point of the design. Pointing a DeepSeek model at an OpenAI upstream produces
no figure, because no candidate key under openai/ matches. The alternative, a suffix search across
every provider, would have found a rate, and it would have been someone else’s.
The walk
Read it left to right. Four inputs go in: the descriptor’s catalog prefix, the model string as sent, the endpoint kind, and a region class where the provider has one. Candidate keys are tried in order, and the first that both exists and serves the requested endpoint wins.
| Order | Candidate | What it is for |
|---|---|---|
| 1 | prefix/region.model |
the region-qualified form, for providers whose catalog is region-keyed |
| 2 | prefix/region/model |
the path-shaped variant of the same |
| 3 | prefix/model |
the plain form |
| 4 | model |
the string as a literal key, for a model id that already carries its own prefix |
| 5 | prefix/…/model |
a suffix match, restricted to keys under the same prefix |
All but one of the five candidates begin with this upstream’s own prefix, and that restriction is
the mechanism behind the rule above: a bare model id cannot reach a neighbouring provider’s key,
however familiar the id looks. No key in the artifact is unprefixed, so there is nothing for a bare
id to hit. The exception is step 4, which tries the string exactly as you sent it: a tier whose
model field already spells out another provider’s namespace resolves under that namespace. That
is the one door across, and you open it by typing it.
The same restriction is why one descriptor covers nothing. openai_compatible binds the catalog
prefix openai_compatible, which is not one of the 107 prefixes in the vendored
litellm@2026-07-14#179affb price file, so its prefixed candidates are keys the artifact does not
hold and the coverage generator counts the descriptor as covering nothing rather than inflating a
number. Rows through it come back model_unknown, which is the escape hatch working as configured.
Step 5 is why most keys look doubled. The vendored catalog namespaces each provider’s models by
provider, and many providers publish model ids that already contain their own name, so the stored
key for a Groq model is groq/groq/llama-3.3-70b-versatile. You send
llama-3.3-70b-versatile; step 3 misses; step 5 finds it inside groq/. When two keys under the
prefix both match, the one with fewer path segments wins, then alphabetical order, so the choice is
stable across catalog updates.
One kind of request probes ahead of step 1. An image call carrying a size or a quality tries the
qualified forms first, prefix/quality/size/model and then prefix/size/model, because the
per-image catalog is keyed by those dimensions. If neither exists the walk continues at step 1, and
a model that has only qualified forms then has nothing left to hit.
Where the model string comes from
Step 1 needs a model string, and it is not always in the JSON body. Three places are read, in a fixed order, and the body wins.
| Where | Consulted | Example |
|---|---|---|
| the body | always first, from the dialect’s own parse or a multipart model form field |
{"model": "gpt-4o"}, or Stability’s generate/sd3 form |
| the URL path | only when the body yielded none | /v1beta/models/{model}:generateContent, /model/{model}/converse |
| the URL query | only when the body and path yielded none | /v1/listen?model=nova-3 |
The multipart form field is the newest of the three, and Stability is why the order is worth
stating. Its generate/sd3 path serves seven catalog rows priced from $0.035 to $0.065, so a model
read from the path would be the same string for all seven and six of them would carry a confidently
wrong dollar. A request whose model is in none of the three is ledgered with an absent model and
resolves model_unknown, which is a stated absence rather than a guess.
The region rule
One step is not a “try next” but a stop.
That trade is visible in the numbers. The same Bedrock model, at three region classes:
PREFIX MODEL REGION RESULT KEY / REASON
bedrock anthropic.claude-3-5-haiku-20241022-v1:0 us PRICED bedrock/us.anthropic.… in=$0.80/M
bedrock anthropic.claude-3-5-haiku-20241022-v1:0 eu PRICED bedrock/eu.anthropic.… in=$0.25/M
bedrock anthropic.claude-3-5-haiku-20241022-v1:0 apac UNPRICED region_unknown
A region-neutral key for that model exists in the catalog and is not used. Between the two priced regions the rate differs by 3.2×, which is the size of the error the rule prevents.
The four reasons the walk runs out
When the walk finds nothing usable, the row carries "unpriced": true and a typed reason. Four of
them come out of the walk itself. They are different diagnoses and they want different fixes.
| Reason | What happened | Usually means |
|---|---|---|
model_unknown |
no key for that model under this prefix | wrong descriptor, or a model the catalog does not carry |
region_unknown |
region-qualified keys exist, none for your region | that rate is not published for your region |
endpoint_unpriced |
keys exist for the model, none serves this endpoint | calling a chat model at an embeddings endpoint |
unit_missing |
a key matched but lacks the units the endpoint bills in | an image call with no size or quality |
One request each, from the committed snapshot:
PREFIX MODEL ENDPOINT REGION REASON
openai deepseek-r1 chat model_unknown
bedrock anthropic.claude-3-5-haiku-20241022-v1:0 chat apac region_unknown
openai gpt-4o embeddings endpoint_unpriced
openai dall-e-3 images unit_missing
None of the four produces usd: 0. A zero would be a claim, and the claim would be wrong; the row
records the absence instead, and downstream totals exclude it and say so.
The two that are not pricing misses
The typed vocabulary holds six values, and the other two do not come from this walk at all. They come from the async settlement plane, where a job is submitted and costed later.
| Reason | What happened | What clears it |
|---|---|---|
settlement_pending |
a job was submitted and the provider has not costed it yet | a settlement companion row, minutes later |
settlement_terminal |
the job reached a terminal state that produced no priceable result | nothing; that money will not become known |
No pricing action clears either. A price-table edit fixes a model_unknown; it does nothing for a
job the provider has not finished, and nothing at all for one that failed. So the two carry their
own reason strings rather than borrowing model_unknown, and a work queue built from the unpriced
reasons should leave them out: counting them as catalog gaps reports a gap that does not exist.
Predicting your key before you send
Two things to do, in this order.
Check the descriptor’s prefix. It is in the provider reference, and it is what the candidate keys are built from.
Ask the binary for the rate, with the prefix attached:
$ tokentriage prices show groq/llama-3.3-70b-versatile
model: groq/llama-3.3-70b-versatile
resolved: groq/groq/llama-3.3-70b-versatile
provider: groq
snapshot: litellm@2026-07-14#179affb
source: litellm fetched 2026-07-14 generator pricegen v2
upstream_sha256: 179affb29103fed8b02a83e22e93209c2c65a393cbf9dd785dbaec0f8748d6f7
RATE USD / TOKEN USD / 1M TOKENS
input 5.9e-07 $0.5900
output 7.9e-07 $0.7900
cache_read (fallback = input) $0.5900
cache_write (fallback = input) $0.5900
reasoning (fallback = output) $0.7900
The snapshot line and the digest are the artifact’s, and they are the same on any machine holding this build; the model string is yours.
Diagnosing a key that came out wrong
Read three fields on the row and the answer is usually immediate:
"http": { "provider": "groq", "endpoint": "chat" },
"cost": { "pricing_key": "groq/groq/llama-3.3-70b-versatile",
"pricing_snapshot": "litellm@2026-07-14#179affb", "estimated": false }
| What you see | What it means |
|---|---|
http.provider is not what you expected |
a routing decision sent the request elsewhere; check decision.tier and decision.upstream on the same row |
pricing_key starts with the wrong prefix |
the upstream names a descriptor you did not intend, or the tier’s model string spells out a namespace itself; two Azure products and twelve Vertex namespaces are the usual culprits |
pricing_key is empty, no unpriced flag |
a declared-free upstream, working as configured |
unpriced: true with a settlement reason |
an async job; the dollar is on a later row, or it is not coming |
unpriced: true with any other reason |
read unpriced_reason and use the table above |
The catalog is upstream data
The vendored price file carries 2,889 priced model keys across 107 prefixes under the stamp
litellm@2026-07-14#179affb. That is a statistic about a vendored price table, not a claim about
what TokenTriage can reach: a key existing does not mean a descriptor binds its prefix, and a
descriptor binding a prefix does not mean it can reach every model priced inside it.
It also carries defects, and the copy is not edited to hide them. Under oci/ the artifact prices
openai.gpt-5, -mini and -nano, which Oracle does not document on OCI, while the two OpenAI
models Oracle does document there, gpt-oss-120b and gpt-oss-20b, carry no price at all. Call
either of those two through an OCI upstream and the row comes back model_unknown, because the
catalog holds no key for it. Silently correcting our copy would make it disagree with its source
and bury the defect rather than publishing it.
Where operator overrides fit
A pricing override you have configured is consulted before the catalog for token-billed endpoints,
and a row it prices carries an operator@ stamp instead of the catalog snapshot. So a
pricing_snapshot that does not start with litellm@ means the figure came from your own rate
table, which is what you want to see if you negotiated one.
Overrides carry token rates, so they are consulted for token-billed endpoints and skipped for the per-image, per-second and per-character ones, which fall straight through to the catalog.