Image generation
Image-Gen-Modelle laufen über /v1/chat/completions (gleich wie Text-Chat) und über /c1/chat (mit Conversation-Persistenz). Die Response folgt dem OpenRouter-Industriestandard: ein images[]-Array auf choices[0].message, mit Server-URL für die volle Auflösung und einem WebP-Thumbnail als data-URL für sofortige Inline-Anzeige.
Modell-Aliase
Stack-level (alle Tenants):
| Alias | Underlying model | Cold latency | Cost (rough) |
|---|---|---|---|
google-nano-banana-latest | google/gemini-3.1-flash-image-preview | 12-15 s | ~$3/M tokens |
openai-image-latest | openai/gpt-5.4-image-2 | 100-180 s | ~$15/M tokens |
gpt-5-image-mini (Cut 2.43) | openai/gpt-5-image-mini | 60-90 s | ~$1/M tokens |
gpt-5-image (Cut 2.43) | openai/gpt-5-image | 80-120 s | ~$5/M tokens |
gemini-2.5-flash-image (Cut 2.43) | google/gemini-2.5-flash-image | 8-12 s | ~$1.5/M tokens |
gemini-3-pro-image-preview (Cut 2.43) | google/gemini-3-pro-image-preview | 60-100 s | ~$10/M tokens |
bfl-flux-latest wurde mit Cut 2.43 (CR-0011) entfernt — BFL Flux ist bei OpenRouter retired.
Tenants haben zusätzlich virtual aliases (z.B. godelmann-gocreate-premium-gemini-image-standard → nano-banana). Siehe tenant-config.md.
Cut 2.42 (2026-05-16, CR-0010 / GoCreate-CR-011) — Multi-image-Conversations sind jetzt ohne Context-Window-Blast funktional. Vorher: bei Folge-Calls in einer Conversation mit prior image-gen-turn wurde die volle base64 data-URL der historischen Bilder mit in den prompt-context gepackt (~600k tokens pro Bild) → Llama-FP8/gpt-image blockten mit
ContextWindowExceededError. Cut 2.42 ersetzt das in der history-replay durchstorage_ref(=/v1/images/<uuid>-marker, ~50 bytes). Read-side fix, keine Caller-Änderung nötig — existing "dead" conversations leben sofort wieder.
Response-Schema (Cut 2.41, ab 2026-05-16) — Industrie-Standard data-URL + clean content
Cut 2.34 (CR-0004) revertiert Cut 2.24:
image_url.urlist wieder eine self-containeddata:<mime>;base64,<b64>-URL, aligned mit OpenAI gpt-image-1 + Anthropic Claude + Google Gemini Imagen. Caller können direkt<img src={image_url.url}>rendern, kein 2. fetch nötig./v1/images/{uuid}bleibt als convenience-Endpoint für lazy-reload aus persisted conversations und ist additive alsstorage_ref-Feld exposed.Cut 2.41 (CR-0009, 2026-05-16):
message.contententhält NICHT mehr den deterministischen TTL-Hint-Suffix ("⏰ Dieses Bild ist X Minuten via lazy-reload ..."). content bleibt vom Provider unverändert (null, leerer string, oder provider-prose wenn vorhanden). Strukturierte TTL-Info weiter inimages[0].expires_at.
{
"choices": [{
"message": {
"role": "assistant",
"content": null,
"images": [{
"type": "image_url",
"image_url": {"url": "data:image/png;base64,iVBORw0KGgoAAAA..."},
"storage_ref": "/v1/images/{uuid}",
"thumbnail_data_url": "data:image/webp;base64,…",
"expires_at": "2026-05-05T16:30:00Z",
"id": "{uuid}",
"mime_type": "image/png",
"model": "google-nano-banana-latest"
}]
},
"finish_reason": "stop"
}]
}
Felder pro image:
| Feld | Typ | Zweck |
|---|---|---|
image_url.url | string | Self-contained data-URL (data:<mime>;base64,<b64> der Original-Bytes). Direkt im <img src> renderbar. Cut 2.24 hatte das auf relative /v1/images/{uuid}-references umgestellt; Cut 2.34 revertiert das auf Industrie-Standard. |
storage_ref | string (NEU Cut 2.34) | /v1/images/{uuid} für lazy-reload aus persisted conversations. Additive — Caller die data-URL direkt rendern brauchen das nicht. |
thumbnail_data_url | string | WebP-q80-Thumbnail (256 px, ~10 KB) als data:image/webp;base64,…. Direkt inline-renderbar ohne zweiten HTTP-Call. |
expires_at | string (RFC 3339) | TTL-Ablauf des storage_ref. Nach diesem Zeitpunkt liefert GET /v1/images/{uuid} HTTP 404. Data-URL im image_url.url ist davon unabhängig (persistiert in conversation-DB). Per-tenant via image_default_ttl_hours (default 12) und image_max_ttl_hours (default 168 = 7 Tage). |
id | string | UUID des Image-Store-Eintrags (= UUID-Teil im storage_ref). |
mime_type | string | image/png | image/jpeg | image/webp. |
model | string | Welcher Alias das Bild erzeugt hat (für Cost-Reports). |
content ist seit Cut 2.41 wieder typisch null — der dgx-server hängt keinen TTL-Hinweis mehr an (Cut 2.34's "⏰ Dieses Bild ist X Minuten ..." wurde mit Cut 2.41 (CR-0009) entfernt). content kommt vom upstream-Provider unverändert: meist null (pure image-only) oder kurzer Begleittext den der Provider selbst liefert. Strukturierte TTL-Info weiterhin in images[0].expires_at (RFC 3339). Multi-modal-Decoder müssen weiterhin vier Modi unterstützen (text-only, tool_calls-only, image+text, full-message-fallback) damit auch null-content-Pfade nicht crashen (siehe examples-tools.md).
Generate one image
curl -s --max-time 240 https://dgx.spass.fun/v1/chat/completions \
-H "Authorization: Bearer $BEARER" \
-H "SPASS-User-Id: $USER_ID" \
-H "Content-Type: application/json" \
-d '{
"model": "google-nano-banana-latest",
"messages": [{
"role": "user",
"content": "Generate a photo of a small green frog wearing a tiny crown, photorealistic"
}],
"stream": false
}' \
| jq -r '.choices[0].message.images[0]'
Das liefert ein Objekt wie oben. Drei typische Caller-Patterns:
Pattern A — Inline-Display sofort, volle Auflösung lazy
const img = response.choices[0].message.images[0];
// Sofort: thumbnail anzeigen
imgElement.src = img.thumbnail_data_url;
// Optional bei Klick: volle Auflösung nachladen
imgElement.addEventListener('click', () => {
imgElement.src = img.image_url.url; // /v1/images/{uuid}
});
Pattern B — Direkt vollformat herunterladen
URL=$(curl … | jq -r '.choices[0].message.images[0].image_url.url')
curl -s -H "Authorization: Bearer $BEARER" "https://dgx.spass.fun$URL" -o frog.png
/v1/images/{uuid} ist NICHT public — Bearer-Auth ist Pflicht, der Tenant des Bearers muss derselbe sein, der das Bild erzeugt hat.
Pattern C — Caller braucht data-URL aus Legacy-Kompatibilität
Wenn ihr aus einem älteren Stack auf data-URL angewiesen seid: das thumbnail_data_url IST eine data-URL (WebP-encoded, full-quality 256 px). Für die volle Auflösung als data-URL: per JS fetch(url) → blob → FileReader.readAsDataURL:
const fullRes = await fetch(img.image_url.url, {
headers: {Authorization: `Bearer ${BEARER}`}
});
const blob = await fullRes.blob();
const dataUrl = await new Promise(r => {
const fr = new FileReader();
fr.onloadend = () => r(fr.result);
fr.readAsDataURL(blob);
});
Compare aliases
PROMPT="A vintage scientific illustration of a hummingbird"
for ALIAS in google-nano-banana-latest openai-image-latest gemini-2.5-flash-image gpt-5-image; do
echo "=== $ALIAS ==="
T0=$(date +%s)
curl -s --max-time 240 https://dgx.spass.fun/v1/chat/completions \
-H "Authorization: Bearer $BEARER" \
-H "SPASS-User-Id: $USER_ID" \
-H "Content-Type: application/json" \
-d "{\"model\": \"$ALIAS\", \"messages\": [{\"role\": \"user\", \"content\": \"$PROMPT\"}], \"stream\": false}" \
| jq -r '.choices[0].message.images[0].image_url.url'
echo " time: $(($(date +%s) - T0)) s"
done
When to pick which
google-nano-banana-latest— default für schnelle Iteration, photorealism, simple composition. Bestes Preis-/Qualitäts-Verhältnis.openai-image-latest— wenn Text korrekt im Bild gerendert werden muss (Schilder, Labels, UI-Mockups), komplexe Multi-Element-Kompositionen, oder detaillierte Instruktion-Verfolgung.gemini-3-pro-image-preview(Cut 2.43) — alternative Top-Quality-Engine (Google Gemini 3 Pro Image Preview), höchste Qualität bei komplexer Komposition. Langsamer.gpt-5-image-mini/gemini-2.5-flash-image(Cut 2.43) — günstige cheap-tier Varianten für einfache Generierungen.
Cut 2.43:
bfl-flux-latestist nicht mehr verfügbar (BFL Flux bei OpenRouter retired). Ersatz für photo-realistic + artistic:gemini-3-pro-image-previewoderopenai-image-latest.
Image input + image output
Image-Aliase akzeptieren auch image input — für Variation / Edit workflows. Inputs sind data-URLs, Outputs sind das oben dokumentierte Server-URL-Schema:
B64=$(base64 -w 0 source.jpg)
curl -s --max-time 240 https://dgx.spass.fun/v1/chat/completions \
-H "Authorization: Bearer $BEARER" \
-H "SPASS-User-Id: $USER_ID" \
-H "Content-Type: application/json" \
-d "{
\"model\": \"google-nano-banana-latest\",
\"messages\": [{
\"role\": \"user\",
\"content\": [
{\"type\": \"text\", \"text\": \"Make this look like an oil painting\"},
{\"type\": \"image_url\", \"image_url\": {\"url\": \"data:image/jpeg;base64,$B64\"}}
]
}],
\"stream\": false
}"
Persistenz im /c1/chat-Pfad
Image-Gen-Calls über /c1/chat (mit conversation_id) werden persistiert. Beim nächsten Turn liefert der Server eine kompakte Text-Marker-Form an das LLM ("[image generated: /v1/images/{uuid}]"), damit nicht 10 KB Thumbnails pro Folge-Turn als Token-Last anfallen. Der Caller liest die volle multimodal-message via GET /c1/conversations/{conversation_id} und hat dort den vollen images[]-Array.
Rate-Limiting (per Tenant)
image_gen_rate_per_hour (default 20, configurable via tenant-config). Sliding-Hour-Window pro Tenant. Über-Limit-Calls bekommen HTTP 429 rate_limit_exceeded.
Quota-Errors
Wenn der Cloud-Provider out-of-credits ist: HTTP 429 model_quota_exhausted (Cut 2.23d). Nutze einen anderen Image-Alias oder warte aufs Reset. Body ist provider-agnostisch (kein OpenRouter-Name, keine Token-Counts) per ADR 0006 v2.
Limitations
- No tool calling —
constraints.toolsistfalsefür alle Image-Aliase. - No streaming — image content wird atomar geliefert, nicht als SSE-Chunks.
- Body size — base64 encoding inflates by ~33 %. Image-Inputs sollten vor dem encode aggressiv resized werden (
MAX_BODY_BYTESdefault 32 MB). - TTL-Erlöschung —
/v1/images/{uuid}liefert nachexpires_atHTTP 404. Caller die Bilder dauerhaft brauchen müssen sie selbst archivieren (Bytes viaPattern Bherunterladen, eigenes Storage).