通过 统一 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 缺失表示未知,不能据此认定零计费;结算请查看控制台 → 日志。
- 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、而流已经输出过内容时,上游没有报告的部分按估算计费(截至 2026-10-13)——输出 Token 按已经流出的文本估算,输入 Token 按请求内容估算;在输出任何内容之前就取消、且没拿到任何 usage 的流不计费。控制台 → 日志显示实际结算的费用,Token 数只记上游报告的部分,所以按估算结算的请求可能显示 0 Token、费用却不为零。
- stream: false 的调用结算方式不同(截至 2026-10-09):在 /v1/chat/completions、/v1/responses 与 /v1/messages 上,关闭连接不会让模型停下——网关会在非流式时间窗内继续等它;模型跑完,就按它报告的用量计费,并把请求记为 HTTP 499 client_cancelled、带上这部分 Token;模型失败或超出时间窗,则不计费。所以长时间的非流式调用即使 SDK 超时,也可能照常计费;SDK 超时后自动发出的重试是另一个独立的请求,也会单独计费——按 openai-python 3.29.0 的 README(2026-10-13 核对),超时的请求默认重试两次。需要几分钟的生成请用 stream: true,或把客户端超时设得比预计生成时间更长。
- 指定模型的原生 Responses 流不会因为「等太久」被网关掐断:这条路径没有网关侧的响应头超时或空闲超时,首个事件之前或事件之间的长时间等待,只会因客户端断开、请求 deadline 到期、上游报错或上游关闭流而结束。model:auto 的候选仍保留各自的时间预算。
- 控制台 → 日志显示每个请求的总耗时,产生过输出的流式请求还显示首字延迟(TTFT)。两者都从网关开始执行请求时起算,因此首字延迟包含故障转移前失败尝试的耗时。客户端测得的首个可见 Token 时间与日志中的首字延迟之差,来自你的网络、客户端和网关接收请求的环节;首字延迟本身就长,则指向排队、故障转移前的失败尝试或模型本身的等待。
怎样 判断 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;重放前先排查,因为此前可能已经产生输出或工具副作用。 |
在请求日志 中核 对流式 用量与 结算
已入账的流式请求在控制台 → 日志展示模型、Token、结算费用、总耗时(流产生过输出时另有首字延迟)和状态,同样受 API Key 的预算约束。核账以记录的 usage、实际结算费用和详情中的折扣为准;等待定价的请求可能暂未出现在列表中。客户界面不展示供应商或中间尝试,仅凭状态也无法知道客户端实际收到多少 Token;排查时请保留流内错误与 request_id。
常见 问题
流式会更贵吗?
不会。相同内容下 Token 计费与非流式完全一致——流式只改变响应的交付方式。每请求 Trace 的 Token 与费用字段两种方式相同;产生过输出的流式请求还会多显示首字延迟。
好几秒 没有 任何 输出——请求 卡住了吗?
几秒没有输出还不能认定卡住。长 prompt 和推理会延后首个正文 Token,排队与网络问题也可能造成等待。记录收到响应头与首个正文的时间,把客户端超时设置与控制台 → 日志的最终请求对照——流产生过输出时,其首字延迟(TTFT)表示从网关开始执行请求到第一段输出用了多久;需要支持排查时保留 request_id。
工具 调用 可以 流式吗?
可以——工具调用的片段在 delta 分块中逐步到达:累积 id、函数名和参数片段,直到流以 finish_reason 为 tool_calls 结束,然后走正常的工具循环。见 LLM 工具调用指南。
流到 一半 上游挂了 怎么办?
可重试的上游失败在生成尚未展开前可以转移到其他符合条件的路由;一旦你已经在接收 Token,上游硬故障会表现为流中断,客户端应当处理。哪些失败可重试,见自动故障转移页。
流到 一半我 主动 取消了,会扣费吗?
如果已经开始输出,会。在 POST /v1/chat/completions 与 POST /v1/responses 上,被取消的请求记为 HTTP 499 client_cancelled(在输出任何内容之前就取消的流可能没有记录):网关最多等 5 秒拿最终 usage 并按它计费;没拿到、而流已经输出过内容时,上游没有报告的部分按已流出的文本与请求内容估算计费(截至 2026-10-13)。在输出任何内容之前就取消、且没拿到任何 usage 的流不计费。预留余额中未用的部分会释放,被取消的请求不会重试或故障转移。记录可见后,在控制台 → 日志核对结算金额与适用折扣:即使 Token 数显示为 0,那里显示的费用也是实际扣费;客户端没收到 usage 分块,或记录仍在等待定价,都不能证明零计费。
所有 模型都 支持 流式吗?
Router One 遵循 OpenAI Chat Completions 规范,包括流式输出,具体支持程度因底层模型而异。先查 /models 目录里该模型的条目,各功能的最新细节见文档。