Zum Inhalt springen
Streaming

Streaming

Setzen Sie stream auf true, und die Antwort trifft als Server-Sent Events ein, während das Modell sie schreibt: zuerst das Reasoning, dann die Antwort, dann ein letzter Chunk mit dem Finish-Grund und den Nutzungsdaten. Verwenden Sie es immer, wenn ein Mensch auf den Text wartet, und bei langen Antworten.

POST https://api.shannon-ai.com/v1/chat/completions

Die OpenAI-SDKs geben Ihnen die Chunks einzeln. Lesen Sie jeden Chunk nach dem, was er trägt: ein Stück Reasoning, ein Stück der Antwort, einen Tool-Aufruf oder das Ende der Antwort.

from openai import OpenAI, APIError

client = OpenAI(
    api_key="YOUR_API_KEY",
    base_url="https://api.shannon-ai.com/v1",
)

stream = client.chat.completions.create(
    model="shannon-3",
    messages=[{"role": "user", "content": "Explain server-sent events in three sentences."}],
    stream=True,
)

answer, reasoning, tool_calls = [], [], []
finish_reason = usage = None

try:
    for chunk in stream:
        if chunk.usage:
            usage = chunk.usage
        if not chunk.choices:
            continue
        choice = chunk.choices[0]
        delta = choice.delta

        thought = getattr(delta, "reasoning_content", None)
        if thought:
            reasoning.append(thought)
        if delta.content:
            answer.append(delta.content)
            print(delta.content, end="", flush=True)
        for call in delta.tool_calls or []:
            tool_calls.append((call.id, call.function.name, call.function.arguments))
        if choice.finish_reason:
            finish_reason = choice.finish_reason
except APIError as error:
    # A failure after the stream started arrives as an error chunk.
    print(f"\nstream failed: {error}")

print(f"\n{finish_reason} {usage}")

Wie ein Stream aufgebaut ist

  • Die Antwort hat den Status 200 und die Header content-type: text/event-stream und cache-control: no-cache.
  • Jedes Event ist eine Zeile, die mit data: beginnt und auf die eine leere Zeile folgt. Der Text nach data: ist ein JSON-Chunk. Es gibt keine Zeilen event: oder id:.
  • Eine Zeile, die mit einem Doppelpunkt beginnt, wie : ping, ist ein Kommentar, der die Verbindung offen hält. Überspringen Sie sie.
  • Der Stream endet mit data: [DONE]. Ein Stream, der ohne diese Zeile aufhört, ist unvollständig.

Chunks in der Reihenfolge

Jeder Chunk ist ein Objekt chat.completion.chunk:

JSON
{
  "id": "chatcmpl-5f0c1e7a9b3d4c62a8e1f07d2b46c9a3",
  "object": "chat.completion.chunk",
  "created": 1791625200,
  "model": "shannon-3",
  "choices": [
    {
      "index": 0,
      "delta": {
        "content": "Server-sent"
      },
      "finish_reason": null
    }
  ]
}

Alle Chunks eines Streams tragen dieselben id, created und model. choices enthält immer einen Eintrag. Sein delta enthält, was neu ist, und finish_reason ist null bis zum letzten Chunk.

Schlüssel im Delta Typ Beschreibung Gesendet von
role string Immer assistant. Es trifft einmal ein, am Anfang des Streams. Gehostete Open-Weight-Modelle, shannon-2-lite, shannon-2-pro
reasoning_content string Ein Stück des Reasonings des Modells. Fügen Sie die Stücke der Reihe nach zusammen. Gehostete Open-Weight-Modelle, Shannon-3-Familie, shannon-1.6-pro, shannon-coder-1
content string Ein Stück der Antwort. Fügen Sie die Stücke der Reihe nach zusammen. Alle Modelle
tool_calls array Ein vollständiger Tool-Aufruf. Siehe Deltas von Tool-Aufrufen. Alle Modelle

Zuerst kommt das Reasoning, dann die Antwort, dann eventuelle Tool-Aufrufe. Lesen Sie einen Chunk anhand der Schlüssel in seinem delta, nicht anhand seiner Position im Stream: Ein delta kann role zusammen mit dem ersten Textstück enthalten, und das letzte ist leer.

Ein ganzer Stream, so wie er übertragen wird. Damit die Zeilen kurz bleiben, sind id, object, created und model hier in jedem Chunk weggelassen.

200 text/event-stream
data: {"choices":[{"index":0,"delta":{"reasoning_content":"Three sentences:"},"finish_reason":null}]}

data: {"choices":[{"index":0,"delta":{"reasoning_content":" what it is, how it is framed, why it is used."},"finish_reason":null}]}

: ping

data: {"choices":[{"index":0,"delta":{"content":"Server-sent"},"finish_reason":null}]}

data: {"choices":[{"index":0,"delta":{"content":" events let a server push text to a client over one HTTP reply."},"finish_reason":null}]}

data: {"choices":[{"index":0,"delta":{},"finish_reason":"stop"}],"usage":{"prompt_tokens":1190,"completion_tokens":84,"total_tokens":1274}}

data: [DONE]

Reasoning-Deltas

Ein Modell, das nachdenkt, sendet sein Reasoning in delta.reasoning_content vor der Antwort. Halten Sie die beiden Texte getrennt: Zeigen Sie das Reasoning als eigenen, einklappbaren Teil an oder lassen Sie es weg.

Die gehosteten Open-Weight-Modelle senden kein Reasoning, wenn reasoning_effort off ist, und auch keines, wenn die Anfrage ein response_format hat.

Auch shannon-2-lite und shannon-2-pro denken nach, bevor sie antworten. Bei diesem Endpunkt senden sie dabei Kommentarzeilen : thinking und danach die Antwort.

reasoning_content steht nicht in den typisierten Modellen der OpenAI-SDKs. In Python lesen Sie es mit getattr(delta, "reasoning_content", None); in JavaScript funktioniert delta.reasoning_content unverändert.

Deltas von Tool-Aufrufen

Ein Tool-Aufruf trifft vollständig ein: ein Chunk pro Aufruf, dessen delta den Aufruf mit dem vollständigen arguments-String enthält. index nummeriert die Aufrufe der Antwort ab 0. Code, der arguments Stück für Stück zusammenfügt, funktioniert unverändert, weil er ein Stück zusammenfügt.

delta
{
  "tool_calls": [
    {
      "index": 0,
      "id": "call_3d9a7c1e5b2f4a60c8e1d7f09b24a6c5",
      "type": "function",
      "function": {
        "name": "get_weather",
        "arguments": "{\"city\":\"Paris\"}"
      }
    }
  ]
}

Nach den Aufrufen hat der letzte Chunk finish_reason tool_calls.

Hat die Anfrage tools, senden shannon-1.6-lite, shannon-1.6-pro und shannon-coder-1 den Antworttext als ein Delta am Ende der Antwort.

Der letzte Chunk

Der letzte Chunk vor data: [DONE] hat ein leeres delta, den finish_reason und die usage der Anfrage, alles im selben Chunk. Jeder Stream hat ihn; Sie brauchen stream_options nicht.

JSON
{
  "id": "chatcmpl-5f0c1e7a9b3d4c62a8e1f07d2b46c9a3",
  "object": "chat.completion.chunk",
  "created": 1791625200,
  "model": "shannon-3",
  "choices": [
    {
      "index": 0,
      "delta": {},
      "finish_reason": "stop"
    }
  ],
  "usage": {
    "prompt_tokens": 1190,
    "completion_tokens": 84,
    "total_tokens": 1274
  }
}

Bei den gehosteten Open-Weight-Modellen enthält usage außerdem prompt_tokens_details.cached_tokens und completion_tokens_details.reasoning_tokens.

Nach einer Anfrage mit web_search: true, deren Suche etwas gefunden hat, trägt der letzte Chunk auch sources, und ein Chunk davor trägt die Zitatmarker als delta.annotations. Integrierte Websuche

finish_reason Beschreibung Gesendet von
stop Das Modell hat seine Antwort beendet, oder ein stop-String ist aufgetreten. Alle Modelle
tool_calls Die Antwort enthält einen oder mehrere Tool-Aufrufe. Alle Modelle
length Die Antwort wurde am Output-Limit abgeschnitten. Shannon-3-Familie, shannon-1.6-lite, shannon-1.6-pro, shannon-coder-1

Die Felder von usage sind beim Endpunkt beschrieben. Chat Completions

Keep-alive-Zeilen

Solange das Modell arbeitet und noch nichts zu senden hat, trägt der Stream Kommentarzeilen. Sie enthalten keine Daten. Ein SSE-Parser überspringt sie von selbst; Code, der rohe Zeilen liest, muss jede Zeile überspringen, die mit einem Doppelpunkt beginnt.

Zeile Gesendet von Wann
: ping Shannon-3-Familie, shannon-1.6-lite, shannon-1.6-pro, shannon-coder-1 Alle 15 Sekunden.
: keepalive Gehostete Open-Weight-Modelle Alle 15 Sekunden, solange das Modell arbeitet.
: thinking shannon-2-lite, shannon-2-pro Solange das Modell nachdenkt.

Fehler innerhalb eines Streams

Sobald ein Stream begonnen hat, ist sein Status 200, daher trifft ein Fehler innerhalb des Streams ein: ein Chunk mit einem Member error an der Stelle, an der choices stünde.

200 text/event-stream
data: {"error":{"message":"The model backend failed to answer. Please retry.","type":"api_error","code":null,"param":null}}

data: {"choices":[{"index":0,"delta":{},"finish_reason":"stop"}],"usage":{"prompt_tokens":1190,"completion_tokens":12,"total_tokens":1202}}

data: [DONE]

Nach dem Fehler-Chunk folgen weiterhin der letzte Chunk mit finish_reason und usage sowie data: [DONE]. Prüfen Sie jeden Chunk auf error. Ohne diese Prüfung sieht eine Antwort, die auf halbem Weg fehlgeschlagen ist, vollständig aus.

Typ Code Nachricht Wann
api_error The model backend failed to answer. Please retry. Das Modell ist fehlgeschlagen oder hat nichts geschrieben. Senden Sie die Anfrage erneut.
rate_limit_error Shannon routes are temporarily busy. Please retry. Das Modell ist im Moment ausgelastet. Warten Sie einige Sekunden und senden Sie die Anfrage erneut.
invalid_request_error context_length_exceeded Die Konversation ist länger als das Kontextfenster des Modells. Kürzen Sie sie, bevor Sie sie erneut senden. Der Nachrichtentext variiert.

Die gehosteten Open-Weight-Modelle senden den Member error nur mit type und message.

Die OpenAI-SDKs für Python und JavaScript lösen einen Fehler-Chunk als API-Fehler aus, während Sie iterieren, packen Sie die Schleife daher in Ihre übliche Fehlerbehandlung.

Statuscodes, der Fehler-Body und was wiederholt werden sollte haben eine eigene Seite. Fehlerbehandlung

Die Verbindung schließen

Das Schließen der Verbindung stoppt die Zustellung, nicht die Anfrage. Das Modell schreibt die Antwort bis zum Ende, und die Anfrage wird so berechnet, als hätten Sie alles gelesen. Um weniger zu zahlen, fordern Sie weniger an: Setzen Sie ein niedrigeres max_tokens bei einem Modell, das es anwendet.

Ein Stream kann auch auf dem Weg zu Ihnen vorzeitig enden, zum Beispiel bei einem Netzwerkabbruch oder während ein Server aktualisiert wird. Er hört dann ohne den letzten Chunk und ohne data: [DONE] auf. Behandeln Sie diese Antwort als unvollständig und senden Sie die Anfrage erneut.

Einen Stream ohne SDK lesen

Ein korrekter Reader tut fünf Dinge: Er leitet Fehler, die vor dem Stream eintreffen, an seine Fehlerbehandlung weiter, teilt den Body in Zeilen, überspringt Kommentarzeilen, hört bei data: [DONE] auf und prüft jeden Chunk auf error.

import json
import requests

response = requests.post(
    "https://api.shannon-ai.com/v1/chat/completions",
    headers={"Authorization": "Bearer YOUR_API_KEY"},
    json={
        "model": "shannon-3",
        "messages": [{"role": "user", "content": "Explain server-sent events in three sentences."}],
        "stream": True,
    },
    stream=True,
)

# 1. Errors before the stream are plain JSON with their own status.
if response.status_code != 200:
    raise RuntimeError(f"{response.status_code}: {response.text}")

answer, done = [], False
for raw in response.iter_lines():          # 2. one line at a time
    line = raw.decode("utf-8")
    if not line.startswith("data: "):      # 3. empty lines and ": ping" comments
        continue
    data = line[len("data: "):]
    if data == "[DONE]":                   # 4. the end of the stream
        done = True
        break
    chunk = json.loads(data)
    if "error" in chunk:                   # 5. a failure inside the stream
        raise RuntimeError(chunk["error"]["message"])
    delta = chunk["choices"][0]["delta"]
    if delta.get("content"):
        answer.append(delta["content"])
    if chunk.get("usage"):
        print(chunk["choices"][0]["finish_reason"], chunk["usage"])

if not done:
    raise RuntimeError("the stream ended before [DONE]")
print("".join(answer))

Das Python-Beispiel verwendet das Paket requests. Das JavaScript-Beispiel läuft auf Node.js 18 oder neuer.

Streams der anderen Formate

Auch die Endpunkte Responses und Messages streamen. Ihre Frames tragen eine event:-Zeile mit dem Namen des Events und eine data:-Zeile mit dessen JSON. Jede Seite listet ihre Events der Reihe nach auf.

Im Stream Chat CompletionsResponsesMessages
Frames data:-Zeilenevent:- und data:-Zeilenevent:- und data:-Zeilen
Reasoning trifft ein in delta.reasoning_contentresponse.reasoning_summary_text.deltathinking_delta
Antworttext trifft ein in delta.contentresponse.output_text.deltatext_delta
Nutzungsdaten treffen ein in Der letzte Chunkresponse.completedmessage_delta
Der Stream endet mit Der letzte Chunk, dann data: [DONE]response.completedmessage_stop