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}") import OpenAI from "openai";
const client = new OpenAI({
apiKey: "YOUR_API_KEY",
baseURL: "https://api.shannon-ai.com/v1",
});
const stream = await client.chat.completions.create({
model: "shannon-3",
messages: [{ role: "user", content: "Explain server-sent events in three sentences." }],
stream: true,
});
let answer = "";
let reasoning = "";
const toolCalls = [];
let finishReason = null;
let usage = null;
try {
for await (const chunk of stream) {
if (chunk.usage) usage = chunk.usage;
const choice = chunk.choices?.[0];
if (!choice) continue;
const delta = choice.delta;
if (delta.reasoning_content) reasoning += delta.reasoning_content;
if (delta.content) {
answer += delta.content;
process.stdout.write(delta.content);
}
for (const call of delta.tool_calls ?? []) {
toolCalls.push({ id: call.id, name: call.function.name, arguments: call.function.arguments });
}
if (choice.finish_reason) finishReason = choice.finish_reason;
}
} catch (error) {
// A failure after the stream started arrives as an error chunk.
console.error("\nstream failed:", error.message);
}
console.log("\n", finishReason, usage); curl -N https://api.shannon-ai.com/v1/chat/completions \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "shannon-3",
"messages": [{"role": "user", "content": "Explain server-sent events in three sentences."}],
"stream": true
}' Wie ein Stream aufgebaut ist
- Die Antwort hat den Status
200und die Headercontent-type: text/event-streamundcache-control: no-cache. - Jedes Event ist eine Zeile, die mit
data:beginnt und auf die eine leere Zeile folgt. Der Text nachdata:ist ein JSON-Chunk. Es gibt keine Zeilenevent:oderid:. - 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:
{
"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.
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.
{
"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.
{
"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.
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)) const response = await fetch("https://api.shannon-ai.com/v1/chat/completions", {
method: "POST",
headers: {
Authorization: "Bearer YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "shannon-3",
messages: [{ role: "user", content: "Explain server-sent events in three sentences." }],
stream: true,
}),
});
// 1. Errors before the stream are plain JSON with their own status.
if (!response.ok) throw new Error(`${response.status}: ${await response.text()}`);
const decoder = new TextDecoder();
let buffer = "";
let answer = "";
let done = false;
read: for await (const bytes of response.body) {
buffer += decoder.decode(bytes, { stream: true });
const lines = buffer.split("\n"); // 2. one line at a time
buffer = lines.pop(); // keep the unfinished line
for (const line of lines) {
if (!line.startsWith("data: ")) continue; // 3. empty lines and ": ping" comments
const data = line.slice(6);
if (data === "[DONE]") { // 4. the end of the stream
done = true;
break read;
}
const chunk = JSON.parse(data);
if (chunk.error) throw new Error(chunk.error.message); // 5. a failure inside the stream
const choice = chunk.choices[0];
if (choice.delta.content) answer += choice.delta.content;
if (chunk.usage) console.log(choice.finish_reason, chunk.usage);
}
}
if (!done) throw new Error("the stream ended before [DONE]");
console.log(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 Completions | Responses | Messages |
|---|---|---|---|
| Frames | data:-Zeilen | event:- und data:-Zeilen | event:- und data:-Zeilen |
| Reasoning trifft ein in | delta.reasoning_content | response.reasoning_summary_text.delta | thinking_delta |
| Antworttext trifft ein in | delta.content | response.output_text.delta | text_delta |
| Nutzungsdaten treffen ein in | Der letzte Chunk | response.completed | message_delta |
| Der Stream endet mit | Der letzte Chunk, dann data: [DONE] | response.completed | message_stop |