LiteLLM 限流(Rate Limiting / Quota)全链路文档¶
本系列文档完整描述 LiteLLM 代理服务中 限流与额度(quota) 的执行链路:限流类型、触发位置、变量来源、更新时机,以及一个生产环境踩中的真实坑(cache 价格未配置导致 max_budget 限不住)。
适用场景: - 排查"我设置了 max_budget 但用户花完了限额还能继续请求" - 排查"为什么同一把 key、同样的 max_budget,限住了 A 模型却限不住 B 模型" - 设计新的限流策略前,理解现有四类限流器各自的语义边界
文档目录¶
| 文件 | 内容 |
|---|---|
| 01-limiter-types.md | 四类限流器全景:max_budget / TPM / RPM / max_parallel_requests,以及它们检查的字段、作用域(key/user/team/org/model) |
| 02-pre-call-flow.md | 请求到达后,限流检查在调用栈中的位置:user_api_key_auth → pre_call_hook 链路与触发顺序 |
| 03-spend-update-flow.md | 请求结束后,response_cost 如何被算出、写回 spend 列、刷新缓存,下次请求才能感知 |
| 04-cache-pricing-trap.md | 次要因素:未配置 cache_read_input_token_cost 时 response_cost 被严重低估、spend 涨得慢;这只能解释"晚熄火"而非"完全不熄火" |
| 05-skip-budget-checks-bug.md | 真正的根因:_is_model_cost_zero 旁路——模型名不在 litellm.model_cost JSON 里时,所有 budget 检查被整段跳过,即使 PG 中 spend > max_budget 也照样放行 |
| 06-user-request-limiter.md | 第五类限流器:按用户请求数限流(Redis ZSET 滑动窗口),pre-call 计数,不依赖 spend/token/模型价格;支持开关和分钟级窗口配置 |
整体链路一览¶
flowchart TD
A[HTTP 请求到达] --> B[user_api_key_auth<br/>auth/user_api_key_auth.py]
B --> Z[_is_model_cost_zero?<br/>auth_checks.py:109]
Z -- True 旁路 --> D[ProxyLogging.pre_call_hook 链]
Z -- False --> C[_virtual_key_max_budget_check<br/>auth_checks.py:2669]
C -- spend < max_budget --> D
C -- spend ≥ max_budget --> X1[BudgetExceededError 429]
D --> D1[_PROXY_MaxBudgetLimiter<br/>hooks/max_budget_limiter.py]
D --> D2[_PROXY_MaxParallelRequestsHandler_v3<br/>hooks/parallel_request_limiter_v3.py]
D1 -- user.spend < max_budget --> E
D2 -- TPM/RPM/parallel 未超 --> E
D1 -. 超额 .-> X2[429 Max budget reached]
D2 -. 超额 .-> X3[429 TPM/RPM/parallel limit]
E[Router.acompletion<br/>调上游 LLM] --> F[litellm_logging<br/>async_success_handler]
F --> G1[response_cost_calculator<br/>cost_calculator.py:1528]
F --> G2[parallel_request_limiter.async_log_success_event<br/>累加 current_tpm/current_rpm]
G1 --> G1a[generic_cost_per_token<br/>llm_cost_calc/utils.py:580]
G1a --> H[_PROXY_track_cost_callback<br/>hooks/proxy_track_cost_callback.py:123]
H --> I[db_spend_update_writer.update_database<br/>累加 spend 到 PG]
I --> J[update_cache<br/>用户/key/team 的内存缓存同步]
J -.下次请求读到新 spend.-> C
G2 -.下次请求读到新 TPM 计数.-> D2
style Z fill:#fdd,stroke:#c33,stroke-width:2px
⚠️ 红色节点
_is_model_cost_zero?是真正出问题的旁路。一旦它返回 True,整条 budget 链路(包括_virtual_key_max_budget_check与_PROXY_MaxBudgetLimiter)都被跳过,请求直接进入 LLM 调用。详情见 05-skip-budget-checks-bug.md §0.三条独立路径并存: - 旁路开关
_is_model_cost_zero:只要它对该模型判 True,本次请求所有 budget 校验都不跑。这是"DB 里 spend 已超额仍放行"的根因。 - Budget(spend / max_budget)路径:依赖response_cost累加。response_cost算错为 0,spend就不增长,限流就失效(次要因素,参见 04-cache-pricing-trap.md)。 - TPM/RPM/parallel 路径:依赖 token 计数和并发计数,与response_cost和旁路都无关。即使 cost=0、即使旁路触发,TPM 仍会准确累加。这就是为什么"TPM 限得住但 max_budget 限不住"是命中本 bug 的强信号。
速查:四类限流器与失效条件¶
| 限流器 | 比较对象 | 累加来源 | 失效条件 |
|---|---|---|---|
max_budget(key/user/team/org/end_user) |
spend ≥ max_budget(USD) |
response_cost(DB 列 spend 累加) |
✗ 两种独立失效路径:(1) 旁路:模型名不在 JSON cost map → _is_model_cost_zero=True → 整段跳过(05);(2) cost=0:spend 不增长(04) |
tpm_limit(per-key/user/model) |
current_tpm ≥ tpm_limit |
usage.total_tokens − cached_tokens(V3) |
✓ 仍生效(旁路和 cost 都不影响) |
rpm_limit |
current_rpm ≥ rpm_limit |
每个请求 +1 | ✓ 仍生效 |
max_parallel_requests |
current_parallel_requests ≥ 限值 |
进入时 +1 / 完成时 −1 | ✓ 仍生效 |
注意:V3 限流器在累加 TPM 时会主动减去
cached_tokens(parallel_request_limiter_v3.py:1286-1288)——这是与上游 provider(Bedrock 等)行为对齐的设计,与 cost 配置无关。
直接结论:为什么某些模型"花超额还能调"¶
⚠️ 更新:早期猜测是"cache 价格漏配 → cost 低估 → spend 涨得慢"。但这只能解释"晚熄火",无法解释"DB 里 spend 已经超过 max_budget 还在放行"。真正根因是另一条独立旁路:
user_api_key_auth.py:1086-1163 在每次请求开始时跑:
skip_budget_checks = _is_model_cost_zero(model=model, llm_router=llm_router)
...
if not skip_budget_checks:
if RouteChecks.is_llm_api_route(route=route):
await _virtual_key_max_budget_check(...)
# 其他 5 类 budget 检查也都包在这个 if 里
_is_model_cost_zero 的判断走 router.get_model_group_info → _set_model_group_info → get_deployment_model_info,router.py:7119-7129 有一个分支遗漏:当 litellm.get_model_info(litellm_params.model) 失败(即模型名不在 JSON cost map 里)时,即使 UUID 路径已经注册了真实价格,函数仍返回 None。返回 None 后 router.py:7228-7234 兜底用 input_cost_per_token=0, output_cost_per_token=0 构造 ModelGroupInfo。_is_model_cost_zero 看到两个 0 → 返回 True → skip_budget_checks=True → 全部 6 类 budget 检查整段跳过。
GLM-5.1、MiniMax 自定义部署名 不在 model_prices_and_context_window_backup.json 里 → 命中本 bug。Haiku-4-5、DeepSeek 在 JSON 里 → 不命中。
完整复盘见 05-skip-budget-checks-bug.md。Cache 价格漏配仍是另一个独立问题(cost 算少),见 04-cache-pricing-trap.md。
一分钟自检¶
如果你怀疑某个 key 的 max_budget 限不住:
- 看 INFO 级日志 有没有
Skipping all budget checks for zero-cost model: <你的模型名>—— 有就是 100% 命中本 bug grep "<模型名>" litellm/model_prices_and_context_window_backup.json—— 没有匹配就是命中- 比对:TPM 限得住吗? TPM 走 Redis 计数器、与 cost 无关;TPM 限得住而 budget 限不住,强烈指向本 bug