跳转至

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_authpre_call_hook 链路与触发顺序
03-spend-update-flow.md 请求结束后,response_cost 如何被算出、写回 spend 列、刷新缓存,下次请求才能感知
04-cache-pricing-trap.md 次要因素:未配置 cache_read_input_token_costresponse_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) spendmax_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_tpmtpm_limit usage.total_tokens − cached_tokens(V3) ✓ 仍生效(旁路和 cost 都不影响)
rpm_limit current_rpmrpm_limit 每个请求 +1 ✓ 仍生效
max_parallel_requests current_parallel_requests ≥ 限值 进入时 +1 / 完成时 −1 ✓ 仍生效

注意:V3 限流器在累加 TPM 时会主动减去 cached_tokensparallel_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_infoget_deployment_model_inforouter.py:7119-7129 有一个分支遗漏:当 litellm.get_model_info(litellm_params.model) 失败(即模型名不在 JSON cost map 里)时,即使 UUID 路径已经注册了真实价格,函数仍返回 None。返回 Nonerouter.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 限不住:

  1. 看 INFO 级日志 有没有 Skipping all budget checks for zero-cost model: <你的模型名> —— 有就是 100% 命中本 bug
  2. grep "<模型名>" litellm/model_prices_and_context_window_backup.json —— 没有匹配就是命中
  3. 比对:TPM 限得住吗? TPM 走 Redis 计数器、与 cost 无关;TPM 限得住而 budget 限不住,强烈指向本 bug