跳到主要内容

数据截至 (上游 commit 3f15dc32871c)

Router:模型组、负载均衡、冷却与回退

30 秒导读: 前面四章讲的是「怎么把一次调用打到一个模型上」。这一章讲的是上面那一层:你有 4 把 Azure GPT-4o 的 key、2 个 OpenAI 账号,想让它们对外只是一个名字 gpt-4o,还想要负载均衡、限流退避、坏节点自动摘除、模型挂了自动换一家。Router 就是干这个的——它是 litellm 从「SDK」变成「能上生产」的那道分水岭。


1. 这是什么(零基础也能懂)

一句话定义: Router 是一个调度层——你给它一张「同一个对外模型名下挂了哪几份真实部署」的表,它替你决定每次请求打给哪一份,并在失败时自动重试或换一家。

它解决什么问题。 假设你在做一个内部 AI 网关:

  • 单个 Azure 部署有 TPM/RPM 限额,一份不够用,你开了 4 份 → 需要负载均衡
  • 其中一份突然 429/500 了 → 需要暂时别再往那儿打(冷却)。
  • 整个 OpenAI 区域挂了 → 需要整体切到 Anthropic(跨模型组回退)。
  • 用户塞了一个 300K token 的 prompt → 需要换一个上下文更长的模型,而不是直接报错。

这四件事,你自己写要写几百行状态管理;Router 把它们做成了固定流水线。

核心概念:模型组(model group)。 这是全章的地基,只有一句话:

model_listmodel_name 相同的若干条,就是一个「模型组」;每一条叫一份「部署(deployment)」。

# 示意,非源码
from litellm import Router

router = Router(
model_list=[
# ↓ 这三条 model_name 都叫 "gpt-4o" —— 它们构成一个模型组
{"model_name": "gpt-4o",
"litellm_params": {"model": "azure/my-gpt4o-east", "api_key": "...", "rpm": 600}},
{"model_name": "gpt-4o",
"litellm_params": {"model": "azure/my-gpt4o-west", "api_key": "...", "rpm": 600}},
{"model_name": "gpt-4o",
"litellm_params": {"model": "openai/gpt-4o", "api_key": "...", "rpm": 200}},
# ↓ 另一个模型组,用来当回退目标
{"model_name": "claude",
"litellm_params": {"model": "anthropic/claude-sonnet-4-5", "api_key": "..."}},
],
routing_strategy="usage-based-routing-v2", # 怎么在组内挑一份
num_retries=2, # 组内重试几次
fallbacks=[{"gpt-4o": ["claude"]}], # 组都不行了,换哪个组
redis_host="...", # 多副本网关共享状态
)

resp = await router.acompletion(model="gpt-4o", messages=[{"role": "user", "content": "hi"}])

调用方只写 model="gpt-4o"——组名;三份部署里选哪一份、失败了怎么办,全在 Router 内部。

一句话直觉: 把模型组当成 Nginx 的 upstream,把部署当成 upstream 里的 server。weight 是权重、冷却是健康检查摘除、fallbacks 是「整个 upstream 挂了就转到备用 upstream」。差别只在于:LLM 的「健康」不是能不能 TCP 连上,而是这一分钟的配额还剩多少、错误率多高、上下文放不放得下


2. 顶层全景(它大概怎么转)

一次 router.acompletion() 从外到内套了三层壳,最里面才是 01 章讲的那条 completion() 主线。怎么读这张图:从上往下是「由外到内」,任何一层成功就直接一路返回,不再往下走。

router.acompletion(model="gpt-4o", ...) ← 调用方只知道组名


┌───────────────────────────────────────┐
│ ① 跨模型组回退壳 │ gpt-4o 整组不行 → 换 claude 组
│ async_function_with_fallbacks │ router.py:6540
└───────────────────┬───────────────────┘

┌───────────────────────────────────────┐
│ ② 同组重试壳 │ 同一组内再试 num_retries 次
│ async_function_with_retries │ router.py:6635
└───────────────────┬───────────────────┘

┌───────────────────────────────────────┐
│ ③ 选一份部署 │ 筛候选 → 按策略挑一份
│ async_get_available_deployment │ router.py:11129
└───────────────────┬───────────────────┘

┌───────────────────────────────────────┐
│ ④ 真正发请求 │ → 01/02/03 章的主线
│ litellm.acompletion(**deployment) │ router.py:2923 _acompletion
└───────────────────────────────────────┘

各部件一句话职责:

部件干什么在哪
Router.__init__吃下 model_list,建索引、建缓存、装策略、校验回退表litellm/router.py:382
set_model_list把每条配置补上 model_info.id,建 model_name → 下标 索引litellm/router.py:8174
async_get_available_deployment选部署总入口:预路由钩子 → 筛候选 → 策略选一litellm/router.py:11129
async_get_healthy_deployments候选过滤流水线(健康/冷却/预算/上下文/tag/order)litellm/router.py:10993
router_strategy/各种「怎么挑一份」的策略实现见 §3.3 表
async_function_with_retries同组重试 + 退避 + 「这个错该不该重试」litellm/router.py:6635
async_function_with_fallbacks_common_utils跨组回退:按异常类型挑回退表litellm/router.py:6287
router_utils/cooldown_handlers.py失败后决定要不要把这份部署关小黑屋_set_cooldown_deployments:413
Router.cache(DualCache)内存 + Redis 双层,存 TPM/RPM/延迟/冷却,多副本共享litellm/router.py:584

主线走一遍(高层): 请求进来 → 组名解析成一批候选部署 → 一串过滤器砍掉不能用的 → 策略从剩下的里挑一份 → 发请求 → 成功回调记账、失败回调可能触发冷却 → 失败则重试 → 还失败则换组。


3. 核心原理

3.1 模型组是怎么长出来的

要解决的小问题: 用户写的是一个扁平列表,Router 需要「按名字快速拿到一组部署」。

set_model_list(litellm/router.py:8174)遍历原始列表,给每条没有 id 的部署生成一个稳定 id(_generate_model_id),然后 _create_deployment 把它塞进 self.model_list 并顺手建三张索引:model_id_to_deployment_index_mapmodel_name_to_deployment_indicesteam_model_to_deployment_indices。最后一行 self.model_names = {m["model_name"] for m in model_list} 就是「有哪些模型组」。

要点有两个:

  • id 是冷却和计量的主键。 后面所有「这份部署本分钟用了多少 tpm」「这份部署在不在冷却里」都以 model_info.id 为 key,不是以模型名。
  • 一份配置可能展开成多份部署。 例如 organization 传了一个列表时,会按 organization 循环调用 _create_deployment(router.py:8212 起),一条配置变多条部署。

__init__ 里还有一步容易被忽略的启动期校验:validate_fallbacks(litellm/router.py:1707,在 router.py:696 / :710 被调用)要求回退表的每个元素都是「恰好一个 key 的 dict」,写错了在启动就炸,而不是等到线上真回退时才炸。

3.2 候选过滤:选之前先砍

要解决的小问题: 「组里有 6 份」不等于「6 份都能接这个请求」。有的在冷却、有的这分钟配额用完了、有的上下文窗口装不下、有的不在允许的区域。

async_get_healthy_deployments(litellm/router.py:10993)是一条顺序过滤流水线,每一步都只做减法:

组名 "gpt-4o"

▼ _common_checks_available_deployment:9930 ← 别名/通配符/团队路由 → 展开成候选池
[6 份候选]

├─▶ 团队 / web-search 过滤 filter_team_based_models
├─▶ 健康检查过滤 _async_filter_health_check_unhealthy_deployments
├─▶ 冷却过滤(§3.5) _filter_cooldown_deployments
├─▶ 被 block 的过滤 _filter_blocked_deployments
├─▶ 回调过滤(预算 / 亲和) async_callback_filter_deployments:7161
├─▶ pre_call_checks(§3.2.1) _pre_call_checks:9698
├─▶ tag 路由过滤 get_deployments_for_tag
└─▶ order / 加权失败排除 _get_order_filtered_deployments

[剩 2 份] ── 空了就 raise ──▶ async_raise_no_deployment_exception

注意顺序有意义:冷却过滤发生在 _pre_call_checks 之前,所以「已经进小黑屋的部署」根本不参与后面的 token 计数,省掉了昂贵的 tiktoken 调用。

3.2.1 _pre_call_checks:四道前置筛(要显式开启)

只有 enable_pre_call_checks=True 且带 messages 时才跑(router.py:11081)。_pre_call_checks(litellm/router.py:10537)在一个循环里对每份部署做四类判断:

检查依据砍掉后记的标志
上下文窗口model_info.max_input_tokens vs litellm.token_counter(messages)_context_window_error
RPM 软限本分钟 model:rpm:HH-MM 本地缓存计数 vs litellm_params.rpm_rate_limit_error
区域合规request_kwargs["allowed_model_region"] + is_region_allowed
参数支持度get_supported_openai_params 里没有 response_format 等特殊参数

巧妙处 1:token 只数一次,而且能不数就不数。 input_tokens 是个惰性变量,只有当某份部署真的声明了 max_input_tokens 时才第一次调用 token_counter,之后复用(router.py:10565 + :10597)。对没设上下文上限的模型组,这段热路径上完全跳过 tiktoken。

巧妙处 2:全砍光时抛的异常是有讲究的。 如果所有部署都被砍了,它先看是不是限流再看是不是上下文超限(router.py:10690):限流抛 RouterRateLimitErrorBasic(让通用回退逻辑接手),上下文超限抛 litellm.ContextWindowExceededError(让 context_window_fallbacks 接手)。异常类型在这里就是给上层回退逻辑的信号——这正好接上 04 章的异常分类。

3.3 策略:从剩下的里挑一份

要解决的小问题: 候选还剩 3 份,挑哪个?

入口在 async_get_available_deployment(router.py:11129)。它做三件事:

  1. 预路由钩子:async_pre_routing_hook(router.py:11498)——这一步可以改写 model 本身(§3.3.2)。
  2. 解析策略:_get_routing_context(router.py:1236)按模型名查它属于哪个 routing_group,拿到 (策略名, 选择器对象);没配 routing_groups 就落到隐式的 "default" 组。
  3. :simple-shuffle 走函数直调,其余走 _select_deployment_async(router.py:1277)——一个 match strategy 分派表。

3.3.1 内置策略横向对比

routing_strategy怎么挑状态从哪来实现(文件:行 符号)
simple-shuffle(默认)依次看 weight / rpm / tpm,谁先有值就按它加权随机;全没有就均匀随机纯静态配置,零运行时状态router_strategy/simple_shuffle.py:21 simple_shuffle
least-busy挑「在飞请求数」最小的log_pre_api_call 计数 +1,成功/失败回调 −1least_busy.py:16 LeastBusyLoggingHandler / :160 _get_available_deployments
usage-based-routing(v1)挑本分钟 TPM 最低的成功回调按模型组维度累加 tpm/rpmlowest_tpm_rpm.py:19 LowestTPMLoggingHandler / :149
usage-based-routing-v2同 v1,但按单份部署缓存、用 Redis mget 批量读、incr 原子加成功回调 + 调用前 async_pre_call_check 先抢配额lowest_tpm_rpm_v2.py:32 LowestTPMLoggingHandler_v2 / :141 / :431
latency-based-routing挑近 N 次平均延迟最低的(流式看 TTFT),再在 lowest_latency_buffer 内随机成功/失败回调维护每份部署的延迟滑窗(默认最多 10 条)lowest_latency.py:28 LowestLatencyLoggingHandler / :356
cost-based-routing挑单位 token 成本最低、且没超 tpm/rpm 的成功回调 + litellm 成本表lowest_cost.py:13 LowestCostLoggingHandler / :181
lar1先把请求分类,再按阈值挑请求 metadata + 阈值配置lar1_routing.py:80 LAR1RoutingStrategy

最值得读的是最简单的那个。simple_shuffle(router_strategy/simple_shuffle.py:21)不到 70 行,核心就三步:

for weight_by in ["weight", "rpm", "tpm"]: # 依次尝试三种权重来源
weight = healthy_deployments[0].get("litellm_params").get(weight_by, None)
...
weights = [weight / total_weight for weight in weights]
selected_index = random.choices(range(len(weights)), weights=weights)[0]

它的默认地位很关键:因为不读运行时状态,它是唯一一个多副本网关之间零协调开销的策略——所以 litellm 把它设成默认(router.py:382 起构造函数的 routing_strategy 默认值)。

v2 版 TPM/RPM 那个「先抢后用」很值得学。 LowestTPMLoggingHandler_v2.async_pre_call_check(lowest_tpm_rpm_v2.py:135)不是选完就发请求,而是在信号量内先对 {id}:{model}:rpm:{minute} 做一次 _increment_value_in_current_window,发现 incr 后的值超限就当场抛 RateLimitError。这解决的是并发竞态:十个协程同时看到「还剩 1 个配额」然后一起冲。设计说明直接写在 docstring 里(lowest_tpm_rpm_v2.py:135,指向 issue #2994)。

3.3.2 两类「不是选择器」的东西,别搞混

router_strategy/ 目录下并非所有文件都在做「从 N 个里挑 1 个」。按职责分成三类:

类别干什么在流水线的哪一步代表
选择器从候选里挑 1 份过滤完之后simple_shuffle / lowest_latency / lowest_tpm_rpm_v2 / least_busy / lowest_cost
过滤器砍掉一批候选,不做最终决定过滤流水线中tag_based_routing.py:450 get_deployments_for_tagbudget_limiter.py:115 RouterBudgetLimiting.async_filter_deployments
预路由器改写 model 名本身,之后照常走普通流程一切之前auto_router / complexity_router / adaptive_router / quality_router

第三类是较新的方向:它们不参与「组内挑一份」,而是接在 Router.async_pre_routing_hook(router.py:11498)上,把用户请求的那个「虚拟模型名」翻译成一个真实模型组名,然后交还给上面那条普通流水线。

目录定位分类依据入口符号
auto_router/语义路由把 prompt 做 embedding,和预设 Route 比相似度auto_router.py:24 AutoRouter / :116 async_pre_routing_hook
complexity_router/规则复杂度路由7 个维度的加权打分(token 数、代码特征、推理标记…),零 API 调用、亚毫秒complexity_router.py:666 ComplexityRouter / :935 classify
adaptive_router/老虎机(bandit)路由按请求类型分 7 桶,每个 (类型, 模型) 维护 Beta(α,β) 后验做 Thompson 采样;质量分 + 归一化成本加权adaptive_router/adaptive_router.py:85 AdaptiveRouter / :211 pick_model
quality_router/质量分层路由model_info 里声明的 quality_tier 建索引后挑quality_router/quality_router.py:38 QualityRouter

adaptive_router 还带一条反馈闭环:调用后的钩子用正则和 tool-call 检测给「这一轮服务的模型」记功过,批量攒在内存里再定期落 Postgres(见 adaptive_router/README.mdsignals.py)。

3.4 失败处理的三层楼

要解决的小问题: 请求失败了,是「换一份同样的部署再试」,还是「换一个厂商」,还是「直接把错误抛给用户」?答案取决于错误类型

怎么读这张图:从上往下,任一层成功即返回(命中即停)。

一次 acompletion

┌───┴──────────────────────────────────────────────┐
│ 第 0 层:直接打 │ ── 成功 ──▶ 返回
│ response = await make_call(...) │ (带 x-litellm-attempted-retries: 0)
└───┬──────────────────────────────────────────────┘
│ 失败 → should_retry_this_error:6858 判「该不该重试」→ 不该就直接抛

┌──────────────────────────────────────────────────┐
│ 第 1 层:同模型组重试 × num_retries │ ── 成功 ──▶ 返回
│ 每次都重新走「选部署」,刚失败那份可能已进冷却被筛掉 │
│ 退避:_time_to_sleep_before_retry:6968 │
└───┬──────────────────────────────────────────────┘
│ 重试耗尽

┌──────────────────────────────────────────────────┐
│ 第 2 层:跨模型组回退 │ ── 成功 ──▶ 返回
│ 按异常类型挑回退表 → 换 model group → 从头再来一遍 │
│ 深度上限 max_fallbacks(默认 5) │
└───┬──────────────────────────────────────────────┘
│ 全部失败

抛出最后一个异常

第 1 层:重试(async_function_with_retries,router.py:6635)

三个关键设计:

  • 不是所有错都值得重试。 should_retry_this_error(router.py:6858)是一张判决表:NotFoundError 直接抛;ContextWindowExceededError + 配了 context_window_fallbacks → 直接抛(让第 2 层去处理);AuthenticationError 只有组里不止一份部署时才重试;组内已无健康部署也直接抛
  • 退避是「有条件的」。 _time_to_sleep_before_retry(router.py:6968)开头就写着:只要同组还有别的健康部署,就 return 0 —— 立即重试,不等。因为换一份部署试比等下去便宜。只有单部署组或组内全灭时,才走 litellm._calculate_retry_after,并优先读响应头里的 retry-after
  • 重试次数写进响应头。 成功后 add_retry_headers_to_response(router_utils/add_retry_fallback_headers.py:205)往 _hidden_params.additional_headers 里塞 x-litellm-attempted-retries / x-litellm-max-retries。调用方能从响应里看出「这次是重试来的」。

还有个细节:每轮 except 里都会 original_exception = e(router.py:6673),所以最终抛出的是最后一次的错误,不是第一次的——排障时看到的是最新状态。

第 2 层:回退(async_function_with_fallbacks_common_utils,router.py:6287)

回退有三张互不相同的表,靠异常类型分流,而这些异常正是 04 章讲的统一异常映射产出的:

回退表触发异常语义判断位置
context_window_fallbackslitellm.ContextWindowExceededErrorprompt 太长 → 换个上下文更大的组router.py:6414
content_policy_fallbackslitellm.ContentPolicyViolationError被内容安全拦了 → 换个策略更松的组router.py:6447
fallbacks(通用)其余一切整组不可用 → 换备用组router.py:6479

分流有个明确的兜底约定:如果命中了 ContextWindowExceededError 但用户没配 context_window_fallbacks,不会直接抛错,而是打一行日志后继续往下走通用 fallbacks(router.py:6440-6443)。

真正执行回退的是 run_async_fallback(router_utils/fallback_event_handlers.py:277)。它的形状是递归而不是循环嵌套:

# 示意,非源码
if fallback_depth >= max_fallbacks: # 基线条件:深度到顶就抛原始异常
raise original_exception
for mg in fallback_model_group:
if mg == original_model_group: # 不回退给自己
continue
kwargs["model"] = mg
kwargs["fallback_depth"] = fallback_depth + 1
return await litellm_router.async_function_with_fallbacks(**kwargs) # 整条链重来

重点看最后一行:回退目标不是直接发请求,而是重新进入整条流水线——所以备用组同样享受候选过滤、策略选择和自己的重试。深度靠 fallback_depth / max_fallbacks(默认 ROUTER_MAX_FALLBACKS = 5,litellm/constants.py:10)刹车,防止 A→B→A 无限绕。

成功后同样打头:add_fallback_headers_to_response(add_retry_fallback_headers.py:222)写 x-litellm-attempted-fallbacks;开了 include_fallback_errors 还会把沿途每一次失败的错误信息 JSON 化塞进 x-litellm-fallback-errors。注意它故意不写 max_fallbacks,注释说明理由是避免响应头膨胀(add_retry_fallback_headers.py:237)。

3.5 冷却:让坏节点自己退场

要解决的小问题: 一份部署刚 429 了,下一个请求还是有可能被随机挑到它。得有个「小黑屋」。

冷却是写路径和读路径分离的:

失败发生


deployment_callback_on_failure (router.py:7177)
│ · 本分钟失败数 +1
│ · 定冷却时长:部署配置 > 响应头 retry-after > router 默认(5s)

_set_cooldown_deployments (cooldown_handlers.py:413)
│ · _should_run_cooldown_logic:258 ← 该不该跑这套逻辑
│ · _should_cooldown_deployment:317 ← 这次失败够不够格

CooldownCache.add_deployment_to_cooldown (cooldown_cache.py:64)
写 key "deployment:{id}:cooldown",TTL = 冷却秒数

═══════════════ DualCache(内存 + Redis)═══════════════

读:_async_get_cooldown_deployments (cooldown_handlers.py:478)

async_get_healthy_deployments 里把它们筛掉 (router.py:11052)

够不够格进小黑屋,判定挺讲究(_should_cooldown_deployment,cooldown_handlers.py:317)。默认(没设 allowed_fails / allowed_fails_policy)走的是错误率而不是错误数:

条件结果常量
429,且组内不止一份部署冷却
本分钟全失败,且请求数 ≥ 1000冷却SINGLE_DEPLOYMENT_TRAFFIC_FAILURE_THRESHOLD(constants.py:77)
失败率 > 50%,请求数 ≥ 5,且组内不止一份冷却DEFAULT_FAILURE_THRESHOLD_PERCENT(constants.py:28)、DEFAULT_FAILURE_THRESHOLD_MINIMUM_REQUESTS(constants.py:80)
litellm._should_retry(status) 为假(如 401/404)冷却
其余不冷却

反复出现的 is_single_deployment_model_group 是全章最实用的一条工程经验:只有一份部署的模型组,默认不因错误率进冷却——否则一冷却整个组就直接不可用了,还不如让请求带着真实错误打过去。

再往上一层还有两道闸:_is_cooldown_required(cooldown_handlers.py:205)明确规定 4xx 里只有 429/401/408/404 才冷却,其余 4xx(比如 400 参数错)是你自己的请求有问题,冷却部署毫无意义;而 APIConnectionError 字样的错误直接跳过冷却。

计量数据从哪来?成功侧是 deployment_callback_on_success(router.py:7015)调 increment_deployment_successes_for_current_minute,失败侧是 deployment_callback_on_failure(router.py:7177)调 increment_deployment_failures_for_current_minute;async_deployment_callback_on_failure(router.py:7264)另外负责把 RPM 计数补上(失败也占了一次请求配额)。

存进缓存的异常字符串会先过一遍脱敏:CooldownCacheSensitiveDataMasker 只保留前 50 个字符(cooldown_cache.py:44),避免把 key 之类的东西写进 Redis。

3.6 通配符模型:openai/* 也能路由

要解决的小问题: 你不想给 OpenAI 的每个模型都写一条配置。

PatternMatchRouter(router_utils/pattern_match_deployments.py:50)让你写一条 model_name: "openai/*" 就覆盖一整片。三个方法就是全部:

  • add_pattern(:62)→ _pattern_to_regex(:87):实现只有一行 —— re.escape(pattern).replace(r"\*", "(.*)"),先整体转义再把 * 换成捕获组,避免用户模式里的 . 被当通配符。
  • route(:123):按 PatternUtils.sorted_patterns(:34,依据 calculate_pattern_specificity)从最具体的模式开始匹配,所以 openai/gpt-4* 会排在 openai/* 前面。
  • 命中后 set_deployment_model_name(:159)把捕获到的部分回填进部署的 litellm_params.model,于是 openai/* 这条配置能真的发出 openai/gpt-4.1-mini

接入点在 _common_checks_available_deployment_try_early_resolve_deployments_for_model_not_in_names(router.py:10730 起):模型名不在 self.model_names 里时,依次试团队专属部署 → 全局 pattern → 团队 pattern → default deployment,顺序注释写明了「命名的团队部署要盖过通配符路由」。

3.7 多副本网关怎么共享状态

要解决的小问题: 生产上网关会开 8 个副本。副本 A 把某部署打到限流了,副本 B 怎么知道?

答案是 Router.cache——一个 DualCache(router.py:584),内存 + Redis 两层。传了 redis_url / redis_host 才会有 Redis 层,否则退化成纯内存(单副本可用,多副本各算各的)。

跑在这条通道上的共享状态有四类:

共享的东西key 形状写者
冷却名单deployment:{id}:cooldownCooldownCache.add_deployment_to_cooldown
TPM / RPM 计数global_router:{id}:{model}:tpm|rpm:{minute}RouterCacheEnum(litellm/types/router.py:823)
延迟滑窗{model_group}_mapLowestLatencyLoggingHandler
预算已花_async_get_cache_keys_for_router_budget_limiting 生成RouterBudgetLimiting

性能上有两处让步:读冷却用 async_batch_get_cache(cooldown_cache.py:134 async_get_active_cooldowns),一次 mget 拿全部 id,注释直白写着「每次 redis 调用要 ~100ms」;写 TPM/RPM 则由 BaseRoutingStrategy(router_strategy/base_routing_strategy.py:15)的 setup_sync_task(:30)攒批,v2 策略构造时设的同步间隔是 0.1 秒(lowest_tpm_rpm_v2.py:57)。代价是一致性:0.1 秒窗口内多副本的计数会偏低,所以 v2 才要在调用前再 incr 一次做最终把关。


4. 深入实现:一次 acompletion 的调用链

按顺序读这张表就是一遍完整走读(异步路径;同步路径把 async_ 前缀去掉,选部署走 get_available_deployment:11702):

符号位置做了什么
1acompletionrouter.py:2111original_function=self._acompletion,进回退壳
2async_function_with_fallbacksrouter.py:6540try 里调重试壳;except 交给 ..._common_utils
3async_function_with_retriesrouter.py:6635首发 + 失败后 num_retries 轮循环
4make_callrouter.py:6819真正 await _acompletion,并处理响应头
5_acompletionrouter.py:2923选部署 → 信号量 → litellm.acompletion
6async_get_available_deploymentrouter.py:11129预路由钩子 → 筛 → 选
7async_get_healthy_deploymentsrouter.py:10993过滤流水线(§3.2)
8_common_checks_available_deploymentrouter.py:10805别名 / 通配符 / 团队 / default 解析出候选池
9_get_routing_context + _select_deployment_asyncrouter.py:1236 / :1277定策略、调选择器
10deployment_callback_on_success / _on_failurerouter.py:7015 / :7177记账、可能触发冷却

统一包装 factory_function(router.py:5671)。 Router 支持的不只是 completion——responses API、files、fine-tuning、vector store、video、container、OCR、search…… 上百种 call_type。factory_function 吃进一个原始 litellm 函数和一个 call_type 字面量,吐出一个包好的 wrapper;绝大多数最终落到 _generic_api_call_with_fallbacks(router.py:4781),从而复用同一套重试/回退/冷却机制。这是 litellm 能让「所有 API 类型都自动带路由能力」的关键,代价是那个 Literal 列表长得离谱(router.py:5674 起的 call_type Literal)。

routing_groups:一个 Router 里多种策略。 routing_strategy_init(router.py:1004)只装配隐式 "default" 组的选择器;_init_routing_groups(router.py:1033)允许你给不同模型子集配不同策略和参数,每组持有独立的选择器实例(状态互不干扰)。约束有三条,违反就在启动抛错:组名不能叫 default、组名不能重复、一个模型名只能属于一个组。

apply_default_settings(router.py:842) 目前是个几乎空的钩子:__init__ 末尾(router.py:829)调它,内部只是用空列表调一次 add_optional_pre_call_checks。它的价值在于给「默认开启某些前置检查」留了一个稳定的挂载点——真正的活都在 add_optional_pre_call_checks(router.py:1792)里,那里把 deployment_affinity / session_affinity / encrypted_content_affinity / prompt_caching 这些可选检查注册成 CustomLogger 回调,再由 §3.2 的 async_callback_filter_deployments(router.py:7530)统一触发。


5. 巧妙之处(可以直接借鉴的)

  1. 异常类型即路由信号。 _pre_call_checks 全砍光时不是笼统抛「无可用部署」,而是分别RouterRateLimitErrorBasicContextWindowExceededError(router.py:10690),让上层三张回退表能各归各位。分类清楚的异常,比一堆 if 更省事。

  2. 「还有备胎就别睡」的退避。 _time_to_sleep_before_retry(router.py:6968)在同组尚有健康部署时直接 return 0。多数重试库只会指数退避,这里的洞察是:换一台比等一会更快

  3. 单部署组豁免冷却。 _should_cooldown_deployment(cooldown_handlers.py:317)里的 is_single_deployment_model_group:没有备胎时把唯一的部署摘除,等于自己把自己搞成 100% 不可用。

  4. 回退是递归而非展开。 run_async_fallback(fallback_event_handlers.py:277)回到 async_function_with_fallbacks 而不是直接发请求,于是备用组自动继承全部机制;fallback_depth 做基线条件防环。

  5. 把「这次经历了什么」写进响应头。 重试次数、回退次数、沿途错误全部进 _hidden_params.additional_headers(add_retry_fallback_headers.py:205 / :222),线上排障不用翻日志。

  6. 通配符路由的正则只有一行。 re.escape(pattern).replace(r"\*", "(.*)")(pattern_match_deployments.py:87)先整体转义再放开 *,顺手把「用户模式里的点号被当通配符」这个经典 bug 堵死;配套 sorted_patterns 保证具体模式优先。

  7. 热路径上惰性算 token。 _pre_call_checksinput_tokens 只在真有部署声明上限时才算,且全循环只算一次(router.py:10597)。源码注释直接标注了 tiktoken 是这段的主要开销。


6. 边界与局限

  • simple-shuffle 之外的策略都需要共享状态才准。 单副本内存缓存下没问题;多副本必须接 Redis,否则每个副本按自己看到的局部计数做决策。
  • 批量同步有窗口。 TPM/RPM 走 0.1 秒批量写(lowest_tpm_rpm_v2.py:57),窗口内计数偏低。v2 用「调用前 incr」补救,其他策略没有这层保护。
  • async_get_available_deployment 只对五种策略走异步路径。 开头的 if 明确列出 usage-based-routing-v2 / simple-shuffle / cost-based-routing / latency-based-routing / least-busy;其他策略会退回同步 get_available_deployment(router.py:11142-11149),注释写着这是为了「防止未实现异步选部署的策略出现回归」。
  • enable_weighted_failover 只支持异步。 构造函数 docstring 明写:同步 router.completion() 走普通回退流程(router.py:487 的参数说明)。
  • _pre_call_checks 默认关闭。 不显式开 enable_pre_call_checks=True,上下文窗口和区域过滤都不会跑。
  • 它管调度,不管钱和人。 HTTP 认证、虚拟 key、预算入库、多租户全在 06 章;Router 层的 budget_limiter 只做「超了就从候选里砍掉」这一件事。
  • 代码里有自认的技术债。 routing_strategy_init(router.py:1026)有一条 TODO 承认 self.<strategy>_loggerself._group_selectors 双份存储是为了向后兼容留下的反模式。

7. 横向对比:和其他章的分工

关切谁负责章节
一次调用怎么走完litellm.completion() 主线01-request-lifecycle.md
参数怎么翻译成各家格式BaseConfig 契约02-translation-layer.md
HTTP 和流式增量怎么拼统一传输层03-http-and-streaming.md
日志 / 缓存 / 成本 / 异常分类@client 装饰器04-cross-cutting-wrapper.md
多份部署之上怎么调度Router本章
认证 / 计费 / 多租户Proxy Gateway06-proxy-gateway.md

一句话定位:04 章把错误分好类,05 章根据分类决定重试还是换厂。 两章的接缝就是 should_retry_this_error(router.py:6858)和三张回退表的 isinstance 判断。


8. 代码地图(导航索引)

主题文件符号
Router 构造与全部配置项litellm/router.pyRouter.__init__(:382)
模型组建索引litellm/router.pyset_model_list(:7704)、_create_deployment
回退表启动期校验litellm/router.pyvalidate_fallbacks(:1707)
策略装配 / 分组策略litellm/router.pyrouting_strategy_init(:1004)、_get_routing_context(:1236)
可选前置检查挂载点litellm/router.pyapply_default_settings(:842)、add_optional_pre_call_checks(:1792)
对外入口litellm/router.pycompletion(:1901)、acompletion(:2111)、factory_function(:5671)
选部署总入口litellm/router.pyasync_get_available_deployment(:11129)、get_available_deployment(:11702)
候选过滤流水线litellm/router.pyasync_get_healthy_deployments(:10993)、_common_checks_available_deployment(:10805)
上下文 / 区域 / RPM 前置过滤litellm/router.py_pre_call_checks(:10537)
预路由钩子分派litellm/router.pyasync_pre_routing_hook(:11498)
同组重试litellm/router.pyasync_function_with_retries(:6635)、should_retry_this_error(:6858)、_time_to_sleep_before_retry(:6968)
跨组回退litellm/router.pyasync_function_with_fallbacks(:6540)、async_function_with_fallbacks_common_utils(:6287)
回退执行与事件litellm/router_utils/fallback_event_handlers.pyrun_async_fallback(:277)、get_fallback_model_group(:217)
重试/回退响应头litellm/router_utils/add_retry_fallback_headers.pyadd_retry_headers_to_response(:205)、add_fallback_headers_to_response(:222)
成功/失败记账litellm/router.pydeployment_callback_on_success(:7015)、deployment_callback_on_failure(:7177)、async_deployment_callback_on_failure(:7264)
冷却判定litellm/router_utils/cooldown_handlers.py_should_cooldown_deployment(:317)、_set_cooldown_deployments(:413)、_is_cooldown_required(:205)
冷却存储litellm/router_utils/cooldown_cache.pyCooldownCache(:38)、add_deployment_to_cooldown(:71)、async_get_active_cooldowns(:134)
通配符路由litellm/router_utils/pattern_match_deployments.pyPatternMatchRouter(:50)、add_pattern(:62)、route(:123)
默认策略litellm/router_strategy/simple_shuffle.pysimple_shuffle(:21)
用量策略 v2litellm/router_strategy/lowest_tpm_rpm_v2.pyLowestTPMLoggingHandler_v2(:32)、async_pre_call_check(:135)
延迟 / 成本 / 忙闲策略litellm/router_strategy/LowestLatencyLoggingHandler(lowest_latency.py:28)、LowestCostLoggingHandler(lowest_cost.py:13)、LeastBusyLoggingHandler(least_busy.py:16)
过滤型策略litellm/router_strategy/get_deployments_for_tag(tag_based_routing.py:450)、RouterBudgetLimiting(budget_limiter.py:94)
跨副本批量同步基类litellm/router_strategy/base_routing_strategy.pyBaseRoutingStrategy(:15)、setup_sync_task(:30)
新型预路由器litellm/router_strategy/AutoRouter(auto_router/auto_router.py:24)、ComplexityRouter(complexity_router/complexity_router.py:666)、AdaptiveRouter(adaptive_router/adaptive_router.py:85)、QualityRouter(quality_router/quality_router.py:38)
冷却阈值常量litellm/constants.pyDEFAULT_COOLDOWN_TIME_SECONDS(:34)、DEFAULT_FAILURE_THRESHOLD_PERCENT(:28)、ROUTER_MAX_FALLBACKS(:10)
缓存 key 形状litellm/types/router.pyRouterCacheEnum(:823)、RoutingStrategy(:814)