Endpoints and cost shapes
The 17 endpoint kinds TokenTriage recognises, which descriptors declare each, the five units they bill in, and the four kinds no descriptor names.
On this page
A provider is not uniformly supported. It serves some endpoints and not others, and the ones it serves bill in different units: chat bills per token, image generation bills per image, text-to-speech bills per character. This page is the lookup for both questions.
An endpoint kind is a dialect-neutral name for a surface. It is what lets /v1/chat/completions
and /v1/messages be the same kind of thing, and it is the field the router, the cache and the cost
engine all key off. The vocabulary registers 17 of them, and four are declared by no descriptor
at all. The generator prints that zero rather than omitting the row, so a registered-but-undeclared
kind is a stated fact instead of a gap in a table.
Two different facts sit behind those 17, and reading one as the other is the most common way to misread this page. Recognition is whether a path dispatches to a kind at all, and it comes from a wire driver’s own table of paths. Declaration is whether a provider descriptor lists the kind in its metadata, which is what config validation, virtual-key scoping and governance matching read. A kind can be recognised and served while no descriptor declares it, and one is.
The 17 registered kinds
TokenTriage ships 95 built-in provider descriptors, gated cell by cell against mocks, with ten of the 95 carrying no gated cell at all; the provider reference sets out the standard and names the ten. The right-hand column is how many of those 95 declare each kind, taken from the generated matrix on 2026-09-06. Declaring a kind and being gated on it are different facts, which is why the batch row below reads 0 while the endpoint is served.
| Kind | Path the openai driver registers |
Descriptors declaring it |
|---|---|---|
chat |
/v1/chat/completions (and /v1/messages on the Anthropic wire) |
80 |
responses |
/v1/responses |
1 |
embeddings |
/v1/embeddings |
17 |
images |
/v1/images/generations |
7 |
audio_speech |
/v1/audio/speech |
3 |
audio_transcription |
/v1/audio/transcriptions |
4 |
rerank |
/v1/rerank |
3 |
models |
GET /v1/models |
61 |
batch |
POST /v1/batches |
0 |
completions |
/v1/completions |
3 |
moderations |
/v1/moderations |
1 |
images_edits |
/v1/images/edits |
3 |
audio_translations |
/v1/audio/translations |
1 |
files |
/v1/files |
2 |
fine_tuning |
/v1/fine_tuning/jobs |
0 |
vector_stores |
/v1/vector_stores |
0 |
realtime |
/v1/realtime |
0 |
Only the openai driver registers all 17 paths; files is additionally registered by assemblyai
and soniox. The four zeros in the right-hand column count declaring descriptors and nothing
else, which is why a zero there is not a statement about whether the path is served.
The batch row, which reads zero and is served anyway
batch is served on the path OpenAI documents, POST /v1/batches, by the openai wire driver,
with reconciliation from the batch’s output file and a settlement companion row. No built-in
descriptor declares it, so the declaration column still reads zero: recognition here comes from the
driver’s own table of paths, not from provider metadata. The undocumented /v1/batch that earlier
builds bound no longer resolves at all, and a test pins both halves of that change.
A batch submission does not price on the spot. It carries the per-token cost shape, so the submit
row lands UNPRICED(settlement_pending) and a settlement companion row resolves it later by summing
the output file’s per-line usage. Providers that take a job and answer
later is the mechanism, and Reading a settled
cost is the arithmetic.
Two different fours
Four kinds have no declaring descriptor, and four kinds are recognition-only surfaces. They are not the same four, and they overlap in three members.
| The four with no declaring descriptor | The four recognition-only surfaces |
|---|---|
batch, fine_tuning, vector_stores, realtime |
files, fine_tuning, vector_stores, realtime |
They differ on batch against files. batch is served by the driver and declared by nobody;
files is a recognition-only surface that two descriptors do declare.
Recognition-only is not silence. For files, fine_tuning, vector_stores and realtime the
driver registers the path with no request parser, no usage extractor and no stream scanner, so both
directions relay byte for byte. What the request gains is the ledger dimension and the controls that
key off it: the row carries http.endpoint, the dialect resolves and picks its default upstream, a
virtual key can be scoped to or away from the path by name, a governance limit can match it, a cache
rule naming it is rejected as not cacheable instead of as an unknown kind, a descriptor may declare
it, and rows carrying it are savings-ineligible rather than falling back to the chat-like default.
What recognition does not give you is the stateful half. File content is not proxied, fine-tune
lifecycle state is not modelled, and vector-store consistency is not maintained. Matching is on the
collection path only, so /v1/files/{id}/content, /v1/fine_tuning/jobs/{id}/cancel and
/v1/vector_stores/{id}/search stay unrecognised ledgered passthrough. For a scoped virtual key
without allow_passthrough an unrecognised path is a scope rejection rather than a free pass, which
is the conservative direction and worth deciding on before you scope a key at /v1/files.
/v1/realtime is a path and not a transport. It is a bidirectional WebSocket surface, and this
gateway’s observation model is a tap on a request-and-response pair, which does not cover that
transport class. Recognition adds two facts to such a request: the row is stamped realtime, and
the OpenAI dialect resolves. There is no upgrade handler, no frame codec and no session state, and
no claim that a realtime session can be served.
Who declares what, beyond chat
Chat and models are near-universal, so the interesting rows are the other fifteen.
| Kind | Descriptors |
|---|---|
embeddings |
azure_ai, azure_openai, cohere, databricks, libertai, mistral, nebius, novita, openai, openai_compatible, perplexity, scaleway, together_ai, vercel_ai_gateway, voyage |
images |
aiml, black_forest_labs, fal_ai, openai, recraft, runwayml, stability |
audio_transcription |
assemblyai, deepgram, openai, soniox |
audio_speech |
elevenlabs, openai, runwayml |
rerank |
jina_ai, novita, voyage |
completions |
azure_text, openai, text_completion_openai |
images_edits |
black_forest_labs, openai, stability |
files |
assemblyai, soniox |
responses, moderations, audio_translations |
openai |
batch, fine_tuning, vector_stores, realtime |
none |
Thirteen descriptors declare no chat at all and exist for one or two surfaces each: assemblyai,
azure_text, black_forest_labs, deepgram, elevenlabs, fal_ai, jina_ai, recraft,
runwayml, soniox, stability, text_completion_openai and voyage. Pointing a routing tier at
one of those is a configuration mistake, and one check catches it: with
routing.allow_cross_dialect: true, validation rejects a tier whose upstream serves no chat
endpoint. Without that flag the check does not run, so nothing warns you.
completions is worth a caveat rather than a footnote. Two dated retirements ride its three
descriptors: OpenAI shuts down gpt-3.5-turbo-instruct, babbage-002 and davinci-002 on
2026-09-28, and Azure’s published retirement schedule lists no instruct row at all. Two further
catalog namespaces were left out of this kind rather than counted into it, because
text-completion-codestral and text-completion-inception are fill-in-the-middle wires on
/v1/fim/completions and not /v1/completions providers.
What each kind is allowed to do
Serving an endpoint is not the same as routing, caching or streaming it. Each kind carries four capability facts, and they explain behaviour that otherwise looks like a bug.
| Kind | Routable | Shadow-eligible | Streamable | Cacheable |
|---|---|---|---|---|
chat |
yes | yes | yes | yes |
responses |
yes | yes | yes | yes |
embeddings |
yes | no | no | no |
images |
no | no | no | no |
audio_speech |
no | no | no | no |
audio_transcription |
no | no | no | no |
rerank |
no | no | no | no |
models |
no | no | no | no |
batch |
no | no | no | no |
completions |
no | no | yes | no |
moderations |
no | no | no | no |
images_edits |
no | no | no | no |
audio_translations |
no | no | no | no |
files |
no | no | no | no |
fine_tuning |
no | no | no | no |
vector_stores |
no | no | no | no |
realtime |
no | no | no | no |
Read the first column first. Only chat, responses and embeddings are routable, and only chat and responses are shadow-eligible, which is why savings evidence is a chat-traffic story: a counterfactual needs a comparable alternative call, and an image or audio request does not have one.
completions is the one surprise in the third column. It streams because the legacy
text-completion stream shape is one the usage scanner already parses, which is why wiring the kind
meant a scanner and not a new codec. Streamable here means the kind has a server-sent-events usage
scanner, which is also why realtime reads no: a WebSocket does not have one.
Two of these columns are enforced where you can see them. A cache rule matching a non-cacheable kind is a configuration error named at its path rather than a silent no-op:
cache.rules[0].match.endpoint: endpoint kind "images" is not cacheable (chat|responses only in v1)
And the specialist audio drivers implement a model rewrite as a no-op, because neither audio kind is
routable, so nothing ever asks them to rewrite one. Where their model travels differs per driver and
is worth reading before you assume: Deepgram carries it in the URL query as ?model=nova-2, while
ElevenLabs carries it in the body as model_id and puts a voice id in the path. Elsewhere the
table is the product’s own classification of what each kind is for rather than a gate on the request
path, so treat a non-routable kind as “not designed to be routed” rather than “rejected if you
try”.
The five cost shapes
A cost shape selects the arithmetic, not the price. Every endpoint kind maps to one.
| Shape | Kinds | The bill is |
|---|---|---|
tokens |
chat, responses, embeddings, rerank, batch, completions | tokens in each bucket × that bucket’s rate |
per_image |
images, images_edits | images returned × the size-and-quality-qualified rate |
per_second |
audio_transcription, audio_translations | seconds of audio × the rate |
per_character |
audio_speech | characters of input text × the rate |
none |
models, moderations, files, fine_tuning, vector_stores, realtime | nothing; the kind is cost-exempt |
Cost-exempt is not priced-at-zero, and the two look identical in a column. A cost-exempt kind
returns a zero with no pricing key and no recorded pricing miss, because the surface bills nothing
per proxied call. The question that separates the two is whether a dollar arrives later: for batch
it does, which is why batch carries the per-token shape and settles rather than reading exempt.
Three of these carry a trap worth stating.
Per-image rates are keyed by size and quality, not by model alone. The catalog stores them as
<quality>/<size>/<model>, and where a model has only those qualified forms an unqualified request
has nothing to hit. openai/dall-e-3 goes further: it carries the image mode and no per-image rate
at all, so it resolves unit_missing rather than a rate that happens to be lying around:
PREFIX MODEL ENDPOINT RESULT REASON
openai dall-e-3 images UNPRICED unit_missing
Audio bills from different places at each end. Text-to-speech counts characters in the request you sent, which is known before the provider answers. Transcription counts seconds from the provider’s own response metadata, which means a response that omits the duration produces no figure rather than a guessed one.
audio_translations carries the per-second shape and prices nothing. No entry in the vendored
catalog declares support for /v1/audio/translations, so a translation resolves
UNPRICED(endpoint_unpriced). The transcription rate for the same model sits next door and is not
borrowed for it, which is the difference between a missing rate and an invented one.
Where a kind is decided
The endpoint kind is resolved from the inbound path and the dialect, before routing and before
pricing, and lands on the row as http.endpoint. Everything downstream reads it there:
"http": { "path": "/v1/chat/completions", "dialect": "openai",
"endpoint": "chat", "provider": "groq", "status": 200 }
If a call you expected to be priced comes back with no cost at all, http.endpoint is the first
field to check. A path that matched no registered kind is forwarded and ledgered, which is the
behaviour you want for an unusual call and looks identical to a pricing failure until you look.
What happens when a path matches no kind
A descriptor that does not declare a kind is not an error and does not block the call, and neither is a path no driver registers. The request takes the passthrough path: forwarded, relayed byte for byte, and written to the ledger, without endpoint-specific parsing or a cost figure. So “not supported” here means “not priced and not routed”, rather than “rejected”.
/v1/assistants is the clearest case, and the only one refused on a vendor’s own notice. OpenAI’s
deprecations page lists the Assistants API with a shutdown date of 2026-08-26, two days after this
page was written, and names the Responses API and Conversations API as replacements. Building an
adapter for a surface its vendor is removing would spend code, tests and documentation on
something dead on arrival, so no kind exists for it and none is planned. The call still relays and
it is still logged; it is not recognised, not priced and not claimed. If you
are on that API, /v1/responses has been a first-class kind here since well before it.