# Embeddings, rerank, images and audio

Configure the providers that serve one surface each, from embeddings to image edits, and read the three non-token units their rows bill in.

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

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

Most of a gateway's traffic is chat, and most of its documentation follows. But a retrieval
pipeline calls embeddings and rerank far more often than it calls a chat model, and an image or
audio pipeline never calls one at all. Those calls cost money that belongs in the same total.

Fifteen of the 95 built-in provider descriptors declare no chat endpoint. This page covers the earlier specialist set; `fireworks_ai_embeddings` and `vertex_ai_embedding`
are also listed in the current provider reference. Where a test gates one of these descriptors'
endpoints, the generated provider matrix names it: the test drives a request through the descriptor
against a mock shaped like that provider and asserts the ledger row's own provider and endpoint
stamps. The verification is against mocks, not the providers' live endpoints. Three of the eleven,
`black_forest_labs`, `fal_ai` and `runwayml`, carry no gated cell at all; the matrix prints
`declared (no e2e gate)` against them and calls that not a support claim, which is worth knowing
before you put one of the three in front of production traffic.

They are configured like any other upstream, and what changes is the unit on the row.

## Some of them answer later

Seven descriptors take a job and answer out of band: `replicate`, `fal_ai`, `black_forest_labs`,
`runwayml`, `stability`, `assemblyai` and `soniox`. Six answer only that way; `stability` answers
15 of its 17 operations inside the call and polls for the other two.

**A job is two rows, not one.** The submit lands on the ledger immediately, terminal and unpriced,
with the typed reason `settlement_pending`. A settlement companion row arrives minutes later,
joined by `req_id`, and its figure replaces the pending zero rather than adding to it. Turning the
plane on is one key, and it is off unless you say otherwise:

```yaml
jobs:
  enabled: true
```

Your client still speaks the provider's own async contract: take the job id off the row and poll
the provider through the gateway. [Providers that take a job and answer
later](/docs/build/providers-async-jobs) is where that mechanism is explained, including what
happens to a job nobody ever answers.

## Embeddings

Fifteen descriptors serve `/v1/embeddings`. Most are chat providers that also carry priced
embedding models; one, `voyage`, exists for retrieval and nothing else.

```yaml
upstreams:
  voyage:
    provider: voyage
    base_url: https://api.voyageai.com   # host only — your client already sends /v1
    api_key_env: VOYAGE_API_KEY
```

Embeddings bill in tokens, like chat, with one difference: there is no output. The rate applied is
the input rate, and the output rate is zero.

```text
PREFIX   MODEL      ENDPOINT    RESULT  KEY                       RATE
voyage   voyage-3   embeddings  PRICED  voyage/voyage/voyage-3    in=$0.06/M
openai   gpt-4o     embeddings  UNPRICED                          endpoint_unpriced
```

The second line is worth noticing. `gpt-4o` has a rate, but not one that serves the embeddings
endpoint, so the answer is `endpoint_unpriced` rather than the chat rate applied to an embedding
call. The endpoint is part of the lookup, not decoration on it.

The full list of fifteen is in [endpoints and cost shapes](/docs/build/providers-endpoints).

## Rerank

Three descriptors serve `/v1/rerank`: `jina_ai`, `voyage` and `novita`.

```yaml
upstreams:
  jina:
    provider: jina_ai
    base_url: https://api.jina.ai
    api_key_env: JINA_API_KEY
```

Rerank bills in tokens over the documents you submitted.

| Descriptor | Model in the catalog | Input rate |
|---|---|---|
| `novita` | `baai/bge-reranker-v2-m3` | $0.010 / M |
| `jina_ai` | `jina-reranker-v2-base-multilingual` | $0.018 / M |
| `voyage` | `rerank-2` | $0.050 / M |

Cohere serves rerank too, and is not claimed here. Its pricing is per search unit rather than per
token, and the vendored catalog carries no per-token rate for it, so a Cohere rerank call would
have to be either unpriced or guessed. It is left out rather than guessed.

## Images

Seven descriptors serve `/v1/images/generations`: `openai`, `aiml`, `recraft`, `stability`,
`fal_ai`, `black_forest_labs` and `runwayml`. Three of those also serve `/v1/images/edits`:
`openai`, `stability` and `black_forest_labs`. Both surfaces bill per image returned.

```yaml
upstreams:
  recraft:
    provider: recraft
    base_url: https://external.api.recraft.ai
    api_key_env: RECRAFT_API_KEY

  stability:
    provider: stability
    base_url: https://api.stability.ai
    api_key_env: STABILITY_API_KEY
```

`recraft` is image-only on the OpenAI wire and answers in the call. `stability` speaks its own
`/v2beta` stable-image wire: multipart in, image bytes out, and one path (`generate/sd3`) serving
seven catalog rows priced from $0.035 to $0.065. It tells those seven apart by reading the `model`
field out of the multipart form, which is a third place a model string can live beside the URL path
and the URL query.

<Callout type="warning">
Image rates are keyed by size and quality, not by model alone. The catalog stores them as
`<quality>/<size>/<model>`, so a request that carries neither dimension has no qualified rate to
hit. `azure/` carries six such entries for `dall-e-3`, from $0.04 to $0.12 per image depending on
size and quality. `openai/dall-e-3` carries the image mode and no per-image rate at all, so it
resolves `unit_missing` however you call it. Check `cost.unpriced_reason` when an image row has no
figure.
</Callout>

Where a model carries a flat per-image rate, it is used directly: `recraftv3` is $0.04 per image
and `aiml`'s `dall-e-3` entry is $0.052. Where the wire reports no quantity at all, as Stability's
and Black Forest Labs' do, the row bills one image per request and says so rather than inferring a
count.

## Text-to-speech

Three descriptors serve `/v1/audio/speech`: `openai`, `elevenlabs` and `runwayml`. ElevenLabs
speaks its own wire, with the voice id in the URL path and opaque binary audio coming back.

```yaml
upstreams:
  elevenlabs:
    provider: elevenlabs
    base_url: https://api.elevenlabs.io
    api_key_env: ELEVENLABS_API_KEY
```

Speech bills **per character of the text you sent**, which means the figure is known from the
request and does not depend on the response at all.

| Descriptor | Model | Rate |
|---|---|---|
| `openai` | `tts-1` | $0.000015 / character |
| `elevenlabs` | `eleven_multilingual_v2` | $0.00018 / character |

The response body is audio and is relayed byte for byte. Nothing parses it, and nothing needs to.

## Speech-to-text

Four descriptors serve `/v1/audio/transcriptions`: `openai`, `deepgram`, `assemblyai` and
`soniox`. Deepgram answers in the call, with the model in the URL query and a `Token ` prefix on
the credential; AssemblyAI and Soniox upload, submit and poll.

```yaml
upstreams:
  deepgram:
    provider: deepgram
    base_url: https://api.deepgram.com
    api_key_env: DEEPGRAM_API_KEY
```

Transcription bills **per second of audio**, taken from the provider's own response metadata.

| Descriptor | Model | Rate |
|---|---|---|
| `deepgram` | `nova-3` | $0.00007167 / second |
| `openai` | `whisper-1` | $0.0001 / second |
| `assemblyai` | `best` | $0.00003333 / second |
| `soniox` | `stt-async-v4` | no rate in the catalog |

That direction matters. Because the duration comes from the response, a reply that omits it
produces no cost figure rather than an estimate from the file size.

AssemblyAI's two catalog rows are its deprecated `speech_model` values, `best` and `nano`. A
present-day request naming a `universal-*` model settles `model_unknown`, with the measured seconds
still on the row, because no primary AssemblyAI document maps today's ids onto those two.

## Covered is not priced

Binding a provider means its traffic is routed, governed, journaled, polled and settled. It does
not mean every model produces a dollar, and the gap between those two claims is widest here.

<Callout type="key">
**Replicate contributes 40 catalog model rows and zero priced dollars.** Its prediction wire
reports elapsed seconds and no token count, while every `replicate/` row in the vendored price file
is priced per token, so the unit the price needs is absent from the wire and all 40 settle
`UNPRICED(unit_missing)`. That is a coverage win, and quoting those 40 in a spend or savings
sentence would be quoting the wrong number. The product publishes a per-prefix ledger saying, for
each async prefix, how many rows it has and what those rows settle to.
</Callout>

`soniox` is the mirror image, and the wire is not the reason. Soniox reports a measured audio
duration in milliseconds, which is what a per-second rate consumes, and the driver settles with it
and infers nothing. Both of its catalog rows carry no rate at all, so the key resolves and then has
nothing to apply, which is what `unit_missing` means. One `per_second` rate on those two rows would
make it priceable with no driver change: a pricing-data gap rather than a driver gap.

Six image rows measure their quantity and then decline to bill it, because the catalog's unit is
not the vendor's unit. Five `fal` models carry a `per_image` rate while fal publishes them per
megapixel or per quality tier, and Runway's `gen4_image` carries a flat rate for the 720p tier
while Runway charges more for 1080p with no tier derivable from the wire.

| Model | The catalog says | The vendor publishes |
|---|---|---|
| `fal-ai/flux/schnell` | `per_image: 0.003` | $0.003 per megapixel |
| `fal-ai/flux-pro/v1.1` | `per_image: 0.04` | $0.04 per megapixel |
| `fal-ai/stable-diffusion-v35-medium` | `per_image: 0.0398` | $0.02 per megapixel |
| `fal-ai/recraft/v3/text-to-image` | `per_image: 0.0398` | $0.04 per image, $0.08 for a vector style |
| `fal-ai/ideogram/v3` | `per_image: 0.06` | $0.03 turbo, $0.06 balanced, $0.09 quality |
| `runwayml/gen4_image` | `per_image: 0.05` | 5 credits at 720p, 8 credits at 1080p |

The job still succeeds and the row still lands; what is withheld is the number, and the row carries
the vendor's own published wording in its `error` field so you can price those calls yourself. Do
not read a missing dollar there as a free call. The withholding list is matched exactly rather than by
prefix, so a sibling id one character different, `fal-ai/flux-pro/v1.1-ultra`, prices normally. Runway's other
unpriced row is a different problem. The vendored catalog carries a key called
`runwayml/eleven_multilingual_v2`, which is the same model name the speech table above lists under
`elevenlabs/` and at a different rate. It bills per character, and Runway's task object carries no
character count anywhere, so this is a missing quantity rather than a mismatched unit.

## What can go wrong

| Symptom | Cause | Fix |
|---|---|---|
| `endpoint_unpriced` | the model has rates, but not for this endpoint | use a model the provider prices for that surface |
| `unit_missing` on an image call | no size or quality in the request, or a withheld unit | send the dimensions your provider expects; check the row's `error` field |
| a submit row with no `settlement_pending` and no later row | the job plane is off, so nothing recognises the submission | set `jobs.enabled: true` and restart |
| a transcription row with no cost | the response carried no duration | check the provider's response shape |
| an embeddings call is not routed | only chat, responses and embeddings are routable, and no rule matched | confirm the tier targets an embeddings-capable upstream |
| a rerank tier fails validation | with `allow_cross_dialect` on, a tier's upstream must serve chat | point the tier at a chat-capable upstream |

<Details summary="What is not claimed here">

AWS Polly for audio speaks a bespoke wire with no verifiable contract, and is published as refused
rather than pending. Cohere's rerank is left out for the reason above. Video generation has no
endpoint kind at all, so no descriptor can declare one: Runway is bound for images and speech only.

[The coverage page](/docs/build/providers-coverage) carries the full uncovered set with the blocker
for each, and it records the class that once held these image and audio job providers, now standing
at zero.

</Details>

<NextSteps items={[
  {title: "Endpoints and cost shapes", href: "/docs/build/providers-endpoints", description: "All the kinds, and the units they bill in, when you need the lookup rather than the configuration."},
  {title: "Which pricing key your model lands under", href: "/docs/build/providers-pricing-keys", description: "How the endpoint narrows the candidates, and what each unpriced reason means."},
  {title: "Providers that take a job and answer later", href: "/docs/build/providers-async-jobs", description: "Most of these submit a job and answer later, which changes what the first row means."},
  {title: "What isn't covered, and why", href: "/docs/build/providers-coverage", description: "The image and audio providers that are still absent, with the blocker for each."}
]} />
