通过统一 API 接入模型流式输出
流式输出把 chat completion 变成 Server-Sent Events:Token 边生成边渲染,用户边读模型边写。Router One 遵循 OpenAI Chat Completions 规范——把 stream 设为 true,同一条代码路径即可流式调用 GPT、Claude、Gemini、Grok 等模型,具体支持因模型而异。本页覆盖传输格式、经得住生产环境的客户端写法,以及流式绕不开的两个运维问题:超时与计费。
开启流式
把 stream 设为 true。-N 参数让 curl 不做缓冲,分块到达即打印。从 /models 选择支持流式、且详情列出 POST /v1/chat/completions 的对话模型:
curl -N https://api.router.one/v1/chat/completions \
-H "Authorization: Bearer sk-your-router-one-key" \
-H "Content-Type: application/json" \
-d '{
"model": "<model-id-from-/models>",
"messages": [{"role": "user", "content": "写一首俳句"}],
"stream": true
}'网络上实际传输的是什么
响应是 text/event-stream:每条 SSE 行携带一个 chat.completion.chunk 对象,choices[].delta 里是片段——第一块通常带 role,后续块携带 content 片段,最后以字面量 [DONE] 结束:
data: {"object":"chat.completion.chunk","choices":[{"delta":{"role":"assistant"},"index":0}]}
data: {"object":"chat.completion.chunk","choices":[{"delta":{"content":"秋"},"index":0}]}
data: {"object":"chat.completion.chunk","choices":[{"delta":{"content":"月"},"index":0}]}
data: {"object":"chat.completion.chunk","choices":[{"delta":{},"finish_reason":"stop","index":0}]}
data: [DONE]客户端写法
官方 Python SDK 负责解析 SSE,应用仍需记录生成如何结束。先在环境变量中设置 ROUTER_ONE_API_KEY。下面是单个 choice 的纯文本示例:主动请求 usage,在 choices 为空时也保留它,并从响应头读取 request_id。SDK 异常会继续抛出,finally 保留诊断信息,已打印的部分正文也不会丢弃。这里关闭 SDK 自动重试,让再次请求成为显式决定:
from openai import OpenAI
import os
import sys
client = OpenAI(
base_url="https://api.router.one/v1",
api_key=os.environ["ROUTER_ONE_API_KEY"],
max_retries=0,
)
request_id = finish_reason = usage = None
try:
with client.chat.completions.create(
model="<model-id-from-/models>",
messages=[{"role": "user", "content": "写一首俳句"}],
stream=True,
stream_options={"include_usage": True},
) as stream:
request_id = stream.response.headers.get("x-request-id")
for chunk in stream:
if chunk.usage is not None:
usage = chunk.usage.model_dump()
if not chunk.choices:
continue
choice = chunk.choices[0]
if choice.delta.content:
print(choice.delta.content, end="", flush=True)
if choice.finish_reason is not None:
finish_reason = choice.finish_reason
finally:
print(f"\nrequest_id={request_id} finish_reason={finish_reason}",
file=sys.stderr)
print(f"usage={usage}", file=sys.stderr)
if finish_reason is None:
raise RuntimeError("未收到 finish_reason;请保留部分输出。")
if finish_reason != "stop":
print("先检查 finish_reason,再决定能否作为最终答案使用。",
file=sys.stderr)- 收到 finish_reason 后继续读取:仅包含 usage 的最终分块可能稍后到达。流被中断时,这块也可能永远收不到。usage 缺失表示未知,不能据此认定零计费;结算请查看 Dashboard → Logs。
- SDK 会在内部消费 [DONE]。这个示例要求收到 finish_reason 才把文本生成视为已结束,Python 迭代器退出本身不能证明这一点。示例不负责重建工具调用,也不验证答案是否满足应用要求。
超时、取消与长生成
- 分别记录建连、等待响应头、等待首个正文 Token、分块间隔和总耗时。推理模型可能正常停顿,但仅凭没有输出,无法判断它是在思考、排队还是已发生中断。
- 结合模型特性与应用等待预算设置客户端超时,并分别检查 SDK、反向代理和托管平台的限制:即使网关允许长等待,它们也可能先关闭流。避免过短的读取或空闲超时误杀正常推理;应用自己的 deadline 到期时,保留取消原因。
- 取消就是关闭 HTTP 连接——没有专门的 API。把 UI 的停止按钮接到请求的 abort 上即可。取消后怎么计费:在 POST /v1/chat/completions 与 POST /v1/responses 上,网关会继续读取上游最多 5 秒以拿到最终 usage,把请求记为 HTTP 499 client_cancelled,只按上游实际报告的用量计费(窗口内没有拿到最终 usage 时,只计已观察到的用量——通常为零),释放预留的余额,并且绝不会把已取消的请求转移到其他路由重试。
- 指定模型的原生 Responses 流不会因为「等太久」被网关掐断:这条路径没有网关侧的响应头超时或空闲超时,首个事件之前或事件之间的长时间等待,只会因客户端断开、请求 deadline 到期、上游报错或上游关闭流而结束。model:auto 的候选仍保留各自的时间预算。
- 在 Dashboard → Logs 对比端到端耗时,先识别慢请求。要区分网络与模型生成耗时,还需要客户端首字时间和网络测量,仅凭总耗时无法判断。
怎样判断 SSE 响应真的完成了?
HTTP 200 只确认流已打开。要读取对应协议的终止信号,并持续处理错误直到流关闭。Chat Completions 和 Responses 使用不同的事件格式,不要把 Responses 事件交给 Chat Completions 的 delta 解析器。连接意外结束时,先保留部分输出与 request_id,再决定是否重试。
| 信号 | 应用应该怎么处理 |
|---|---|
| Chat Completions:finish_reason = stop | 模型正常结束了这个 choice;仍需继续读取 usage 与可能出现的错误,再接受结果。 |
| Chat Completions:length / content_filter / tool_calls | choice 结束不总是完整答案:length 表示达到 Token 上限,content_filter 表示内容被过滤,tool_calls 表示交给应用的工具循环。 |
| Responses:response.completed | 该响应已完成,读取 output 与 usage;单个 response.output_text.done 或 response.output_item.done 不代表整个响应完成。 |
| Responses:response.failed / response.incomplete | 检查 response.error 或 incomplete_details,保留部分输出,不要标记成已完成。 |
| error 事件、读取失败,或缺少预期终止信号就遇到 EOF | 将结果视为失败或未确认,保留 request_id;重放前先排查,因为此前可能已经产生输出或工具副作用。 |
在请求日志中核对流式用量与结算
已入账的流式请求在 Dashboard → Logs 展示模型、Token、结算费用、延迟和状态,同样受 API Key 的预算约束。核账以记录的 usage、实际结算费用和详情中的折扣为准;等待定价的请求可能暂未出现在列表中。客户界面不展示供应商或中间尝试,仅凭状态也无法知道客户端实际收到多少 Token;排查时请保留流内错误与 request_id。
常见问题
流式会更贵吗?
不会。相同内容下 Token 计费与非流式完全一致——流式只改变响应的交付方式。每请求成本 Trace 展示的字段两种方式相同。
好几秒没有任何输出——请求卡住了吗?
几秒没有输出还不能认定卡住。长 prompt 和推理会延后首个正文 Token,排队与网络问题也可能造成等待。记录收到响应头与首个正文的时间,把客户端超时设置与 Dashboard → Logs 的最终请求对照;需要支持排查时保留 request_id。
工具调用可以流式吗?
可以——工具调用的片段在 delta 分块中逐步到达:累积 id、函数名和参数片段,直到流以 finish_reason 为 tool_calls 结束,然后走正常的工具循环。见 LLM 工具调用指南。
流到一半上游挂了怎么办?
可重试的上游失败在生成尚未展开前可以转移到其他符合条件的路由;一旦你已经在接收 Token,上游硬故障会表现为流中断,客户端应当处理。哪些失败可重试,见自动故障转移页。
流到一半我主动取消了,会扣费吗?
只扣上游实际报告的部分。在 POST /v1/chat/completions 与 POST /v1/responses 上,被取消的请求记为 HTTP 499 client_cancelled:网关最多等 5 秒拿最终 usage,对这部分用量计费(没等到就只计已观察到的用量),释放预留余额,不重试也不故障转移。记录可见后,在 Dashboard → Logs 核对结算金额与适用折扣;客户端没收到 usage 分块,或记录仍在等待定价,都不能证明零计费。
所有模型都支持流式吗?
Router One 遵循 OpenAI Chat Completions 规范,包括流式输出,具体支持程度因底层模型而异。先查 /models 目录里该模型的条目,各功能的最新细节见文档。