[Bug]: Stream cut after finish_reason but before the include_usage chunk is logged as success with locally counted usage (cached_tokens lost

在 LiteLLM 中,[Bug]: Stream cut after finish_reason but before the include_usage chunk is logged as success with locally counted usage (cached_tokens lo

快速结论:在 LiteLLM 中,[Bug]: Stream cut after finish_reason but before the include_usage chunk is logged as success with locally counted usage (cached_tokens lost) 通常出现在 OpenAI 兼容上游(如自建 vLLM)已发出 finish_reason 但未发出 include_usage 的 usage chunk 时连接被断开,LiteLLM 仍将流判定为成功并用本地 tokenizer 估算用量。优先排查上游/网关是否在 finish_reason 与 usage chunk 之间提前关闭连接,以及是否可开启 continuous_usage_stats。

适用环境:Issue 中确认:LiteLLM(当前 main,a2773bdcad 复现);上游为自托管 vLLM,经 OpenAI 兼容网关以 openai/<id> + api_base 注册;问题仅出现在流式 /chat/completions,约 1% 请求出现,所有模型均受影响。评论提到 openai SDK 流式层与 httpx/aiohttp 两种 transport 下可复现。其他 Python、CUDA、显卡版本 Issue 未提供。

最快修复方案:暂无确认的一步修复方案(该 Issue 已以“LiteLLM 行为符合预期”关闭,未合并代码)。Issue 给出的缓解方向在上游:若使用 vLLM,可优先尝试在请求中设置 stream_options: {"include_usage": true, "continuous_usage_stats": true},让每个内容 chunk 携带运行中的 usage,LiteLLM 取最后看到的一份;同时排查负载均衡或 sidecar 的空闲超时,避免在 finish_reason 与 usage chunk 之间断连。

注意事项:Issue 作者反馈 continuous_usage_stats 在最终 usage chunk 之前不会带上缓存 token 数,因此 cached_tokens 的丢失需在 vLLM 侧解决;该方案只减少“被截断后无 usage”的窗口,并不能恢复引擎从未上报的缓存命中数据。关闭结论明确:若确认 usage chunk 实际已到达 LiteLLM 却被丢弃,可重新打开该 Issue。

问题场景

用户在 LiteLLM 代理层对接自托管 vLLM(经 OpenAI 兼容网关,模型以 openai/<id> 加 api_base 注册),对 /chat/completions 发起流式请求,并请求 stream_options: {"include_usage": true}。正常顺序是:最后的 content chunk → 带 finish_reason 的 chunk → 独立的 choices: [] usage chunk(含 prompt_tokens_details.cached_tokens)。在上游于 finish chunk 与 usage chunk 之间断连时,LiteLLM 仍把该流记为 success,并用 ChunkProcessor.calculate_usage 回退到 token_counter(model=…, messages=…) 估算用量,prompt_tokens_details 为 None,cached_tokens 读作 0,整段 prompt 按未缓存输入价计费。

报错原文

[Bug]: Stream cut after finish_reason but before the include_usage chunk is logged as success with locally counted usage (cached_tokens lost, billed at full input rate)

status=success
prompt_tokens_details: null

评论补充:若 openai SDK 将中途关闭连接包装为 openai.APIConnectionError,CustomStreamWrapper.__anext__ 中的 except (httpx.ReadError, httpx.RemoteProtocolError) 分支不会触发,流会向客户端抛出 MidStreamFallbackError,即使内容与 finish_reason 已经全部到达。

原因分析

Issue 讨论后的结论是:这并非 LiteLLM 的计费或解析缺陷,而是上游在写完 usage chunk 之前关闭了 socket,真实的 286902 prompt / 285776 cached 数字从未以任何形式到达 LiteLLM。此时客户端已拿到完整回答和停止原因,LiteLLM 选择干净结束流;但本地 tokenizer 无法重建引擎从未上报的缓存命中,因而出现 cached_tokens 归零、按全价计费的现象。可能原因包括负载均衡或 sidecar 的空闲超时在 finish_reason 与 usage chunk 之间触发断连。评论中提到的 SDK 包装异常路径(向客户端报 MidStreamFallbackError)属于同一断连场景的另一种表现,Issue 未采纳改动。

环境排查

  • 确认 LiteLLM 版本,并与 Issue 中复现的 main(a2773bdcad)比对行为。
  • 确认上游是否为 vLLM,以及是否经网关或 api_base 转发。
  • 检查请求是否携带 stream_options: {"include_usage": true},以及是否可加 continuous_usage_stats。
  • 检查客户端与上游之间的负载均衡、反向代理、sidecar 的超时配置,确认是否在 finish_reason 与 usage chunk 之间关闭连接。
  • 对比 vLLM 引擎日志,确认该请求是否命中前缀缓存,以区分“引擎计数错误”和“usage 未送达”。

解决步骤

  1. 先用 vLLM 日志核对该类请求在引擎侧确实命中缓存,确认问题属于 usage chunk 未送达,而非 LiteLLM 计数错误。
  2. 排查并调整上游链路中的空闲超时(负载均衡、反向代理、sidecar),避免在 finish_reason 与 usage chunk 之间断连。
  3. 若使用 vLLM,可优先尝试在请求中加入 stream_options: {"include_usage": true, "continuous_usage_stats": true},让内容 chunk 也携带运行中的 usage,LiteLLM 会取最后看到的一份。
  4. 注意 continuous_usage_stats 在最终 usage chunk 之前不包含缓存 token 数,cached_tokens 的准确上报仍需在 vLLM 侧跟进。
  5. 若发现 usage chunk 确实已到达 LiteLLM 却被丢弃,按 Issue 关闭说明重新打开并附上抓包或日志证据。

验证方法

开启 continuous_usage_stats 并修复上游超时后,重复同一长上下文流式请求,检查 LiteLLM 记录的 prompt_tokens_details 不再为 null、cached_tokens 与 vLLM 引擎日志一致,且 SpendLogs 中的计费与 provider 上报的 usage 相符;同时确认客户端收到的流以 finish_reason 正常结束、不再出现 MidStreamFallbackError。

参考来源

BerriAI/litellm #47332

GamsGo AI

AI 工具推荐

想把多个 AI 模型放在一个入口?

GamsGo AI 集成 ChatGPT、DeepSeek、Gemini、Claude、Midjourney、Veo 等常用模型,适合写作、绘图、视频和日常 AI 工作流。

了解 GamsGo AI

推广链接:通过此链接购买,我可能获得佣金,不影响你的价格。

这个方案解决了吗?

celebrityanime
celebrityanime
文章: 29433

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注