Edit images
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.
/v1/images/editsAuthentication
Send your API key in a request header:
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.
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
modelfield; 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-2openai/gpt-image-2.5-flareopenai/gpt-image-2.5-sunburst |
Pass-through | None; everything is forwarded unchanged |
| Gemini Image | google/gemini-3-pro-imagegoogle/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.
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 |
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. 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 |
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.
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 |
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.
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'.
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"
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)
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:
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
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"]))
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: 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.
{
"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
}
}
{
"error": {
"message": "unsupported parameters for this model: quality",
"type": "invalid_request_error",
"code": "unsupported_parameters"
}
}
{
"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 |
Prices are those shown in the model marketplace in the console; the actual charge for each call is in the request logs.
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.
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
modelfield 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.
- Dropped parameters and strict mode: only Gemini drops parameters, listing them in the
X-Gateway-Unsupported-Parametersresponse header; withX-Gateway-Strict-Parameters: truethe 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.codeisupstream_rejectedanderror.messageis the upstream's original text; when no image was produced, the code isupstream_no_artifactwithupstream_finish_reason(such asNO_IMAGEorSAFETY).upstream_statuskeeps 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_statusandgenerated_by: "ai-gateway", plusupstream_request_idwhen 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.
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 |
| 400 | This model does not support the images API |
unsupported_model_api |
The interfaces the model declares (api_types in List models) 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 |
| 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; for 429, see 429: rate limits and quotas.
- 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.
- For the full list, see Errors and request identifiers.