Image Generation
RouterHub supports image generation and editing through the following API interfaces on this page:
| Interface | Endpoint | Request Shape |
|---|---|---|
| Chat Completions | POST /v1/chat/completions |
OpenAI Chat format — works with OpenAI SDK directly |
| Gemini Native | POST /v1beta/models/{model}:generateContent |
Google contents / parts format |
| OpenAI Images (Generations) | POST /v1/images/generations |
OpenAI-style model / prompt / size JSON |
| OpenAI Images (Edits) | POST /v1/images/edits |
OpenAI-style multipart/form-data with image file(s) and prompt |
Chat Completions Interface
Generate images using the standard OpenAI Chat Completions endpoint. This is the recommended approach if you already use the OpenAI SDK—no code changes needed beyond switching the model name. Both GPT Image and Gemini Image models are supported.
This endpoint is non-streaming only for image models. Setting stream: true will
return an error. Features like tools, response_format, reasoning, and
logprobs are not supported for image generation.
For the current list of available models, call GET /v1/models. See Models.
Request Body
| Field | Type | Description | |
|---|---|---|---|
| model | string | Required | An image model ID from the table above. |
| messages | array | Required | Array of message objects. The last user message's text content is used as the image prompt.
Content can be a plain string or an array of content parts. |
| n | number | Optional | Number of images to generate (default: 1). Only applies to GPT Image models. |
Response Format
The response follows the standard Chat Completions shape. Generated images are returned as image_url
content parts with base64 data URIs:
{
"id": "chatcmpl-abc123",
"object": "chat.completion",
"created": 1735689600,
"model": "openai/gpt-image-2",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": [
{
"type": "image_url",
"image_url": {
"url": "data:image/png;base64,iVBORw0KGgo..."
}
}
]
},
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 12,
"completion_tokens": 1290,
"total_tokens": 1302
}
}
Examples — GPT Image
# Generate an image and save it to a file
curl https://api.routerhub.ai/v1/chat/completions \
-H "Authorization: Bearer $ROUTERHUB_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "openai/gpt-image-2",
"messages": [
{"role": "user", "content": "A cute baby otter wearing a tiny top hat, watercolor style"}
]
}' | jq -r '.choices[0].message.content[0].image_url.url' \
| sed 's/^data:image\/png;base64,//' \
| base64 -d > otter.png
import base64
from openai import OpenAI
client = OpenAI(
api_key="YOUR_API_KEY",
base_url="https://api.routerhub.ai/v1",
)
response = client.chat.completions.create(
model="openai/gpt-image-2",
messages=[
{"role": "user", "content": "A cute baby otter wearing a tiny top hat, watercolor style"}
],
)
# Find the image part in the response (may be preceded by text)
for part in response.choices[0].message.content:
if part["type"] == "image_url":
img_b64 = part["image_url"]["url"].replace("data:image/png;base64,", "")
img_bytes = base64.b64decode(img_b64)
with open("otter.png", "wb") as f:
f.write(img_bytes)
print("Saved otter.png")
break
Examples — Gemini Image
# Generate an image with Gemini and save it
curl https://api.routerhub.ai/v1/chat/completions \
-H "Authorization: Bearer $ROUTERHUB_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "google/gemini-3.1-flash-image-preview",
"messages": [
{"role": "user", "content": "A serene mountain landscape at sunset, oil painting style"}
]
}' | jq -r '.choices[0].message.content[0].image_url.url' \
| sed 's/^data:image\/png;base64,//' \
| base64 -d > landscape.png
import base64
from openai import OpenAI
client = OpenAI(
api_key="YOUR_API_KEY",
base_url="https://api.routerhub.ai/v1",
)
response = client.chat.completions.create(
model="google/gemini-3.1-flash-image-preview",
messages=[
{"role": "user", "content": "A serene mountain landscape at sunset, oil painting style"}
],
)
# Find the image part in the response (may be preceded by text)
for part in response.choices[0].message.content:
if part["type"] == "image_url":
img_b64 = part["image_url"]["url"].replace("data:image/png;base64,", "")
img_bytes = base64.b64decode(img_b64)
with open("landscape.png", "wb") as f:
f.write(img_bytes)
print("Saved landscape.png")
break
Gemini Native Interface
Generate images using Google's Gemini image generation models. This endpoint uses the native Gemini
generateContent API format—not the OpenAI Images format.
This endpoint is non-streaming only. The response is returned as a single JSON object once generation is complete.
For text-only Gemini requests (:generateContent on any text model, plus
:streamGenerateContent) see Generate Content. The same URL
pattern serves both flows — RouterHub selects the handler based on the resolved model class.
For the current list of available models, call GET /v1/models. See Models.
The google/ prefix is optional in the URL. For example, both
/v1beta/models/gemini-2.5-flash-image:generateContent and
/v1beta/models/google/gemini-2.5-flash-image:generateContent work.
Authentication
This endpoint supports two authentication methods:
| Method | Header | Example |
|---|---|---|
| Bearer token | Authorization |
Authorization: Bearer rh_your_api_key |
| Google-style API key | x-goog-api-key |
x-goog-api-key: rh_your_api_key |
Request Body
| Field | Type | Description | |
|---|---|---|---|
| contents | array | Required | Array of Content objects with role and parts. See Content Object. |
| generationConfig | object | Optional | Generation parameters (temperature, responseModalities, etc.). Defaults to
responseModalities: ["TEXT", "IMAGE"] if omitted. |
| safetySettings | array | Optional | Safety filter thresholds for content categories. |
| systemInstruction | object | Optional | System-level instruction as a Content object. |
| cachedContent | string | Optional | Resource name of cached content (e.g., projects/my-project/cachedContents/abc123). |
Content Object
| Field | Type | Description | |
|---|---|---|---|
| role | string | Required | "user" or "model" |
| parts | array | Required | Array of parts. Each part contains one of: text (string) or inlineData (object
with mimeType and base64-encoded data). |
Response Body
| Field | Type | Description |
|---|---|---|
| candidates | array | Array of candidate responses. Each has content (with parts),
finishReason, and optional safetyRatings. |
| modelVersion | string | The model version used. |
| usageMetadata | object | Token usage: promptTokenCount, candidatesTokenCount,
totalTokenCount. |
| promptFeedback | object | Present if the prompt was blocked. Contains blockReason and safetyRatings. |
Generated images appear as inlineData parts in the candidate's content, with mimeType
(e.g., image/png) and base64-encoded data.
Examples
Text-to-Image
curl https://api.routerhub.ai/v1beta/models/gemini-2.5-flash-image:generateContent \
-H "Authorization: Bearer $ROUTERHUB_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"contents": [
{
"role": "user",
"parts": [{"text": "A cute baby otter wearing a tiny top hat, watercolor style"}]
}
]
}'
import requests
import base64
response = requests.post(
"https://api.routerhub.ai/v1beta/models/gemini-2.5-flash-image:generateContent",
headers={
"Authorization": "Bearer YOUR_API_KEY",
"Content-Type": "application/json",
},
json={
"contents": [
{
"role": "user",
"parts": [{"text": "A cute baby otter wearing a tiny top hat, watercolor style"}],
}
],
},
)
data = response.json()
for part in data["candidates"][0]["content"]["parts"]:
if "inlineData" in part:
img_bytes = base64.b64decode(part["inlineData"]["data"])
with open("otter.png", "wb") as f:
f.write(img_bytes)
print("Saved otter.png")
elif "text" in part:
print(part["text"])
Image Editing
Send an existing image along with an editing instruction:
# Base64-encode your image first
IMG_B64=$(base64 -w0 photo.png)
curl https://api.routerhub.ai/v1beta/models/gemini-3-pro-image-preview:generateContent \
-H "Authorization: Bearer $ROUTERHUB_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"contents": [
{
"role": "user",
"parts": [
{"inlineData": {"mimeType": "image/png", "data": "'${IMG_B64}'"}},
{"text": "Remove the background and replace it with a beach scene"}
]
}
]
}'
import requests
import base64
with open("photo.png", "rb") as f:
img_b64 = base64.b64encode(f.read()).decode()
response = requests.post(
"https://api.routerhub.ai/v1beta/models/gemini-3-pro-image-preview:generateContent",
headers={
"Authorization": "Bearer YOUR_API_KEY",
"Content-Type": "application/json",
},
json={
"contents": [
{
"role": "user",
"parts": [
{"inlineData": {"mimeType": "image/png", "data": img_b64}},
{"text": "Remove the background and replace it with a beach scene"},
],
}
],
},
)
data = response.json()
for part in data["candidates"][0]["content"]["parts"]:
if "inlineData" in part:
img_bytes = base64.b64decode(part["inlineData"]["data"])
with open("edited.png", "wb") as f:
f.write(img_bytes)
print("Saved edited.png")
With Generation Config
curl https://api.routerhub.ai/v1beta/models/gemini-3.1-flash-image-preview:generateContent \
-H "Authorization: Bearer $ROUTERHUB_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"contents": [
{
"role": "user",
"parts": [{"text": "A serene mountain landscape at sunset"}]
}
],
"generationConfig": {
"temperature": 1.0,
"responseModalities": ["TEXT", "IMAGE"]
}
}'
Sample Response
{
"candidates": [
{
"content": {
"role": "model",
"parts": [
{
"inlineData": {
"mimeType": "image/png",
"data": "iVBORw0KGgo..."
}
},
{
"text": "Here is your otter wearing a top hat!"
}
]
},
"finishReason": "STOP"
}
],
"modelVersion": "gemini-2.5-flash-image",
"usageMetadata": {
"promptTokenCount": 12,
"candidatesTokenCount": 1290,
"totalTokenCount": 1302
}
}
Gemini Error Format
This endpoint returns errors in Google API format, different from the OpenAI and Anthropic error formats used by other endpoints.
{
"error": {
"code": 400,
"message": "body must be valid JSON",
"status": "INVALID_ARGUMENT"
}
}
See Errors for the full list of HTTP status codes and retry guidance.
OpenAI Images Interface
Generate images using OpenAI-compatible request and response shapes. This interface is different from Gemini
native :generateContent and should be called via /v1/images/generations.
For the current list of available models, call GET /v1/models. See Models.
Request Body
| Field | Type | Description | |
|---|---|---|---|
| model | string | Required | One of openai/gpt-image-2, openai/gpt-image-2.5-flare, or
openai/gpt-image-2.5-sunburst. |
| prompt | string | Required | Text prompt describing the image to generate. |
| size | string | Optional | Output resolution, such as 1024x1024. |
| quality | string | Optional | Quality tier, such as medium. |
| output_compression | number | Optional | Output compression level (for supported formats). |
| output_format | string | Optional | Output image format, such as png or jpeg. |
| n | number | Optional | Number of images to generate. Commonly 1. |
| stream | boolean | Optional | When true, stream the image back as Server-Sent Events (SSE) — the model emits
progressively sharper preview images followed by the final image. Defaults to false (single
JSON response). |
| partial_images | number | Optional | Number of preview images to emit during streaming, 0–3. Requires
stream: true — sending partial_images > 0 without streaming
returns 400. 0 sends just the final image in a single event. |
Examples
This example uses openai/gpt-image-2 and returns base64 image data at data[0].b64_json.
curl -v https://api.routerhub.ai/v1/images/generations \
-H "Authorization: Bearer $ROUTERHUB_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "openai/gpt-image-2",
"prompt" : "A cute baby otter wearing a tiny top hat, watercolor style",
"size" : "1024x1024",
"quality" : "medium",
"output_compression" : 100,
"output_format" : "png",
"n" : 1
}' | jq -r '.data[0].b64_json' | base64 --decode > generated_image_gpt_image_2.png
import base64
import requests
response = requests.post(
"https://api.routerhub.ai/v1/images/generations",
headers={
"Authorization": "Bearer YOUR_API_KEY",
"Content-Type": "application/json",
},
json={
"model": "openai/gpt-image-2",
"prompt": "A cute baby otter wearing a tiny top hat, watercolor style",
"size": "1024x1024",
"quality": "medium",
"output_compression": 100,
"output_format": "png",
"n": 1,
},
)
response.raise_for_status()
payload = response.json()
img_b64 = payload["data"][0]["b64_json"]
img_bytes = base64.b64decode(img_b64)
with open("generated_image_gpt_image_2.png", "wb") as f:
f.write(img_bytes)
print("Saved generated_image_gpt_image_2.png")
Sample Response
{
"created": 1735689600,
"data": [
{
"b64_json": "iVBORw0KGgoAAAANSUhEUgAA..."
}
]
}
Streaming
Set stream: true to receive the image as Server-Sent Events. With partial_images set to
1–3, the model emits that many progressively sharper preview images before the
final one — useful for showing a live preview in a UI. The same streaming works on
/v1/images/edits; only the event names differ (image_edit.* instead of
image_generation.*).
Each SSE event carries base64 image data in b64_json. Preview events
(image_generation.partial_image) also include a 0-based partial_image_index; the
terminal event (image_generation.completed) additionally includes token usage.
# Stream and save each preview + the final image to separate files.
# -N disables curl buffering so events arrive live.
curl -sN https://api.routerhub.ai/v1/images/generations \
-H "Authorization: Bearer $ROUTERHUB_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "openai/gpt-image-2",
"prompt": "A cute baby otter wearing a tiny top hat, watercolor style",
"size": "1024x1024",
"stream": true,
"partial_images": 2
}' \
| grep '^data: ' | sed 's/^data: //' \
| while IFS= read -r line; do
type=$(printf '%s' "$line" | jq -r '.type')
b64=$(printf '%s' "$line" | jq -r '.b64_json')
case "$type" in
image_generation.partial_image)
idx=$(printf '%s' "$line" | jq -r '.partial_image_index')
printf '%s' "$b64" | base64 --decode > "otter_partial_${idx}.png" ;;
image_generation.completed)
printf '%s' "$b64" | base64 --decode > "otter_final.png" ;;
error)
msg=$(printf '%s' "$line" | jq -r '.error.message // "unknown error"')
printf 'Stream error: %s\n' "$msg" >&2
break ;;
esac
done
import base64
import json
import requests
with requests.post(
"https://api.routerhub.ai/v1/images/generations",
headers={
"Authorization": "Bearer YOUR_API_KEY",
"Content-Type": "application/json",
},
json={
"model": "openai/gpt-image-2",
"prompt": "A cute baby otter wearing a tiny top hat, watercolor style",
"size": "1024x1024",
"stream": True,
"partial_images": 2,
},
stream=True,
) as response:
response.raise_for_status()
for raw in response.iter_lines():
if not raw or not raw.startswith(b"data: "):
continue
event = json.loads(raw[len(b"data: "):])
etype = event.get("type")
if etype == "error":
print("Stream error:", event.get("error", {}).get("message", "unknown error"))
break
if "b64_json" not in event:
continue
img_bytes = base64.b64decode(event["b64_json"])
if etype == "image_generation.partial_image":
name = f"otter_partial_{event['partial_image_index']}.png"
elif etype == "image_generation.completed":
name = "otter_final.png"
else:
continue
with open(name, "wb") as f:
f.write(img_bytes)
print("Saved", name)
Sample Stream Events
event: image_generation.partial_image
data: {"type":"image_generation.partial_image","partial_image_index":0,"b64_json":"iVBORw0KGgo..."}
event: image_generation.partial_image
data: {"type":"image_generation.partial_image","partial_image_index":1,"b64_json":"iVBORw0KGgo..."}
event: image_generation.completed
data: {"type":"image_generation.completed","b64_json":"iVBORw0KGgo...","usage":{"input_tokens":12,"output_tokens":1290,"total_tokens":1302}}
Mid-stream failures. If the upstream fails after the first image event was already
sent (the HTTP status is locked at 200), the gateway emits a final error event instead
of silently closing the connection. Clients should treat this event as a terminal failure signal in addition to
image_generation.completed / image_edit.completed. If the request fails
before any event is sent, a normal HTTP error status (e.g. 400/502) is
returned instead.
event: error
data: {"type":"error","error":{"message":"upstream provider error"}}
OpenAI Image Edits Interface
Edit, modify, or inpaint existing images by uploading one or more reference image files with a text prompt
describing the desired changes. Requests must use multipart/form-data encoding.
Supports uploading up to 16 images per request (using image or image[] form fields).
Each uploaded image must be ≤ 50 MB (returns 400 if exceeded; supported formats: PNG, JPEG, WEBP), and the total request payload across all files and fields is capped at 100 MB (returns 413 if exceeded).
For the current list of available models, call GET /v1/models. See Models.
Multipart Form Fields
| Field | Type | Description | |
|---|---|---|---|
| model | string | Required | Image model ID, such as openai/gpt-image-2, openai/gpt-image-2.5-flare, or
openai/gpt-image-2.5-sunburst. |
| prompt | string | Required | Text prompt describing the desired edit or transformation. |
| image / image[] | file | Required | One or more reference image files to edit (up to 16 files). |
| mask | file | Optional | An optional mask image file where fully transparent areas indicate where the image should be edited. |
| size | string | Optional | Output resolution, such as 1024x1024 or 2880x2880. |
| quality | string | Optional | Quality tier, such as medium or high. |
| background | string | Optional | Background preference (e.g. transparent). |
| input_fidelity | string | Optional | Input fidelity control for supported models (e.g. high, low). |
| output_format | string | Optional | Output image format, such as png or jpeg. |
| output_compression | number | Optional | Output compression level (0–100). |
| n | number | Optional | Number of images to generate (1–10, default: 1). |
| stream | boolean | Optional | When true, stream edited images back as Server-Sent Events (SSE). Defaults to
false. |
| partial_images | number | Optional | Number of intermediate preview images to emit during streaming (0–3).
Requires stream: true. |
Examples
This example edits an image using openai/gpt-image-2.5-sunburst via multipart form upload and saves
the decoded base64 output.
curl -X POST https://api.routerhub.ai/v1/images/edits \
-H "Authorization: Bearer $ROUTERHUB_API_KEY" \
-F "model=openai/gpt-image-2.5-sunburst" \
-F "prompt=Add a tiny red party hat on top of the otter's head" \
-F "size=1024x1024" \
-F "quality=medium" \
-F "output_format=png" \
-F "n=1" \
-F "image=@otter.png;type=image/png" \
| jq -r '.data[0].b64_json' | base64 --decode > edited_otter.png
import base64
import requests
url = "https://api.routerhub.ai/v1/images/edits"
headers = {
"Authorization": "Bearer YOUR_API_KEY",
}
data = {
"model": "openai/gpt-image-2.5-sunburst",
"prompt": "Add a tiny red party hat on top of the otter's head",
"size": "1024x1024",
"quality": "medium",
"output_format": "png",
"n": 1,
}
# For multi-image edits, append additional ("image", ("name.png", f2, "image/png")) tuples
with open("otter.png", "rb") as f:
files = [
("image", ("otter.png", f, "image/png")),
]
response = requests.post(url, headers=headers, data=data, files=files)
response.raise_for_status()
payload = response.json()
img_b64 = payload["data"][0]["b64_json"]
img_bytes = base64.b64decode(img_b64)
with open("edited_otter.png", "wb") as f:
f.write(img_bytes)
print("Saved edited_otter.png")
Sample Response
{
"created": 1735689600,
"data": [
{
"b64_json": "iVBORw0KGgoAAAANSUhEUgAA..."
}
]
}
Streaming
Set stream=true in the multipart form to receive edited images as Server-Sent Events (SSE). The
event structure mirrors /v1/images/generations streaming with image_edit.partial_image
and image_edit.completed event types.
# Stream image editing and save preview + final images
curl -sN -X POST https://api.routerhub.ai/v1/images/edits \
-H "Authorization: Bearer $ROUTERHUB_API_KEY" \
-F "model=openai/gpt-image-2.5-sunburst" \
-F "prompt=Add a tiny red party hat on top of the otter's head" \
-F "size=1024x1024" \
-F "stream=true" \
-F "partial_images=2" \
-F "image=@otter.png;type=image/png" \
| grep '^data: ' | sed 's/^data: //' \
| while IFS= read -r line; do
type=$(printf '%s' "$line" | jq -r '.type')
b64=$(printf '%s' "$line" | jq -r '.b64_json')
case "$type" in
image_edit.partial_image)
idx=$(printf '%s' "$line" | jq -r '.partial_image_index')
printf '%s' "$b64" | base64 --decode > "edited_partial_${idx}.png" ;;
image_edit.completed)
printf '%s' "$b64" | base64 --decode > "edited_final.png" ;;
error)
msg=$(printf '%s' "$line" | jq -r '.error.message // "unknown error"')
printf 'Stream error: %s\n' "$msg" >&2
break ;;
esac
done
import base64
import json
import requests
url = "https://api.routerhub.ai/v1/images/edits"
headers = {
"Authorization": "Bearer YOUR_API_KEY",
}
data = {
"model": "openai/gpt-image-2.5-sunburst",
"prompt": "Add a tiny red party hat on top of the otter's head",
"size": "1024x1024",
"stream": "true",
"partial_images": 2,
}
with open("otter.png", "rb") as f:
files = [
("image", ("otter.png", f, "image/png")),
]
with requests.post(url, headers=headers, data=data, files=files, stream=True) as response:
response.raise_for_status()
for raw in response.iter_lines():
if not raw or not raw.startswith(b"data: "):
continue
event = json.loads(raw[len(b"data: "):])
etype = event.get("type")
if etype == "error":
print("Stream error:", event.get("error", {}).get("message", "unknown error"))
break
if "b64_json" not in event:
continue
img_bytes = base64.b64decode(event["b64_json"])
if etype == "image_edit.partial_image":
name = f"edited_partial_{event['partial_image_index']}.png"
elif etype == "image_edit.completed":
name = "edited_final.png"
else:
continue
with open(name, "wb") as f:
f.write(img_bytes)
print("Saved", name)
Sample Stream Events
event: image_edit.partial_image
data: {"type":"image_edit.partial_image","partial_image_index":0,"b64_json":"iVBORw0KGgo..."}
event: image_edit.partial_image
data: {"type":"image_edit.partial_image","partial_image_index":1,"b64_json":"iVBORw0KGgo..."}
event: image_edit.completed
data: {"type":"image_edit.completed","b64_json":"iVBORw0KGgo...","usage":{"input_tokens":12,"output_tokens":1290,"total_tokens":1302}}
OpenAI Images Error Format
Both POST /v1/images/generations and POST /v1/images/edits return standard OpenAI-style errors:
{
"error": {
"message": "Invalid value for 'model'",
"type": "invalid_request_error",
"param": "model",
"code": "invalid_model"
}
}