流式响应

请求中设置 "stream": true 后,Chat Completions、Responses 和 Anthropic Messages 三种对话接口都以 SSE(Server-Sent Events)逐段返回内容。三种协议的事件格式、结束标记和 usage 的位置各不相同,客户端要按所用协议解析,并以协议的结束标记判断输出是否完整。本章讲协议格式与客户端实现;流式中断、超时的排查见超时与流式中断。

开启流式

接口 开启方式 响应 Content-Type
Chat Completions(/v1/chat/completions) "stream": true text/event-stream
Responses(/v1/responses) "stream": true text/event-stream
Anthropic Messages(/v1/messages) "stream": true text/event-stream
  • 请求在开始输出之前就被拒绝时(鉴权失败、余额不足、模型不可用等),返回的是普通的 HTTP 错误响应,不是事件流:网关拒绝的请求是 JSON 错误体(/v1/messages 为 Anthropic 错误格式),500 / 504 是 HTML 页面。解析事件流之前先检查状态码和 Content-Type。
  • 流式输出一旦开始,网关不再换通道;上游失败时的处理见下文「中途失败」。

SSE 的基本格式

一个事件由若干行组成,以一个空行结束:

event: <事件名>
data: <JSON 或文本>

  • data: 行是事件内容;同一事件有多行 data: 时,按换行拼接。Chat Completions 只有 data: 行,Responses 和 Anthropic Messages 还有 event: 行给出事件名。
  • 冒号后的空格可有可无:data: {...} 与 data:{...} 按 SSE 规范都合法,实际的流中两种都会出现,自己写解析器时要兼容。
  • 还可能有 id: 等其他字段行,例如 Responses 流的每个事件前都有一行 id:,不需要时忽略即可。
  • 以 : 开头的行是注释,部分上游用它保持连接,解析时忽略。
  • 网络上的一个数据块可能只包含半个事件,也可能包含多个事件。请把数据累积到缓冲区,遇到空行再处理一个完整事件。

Chat Completions

每个事件是一行 data: <chat.completion.chunk>,最后以 data: [DONE] 结束:

data: {"id":"chatcmpl-9f1c2a7e4b","object":"chat.completion.chunk","created":1791950400,"model":"MODEL_ID","choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null}]}

data: {"id":"chatcmpl-9f1c2a7e4b","object":"chat.completion.chunk","created":1791950400,"model":"MODEL_ID","choices":[{"index":0,"delta":{"content":"秋风起,"},"finish_reason":null}]}

data: {"id":"chatcmpl-9f1c2a7e4b","object":"chat.completion.chunk","created":1791950400,"model":"MODEL_ID","choices":[{"index":0,"delta":{"content":"落叶黄。"},"finish_reason":null}]}

data: {"id":"chatcmpl-9f1c2a7e4b","object":"chat.completion.chunk","created":1791950400,"model":"MODEL_ID","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}

data: {"id":"chatcmpl-9f1c2a7e4b","object":"chat.completion.chunk","created":1791950400,"model":"MODEL_ID","choices":[],"usage":{"prompt_tokens":18,"completion_tokens":12,"total_tokens":30,"prompt_tokens_details":{"cached_tokens":0}}}

data: [DONE]
内容 位置
正文增量 choices[0].delta.content
思考过程增量 choices[0].delta.reasoning_content(个别上游为 delta.reasoning)
工具调用增量 choices[0].delta.tool_calls,按 index 拼接,见工具调用
结束原因 倒数几个 chunk 中的 choices[0].finish_reason
用量 choices 为空数组的那个 chunk 的 usage
结束标记 data: [DONE]
  • 网关总会把 stream_options 设为 {"include_usage": true},所以一定有带 usage 的 chunk,它的 choices 是空数组。读取 choices[0] 前先判断数组非空。
  • 由 Anthropic 协议通道服务时,chunk 由网关转换生成:带 usage 的 chunk 只有 prompt_tokens、completion_tokens、total_tokens,其中 prompt_tokens 不含缓存命中,缓存数量请在请求日志中查看。
  • 字段的含义与日志四项 Token 的对应关系见 Chat Completions。

Responses

每个事件带事件名,data 中的 type 与事件名相同,sequence_number 递增。每个事件前还有一行 id:,下面的片段省略了它:

event: response.created
data: {"type":"response.created","sequence_number":0,"response":{"id":"resp_68f1a2b3","object":"response","created_at":1791950400,"status":"queued","model":"MODEL_ID","output":[],"usage":null}}

event: response.output_item.added
data: {"type":"response.output_item.added","sequence_number":2,"output_index":0,"item":{"id":"msg_68f1a2c4","type":"message","status":"in_progress","role":"assistant","content":[]}}

event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":4,"item_id":"msg_68f1a2c4","output_index":0,"content_index":0,"delta":"秋风起,"}

event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":5,"item_id":"msg_68f1a2c4","output_index":0,"content_index":0,"delta":"落叶黄。"}

event: response.output_text.done
data: {"type":"response.output_text.done","sequence_number":6,"item_id":"msg_68f1a2c4","output_index":0,"content_index":0,"text":"秋风起,落叶黄。"}

event: response.completed
data: {"type":"response.completed","sequence_number":9,"response":{"id":"resp_68f1a2b3","object":"response","status":"completed","model":"MODEL_ID","output":[{"id":"msg_68f1a2c4","type":"message","status":"completed","role":"assistant","content":[{"type":"output_text","text":"秋风起,落叶黄。","annotations":[]}]}],"usage":{"input_tokens":18,"input_tokens_details":{"cached_tokens":0},"output_tokens":12,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":30}}}
事件 含义
response.created、response.in_progress 响应已创建、正在生成。response.created 中的 status 可能是 queued
response.output_item.added / .done 一个输出项开始 / 结束,类型为 message、reasoning、function_call 等
response.content_part.added / .done 消息中的一个内容块开始 / 结束
response.output_text.delta / .done 文本增量(delta)/ 该内容块的完整文本(text)
response.reasoning_text.delta / .done 推理文本增量 / 完整推理文本,推理模型会返回
response.function_call_arguments.delta / .done 函数调用参数增量 / 完整参数,见工具调用
response.completed 正常结束,response.usage 是本次用量
response.incomplete 未完整生成就结束,例如达到 max_output_tokens
response.failed、error 生成失败、上游报告错误
  • 网关原样转发通道返回的 Responses 事件,不追加 stream_options,也通常没有 data: [DONE]。
  • 用量在 response.completed 的 response.usage 中:input_tokens 包含缓存命中,命中数量在 input_tokens_details.cached_tokens。

Anthropic Messages

event: message_start
data: {"type":"message_start","message":{"id":"msg_01XFDUDYJgAACzvnptvVoYEL","type":"message","role":"assistant","model":"MODEL_ID","content":[],"usage":{"input_tokens":18,"output_tokens":0}}}

event: content_block_start
data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}

event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"秋风起,"}}

event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"落叶黄。"}}

event: content_block_stop
data: {"type":"content_block_stop","index":0}

event: message_delta
data: {"type":"message_delta","delta":{"stop_reason":"end_turn"},"usage":{"input_tokens":18,"output_tokens":12}}

event: message_stop
data: {"type":"message_stop"}
事件 含义
message_start 消息开始,message.usage 中有输入 Token 数
content_block_start 一个内容块开始,类型为 text、thinking 或 tool_use
content_block_delta 增量:text_delta(正文)、thinking_delta(思考)、signature_delta(思考块签名)、input_json_delta(工具参数)
content_block_stop 内容块结束
message_delta delta.stop_reason(end_turn、max_tokens、tool_use 等)和最终 usage
message_stop 正常结束
ping 保持连接,忽略即可
error 错误
  • 以 message_delta 中的 usage 为准。由 OpenAI 兼容通道服务时,事件由网关转换生成,message_start 中的 input_tokens 为 0,最终数量只在 message_delta 中。有缓存读取或写入时,message_delta.usage 还分别带 cache_read_input_tokens、cache_creation_input_tokens。
  • input_tokens 不含缓存读取和缓存写入,计算方法见调用费用与缓存计费。

结束标记与 usage 一览

接口 正常结束 usage 在哪里 中途失败的错误事件
Chat Completions data: [DONE] choices 为空数组的 chunk data: {"error": {...}}
Responses response.completed 事件 response.completed 的 response.usage response.failed 事件
Anthropic Messages message_stop 事件 message_delta 的 usage event: error

没有收到正常结束标记就断开的流,一律按失败处理。

中途失败

流式输出开始后上游中断或超时,网关不换通道,在流的末尾补一个错误事件后结束连接:

data: {"error": {"message": "upstream timed out after the response had started", "type": "api_error", "code": "stream_interrupted"}}
event: error
data: {"type": "error", "error": {"type": "api_error", "message": "upstream timed out after the response had started"}}
  • 第一种用于 Chat Completions,第二种用于 Anthropic Messages。Responses 补的是 response.failed 事件,response.error.code 为 server_error,sequence_number 接着流中已发出的事件编号:
event: response.failed
data: {"type": "response.failed", "sequence_number": 42, "response": {"id": "resp_...", "object": "response", "status": "failed", "error": {"code": "server_error", "message": "upstream timed out after the response had started"}}}
  • 超时时 message 为 upstream timed out after the response had started,连接或读取失败时为 upstream failed after the response had started。
  • 出现错误事件后不会再有 data: [DONE]、message_stop 或 response.completed。上游自己在流中发出的错误事件(例如 Anthropic 的 overloaded_error)会按你调用的协议转给你。
  • OpenAI、Anthropic 官方 SDK 收到对应格式的错误事件会抛出异常。日志中这类请求的状态是 200,原因和重试建议见超时与流式中断。

客户端实现要点

  • 按完整事件解析:累积数据、遇到空行再处理;拼接多行 data:;忽略注释行和不认识的事件;data: [DONE] 不是 JSON,要先判断再解析。
  • 以结束标记判断完整:收到结束标记才把结果当作完整输出;收到错误事件或连接提前关闭,都按失败处理,重试前做好去重(例如已经执行过的工具操作)。
  • 超时:网关等待上游时,两段数据之间最多等 6 分钟。客户端的读取超时设得比 6 分钟长,并优先使用两次读取之间的空闲超时,而不是整次请求的总超时。
  • 代理不缓冲:应用与 LYRouter 之间有 Nginx 等反向代理时,关闭响应缓冲(Nginx 设置 proxy_buffering off),并把代理的读超时设得比 6 分钟长,否则内容会攒成一大块到达,或被代理提前断开。
  • 命令行测试:curl 加 -N 关闭输出缓冲,才能看到逐段到达的事件。

下面是不依赖 SDK、手动解析 Chat Completions 事件流的写法,可作为其他语言实现的参考:

curl -N https://api.lyrouter.com/v1/chat/completions \
  -H "Authorization: Bearer $LYROUTER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model":"MODEL_ID","stream":true,"messages":[{"role":"user","content":"写两行关于秋天的诗"}]}'

使用官方 SDK 时,SDK 已经处理了事件解析和结束标记,用法见 Chat Completions 的流式示例;Responses 与 Anthropic SDK 的流式写法见 Responses 和 Anthropic Messages。

常见问题

最后一个 chunk 的 choices 为什么是空数组?

答: 网关总会开启 include_usage,上游在结束前单独发送一个只带 usage 的 chunk,它的 choices 为空。代码中直接读取 choices[0] 会出错,请先判断数组非空。这个 chunk 无法关闭。

stream: true 却收到了一个 JSON 错误,而不是事件流?

答: 请求在开始输出之前就被拒绝了,例如鉴权失败、余额不足或模型不可用,网关按普通响应返回状态码和错误体。按状态码处理,见错误码与请求标识。

流式输出很久才一次性出现?

答: 通常是中间的代理缓冲了响应。用 curl -N 直接请求 LYRouter 对比;直连正常、经过代理不正常时,关闭代理的响应缓冲。如果直连也要等很久才有第一段内容,在请求日志中查看「首 Token」,见超时与流式中断。

没有收到结束标记,已经收到的内容能用吗?

答: 不能当作完整结果。网关在中途失败时补发错误事件,上游直接断开时可能连错误事件都没有。请把这次调用视为失败,按需重试,并处理好重复内容和已执行工具的去重。