Suoratoisto
Aseta stream arvoon true, niin vastaus saapuu server-sent events -tapahtumina sitä mukaa kuin malli kirjoittaa sitä: ensin päättely, sitten vastaus ja lopuksi viimeinen chunk, jossa on lopetussyy ja käyttötiedot (usage). Käytä sitä aina, kun ihminen odottaa tekstiä, ja pitkissä vastauksissa.
POST https://api.shannon-ai.com/v1/chat/completions
OpenAI-SDK:t antavat chunkit yksi kerrallaan. Lue jokainen chunk sen mukaan, mitä se sisältää: pala päättelyä, pala vastausta, työkalukutsu tai vastauksen loppu.
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
}' Miten striimi kehystetään
- Vastauksen tila on
200ja otsakkeet ovatcontent-type: text/event-streamjacache-control: no-cache. - Jokainen tapahtuma on yksi rivi, joka alkaa merkinnällä
data:ja jonka perässä on tyhjä rivi. Teksti merkinnändata:jälkeen on yksi JSON-chunk.event:- taiid:-rivejä ei ole. - Rivi, joka alkaa kaksoispisteellä, kuten
: ping, on kommentti, joka pitää yhteyden auki. Ohita se. - Striimi päättyy riviin
data: [DONE]. Striimi, joka pysähtyy ilman sitä, on keskeneräinen.
Chunkit järjestyksessä
Jokainen chunk on chat.completion.chunk-objekti:
{
"id": "chatcmpl-5f0c1e7a9b3d4c62a8e1f07d2b46c9a3",
"object": "chat.completion.chunk",
"created": 1791625200,
"model": "shannon-3",
"choices": [
{
"index": 0,
"delta": {
"content": "Server-sent"
},
"finish_reason": null
}
]
} Saman striimin kaikilla chunkeilla on sama id, created ja model. choices sisältää aina yhden alkion. Sen delta sisältää sen, mikä on uutta, ja finish_reason on null viimeiseen chunkiin asti.
| Avain deltassa | Tyyppi | Kuvaus | Lähettäjä |
|---|---|---|---|
role | string | Aina assistant. Se saapuu kerran, striimin alussa. | Hostatut avoimen painon mallit, shannon-2-lite, shannon-2-pro |
reasoning_content | string | Pala mallin päättelyä. Yhdistä palat järjestyksessä. | Hostatut avoimen painon mallit, Shannon 3 -perhe, shannon-1.6-pro, shannon-coder-1 |
content | string | Pala vastausta. Yhdistä palat järjestyksessä. | Kaikki mallit |
tool_calls | array | Yksi täydellinen työkalukutsu. Katso Työkalukutsun deltat. | Kaikki mallit |
Päättely tulee ensin, sitten vastaus ja lopuksi mahdolliset työkalukutsut. Lue chunk sen delta-kentän avainten mukaan, ei sen paikan mukaan striimissä: delta voi sisältää role-kentän yhdessä ensimmäisen tekstipalan kanssa, ja viimeinen on tyhjä.
Kokonainen striimi sellaisena kuin se kulkee. Rivien pitämiseksi lyhyinä id, object, created ja model on jätetty tässä pois jokaisesta chunkista.
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] Päättelydeltat
Päättelevä malli lähettää päättelynsä kentässä delta.reasoning_content ennen vastausta. Pidä nämä kaksi tekstiä erillään: näytä päättely erillisenä, kutistettavana osana tai jätä se pois.
Hostatut avoimen painon mallit eivät lähetä päättelyä, kun reasoning_effort on off, eivätkä silloin, kun pyynnössä on response_format.
Myös shannon-2-lite ja shannon-2-pro päättelevät ennen vastaamista. Tässä päätepisteessä ne lähettävät : thinking-kommenttirivejä päätellessään ja sitten vastauksen.
reasoning_content ei ole OpenAI-SDK:iden tyypitetyissä malleissa. Pythonissa lue se komennolla getattr(delta, "reasoning_content", None); JavaScriptissä delta.reasoning_content toimii sellaisenaan.
Työkalukutsun deltat
Työkalukutsu saapuu kokonaisena: yksi chunk kutsua kohti, jonka delta sisältää kutsun koko arguments-merkkijonon. index numeroi vastauksen kutsut luvusta 0 alkaen. Koodi, joka yhdistää arguments-osia pala palalta, toimii muuttumattomana, koska se yhdistää yhden palan.
{
"tool_calls": [
{
"index": 0,
"id": "call_3d9a7c1e5b2f4a60c8e1d7f09b24a6c5",
"type": "function",
"function": {
"name": "get_weather",
"arguments": "{\"city\":\"Paris\"}"
}
}
]
} Kutsujen jälkeen viimeisessä chunkissa finish_reason on tool_calls.
Kun pyynnössä on tools, shannon-1.6-lite, shannon-1.6-pro ja shannon-coder-1 lähettävät vastaustekstin yhtenä deltana vastauksen lopussa.
Viimeinen chunk
Viimeisessä chunkissa ennen data: [DONE] on tyhjä delta, finish_reason ja pyynnön usage, kaikki samassa chunkissa. Jokaisessa striimissä se on; stream_options ei tarvita.
{
"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
}
} Hostatuilla avoimen painon malleilla usage sisältää myös prompt_tokens_details.cached_tokens ja completion_tokens_details.reasoning_tokens.
Kun pyynnössä on web_search: true ja sen haku löysi jotain, viimeisessä chunkissa on myös sources, ja yksi chunk sitä ennen kantaa viittausmerkinnät muodossa delta.annotations. Sisäänrakennettu verkkohaku
| finish_reason | Kuvaus | Lähettäjä |
|---|---|---|
stop | Malli päätti vastauksensa, tai stop-merkkijono ilmestyi. | Kaikki mallit |
tool_calls | Vastauksessa on yksi tai useampi työkalukutsu. | Kaikki mallit |
length | Vastaus katkaistiin tulosteen rajalla. | Shannon 3 -perhe, shannon-1.6-lite, shannon-1.6-pro, shannon-coder-1 |
usage-kentän kentät on kuvattu päätepisteen yhteydessä. Chat Completions
Keep-alive-rivit
Kun malli työskentelee eikä sillä ole vielä mitään lähetettävää, striimissä on kommenttirivejä. Niissä ei ole dataa. SSE-jäsennin ohittaa ne itse; koodin, joka lukee raakoja rivejä, on ohitettava jokainen kaksoispisteellä alkava rivi.
| Rivi | Lähettäjä | Milloin |
|---|---|---|
: ping | Shannon 3 -perhe, shannon-1.6-lite, shannon-1.6-pro, shannon-coder-1 | 15 sekunnin välein. |
: keepalive | Hostatut avoimen painon mallit | 15 sekunnin välein, kun malli työskentelee. |
: thinking | shannon-2-lite, shannon-2-pro | Kun malli päättelee. |
Virheet striimin sisällä
Kun striimi on alkanut, sen tila on 200, joten virhe saapuu striimin sisällä: chunk, jossa on error-jäsen siinä, missä choices olisi.
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] Virhechunkin jälkeen tulevat silti viimeinen chunk, jossa on finish_reason ja usage, sekä data: [DONE]. Tarkista jokaisesta chunkista error. Ilman tätä tarkistusta puolivälissä epäonnistunut vastaus näyttää täydeltä.
| Tyyppi | Koodi | Viesti | Milloin |
|---|---|---|---|
api_error | The model backend failed to answer. Please retry. | Malli epäonnistui tai ei kirjoittanut mitään. Lähetä pyyntö uudelleen. | |
rate_limit_error | Shannon routes are temporarily busy. Please retry. | Malli on juuri nyt kiireinen. Odota muutama sekunti ja lähetä pyyntö uudelleen. | |
invalid_request_error | context_length_exceeded | Keskustelu on pidempi kuin mallin kontekstikoko. Lyhennä sitä ennen kuin lähetät sen uudelleen. Viestin teksti vaihtelee. |
Hostatut avoimen painon mallit lähettävät error-jäsenen, jossa on vain type ja message.
OpenAI:n Python- ja JavaScript-SDK:t nostavat virhechunkin API-virheenä iteroidessasi, joten kääri silmukka tavalliseen virheenkäsittelyysi.
Tilakoodit, virheen runko ja uudelleen yritettävät asiat ovat omalla sivullaan. Virheiden käsittely
Yhteyden sulkeminen
Yhteyden sulkeminen lopettaa toimituksen, ei pyyntöä. Malli kirjoittaa vastauksen loppuun asti ja pyynnöstä veloitetaan ikään kuin olisit lukenut sen kokonaan. Jos haluat maksaa vähemmän, pyydä vähemmän: aseta pienempi max_tokens mallille, joka ottaa sen huomioon.
Striimi voi myös päättyä ennenaikaisesti matkalla sinulle, esimerkiksi verkkokatkoksen vuoksi tai palvelimen päivityksen aikana. Silloin se pysähtyy ilman viimeistä chunkia ja ilman data: [DONE]. Pidä tätä vastausta keskeneräisenä ja lähetä pyyntö uudelleen.
Striimin lukeminen ilman SDK:ta
Oikein toimiva lukija tekee viisi asiaa: se ohjaa ennen striimiä saapuvat virheet virheenkäsittelyynsä, jakaa rungon riveihin, ohittaa kommenttirivit, pysähtyy riviin data: [DONE] ja tarkistaa jokaisesta chunkista error-kentän.
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-esimerkki käyttää requests-pakettia. JavaScript-esimerkki toimii Node.js 18:lla tai uudemmalla.
Muiden muotojen striimit
Myös Responses- ja Messages-päätepisteet striimaavat. Niiden kehyksissä on event:-rivi tapahtuman nimellä ja data:-rivi sen JSON-sisällöllä. Kukin sivu listaa tapahtumansa järjestyksessä.
| Striimissä | Chat Completions | Responses | Messages |
|---|---|---|---|
| Kehykset | data:-rivit | event:- ja data:-rivit | event:- ja data:-rivit |
| Päättely saapuu | delta.reasoning_content | response.reasoning_summary_text.delta | thinking_delta |
| Vastausteksti saapuu | delta.content | response.output_text.delta | text_delta |
| Käyttötiedot saapuvat | Viimeinen chunk | response.completed | message_delta |
| Striimi päättyy | Viimeinen chunk, sitten data: [DONE] | response.completed | message_stop |