Rabatt auf Premium-Pläne, wenn du dich registrierstRabatt sichern

Blog/developers·12. Aug. 2026·4 Min.·vom eroq-Team

KI-Antworten per SSE streamen – ein Praxisleitfaden

Server-Sent Events komplett: Chunk-Frames parsen, Streams über dein Backend weiterleiten – und Pufferfehler, die erst in Produktion auftauchen.


Streaming ist der Unterschied zwischen einem Charakter, der präsent ist, und einem Ladekreisel. Die Mechanik von SSE ist simpel; die Bugs stecken alle in den Details, um die es in diesem Leitfaden geht.

Das Wire-Format

Mit stream: true antwortet /v1/chat/completions mit text/event-stream. Jeder Frame besteht aus einer data:-Zeile und einer Leerzeile; die Deltas kommen im üblichen Chunk-Format:

data: {"id":"cmpl_…","object":"chat.completion.chunk","choices":[{"delta":{"content":"That "}}]}

data: {"id":"cmpl_…","object":"chat.completion.chunk","choices":[{"delta":{"content":"noise"}}]}

data: {"object":"chat.completion.usage","usage":{"credits_spent":3,"credits_remaining":997}}

data: [DONE]

Zwei eroq-spezifische Frames solltest du kennen: Das Usage-Event direkt vor [DONE] liefert den Verbrauch, und ein Error-Event ({"error":{…}}) ersetzt den Absturz, den du sonst aus einer abgebrochenen Verbindung erraten müsstest. Bricht der Stream mit einem Fehler ab, bevor Inhalt angekommen ist, hat sich der Aufruf bereits selbst erstattet.

Parsen ohne den klassischen Bug

Der klassische Bug: jeden Netzwerk-Chunk als vollständigen Frame zu behandeln. TCP hält sich nicht an deine Zeilenumbrüche – ein Frame kann auf mehrere Reads verteilt ankommen. Also puffern, an \n\n splitten und den Rest aufheben:

const reader = res.body.getReader()
const decoder = new TextDecoder()
let buffer = ''
while (true) {
  const { done, value } = await reader.read()
  if (done) break
  buffer += decoder.decode(value, { stream: true })   // stream: true matters for UTF-8
  const frames = buffer.split('\n\n')
  buffer = frames.pop() ?? ''                          // last piece may be incomplete
  for (const frame of frames) {
    const data = frame.replace(/^data: /, '')
    if (data === '[DONE]') continue
    const parsed = JSON.parse(data)
    if (parsed.error) throw new Error(parsed.error.message)
    const delta = parsed.choices?.[0]?.delta?.content
    if (delta) render(delta)
  }
}

Das { stream: true } bei decoder.decode ist keine Deko: Ohne es wird ein Multibyte-Zeichen, das auf zwei Chunks verteilt ankommt, zu Zeichensalat (Mojibake). Emoji-lastiges Rollenspiel findet diesen Bug innerhalb einer Stunde.

Über dein Backend weiterleiten

Gib deinen API-Schlüssel niemals an einen Browser – leite den Stream über das Backend weiter, das ohnehin deinen Schlüssel und deine Charakterbögen verwaltet. Das Relay ist schlank, aber zwei Details entscheiden darüber, ob es funktioniert:

// Node/Express-style relay
app.post('/chat', async (req, res) => {
  res.setHeader('Content-Type', 'text/event-stream')
  res.setHeader('Cache-Control', 'no-cache')
  res.flushHeaders()                                   // 1. headers out immediately

  const upstream = await fetch('https://eroq.ai/v1/chat/completions', { /* … */ })
  for await (const chunk of upstream.body) {
    res.write(chunk)                                   // 2. relay bytes, re-frame nothing
  }
  res.end()
})
  1. Header sofort flushen, sonst puffert dein Reverse Proxy womöglich die gesamte Antwort und liefert sie auf einmal aus – ein Stream, der als Block ankommt. Bei nginx setzt du zusätzlich X-Accel-Buffering: no.
  2. Bytes unverändert weiterreichen. Frames im Relay zu parsen und neu zu serialisieren verdoppelt die Fläche für Bugs, ohne irgendetwas zu bringen. Geparst wird im Client, dort, wo du renderst.

UX-Details, die gut von großartig trennen

  • Rendere über einen kurzen Timer (30–50 ms), nicht bei jedem Delta – DOM-Schreibvorgänge pro Token ruckeln auf dem Smartphone.
  • Zeig das erste Token schnell, dann lass es fließen. Die gefühlte Latenz steckt fast vollständig in der Zeit bis zum ersten Token.
  • Bricht der Stream mittendrin ab, behalte den Teiltext und biete einen Button für einen neuen Versuch an. Eine halbe Antwort, die stehen bleibt, ist besser als eine, die verschwindet – und da unterbrochene Streams nach der ersten Ausgabe trotzdem generiert wurden, respektierst du mit dem behaltenen Text, wofür bezahlt wurde.
  • Lass Nutzer abbrechen. Schließt sich die Antwort deines Relays, sollte sich auch die Upstream-Anfrage schließen; ein verwaister Stream, den du weiter konsumierst, ist Geld, das du ausgibst, um für niemanden zu rendern.

Häufige Fragen

Wie parse ich SSE-Chunks der eroq-API richtig?

Puffere die Bytes, splitte an \n\n und heb das letzte Fragment für den nächsten Read auf – ein Frame kann auf mehrere Netzwerk-Chunks verteilt ankommen. Übergib { stream: true } an TextDecoder.decode, damit Multibyte-Zeichen eine Trennung überstehen, und überspring das [DONE]-Signal, bevor du JSON parst.

Kostet Streaming mehr als eine normale Completion?

Nein. Credits werden pro Completion berechnet, nicht pro Token. Eine gestreamte und eine gepufferte Antwort kosten also gleich viel: 1 Credit bei RP mini oder 3 bei RP+ – egal, ob die Antwort 50 Token lang ist oder das Limit von 1.200 Token ausreizt. Preislich gibt es keinen Grund, bei stream: false zu bleiben.

Was ist das Usage-Event am Ende des Streams?

Ein Frame direkt vor data: [DONE] mit credits_spent und credits_remaining – so aktualisierst du eine Guthabenanzeige ohne zweiten API-Aufruf. Ein Error-Frame der Form {"error":{…}} ersetzt die abgebrochene Verbindung, aus der du den Fehler sonst ableiten müsstest, und ein Stream, der vor dem ersten Inhalt fehlschlägt, hat sich bereits selbst erstattet.

Warum kommt mein Stream auf einmal an statt Token für Token?

Fast immer, weil ein Proxy die Antwort puffert. Flushe deine Header vor dem ersten Write und setz bei nginx X-Accel-Buffering: no. Der zweite übliche Verdächtige: Frames, die in deinem Relay neu serialisiert werden – reich die Bytes unverändert weiter und parse im Client, wo du renderst.

Der Festpreis pro Antwort hat hier noch eine Folge: Streaming kostet exakt so viel wie Puffern – dieselben 1 oder 3 Credits. Es gibt keinen Grund, nicht zu streamen, und deshalb streamt jedes Beispiel in der Doku.

Tagsstreamingssechat api

Mach das mit den Modellen hinter diesem Artikel – starte mit 50 Gratis-Credits oder sieh dir alle Engines und ihre Preise an.