Add a provider TokenTriage doesn't ship
Two ways to reach a provider with no built-in descriptor, what each gives up, and how to tell which one your provider needs.
On this page
TokenTriage ships 95 built-in provider descriptors, and that is still not every provider. A list of supported vendors is a poor place to discover that yours is missing.
The generated matrix names the test behind each gated provider-by-endpoint cell and prints the
ungated ones rather than hiding them; ten of the 95 carry no gated cell at all, and
openai_compatible — route one below — is one of the ten. Where a test does exist it runs
against a mock shaped like that provider, not the provider’s live endpoint.
The provider reference prints the evidence cell by cell.
If your provider speaks the OpenAI chat wire, you can reach it today without waiting for anybody, and you can decide whether its rows carry a dollar.
Two routes exist. Both are configuration; neither needs a build.
| Route | Use it when | Rows are |
|---|---|---|
provider: openai_compatible |
you want it working in four lines | unpriced, unless you assert free |
a providers: block |
you want it priced under a namespace you nominate | priced under that namespace |
If your provider does not speak the OpenAI wire, neither route helps, and the coverage page says why and what would be needed.
Route one: the generic base
openai_compatible is a built-in descriptor with the OpenAI wire, bearer authentication, and chat,
embeddings and models declared.
upstreams:
newvendor:
provider: openai_compatible
base_url: https://api.newvendor.example # host only — your client already sends /v1
api_key_env: NEWVENDOR_API_KEY
tiers:
- name: cheap
upstream: newvendor
model: nv-fast-1
That request is authenticated, forwarded, relayed byte for byte, and written to the ledger with its
usage counts. What it does not get is a price: the descriptor’s namespace is not one the vendored
catalog carries, so every row comes back unpriced with model_unknown.
That outcome is visible rather than silent. If the vendor charges you nothing, or you have a flat arrangement, say so with an inline descriptor and the rows become an asserted zero instead:
providers:
newvendor: # the descriptor id you name below
dialect: openai
pricing: { free: true } # your arrangement, asserted
upstreams:
newvendor:
provider: newvendor
base_url: https://api.newvendor.example
api_key_env: NEWVENDOR_API_KEY
The assertion has to live on a descriptor, not on the upstream. An
upstreams[…].pricing: free key validates and warns about catalog collisions, but nothing
forwards it to the transport, so rows through that upstream still resolve against the catalog and
still come back model_unknown. Route two below is the same mechanism with more of it filled in.
Route two: your own descriptor, in config
The providers: block declares a descriptor without touching Go. It is the same shape the built-in
ones have, restricted to OpenAI-compatible refinements.
providers:
newvendor: # the descriptor id you will name in upstreams
dialect: openai
auth: { scheme: bearer }
pricing: { catalog_prefix: newvendor }
endpoints: [chat, embeddings]
upstreams:
newvendor:
provider: newvendor
base_url: https://api.newvendor.example
api_key_env: NEWVENDOR_API_KEY
The fields available to you:
| Field | Required | What it does |
|---|---|---|
dialect |
yes | one of the known dialects; v1 inline providers are OpenAI-compatible refinements |
pricing.catalog_prefix |
one of the two | the namespace every lookup for this provider starts with |
pricing.free |
one of the two | declared-free instead of a lookup |
endpoints |
no | defaults to chat |
auth.scheme |
no | defaults to bearer; any implementable scheme is accepted, and each brings its own required upstream fields |
paths |
no | per-endpoint URL templates, over the closed variable set |
Naming a catalog_prefix does not create prices. It says where to look. If the vendored
catalog carries no entries under that prefix, you get model_unknown as before. The prefix is a
promise about which namespace, not that one exists.
That makes this route worth taking in two cases: your provider does have a prefix in the catalog that no descriptor binds, or you maintain your own rate table through pricing overrides and want the rows keyed somewhere stable.
On auth.scheme, one thing to know before you type it. Every scheme the authenticator
implements is accepted here, including oci_sig, which validates and boots and then injects no
credential at all, because its signer was specified and not built. If you write it you will get a
401 from the provider and a credential health state that reads ok, meaning only that your key
file parsed. The scheme list says what each of them does.
A provider whose URL shape differs
paths takes an object per endpoint, with a template and an optional stream_template, over the
same closed variable set the built-in descriptors use.
providers:
newvendor:
dialect: openai
pricing: { catalog_prefix: newvendor }
endpoints: [chat]
paths:
chat:
template: /api/v2/models/{model}/completions
stream_template: /api/v2/models/{model}/completions/streamA bare string in place of that object is a parse error, so you get the shape wrong once and not again. A template naming a variable outside the set is turned away with its position:
$ tokentriage validate --config tokentriage.yaml
Error: configuration is invalid: providers["newvendor"].paths["chat"]: path template "/api/v2/models/{deployment_id}/completions": unknown variable {deployment_id} at offset 15 (allowed: model, deployment, project, location, region, api_version)model comes from the tier or the request. Any other variable in a template becomes a required
params: key on every upstream naming this descriptor.
Checking it
The same three checks as any provider, and one extra question at the end.
$ tokentriage validate --config tokentriage.yaml
$ tokentriage doctor --config tokentriage.yaml
Then send one request and read the row. If you expected a price, check cost.pricing_key and
cost.unpriced_reason:
| What you see | What it means |
|---|---|
unpriced_reason: model_unknown |
nothing under your prefix matches that model string: either the catalog has no entry, or the vendor’s published id differs from what you sent |
a pricing_key under your prefix |
you are done |
pricing_key empty, no unpriced flag |
your descriptor’s pricing: { free: true } took effect |
When neither route works
Three shapes cannot be reached from config, however patient you are.
A different wire. A provider whose request body is not OpenAI-shaped needs a driver, and a driver is Go code rather than a config block. This is the most common reason a provider is absent.
A provider that answers later and nobody has written the poll for. The execution model itself is no longer the obstacle: submit-and-poll ships, with a job journal, a poller and a settlement row that carries the dollar when it arrives. What is per-provider code is the protocol, because how you submit a job, what handle comes back and how you read a terminal result differ at every vendor. So a new asynchronous provider is a driver, even though asynchronous providers in general are now a solved shape here.
An authentication scheme the authenticator does not implement. gigachat is the live example:
its blocker is a TLS client certificate, which belongs to the shared HTTP transport rather than to
the per-provider authenticator, so no config field reaches it. A scheme that needs new behaviour, as
opposed to new values, is code.
What is not covered groups every absent prefix by the blocker behind it, with a count for each group. If yours is there, the page tells you which blocker applies rather than leaving you to guess.
Two things this page used to list here, and how they came off
This section carried two more entries until 2026-08-24, and what happened to them is worth a paragraph, because one of them did not resolve the way the page predicted.
Submit-and-poll used to be the largest thing config could not reach, and this page said one
piece of infrastructure would open all of it. That infrastructure was built. replicate, fal_ai,
black_forest_labs, runwayml, stability, assemblyai and soniox now ship as built-in
descriptors, and OpenAI’s Batch API rides the same machinery. What remains, above, is narrower than
what was written here: the shape is solved, the per-vendor protocol is not.
Two credential paths were listed as schemes we did not have: Oracle Cloud’s request signing,
and Snowflake’s account-scoped keypair JWT. Both providers now ship, and only one of them arrived by
the predicted route. Snowflake did get a new scheme, snowflake_jwt, which signs a short-lived
token with your key pair. Oracle did not: Oracle documents a bearer credential for its Generative
AI service and states it is different from an IAM API key, so oci landed on the bearer scheme
that already shipped and no signer was written. The scheme constant for request signing exists and
nothing declares it.
The lesson worth keeping is that “this needs a new scheme” was a guess about a vendor’s documentation, and reading the documentation was cheaper than building the scheme.
What can go wrong
| Symptom | Cause | Fix |
|---|---|---|
does not name a known provider |
the providers: key and the provider: value disagree |
they must match exactly |
dialect: required |
an inline provider with no dialect | openai for an OpenAI-shaped vendor |
every row is model_unknown |
the prefix has no catalog entries | expected; assert free, or add overrides |
is not a template variable |
a paths template using an unknown variable |
only the six closed variables are allowed |
| requests 404 at the vendor | the vendor’s path differs from the driver’s default | declare it under paths |
a 401 with auth_health reading ok |
a scheme whose credential loaded but injects nothing | check the scheme you declared against the scheme list |