# Image inputs

> Send photos, screenshots and diagrams to vision-capable models with the image_url content part, by HTTPS URL or inline base64.

Source: https://0.0.0.0:3000/docs/multimodal/images
Documentation index: https://0.0.0.0:3000/docs/llms.txt

Add an `image_url` content part to a `user` message, pointing at an HTTPS URL or holding the image as a base64 `data:` URL. The examples use `openai/gpt-6-luna`; replace it with a model whose endpoints accept `image` input (see [Route to vision-capable endpoints](#route-to-vision-capable-endpoints)).

## Send an image by URL

```request
{
  "method": "POST",
  "path": "/chat/completions",
  "body": {
    "model": "openai/gpt-6-luna",
    "messages": [
      {
        "role": "user",
        "content": [
          { "type": "text", "text": "Transcribe the total and the date from this receipt." },
          { "type": "image_url", "image_url": { "url": "https://example.com/images/receipt.jpg" } }
        ]
      }
    ]
  }
}
```

The URL must be publicly reachable over HTTPS when the request runs. Signed URLs work while they're valid. Images on `localhost` or a private network can't be fetched; send them inline instead.

## Send a local image as base64

Send a `data:` URL with the media type `image/png`, `image/jpeg`, `image/gif` or `image/webp`, matching the actual file, and standard (not URL-safe) base64:

:::code-group
```python title="Python"
import base64
import os
from pathlib import Path

from openai import OpenAI

MEDIA_TYPES = {".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif", ".webp": "image/webp"}

def image_data_url(path: str) -> str:
    file = Path(path)
    encoded = base64.b64encode(file.read_bytes()).decode("ascii")
    return f"data:{MEDIA_TYPES[file.suffix.lower()]};base64,{encoded}"

client = OpenAI(base_url="https://api.numes.ai/api/v1", api_key=os.environ["NUMES_API_KEY"])

completion = client.chat.completions.create(
    model="openai/gpt-6-luna",
    messages=[
        {
            "role": "user",
            "content": [
                {"type": "text", "text": "Describe this chart in two sentences."},
                {"type": "image_url", "image_url": {"url": image_data_url("chart.png")}},
            ],
        }
    ],
)
print(completion.choices[0].message.content)
```

```typescript title="TypeScript"
import { readFile } from "node:fs/promises";
import { extname } from "node:path";
import OpenAI from "openai";

const MEDIA_TYPES: Record<string, string> = {
  ".png": "image/png",
  ".jpg": "image/jpeg",
  ".jpeg": "image/jpeg",
  ".gif": "image/gif",
  ".webp": "image/webp",
};

async function imageDataUrl(path: string): Promise<string> {
  const mediaType = MEDIA_TYPES[extname(path).toLowerCase()];
  if (!mediaType) throw new Error(`Unsupported image type: ${path}`);
  return `data:${mediaType};base64,${(await readFile(path)).toString("base64")}`;
}

const client = new OpenAI({ baseURL: "https://api.numes.ai/api/v1", apiKey: process.env.NUMES_API_KEY });

const completion = await client.chat.completions.create({
  model: "openai/gpt-6-luna",
  messages: [
    {
      role: "user",
      content: [
        { type: "text", text: "Describe this chart in two sentences." },
        { type: "image_url", image_url: { url: await imageDataUrl("chart.png") } },
      ],
    },
  ],
});
console.log(completion.choices[0]?.message.content);
```
:::

Inline images count toward the 10 MiB chat body limit, and base64 adds about a third to their size. Keep inline images under about 7.5 MiB in total, or send HTTPS URLs.

## The image_url part

:::fields Content part
- `type` `"image_url"` required — Identifies an image part.
- `image_url` `object` required — The image to attach.
  - `url` `string` required — An `https://` URL or a `data:image/...;base64,` URL.
  - `detail` `"auto" | "low" | "high"` — Accepted for OpenAI compatibility. It isn't forwarded to providers, so it doesn't change resolution or cost.
:::

Add one part per image; images reach the model in the order you list them. The model doesn't see which sentence an image sat next to, so refer to images by position ("the first screenshot"). Images in earlier turns that you resend are sent, and billed, again.

## Route to vision-capable endpoints

Routing considers only endpoints whose `input_modalities` include `image`. Check a model's endpoints before you send images:

```request
{
  "method": "GET",
  "path": "/models/author/vision-model/endpoints"
}
```

You can combine image input with the usual [routing preferences](/docs/routing/provider-routing).

## Limits

| Limit | Value |
| --- | --- |
| Formats | PNG, JPEG, GIF, WebP |
| One image | 8 MiB decoded |
| All media in one request | 24 MiB decoded |
| Chat request body | 10 MiB, including inline `data:` URLs |

See [Sending media](/docs/multimodal/overview#sending-media) for the rules every media URL follows.

## Billing

Endpoints price images either per image, reported as `input_images` in `usage`, or as prompt tokens. Text tokens and any per-request price apply as usual. `pricing.image` in `GET /models` shows the lowest image rate of the model's representative endpoint. See [Usage accounting](/docs/account/usage-accounting).

## Troubleshooting

Image problems return 400 `invalid_request`, and the message names the failing part, such as `messages[0].content[1].image_url.url`.

| Symptom | Cause and fix |
| --- | --- |
| 400 with a `data:` URL | The media type isn't one of the four supported types (use `image/jpeg`, not `image/jpg`), doesn't match the bytes, or the data isn't standard base64. |
| 400 with an HTTPS URL | The server returned an error status, a compressed response or a mismatched `Content-Type` (such as `text/html` for an error page), or the URL isn't publicly reachable. |
| 400 for an `http://` URL | Use HTTPS or inline the image. |
| 400 for a large image | The image exceeds 8 MiB, or all media together exceed 24 MiB. |
| 400 for SVG, HEIC, BMP or TIFF | Convert the image to PNG, JPEG, GIF or WebP. |
| 400 with an image in a system message | Move media into a `user` message. |
| 408 `timeout` | The image host didn't respond in time. |
| 413 `payload_too_large` | Inline images pushed the body over 10 MiB. Use HTTPS URLs. |
| 503 `provider_unavailable` | No endpoint for the model accepts image input under your routing preferences. |

## Next steps

:::cards
- {file} [PDFs and files](/docs/multimodal/pdfs) Send PDF documents with the file content part.
- {audio} [Audio inputs](/docs/multimodal/audio) Send recorded audio to chat models.
- {sparkles} [Image generation](/docs/multimodal/image-generation) Create images from a prompt and reference images.
- {network} [Provider routing](/docs/routing/provider-routing) Choose which endpoints serve your requests.
:::
