Tag-budget enforcement silently skipped on x-litellm-tags header path

在 LiteLLM (版本 v1.83.14-stable.patch.2 )中,当满足以下条件时触发:

快速结论:当客户端通过 HTTP 请求头 x-litellm-tags 传递标签时,LiteLLM 的标签预算检查(_tag_max_budget_check)会静默跳过,导致即使标签累计消费超过 max_budget,请求仍被放行(HTTP 200)。优先排查:确认你使用的是请求头传标签方式,如果是,则问题必然存在;将标签改为请求体({"tags": [...]})传递可以绕过此问题。

问题场景

LiteLLM(版本 v1.83.14-stable.patch.2)中,当满足以下条件时触发:

  • 生成的虚拟密钥(virtual key)设置了 metadata.allow_client_tags: true
  • 客户端通过 x-litellm-tags 请求头传递标签
  • 该标签关联了预算(max_budget),并且累计消费已超过预算上限

预期行为:LiteLLM 应返回 HTTP 400 错误(budget_exceeded)。实际行为:返回 HTTP 200,请求正常处理,标签消费继续增长。

报错原文

# 实际无报错,请求被静默放行
# 预期报错:
HTTP 400
{"error": {"type": "budget_exceeded", "message": "Budget has been exceeded! Tag=tenant:acme ..."}}

原因分析

授权链中的执行顺序问题导致。LiteLLM 的预算检查函数 _tag_max_budget_checklitellm/proxy/auth/auth_checks.py 中调用,该函数通过 get_tags_from_request_body(request_body)(位于 litellm/proxy/common_utils/http_parsing_utils.py)获取标签,但该函数只解析请求体中的 tags 字段和 metadata.tags 字段

x-litellm-tags 请求头的解析和合并发生在 LiteLLMProxyRequestSetup.add_request_tag_to_metadata(位于 litellm/proxy/litellm_pre_call_utils.py),此函数在 授权链完成后 才执行(在 add_litellm_data_to_request 内部)。因此当预算检查运行时,get_tags_from_request_body 返回空列表([]),预算检查循环被跳过,静默放行所有请求

根本原因:预算检查基于请求体读取标签,但请求头标签在授权之后才合并,形成了一个“时间差”漏洞。

环境排查

  • LiteLLM 版本:确认是否 v1.83.14-stable.patch.2 或相似版本(该问题在旧版本中可能存在更久)
  • 部署方式:Docker Compose 部署(附带 Postgres + Redis)或自定义部署
  • Key 配置:检查虚拟密钥是否设置了 metadata.allow_client_tags: true
  • 预算配置:确认标签已关联预算(budget_id)且已超支
  • 请求方式:确认标签是通过 x-litellm-tags 请求头传递,而不是请求体

解决步骤

  1. 临时规避方案:将标签改为通过请求体传递。例如:
    # 当前使用请求头(会触发问题)
    -H "x-litellm-tags: tenant:acme"
    
    # 改为请求体
    -d '{"tags": ["tenant:acme"], ...}'

    注意:如果必须通过请求头传递,此方案不适用。

  2. 官方修复建议(两个等效方案):
    • 最小化修复(Option A,推荐优先尝试):修改 _tag_max_budget_check 函数(位于 litellm/proxy/auth/auth_checks.py),增加对 x-litellm-tags 请求头的直接读取,并与请求体中的标签合并。大致逻辑:
      async def _tag_max_budget_check(
          request_body: dict,
          request_headers: Optional[dict],  # <-- 新增参数
          prisma_client: ...,
      ):
          tags = get_tags_from_request_body(request_body=request_body)
          if request_headers and request_headers.get("x-litellm-tags"):
              header_tags = [t.strip() for t in request_headers["x-litellm-tags"].split(",")]
              tags = list({*tags, *header_tags})  # 合并去重
          if not tags:
              return
          # ... 原有预算检查逻辑
    • 更干净的修复(Option B):add_request_tag_to_metadata 函数(或一个仅处理请求头标签的变体)提前到 common_checks 之前执行,这样请求体中的 metadata.tags 已在预算检查时完成合并。
  3. 等待官方发布修复版本:检查后续 LiteLLM 发布版本,确认该问题是否被解决。如果你自行应用了上述补丁,请保留原始代码备份。

验证方法

  1. 重现验证:按照 Issue 中的 Reproduce 步骤操作(创建密钥 → 创建预算 + 标签 → 通过请求头发送请求使消费超支 → 再次请求),确认修复前返回 HTTP 200,修复后返回 HTTP 400。
  2. 对比测试:同时使用请求头传标签和请求体传标签两种方式,确认两者在超预算时均返回 budget_exceeded
  3. 检查标签消费:通过 /spend/tags 端点查看标签累计消费,确认消费不再无限增长(在预算上限处被拦截)。

参考来源

BerriAI/litellm #27480

GamsGo AI

AI 工具推荐

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

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

了解 GamsGo AI

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

这个方案解决了吗?

celebrityanime
celebrityanime
文章: 16197

发表回复

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