DGX LLM Chat Gateway

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):

AliasUnderlying modelCold latencyCost (rough)
google-nano-banana-latestgoogle/gemini-3.1-flash-image-preview12-15 s~$3/M tokens
openai-image-latestopenai/gpt-5.4-image-2100-180 s~$15/M tokens
gpt-5-image-mini (Cut 2.43)openai/gpt-5-image-mini60-90 s~$1/M tokens
gpt-5-image (Cut 2.43)openai/gpt-5-image80-120 s~$5/M tokens
gemini-2.5-flash-image (Cut 2.43)google/gemini-2.5-flash-image8-12 s~$1.5/M tokens
gemini-3-pro-image-preview (Cut 2.43)google/gemini-3-pro-image-preview60-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-standardnano-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 durch storage_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.url ist wieder eine self-contained data:<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 als storage_ref-Feld exposed.

Cut 2.41 (CR-0009, 2026-05-16): message.content enthä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 in images[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:

FeldTypZweck
image_url.urlstringSelf-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_refstring (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_urlstringWebP-q80-Thumbnail (256 px, ~10 KB) als data:image/webp;base64,…. Direkt inline-renderbar ohne zweiten HTTP-Call.
expires_atstring (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).
idstringUUID des Image-Store-Eintrags (= UUID-Teil im storage_ref).
mime_typestringimage/png | image/jpeg | image/webp.
modelstringWelcher 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

Cut 2.43: bfl-flux-latest ist nicht mehr verfügbar (BFL Flux bei OpenRouter retired). Ersatz für photo-realistic + artistic: gemini-3-pro-image-preview oder openai-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