跳转至

04 — 次要因素:cache 价格漏配让 cost 被低估

⚠️ 本文是排查初期的猜想,已被证伪为次要因素。它能解释"spend 涨得慢、限流晚熄火",但不能解释"DB 中 spend > max_budget 仍放行"——后者是另一个独立 bug,见 05-skip-budget-checks-bug.md

本文保留是因为: 1. cache 价格漏配确实是一个独立 bug,会让 cost 数字失真(与上游 billing-and-pricing/05-cache-pricing-bugs.md 同源) 2. 它会放大主 bug 的影响——主 bug 让 budget 检查失效、本 bug 让 cost 数字偏小,叠加起来现场更乱

现场

某 API key 限制白名单为:glm-5.1deepseek-V4-prohaikuminimax,并在 LiteLLM_UserTable / LiteLLM_VerificationToken 上配置了 max_budget

观察到的现象:

  • ✅ 用 haiku / deepseek-V4-pro 跑:花完限额后 正确返回 BudgetExceededError
  • ❌ 用 glm-5.1 / minimax 跑:永远不报 budget 超限,无限请求;DB 中确认 spend > max_budget 仍放行

事后比对配置发现:haiku / deepseek-V4-pro 在价格表中配置了 cache_read_input_token_cost,而 glm-5.1 / minimax 没有。这个差异确实存在,但它不是限流失效的根因——根因是模型名是否在 litellm.model_cost JSON map 里(决定 _is_model_cost_zero 是否旁路 budget 检查),见 05-skip-budget-checks-bug.md


真实日志(GLM 单次请求)

{
  "model": "glm-5.1",
  "usage": {
    "prompt_tokens": 9669,
    "completion_tokens": 145,
    "prompt_tokens_details": { "cached_tokens": 6335 },
    "completion_tokens_details": { "reasoning_tokens": 88 }
  },
  "model_map_value": {
    "input_cost_per_token": 0.00000086,
    "output_cost_per_token": 0.0000035,
    "cache_read_input_token_cost": null,        // ← 关键
    "cache_creation_input_token_cost": null
  },
  "response_cost":  0.00316474,
  "input_cost":     0.00286724,
  "output_cost":    0.00050750
}

实际计算:

text_tokens = 9669 − 6335 = 3334
input_cost  = 3334 × 8.6e-7 + 6335 × 0     = $0.00286724  ✓ 与日志吻合
output_cost = 145 × 3.5e-6                  = $0.00050750  ✓

6335 个 cache 命中 token 被计入 $0。如果该 provider(GLM)实际对 cache hit 仍按比例收费(比如官方价的 10–50%),用户就在"白嫖"自家的 max_budget——cost 被低估,spend 增长缓慢,限流永不触发。


根因链路(精确到代码行)

Step 1:_get_cost_per_unit 静默返回 0.0

litellm/litellm_core_utils/llm_cost_calc/utils.py:318-357

def _get_cost_per_unit(
    model_info: ModelInfo, cost_key: str, default_value: Optional[float] = 0.0
) -> Optional[float]:
    cost_per_unit = model_info.get(cost_key)
    if isinstance(cost_per_unit, float):
        return cost_per_unit
    if isinstance(cost_per_unit, int):
        return float(cost_per_unit)
    ...
    return default_value   # ← 字段不存在 / 为 None → 0.0

GLM / MiniMax 的 model_info 中没有 cache_read_input_token_cost 这个 key,命中 default_value=0.0 分支。没有 warning,没有 metric,没有 exception。

Step 2:_calculate_input_cost 用 0.0 做乘法

litellm/litellm_core_utils/llm_cost_calc/utils.py:524-527

prompt_cost = float(prompt_tokens_details["text_tokens"]) * prompt_base_cost
prompt_cost += float(prompt_tokens_details["cache_hit_tokens"]) * cache_read_cost
#                                                                ^^^^^^^^^^^^^^^
#                                                               这里是 0.0

Step 3:text_tokens 矫正 (此机制反而放大了问题)

litellm/litellm_core_utils/llm_cost_calc/utils.py:631-643

total_details = text_tokens + cache_hit + audio_tokens + cache_creation + image_tokens
has_double_counting = cache_hit > 0 and total_details > usage.prompt_tokens

if (text_tokens == 0 and prompt_tokens_details["image_count"] == 0) or has_double_counting:
    text_tokens = (
        usage.prompt_tokens
        - cache_hit
        - audio_tokens
        - cache_creation
        - image_tokens
    )

这段是为了避免 double-counting,但副作用是:当 provider 把 prompt_tokens 报成"包含 cache 的总数"时,LiteLLM 会从总数中减去 cache_hit_tokens这意味着,cache 部分既不计入 text 价格,也不计入 cache 价格——直接消失

Step 4:response_cost 流入 spend

litellm/proxy/hooks/proxy_track_cost_callback.py:178

await proxy_logging_obj.db_spend_update_writer.update_database(
    token=user_api_key,
    response_cost=response_cost,    # 显著低估
    ...
)

PG 表 spend 列以低估值累加,DualCache 同步写入。

Step 5:Pre-call 检查永远通过

litellm/proxy/auth/auth_checks.py:2713

if valid_token.spend >= valid_token.max_budget:
    raise litellm.BudgetExceededError(...)

valid_token.spend 因为长期被低估,>= valid_token.max_budget 永远不成立。limiter 静默失效。


量化对比(同样 cache 命中率 60%)

max_budget = $1,单次 prompt 9669 tokens / completion 145 tokens / cache 6335 tokens:

模型 input_cost_per_token cache_read_input_token_cost 实际 cost / 请求 触发限流所需请求数
Haiku(cache 配齐) $8.0e-7 $1.0e-7 ~$0.0035 ~286
DeepSeek(cache 配齐) 类似配齐 类似配齐 ~$0.003 量级 几百
GLM-5.1(cache 缺) $8.6e-7 null → 0 $0.00316 ~316
MiniMax(cache 缺) $3.0e-7 null → 0 $0.00067 ~1493

单看 GLM 数字看不出戏剧化差距,原因是 GLM 的 input_cost_per_token 本身偏低、且 cache 命中率不算极端。但 MiniMax 的 input 单价更低($0.30 / 1M),叠加 cache 命中"白嫖",达到限额所需请求数翻 5 倍以上。

对于 "agent 长 system prompt + 反复调用" 的工作流,cache 命中率往往在 80–95%。极端情况下:cost ≈ (1 − cache_ratio) × prompt × input_price。当 cache_ratio = 0.95、input_price = \(3e-7 时,单次请求成本约 **\)0.00015,达到 max_budget = $1 需要 ~6700 次请求**——表现就是"限流根本限不住"。


验证方法

方法 A:直接看日志

DEBUG 级别下搜:

input_cost: ...
cache_read_input_token_cost: null
prompt_tokens_details.cached_tokens: <非零>

只要 cache_read_input_token_cost 是 null 且 cached_tokens > 0,就命中本 bug。

方法 B:查 DB SpendLogs

SELECT model, COUNT(*), SUM(spend), AVG(spend), AVG((response->'usage'->'prompt_tokens_details'->>'cached_tokens')::int)
FROM "LiteLLM_SpendLogs"
WHERE startTime > NOW() - INTERVAL '1 day'
GROUP BY model;

如果某个模型的 AVG(cached_tokens) > 0AVG(spend) 远低于同类模型,怀疑本 bug。

方法 C:TPM vs Budget 行为分裂

  • 如果你给 key 同时设了 tpm_limitmax_budget
  • TPM 能限住,Budget 限不住 → 几乎可以确认是 cache 价格漏配(因为 TPM 与价格无关)
  • 两者都限不住 → 排查别的问题(缓存刷新、disable_spend_logs 之类的开关)

修复

两条路径,任选其一(与 billing-and-pricing/05-cache-pricing-bugs.md 同源):

路径 A:DB / UI 配置(custom_pricing 路径)

在 UI 的 LiteLLM Params 面板,给该 model 行补齐:

字段 推荐值(按 provider 文档填实际值)
input_cost_per_token provider 官方价
output_cost_per_token provider 官方价
cache_creation_input_token_cost 官方 cache 写入价(无则填等于 input_cost)
cache_read_input_token_cost 官方 cache 读取价(无则填 0.1× input_cost 作保守估算)

⚠️ 千万不要设为 null 或留空——_get_cost_per_unit 会回退到 0.0。至少填一个非 0 的小数,例如 input 价的 10%。

路径 B:JSON 路径

编辑 litellm/model_prices_and_context_window_backup.json,给 GLM/MiniMax 补全字段:

"glm-5.1": {
    "input_cost_per_token": 8.6e-7,
    "output_cost_per_token": 3.5e-6,
    "cache_read_input_token_cost": 8.6e-8,         // 按 0.1× 估
    "cache_creation_input_token_cost": 8.6e-7,     // 同 input
    "litellm_provider": "openai_compatible",
    ...
}

并设 LITELLM_LOCAL_MODEL_COST_MAP=true,重启进程;同时在 UI 把对应行的 cache 字段清空(避免走 custom_pricing UUID 路径覆盖 JSON)。


上游层面的修复方向(不动当前生产)

如果要从根本上消除"静默 0.0"陷阱,可在 LiteLLM 上游 PR:

  1. _get_cost_per_unit()default_value 改为 None,让上层显式判断
  2. _calculate_input_cost 中,若 cache_hit_tokens > 0cache_read_cost is None回退到 prompt_base_cost(按 input 全价计费),并打印一条 warning
  3. 启动时扫描所有 model_cost,对 supports_prompt_caching=true 但缺 cache 价的模型告警

但这是上游改造,本仓库当前选择"配置侧补齐"的方案,原因有二:成本可控、不污染主仓代码。


与现有文档的关系

文档 内容侧重
billing-and-pricing/04-billing-flow.md 完整 cost 计算路径,UUID vs JSON 路径
billing-and-pricing/05-cache-pricing-bugs.md Anthropic Vendor2 cache 计费 bug 的具体配置修复
本文(04-cache-pricing-trap) 从限流视角解释 cost 失真如何让 max_budget 静默失效