请求输出上限
Sunra 会把您传入的上限转发给上游 provider。响应因触及该上限而停止时,会返回
finish_reason: "length"(Chat Completions 与 Responses)或 stop_reason: "max_tokens"(Messages)——与您直接调用 provider 得到的信号一致。
在 Chat Completions 上,部分 provider 只认
max_tokens 而忽略 max_completion_tokens——于是您以为设了上限,实际得到的是无上限的补全。对于 deepseek/ 系列模型,网关会代您把 max_completion_tokens 翻译为 max_tokens。两个字段同时传入时,取较小者生效,因为两者都是上限语义。原生支持 max_completion_tokens 的 provider 不做改写,因此您设置的上限只会在一处被执行一次。模型输出上限
每个模型都有自己的最大输出长度,与您请求的数值无关。请求超过模型允许的值会被拒绝,而不是被静默削平——上游 provider 返回400,并在消息中给出合法区间。例如 deepseek/deepseek-v4-flash 与 deepseek/deepseek-v4-pro 最高接受 393,216 个输出令牌,超出即拒绝:
400 是刻意设计。被静默降低的上限与「正常跑到上限」无法区分:两者都报 finish_reason: "length",usage 里也没有任何字段能告诉您发生的是哪一种。
输出上限因模型而异,并随 provider 发布新版本而变化。请把 400 当作权威答案,不要在客户端里硬编码某个上限值。
非流式请求:边缘超时
api-llm.sunra.ai 经由 CDN 边缘对外提供服务,该边缘会放弃迟迟无法送达的响应。对于非流式请求,计时器到期时仍在生成的请求会在边缘被终止,网关来不及对它做出任何说明。取决于时机与连接复用情况,这次切断会表现为以下之一:
- HTTP
524,无响应体 - 连接在没有任何响应的情况下被关闭(
RemoteDisconnected/ECONNRESET) - TLS 层 EOF 且没有
close_notify(SSL: UNEXPECTED_EOF_WHILE_READING)
stream: true 发送。**流式请求下,即使 provider 尚未响应,网关也会在约 60 秒内作出响应——见 provider 作出响应之前——随后持续输出字节,边缘计时器因此永远不会触发;长生成转而受下方宽松得多的流生命周期限制约束。作为估算参考:模型通常每秒产出 30–80 个令牌,因此数千量级的输出上限——或先长时间思考再吐出第一个输出令牌的推理模型——都无法塞进这个窗口。
如果您的应用希望把整个响应作为单个对象取得,也请照样使用流式,并在客户端把增量拼装起来;流的最后几个事件携带的 finish_reason 与非流式下您会收到的完全一致。
provider 作出响应之前
推理模型在思考完成之前经常什么都不发——不只是没有第一个令牌,连 HTTP 响应头都没有。本网关实测到的「首个响应头」耗时从几秒到超过六分钟不等(最重的推理请求)。 流式请求下,网关不再为此静默等待。如果 provider 在约 60 秒内仍未响应,网关会自行提交响应:: keepalive 注释,直到 provider 开始产出。您的客户端看到的是一条正常的、只是短暂安静的 SSE 流;provider 自己的帧会在到达时接上。
这带来一个需要在设计上考虑的后果:一旦这个 200 已经上线,错误就不可能再是 HTTP 状态码——因此提交之后到达的 provider 错误会在流内投递,形式是一个携带 prediction_id 和 provider 原始状态码 upstream_status 的 error 帧:
/v1/messages 上,同样的内容以 Anthropic 方言到达:
error 对象的帧当作终止帧处理。
前 60 秒内到达的错误保持不变:它们仍然是原生 HTTP 状态码加 provider 的响应体,与此前完全一致。实际上绝大多数错误都属于这一类——会拒绝请求的 provider 通常在毫秒级就拒绝了。
非流式请求完全不受此影响,仍然只受 边缘超时 约束。
流生命周期
流式请求(stream: true)会经过三个阶段,每个阶段各有一条限制:
这三个阶段是顺序发生的;而且「响应头」与「第一个输出令牌」的区别很重要:120 秒空闲超时与 14 分钟上限都从 provider 的响应头到达时起算,而不是从请求发出时起算。因此一个思考了八分钟才发出响应头的 provider,之后仍然拥有完整的 14 分钟流时长。
空闲超时只针对已经沉默的 provider;正在输出令牌的流不会因为慢而被切断。生命周期上限则是绝对的:它对正在活跃投递的流同样生效——这正是它存在的意义,它是对「永不终止的生成」的兜底。
非流式请求不受上述任何一条约束;网关会给整个请求最多 6 分钟,不过上方的 边缘超时早在此之前就已将其切断。
高强度推理任务
对于openai/gpt-6-astra,复杂提示词搭配 reasoning_effort: "xhigh" 可能超过约 10 分钟(630 秒)的响应头等待上限。若需要更可预测的完成时间,建议使用 high 或简化任务。网关不保证所有高强度推理任务都能在时限内完成。
推理和可见文本共用输出 token 额度。请为 max_completion_tokens(Chat Completions)、max_output_tokens(Responses)或 max_tokens(Messages)预留足够的推理及答案空间;额度过小可能导致尚未输出答案就结束。即使没有可见答案,上游报告的推理用量仍可能计费。
在 /v1/responses 上,较早收到 response.created 并不代表答案已经生成。独立的 14 分钟流生命周期从上游响应头到达起算,也包含该事件之后的推理时间。
保活注释
流处于打开状态但 provider 尚无产出时,网关大约每 20 秒写入一行注释,使这条连接在我们之间的网络看来不会显得空闲:: 开头的行是注释,必须被忽略,主流客户端也都是这样处理的——OpenAI 与 Anthropic 官方 SDK 会在您看到之前就将其丢弃。只有在您手写解析器时才需要考虑这一点:跳过所有以 : 开头的行,就像您已经在跳过空行一样。
该保活不携带任何含义。它既不代表 provider 仍然存活,也不会重置下文的空闲超时。
中止帧
任一限制触发时——或流中途上游连接故障时——网关不会裸断连。它会把仍持有的内容刷出,按您所调用 endpoint 的 SSE 方言发送一个 error 帧,然后干净地关闭流。该帧之前已投递的所有内容都是有效输出,可以照常使用。 在/v1/chat/completions 与 /v1/responses 上:
[DONE] 哨兵只属于 Chat Completions 方言;Responses 流在 error 帧之后直接结束,没有 [DONE]。
在 /v1/messages 上,该帧遵循 Anthropic 方言——一个带类型的 error 事件,且没有 [DONE]:
type 恒为 gateway_stream_aborted;code 在所有方言上都存在,指明发生了什么:
server_shutdown 可能出现在任何输出之前,也可能出现在生成途中。请保留已收到的部分输出;重试会发起新请求,不会接着原输出继续。计费遵循下方中断流的规则。若网关尚未提交响应头(包括非流式请求),关闭时返回 HTTP 503,携带 error.type: "gateway_error" 和 error.code: "server_shutdown"。关闭开始后被拒绝的新请求也收到相同状态码和 code。
upstream_empty_stream 是唯一一个既非限制触发、也非连接故障的 code:流干净地结束了,只是空的。由于没有可保留的部分输出,该帧携带的 message 也不同——Upstream returned an empty stream: the response ended before any billable output or terminal usage was received——它就是这次请求全部的可见结果。
最后三个 code 只会出现在 provider 作出响应之前就已被提交 的请求上,因此按构造它们之前不存在任何部分输出;帧里的 message 也是这么说的——Upstream failed before the stream started (<code>); no output was produced。
提交之后投递的 provider 错误则是另一种形状:它保留 provider 自己的 type 与 code(而不是 gateway_stream_aborted),并附加 upstream_status。用是否存在 upstream_status 来区分「provider 拒绝了这次请求」与「网关结束了这条流」。
既没有中止帧、也没有正常终止事件的流,是在传输层被切断的。只要与您之间的连接仍然可写,网关就会写出中止帧;但它无法在所有情况下都做到这一点:如果连接本身已经消失——socket 错误,或某个中间环节放弃了一条空闲连接——那就没有任何地方可供写入了。
因此,被截断且没有中止帧的流应视为连接被中断,而不是一次完成的响应,也不能据此认定问题出在您这一侧。切断之前已投递的内容仍然是有效输出。
被中止的流如何计费
被中止的流按已投递给您的内容计费,绝不会按 provider 内部可能继续生成的完整内容计费。- 若中止前 provider 已上报最终 usage,则按该上报计费。
- 否则按保守估计结算已投递的输出——大约每 4 个字符折算 1 个令牌,并以您请求的输出上限封顶。该笔记录会标记
usage_source: "conservative_estimate"备查。 - 若没有投递任何内容,或 provider 上报了明确的错误,请求标记为失败并释放预留的额度。失败的请求不计费。