# 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.

Source: https://tokentriage.com/docs/build/providers-pricing-keys

---
import Callout from '../../../components/Callout.astro';
import Details from '../../../components/Details.astro';
import NextSteps from '../../../components/NextSteps.astro';
import PriceKeyResolveDiagram from '../../../components/PriceKeyResolveDiagram.astro';

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:

```text
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

<PriceKeyResolveDiagram />

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.

<Callout type="key">
Once a model has any region-qualified rate, a region without one resolves `region_unknown`. A
region-neutral entry for the same model is not substituted, and no neighbouring region's rate is
borrowed.
</Callout>

That trade is visible in the numbers. The same Bedrock model, at three region classes:

```text
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:

```text
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.

<Callout type="note">
An asserted zero is a different thing again. A declared-free upstream produces `usd: 0` with no
`unpriced` flag and a `descriptor@<name>#<hash>` provenance stamp, because "this costs nothing" is
a fact somebody stated rather than a lookup that failed. A cost-exempt endpoint, such as
`/v1/models`, produces the same unflagged zero for a different reason: that surface bills nothing
per call. See [self-hosted upstreams](/docs/build/providers-self-hosted).
</Callout>

## Predicting your key before you send

Two things to do, in this order.

**Check the descriptor's prefix.** It is in [the provider reference](/docs/build/providers-reference),
and it is what the candidate keys are built from.

**Ask the binary for the rate**, with the prefix attached:

```console
$ 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.

<Callout type="warning">
Pass the prefix. `prices show` is a catalog browser, and on a bare model string it searches across
every provider rather than inside one, so it can show you a rate your traffic will not be billed
at. `prices show deepseek-r1` resolves `azure_ai/azure_ai/deepseek-r1` at $1.35, while a request to
a DeepSeek upstream resolves $0.55. The request path and this command do not use the same lookup.
</Callout>

## Diagnosing a key that came out wrong

Read three fields on the row and the answer is usually immediate:

```json
"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.

<Details summary="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.

</Details>

<NextSteps items={[
  {title: "How to read a cost figure", href: "/docs/build/cost-figures", description: "What happens to these numbers downstream, and which rows a total leaves out."},
  {title: "Provider reference", href: "/docs/build/providers-reference", description: "Every descriptor's catalog prefix, which is where each candidate key starts."},
  {title: "When a provider won't connect", href: "/docs/build/providers-troubleshooting", description: "The wider diagnosis, in the order things usually break."},
  {title: "Reading a settled cost", href: "/docs/build/providers-settlement", description: "Two of the unpriced reasons are cleared by a later row, not by a price."}
]} />
