# 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.

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

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

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](/docs/build/providers-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.

<Callout type="note">
An empty endpoint list on a descriptor means chat, which is the OpenAI-compatible default. The
counts above are what the descriptors declare, so a provider that supports embeddings upstream but
carries no priced embedding models in the catalog is not counted here. That is a coverage choice,
explained under [what is not covered](/docs/build/providers-coverage).
</Callout>

## 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](/docs/build/providers-async-jobs) is the mechanism, and [Reading a settled
cost](/docs/build/providers-settlement) is the arithmetic.

<Callout type="warning">
**A priced batch settlement overstates by a factor of two, and the row says so.** Settlement prices
at the standard rate; OpenAI publishes a batch tier at 50% of standard. The factor rides the
settlement row's own detail field rather than living in a document, so the magnitude travels with
the money. The batch-tier rate is a reserved slot, and it is left empty: "50% of standard" is not
universally derivable, because `gpt-3.5-turbo-instruct` has a standard row and no row at all in
OpenAI's batch pricing table. Halve a priced batch settlement before you quote it as spend.
</Callout>

## 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:

```text
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:

```text
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:

```json
"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.

<NextSteps items={[
  {title: "Embeddings, rerank, images and audio", href: "/docs/build/providers-specialists", description: "Configuring the providers that serve one of these kinds and nothing else, and what each bills you for."},
  {title: "Which pricing key your model lands under", href: "/docs/build/providers-pricing-keys", description: "How the kind narrows the candidate keys, and what each reason code means when none fits."},
  {title: "Providers that take a job and answer later", href: "/docs/build/providers-async-jobs", description: "Batch is wired now, and a batch row arrives in two parts rather than one."},
  {title: "Provider reference", href: "/docs/build/providers-reference", description: "Which descriptors declare each kind, beside their wire, auth and pricing namespace."}
]} />
