Concepts / Images, audio & files

# Images, audio & files

Send images, audio, PDFs and video in a chat request on any API — checked against the model's input modalities before anything is routed or charged.

## Content parts

A user message's `content` can be a list of parts: text plus images, audio, PDF files and video, in the shape each API already uses. Every part is checked against the model's input modalities — the `modalities` list on `GET /v1/models/{id}` and on the model page — before anything is routed or charged.

| Input | /v1/chat/completions | /v1/responses | /v1/messages |
| --- | --- | --- | --- |
| Image | image_url: {url, detail?} | input_image: {image_url: "<url>", detail?} | image (source base64 or url) |
| Audio | input_audio: {data, format} | input_audio: {input_audio: {data, format}} | not available (no audio input) |
| PDF / file | file: {file: {filename?, file_data}} | input_file: {file_data \| file_url, filename?} | document (source base64, url, text or content) |
| Video | video_url: {video_url: {url}} | input_video: {video_url: "<url>"} | not available |

On `/v1/responses`, `input_audio` and `input_video` are gateway extensions in the chat shape: the gateway serves every API over the same models, so audio and video work there too where the model accepts them.

## Examples

An image and a recording in one turn, with the OpenAI Python SDK:

```
import base64
audio = base64.b64encode(open("question.wav", "rb").read()).decode()

r = client.chat.completions.create(
    model="google/gemini-2.5-flash",
    messages=[{"role": "user", "content": [
        {"type": "text", "text": "Answer the question in the recording about this chart."},
        {"type": "image_url", "image_url": {"url": "https://example.com/chart.png"}},
        {"type": "input_audio", "input_audio": {"data": audio, "format": "wav"}},
    ]}],
    max_tokens=400,
)
print(r.usage.prompt_tokens_details)   # cached_tokens, audio_tokens
```

A PDF with cURL (the file is sent inline as a data URL):

```
curl https://api.yelno.ai/v1/chat/completions \
  -H "Authorization: Bearer $YELNO_API_KEY" -H "Content-Type: application/json" \
  -d '{"model": "anthropic/claude-sonnet-4", "max_tokens": 600,
       "messages": [{"role": "user", "content": [
         {"type": "text", "text": "Summarize the termination clause."},
         {"type": "file", "file": {"filename": "contract.pdf",
           "file_data": "data:application/pdf;base64,JVBERi0xLjcK…"}}]}]}'
```

The same document through `/v1/messages` with the Anthropic SDK:

```
client.messages.create(model="anthropic/claude-sonnet-4", max_tokens=600, messages=[{
    "role": "user", "content": [
        {"type": "document", "title": "contract.pdf",
         "source": {"type": "base64", "media_type": "application/pdf", "data": pdf_b64}},
        {"type": "text", "text": "Summarize the termination clause."},
    ]}])
```

Video, where the model takes it, is a `video_url` part with an https URL or a `data:video/mp4;base64,…` URL.

## Limits

| Input | Max | Accepted |
| --- | --- | --- |
| Image | 20 MB | https URL or data:image/…;base64 — png, jpeg, webp, gif |
| Audio | 25 MB | base64 only; format wav, mp3, aiff, aac, ogg, flac, m4a, pcm16 or pcm24 |
| PDF / file | 50 MB | https URL or data:application/pdf;base64 |
| Video | 50 MB | https URL or data:video/mp4\|mpeg\|mov\|webm;base64 |
| All inline media | 70 MB | per request, across every part of every turn sent |

A conversation is sent whole on every turn, so the request limit counts the media of every earlier turn too. Linked (URL) media is fetched by the model's provider and is not measured before dispatch; some models take PDFs inline only, and a linked file to one of them is refused with `input_type_unsupported`. File references by `file_id` are not supported: send the bytes inline or a URL.

## Refusals

| HTTP | code | When |
| --- | --- | --- |
| 400 | input_type_unsupported | The model's input modalities do not include this type (the message lists what it accepts), or no host serving the model can carry it. param points at the part, e.g. messages[0].content[1]. |
| 413 | input_too_large | A part (or the request's inline media) is over its limit, measured on the decoded bytes. |
| 400 | audio_format_unsupported | input_audio.format is not one of the listed formats — convert webm recordings to wav. |
| 400 | file_reference_unsupported | A file_id (or an Anthropic file source) names a vendor's file store this gateway cannot reach — send the content inline or as a URL. |
| 400 | content_part_unsupported | An unknown part type that carries a payload, or media inside a tool result. |
| 400 | invalid_base64 | The payload is not valid base64. |

```
{"error": {"type": "invalid_request", "code": "input_type_unsupported", "param": "messages[0].content[1]",
  "message": "'mistralai/mistral-large-4-0' does not accept audio input; this model accepts text and images", "retriable": false}}
```

Every refusal happens before a provider is contacted and before any funds are held. A request with fallback models (`models` / `fallback_models`) skips the fallbacks that cannot take its inputs.

## How inputs are charged

Images, PDF pages and video are counted by the provider as input tokens and charged at the model's input price. Audio is reported apart (`usage.prompt_tokens_details.audio_tokens`, and `input_audio` in `yelno.usage.units`): it is charged at the model's audio input price where one is published, otherwise as ordinary input tokens — never free. `prompt_tokens` always includes the audio tokens.

The hold is sized for your attachments Before dispatch the gateway holds funds for the request's estimated cost, with media counted by type (about 1,600 tokens per image, 32 tokens per second of audio, 3,000 tokens per PDF page, 300 tokens per second of video). The charge is always the provider's reported usage, so a generous hold is released, not billed.

Source: https://yelno.ai/docs/inputs
