Streaming
Postavite stream na true i odgovor stiže kao server-sent events dok ga model piše: prvo rezonovanje, zatim odgovor, pa poslednji deo sa razlogom završetka i upotrebom. Koristite ga kad god osoba čeka tekst, i za duge odgovore.
POST https://api.shannon-ai.com/v1/chat/completions
OpenAI SDK-ovi vam daju delove jedan po jedan. Čitajte svaki deo prema onome što nosi: deo rezonovanja, deo odgovora, poziv alata ili kraj odgovora.
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
}' Kako je stream uokviren
- Odgovor ima status
200i zaglavljacontent-type: text/event-streamicache-control: no-cache. - Svaki događaj je jedna linija koja počinje sa
data:i iza koje sledi prazna linija. Tekst posledata:je jedan JSON deo. Nema linijaevent:niid:. - Linija koja počinje dvotačkom, kao što je
: ping, je komentar koji drži vezu otvorenom. Preskočite je. - Stream se završava sa
data: [DONE]. Stream koji stane bez toga je nepotpun.
Delovi redom
Svaki deo je objekat 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
}
]
} Svi delovi jednog streama nose isti id, created i model. choices uvek sadrži jedan unos. Njegov delta sadrži ono što je novo, a finish_reason je null do poslednjeg dela.
| Ključ u delta | Tip | Opis | Šalje |
|---|---|---|---|
role | string | Uvek assistant. Stiže jednom, na početku streama. | Hostovani open-weight modeli, shannon-2-lite, shannon-2-pro |
reasoning_content | string | Deo rezonovanja modela. Spojite delove redom. | Hostovani open-weight modeli, porodica Shannon 3, shannon-1.6-pro, shannon-coder-1 |
content | string | Deo odgovora. Spojite delove redom. | Svi modeli |
tool_calls | array | Jedan kompletan poziv alata. Pogledajte Delta-e poziva alata. | Svi modeli |
Prvo dolazi rezonovanje, zatim odgovor, zatim eventualni pozivi alata. Čitajte deo po ključevima u njegovom delta, a ne po mestu u streamu: delta može da sadrži role zajedno sa prvim delom teksta, a poslednji je prazan.
Ceo stream onako kako putuje. Da linije ostanu kratke, id, object, created i model izostavljeni su iz svakog dela ovde.
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] Delta-e rezonovanja
Model koji rezonuje šalje svoje rezonovanje u delta.reasoning_content pre odgovora. Držite ta dva teksta odvojeno: prikažite rezonovanje kao poseban deo koji se može sklopiti, ili ga izostavite.
Hostovani open-weight modeli ne šalju rezonovanje kada je reasoning_effort off, niti kada zahtev ima response_format.
shannon-2-lite i shannon-2-pro takođe rezonuju pre odgovora. Na ovom endpointu šalju komentarske linije : thinking dok to rade, a zatim odgovor.
reasoning_content nije u tipiziranim modelima OpenAI SDK-ova. U Pythonu ga čitajte pomoću getattr(delta, "reasoning_content", None); u JavaScriptu delta.reasoning_content radi kakav jeste.
Delta-e poziva alata
Poziv alata stiže ceo: jedan deo po pozivu, čiji delta sadrži poziv sa kompletnim stringom arguments. index numeriše pozive odgovora od 0. Kod koji spaja arguments deo po deo radi nepromenjen, jer spaja jedan deo.
{
"tool_calls": [
{
"index": 0,
"id": "call_3d9a7c1e5b2f4a60c8e1d7f09b24a6c5",
"type": "function",
"function": {
"name": "get_weather",
"arguments": "{\"city\":\"Paris\"}"
}
}
]
} Posle poziva, poslednji deo ima finish_reason tool_calls.
Kada zahtev ima tools, shannon-1.6-lite, shannon-1.6-pro i shannon-coder-1 šalju tekst odgovora kao jedan delta na kraju odgovora.
Poslednji deo
Poslednji deo pre data: [DONE] ima prazan delta, finish_reason i usage zahteva, sve u istom delu. Svaki stream ga ima; ne trebaju vam stream_options.
{
"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
}
} Na hostovanim open-weight modelima usage ima i prompt_tokens_details.cached_tokens i completion_tokens_details.reasoning_tokens.
Posle zahteva sa web_search: true čija je pretraga nešto našla, poslednji deo nosi i sources, a jedan deo pre njega nosi oznake citata kao delta.annotations. Ugrađena web pretraga
| finish_reason | Opis | Šalje |
|---|---|---|
stop | Model je završio odgovor, ili se pojavio stop string. | Svi modeli |
tool_calls | Odgovor sadrži jedan ili više poziva alata. | Svi modeli |
length | Odgovor je presečen na ograničenju izlaza. | Porodica Shannon 3, shannon-1.6-lite, shannon-1.6-pro, shannon-coder-1 |
Polja usage opisana su uz endpoint. Chat Completions
Keep-alive linije
Dok model radi i još nema šta da pošalje, stream nosi komentarske linije. One ne sadrže podatke. SSE parser ih sam preskače; kod koji čita sirove linije mora da preskoči svaku liniju koja počinje dvotačkom.
| Linija | Šalje | Kada |
|---|---|---|
: ping | Porodica Shannon 3, shannon-1.6-lite, shannon-1.6-pro, shannon-coder-1 | Na svakih 15 sekundi. |
: keepalive | Hostovani open-weight modeli | Na svakih 15 sekundi dok model radi. |
: thinking | shannon-2-lite, shannon-2-pro | Dok model rezonuje. |
Greške unutar streama
Kada stream počne, njegov status je 200, pa kvar stiže unutar streama: deo sa članom error na mestu gde bi bio choices.
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] Posle dela sa greškom i dalje slede poslednji deo, sa finish_reason i usage, i data: [DONE]. Proverite svaki deo na error. Bez te provere odgovor koji je pao na pola izgleda kompletno.
| Tip | Kod | Poruka | Kada |
|---|---|---|---|
api_error | The model backend failed to answer. Please retry. | Model nije uspeo, ili nije napisao ništa. Ponovo pošaljite zahtev. | |
rate_limit_error | Shannon routes are temporarily busy. Please retry. | Model je u ovom trenutku zauzet. Sačekajte nekoliko sekundi i ponovo pošaljite zahtev. | |
invalid_request_error | context_length_exceeded | Razgovor je duži od kontekstnog prozora modela. Skratite ga pre ponovnog slanja. Tekst poruke varira. |
Hostovani open-weight modeli šalju član error samo sa type i message.
OpenAI Python i JavaScript SDK-ovi podižu deo sa greškom kao API grešku dok iterirate, zato petlju obmotajte uobičajenim rukovanjem greškama.
Statusni kodovi, telo greške i šta ponoviti imaju svoju stranicu. Upravljanje greškama
Zatvaranje veze
Zatvaranje veze zaustavlja isporuku, a ne zahtev. Model piše odgovor do kraja i zahtev se naplaćuje kao da ste pročitali sve. Da platite manje, tražite manje: postavite niži max_tokens na modelu koji ga primenjuje.
Stream se može i prerano prekinuti na putu do vas, na primer pri prekidu mreže ili dok se server ažurira. Tada staje bez poslednjeg dela i bez data: [DONE]. Smatrajte takav odgovor nepotpunim i ponovo pošaljite zahtev.
Čitanje streama bez SDK-a
Ispravan čitač radi pet stvari: greške koje stignu pre streama šalje svom rukovanju greškama, deli telo na linije, preskače komentarske linije, staje na data: [DONE] i proverava svaki deo na 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); Python primer koristi paket requests. JavaScript primer radi na Node.js 18 ili novijem.
Streamovi drugih formata
Responses i Messages endpointi takođe strimuju. Njihovi frameovi nose liniju event: sa imenom događaja i liniju data: sa njegovim JSON-om. Svaka stranica navodi svoje događaje redom.
| U streamu | Chat Completions | Responses | Messages |
|---|---|---|---|
| Frameovi | Linije data: | Linije event: i data: | Linije event: i data: |
| Rezonovanje stiže u | delta.reasoning_content | response.reasoning_summary_text.delta | thinking_delta |
| Tekst odgovora stiže u | delta.content | response.output_text.delta | text_delta |
| Upotreba stiže u | Poslednji deo | response.completed | message_delta |
| Stream se završava sa | Poslednji deo, zatim data: [DONE] | response.completed | message_stop |