# Images

llm.buzz serves image generation models on OpenRouter's Image API, `POST /api/v1/images`, and on the OpenAI Images API, `POST /api/v1/images/generations`, so the OpenRouter and OpenAI SDKs reach them with the base URL alone. An answer is never streamed, and every image comes back as base64 in `b64_json`.

Both routes serve the same models, each with the same sizes and settings, and take your key as a bearer token ([Authentication](authentication.md)). Chat Completions, Responses and Messages refuse an image model with a 400 on `model` that names `POST /api/v1/images`. A chat model that draws returns its images in a Chat Completions answer instead, as the [API reference](api-reference.md#responses) shows; Responses and Messages refuse such a model.

## Find an image model

`GET https://api.llm.buzz/api/v1/models?output_modalities=image` lists the models that make images: the image generation models these routes serve, and chat models that return images in a chat answer. An image generation model lists as `text->image`, with a `context_length` of 32,000, the most characters its prompt takes. The [Models page](/models)'s Output filter picks out the models that make images, and an image model's page shows its price: per image, or per million input and output tokens.

In this list, `pricing.completion` of an image model is `"0"`, and its image price is in other keys. A model priced per token lists its input price as `pricing.prompt` and its output price as `pricing.image_output`, in US dollars per token. A model priced per image lists `pricing.image_token` and `pricing.image_output`, its price per image token; the price of one image is the `cost_usd` of its endpoints listing, below.

With your key, `GET https://api.llm.buzz/api/v1/images/models` lists exactly the image models your key can call, in OpenRouter's image model shape. Each one's `supported_parameters` names every field the model takes with its values: its sizes, aspect ratios, resolutions, qualities, backgrounds and formats, and the range of `n` up to its most images per request. `size` lists the model's sizes, where OpenRouter lists a boolean.

```bash
curl https://api.llm.buzz/api/v1/images/models \
  -H "Authorization: Bearer $LLM_BUZZ_KEY"
```

`GET https://api.llm.buzz/api/v1/images/models/{author}/{slug}/endpoints`, or `/images/models/{name}/endpoints` for a name with no `/`, lists the providers that may serve you the model, each with its `provider_tag`, its `supported_parameters` and its `pricing` at your rate. A model priced per image has one line, the price of one image:

```json
{"billable": "output_image", "unit": "image", "cost_usd": 0.03}
```

A model priced per token has an `input_text` and an `output_image` line, in US dollars per token. A model that makes no images answers 404, naming `GET /api/v1/images/models`. Both image model routes need a key: without one, they answer 401.

## Generate an image

```bash
curl https://api.llm.buzz/api/v1/images \
  -H "Authorization: Bearer $LLM_BUZZ_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model": "qwen/qwen-image-3", "prompt": "A line drawing of a beehive", "aspect_ratio": "16:9"}'
```

With the OpenAI SDK, call the OpenAI route. `model` and `prompt` are enough: the model's first size is its default.

```python
import base64
import os
from openai import OpenAI

client = OpenAI(
    base_url="https://api.llm.buzz/api/v1",
    api_key=os.environ["LLM_BUZZ_KEY"],
)
result = client.images.generate(
    model="qwen/qwen-image-3",
    prompt="A line drawing of a beehive",
)
# PNG, JPEG or WebP: output_format names which when the answer carries it; name the file to match.
with open("beehive.png", "wb") as file:
    file.write(base64.b64decode(result.data[0].b64_json))
```

The fields of `POST https://api.llm.buzz/api/v1/images`:

| Field | Rule |
|---|---|
| `model` | Required: an image model. Another model is a 400 on `model` that names `GET /api/v1/images/models` and the route you called. `:floor` and `:nitro` choose the provider order as on chat. |
| `prompt` | Required: 1 to 32,000 characters. |
| `n` | 1 to 10 images, 1 when omitted, and at most the model's most images per request. |
| `size` | One of the model's sizes, `WIDTHxHEIGHT` or `auto`, or a resolution tier such as `1K` or `2K`, read as `resolution`. |
| `aspect_ratio`, `resolution` | Choose one of the model's sizes, as below. |
| `quality`, `background`, `output_format` | One of the values the model takes. A model that takes none ignores the field and never sends it. `output_format` is `png`, `jpeg` or `webp`. |
| `output_compression` (0 to 100), `seed` | Sent only to a model that takes them, else ignored. |
| `provider` | Steers the request as on chat: `order`, `only`, `ignore`, `allow_fallbacks`, `sort`, `zdr` and `data_collection`; see [Provider preferences](models-and-routing.md#provider-preferences). |
| `user`, `session_id`, `trace` | Accepted. `user` is recorded as the end-user label; none of them is sent to the provider. |

`POST https://api.llm.buzz/api/v1/images/generations` takes the OpenAI SDK's fields under the same rules: `model`, `prompt`, `n`, `size`, `quality`, `background`, `output_format`, `output_compression`, `moderation`, `user`, and `provider` for routing. `moderation` is sent only to a model that takes it, and `style` is accepted and never sent. The OpenAI SDK's `standard` and `hd` qualities match none of a model's qualities, so send one that `GET /api/v1/images/models` lists. Both routes drop fields they do not know.

### How a size is chosen

A request resolves to exactly one of the model's sizes:

- An explicit `size` must be one of the model's sizes, and an `aspect_ratio` or `resolution` beside it must be that size's own.
- Otherwise `resolution` keeps the sizes of that tier, and `aspect_ratio` picks the first of them with that ratio (`auto` picks the model's `auto` size, else its first).
- With neither, the model's first size, its default.

A size, ratio or tier the model does not take is a 400 that names the field and the values it takes, such as "qwen/qwen-image-3 serves aspect_ratio 1:1, 4:3, 3:4, 16:9, 9:16, 3:2, 2:3; resend with one of them." Only what the model takes reaches the provider: `model`, `prompt`, `n`, the size (left out for `auto`), and the `quality`, `background`, `output_format`, `output_compression`, `seed` and `moderation` the model takes.

### The answer

```json
{"created": 1792900000, "data": [{"b64_json": "iVBORw0...", "media_type": "image/png"}],
 "usage": {"prompt_tokens": 0, "completion_tokens": 4175, "total_tokens": 4175, "cost": 0.03}}
```

Every image is PNG, JPEG or WebP, in base64 in `b64_json`, and `media_type` names its format from its bytes. `usage.cost` is what the call billed, in US dollars at your rate. A model priced per image counts each image as a fixed number of output tokens, so its `completion_tokens` is that count per image and its `prompt_tokens` is 0: 4,175 tokens an image on most models, the count OpenRouter's Image API reports for one image, and 1,120 on the Gemini image models. The OpenAI route answers in OpenAI's shape: `data` with `b64_json` and any `revised_prompt`, the `background`, `output_format`, `quality` and `size` the provider reports, and `usage` in OpenAI's names (`input_tokens`, `output_tokens`, `total_tokens`, `input_tokens_details` and `cost`). Both carry an `X-Generation-Id` header, which names the call's generation record: `GET /api/v1/generation?id=` answers what it cost ([Cost and the generation record](audio.md#cost-and-the-generation-record)).

A provider may return fewer images than `n`, as on OpenRouter: you get and pay for the images returned. A high-quality image can take minutes, and each provider gets at least 300 seconds to answer, so give your client a long timeout.

A thinking image model, such as `google/gemini-3-pro-image`, can draw up to two interim images before its final one, and a provider may send them ahead of it. llm.buzz then serves the last `n` images of the answer, and bills only those.

## Refusals

Each is a 400 before any provider is called, with `error.metadata.param` naming the field, and bills nothing:

| Request | Answer |
|---|---|
| A chat or embeddings model on either image route | `unsupported_capability` on `model`, naming the route you called: "The model 'deepseek/deepseek-v4.1-flash' does not generate images. Choose an image model from GET /api/v1/images/models and resend the request to POST /api/v1/images." |
| An image model on Chat Completions, Responses or Messages | `model`: "The model 'qwen/qwen-image-3' generates images and serves no chat request; send it to POST /api/v1/images." |
| A `size`, `aspect_ratio` or `resolution` the model does not take | The field and the values the model takes. |
| A `quality`, `background` or `output_format` value outside the ones the model takes | The field and the values the model takes. A model that takes none of a field ignores it. |
| `n` above the model's most images | `n`, with the most: "qwen/qwen-image-3 makes at most 6 images per request; resend with n at most 6." |
| `input_references`, `stream: true`, `output_format: "svg"`, `response_format: "url"` | The field and what to send: a text prompt only, no `stream`, `png`, `jpeg` or `webp`, and no `response_format`, since every image is `b64_json`. |

## Billing

An image model bills in one of two ways, which its price shows:

- **Per image:** each image the call returns, at the model's price per image and your rate. The model's pages show the price of one image, such as "$0.03/image".
- **Per token:** the input and output tokens the provider reports, at the model's input and output prices per million tokens and your rate.

Your credit, budgets and key limits apply as on chat; [Errors](errors.md) lists the 402 and 403 answers.

- A call that fails over to another provider bills only the provider that answered.
- A call bills nothing when it is refused, when its provider answered an error (a refusal of your prompt by the provider's moderation is a 400 you receive), when its provider never received the request, or when what the provider answered cannot be delivered, such as no image, more images than you asked for (apart from a thinking model's interim images, above), bytes that are not a PNG, JPEG or WebP image, an image over 16 MiB, or an answer over 48 MiB. Such an answer moves the call to the next provider.
- A timeout, a network fault after the request was sent, or your client disconnecting bills what llm.buzz reserved for the call, since the provider may have made and billed images nobody received: for a model priced per image, the `n` images you asked for; for a model priced per token, the most your prompt and `n` images can cost. A client that gives up on a slow image can be charged.
- Neither route reads an `Idempotency-Key`: a retry is a new request, served and billed again.

## Not supported

Image-to-image (`input_references`), image edits and variations, streaming partial images, SVG output, image URLs in the answer (every image is `b64_json`), separate prices per resolution or megapixel (a model's sizes share one price per image), the image model routes without a key, and image models in the console's Playground.
