Image Generation API Integration: OpenAI-Compatible Patterns
Integrate image generation through OpenAI-compatible endpoints, handle edits, bound n/size safely, and persist temporary media correctly with gpt-image-2.
Image generation API integration is the practice of calling a stable image endpoint—usually POST /v1/images/generations and often POST /v1/images/edits—from product code while keeping model selection, credentials, quotas, and media storage outside every client. In an OpenAI-compatible gateway, applications send a prompt plus optional model, n, size, quality, and GPT Image fields such as output_format, then decode base64 payloads (or handle legacy URL responses only where a model still supports them). This guide shows a production-shaped integration path using gateway routes this repository already exposes, without inventing provider prices or quality scores.
Minimal correct shape for GPT Image
Use one authenticated base URL, one Bearer token issued by your gateway, and the current OpenAI GPT Image contract. Official docs present gpt-image-2 as the current main image model; GPT Image generations return data[].b64_json. The response_format parameter applies to legacy DALL·E models, not GPT Image.
curl -sS https://YOUR_GATEWAY/v1/images/generations \
-H "Authorization: Bearer $GATEWAY_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-image-2",
"prompt": "A product photo of a ceramic mug on a wooden table, soft window light",
"n": 1,
"size": "1024x1024",
"output_format": "png"
}' | jq -r '.data[0].b64_json' | base64 --decode > mug.png
Treat the response as ephemeral media plus billing metadata, not as durable product assets. If your product needs images after a temporary provider URL expires (legacy models or non-OpenAI adapters), download immediately into object storage you control, or use the personal image workbench persistence path the gateway authorizes by user ownership.
Always re-check model-specific fields on the current OpenAI image generation guide and Images API reference. Google documents native Gemini image generation separately on ai.google.dev. A gateway does not replace those docs—it normalizes access.
Why integrate through a gateway instead of many SDKs
Product teams usually fail image integration for operational reasons, not model quality:
- each service holds a different provider key
- prompt experiments bypass budgets
- temporary CDN URLs get stored in databases and break later
- image
nor resolution multiplies cost without validation - personal sandboxes and team projects share one key, so cost attribution collapses
A multi-provider gateway pattern—covered more broadly in multi-provider LLM gateway and OpenAI-compatible API gateway—keeps clients stable while operators map stable aliases to channels, enforce quotas, and write usage logs. For media, the same control plane must also answer “who may download this asset later?”
Endpoints and request fields that matter
Generations
POST /v1/images/generations is the text-to-image entry point. Fields that commonly appear in this repository’s image request DTO include:
| Field | Role on GPT Image / gateway |
|---|---|
model | Prefer stable alias gpt-image-2 (or your mapped alias); older gpt-image-1* IDs are on OpenAI’s deprecation path toward gpt-image-2 |
prompt | required text instruction |
n | optional count; pointer type preserves JSON presence, but this gateway normalizes n=0 (or omitted) to 1 before billing |
size | resolution string such as 1024x1024, 1536x1024, 1024x1536; GPT Image 2 also documents broader WIDTHxHEIGHT rules |
quality | provider-specific quality token when supported |
output_format | png / jpeg / webp for GPT Image families |
output_compression | compression for formats that support it |
response_format | legacy DALL·E only (url or b64_json); unsupported for GPT Image, which always returns base64 |
background, moderation, stream | model-family extensions when the adapter supports them |
user | end-user tag for abuse tracing |
Do not assume every field works on every model. Validate against the models actually enabled in your environment and the provider’s current docs.
Edits
POST /v1/images/edits is the edit/inpaint surface. Integrations usually send multipart form fields for the source image, optional mask, and prompt. Product code should:
- accept user uploads into a size-capped temporary store
- normalize MIME types before calling the gateway
- keep the same auth and billing owner as generations
- never log raw image bytes in application logs
If your product only needs “regenerate with a tighter prompt,” generations alone may be enough. Add edits when users must preserve composition or brand assets.
Billing safety is part of integration
Image generation is a multiplier-heavy API. A hostile or buggy client that sets n to an absurd value can turn one HTTP request into an accounting disaster. In this codebase:
- request validation rejects
naboveMaxImageN(128) before quota math nmissing or0is normalized to1(so “preserve pointer presence” is not the same as “honor zero images”)- pre-consume and settle paths must still refuse negative or saturated charges
- quality/size ratios only become multipliers after validation
Product rules:
- default UI
nto1 - never accept user-supplied
nwithout an upper clamp below platform max - show estimated cost before multi-variant generation
- if pre-consume fails, fail the UX; do not “queue optimistically” against another wallet
TypeScript: generate, decode, persist
type ImageGenResponse = {
data: Array<{ b64_json?: string; url?: string; revised_prompt?: string }>;
};
async function generatePng(prompt: string, model = "gpt-image-2") {
const res = await fetch(`${process.env.GATEWAY_BASE}/v1/images/generations`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.GATEWAY_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model,
prompt,
n: 1,
size: "1024x1024",
output_format: "png",
}),
});
if (!res.ok) throw new Error(await res.text());
const body = (await res.json()) as ImageGenResponse;
const b64 = body.data?.[0]?.b64_json;
if (!b64) {
// Legacy/provider-adapter path only: some non-GPT models may still return url.
if (body.data?.[0]?.url) {
return { kind: "url" as const, url: body.data[0].url, revised: body.data[0].revised_prompt };
}
throw new Error("no image payload");
}
const bytes = Buffer.from(b64, "base64");
// upload bytes to object storage under the billing owner's key prefix
return { kind: "bytes" as const, bytes, revised: body.data[0].revised_prompt };
}
Python: same contract with explicit decode
import base64
import os
from pathlib import Path
import requests
def generate_and_store(prompt: str, model: str = "gpt-image-2") -> Path:
r = requests.post(
f"{os.environ['GATEWAY_BASE']}/v1/images/generations",
headers={"Authorization": f"Bearer {os.environ['GATEWAY_KEY']}"},
json={
"model": model,
"prompt": prompt,
"n": 1,
"size": "1024x1024",
"output_format": "webp",
"output_compression": 80,
},
timeout=120,
)
r.raise_for_status()
b64 = r.json()["data"][0]["b64_json"]
out = Path("/tmp/out.webp")
out.write_bytes(base64.b64decode(b64))
return out
Prefer b64_json (the GPT Image default) when you control storage and want one round trip. Mention url only as legacy-model or provider-adapter behavior. Never assume a URL remains valid indefinitely.
Personal image workbench vs team video workbench
This repository exposes an image workbench that is personal / session / token scoped:
- UI: image workbench
- User APIs under
/api/user/image-workbench/*(presets, templates, runs, assets, generate, edit) - API-key variants under
/api/image-workbench/*for generate/edit/download - Admin template tooling under
/api/image-workbench/admin/* - Object keys and history use user ownership
There are no team-scoped /api/teams/:team_id/image-workbench routes in the current tree. Do not claim team image history or project-scoped image assets unless your fork adds them.
By contrast, video workbench does include team/project routes under /api/teams/:team_id/video-workbench. If product managers ask for “shared image history like video,” that is a feature gap—not something the current image workbench already provides. Multi-tenant billing for raw /v1/images/* still follows the token’s billing owner; see multi-tenant LLM gateway architecture.
Use the workbench when operators or designers need interactive history and presets. Use raw /v1/images/* when backend services generate assets programmatically. Both paths should share the same model aliases and usage-log fields for later attribution.
Media lifecycle checklist
- Submit with a scoped gateway key, not a provider key.
- Validate
n(remember0becomes1here), size, and prompt length before send. - Record request id / usage log id for support.
- Decode
b64_json(or fetch a legacy URL once) and persist object-store keys under your retention policy. - Authorize later downloads by owner (user for image workbench assets; billing owner for API jobs you store yourself).
- Expire or re-moderate assets when product policy requires it.
- Observe failure rates and cost by model using your monitoring stack.
If step 4 is skipped, product bugs appear weeks later as broken thumbnails—not as API errors.
Prompt and product engineering notes
- Separate system intent (brand rules) from user prompt in your app layer; the images API is usually a single
promptstring, so compose carefully. - Store the final prompt you sent, not only the UI draft, because providers may return
revised_prompt. - Cap concurrent image jobs per user; image endpoints are easy to fan out accidentally from retries.
- Map HTTP 429/5xx with bounded retries and idempotent client keys where your stack supports them (LLM API retries and idempotency).
- For multi-model experiments, change only the
modelalias after verifying size/quality support—do not fork the entire client. - Prefer
gpt-image-2as the default example; map older aliases explicitly if a channel still exposes them.
Common integration failures
| Symptom | Likely cause | Fix |
|---|---|---|
| Empty images after “success” | Looked for url on GPT Image | Read b64_json and decode |
| Sudden quota burn | Unbounded n or retry storms | Clamp n, dedupe retries |
Expected n=0 to mean “dry run” | Gateway normalizes 0 → 1 | Omit the call instead of sending zero |
| Works in personal account, fails with team key | Key lacks project grant / wrong model group | Fix team token grants; do not assume image workbench is team-scoped |
| Edit endpoint 400 | Wrong multipart field names or mask size | Align with provider/gateway form contract |
| Model not found | Alias not mapped on channel | Map stable alias → upstream id in channel config |
Related reading
Sources
- OpenAI image generation guide
- OpenAI Images API reference
- OpenAI deprecations (older GPT Image →
gpt-image-2) - Google Gemini image generation
- OWASP Top 10 for LLM Applications
Failure Q&A
Should I call OpenAI, Gemini, and others with three SDKs?
Not if your product only needs one client contract. Prefer an OpenAI-compatible gateway path for generations/edits, and keep native SDKs for features the gateway does not map yet.
Is response_format=url safe for production databases?
It is the wrong default for GPT Image (unsupported / always base64). For legacy models that still return URLs, treat the URL as a temporary pointer only and re-host before long-lived product pages.
What is a safe default for n?
Start at 1 in production UX. Raise only where the product explicitly needs variants, and keep a hard product max below platform max (128 in this codebase). Do not send n=0 expecting a no-op.
Do image requests need streaming?
Usually no. Enable streaming only if your adapter and UI benefit from partial previews; otherwise keep the simple request/response path.
How do teams share image history without leaking assets?
With the current gateway, image workbench history is personal. For shared libraries, build an application-layer share on top of bytes you store, or use team project keys only for billing of /v1/images/* while implementing your own ACL on persisted objects.
First production wire-up
Wire one backend job to POST /v1/images/generations with model=gpt-image-2, n=1, output_format=png, base64 decode, and immediate object-storage upload. Confirm the usage log shows the expected billing owner, then add a second model alias only after size/quality compatibility is verified in your channel config.