# Images in requests

Models that read images take them inside a message. This page covers images on Chat Completions and Messages, and a message can mix text and several images.

## Which models read images

A model reads images when `image` is in `architecture.input_modalities` on its entry in `GET https://api.llm.buzz/api/v1/models`. Most text models do today, and this lists them. Before a provider serves a model that takes images, an automated check sends it real requests for each capability the model declares, image input included, and a provider that fails stays out of routing: [Which providers serve](models-and-routing.md#which-providers-serve).

```bash
curl -s https://api.llm.buzz/api/v1/models \
  | jq -r '.data[] | select(.architecture.input_modalities | index("image")) | .id'
```

The [vision and image pipelines guide](/docs/guides/vision-and-image-pipelines) lists the models that read images and the ones that make them.

## Chat Completions

An image is an `image_url` part beside the text of a `user` message. This request sends a local file as a `data:` URL:

```python tab="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"],
)
with open("receipt.png", "rb") as image:
    encoded = base64.b64encode(image.read()).decode()
reply = client.chat.completions.create(
    model="deepseek/deepseek-v4.1-flash",
    messages=[
        {
            "role": "user",
            "content": [
                {"type": "text", "text": "What is the total on this receipt?"},
                {"type": "image_url", "image_url": {"url": f"data:image/png;base64,{encoded}"}},
            ],
        }
    ],
)
print(reply.choices[0].message.content)
```

```ts tab="TypeScript"
import { readFileSync } from "node:fs";
import OpenAI from "openai";

const client = new OpenAI({
  baseURL: "https://api.llm.buzz/api/v1",
  apiKey: process.env.LLM_BUZZ_KEY,
});
const encoded = readFileSync("receipt.png").toString("base64");
const reply = await client.chat.completions.create({
  model: "deepseek/deepseek-v4.1-flash",
  messages: [
    {
      role: "user",
      content: [
        { type: "text", text: "What is the total on this receipt?" },
        { type: "image_url", image_url: { url: `data:image/png;base64,${encoded}` } },
      ],
    },
  ],
});
console.log(reply.choices[0].message.content);
```

```bash tab="curl"
base64 -w0 receipt.png | jq -Rs '{
  model: "deepseek/deepseek-v4.1-flash",
  messages: [{role: "user", content: [
    {type: "text", text: "What is the total on this receipt?"},
    {type: "image_url", image_url: {url: ("data:image/png;base64," + .)}}
  ]}]
}' | curl https://api.llm.buzz/api/v1/chat/completions \
  -H "Authorization: Bearer $LLM_BUZZ_KEY" \
  -H "Content-Type: application/json" \
  -d @-
```

To send a URL instead, put it in `url`: `{"type": "image_url", "image_url": {"url": "https://example.com/receipt.png", "detail": "high"}}`.

- `url` is a `data:` URL with a base64 PNG, JPEG, GIF or WebP image, or an `http` or `https` URL. llm.buzz does not download a URL: the provider fetches it. A data URL that is not a base64 image of one of those types is 400 `invalid_parameter` naming the part.
- `detail` is `auto`, `low` or `high`, and is passed on to the provider. Another value is 400 `invalid_parameter`.
- `media_type` is an optional hint, one of `image/png`, `image/jpeg`, `image/gif` and `image/webp`, for clients such as GitHub Copilot that send it. llm.buzz accepts it and does not send it on: the `url` and `detail` decide. A data URL keeps its own type.
- An `image_url` object with a key other than `url`, `detail` and `media_type`, or an `image_url` that is a plain string, is 400 `invalid_parameter`.
- Image parts belong in `user` messages. A `tool` message may carry them too ([Images in tool results](#images-in-tool-results)), and an `assistant` message may carry the images a model that makes images returned, sent back to continue the conversation. An image part in a `system` or `developer` message is 400 `invalid_parameter`.
- An inline image is part of the request body, so it counts toward the 64 MiB limit: a larger body answers 413 `request_too_large`.

## Messages

An `image` block in a `user` message takes a `source`: base64 data with its `media_type`, or a URL.

```python
import base64

with open("receipt.png", "rb") as image:
    encoded = base64.standard_b64encode(image.read()).decode()
message = client.messages.create(
    model="anthropic/claude-sonnet-5.5",
    max_tokens=4096,
    messages=[
        {
            "role": "user",
            "content": [
                {"type": "image", "source": {"type": "base64", "media_type": "image/png", "data": encoded}},
                {"type": "text", "text": "What is the total on this receipt?"},
            ],
        }
    ],
)
for block in message.content:
    if block.type == "text":
        print(block.text)
```

The Anthropic SDK builds `client` as in [Messages](messages.md#send-a-request). A URL source is `{"type": "url", "url": "https://example.com/receipt.png"}`. The formats are PNG, JPEG, GIF and WebP. A base64 source of another type, and an image block in an `assistant` message, is a 400 `invalid_request_error` naming the field.

## Images in tool results

A tool can return an image, such as a screenshot, in the result it sends back to the model. Chat Completions, Responses and Messages each carry it in their own shape, and a provider whose tool results take only text gets the image in the user message that follows: [Tools and structured output](tools-and-structured-output.md#ids-arguments-and-tool-results).

## PDFs, audio and video

No chat model on llm.buzz reads PDFs, audio or video today: none lists them in `architecture.input_modalities`. A model that does lists them there. A Messages `document` block and a Chat `input_audio` part reach the same check as an image, so a request that carries one for a model that cannot read it is refused. To turn speech into text, use a transcription model: [Audio](audio.md).

## When a request is refused

llm.buzz refuses a part in a user or assistant message that the model cannot read with a 400 before any provider is called, instead of answering from the text around it. That covers an image for a model that does not read images, a URL image when every provider of the model takes inline images only, and a PDF, audio or video part. An image in a tool result is not refused: a provider that takes no image input gets placeholder text where the image was, named `messages.content.tool_result.image->placeholder`. Chat Completions answers 400 `unsupported_capability` with `metadata.param` set to `messages`. Messages answers 400 `invalid_request_error` and ends the message with `(param: messages)`. Send the image inline (a `data:` URL on Chat Completions, a base64 source on Messages) when a URL is refused, or choose a model that lists `image` in its `architecture.input_modalities`.

## Related

- [Chat Completions](chat-completions.md#the-messages-array) and [Messages](messages.md#request-fields) list every field of their API.
- [Errors](errors.md) lists every code.
