跳转至

06 - 流式响应中的 usage 字段

面向对象:调 LiteLLM proxy /v1/chat/completions 流式接口、想在客户端拿到 prompt_tokens / completion_tokens / total_tokens 的下游开发人员。

核心结论:流式响应默认不返回 usage 字段,这是 OpenAI 协议标准行为。要拿到 usage 必须显式传 stream_options.include_usage=true


1. 行为矩阵

下表是 scripts/test_chat_completions_usage_field.py 对本 proxy 上 8 个能跑通的模型逐个验证的结果,所有模型在同一模式下表现完全一致

调用方式 客户端能拿到 usage? 说明
stream=false(非流式) ✅ 永远有 response 顶层 usage 字段总在
stream=true + stream_options.include_usage=true ✅ 永远有 [DONE] 之前会多发一个 choices=[] 的 chunk,里面带 usage
stream=true(未指定 stream_options ❌ 不会有 OpenAI 默认行为
stream=true + stream_options.include_usage=false ❌ 不会有 客户端显式不要

重要前提:proxy 内部计量不受影响。无论客户端传不传 include_usage,proxy 都会在内部把所有 chunk 聚合一次算出 usage,并写入 LiteLLM_SpendLogs / Prometheus / 任何已配置的 success_callback。扣费、限额、审计、监控 永远是准确的,受影响的只是 HTTP 响应给客户端这一段。

代码依据:


2. 流式 + include_usage=true 的 SSE 形态

携带 stream_options.include_usage=true 时,SSE 的最后两条数据形如:

data: {"id":"...","object":"chat.completion.chunk","choices":[{"index":0,"delta":{},"finish_reason":"stop"}],"usage":null}

data: {"id":"...","object":"chat.completion.chunk","choices":[],"usage":{"prompt_tokens":35,"completion_tokens":5,"total_tokens":40}}

data: [DONE]

注意: 1. 倒数第二条 chunk 的 choices 是空数组 [],仅承载 usage。很多手写的流式解析逻辑会假设每个 chunk 都有 choices[0],遇到这条会 IndexError,需要先判空。 2. 之前所有正常的 delta chunk 里也会带 "usage": null(不会缺这个 key),但只有最后一个 chunk 里 usage 是真值。 3. [DONE] 之后没有 chunk。


3. 各客户端怎么传

3.1 curl

curl -N http://your-proxy/v1/chat/completions \
  -H "Authorization: Bearer $LITELLM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "gpt-4o-mini",
    "messages": [{"role":"user","content":"hi"}],
    "stream": true,
    "stream_options": {"include_usage": true}
  }'

3.2 openai-python(v1.x)

from openai import OpenAI
client = OpenAI(base_url="http://your-proxy/v1", api_key="...")

stream = client.chat.completions.create(
    model="gpt-4o-mini",
    messages=[{"role": "user", "content": "hi"}],
    stream=True,
    stream_options={"include_usage": True},   # ← 关键
)
for chunk in stream:
    if chunk.usage is not None:
        print("FINAL USAGE:", chunk.usage)
    elif chunk.choices:
        delta = chunk.choices[0].delta
        if delta.content:
            print(delta.content, end="")

openai-python>=1.26 才支持 stream_options 参数;老版本不会报错,会被 SDK 直接丢掉。

3.3 langchain-openai

ChatOpenAI 默认不传 include_usage,需要在构造时显式开:

from langchain_openai import ChatOpenAI

llm = ChatOpenAI(
    base_url="http://your-proxy/v1",
    model="gpt-4o-mini",
    streaming=True,
    stream_usage=True,    # langchain-openai >= 0.1.9 暴露的开关
)

stream_usage=True 内部会自动注入 stream_options={"include_usage": True}

3.4 LangGraph / LangChain.js

同上,构造 ChatOpenAI 时传 streamUsage: true

3.5 axios / fetch / httpx 手写 SSE

只要在 body 里加 "stream_options": {"include_usage": true} 即可,没有 SDK 层差异。


4. 常见错误模式

现象 根因 解决
流式响应里没有 usage chunk 客户端没传 include_usage=true 加上 stream_options.include_usage=true
同样的代码,非流式有 usage、流式没有 流式分支没传 stream_options 流式分支独立加 stream_options.include_usage=true
升级 SDK 后 usage 突然丢失 部分 SDK 在某个版本前会自动注入 include_usage,新版本变成显式 opt-in 检查 SDK changelog,显式传 stream_options
自己解析 SSE 时在最后一个 chunk 上 IndexError usage chunk 的 choices[] 解析时先判 if chunk["choices"]: ... 再取 choices[0]
usage chunk 里 prompt_tokens / completion_tokens = 0 上游 provider 未返回 token 计数,LiteLLM 在某些 provider 下会回退到 0 单独排查该 provider,提 issue

5. 如果你只是想做计费 / 审计 / 监控

别想从客户端响应里抓 usage。正确路径:

  • 数据库:查 LiteLLM_SpendLogs 表,按 request_id / api_key 维度都能查到 usage
  • Prometheus:litellm_input_tokens_total / litellm_output_tokens_total 指标
  • S3 logs:standard_logging_payload 里有 prompt_tokens / completion_tokens / total_tokens 完整字段

这些数据源不依赖客户端是否传 include_usage,永远准确。


6. 历史背景

如果你之前看到"返回结果有时候没有 usage 字段"的现象,最可能的来源是:

  1. 同一服务里不同业务方用了不同的 SDK / 调用方式,有的传了 include_usage,有的没传
  2. 同一段代码在非流式路径下能拿到 usage,切到流式就拿不到了
  3. SDK 升级前后默认行为变化

不是间歇性 bug——在固定调用方式下 LiteLLM 的行为完全确定,与上游 OpenAI 协议保持一致。回归脚本 scripts/test_chat_completions_usage_field.py 用本 proxy 上 8 个不同 provider 的模型验证过,所有模型在 4 种模式下的结果与本文表格完全吻合。