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.1、deepseek-V4-pro、haiku、minimax,并在 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
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 级别下搜:
只要 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) > 0 但 AVG(spend) 远低于同类模型,怀疑本 bug。
方法 C:TPM vs Budget 行为分裂¶
- 如果你给 key 同时设了
tpm_limit和max_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:
_get_cost_per_unit()的default_value改为None,让上层显式判断- 在
_calculate_input_cost中,若cache_hit_tokens > 0且cache_read_cost is None,回退到prompt_base_cost(按 input 全价计费),并打印一条 warning - 启动时扫描所有
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 静默失效 |