LiteLLM Dynamic Rate Limiter v3:饱和度感知的优先级限流机制全面解析
本技术指南以 LiteLLM Proxy 中动态限流器的 v3 实现(_PROXY_DynamicRateLimitHandlerV3)为核心,系统讲解"饱和度感知 + 优先级分配"的限流设计思路、Generous/Strict 双模式行为、完整配置方法与源码级实现原理。读完本文,你将掌握如何在 LiteLLM Proxy 上为不同业务等级(如 premium/standard)预留模型吞吐能力,并理解为何该方案能同时兼顾"低负载时的资源利用率"与"高负载下的公平性"。
本文依据的关联文档为 README.dynamic_rate_limiter_v3.md,并对应当前仓库 dynamic_rate_limiter_v3.py 的实际源码与 test_dynamic_rate_limiter_v3.py 中的测试用例进行交叉印证。
一、背景:传统限流的两个痛点
在 Proxy 网关中,模型组的 RPM(每分钟请求数)与 TPM(每分钟 Token 数)是稀缺资源。传统做法通常要么"一刀切"地对所有用户施加相同上限(公平但低效),要么仅做模型级总量限流(高效但无差别,高价值用户可能被低价值流量挤掉)。
v3 动态限流器要同时解决这两个问题:
- 资源效率:系统处于低负载时,允许某个优先级"借用"其它优先级未被使用的剩余容量,不让模型容量空转。
- 公平性:系统接近饱和时,严格按预设的优先级权重为每个等级分配独立的保留额度,保证高优先级用户在高压下依然得到其应得份额。
v3 区别于早期版本(_PROXY_DynamicRateLimitHandler,见 dynamic_rate_limiter.py)的关键在于引入了模型饱和度(saturation) 作为实时切换限流策略的依据,并完全复用 v3 限流器(parallel_request_limiter_v3.py)基于 Redis + Lua 的原子计数基础设施,天然支持多实例部署。
二、核心机制:饱和度感知的双模式策略
2.1 两条核心行为规则
限流器维护一个"当前用量 / 模型容量"的饱和度比值,并以此划分两种模式:
- 饱和度 < 80%(Generous 宽松模式):优先考虑资源利用率,允许优先级借用剩余容量,按先到先得直到模型总容量耗尽。
- 饱和度 ≥ 80%(Strict 严格模式):优先保证公平性,按归一化后的优先级权重执行各自的保留额度。
2.2 完整请求处理流程
原文档给出了完整的请求流程示意,整理如下:
Incoming Request
│
▼
1. Check Model Saturation
- 查询 v3 limiter 的 Redis 计数器
- 计算:current_usage / capacity
- 返回值区间:0.0(空闲)~ 1.0+(超饱和)
│
▼
Saturation < 80% ────────────── Saturation >= 80%
│ │
▼ ▼
Generous Mode Strict Mode
- 只执行模型级总量限制 - 先归一化优先级权重(若合计 > 1.0)
- 不做优先级限制 - 为每个优先级创建独立描述符
- 允许借用(borrowing) - 强制执行各优先级保留额度
- 先到先得直到容量占满 - 单独追踪模型级用量供后续饱和度判断
│ │
└───────────────┬────────────────┘
▼
v3 Limiter Check(原子检查 + 计数)
│ │
▼ ▼
OVER_LIMIT OK
│ │
▼ ▼
返回 429 错误 放行请求
关于阈值"80%",需要特别注意:文档示例与 types/utils.py 中 PriorityReservationSettings 的实际默认值存在差异——源码中 saturation_threshold 的实际默认值为 0.50(50%),文档示例中的 0.80 是可选的自定义取值。这意味着若用户不显式配置,v3 会在饱和度达到 50% 时便进入严格模式。配置时请以源码默认值为基准,切勿假设阈值恒为 80%。
三、配置指南
v3 动态限流器的行为由两处全局配置驱动:litellm.priority_reservation(每级优先级各保留多少容量)与 litellm.priority_reservation_settings(模式切换与兜底策略)。
3.1 优先级容量预留:priority_reservation
在代理配置中,通过全局字典声明各优先级等级占模型容量的权重:
litellm.priority_reservation = {
"premium": 0.75, # 75% of capacity
"standard": 0.25 # 25% of capacity
}
值既可以是普通浮点百分比,也可以是精确容量值(PriorityReservationDict 类型)。从 rate_limiter_utils.py 中 convert_priority_to_percent() 的实现可看到三种合法取值格式:
| 格式 | 写法 | 语义 |
|---|---|---|
| 百分比 | 0.9 或 {"type": "percent", "value": 0.9} |
占模型容量的 90% |
| RPM 绝对值 | {"type": "rpm", "value": 900} |
每分钟保留 900 个请求(900 / 模型 rpm) |
| TPM 绝对值 | {"type": "tpm", "value": 900000} |
每分钟保留 900000 Token(900000 / 模型 tpm) |
注意 rpm/tpm 绝对值换算依赖模型组的 rpm/tpm 配置;同时,模型组必须先在 Router 中声明 rpm/tpm 上限,限流描述符才有生成基础(详见下文 5.3 节)。
此外,源码在 _get_priority_weight() 中对企业许可证做了显式检查:若 os.getenv("LITELLM_LICENSE") 未设置,会记录一条 PREMIUM FEATURE 错误日志并退回默认权重。这意味着按优先级保留 tpm/rpm 属于企业版(premium)能力,需要在 .env 中配置有效的 LITELLM_LICENSE 才能生效。
3.2 饱和度行为设置:priority_reservation_settings
from litellm.types.utils import PriorityReservationSettings
litellm.priority_reservation_settings = PriorityReservationSettings(
default_priority=0.5, # 无显式优先级的用户的默认权重
saturation_threshold=0.80, # 进入严格模式的饱和度阈值
tracking_multiplier=10 # 严格模式下模型级追踪计数的放大倍数
)
对照源码字段(litellm/types/utils.py),各配置项的实际默认值与语义如下:
default_priority:文档示例默认0.5,源码实际默认0.25。语义为:没有携带任何优先级元数据的 key/team 所使用的兜底权重。这些 key 并不会各自独享一份额度,而是全部共享同一个default_pool。saturation_threshold:文档示例默认0.80,源码实际默认0.50。取值范围0.0 ~ 1.0,是宽松/严格模式的分界点。tracking_multiplier:文档中的10表示严格模式下"仅追踪、不拦截"的模型级计数所采用的放大倍数;当前实现中该类通过_create_model_tracking_descriptor(..., high_limit_multiplier=...)参数来放大限制值。需要指出的是,当前 dynamic_rate_limiter_v3.py 内调用该函数时传入的是high_limit_multiplier=1,即模型级容量以 100% 上限被始终强制执行(用于防止过度订阅),请以源码现状为准。saturation_check_cache_ttl:文档未列出但源码中真实存在的字段,默认60秒。控制读取饱和度值时本地缓存(DualCache 的 local cache)的 TTL,用于削峰 Redis 读取压力。对应测试用例见test_saturation_check_cache_ttl_configuration。
3.3 为请求分配优先级:user / team 元数据
优先级是挂在调用方身份上的属性。_get_priority_from_user_api_key_dict()(源码位于 dynamic_rate_limiter_v3.py)的解析顺序为:先读 team_metadata 中的 priority,再回退到 key 的 metadata:
user_api_key_dict.metadata = {"priority": "premium"} # 按 key 维度
user_api_key_dict.team_metadata = {"priority": "premium"} # 按团队维度(优先)
测试 test_async_log_success_event_uses_team_priority_from_auth_metadata 专门验证了"团队优先级优先于 key 优先级、并贯穿到成功回调的 Token 计数"这一链路。若两处都未设置,则落入 default_priority 对应的共享默认池。
四、优先级权重归一化:防止过度分配
当各优先级权重之和超过 1.0 时,v3 会自动归一化,保证总分配永远不会超过模型容量:
输入: {key_a: 0.60, key_b: 0.80} = 合计 1.40
输出: {key_a: 0.43, key_b: 0.57} = 合计 1.00
归一化的工程实现在 _normalize_priority_weights()(dynamic_rate_limiter_v3.py):
- 先将
priority_reservation中的每个值(浮点、rpm、tpm)统一换算为百分比; - 计算
total_weight; - 若总和大于
1.0,按v / total_weight等比压缩。
测试 test_fake_calls_case_4_over_allocated_with_normalization 覆盖了"过度分配时自动归一化"场景,验证归一化后的保留额度确实按比例生效。
五、源码级实现纵深
核心类 _PROXY_DynamicRateLimitHandlerV3(dynamic_rate_limiter_v3.py)继承自 CustomLogger,通过 Proxy 的 pre-call / post-call / log-success 三类钩子接入请求生命周期。类文档字符串总结了它的五个关键设计点:
- 模型容量始终按 100% 强制执行(防止过度分配);
- 优先级用量从第一个请求起就持续追踪(保证计费与饱和度量准确);
- 优先级限制仅在饱和度 ≥ 阈值时才强制执行;
- 三阶段检查杜绝计数器的"部分自增";
- 复用 v3 限流器的 Redis 追踪机制(多实例安全)。
5.1 关键方法一览
| 方法 | 职责 |
|---|---|
async_pre_call_hook() |
主入口,路由到宽松/严格模式(源码) |
_check_model_saturation() |
只读查询 Redis 计数器,计算饱和度(源码) |
_check_rate_limits() |
三阶段检查:先只读校验、再按饱和度决策、最后原子自增(源码) |
_handle_generous_mode() / _handle_strict_mode() |
两种模式对应的描述符组装与执行逻辑 |
_normalize_priority_weights() |
处理权重过度分配 |
_create_priority_based_descriptors() |
构建按优先级拆分的限流描述符 |
async_post_call_success_hook() / async_log_success_event() |
成功回调中回写请求/Token 计数与响应头 |
5.2 饱和度检测:只读不递增
_check_model_saturation() 的实现要点:
- 复用 v3 limiter 的
create_rate_limit_keys()生成与限流完全一致的 Redis 计数键({key:value}:rate_limit_type),键格式完全对齐; - 分别查询 RPM(键后缀
requests)与 TPM(键后缀tokens)两个计数器,返回两者中更高的饱和度值; - 纯读操作,绝不递增计数器,因此饱和度检查本身不会污染用量统计;
- 读取走
saturation_check_cache_ttl控制 TTL 的 DualCache(见_get_saturation_value_from_cache(),本地读 Redis); - 容错策略为 Fail-open:任一异常都会记录错误并返回
0.0(按"未饱和"处理),避免限流器自身的故障阻断线上请求。
5.3 两种模式的描述符差异
Generous 模式(饱和度 < 阈值):仅构建一个模型级描述符(model_saturation_check),只校验模型总容量,任何优先级都能使用剩余容量;对应的"低负载不设限、先到先得"语义由 test_fake_calls_case_1_no_rate_limiting_at_capacity 验证。
Strict 模式(饱和度 ≥ 阈值):在模型级描述符之外,再构建按优先级拆分的描述符(priority_model)。_create_priority_based_descriptors() 与 _get_priority_allocation() 的关键逻辑是:
- 显式优先级(在
priority_reservation中有登记):拿到自己的归一化权重与专属 pool key"{model}:{priority}",独占一份model_rpm × weight/model_tpm × weight的保留额度; - 无显式优先级的 key:全部共享
"{model}:default_pool",共同瓜分default_priority那份额度——这意味着低优先级流量内部是"拼池子"关系,容量有盈余时可彼此借用(由test_fake_calls_case_3_spillover_capacity_default_keys覆盖); - 若模型组未配置 rpm/tpm,则不会生成任何优先级描述符,限流退化为仅模型级。
5.4 三阶段原子检查:杜绝"幻影计数"
_check_rate_limits() 采用注释中明确描述的 THREE-PHASE 流程:
- Phase 1(只读):一次性把"模型级 + 当前优先级"的全部描述符、限额检查完毕,不做任何自增;
- Phase 2(决策):依据
saturation >= saturation_threshold决定哪些描述符进入"强制执行集"——model_saturation_check永远强制,priority_model仅在饱和时加入; - Phase 3(原子自增):调用 v3 limiter 的
atomic_check_and_increment_by_n(),由 Redis Lua 脚本保证多描述符的"检查 + 自增 + 到期清理"在进程外原子完成(asyncio.Lock+ 内存实现作为单进程回退)。
这套流程同时修复了两类经典 bug:
- 模型计数器先自增、随后优先级检查失败 → 请求被拦却已消耗容量;
- 优先级计数器未强制却自增 → 指标失真。
只要返回码为 OVER_LIMIT,任何计数器都不会被修改(all-or-nothing)。低饱和度下优先级描述符虽然不参与强制,仍通过 v3 的 should_rate_limit()(read_only=False)以"只计数不拦截"的方式追踪,保证一旦切换为严格模式时用量数据是完整的。
5.5 从请求到 Token 的全链路计数
限流并不只发生在请求进入时:
async_pre_call_hook:每次请求原子 +1 请求计数;async_post_call_success_hook:委托 v3 limiter 写入标准RateLimit响应头,并追加x-litellm-priority(取值为优先级名或default)与x-litellm-rate-limiter-version: v3;async_log_success_event:从standard_logging_object.metadata.user_api_key_auth_metadata中还原优先级,按rate_limit_type(input/output/total)从响应usage中取对应 Token 数,通过 Redis pipeline(async_increment_tokens_with_ttl_preservation,TTL=window)分别递增model_saturation_check与priority_model两个 Token 键——TPM 饱和度与优先级 TPM 消耗由此保持实时准确。
一个值得注意的工程细节:日志打印优先级时会先与安全白名单 {"low", "medium", "high", "default"} 比对,白名单之外的优先级一律以 REDACTED 记录,避免把客户自定义的敏感等级名泄漏进日志。
5.6 拒绝响应与限流窗口
当原子检查返回 OVER_LIMIT,方法抛出 ProxyRateLimitError(其 HTTP 状态即 429)。根据命中的描述符,错误详情与响应头有所区分:
| 命中的描述符 | 错误语义 | 关键响应头 |
|---|---|---|
model_saturation_check |
模型总容量已达上限(与优先级无关) | retry-after(=window)、rate_limit_type、x-litellm-priority |
priority_model |
该优先级保留额度耗尽 | 额外附加 x-litellm-saturation(如 83.00%) |
所有响应头中的 retry-after 取 v3_limiter.window_size,其默认值为环境变量 LITELLM_RATE_LIMIT_WINDOW_SIZE(默认 60 秒,见 parallel_request_limiter_v3.py)。另外,v3 的原子响应包含 OVER_LIMIT 状态,但对未知 descriptor_key 存在一套 fail-closed 兜底:宁可拒绝请求也不让超限流量静默穿透到模型。
5.7 一个直观的算例
类文档给出了理解两种模式的最简算例。假设某模型组上限为 100 RPM、某优先级保留 60% 容量、阈值为 80%:
- 饱和度 < 80%:该优先级最多可用满 100 RPM——模型级上限是唯一约束,剩余容量可被任意优先级借走;
- 饱和度 ≥ 80%:该优先级被限制在 60 RPM——模型级与优先级两个上限同时生效,先到先得让位于公平分配。
六、测试覆盖:五类场景逐一验证
test_dynamic_rate_limiter_v3.py(共 1800+ 行)完整覆盖了文档列出的全部五类场景,且测试命名与文档条目一一对应:
| 文档场景 | 对应测试 |
|---|---|
| 1. 容量未满时不限流 | test_fake_calls_case_1_no_rate_limiting_at_capacity |
| 2. 饱和期间的优先级队列行为 | test_fake_calls_case_2_priority_queue_during_saturation |
| 3. 默认 key 的溢出(借用)容量 | test_fake_calls_case_3_spillover_capacity_default_keys |
| 4. 过度分配时的归一化 | test_fake_calls_case_4_over_allocated_with_normalization |
| 5. 默认优先级取值处理 | test_fake_calls_case_5_default_value_priority_reservation |
此外还有一组值得关注的补充测试:
test_priority_weight_allocation:用{"high": 0.9, "low": 0.1}验证"高优先级拿 90% TPM(900/1000)、低优先级拿 10%(100/1000)",而非错误的五五平分——测试文件开头即注明这是 v3 相对旧版的核心修复点;test_concurrent_priority_requests/test_100_concurrent_priority_requests/test_concurrent_pre_call_hooks_stress:并发压力与竞态验证;test_default_priority_shared_pool:验证所有默认优先级 key 共享同一额度池;test_tpm_only_model_enforces_priority_and_model_capacity:仅配置 TPM 的模型同样正确执行优先级与容量约束;test_priority_429_includes_model_name_and_configured_limits:429 错误体包含模型名、TPM/RPM、剩余额度等排障信息。
测试通过注入 TimeController 伪时钟推进 time.time()、以 Router(model_list=[...]) 声明 rpm/tpm、用 mock 的 AsyncMock 替代 Redis 依赖,可以在不启动真实 Redis 的前提下跑通上述场景。
七、适用前提与使用注意
综合文档与源码,落地 v3 动态限流器时建议确认以下几点:
- 模型组必须声明 rpm/tpm:饱和度计算、保留额度换算均以
ModelGroupInfo的rpm/tpm为分母。模型组无此信息时,优先级描述符不会生成,功能退化为空操作。 - 依赖 v3 限流器的 Redis 追踪:原子 Lua 脚本与多实例一致读写在真实部署中依赖 Redis;其"读 Redis、跳过本地缓存"的饱和度查询对
saturation_check_cache_ttl做了缓存折衷,可根据 Redis 负载调整。 - 企业许可证:为优先级保留 rpm/tpm 是 premium 特性,需要在环境变量中配置
LITELLM_LICENSE,否则将记录错误日志并回退到默认权重。 - 显式区分"文档默认值"与"代码默认值":
default_priority与saturation_threshold在本文所依据的文档示例(0.5 / 0.80)与当前源码(0.25 / 0.50)中不同,配置前请以本仓库实际版本与 types/utils.py 中字段定义为准。 - 限流窗口:所有计数键的过期时间与
retry-after均基于LITELLM_RATE_LIMIT_WINDOW_SIZE(默认 60s),多实例共享 Redis 时保证窗口语义一致。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00