Image generation OpenAI compatible API integration Media persistence Billing safety

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.

AveMujica API 8 min read

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 n or 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:

FieldRole on GPT Image / gateway
modelPrefer stable alias gpt-image-2 (or your mapped alias); older gpt-image-1* IDs are on OpenAI’s deprecation path toward gpt-image-2
promptrequired text instruction
noptional count; pointer type preserves JSON presence, but this gateway normalizes n=0 (or omitted) to 1 before billing
sizeresolution string such as 1024x1024, 1536x1024, 1024x1536; GPT Image 2 also documents broader WIDTHxHEIGHT rules
qualityprovider-specific quality token when supported
output_formatpng / jpeg / webp for GPT Image families
output_compressioncompression for formats that support it
response_formatlegacy DALL·E only (url or b64_json); unsupported for GPT Image, which always returns base64
background, moderation, streammodel-family extensions when the adapter supports them
userend-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:

  1. accept user uploads into a size-capped temporary store
  2. normalize MIME types before calling the gateway
  3. keep the same auth and billing owner as generations
  4. 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 n above MaxImageN (128) before quota math
  • n missing or 0 is normalized to 1 (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 n to 1
  • never accept user-supplied n without 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

  1. Submit with a scoped gateway key, not a provider key.
  2. Validate n (remember 0 becomes 1 here), size, and prompt length before send.
  3. Record request id / usage log id for support.
  4. Decode b64_json (or fetch a legacy URL once) and persist object-store keys under your retention policy.
  5. Authorize later downloads by owner (user for image workbench assets; billing owner for API jobs you store yourself).
  6. Expire or re-moderate assets when product policy requires it.
  7. 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 prompt string, 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 model alias after verifying size/quality support—do not fork the entire client.
  • Prefer gpt-image-2 as the default example; map older aliases explicitly if a channel still exposes them.

Common integration failures

SymptomLikely causeFix
Empty images after “success”Looked for url on GPT ImageRead b64_json and decode
Sudden quota burnUnbounded n or retry stormsClamp n, dedupe retries
Expected n=0 to mean “dry run”Gateway normalizes 0 → 1Omit the call instead of sending zero
Works in personal account, fails with team keyKey lacks project grant / wrong model groupFix team token grants; do not assume image workbench is team-scoped
Edit endpoint 400Wrong multipart field names or mask sizeAlign with provider/gateway form contract
Model not foundAlias not mapped on channelMap stable alias → upstream id in channel config

Related reading

Sources

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.