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

POST /v1/chat/completions

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

POST /v1beta/models/{model}:generateContent

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

POST /v1/images/generations

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

POST /v1/images/edits

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"
  }
}