ストリーミング
stream を true に設定すると、モデルが書いている間、応答が server-sent events として届きます。最初に推論、次に回答、最後に終了理由と使用量を含むチャンクです。人がテキストを待っているときや、長い応答に使ってください。
POST https://api.shannon-ai.com/v1/chat/completions
OpenAI SDK は、チャンクをひとつずつ返します。各チャンクは、含まれているもので読み取ってください。推論の一部、回答の一部、ツール呼び出し、応答の終わりのいずれかです。
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
}' ストリームのフレーム形式
- 応答のステータスは
200で、ヘッダーはcontent-type: text/event-streamとcache-control: no-cacheです。 - 各イベントは
data:で始まる一行で、その後に空行が続きます。data:の後ろのテキストは、ひとつの JSON チャンクです。event:行やid:行はありません。 : pingのようにコロンで始まる行は、接続を開いたままにするためのコメントです。スキップしてください。- ストリームは
data: [DONE]で終わります。これがないまま止まったストリームは不完全です。
チャンクの順序
すべてのチャンクは 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
}
]
} 同じストリームのすべてのチャンクは、同じ id、created、model を持ちます。choices には常にひとつのエントリがあります。その delta に新しい内容が入り、finish_reason は最後のチャンクまで null です。
| delta 内のキー | 型 | 説明 | 送信元 |
|---|---|---|---|
role | string | 常に assistant。ストリームの冒頭で一度だけ届きます。 | ホスト型オープンウェイトモデル、shannon-2-lite、shannon-2-pro |
reasoning_content | string | モデルの推論の一部。順番に連結してください。 | ホスト型オープンウェイトモデル、Shannon 3 ファミリー、shannon-1.6-pro、shannon-coder-1 |
content | string | 回答の一部。順番に連結してください。 | すべてのモデル |
tool_calls | array | 完全なツール呼び出しがひとつ。「ツール呼び出しのデルタ」を参照してください。 | すべてのモデル |
推論が先に届き、次に回答、その後にツール呼び出しが続きます。チャンクは、ストリーム内の位置ではなく、delta のキーで読み取ってください。delta には最初のテキストと一緒に role が入ることがあり、最後のものは空です。
ストリーム全体の通信内容。行を短くするため、ここでは各チャンクの id、object、created、model を省略しています。
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.reasoning_content で推論を送信します。二つのテキストは分けて扱ってください。推論は折りたたみ可能な別の部分として表示するか、省略します。
ホスト型オープンウェイトモデルは、reasoning_effort が off の場合、およびリクエストに response_format がある場合は、推論を送信しません。
shannon-2-lite と shannon-2-pro も、回答の前に推論します。このエンドポイントでは、推論している間 : thinking コメント行を送信し、その後に回答を送信します。
reasoning_content は、OpenAI SDK の型付きモデルにはありません。Python では getattr(delta, "reasoning_content", None) で読み取ります。JavaScript では delta.reasoning_content がそのまま使えます。
ツール呼び出しのデルタ
ツール呼び出しは、まとまって届きます。呼び出しごとにひとつのチャンクで、その delta に、完全な arguments 文字列を含む呼び出しが入ります。index は、応答の呼び出しを 0 から数えます。arguments を断片ごとに連結するコードは、断片がひとつだけなので、そのまま動作します。
{
"tool_calls": [
{
"index": 0,
"id": "call_3d9a7c1e5b2f4a60c8e1d7f09b24a6c5",
"type": "function",
"function": {
"name": "get_weather",
"arguments": "{\"city\":\"Paris\"}"
}
}
]
} 呼び出しの後、最後のチャンクの finish_reason は tool_calls になります。
リクエストに tools がある場合、shannon-1.6-lite、shannon-1.6-pro、shannon-coder-1 は、応答の最後にひとつのデルタとして回答テキストを送信します。
最後のチャンク
data: [DONE] の直前の最後のチャンクには、空の delta、finish_reason、リクエストの usage が、すべて同じチャンク内に入っています。すべてのストリームにあり、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
}
} ホスト型オープンウェイトモデルでは、usage に prompt_tokens_details.cached_tokens と completion_tokens_details.reasoning_tokens も含まれます。
検索で何かが見つかった web_search: true のリクエストの後では、最後のチャンクに sources も含まれ、その直前のチャンクに引用の印が delta.annotations として含まれます。 内蔵Web検索
| finish_reason | 説明 | 送信元 |
|---|---|---|
stop | モデルが回答を終えたか、stop の文字列が現れました。 | すべてのモデル |
tool_calls | 応答にひとつ以上のツール呼び出しが含まれます。 | すべてのモデル |
length | 応答が出力上限で打ち切られました。 | Shannon 3 ファミリー、shannon-1.6-lite、shannon-1.6-pro、shannon-coder-1 |
usage のフィールドについては、エンドポイントの説明を参照してください。 Chat Completions
キープアライブ行
モデルが処理中でまだ送るものがない間、ストリームはコメント行を送ります。これらにデータは含まれません。SSE パーサーは自動的にスキップします。生の行を読むコードは、コロンで始まるすべての行をスキップする必要があります。
| 行 | 送信元 | 発生条件 |
|---|---|---|
: ping | Shannon 3 ファミリー、shannon-1.6-lite、shannon-1.6-pro、shannon-coder-1 | 15 秒ごと。 |
: keepalive | ホスト型オープンウェイトモデル | モデルの処理中、15 秒ごと。 |
: thinking | shannon-2-lite、shannon-2-pro | モデルが推論している間。 |
ストリーム内のエラー
ストリームが始まった後は、ステータスは 200 なので、失敗はストリーム内で届きます:choices があるはずの場所に error メンバーを持つチャンクです。
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] エラーチャンクの後にも、finish_reason と usage を含む最後のチャンクと、data: [DONE] が続きます。すべてのチャンクで error を確認してください。この確認がないと、途中で失敗した応答が完了したように見えます。
| タイプ | コード | メッセージ | 発生条件 |
|---|---|---|---|
api_error | The model backend failed to answer. Please retry. | モデルが失敗したか、何も書きませんでした。リクエストを再送信してください。 | |
rate_limit_error | Shannon routes are temporarily busy. Please retry. | 現在、モデルがビジー状態です。数秒待ってから、リクエストを再送信してください。 | |
invalid_request_error | context_length_exceeded | 会話がモデルのコンテキストウィンドウより長くなっています。再送信する前に短くしてください。メッセージのテキストは変わることがあります。 |
ホスト型オープンウェイトモデルは、type と message のみを持つ error メンバーを送信します。
OpenAI の Python SDK と JavaScript SDK は、反復処理中にエラーチャンクを API エラーとして発生させるため、ループを通常のエラー処理で囲んでください。
ステータスコード、エラーボディ、再試行すべきものについては、専用のページがあります。 エラー処理
接続を閉じる
接続を閉じると配信は止まりますが、リクエストは止まりません。モデルは応答を最後まで書き、リクエストは、すべて読んだ場合と同じように課金されます。支払いを減らすには、要求を減らしてください。適用するモデルでは、max_tokens を小さく設定します。
ストリームは、ネットワーク断やサーバーの更新中など、あなたへの経路で途中終了することもあります。その場合は、最後のチャンクも data: [DONE] もなく停止します。その応答は不完全として扱い、リクエストを再送信してください。
SDK を使わずにストリームを読む
正しい読み取り処理は、五つのことを行います。ストリームの前に届いたエラーをエラー処理に回し、ボディを行に分割し、コメント行をスキップし、data: [DONE] で停止し、各チャンクの 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 のサンプルは requests パッケージを使います。JavaScript のサンプルは Node.js 18 以降で動作します。
他の形式のストリーム
Responses と Messages のエンドポイントもストリーミングできます。そのフレームは、イベント名を示す event: 行と、その JSON を示す data: 行を持ちます。各ページに、イベントが順序どおり一覧になっています。
| ストリーム内 | Chat Completions | Responses | Messages |
|---|---|---|---|
| フレーム | data: 行 | event: 行と data: 行 | event: 行と data: 行 |
| 推論の届く場所: | delta.reasoning_content | response.reasoning_summary_text.delta | thinking_delta |
| 回答テキストの届く場所: | delta.content | response.output_text.delta | text_delta |
| 使用量の届く場所: | 最後のチャンク | response.completed | message_delta |
| ストリームの終わり: | 最後のチャンク、続いて data: [DONE] | response.completed | message_stop |