# What a provider descriptor carries

The five facts that make a provider reachable, why four are data rather than code, and when a provider needs its own descriptor.

Source: https://tokentriage.com/docs/build/providers-descriptors

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

Every gateway faces the same question: what actually differs between two providers? If the answer
is "everything", each one costs an integration and a release. If the answer is "five specific
things, four of which are values", most of them cost a row in a table.

TokenTriage takes the second answer. A **provider descriptor** is the row: the closed set of facts
that make one provider reachable, priced and distinguishable from its neighbours. The binary
compiles in 95 built-in provider descriptors, gated cell by cell against mocks, with ten of them
carrying no gated cell at all; the [provider reference](/docs/build/providers-reference) sets out
the standard and names the ten.

## The five facts

| Fact | What it decides | Code or data |
|---|---|---|
| wire dialect | the request and response shape on the wire | a reference to one of 15 wire-dialect drivers |
| auth scheme | how a credential becomes a signed request | data, one of 7 implementable schemes |
| URL shaping | per-endpoint path templates and required query parameters | data |
| pricing binding | which catalog namespace a model is looked up under | data |
| endpoints | which of the 17 registered endpoint kinds this provider serves | data |

Only the first is code, and it is a *reference* to code that already exists. A descriptor that
names the `openai` dialect adds no wire handling at all; it inherits the driver's default paths
and overrides only what differs.

That is why 78 of the 95 point at `openai`. The other 16 spread thinly across the remaining wires,
and each of those wires exists because a provider family speaks a genuinely different shape.
[Wire dialects](/docs/build/providers-dialects) has the assignment, driver by driver.

Two of the five facts have a closed vocabulary, which means you can count how the descriptors
divide across it. Seven authentication schemes are implementable and six are in use by a shipped
descriptor:

| Scheme | Descriptors declaring it |
|---|---|
| `bearer` | 61 |
| `gcp_oauth` | 13 |
| `header` | 10 |
| `none` | 8 |
| `aws_sigv4` | 2 |
| `snowflake_jwt` | 1 |
| `oci_sig` | 0 |
| `query` | reserved, and rejected when a descriptor declares it |

The six with a figure against them account for all 95. `oci_sig` is the seventh implementable
scheme and nothing declares it: the axis is wired and the signer was not built, so the builder
hands back an authenticator that loads the key file, reports its health honestly and injects
nothing, and the provider answers its own 401. The shipped `oci` descriptor authenticates with
`bearer`. The eighth name, `query`, is reserved and rejected when a descriptor declares it, because
a query-string credential lands in access logs, proxy logs and browser history: the carrier is the
objection rather than the provider.
[Authentication schemes](/docs/build/providers-auth) has each one end to end.

17 endpoint kinds are registered in the vocabulary, and four of them are declared by no descriptor
at all. A descriptor lists the subset it serves, and that list is what virtual-key scoping and
governance matching read.

## What "adding a provider" means in practice

Together AI was added as a descriptor file and one line in the aggregation list, plus its golden
and its end-to-end matrix cells. Nothing in the proxy, the routing engine or any driver changed. An
operator then reaches it in four lines:

```yaml
upstreams:
  together:
    provider: together_ai            # openai wire, bearer auth, together_ai/ pricing
    base_url: https://api.together.xyz   # host only — your client already sends /v1
    api_key_env: TOGETHER_API_KEY
```

That guarantee is checked rather than asserted, and both halves are checks you can run against the
repository: the suite drives a Together request end to end through the existing driver and asserts
the dollar it resolved, and a diff-surface check fails when a provider added that way touches a
file outside the descriptor allowlist.

<Callout type="key">
Two facts fall out of the descriptor without you configuring anything. A Together model prices
under `together_ai/`, and the identical model string pointed at an `openai` upstream comes back
`model_unknown` rather than borrowing OpenAI's rate. A self-hosted upstream gets an explicit `$0`
with a provenance stamp instead of an unpriced miss, so genuine zero and unknown stay distinct.
</Callout>

## URL shaping, and the closed variable set

Most descriptors carry no paths at all. The ones that do use templates over a closed set of six
variables: `model`, `deployment`, `project`, `location`, `region` and `api_version`. Anything else
in a template is rejected when the catalog is built.

Two of those variables are not yours to supply. `model` comes from the tier or the request, and
`region` has its own top-level field, so both are refused as `params:` keys with a message saying
where they belong. The rest you provide, and only the ones your descriptor's templates reference:

```yaml
upstreams:
  azure:
    provider: azure_openai
    base_url: https://my-resource.openai.azure.com
    params:
      deployment: gpt5-prod
      api_version: "2025-04-01-preview"
```

Validation checks that set both ways. An unknown key is named alongside the allowed ones, and a
missing required one is named too:

```console
$ tokentriage validate --config tokentriage.yaml
Error: configuration is invalid: upstreams["azure"].params.deployment_name: "deployment_name" is not a template variable of provider "azure_openai" (allowed: api_version, deployment)
upstreams["azure"].params.api_version: provider "azure_openai" requires template variable "api_version" (missing)
upstreams["azure"].params.deployment: provider "azure_openai" requires template variable "deployment" (missing)
```

A descriptor can also pin outbound headers. The `anthropic` descriptor sends
`anthropic-version: 2023-06-01`, and does so only when the inbound request did not already carry
its own, so a client that pinned a version keeps it.

## When a provider needs its own descriptor

Reuse the generic base when your provider is OpenAI-shaped and you only need it to work. Ask for a
descriptor when you need it to be *distinguishable*.

**A descriptor of its own is warranted when** the provider has a catalog namespace whose prices
differ from every neighbour's, or a URL shape that a base URL cannot express, or an authentication
scheme nothing else uses, or a set of endpoints that is not "chat and models".

**The generic base is enough when** you want a working upstream today and can accept that the
rows either price under a namespace you nominate or come back unpriced.
[Add a provider TokenTriage doesn't ship](/docs/build/providers-custom) covers both routes,
including the `providers:` block that lets you declare a descriptor in your own config without
touching Go.

The strongest single argument for a per-provider descriptor is the pricing namespace, because it
is the one that produces a wrong number rather than a missing one. `bedrock_converse` exists for
that reason: it speaks the same wire as `bedrock` and reuses the same driver, but it binds a
separate price bucket of 138 models. Zero new wire code, and a set of rates that would otherwise
be unreachable.

## What a descriptor does not carry

- **A host.** `base_url` is operator config, always. Descriptors hold relative paths, which is why
  `base_url` stays required even when `provider:` is set.
- **A credential.** The descriptor names a scheme; your config names an environment variable. See
  [how credentials are configured](/docs/build/providers-credentials).
- **A model list.** Models come from your tiers and from the requests your clients send. The price
  catalog is separate from the descriptor set, and the two are counted separately for that reason.
- **Rate limits or retries.** Cooldown and circuit-breaking are routing behaviour, not provider
  data — see [routing limits](/docs/build/routing-limits).

<NextSteps items={[
  {title: "Wire dialects", href: "/docs/build/providers-dialects", description: "The one fact on this list that is code rather than data, what a driver owns, and what naming a new one costs."},
  {title: "Endpoints and cost shapes", href: "/docs/build/providers-endpoints", description: "What \"the endpoints it serves\" is chosen from, and what each kind bills in once a request lands on it."},
  {title: "Add a provider TokenTriage doesn't ship", href: "/docs/build/providers-custom", description: "You now know the facts a descriptor carries; this is where you declare them yourself, in your own config."}
]} />
