仅当同时满足以下三个条件时,重试流式请求才是安全的:没有任何内容到达您的客户端;没有任何可观察到的计费发生;且请求不携带任何服务器端状态。在第一个输出事件之后,正确的做法是报告失败,而不是重试。
TokenLab 在其网关上针对 Responses API 流式传输应用了这一规则,涵盖 HTTP 和 WebSocket。WebSocket 路径已于 2026 年 9 月 28 日更改,以与 HTTP 保持一致。
为什么流式请求不同于普通请求
非流式调用会返回一个主体或一个错误。您可以重试该错误,因为您没有收到任何内容。
流式传输在请求完成之前就会向您交付输出。第一个输出事件是“不归路”。如果在此之后连接中断,您将持有部分文本。重试请求意味着再次生成相同的答案并为此付费。您还可能重复执行代理已经运行的工具调用。
TokenLab 流式传输指南对此有直接说明:
在第一个事件到达后,中断的流是不完整的,不会自动重启。
因此,您的客户端需要一个本地状态位:saw_output。当任何输出到达您的代码时,它会翻转为 true。每次重试决策都会首先读取该位。
以非 response.completed 结束的流即为失败。不要假设您拥有的文本是完整的。请处理 response.failed、response.incomplete 和 error 事件。
重试决策逐点分析
当满足以下所有条件时,TokenLab 会在另一个可用路由上重试一次请求:请求是无状态的;没有任何内容到达客户端;对于失败的尝试,没有观察到任何结果或用量;且失败要么是可重试的输出前事件,要么是第一个事件之前的上游读取错误。每个请求最多重试一次。如果替换请求在输出前也失败了,该失败将不会再次重试。
来源:TokenLab 流式传输指南和网关行为,观察于 2026 年 9 月 28 日。
| 失败点 | TokenLab 是否重试? | 原因 |
|---|---|---|
可重试的输出前事件(response.failed,或标记为可重试的 error 事件,例如过载或内部上游错误) |
是,一次,如果请求是无状态的 | 没有任何内容到达客户端且未观察到用量,因此第二次执行是不可见的。 |
| 上游流在第一个事件之前中断(读取错误) | 是,一次,如果请求是无状态的 | 同上。客户端未持有任何输出且未产生费用。 |
| 重试一次后,在输出前再次失败 | 否 | 每个请求的重试预算为一次。 |
| 输出到达客户端后的任何失败 | 否 | 客户端已持有部分文本。重试会导致输出重复并产生额外费用。 |
存储的响应 (store)、延续 (previous_response_id) 或源绑定请求 |
否 | 第二次执行可能会创建第二个存储的响应或导致对话状态分歧。 |
| 第一个事件超时 | 否 | 上游可能仍在生成。重试可能会在第一次尝试继续的同时运行两次相同的工作。 |
| 输出前缓冲区溢出 | 否 | 该限制是网关本地的。相同的超大前缀在下一个路由上极有可能再次触发该限制。 |
| 客户端断开连接 | 否 | 客户端已停止监听。 |
| 确定性失败,例如无效请求 | 否 | 重试无法改变结果。按原样交付。 |
| 已产生用量的失败 | 否 | 该尝试已被计费。按原样交付。 |
| 没有剩余其他路由 | 否 | 没有地方可以发送请求。客户端会收到带有自身代码的失败响应。 |
当失败未被重试,或没有剩余其他路由时,您会收到带有自身错误代码的失败响应。公开示例:当上游流中断时的 stream_read_error,以及缓冲区溢出时的 upstream_stream_buffer_limit。如果路由选择本身在重试决策后失败,WebSocket 会话将以 websocket_response_failed(状态 500)结束,并退还预留费用。
计费遵循相同的原则。您仅为交付的尝试付费。重试的请求可能在上游执行了两次,但该额外的上游成本由 TokenLab 承担,因为第一次尝试没有向您交付任何内容。未交付任何内容的失败会话将被退款。
一个时间细节对您的错误处理很重要。在输出开始之前,网关会持有 response.created 和 response.in_progress,直到第一个输出事件或失败到达(最多 10 秒)。这些持有的事件随后会与第一个输出一起,或与终止事件一起到达。顺序和内容保持不变。您只是稍晚看到它们。这 10 秒是最大值,而非典型延迟。
WebSocket 在 2026 年 9 月 28 日的变化
TokenLab 通过 HTTP 流式传输("stream": true,服务器发送事件)和 WebSocket(wss://api.tokenlab.sh/v1/responses,客户端发送 response.create 事件)提供 Responses API 服务。WebSocket 响应始终是流式的。它们不支持 background 或 response.cancel。每个连接一次处理一个活跃响应,最长持续 60 分钟。
在更改之前,这两条路径的行为不一致。HTTP 会持有生命周期事件并重试无状态的输出前失败。WebSocket 则立即转发 response.created 并将输出前失败交付给客户端,同时进行退款。相同的上游故障在 HTTP 上会产生干净的答案,而在 WebSocket 上则产生错误。
现在 WebSocket 路径遵循 HTTP 规则,包括重试在任何事件到达前中断的流。在内部,WebSocket 会话中看到的大多数上游失败都发生在任何输出之前。这正是重试安全的窗口期。
网关改进了输出前失败的情况。它不保证流一定能完成。
该更改如何在不破坏其他行为的情况下发布
这项工作遵循了一个旨在捕获静默行为变化的流程。
- 行为锁定。在更改之前,每个 WebSocket 会话场景都被记录为固定装置(fixture):客户端接收的帧、发出的上游调用以及计费结果。在此期间,测试套件增加到 63 个记录的场景。行为更改必须提前声明。只有在该声明中命名的固定装置可以更改。所有其他固定装置必须保持字节相同。
- 变异检查。每个新的决策规则都通过故意翻转它来进行测试,例如重试第一个事件超时或不重试读取器失败,并确认锁定失败。
- 审查捕获。第一个版本还使缓冲区溢出情况可重试,理由是与 HTTP 对齐。审查表明,由于表中的原因,HTTP 从不重试该情况。后续版本恢复了旧行为并添加了边界场景:第二次读取器失败不重试、没有剩余路由、在持有
response.created后的失败,以及随后中断的替换流。
现在,重试后成功的会话请求日志也会记录之前失败的尝试,正如 HTTP 已经做的那样。
拥有重试决策权的客户端代码
将流式调用的 SDK 自动重试设置为 0。这会将决策权保留在您的代码中。将重试决策保持在一个地方,而不是分散在各个处理程序中。对于 HTTP 错误,请遵守 错误处理指南 中描述的 retryable 和 retry_after,并保留请求 ID。
HTTP 上的 SSE
import os
from openai import OpenAI
with OpenAI(
api_key=os.environ["TOKENLAB_API_KEY"],
base_url="https://api.tokenlab.sh/v1",
timeout=30.0,
max_retries=0, # 拥有重试决策权,而不是重新发送半读取的流
) as client:
completed, saw_output = False, False
with client.responses.create(
model="gpt-5.6-terra",
input="Reply with one short sentence about retries.",
stream=True,
) as stream:
for event in stream:
if event.type == "response.output_text.delta":
saw_output = True
print(event.delta, end="", flush=True)
elif event.type == "response.completed":
completed = True
elif event.type in {"response.failed", "response.incomplete", "error"}:
raise RuntimeError(f"{event.type} after_output={saw_output}")
if not completed:
raise RuntimeError(f"stream closed before response.completed, after_output={saw_output}")
print()
该示例使用 OpenAI SDK 2.15.0 对接 https://api.tokenlab.sh/v1,并将 max_retries=0。它跟踪 saw_output,并在 response.failed、response.incomplete 和 error 事件以及在 response.completed 之前关闭的流上引发异常。已于 2026 年 9 月 28 日使用 gpt-5.6-terra 在生产环境中验证。
如果失败到达时 saw_output == false 且请求符合条件(无状态,具有可重试的失败),TokenLab 已经重试了一次;存储的响应、延续和第一个事件超时根本没有被重试。在应用层面决定是否可以接受新的请求,因为新的请求是一次新的生成。如果 saw_output == true,则报告失败并显示您已有的内容,或者故意丢弃部分文本。
WebSocket
import asyncio
import json
import os
import websockets
URL = "wss://api.tokenlab.sh/v1/responses"
TERMINAL = {"response.completed", "response.failed", "response.incomplete", "error"}
async def run_turn(prompt: str) -> str:
headers = {"Authorization": f"Bearer {os.environ['TOKENLAB_API_KEY']}"}
async with websockets.connect(URL, additional_headers=headers, max_size=None) as ws:
await ws.send(json.dumps({
"type": "response.create",
"model": "gpt-5.6-terra",
"input": prompt,
"store": False,
}))
text, saw_output = [], False
async for raw in ws:
event = json.loads(raw)
kind = event.get("type")
if kind == "response.output_text.delta":
saw_output = True
text.append(event["delta"])
elif kind in TERMINAL:
if kind != "response.completed":
# 输出开始后,失败对于本次会话是最终的。
# 仅当您的应用可以丢弃部分文本时才重新发送。
raise RuntimeError(f"{kind} after_output={saw_output}: {json.dumps(event)[:300]}")
return "".join(text)
raise RuntimeError(f"socket closed before a terminal event, after_output={saw_output}")
print(asyncio.run(run_turn("Reply with one short sentence about retries.")))
该示例使用 websockets 16.0,连接到 wss://api.tokenlab.sh/v1/responses 并带有 Bearer 标头,发送一个 store: false 的 response.create,并收集 response.output_text.delta。它在任何非完成的终止事件或提前关闭时引发带有 after_output 的异常。已于 2026 年 9 月 28 日使用 gpt-5.6-terra 在生产环境中验证。
after_output 标志与 saw_output 的概念相同。它告诉您的调用代码在不重复副作用的情况下是否还可以进行新的会话。
您自己的重试逻辑检查清单
- 每次都将没有
response.completed结束的流视为失败。 - 跟踪一个布尔值,用于判断输出是否到达您的代码。在第一个输出事件上翻转它,而不是在第一个生命周期事件上。
- 符合条件的请求的输出前失败已经由网关重试过一次;进一步的尝试由您决定。
- 在部分输出后,仅当您的应用可以丢弃部分文本并接受为两次生成付费时才重新发送。
- 在代理循环中,检查部分流是否已经包含您的代码已执行的工具调用。不要重试您无法撤销副作用的会话。
- 对于存储的响应和
previous_response_id延续,在重新发送任何内容之前检查存在什么状态。 - 在您的 SDK 中将流式重试设置为 0,并将重试决策保持在一个函数中。
- 记录请求 ID,以便您可以将交付的答案与背后的尝试进行匹配。
常见问题解答
TokenLab 会在部分输出后重启流吗?
不会。一旦输出到达您的客户端,失败就会被报告且永远不会重试。您持有部分文本,因此重启会导致输出和成本重复。您的应用决定是否显示、截断或丢弃已有的内容。
如果网关重试我的请求,我会两次被收费吗?
不会。您仅为交付的尝试付费。重试的请求可能在上游执行了两次,但第一次尝试没有向您交付任何内容,该额外的上游成本由 TokenLab 承担。未交付任何内容的失败会话将被退款。
为什么第一个事件超时不重试?
因为上游可能仍在生成。重试可能会在第一次尝试继续的同时运行两次相同的工作。第一个事件超时与在第一个事件之前中断流的读取错误处理方式不同。
我可以重试存储的响应或 previous_response_id 延续吗?
不能自动重试。TokenLab 从不重试存储的响应、延续或源绑定请求,因为第二次执行可能会创建第二个存储的响应或导致对话状态分歧。在重新发送任何内容之前检查存在什么状态,并且仅在您的应用可以协调该状态时才重新发送。
如果您想亲自查看原始事件流,请创建 API 密钥并记录您的客户端收到的每种事件类型。流式传输指南和错误处理指南涵盖了完整的事件集。有关网关如何路由和恢复的背景信息,请参阅 TokenLab AI API 可靠性基础设施 和 Responses API 与代理聊天补全对比。
来源
- https://docs.tokenlab.sh/guides/streaming资料更新于 2026-09-28
- https://docs.tokenlab.sh/guides/error-handling资料更新于 2026-09-28



