Entwickeln / Anleitungen

Streaming

Wie Sie Antworten als Server-Sent Events (SSE) empfangen, auch mit Maskierung, und welche Endpunkte Streaming ablehnen.

Mit "stream": true im Body liefert das Gateway die Antwort als Server-Sent Events (SSE). Die Events haben dasselbe Format wie beim Anbieter. Ein SDK, das mit der OpenAI- oder Anthropic-API streamt, funktioniert deshalb unverändert.

Endpunkte mit Streaming

EndpunktStreaming
POST /v1/chat/completionsja, SSE
POST /v1/responsesja, SSE
POST /v1/messagesja, SSE
POST /v1/embeddingsnein
POST /v1/audio/transcriptionsnein
POST /v1/audio/speechnein
POST /v1/images/generationsnein

Auf den Endpunkten ohne Streaming antwortet das Gateway auf "stream": true mit Statuscode 400 und dem Code streaming_not_supported_for_endpoint. Bei POST /v1/audio/speech gilt das auch für stream_format, bei POST /v1/audio/transcriptions für das Formularfeld stream.

Stream starten

Setzen Sie "stream": true. In curl schaltet -N die Pufferung der Ausgabe ab, damit die Events sofort erscheinen.

Die Beispiele verwenden den Client aus OpenAI-SDK und die Modell-ID qwen3.8-27b.

Shell
curl -N https://api.noirdoc.de/v1/chat/completions \
  -H "Authorization: Bearer $NOIRDOC_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "qwen3.8-27b",
    "stream": true,
    "messages": [{
      "role": "user",
      "content": "Schreiben Sie ein kurzes Anschreiben."
    }]
  }'
Python
stream = client.chat.completions.create(
    model="qwen3.8-27b",
    messages=[
        {
            "role": "user",
            "content": "Schreiben Sie ein kurzes Anschreiben.",
        }
    ],
    stream=True,
)
for chunk in stream:
    if chunk.choices and chunk.choices[0].delta.content:
        text = chunk.choices[0].delta.content
        print(text, end="", flush=True)
TypeScript
const stream = await client.chat.completions.create({
  model: "qwen3.8-27b",
  messages: [
    {
      role: "user",
      content: "Schreiben Sie ein kurzes Anschreiben.",
    },
  ],
  stream: true,
});
for await (const chunk of stream) {
  process.stdout.write(chunk.choices[0]?.delta?.content ?? "");
}

Mit dem Anthropic-SDK streamen

client.messages.stream streamt POST /v1/messages. In Python liefert text_stream die Textstücke, in TypeScript das Event text. Den Client richten Sie ein wie unter Anthropic-SDK & Claude Code beschrieben.

Python
with client.messages.stream(
    model="<modell-id>",
    max_tokens=1024,
    messages=[
        {
            "role": "user",
            "content": "Schreiben Sie ein kurzes Anschreiben.",
        }
    ],
) as stream:
    for text in stream.text_stream:
        print(text, end="", flush=True)
TypeScript
const stream = client.messages.stream({
  model: "<modell-id>",
  max_tokens: 1024,
  messages: [
    {
      role: "user",
      content: "Schreiben Sie ein kurzes Anschreiben.",
    },
  ],
});
stream.on("text", (text) => process.stdout.write(text));
await stream.finalMessage();

Maskierte Streams

Mit Maskierung sieht das Modell Platzhalter wie <<PERSON_1>>. Das Gateway setzt die Originalwerte in den Stream wieder ein, bevor die Events Ihre Anwendung erreichen. Das gilt für Chat Completions, Responses und Messages.

Ein Platzhalter kann über zwei Events verteilt ankommen. Deshalb hält das Gateway Text zurück, bis ein Satz endet (Punkt, Ausrufe- oder Fragezeichen mit Leerzeichen, oder ein Zeilenumbruch). Erst dann setzt es die Originalwerte ein und sendet den Satz weiter. Ihre Anwendung erhält den Text in maskierten Streams deshalb satzweise statt in einzelnen Tokens. Fehlt ein Satzende, sendet das Gateway spätestens nach 2.000 Zeichen weiter, jedoch nie mitten in einem Platzhalter.

Argumente von Tool-Aufrufen hält das Gateway zurück, bis der Aufruf vollständig ist, und sendet sie dann mit den Originalwerten in einem Stück.

Hat das Gateway in der Unterhaltung keine Platzhalter vergeben, zum Beispiel weil die Maskierung aus ist, leitet es jedes Event sofort weiter.

Nutzungsdaten bei Chat Completions

Für die Abrechnung braucht das Gateway die Token-Zahlen am Ende des Streams. Bei POST /v1/chat/completions setzt es deshalb stream_options.include_usage selbst. Haben Sie die Option nicht gesetzt, entfernt das Gateway das zusätzliche Event mit den Nutzungsdaten wieder aus dem Stream. Ihre Anwendung erhält denselben Stream wie ohne Gateway.

Fehler und Failover

Failover heißt: Das Gateway fällt auf den nächsten Anbieter für dasselbe Modell zurück. Bei Streams geschieht das, solange noch kein Byte der Antwort an Ihre Anwendung gegangen ist, also bei Netzwerkfehlern, 429 oder Statuscodes ab 500 vor dem Beginn des Streams. Mehr dazu unter Anbieter & Routing.

  • Fehler vor dem Stream: Lehnt der Anbieter die Anfrage ab, gibt das Gateway Statuscode und Body des Anbieters weiter, ohne SSE. Fehler des Gateways kommen als Fehlerobjekt mit "type": "proxy_error".
  • Nach dem Start des Streams bleibt der Anbieter fest. Das Gateway wechselt dann nicht mehr.

Auch Streams tragen den Header X-Noirdoc-Provider. Er nennt den Anbieter, der den Stream liefert.

StatusCodeBedeutung
400streaming_not_supported_for_endpointDie Anfrage verlangt Streaming, der Endpunkt unterstützt es aber nicht.
403masking_not_supported_for_endpointDer Endpunkt kann nicht maskieren, aber die Maskierungsrichtlinie ist enforced oder die Anfrage sendet X-Noirdoc-Mask: on.
502provider_unreachableDas Gateway konnte keine Verbindung zum Anbieter herstellen, oder die Verbindung ist abgebrochen.
504provider_timeoutDer Anbieter hat nicht innerhalb der Wartezeit des Gateways geantwortet.