流式
将 stream 设为 true,回复就会在模型写作的同时以服务器发送事件的形式到达:先是推理,然后是答案,最后是带有结束原因和用量的数据块。只要有人在等待文本,以及回复较长时,就应使用它。
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 且搜索找到了结果的请求之后,最后一个 chunk 还会携带 sources,它前面的一个 chunk 以 delta.annotations 的形式携带引用标记。 内置网页搜索
| 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 | 对话超过了模型的上下文窗口。请先缩短它再重新发送。消息文本会有所不同。 |
托管开源权重模型发送的 error 成员只包含 type 和 message。
OpenAI 的 Python 和 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:(事件名称)和一行 data:(其 JSON)。每个页面按顺序列出各自的事件。
| 在流中 | 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 |