OpenAI Python SDK流式响应怎么收完整?用离线SSE样本检查delta和结束状态
作者说明:由FlowBarCN团队提供,涉及自家服务,经AI辅助整理。以下运行与测试全部使用本地模拟HTTP传输,不需要真实密钥,不请求线上模型。
给非流式代码加上 stream=True,随后继续读取 chunk.choices[0].message.content,往往就是流式接入的第一个问题:这里收到的是增量块,结构和完整响应不同。
还有一个更隐蔽的问题:你可能已经收到一些文字,但最终结束原因是 length。如果程序只要“有文字”就返回成功,下游拿到的就是一份没有标明缺失的半成品。
本文给出一个仅收集单候选文本、验证完成状态后才返回的聚合器。你会得到一份可运行脚本,以及能离线复现的异常样本。前一篇已经讨论SDK超时与重试,这篇只处理流式内容与结果完整性,不重写重试循环。
1. 先确定这段代码适用到哪里
本例处理Chat Completions的普通文本流,不处理Responses事件、音频、工具调用、多候选合并或断点续传。
官方流式示例展示了 delta 中的角色信息、内容增量和空增量;流式块参考还说明某些用量块的 choices 可以为空。这里不能不加判断地读取 choices[0]。
这些是上游协议文档。某个兼容网关和具体模型是否支持流式、stream_options或工具参数,需要独立确认。FlowBarCN文档也明确要求按具体模型核对能力。本篇没有进行站内流式实测。
10月22日的官方API更新日志涉及另一API的新能力,不表示这些能力自动出现在兼容的Chat Completions端点里。本篇以常青协议处理为主,近期更新只用来提醒核对接口边界。
2. 准备可重复的运行环境
本轮实际验证:Windows、Python 3.11.9、openai==3.35.1、httpx==0.28.1。这是本文验证组合,不宣称它们永远是最新版本。
在空目录的PowerShell里运行:
python -m venv .venv
.venv\Scripts\python.exe -m pip install openai==3.35.1 httpx==0.28.1
安装依赖会访问包仓库,但下面的演示请求由MockTransport在内存中处理,不会访问模型服务。示例使用占位密钥与 example.invalid,不要换成真实密钥来“验证”这段离线代码。
3. 保存完整脚本stream_text.py
"""A single-choice text-stream collector; the demo is fully offline."""
import json
from dataclasses import dataclass
import httpx
from openai import OpenAI
class StreamRejected(ValueError):
"""Fixed local policy code, not an upstream HTTP status."""
@dataclass(frozen=True)
class TextResult:
text: str
finish_reason: str
def collect_text(client, *, model: str, prompt: str,
max_chars: int = 4096) -> TextResult:
if type(max_chars) is not int or max_chars < 1:
raise ValueError("invalid_limit")
pieces, size, finish = [], 0, None
with client.chat.completions.create(
model=model, messages=[{"role": "user", "content": prompt}],
stream=True,
) as stream:
for chunk in stream:
# A usage-only chunk can have choices=[].
if not chunk.choices:
continue
if len(chunk.choices) != 1 or chunk.choices[0].index != 0:
raise StreamRejected("single_choice_only")
if finish is not None:
raise StreamRejected("content_after_finish")
choice = chunk.choices[0]
if choice.delta.tool_calls or choice.delta.function_call:
raise StreamRejected("tool_output_not_supported")
piece = choice.delta.content
if piece is not None:
if not isinstance(piece, str):
raise StreamRejected("invalid_text_delta")
size += len(piece)
if size > max_chars:
raise StreamRejected("text_limit_exceeded")
pieces.append(piece)
if choice.finish_reason is not None:
finish = choice.finish_reason
if finish != "stop":
raise StreamRejected("not_complete")
text = "".join(pieces)
if not text.strip():
raise StreamRejected("empty_text")
return TextResult(text=text, finish_reason=finish)
def demo_client():
# Every request is handled in memory. No socket or real API key is used.
def handle(request):
assert request.url.path == "/v1/chat/completions"
payload = json.loads(request.content)
assert payload["stream"] is True
chunks = []
for delta, reason in [
({"role": "assistant", "content": ""}, None),
({"content": "Hello "}, None),
({"content": "stream"}, None),
({}, "stop"),
]:
item = {
"id": "offline-demo", "object": "chat.completion.chunk",
"created": 0, "model": "DEMO_MODEL",
"choices": [{"index": 0, "delta": delta,
"finish_reason": reason}],
}
chunks.append("data: " + json.dumps(item) + "\n\n")
body = "".join(chunks) + "data: [DONE]\n\n"
return httpx.Response(
200, headers={"content-type": "text/event-stream"},
content=body.encode("utf-8"),
)
return OpenAI(
api_key="PLACEHOLDER_NOT_A_REAL_KEY",
base_url="https://example.invalid/v1", max_retries=0,
http_client=httpx.Client(transport=httpx.MockTransport(handle)),
)
if __name__ == "__main__":
with demo_client() as client:
result = collect_text(
client, model="DEMO_MODEL", prompt="Offline demo",
)
print(result)
运行:
.venv\Scripts\python.exe stream_text.py
本轮演示输出:
TextResult(text='Hello stream', finish_reason='stop')
Hello stream来自脚本写死的SSE数据,不是模型回答,也不是线上网络测试。
4. 为什么这样处理流式块?
4.1 文本读取delta,不读取message
非流式完整结果常从 message.content 读取,流式文本则读取 delta.content。角色块可能没有正文;结束块也可能只有结束原因。判断 piece is not None 后再收集,能避免把空值直接拼进字符串。
4.2 choices为空时,不能直接下标访问
某些用量块可能是 choices=[],所以代码先判断列表。本文没有发送 stream_options,也没有把用量写入 TextResult;测试中人工加入用量块,是为了检查聚合器不会越界,不代表本站一定发送或支持对应参数。
如果业务需要算用量,应该另行解析已确认的字段;没拿到用量不能当成消耗为0。
4.3 让SDK处理SSE和UTF-8分块
一个网络数据块不保证恰好等于一个JSON对象,更不保证刚好在汉字边界结束。这里用SDK解析SSE,不对原始网络字节逐块 json.loads。
测试把SSE字节按1字节切分,故意把多字节中文拆开,最终仍按预期得到完整中文。这验证的是本地SDK解析这组样本,不是对所有服务器实现的兼容承诺。
4.4 收到文字不等于已经完成
本例把 finish_reason == "stop" 作为文本完成策略。结束原因为 length、content_filter、缺少结束原因,统一返回本地错误 not_complete。这几个状态本身含义不同;业务可以进一步分类展示,但不能静默升级成“完整成功”。
[DONE]是流式传输中的结束标记;SDK不会把它当普通文字交给本循环。这个聚合器检查SDK解析出的结束原因,不另外实现原始SSE终止标记检查,也不宣称 stop 已证明业务事实正确。
4.5 本例为什么拒绝工具输出和其他候选?
工具调用的参数增量不是普通回答文本。代码检测到 tool_calls 或旧式 function_call 就拒绝,不帮它执行工具。
同样,本例只接受 index=0 的单个候选。如果以后明确需要多候选,就应按index分别维护缓冲区,不能把多个候选混成一句话。
5. 错误分类和排查顺序
| 本地错误 | 触发条件 | 下一步 |
|---|---|---|
| not_complete | 非stop或没有结束原因 | 标明不完整;结合结束原因检查限制与处理策略 |
| empty_text | 正常结束但没有非空白文本 | 核对期望输出类型,不直接归因网络故障 |
| tool_output_not_supported | 出现工具调用增量 | 转交独立工具流程,或选择确认为纯文本的调用方式 |
| single_choice_only | 非零index或一个块含多个候选 | 明确多候选策略后再扩展 |
| text_limit_exceeded | 累计文本超出本例字符上限 | 调整本地结果预算,不能当作Token上限 |
| content_after_finish | 同一文本候选完成后又收到候选内容 | 留下脱敏协议样本,核对服务端或适配器行为 |
这些固定代码是本文的本地策略,不是上游HTTP状态,也不是OpenAI SDK官方异常名。
网络异常仍可能从SDK抛出,本例不在聚合器里重试。与此前教程一样,重试与重复费用要单独设计,不能在这个循环外无条件再跑一遍。
6. “完整收集后返回”的取舍
为了便于验证,本例把文本保存在内存里,完成后一次返回,没有实现前端逐字显示。如果业务要实时展示,可以增加增量回调,但在最终完成前,应把内容标为临时结果;取消、截断或断连后,保留“不完整”状态。
max_chars=4096只是本地累计文本字符限制,不能限制上游已经生成的Token或费用,也不能代替总时限、事件数、并发限制。过早退出时,with负责关闭这个流的资源;它不保证服务端立即停止计费或任务处理。
把演示接入真实服务前,应核对模型ID、流式支持、费用规则和超时策略。FlowBarCN公开接入地址见当前文档,计费见定价页。本文不包含真实请求入口;真实生成操作可能收费,不能把占位模型DEMO_MODEL直接拿去请求线上。
7. 本轮离线检查结果
配套测试文件(本轮内部验证材料)中,流式部分有9个测试方法,覆盖:
- 角色空块、中文UTF-8按单字节拆分,以及空choices用量块。
- 收到文字却缺少结束原因;没有正常结束原因时直接EOF。
- length和content_filter结束、空文本结果。
- 工具增量、超过本地大小限制、错误候选index、完成后又出现内容。
每组流式样本都断言只发生一次模拟请求、响应流被关闭。工单JSON校验另有7个测试方法,两组共16个方法通过。运行脚本也得到上面的人工SSE样本文本。
上文stream_text.py是完整的离线演示,可单独复制运行。本文报告的16个测试方法为内部回归检查;其中流式部分9个、工单校验7个,不把未公开的测试文件说成可下载资源。
今日练习与下一篇
在聚合器外面为 not_complete 增加一个“显示部分内容但禁止业务提交”的状态设计。先写清楚:部分文本保存在哪里、谁能看到、是否允许重新生成,再决定是否扩展返回类型。不要先加自动重试。
下一篇将继续做接入前预检:如何从精确模型ID出发,区分公开目录、账户可见和实际调用这几层证据。
欢迎提供脱敏后的字段结构或错误分类,不需要提交真实密钥、请求头和完整业务内容。
来源
- OpenAI流式示例:协议教学背景,页面最初发布于2022年,不作为近期新闻。
- Chat Completions流式块参考:delta、choices、结束原因等字段。
- OpenAI Python SDK:SDK调用背景;本文实际验证版本以上面的本地结果为准。
- FlowBarCN文档、定价:站内接入与计费边界,不等于已逐模型实测。
- OpenAI API更新日志:10月22日近期条目核查,未把另一API能力嫁接到本站。
441

被折叠的 条评论
为什么被折叠?



