三种路径形态下的流式响应

三条路都用 stream: true,各自的事件格式不同。首字节之前网关做了什么,以及为什么中途 abort 不等于不付钱。

同一个开关,三种事件格式

开启流式的方式三条路一样:请求体里写 stream: true。回来的东西不一样,因为每条路各说各的事件格式,网关不会把它们抹平成统一的一种。

/v1/messages 返回 Anthropic 那套事件序列——message_start、content_block_delta、message_delta、message_stop。/v1/chat/completions 返回 chat.completion.chunk 帧,以 data: [DONE] 结束。/v1/responses 返回 Responses 事件流。每一种都是对应 SDK 本来就会迭代的格式。

响应体是直通转发而不是先缓冲,所以首 token 时延没有为了网关自己记账而被牺牲。相对地,你的请求体会被完整读入之后才转发——必须如此,否则无法确定模型、也无法计价。

三种形态各自的流式调用。开关一样,帧格式不一样。
# Anthropic events on /v1/messages
curl -N https://token-share.app/v1/messages \
  -H "x-api-key: $TOKEN_SHARE_KEY" \
  -H "anthropic-version: 2023-06-01" \
  -H "content-type: application/json" \
  -d '{
    "model": "claude-sonnet-5",
    "max_tokens": 512,
    "stream": true,
    "messages": [{"role": "user", "content": "Count to five slowly."}]
  }'

# chat.completion.chunk frames, terminated by [DONE]
curl -N https://token-share.app/v1/chat/completions \
  -H "Authorization: Bearer $TOKEN_SHARE_KEY" \
  -H "content-type: application/json" \
  -d '{
    "model": "grok-4.6",
    "stream": true,
    "messages": [{"role": "user", "content": "Count to five slowly."}]
  }'

# Responses event stream
curl -N https://token-share.app/v1/responses \
  -H "Authorization: Bearer $TOKEN_SHARE_KEY" \
  -H "content-type: application/json" \
  -d '{
    "model": "gpt-5.6-terra",
    "stream": true,
    "input": "Count to five slowly."
  }'

首字节之前发生了什么

流式响应一旦开始返回,状态行就定死了,改不了。所以网关会先等上游发来第一个有内容的 chunk,然后才提交这个响应。如果上游开了 200 text/event-stream 却什么都没发就关了,你收到的是 502,而不是一个既解析不了、又不好重试的「成功」。

换行心跳不算这第一个 chunk——它会被保留下来,好让成功的流逐字节完整,但不会被当作「答案已经开始」的证据。只有携带非空白字节的 chunk 才会提交响应。

这一个 chunk 的等待,就是全部的缓冲代价。之后不再有任何滞留,上游产出多快,body 就流多快。

中途 abort,以及流中失败长什么样

用量统计走的是另一条分支,与你的客户端消费的那条彼此独立。客户端断开之后它照样把流读完,所以赶在最后一个 usage 事件之前 abort,并不能白拿答案——已产出的 token 照计。

上游在流中途断连时,断连前读到的部分会被保留,因此失败之前已报出的正数用量仍然可以计费;若压根没报出任何用量,这次请求记为未计费,而不是拍脑袋估一个数收钱。

状态行一旦提交,再出问题就只能在流内部报告,因为 200 已经在线上了。stable-* 通道上,提交之前的失败会按你所走路径的格式发出错误事件——Anthropic 的 error 事件、OpenAI 的错误帧加 [DONE]、或者 response.failed——让正在迭代的客户端干净收尾,而不是挂在一条已经失败的流上。

常见问题

流式比一次性返回更贵还是更便宜?
一样。计费按流里报出的 usage 逐 token 算,单价和倍率与非流式调用完全相同。流式改变的是你什么时候看到 token,不是它们值多少钱。
流结束了却没有终止事件,客户端该怎么办?
把「没有终止帧就结束」的流当作这一轮失败并重试,不要把半截文本当成答案提交。stable 通道在上游中断时会补一个终止帧,所以等 message_stop 或 [DONE] 的 SDK 不会一直挂着——但直接消费裸流的客户端仍然要自己处理这种情况。
能从流里拿到 usage 吗?
可以,usage 在流自己的收尾帧里,形式取决于你走的路径。chat completions 形态下,开了 stream_options.include_usage 的 OpenAI SDK 会在 [DONE] 之前的一个不带 choice 的 chunk 里找到它。网关自己另读一份,所以你看到的和你被收的钱出自同一组数字。