---
title: "Generate and edit images"
description: "Generate an image from a prompt with pirouter, then upload images for the model to edit: masks, combining several images, streamed previews, and which tokens a request is charged for."
canonical: "https://pirouter.ai/docs/images"
last-updated: "2026-10-16"
lang: "en"
source: "content/docs/en/(start)/images.mdx"
---

# Generate and edit images

Generate an image from a prompt with pirouter, then upload images for the model to edit: masks, combining several images, streamed previews, and which tokens a request is charged for.

pirouter has two image endpoints, both on the OpenAI Images protocol:

| Endpoint                      | What it does                                             | Request body          |
| ----------------------------- | -------------------------------------------------------- | --------------------- |
| `POST /v1/images/generations` | Generates images from a prompt                           | JSON                  |
| `POST /v1/images/edits`       | Edits one or more uploaded images, as a prompt describes | `multipart/form-data` |

This page follows one kite through both: generate a red kite, then turn it blue.

## Which models you can use

| Model                                | Generate | Edit |
| ------------------------------------ | -------- | ---- |
| `openai/gpt-image-2`                 | Yes      | Yes  |
| `openai/gpt-image-2.5-flare`         | Yes      | Yes  |
| `openai/gpt-image-2.5-sunburst`      | Yes      | Yes  |
| `google/gemini-3-pro-image`          | Yes      | No   |
| `google/gemini-3.1-flash-image`      | Yes      | No   |
| `google/gemini-3.1-flash-lite-image` | Yes      | No   |

The endpoints a model serves are the `supported_endpoints` returned by `GET /v1/models/{vendor}/{model}/endpoints`. See [Models & protocols](https://pirouter.ai/docs/models.md). Calling a model on an endpoint it does not serve returns `422`, and the `message` lists the endpoints it does serve.

## Generate an image

cURL:

```bash
curl https://api.pirouter.ai/v1/images/generations \
  -H "Authorization: Bearer $PIROUTER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "openai/gpt-image-2",
    "prompt": "A flat vector icon of a red paper kite on a plain white background",
    "size": "1024x1024",
    "quality": "low"
  }'
```

Python:

```python
import base64
import os

from openai import OpenAI

client = OpenAI(base_url="https://api.pirouter.ai/v1", api_key=os.environ["PIROUTER_API_KEY"])
result = client.images.generate(
    model="openai/gpt-image-2",
    prompt="A flat vector icon of a red paper kite on a plain white background",
    size="1024x1024",
    quality="low",
)
open("kite.png", "wb").write(base64.b64decode(result.data[0].b64_json))
```

The image comes back base64-encoded in `data[0].b64_json`. Decoded, it is a PNG file. `response_format: "url"` is not supported and returns `400 unknown_parameter`.

## Edit the image

Upload `kite.png` from the previous step as `image[]`, and say in `prompt` what to change:

cURL:

```bash
curl https://api.pirouter.ai/v1/images/edits \
  -H "Authorization: Bearer $PIROUTER_API_KEY" \
  -F model=openai/gpt-image-2 \
  -F "prompt=Make the kite blue; keep the background and composition unchanged" \
  -F "image[]=@kite.png" \
  -F size=1024x1024 \
  -F quality=low
```

Python:

```python
result = client.images.edit(
    model="openai/gpt-image-2",
    image=[open("kite.png", "rb")],
    prompt="Make the kite blue; keep the background and composition unchanged",
    size="1024x1024",
    quality="low",
)
open("kite-blue.png", "wb").write(base64.b64decode(result.data[0].b64_json))
```

| Original                                                             | Edited                                                 |
| -------------------------------------------------------------------- | ------------------------------------------------------ |
| ![A red paper kite on a white background](/docs/images/kite-red.jpg) | ![The same kite, now blue](/docs/images/kite-blue.jpg) |

The response has the same shape as a generation: the new image is in `data[0].b64_json`. Apart from `model` and `stream`, every form field (`size`, `quality`, `n`, `output_format` and so on) goes to the model unchanged.

The routing fields `provider`, `models`, `route` and `fallbacks` have no effect on the edit endpoint and are removed before forwarding, so an edit request does not [fall back](https://pirouter.ai/docs/fallbacks.md) to another model.

The whole request body can be at most 32 MiB. A larger one returns `413` and is neither forwarded nor charged.

### Edit only part of the image

Also upload a `mask`: a PNG the same size as the image, transparent where the model may make changes.

```bash
curl https://api.pirouter.ai/v1/images/edits \
  -H "Authorization: Bearer $PIROUTER_API_KEY" \
  -F model=openai/gpt-image-2 \
  -F "prompt=Add a small bright yellow sun in the masked area of the sky; keep everything else unchanged" \
  -F "image[]=@kite.png" \
  -F "mask=@mask.png"
```

### Combine several images

`image[]` can repeat. This combines the kite and a seascape into one poster with the official SDKs:

Python:

```python
result = client.images.edit(
    model="openai/gpt-image-2",
    image=[open("kite.png", "rb"), open("sea.png", "rb")],
    prompt="Combine the two pictures into one poster: the kite flying over the watercolor sea",
    size="1024x1024",
    quality="low",
)
```

TypeScript:

```ts
import fs from 'node:fs'
import OpenAI, { toFile } from 'openai'

const client = new OpenAI({ baseURL: 'https://api.pirouter.ai/v1', apiKey: process.env.PIROUTER_API_KEY })
const images = await Promise.all(
  ['kite.png', 'sea.png'].map((path) => toFile(fs.createReadStream(path), path, { type: 'image/png' })),
)
const result = await client.images.edit({
  model: 'openai/gpt-image-2',
  image: images,
  prompt: 'Combine the two pictures into one poster: the kite flying over the watercolor sea',
  size: '1024x1024',
  quality: 'low',
})
```

| Second input                              | Result                                                                  |
| ----------------------------------------- | ----------------------------------------------------------------------- |
| ![A watercolor sea](/docs/images/sea.jpg) | ![Kites flying over the watercolor sea](/docs/images/kite-over-sea.jpg) |

## Stream previews

Both endpoints accept `stream: true` (a form field, `-F stream=true`, on the edit endpoint). Set `partial_images` to the number of previews you want. The response becomes a stream of SSE events:

| Endpoint | Events, in order                                                     |
| -------- | -------------------------------------------------------------------- |
| Generate | `image_generation.partial_image` → `image_generation.completed`      |
| Edit     | `image_edit.partial_image` → `image_edit.completed` → `data: [DONE]` |

The `completed` event carries the final image and `usage`. The generation stream has no `data: [DONE]`: the `completed` event ends it. A model may send fewer previews than `partial_images` asks for, or none, and go straight to `completed`.

A large high-quality image can take over a minute. If nothing has been returned after 90 seconds, the gateway answers `200` first and sends keepalive content, so that nothing in between closes the connection. See [Long requests](https://pirouter.ai/docs/errors.md#long-requests).

## Billing

An image request is charged for three kinds of tokens, at each model's prices:

| Charged for  | What it counts                                    | `pricing` field |
| ------------ | ------------------------------------------------- | --------------- |
| Text input   | The tokens of `prompt`                            | `prompt`        |
| Image input  | Uploaded images and masks, as image tokens        | `image_token`   |
| Image output | The generated images, including streamed previews | `image_output`  |

The usage is in the response's `usage`: `input_tokens_details` splits `text_tokens` from `image_tokens`, and `output_tokens_details.image_tokens` is the image output. A generation has no image input.

The request above that turned the kite blue used 17 text input, 1,024 image input and 196 image output tokens. At the list prices of `openai/gpt-image-2` ($5 text input, $8 image input and $30 image output, per million tokens), that is $0.014157. The model had a 40% limited-time discount when this was measured, and the ledger charged $0.008494.

Two things cost more than you might expect:

- With `n` above 1, the input is charged once per image. An edit with `n=2` is charged twice the text and image input of the same edit with `n=1`.
- Streamed previews count as image output. The same edit with one preview had 273 image output tokens instead of 196.

A request with no image or no `prompt` returns `400`. A refused request is not charged. For other failures, see [Requests that fail](https://pirouter.ai/docs/billing.md#requests-that-fail).

## Next steps

- [Create image API](https://pirouter.ai/docs/api/images.md): Every parameter and response of the generation endpoint.
- [Edit image API](https://pirouter.ai/docs/api/image-edits.md): Every parameter and response of the edit endpoint.
