# Quote a media model

This page shows how to quote an image, speech, transcription, embedding or video model. It tells you which features to declare, how to declare sizes, and how to write the price table. Read [Quote over the session](https://inference.ai.exchange/docs/provider-quoting.md) first for the frames.

A buyer's control works only when an offer declares it. If you do not declare a control, no job that sets it reaches you. This is on purpose: an offer that ignores a control and still charges for the job is not a fair sale.

## 1. Declare the controls

Each control has a feature. The feature has the name of the request field of the buyer. Put the features that you support in `supported_features` of the quote.

| Kind          | Field of the buyer                       | Feature                    |
| ------------- | ---------------------------------------- | -------------------------- |
| Image         | `seed`                                   | `image.seed`               |
| Image         | `background` (`transparent` or `opaque`) | `image.background`         |
| Image         | `output_compression`                     | `image.output_compression` |
| Image         | `stream: true`                           | `image.stream`             |
| Video         | `seed`                                   | `video.seed`               |
| Video         | `creativity`                             | `video.creativity`         |
| Video         | `upscale_factor`                         | `video.upscale_factor`     |
| Video         | `previous_job_id`                        | `video.continue`           |
| Speech        | `instructions`                           | `speech.instructions`      |
| Speech        | `input` as more than one turn            | `speech.turns`             |
| Transcription | `diarize`                                | `transcription.diarize`    |
| Transcription | `keyterms`                               | `transcription.keyterms`   |
| Embedding     | `input_type`                             | `embedding.input_type`     |

- Declare a control only if your agent sends it to the upstream and the upstream honors it. If you declare a control that you ignore, you are paid for a job that the buyer did not get.
- A reference input has its own feature: `input.image`, `input.audio`, `input.video` or `input.file`. Declare the kinds that your upstream reads.
- The exchange drops a name that it does not know. A typo in a name means that you win no job with that control.
- `resolution`, `aspect_ratio`, `size`, `voice`, `quality` and `generate_audio` are not features. The exchange matches them with the sizes, voices and options that you registered. See [Sizes and resolutions](#2-declare-sizes-and-resolutions).

### In an agent that you write

Send the features in the `quote` frame:

```json
{ "type": "quote", "supported_features": ["text", "image.seed", "image.background"] }
```

With the Rust agent library, an `Upstream` lists its controls in `Upstream::controls(kind, route, handle)`. It returns the features for that kind of job. The agent keeps only the controls that fit the kind. A rate card can narrow the list with `ExecutionRoute::features`. It cannot add to it. `Upstream::reference_inputs(kind)` lists the `input.*` features.

The Python reference provider declares only `text`. It cannot win a job that sets a control. Add the names that your upstream honors.

### What the OpenAI adapter declares

The first-party OpenAI adapter declares these controls:

| Kind   | Declared                                                                                                                                       |
| ------ | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| Image  | `image.background` and `image.output_compression` for a model whose name starts with `gpt-image`. `image.seed` when the route has a seed rule. |
| Video  | `video.seed` when the route has a seed rule.                                                                                                   |
| Speech | `speech.instructions` for a model whose name contains `gpt-4o`.                                                                                |

It declares nothing else. In particular, it declares no `image.stream`, `speech.turns`, transcription control, embedding control, `video.creativity`, `video.upscale_factor` or `video.continue`. To sell those, write your own agent.

## 2. Declare sizes and resolutions

The registration of the deployment sets what the model makes. See [Register deployments](https://inference.ai.exchange/docs/provider-models.md). The buyer's `resolution` and `aspect_ratio` resolve against it.

### Images

List the pixel sizes in `sizes` of the `image` output, as `<width>x<height>`. There is no separate list of tiers or shapes. The exchange works them out from the sizes.

- The tier of a size is the tier that is nearest to its area. The tiers are `512`, `768`, `1K`, `1.5K`, `2K` and `4K`. For example, `1024x1024`, `1536x1024` and `1024x1536` are `1K`, `1920x1080` is `1.5K`, and `2048x2048` is `2K`.
- A size has a shape when the two ratios differ by 1% or less.
- A buyer who sends `resolution` or `aspect_ratio` gets the declared size that has the tier and the shape. If more than one fits, the exchange picks the size that is nearest to the tier, then the one that is nearest to a square.
- That size is the size of the dispatch, and it is the size that sets the price.
- An offer with no `sizes` cannot win a job that sets `size`, `resolution` or `aspect_ratio`.
- If no size fits, the buyer gets `400 unsupported_parameter`, and the error lists the sizes that offers declare.

### Video

List the pixel sizes in `sizes` and the labels in `resolutions`, for example `["1280x720"]` and `["720p"]`.

- A `resolution` is a label. The exchange compares it with your `resolutions` as a string. An offer that lists none cannot win a job that sets `resolution`.
- With `aspect_ratio` and no `size`, the exchange picks the smallest declared size that has the shape. If the buyer also sent a label such as `720p`, the short side of the size must be 720.
- With `resolution` and no `aspect_ratio` and no `size`, the exchange picks no size. The label only selects offers.
- So to serve both fields, declare the sizes and the labels. An offer with `default_size_only` set declares no size for this.

## 3. Write the price table

The quote states prices in `pricing`. A price is USD for one unit. Each line has `billable`, `unit`, `usd` and, if the price depends on an option, a `variant`.

| `billable`              | `unit`      | Counted                                    |
| ----------------------- | ----------- | ------------------------------------------ |
| `input_text`            | `mtok`      | Input tokens that no cache served          |
| `cache_read`            | `mtok`      | Input tokens that a cache served           |
| `cache_write_short`     | `mtok`      | Tokens written to a cache for five minutes |
| `cache_write_long`      | `mtok`      | Tokens written to a cache for one hour     |
| `output_text`           | `mtok`      | Output tokens, not the audio tokens        |
| `output_audio`          | `mtok`      | Audio output tokens of a chat answer       |
| `output_image`          | `image`     | Images                                     |
| `output_speech`         | `character` | Characters of the text                     |
| `output_video`          | `second`    | Seconds of video                           |
| `input_audio`           | `second`    | Seconds of audio to transcribe             |
| `input_reference_image` | `image`     | Reference images                           |
| `input_reference_video` | `second`    | Seconds of reference video                 |
| `input_reference_audio` | `second`    | Seconds of reference audio                 |

- `unit` must be the unit of the billable. A speech price of $15.00 per million characters is `0.000015` for the unit `character`.
- An image, speech, video or transcription offer must have one line with no variant for its own billable: `output_image`, `output_speech`, `output_video` or `input_audio`. It cannot price the unit of another kind.
- A reference price is optional. A reference image that you do not price is free. A job with reference video or audio goes only to an offer that prices it.
- A `variant` is allowed on `output_video`, the three `input_reference_*` billables, `output_image` and `output_speech`. It has the options `resolution`, `audio`, `quality` and `size`. A video takes `resolution` and `audio`. An image takes `quality` and `size`. Speech takes `quality`.
- A variant line holds for a job that states every option the variant names. If more than one line holds, the line that names more options wins. If none holds, the line with no variant sets the price. A video job that does not state `generate_audio` counts as `audio: true`.
- A variant that names `audio` needs `audio` as `optional` in the `video` output. A variant that names a `resolution` needs that label in `resolutions`. A variant that names an image `size` needs that size in `sizes`. Otherwise the quote is refused, because no job can reach the price.

The cap and the charge of a counted job are known before dispatch. The exchange multiplies the count by the line that holds for the job. So the cap is exactly the charge.

### A video model

Registration:

```json
{
  "kind": "video_generation",
  "output_modalities": [
    { "type": "text" },
    {
      "type": "video",
      "sizes": ["1280x720", "1920x1080"],
      "resolutions": ["720p", "1080p"],
      "seconds": [4, 8],
      "audio": "optional",
      "formats": ["video/mp4"]
    }
  ]
}
```

Quote:

```json
"pricing": [
  { "billable": "output_video", "unit": "second", "usd": 0.10 },
  { "billable": "output_video", "unit": "second", "variant": { "audio": false }, "usd": 0.07 },
  { "billable": "output_video", "unit": "second", "variant": { "resolution": "720p" }, "usd": 0.06 },
  { "billable": "output_video", "unit": "second", "variant": { "resolution": "720p", "audio": false }, "usd": 0.04 },
  { "billable": "input_reference_video", "unit": "second", "usd": 0.02 }
]
```

A job for 8 seconds at `720p` with sound pays the third line: 8 × $0.06. A job for `1080p` with no other option pays the first line. A job for `720p` without sound pays the fourth line, because it names more options than the second and the third.

### An image model

Registration:

```json
{
  "kind": "image_generation",
  "output_modalities": [
    { "type": "text" },
    {
      "type": "image",
      "sizes": ["1024x1024", "1536x1024", "2048x2048"],
      "formats": ["png", "webp"],
      "max_count": 4
    }
  ]
}
```

Quote:

```json
"pricing": [
  { "billable": "output_image", "unit": "image", "usd": 0.04 },
  { "billable": "output_image", "unit": "image", "variant": { "size": "2048x2048" }, "usd": 0.08 }
]
```

A buyer who sends `resolution: "2K"` gets the size `2048x2048` and pays $0.08 for each image. A buyer who sends `1K` gets `1024x1024` and pays $0.04. A buyer who sends `quality` or `size` pays the line whose variant matches, and the line with no variant otherwise.

If your upstream bills tokens, set `pricing_unit` to `tokens` and `image_output_tokens_max` in the registration. Quote `input_text` and `output_text` lines, and no `output_image` line. See [Sell images](https://inference.ai.exchange/docs/provider-images.md).

### A speech model

Registration lists the `voices` and `formats`. Quote one line, and a variant for a quality that costs more:

```json
"pricing": [
  { "billable": "output_speech", "unit": "character", "usd": 0.000015 },
  { "billable": "output_speech", "unit": "character", "variant": { "quality": "hd" }, "usd": 0.00003 }
]
```

The charge is the characters of the text of all turns, times the price. A job for 500 characters pays $0.0075. With `quality: "hd"` it pays $0.015. A request with no `quality`, or a name that you do not price, pays the line with no variant.

### A transcription model

Quote one line: `{ "billable": "input_audio", "unit": "second", "usd": 0.0001 }`. The charge is the seconds of audio. See [Sell transcription](https://inference.ai.exchange/docs/provider-transcription.md).

### Old rate fields

An older quote states prices in fields such as `rate_per_image`, `rate_per_video_second` and `pricing_skus`. The exchange still reads them, but they are deprecated. No `rate_*` field is required.

- Send `pricing` only. It is the form that the ranking, the hold and the charge read.
- If a quote has both forms, they must state the same prices. If they differ, the exchange refuses the quote with `invalid_terms` on the field `pricing`. For example: `pricing states output_text at $2.000000000 and the old rate fields state output_text at $1.000000000`.
- Send both forms only to an exchange of an older release. It does not know `pricing`.

## 4. What your agent receives

A media dispatch has one object for its kind: `image`, `audio` (speech), `transcription`, `embedding` or `video`. Control fields are present only if the buyer set them and you declared the feature.

- **Image:** `count`, `size`, `output_format`, `quality`, `response_format`, `seed`, `background`, `output_compression` and `stream`. If `stream` is true, you can send `image_partial` events. See [Images](https://inference.ai.exchange/docs/provider-dispatch.md#images).
- **Speech:** `characters`, `voice`, `instructions`, `turns`, `response_format`, `speed` and `quality`. See [Speech](https://inference.ai.exchange/docs/provider-dispatch.md#speech).
- **Transcription:** `seconds`, `format`, `language`, `timestamps`, `prompt`, `temperature`, `diarize` and `keyterms`. Return one `transcript` event. With `diarize`, give each segment a `speaker`, such as `speaker_0`.
- **Embeddings:** `inputs`, `dimensions` and `input_type`. Return one vector for each input.
- **Video:** `seconds`, `size`, `audio`, `resolution`, `seed`, `creativity`, `upscale_factor` and `previous_job_id`. See [Video](https://inference.ai.exchange/docs/provider-dispatch.md#video).

For a video job, the exchange sends no cancel message. When the buyer cancels, or the job runs past the video job limit, it closes the dispatch connection. Watch for the close and stop. Each case costs the buyer nothing. See [Cancel and expiry](https://inference.ai.exchange/docs/provider-video.md#cancel-and-expiry).

For `previous_job_id`, the exchange sends only the job id of the earlier video. It does not check that the earlier job ran on your deployment. Map the id to your own handle, and fail the job if you do not know it. See [Continue a video](https://inference.ai.exchange/docs/provider-video.md#continue-a-video).

## 5. What buyers see

- The model card lists a control in `parameters` when at least one live offer declared the feature. `seed`, `background`, `output_compression`, `stream`, `creativity`, `upscale_factor`, `previous_job_id`, `instructions`, `turns`, `diarize`, `keyterms` and `input_type` are the keys.
- It also lists `size`, `output_format`, `quality`, `resolution`, `seconds` and `generate_audio` from your registration. Image `resolution` and `aspect_ratio` are not keys. Buyers read your sizes in `image_sizes`.
- It lists the cheapest line of each billable in `pricing`. If a price depends on an option, the line shows the `variant`.
- The receipt of a job has `usage.pricing`. Each line has the billable, the unit, the quantity and the provider charge. See [Receipts](https://inference.ai.exchange/docs/receipts.md).
- A buyer who sets a control that no offer declared gets `400 unsupported_parameter`. The error names the feature.

## 6. Checklist for a new model

1. Register the deployment with the right `kind` and the `output_modalities` of the model: sizes, voices, formats, lengths, `resolutions` and `audio`.
2. List only the sizes, voices and lengths that your upstream makes. A buyer can ask for any value you list.
3. Choose the controls that your agent sends and your upstream honors. Declare their features in `supported_features`.
4. Declare the `input.*` features for the reference inputs that you read.
5. Write `pricing`: one line with no variant for the billable of the kind, then the variant lines, then any reference prices. Use the unit of each billable.
6. Make sure that each variant names an option that the registration states.
7. Send the quote. Read the `quote_ack` or the `quote_reject`. For `invalid_terms`, read `field` and `reason`.
8. Quote as an unverified offer first. Buy one job from your own workspace for each control, and compare the result with the request. See [Unverified offers](https://inference.ai.exchange/docs/provider-models.md#unverified-offers).
9. Open the model card. Check `parameters`, `image_sizes` and `pricing`.
10. For video, test a cancel: the agent must stop when the dispatch closes.
