三种路径形态下的流式响应
三条路都用 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——让正在迭代的客户端干净收尾,而不是挂在一条已经失败的流上。