# Edit images

Source: https://platform.lyrouter.com/ai_gateway/docs/images-edits/en
Last updated: 2026-10-19

Modifies input images according to a prompt, or generates a new image using one or more images as references. It is compatible with `images.edit` in the OpenAI Images API, and the request body can be written in two ways: `multipart/form-data` with the image files uploaded (what the official OpenAI SDKs send), or JSON with the images as Base64 in the body (a LYRouter extension). The GPT-Image and Gemini Image families support editing, and both accept either form; Seedream does not support this endpoint, so for Seedream image-to-image use the `image` parameter of [Create images](https://platform.lyrouter.com/ai_gateway/docs/images-generations/en).

`POST /v1/images/edits`

## Authentication

Send your API key in a request header:

```text
Authorization: Bearer <API Key>
```

`Bearer` is case-sensitive. You can use `x-api-key: <API Key>` instead; when both are present, only `Authorization` is read. Create API keys on the workspace's **API Keys** page. Your API key is used only to authenticate with LYRouter and is never forwarded to the upstream provider. See [Base URLs and authentication](https://platform.lyrouter.com/ai_gateway/docs/call-gateway/en).

## Headers

| Header | Required | Description |
| --- | --- | --- |
| `Content-Type` | Yes | `multipart/form-data; boundary=...` or `application/json`. The multipart boundary is generated automatically by curl `-F` or the SDKs; if you build the request yourself, do not send a bare `multipart/form-data` |
| `X-Gateway-Strict-Parameters` | No | When set to `true` (lowercase only), a request containing any parameter the gateway would drop returns 400 `unsupported_parameters` immediately, without calling the upstream and without charge. On this endpoint only Gemini drops parameters; GPT-Image parameters are all forwarded unchanged and are not affected by this header |

## Request body

Both forms use the same field names; pick whichever is convenient. GPT-Image and Gemini support both.

### multipart/form-data

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `model` | string | Yes | Model ID, such as `openai/gpt-image-2`. Only concrete model IDs work: a virtual model `@vmodel/<name>` or `lyrouter/auto` returns 403 |
| `image` | file | Yes | The image file to edit. On Gemini, repeat the `image` field to send several images; they are used in the order they appear |
| `prompt` | string | Yes | Editing instruction |
| `mask` | file | No | Mask image, used by GPT-Image; dropped for Gemini |
| `quality` | string | See description | Used by GPT-Image and required for `gpt-image-2`; dropped for Gemini |
| `n` | integer | No | Number of images, used by GPT-Image (up to 10); dropped for Gemini |
| `size` | string | No | Output size; see the model sections for how to write it |

- GPT-Image: the gateway rewrites only the `model` field; files and other fields are forwarded to the upstream byte for byte.
- Gemini: the gateway parses the form, converts the files to Base64 (the format comes from the part's `Content-Type`, or from the file header when there is none), then translates it the same way as the JSON form. All files together must not exceed 30 MB, or the request returns 400.

### JSON

The official OpenAI `/images/edits` accepts only multipart; the JSON form is a LYRouter extension, with the images as Base64 in `image`.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `model` | string | Yes | Model ID, as above |
| `prompt` | string | Yes | Editing instruction; must not be empty |
| `image` | string \| array | Yes | One or more input images, written as `data:image/png;base64,<data>`; use an array for several images. Gemini also accepts Base64 without the prefix; see below |
| `quality`, `n`, `size` and others | — | No | Same as in the multipart form; see the model sections |

### How each model is handled

| Model family | Model IDs | Handling | Parameters dropped by the gateway |
| --- | --- | --- | --- |
| GPT-Image | `openai/gpt-image-2`<br>`openai/gpt-image-2.5-flare`<br>`openai/gpt-image-2.5-sunburst` | Pass-through | None; everything is forwarded unchanged |
| Gemini Image | `google/gemini-3-pro-image`<br>`google/gemini-3.1-flash-image` | Contract translation | Every parameter except `model`, `prompt`, `image` and `size`; `size` is also dropped when it cannot be matched |

Dropped parameters are listed in the `X-Gateway-Unsupported-Parameters` response header and the request still runs; with `X-Gateway-Strict-Parameters: true` it returns 400 instead. For a comparison of model capabilities and use cases, see [Multimodal capabilities](https://platform.lyrouter.com/ai_gateway/docs/multimodal/en).

### GPT-Image (2 / 2.5-flare / 2.5-sunburst)

Pass-through: the body is forwarded unchanged with only the model name rewritten. Parameters match the OpenAI image API, and the result is always `b64_json`.

| Parameter | Type | Description |
| --- | --- | --- |
| `quality` | string | Values differ by model: `gpt-image-2` takes `low`, `medium`, `high` or `auto` (`auto` behaves as `low` in measurements) and requires it, returning 400 intermittently when it is omitted (`INVALID_REQUEST_BODY`); the 2.5 family takes `low`, `medium`, `high`, `xhigh`, `max` or `auto`, defaults to `low`, and `xhigh` and `max` may be rejected upstream (if you get `Invalid value: 'xhigh'`, use `high`). See [Create images](https://platform.lyrouter.com/ai_gateway/docs/images-generations/en#gpt-image-2-2-5-flare-2-5-sunburst) |
| `size` | string | `widthxheight` only; tier names are not accepted. Must meet four hard constraints: width and height both divisible by 16, longest edge no more than 3840, and between about 1 M and about 8.3 M pixels in total; see [Create images](https://platform.lyrouter.com/ai_gateway/docs/images-generations/en#gpt-image-2-2-5-flare-2-5-sunburst). When it is omitted, the upstream picks the output size: in measurements, `gpt-image-2` returned `1254x1254` for a 1024×1024 input (not divisible by 16, so requesting this size on the generation endpoint is rejected), and `gpt-image-2.5-flare` returned `1024x1024` for a 1536×1024 input. Pass `size` explicitly when you need a fixed size |
| `mask` | file | Mask image, uploaded in the multipart form |
| `n`, `background`, `output_format`, `moderation`, `user` | — | Same as in [Create images](https://platform.lyrouter.com/ai_gateway/docs/images-generations/en#gpt-image-2-2-5-flare-2-5-sunburst) |
| `response_format`, `stream` | — | Do not send them. On this endpoint the gateway does not drop them: `response_format` is forwarded to the upstream unchanged, and this family always returns `b64_json`; when `stream` is `true`, the gateway currently adds `stream_options` itself, and the upstream returns 400 `Unknown parameter: 'stream_options'.` |

- **Latency**: the two forms cost the same on the gateway side (about 0.1 s); nearly all the time is spent upstream, anywhere from 20 s to more than 4 minutes, and the same request can take several times longer on a second attempt. Set the client timeout to at least 300 s.
- **Image validation**: the upstream validates the image itself and rejects images that are too small or corrupted (for example with `Invalid image file or mode`); retry with a normal image.

**Billing**: the same as for generation: per token, with input text, input image and output priced separately. Editing adds the cost of the input image: images are converted to tokens in 32×32-pixel blocks and reported in `usage.input_tokens_details.image_tokens`: an image whose long edge is under 1024 is first enlarged to twice its long edge (at most 1024), an image over about 1.52 million pixels is first scaled down proportionally, and the count is `ceil(width/32) × ceil(height/32)`. Measured: 1024 tokens for a 1024×1024 image and 1 token for an 8×8 image. A `mask` is counted into `image_tokens` by the same rule: a 1024×1024 image plus a mask of the same size measured 2048 `image_tokens`. With the same `quality` and `size`, editing produces the same output tokens as generation; for how output tokens vary with `quality`, see [Create images](https://platform.lyrouter.com/ai_gateway/docs/images-generations/en#gpt-image-2-2-5-flare-2-5-sunburst).

### Gemini Image (3 Pro / 3.1 Flash)

Contract translation: the gateway translates the request into Gemini's native `generateContent` format, uses only the parameters marked as applied below, and drops the rest, listing them in the response header. The result is always `b64_json`.

| Parameter | Applied | Description |
| --- | --- | --- |
| `prompt` | Applied | Required; omitting it or sending it empty returns 400 `prompt is required for this model` |
| `image` | Applied | One or more images. In the JSON form, when the value has a `data:<format>;base64,` prefix, the declared format is used; without the prefix, the format is detected from the PNG, JPEG, WEBP or GIF file header; if neither works, the request returns 400 naming which image failed. Image URLs are not accepted |
| `size` | Applied | Same as for generation: a tier name (`1K`, `2K`, `4K`) controls the resolution, and the provider rejects the `512` tier in production, so do not use it; `widthxheight` matches the aspect ratio and picks a tier by the long edge, and when the ratio is more than 2% off, the whole `size` is dropped while the request still returns 200 and is billed. Output tokens per tier differ between the two models; see [Create images](https://platform.lyrouter.com/ai_gateway/docs/images-generations/en#gemini-image-3-pro-3-1-flash) |
| `n`, `quality`, `mask`, `response_format`, `user` and others | Dropped | — |

- In the multipart form, all files together must not exceed 30 MB. The official OpenAI SDKs' `images.edit()` always sends multipart and can be used with Gemini directly.

**Billing**: per token, with input, output and thinking priced separately. Input images count toward "input" (`usage.input_tokens`), and each input image counts as a fixed number of tokens regardless of its size: production measurements give 560 per image for `gemini-3-pro-image` and 1120 per image for `gemini-3.1-flash-image`, twice as many. For thinking tokens, see [Create images](https://platform.lyrouter.com/ai_gateway/docs/images-generations/en#gemini-image-3-pro-3-1-flash).

## Request examples

### multipart/form-data

Using `openai/gpt-image-2` as an example, upload `cat.png` and save the result to a file. The Python example needs a recent version of the openai SDK: with 1.58.1, `images.edit()` does not accept `quality` and raises `TypeError: edit() got an unexpected keyword argument 'quality'`.

**Request examples (multipart)**

cURL:
```bash
curl https://api.lyrouter.com/v1/images/edits \
  --max-time 300 \
  -H "Authorization: Bearer $LYROUTER_API_KEY" \
  -F model=openai/gpt-image-2 \
  -F quality=medium \
  -F image=@cat.png \
  -F prompt="Replace the background with snowy mountains"
```

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

client = OpenAI(
    api_key=os.environ["LYROUTER_API_KEY"],
    base_url="https://api.lyrouter.com/v1",
    timeout=300,
)
with open("cat.png", "rb") as image:
    result = client.images.edit(
        model="openai/gpt-image-2",
        image=image,
        prompt="Replace the background with snowy mountains",
        quality="medium",
    )
with open("cat-snow.png", "wb") as f:
    f.write(base64.b64decode(result.data[0].b64_json))
print(result.usage)
```

Node.js:
```js
import { createReadStream, writeFileSync } from "node:fs";
import OpenAI, { toFile } from "openai";

const client = new OpenAI({
  apiKey: process.env.LYROUTER_API_KEY,
  baseURL: "https://api.lyrouter.com/v1",
  timeout: 300_000,
});
const result = await client.images.edit({
  model: "openai/gpt-image-2",
  // Use toFile to set the image type: with a bare createReadStream the file part is sent as application/octet-stream and the upstream returns 400
  image: await toFile(createReadStream("cat.png"), "cat.png", { type: "image/png" }),
  prompt: "Replace the background with snowy mountains",
  quality: "medium",
});
writeFileSync("cat-snow.png", Buffer.from(result.data[0].b64_json, "base64"));
console.log(result.usage);
```

### JSON

Using `google/gemini-3-pro-image` as an example, encode the image as Base64 and put it in the body:

**Request examples (JSON)**

cURL:
```bash
printf '{"model":"google/gemini-3-pro-image","prompt":"Replace the background with snowy mountains","image":"data:image/png;base64,%s"}' \
  "$(base64 < cat.png | tr -d '\n')" > body.json
curl https://api.lyrouter.com/v1/images/edits \
  --max-time 300 \
  -H "Authorization: Bearer $LYROUTER_API_KEY" \
  -H "Content-Type: application/json" \
  --data-binary @body.json
```

Python:
```python
import base64
import os
import httpx

with open("cat.png", "rb") as f:
    b64 = base64.b64encode(f.read()).decode()
resp = httpx.post(
    "https://api.lyrouter.com/v1/images/edits",
    headers={"Authorization": f"Bearer {os.environ['LYROUTER_API_KEY']}"},
    json={
        "model": "google/gemini-3-pro-image",
        "prompt": "Replace the background with snowy mountains",
        "image": f"data:image/png;base64,{b64}",  # use an array for several images
    },
    timeout=300,
)
resp.raise_for_status()
with open("cat-snow.png", "wb") as f:
    f.write(base64.b64decode(resp.json()["data"][0]["b64_json"]))
```

Node.js:
```js
import { readFileSync, writeFileSync } from "node:fs";

const b64 = readFileSync("cat.png").toString("base64");
const resp = await fetch("https://api.lyrouter.com/v1/images/edits", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.LYROUTER_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    model: "google/gemini-3-pro-image",
    prompt: "Replace the background with snowy mountains",
    image: `data:image/png;base64,${b64}`, // use an array for several images
  }),
  signal: AbortSignal.timeout(300_000),
});
const result = await resp.json();
if (!resp.ok) throw new Error(JSON.stringify(result.error));
writeFileSync("cat-snow.png", Buffer.from(result.data[0].b64_json, "base64"));
```

## Response

The response has the same format as [Create images](https://platform.lyrouter.com/ai_gateway/docs/images-generations/en): on success the images are in the top-level `data` array as `b64_json`; an error returns an `error` object. The 200 below is a GPT-Image edit (the multipart example above, without `size`); the 400 is Gemini receiving `quality`, which it does not support, with strict mode on; the 403 means the model is not enabled, or a virtual model or Auto was used.

**Response examples**

200:
```json
{
  "created": 1791950400,
  "data": [
    { "b64_json": "iVBORw0KGgoAAAANSUhEUgAA…" }
  ],
  "size": "1254x1254",
  "quality": "medium",
  "output_format": "png",
  "background": "opaque",
  "usage": {
    "input_tokens": 1036,
    "input_tokens_details": { "text_tokens": 12, "image_tokens": 1024 },
    "output_tokens": 2058,
    "total_tokens": 3094
  }
}
```

400:
```json
{
  "error": {
    "message": "unsupported parameters for this model: quality",
    "type": "invalid_request_error",
    "code": "unsupported_parameters"
  }
}
```

403:
```json
{
  "error": {
    "message": "Model is not available in the model plaza",
    "type": "access_denied",
    "code": "model_not_available"
  }
}
```

| Field | Description |
| --- | --- |
| `created` | Unix timestamp (seconds). Not returned by Gemini |
| `data` | Array of results; it is an array even for a single image |
| `data[].b64_json` | The image encoded in Base64 |
| `size`, `quality`, `output_format`, `background` | The generation settings actually used (returned by GPT-Image) |
| `usage` | Metering information. GPT-Image reports text and image tokens separately in `input_tokens_details`; for Gemini, input images count toward `input_tokens`, plus `output_tokens_details.reasoning_tokens` when there are thinking tokens. For each field, see [Create images](https://platform.lyrouter.com/ai_gateway/docs/images-generations/en) |

Prices are those shown in the model marketplace in the console; the actual charge for each call is in the [request logs](https://platform.lyrouter.com/ai_gateway/docs/logs/en).

### Response headers

| Header | Description |
| --- | --- |
| `X-Gateway-Unsupported-Parameters` | Present when the gateway dropped parameters (which happens only on Gemini); the value is a comma-separated list of parameter names, such as `n,quality`. The request still ran, and the listed parameters did not take effect |

When quotas are configured, the response also carries the `x-aigw-quota-*` headers; see [Gateway headers and extensions](https://platform.lyrouter.com/ai_gateway/docs/gateway-headers/en).

## Gateway behavior

Compared with calling the upstream directly, calling through LYRouter differs in these ways:

- **Two request body forms**: for GPT-Image, a multipart request has only its `model` field rewritten and the files are forwarded unchanged; a JSON request is forwarded unchanged. For Gemini, both forms are first normalized into the same set of fields and then translated into Gemini's format, so they behave identically.
- **Only the requested model is used**: image endpoints never switch to another model. When the same model has several channels, the gateway tries the other channels in turn after a failure; virtual models, Auto smart routing and fallback to the workspace's Default Model do not apply to image endpoints. See [Retries and fallback](https://platform.lyrouter.com/ai_gateway/docs/fallback/en).
- **Dropped parameters and strict mode**: only Gemini drops parameters, listing them in the `X-Gateway-Unsupported-Parameters` response header; with `X-Gateway-Strict-Parameters: true` the request returns 400 immediately instead, without calling the upstream and without charge. GPT-Image parameters are all forwarded unchanged, and the upstream decides whether to accept them.
- **Errors declared in a successful upstream response**: when Gemini's upstream rejects a request, it may return HTTP 200 with no image. The gateway rewrites this into an error response: when the upstream rejected the request, `error.code` is `upstream_rejected` and `error.message` is the upstream's original text; when no image was produced, the code is `upstream_no_artifact` with `upstream_finish_reason` (such as `NO_IMAGE` or `SAFETY`). `upstream_status` keeps the upstream's original 200. These errors are not retried on another channel; change the prompt or image as indicated.
- **Upstream errors are returned unchanged**: parameter and image errors for GPT-Image come from the upstream, with the upstream's format and error codes. When the upstream returns an error status with no body, the gateway fills in an error body with `upstream_status` and `generated_by: "ai-gateway"`, plus `upstream_request_id` when the upstream supplied a request ID; give that ID to the provider when reporting an issue.
- **Images are not stored in logs**: Base64 images in requests and responses are replaced in the logs with a `[base64 <bytes>B #<CRC32>]` summary. Not seeing the image in the log details does not mean the request failed; the image is in the response body.
- **Timeouts**: editing is synchronous too, and the GPT-Image family often takes several minutes. Set the client timeout to at least 300 s; after a client timeout, do not resubmit right away; first check the result of the previous request in the [request logs](https://platform.lyrouter.com/ai_gateway/docs/logs/en).

## Common errors

Errors returned by the gateway take the form `{"error":{"message":"...","type":"...","code":"..."}}`; parameter errors for GPT-Image come from the upstream in the upstream's format.

| Status code | `error.message` | `error.code` | Cause and fix |
| --- | --- | --- | --- |
| 400 | `unsupported parameters for this model: <names>` | `unsupported_parameters` | `X-Gateway-Strict-Parameters: true` was sent and Gemini received parameters it does not support. Remove the listed parameters |
| 400 | `prompt is required for this model` | `invalid_parameter` | No `prompt`, or an empty one, was sent to Gemini |
| 400 | ``image[<index>]: cannot determine the image MIME type; send a data URI such as `data:image/png;base64,<payload>` `` | `invalid_parameter` | Gemini could not recognize the format of the numbered image, for example because an image URL was sent. Send it as a data URI |
| 400 | `inline file parts exceed the limit of 31457280 bytes` | `invalid_parameter` | The files in a multipart request to Gemini exceed 30 MB in total. Compress the images and retry |
| 400 | `you must provide a model parameter` | `missing_model` | No `model` was given (a multipart request without a `model` field counts too) and the workspace has no Default Model. Always specify the model ID on image endpoints |
| 400 | `This model does not support the images_edits operation` | `unsupported_model_operation` | The selected model does not support image editing. Use GPT-Image or Gemini; for Seedream image-to-image, use [Create images](https://platform.lyrouter.com/ai_gateway/docs/images-generations/en) |
| 400 | `This model does not support the images API` | `unsupported_model_api` | The interfaces the model declares (`api_types` in [List models](https://platform.lyrouter.com/ai_gateway/docs/models/en)) do not include the image endpoints, for example a chat model used on this endpoint. A chat model that declares no `api_types` does not trigger this error: the request is forwarded upstream and may fail with an upstream error such as 404 `upstream_not_found`. Use only image models on image endpoints |
| 400 | The upstream's text, such as `Request contains an invalid argument.` | `upstream_rejected` | Gemini's upstream rejected the request; fix the parameters or image as the text says |
| 400 | `upstream produced no image (finishReason: <reason>)` | `upstream_no_artifact` | Gemini produced no image. Change the prompt or image according to `upstream_finish_reason` and retry |
| 400 | The upstream's text, such as `Invalid size '<width>x<height>'. Width and height must both be divisible by 16.` or `Invalid size '<width>x<height>'. Requested resolution is below the current minimum pixel budget.` | — | The GPT-Image `size` breaks one of the four hard constraints. Adjust it against the constraint table in [Create images](https://platform.lyrouter.com/ai_gateway/docs/images-generations/en#gpt-image-2-2-5-flare-2-5-sunburst) |
| 400 | The upstream's text | `INVALID_REQUEST_BODY` | The GPT-Image request body is invalid; first check that `quality` is set: omitting it on `gpt-image-2` returns 400 intermittently |
| 403 | `Model is not available in the model plaza` | `model_not_available` | The workspace has not enabled this model, or `@vmodel/<name>` or `lyrouter/auto` was used. Ask an administrator to enable it in the model marketplace, or use a concrete model ID that is enabled |

- For 401 / 403 authentication issues, see [401 / 403: authentication and access](https://platform.lyrouter.com/ai_gateway/docs/auth-errors/en); for 429, see [429: rate limits and quotas](https://platform.lyrouter.com/ai_gateway/docs/rate-errors/en).
- 500 / 504 mean the connection to the upstream failed or the wait for the upstream timed out; the response body is an HTML page rather than JSON. See [Timeouts and interrupted streams](https://platform.lyrouter.com/ai_gateway/docs/stream-errors/en).
- For the full list, see [Errors and request identifiers](https://platform.lyrouter.com/ai_gateway/docs/errors/en).
