快速结论:该报错出现在 LiteLLM Proxy 1.108.0 走原生 openrouter/ 路由、并启用流式 usage 统计时:finish_reason: "stop" 之后,Provider 的计费尾包被转成了一个空的 assistant choice(无 finish_reason、无 usage),随后才到达真正的 usage 块和 [DONE]。优先排查流式处理器是否在 finalize 前多发送了一帧中间 choice,而不是 Usage 丢失问题(本复现中最终 usage 与 provider cost 均完整到达)。
适用环境:LiteLLM Proxy 1.108.0(镜像 docker.litellm.ai/berriai/litellm:v1.108.0),OpenAI Python SDK 2.33.0,Provider 端点 https://openrouter.ai/api/v1,模型 openai/gpt-4.1-mini(LiteLLM 请求模型 openrouter/openai/gpt-4.1-mini);操作系统与 Python 版本 Issue 中未记录。
最快修复方案:升级到包含 PR #50210 的 LiteLLM 版本(维护者已确认该“抑制中间空 choice”的修复合并)。如果暂时无法升级,可在客户端侧做窄兼容:忽略 final usage 之前、既无 finish_reason 又无 usage 的语义空 choice,但仍要求后续存在最终 usage 且流以 [DONE] 正常结束。
注意事项:Issue 未验证修复在本地实际请求中的表现,也未给出包含该 PR 的具体发布版本号,升级前请确认目标版本已合入 #50210。空 choice 的判定要严格限定为“无 finish_reason、无 usage、内容为空”,不要放宽成丢弃任何 finish 之后的 chunk,否则可能误删 provider 计费数据。
问题场景
用户将 LiteLLM Proxy 作为应用模型网关与 OpenRouter 之间的中间层,使用原生 openrouter/ 路由、stream: true、stream_options: {"include_usage": true},代理配置中开启 always_include_stream_usage: true。当上游 OpenRouter 在流末尾发送一个带 usage 的计费尾包(其 choices 数组非空、assistant delta 为空)时,LiteLLM 除了最终 usage 块之外,还在 finish_reason: "stop" 之后额外发出一个只有 assistant role、内容为空的 choice。下游流式校验器原本期望 finish 之后的 choice 必须携带计费数据,因此拒绝了这一多余事件。
报错原文
[Bug]: OpenRouter streaming emits an empty post-finish choice without usage before the final usage chunk (v1.108.0)
原因分析
根据报告者对 1.108.0 安装包的代码定位,可能原因集中在流式处理与序列化的衔接处:
litellm/litellm_core_utils/streaming_handler.py约 1070–1080 行:在终止 chunk 已发出后,携带 usage 的响应可能先于 1102–1109 行的 finish 赋值分支返回。- 约 2104–2121 行:异步包装器保留了一份内部副本,并从对外发出的响应中剥离 usage;由于该 chunk 保留了 assistant role,它逃过了空 chunk 过滤而仍然被发送出去。
- 约 2190–2247 行:流 finalize 阶段才另外发出最终 usage。
litellm/proxy/proxy_server.py约 9707–9718 行:代理序列化器省略了值为None的 finish_reason,但保留了空 content 与 assistant role,于是对外表现为“无 finish、无 usage、仅角色”的中间 choice。
换句话说,provider 的计费尾包本身没有被判定为非法,问题在于 LiteLLM 把一个已剥离 usage 的中间帧当作正常 choice 转发,而原始终止原因与最终 usage 均未被破坏。上述行号对应 1.108.0 发行包,而非当时的主干代码。
环境排查
- 确认 LiteLLM Proxy 版本是否为 1.108.0,或是否已包含 PR #50210 的修复。
- 确认 Provider 端点与路由方式:是否为
openrouter/原生路由,端点是否为https://openrouter.ai/api/v1。 - 确认 OpenAI Python SDK 版本(报告环境为 2.33.0)及客户端解析流式 chunk 的逻辑。
- 确认请求参数:
stream: true、stream_options: {"include_usage": true},代理侧always_include_stream_usage: true。 - 确认模型映射(
openrouter/openai/gpt-4.1-mini)是否与上报一致。 - 同时抓取 OpenRouter 直连响应与经代理的响应,比对事件序列差异。
解决步骤
- 抓取并对比数据事件序列:分别记录直连 OpenRouter 与经 LiteLLM Proxy 的 SSE 事件(保留顺序与
[DONE]),定位多出来的那一帧。报告者提供的provider-direct.sse.txt与litellm-proxy.sse.txt为两次独立实时请求的脱敏载荷,仅保留 object、model、choices 与 usage 字段。 - 确认多出的帧确实为“无 finish_reason、无 usage、内容为空、仅含 assistant role”的中间 choice,且最终 usage 与 provider cost 在其后正常到达。
- 升级 LiteLLM 到包含 PR #50210 的版本(可优先尝试),随后重复同一流式请求,检查该中间 choice 是否已被抑制,且最终 usage、原始 finish_reason 与
[DONE]均保留。 - 若暂时无法升级,在客户端校验器中加入窄兼容:在收到最终 usage 之前,跳过既无 finish_reason 又无 usage 的空 choice;同时继续强制校验最终 usage 存在、流以
[DONE]正常终止。 - 如需回归验证,可参考报告建议构造离线 provider replay:使用非空 choices 数组、空 assistant delta、重复 finish 的 usage 尾包,并经过真实的 Proxy 序列化器,而不仅是 SDK 层流对象。
验证方法
对同一配置和同一 stream: true、include_usage: true 请求,经代理输出的事件序列应依次为:内容 delta、终止 finish、最终 usage 块、[DONE],中间不再出现仅供角色或空内容的 choice。同时核对最终 usage 的 prompt/completion token 数与 provider cost 是否仍然完整(本 Issue 复现为 14 prompt tokens、2 completion tokens、0.0000088 USD),并确认原始 finish_reason 未被改写。
参考来源
BerriAI/litellm #50156(修复 PR:#50210)
AI 工具推荐
想把多个 AI 模型放在一个入口?
GamsGo AI 集成 ChatGPT、DeepSeek、Gemini、Claude、Midjourney、Veo 等常用模型,适合写作、绘图、视频和日常 AI 工作流。
推广链接:通过此链接购买,我可能获得佣金,不影响你的价格。
这个方案解决了吗?
可以继续搜索完整报错,或查看同一工具的其他排查指南。


![[Custom Node Registration] ANIMA_BOOSTER (BSS)](https://www.chat-gpts.plus/wp-content/uploads/2026/11/2893-6536225a-768x403.jpg)