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.
On this page
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 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 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 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:
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.
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:
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:
$ 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 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_urlis operator config, always. Descriptors hold relative paths, which is whybase_urlstays required even whenprovider:is set. - A credential. The descriptor names a scheme; your config names an environment variable. See how credentials are configured.
- 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.