Search

↑↓ to move↵ to openesc to closetop 6 documentation results

Book a demoTry TokenTriage

In development · Explore the product preview. Join the launch list or book a walkthrough.

Browse docs

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.

How-to guideLast updated

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/stream

A 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