跳到主要内容

数据截至 (上游 commit 3f15dc32871c)

第 6 章:AI Gateway —— 把 SDK 包成带认证与计费的多租户网关

前五章讲的都是:你 pip install litellm,在自己进程里调 completion()。这一章讲的是服务:同一套代码被塞进一个 FastAPI 应用,变成一个对外的 HTTP 网关,别人拿一把 sk-… 开头的 key 来调,你能管住「谁能调、能调多少、花了多少」。


6.1 为什么 Router 之上还要再包一层

05 章Router 已经解决了「一个模型组下面挂多个部署、坏了自动冷却、超了自动回退」。但 Router 是进程内对象:它信任调用方,没有身份概念,也不知道钱花在谁头上。

一个组织要把 LLM 能力开放给二十个团队用,还缺三件东西:

缺的东西具体是什么本章对应小节
身份这个请求是谁发的?属于哪个团队/组织?6.4
配额他这分钟还能发几个请求?这个月还剩多少预算?6.5
账本这次调用花了多少钱?怎么落到数据库、按天按团队聚合?6.7

Proxy 层就是补这三样。它不重新实现调模型——真正发请求的还是 Router 和 0104 章那套 SDK。它做的是在 SDK 前后各加一段:前面认人、后面记账。

一句话直觉: 把 SDK 当成「数据库引擎」,Proxy 就是套在外面的「数据库服务器」——加了连接认证、权限、配额和审计日志,SQL 执行内核没变。


6.2 顶层全景:一次请求穿过网关的六道工序

先看请求怎么走。从上往下读,每一格是一道必须过的工序,任何一道拒绝就直接返回错误:

HTTP POST /v1/chat/completions
Authorization: Bearer sk-… 客户端只认 OpenAI 协议


① 认证 key/JWT → 身份对象(user/team/org + 各级配额)
│ proxy/auth/user_api_key_auth.py

② 闸门 RPM/TPM/并发限流、预算检查与「预留」
│ proxy/hooks/*

③ 统一预处理 注入 metadata、建日志对象、跑输入侧 guardrail
│ proxy/common_request_processing.py

④ 分发 HTTP 路由 → Router 的哪个方法(acompletion / aembedding / …)
│ proxy/route_llm_request.py

⑤ Router + SDK 模型组选部署 → 翻译层 → 供应商 HTTP
│ (第 05 章 + 第 01-03 章)

⑥ 响应 OpenAI 格式的 body + x-litellm-response-cost 等响应头

部件与落点。 每道工序对应的真实入口:

工序干什么文件关键符号
HTTP 端点暴露 82 个 @router.* 路由,含 OpenAI 全套兼容端点litellm/proxy/proxy_server.pyapp = FastAPI(...)(:1379)、chat_completion(:9888)
配置面读 YAML + 从数据库热加载部署litellm/proxy/proxy_server.pyProxyConfig(:4123)、load_config(:4679)、add_deployment(:6574)
认证校验 key、拉团队/用户/组织、跑多级检查litellm/proxy/auth/user_api_key_auth.pyuser_api_key_auth(:2628)
钩子限流、预算、内容安全litellm/proxy/hooks/PROXY_HOOKS(hooks/__init__.py:19)
统一处理所有 LLM 端点共用的前后处理litellm/proxy/common_request_processing.pyProxyBaseLLMRequestProcessing(:784)
分发路由类型 → Router 方法litellm/proxy/route_llm_request.pyroute_request(:249)
计费落库成本进队列、后台批量写 Postgreslitellm/proxy/db/db_spend_update_writer.pyDBSpendUpdateWriter(:97)

端点长什么样。 一个 OpenAI 兼容端点的骨架非常薄——认证挂在 FastAPI 依赖上,主体交给统一处理类:

@router.post("/v1/chat/completions", dependencies=[Depends(user_api_key_auth)], ...)
async def chat_completion(request, fastapi_response, model=None,
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth)):

litellm/proxy/proxy_server.py:9867-9888。同一个函数上叠了四个 @router.post,把 /v1/chat/completions/chat/completions/engines/{model}/chat/completions/openai/deployments/{model}/chat/completions(Azure 风格)映到同一实现——这就是「客户端换个 base_url 就能用」的来源。


6.3 配置面:YAML 三段 + 数据库热加载

6.3.1 三段结构

网关的行为几乎全由一个 YAML 决定。仓库根的 proxy_server_config.yaml 是最完整的样例,分三段:

段名管什么典型条目
model_list对外暴露哪些模型名,每个名字底下挂哪些真实部署model_name / litellm_params / model_info(proxy_server_config.yaml:1-60)
litellm_settings直接写进 SDK 全局的开关drop_paramsnum_retriessuccess_callbackcache(proxy_server_config.yaml:168-195)
general_settings网关自己的运维参数master_keystore_model_in_dbproxy_batch_write_atpass_through_endpoints(proxy_server_config.yaml:223-251)

另有 router_settings 段,原样喂给 05 章的 Router(proxy_server_config.yaml:215-221:routing_strategy: usage-based-routing-v2 + Redis 连接)。litellm/proxy/proxy_config.yaml 是个更小的开发用样例,展示了 mcp_servers 段。

读取路径。 ProxyConfig.load_config(proxy_server.py:4679)是唯一的装配点,顺序是:先把 environment_variables 灌进 os.environ,再逐段消费——litellm_settings 写进 litellm.* 全局(:4650 起),general_settings 取出来存成模块级全局(:5074),model_list 变成 router_params["model_list"](:5316)最终构造 litellm.Router(:5399),guardrails 段交给 init_guardrails_v2(:5418)。

配置值里的 os.environ/OPENAI_API_KEY 这种写法由 get_secret 解析,所以 YAML 本身不含密钥。

6.3.2 启动:一个 lifespan 把所有东西点起来

FastAPI 的 lifespan=proxy_startup_event(proxy_server.py:1379、:971)按固定顺序做四件事:

  1. 找配置文件(CONFIG_FILE_PATHWORKER_CONFIG)并 load_config;
  2. 连数据库 —— ProxyStartupEvent._setup_prisma_client(:9387);
  3. 注册计费回调 —— cost_tracking()(:2342)把 _ProxyDBLogger 挂进 litellm.callbacks(:2347);
  4. 起后台任务 —— ProxyStartupEvent.initialize_scheduled_background_jobs(:8726)。

6.3.3 后台任务:网关的「心跳」

所有周期性工作跑在一个 AsyncIOScheduler(APScheduler)上,都在 initialize_scheduled_background_jobs 里注册:

任务干什么注册位置
reset_budget_job到期重置各级预算(budget_duration 到点了就清零)proxy_server.py:8792
update_spend_job把内存/Redis 里攒的花费批量写进 Postgresproxy_server.py:8804
update_daily_tag_spend_job按 tag 的日聚合,间隔更长以降 QPSproxy_server.py:8819
add_deployment_job每 30 秒扫数据库,热加载新增/改动的部署proxy_server.py:8824
key_rotation_job到期轮换虚拟 key(需 LITELLM_KEY_ROTATION_ENABLED=true)proxy_server.py:9224
spend_log_cleanup_job按保留期删旧的 LiteLLM_SpendLogsproxy_server.py:6239 附近

健康检查不走 scheduler,而是一个常驻协程 _run_background_health_check(),在 lifespan 末尾用 asyncio.create_task 拉起(proxy_server.py:1230)。

代码里几处注释直白记录了一次内存事故:APScheduler 的 jitter 参数触发 normalize() 大量分配,他们把所有 jitter 去掉、改用 random.randint 手动加偏移,并把轮询间隔从 10 秒放宽到 30 秒(proxy_server.py:8738-8745、:8909)。

6.3.4 热加载:改配置不用重启

add_deployment(proxy_server.py:6574)是「运行中的网关怎么长出新模型」的答案。它每 30 秒被调一次,做的事:

# 示意,非源码 —— add_deployment 的骨架
async def add_deployment(prisma_client, proxy_logging_obj):
await prefetch_config_params(prisma_client, ["general_settings", "router_settings", ...]) # 预热配置缓存
new_models = await ModelRepository(prisma_client).table.find_many() # 读 DB 里的部署表
await self._update_llm_router(new_models=new_models, ...) # 增量更新 Router
await self._init_non_llm_objects_in_db(prisma_client) # guardrails / vector store / MCP …

重点看容错:_update_llm_router(:5818)遇到 new_models is None(说明 DB 这次读失败)会直接 return 而不是清空,免得一次数据库抖动把线上所有部署踢掉(:5841-5849)。


6.4 认证:虚拟 key 怎么变成一个身份

6.4.1 虚拟 key 是什么

虚拟 key(virtual key)= 网关自己发的、以 sk- 开头的凭证,它不是任何供应商的真 API key。数据库里存的也不是明文,而是它的哈希(LiteLLM_VerificationToken.token 是主键,注释写明「Hashed API Token. Not the actual Virtual Key」,schema.prisma:417、:614)。

一行数据库记录就是一份「配额合同」——挑几列看它能管什么(schema.prisma:416-460):

含义
models / aliases这把 key 能调哪些模型、模型名怎么改写
tpm_limit / rpm_limit / max_parallel_requests每分钟 token 数 / 请求数 / 并发数上限
max_budget / budget_duration / budget_reset_at花费上限、周期、下次重置时间
user_id / team_id / organization_id / project_id挂在谁名下(决定还要继承哪几级限额)
expires / blocked / auto_rotate过期、封禁、自动轮换

团队表 LiteLLM_TeamTable(schema.prisma:118)有一组几乎同名的列,所以同一个请求会被多级约束:key 级 → 用户级 → 团队级 → 组织级。

6.4.2 校验主线

入口是 FastAPI 依赖 user_api_key_auth(user_api_key_auth.py:2628),它自己只做编排,重活在 _user_api_key_auth_builder(:1088):

Authorization / x-litellm-api-key / api-key / x-goog-api-key …

▼ _get_bearer_token(:356) 剥掉 "Bearer "/"Basic "/AWS4-HMAC-SHA256 外壳
裸 token

├─ 是 master key? secrets.compare_digest 常数时间比较(:1660)→ PROXY_ADMIN 身份

├─ 是 JWT? 换成虚拟 key 或走完整 JWT 策略(见 6.4.3)

└─ 是 sk-… ? hash_token → 先查缓存(:1572)→ 未命中查 DB(:1614)


UserAPIKeyAuth 身份对象(带各级限额)

三个细节值得记:

  • 绝不字符串比较 key。 master key 用 secrets.compare_digest(:1660),源码注释点名这是防时序攻击。
  • 命中 master key 后不把它当身份用。 返回的对象里 api_key 被替换成 LITELLM_PROXY_MASTER_KEY_ALIAS,免得 master key 的哈希漏进 spend log、Prometheus 标签和限流桶(:1676-1691)。
  • 只有 sk- 开头才会被哈希后查库(:1722 起);不以 sk- 开头的直接 401,防止有人拿哈希值当 key 用。

6.4.3 JWT:把外部身份换成内部 key

企业场景里客户端拿的是公司 IdP 签发的 JWT,不是 sk-…。LiteLLM 的做法是换票:从 JWT 的某个 claim(virtual_key_claim_field)取值,查 LiteLLM_JWTKeyMapping 表映射到一把已有的虚拟 key,后面完全复用虚拟 key 那条路径(_resolve_jwt_to_virtual_key,user_api_key_auth.py:839)。

没有映射时按 unregistered_jwt_client_behavior 三选一(:881 起):

策略行为
REJECT直接 403
AUTO_REGISTER先跑完整 JWT 策略(RBAC/scope/邮箱域校验),通过后由 _auto_register_jwt_mapping(:703)现建一把 key 并写映射
FALLBACK_TEAM_MAPPING回落到基于团队的标准 JWT 认证

一处安全细节:claim 缺失也按「未注册」处理,不是放行。注释写明否则「客户端只要发一个不含该字段的 JWT 就能绕过 REJECT」(:866-895)。

6.4.4 多级校验与预算「预留」

身份拿到后,user_api_key_auth 在一个 auth span 内串起三件事(:2654 起):

  1. RouteChecks.should_call_route(:2574)—— 这个身份能不能碰这个路由;
  2. _run_centralized_common_checks(:2165)—— 唯一的授权点;
  3. 预算预留 —— _reserve_budget_after_common_checks(:2470)。

第 2 步之所以叫「centralized」,是因为 _user_api_key_auth_builder 有一堆提前 return 的分支(无 master key 的 dev 模式、JWT 短路、OAuth2),历史上容易漏检查。现在它并发拉 team / user / project / end_user 四个对象(:2300-2344),再统一交给 common_checks(:2431)。函数 docstring 明确规定「common_checks 只在 user_api_key_auth 包装层跑一次,builder 路径不自行调用」(:2171-2200)。

第 3 步是本章最有意思的设计。问题: 并发十个请求,每个都读到「已花 $9.9 / 上限 $10」,于是十个全放行,最后超支。做法: 认证阶段就按「这次最多可能花多少」在计数器上先加一笔(reserve_budget_for_request,spend_tracking/budget_reservation.py:148),调用真正结束后再用实际成本把这笔预留改写成准确值。

# 示意,非源码 —— 预留的核心思路
reserved = estimate_request_max_cost(request_body, route, llm_router) # 按 max_tokens 估上限
if reserved is None or reserved <= 0:
return None # 估不出来(如图像/音频路由)就退回「读时判断」
for counter in counters: # key / user / team / org / end_user / tag 各一个计数器
await _reserve_counter(counter, reserved) # 原子自增,超了就抛错并回滚已加的那几个

真实实现在 budget_reservation.py:186,单个计数器的原子自增在 _reserve_counter(:661)。可以用 general_settings.disable_budget_reservation 关掉,但代码里会打一条警告说明关掉后「高并发下预算可能被短暂突破」(user_api_key_auth.py:2488-2491)。


6.5 闸门:限流与预算钩子

6.5.1 钩子是怎么挂上去的

PROXY_HOOKS 是一张名字 → 类的表(proxy/hooks/__init__.py:19-31),启动时由 ProxyLogging._add_proxy_hooks(proxy/utils.py:595)逐个实例化并注册进 litellm.callbacks。企业版钩子通过 ENTERPRISE_PROXY_HOOKS 合并进同一张表(:49-51)。

钩子名管什么
parallel_request_limiter_PROXY_MaxParallelRequestsHandler_v3RPM / TPM / 并发
max_budget_limiter_PROXY_MaxBudgetLimiter个人预算
max_budget_per_session_limiter_PROXY_MaxBudgetPerSessionHandler单会话预算
max_iterations_limiter_PROXY_MaxIterationsHandleragent 循环次数
cache_control_check_PROXY_CacheControlCheck缓存控制头合法性

它们统一在 ProxyLogging.pre_call_hook(proxy/utils.py:1386)里被遍历调用。这个循环做了个便宜的优化:如果没有任何回调覆写 async_pre_call_hook、也没配 guardrail,就整段跳过(:1440-1452)。

6.5.2 限流:描述符 + 滑动窗口

_PROXY_MaxParallelRequestsHandler_v3 的模型是描述符(descriptor):一个请求要被记在哪些桶上。_create_rate_limit_descriptors(parallel_request_limiter_v3.py:2613)逐个生成:

描述符 keyvalue来源字段
api_key虚拟 key 哈希rpm_limit / tpm_limit / max_parallel_requests
useruser_iduser_rpm_limit / user_tpm_limit
teamteam_idteam_rpm_limit / team_tpm_limit
team_memberteam_id:user_idteam_member_*_limit
组织 / 模型级 / MCP / agent各自 idcreate_organization_rate_limit_descriptor(:2229)等

计数用 Redis Lua 脚本做滑动窗口(BATCH_RATE_LIMITER_SCRIPT,:73-108):每个桶两个 key(窗口起点 + 计数器),窗口过期就重置并重设 TTL,一次 EVAL 处理所有 key。窗口长度默认 60 秒(self.window_size,:612)。

TPM 有个额外麻烦:请求发出前不知道会用多少 token。做法和预算预留同构 —— 先按 input_tokens + max_tokens 估一个量(_estimate_tokens_for_request,:826),用 reserve_tpm_tokens(:2012)原子预扣,所以第一遍 RPM 检查必须 skip_tpm_check=True,否则每个在途请求都会把 token 计数器多顶 1,造成假 429(参数说明见 :1231-1255)。

6.5.3 预算钩子

_PROXY_MaxBudgetLimiter.async_pre_call_hook(hooks/max_budget_limiter.py:20)只管一件很窄的事:个人预算。它显式跳过两种情况——带 team_id 的请求(团队 key 由 common_checks 管),以及这个计数器已经在认证阶段被预留过(:33-49),否则会把预留恰好填满到 max_budget 的那个请求误杀。


6.6 中段:统一处理与分发

6.6.1 一个类喂所有端点

ProxyBaseLLMRequestProcessing(common_request_processing.py:1372)是所有 LLM 端点的公共躯干,两个主方法:

common_processing_pre_call_logic(:1551) —— 调用前的装配线,顺序固定:

  1. add_litellm_data_to_request(litellm_pre_call_utils.py:1582,调用点 :1660):把 key/team/org 身份、tag、header 塞进 data["metadata"];
  2. 解析最终模型名:general_settings.completion_model → CLI --model → 路径参数 → body 里的 model(:1085-1090),再过全局别名表和 key 级别名表(:1719-1729);
  3. function_setup(:1784)建 LiteLLMLoggingObj —— 注释特别说明必须在跑检查之前建,这样被拒绝的请求也能落到 langfuse 之类的日志后端(:1780-1783);
  4. pre_call_hook(:1808)—— 6.5 的那些钩子在这里执行。

base_process_llm_request(:2002) —— 真正发出去。它把内容审核和 LLM 调用并发跑:during_call_hookroute_request 各起一个 task,asyncio.gather 一起等,审核不额外增加延迟。

6.6.2 分发:HTTP 路由 → Router 方法

route_request(route_llm_request.py:323)接收一个 route_type(如 "acompletion"),最终执行的是 getattr(llm_router, route_type)(**data) —— 用字符串反射调到 Router 上的同名方法。ROUTE_ENDPOINT_MAPPING(:76-136)是反向表,只在报错时把 route_type 翻回人类可读的 URL(:674)。

选择逻辑是一条长 if/elif 链,优先级从高到低:

data 里带 api_key/api_base ────▶ 直接透传,不经模型组
逗号分隔的多个 model ─────────▶ abatch_completion / …fastest_response
带 user_config ───────────────▶ 用请求自带的临时 Router
带 router_settings_override ──▶ 把 key/team 级 router 设置合并进 kwargs(不新建 Router)
model 命中团队别名 / 模型组 / model_id / model_group_alias ─▶ llm_router.<route_type>()
以上都不中 ──────────────────▶ 通配符 → default_deployment → deployment_names → 报 404

对应 route_llm_request.py:446-674。注意 router_settings_override 分支的注释:每请求新建 Router 太贵,所以只把 fallbacks/num_retries/timeout 等几个字段当 per-request kwargs 合并(:469-495)。

这里还埋了一处安全修补:进函数就把 mock_testing_* 系列参数从 data 里剔掉(需管理员显式开 dangerously_allow_mock_testing_request_params,否则拒绝,:13-25:181-218),否则调用方能配合 router_settings_override 里的 fallback 定向命中受限模型。

6.6.3 响应:成本写进 header

非流式路径在返回前用 get_custom_headers(:1377)拼一批 x-litellm-* 响应头塞进 fastapi_response.headers(:2483)。最有用的几个:

响应头含义
x-litellm-response-cost这次调用的美元成本(:1425)
x-litellm-key-spend / x-litellm-key-max-budget这把 key 的累计花费(已含本次)与上限
x-litellm-key-tpm-limit / x-litellm-key-rpm-limit当前限额
x-litellm-model-id / x-litellm-model-api-base实际命中的部署与其 base URL

api_base 在写入前会 split("?")[0] 去掉 query,注释说明是防止把签名类参数泄漏出去(:1418-1423)。流式路径同样拼这批 header,只是挂在 StreamingResponse 上(:2305 附近)。


6.7 计费:从响应头到 Postgres

6.7.1 为什么不能同步写库

响应已经返回给客户端了,记账不该再占用请求延迟;而且 1K RPS 下每请求一次 UPDATE 会把 Postgres 锁死(源码原话:「This flow causes Deadlocks in production (1K RPS+)」,db_spend_update_writer.py:1011)。所以整条链路是异步 + 批量的:

响应已返回客户端

▼ 异步成功回调 _ProxyDBLogger.async_log_success_event
取 response_cost(缓存命中则记 0)

├──▶ 插一条明细 LiteLLM_SpendLogs
└──▶ 增量进程内队列 SpendUpdateQueue / DailySpendUpdateQueue

▼ (可选) 每个 pod 把队列刷进 Redis buffer
Redis ── 抢分布式锁 ──▶ 只有拿到锁的 pod 读 buffer 写 DB

▼ APScheduler update_spend_job 定时触发
Postgres:VerificationToken.spend / TeamTable.spend / DailyUserSpend …

6.7.2 三段代码

回调端。 _ProxyDBLogger(hooks/proxy_track_cost_callback.py:61)由 cost_tracking() 注册(proxy_server.py:2347),核心在 _PROXY_track_cost_callback(:211):从 standard_logging_objectresponse_cost,缓存命中直接置 0(:260),然后调 _update_database_and_spend_counters。若 response_costNone 且确实是个模型调用,它会抛异常并提示配自定义定价(:334-347)——宁可报错也不静默漏账。

写入端。 DBSpendUpdateWriter(db_spend_update_writer.py:121)持有七个队列(总量队列 + 六个按天聚合队列,:136-142)。update_database(:145)先同步插 LiteLLM_SpendLogs 明细,再用一个 asyncio.create_task 把所有增量更新合成一次 _batch_database_updates(:415,注释写明「Single task replaces 11 create_task() calls」,见 :225)。

提交端。 db_update_spend_transaction_handler(:830)按 use_redis_transaction_buffer 二选一:

模式路径适用
带 Redis buffer_commit_spend_updates_to_db_with_redis(:867)多 pod。所有 pod 只写 Redis;pod_lock_manager.acquire_lock 选出一个 leader 写 DB(:892)
不带 buffer_commit_spend_updates_to_db_without_redis_buffer(:1000)单 pod / 低 QPS

6.7.3 表与数据访问层

落地的核心表(schema.prisma):

作用
LiteLLM_VerificationToken(:416)虚拟 key 本体:限额、预算、归属、轮换状态
LiteLLM_TeamTable(:118)团队:成员、允许的模型、团队级限额与预算
LiteLLM_SpendLogs(:611)每次调用一行明细:成本、token 数、模型、tag、耗时
LiteLLM_DailyUserSpend / DailyTeamSpend / DailyTagSpend(:738/:876/:912)预聚合表,给报表和 UI 用

上层是一层薄仓储:BaseRepository(litellm/repositories/base_repository.py:38)是个泛型 CRUD 基类(Generic[T],提供 find_by_id / find_many / create / update / delete / count / exists),litellm/models/ 放对应的 Pydantic 领域模型。两个值得看的特化:

  • 归档后删除。 VerificationTokenRepository.delete_token(verification_token_repository.py:303)在一个事务里先把整行写进 LiteLLM_DeletedVerificationToken,再删原行(:303-310)——审计不断链。
  • 原子 push。 往团队加成员用 data={"members": {"push": user_id}}(team_repository.py:298),让数据库做数组追加,避免「读-改-写」丢更新。

6.8 扩展面(知道在哪就行)

这三块都挂在同一套认证/计费管道上,本章只给定位:

扩展位置一句话
Guardrails(内容安全)litellm/proxy/guardrails/init_guardrails_v2(init_guardrails.py:19)把 YAML 的 guardrails 段实例化并注册成回调;GuardrailRegistry(guardrail_registry.py:247)负责数据库里的增删改查与热更新
MCP 工具网关litellm/proxy/_experimental/mcp_server/MCPServerManager(mcp_server_manager.py:1330)把配置里的 mcp_servers 聚合成一个对外的 MCP 端点,工具调用同样过认证与计费(route_type="call_mcp_tool")
原生 API 透传litellm/proxy/pass_through_endpoints/create_pass_through_route(pass_through_endpoints.py:1668)把 /v1/rerank 这类路径原样转发到供应商,只借用网关的认证与记账,不做协议翻译

6.9 巧妙之处

  • 认证阶段就动手扣预算,而不是只读。 预留 + 事后改写(budget_reservation.py:148)把「读时判断」这个天然有竞态的模式,换成了原子计数器,代价只是需要能估出成本上限。
  • 同一套「先估、后原子扣、再回填」用了两遍。 预算是钱,TPM 是 token(parallel_request_limiter_v3.py:2012),两个问题结构相同,解法也复用。
  • 授权收敛到一个点。 _run_centralized_common_checks 的 docstring 直接把「builder 路径不许自己调 common_checks」写成不变式(user_api_key_auth.py:2171-2200——这类规则写进代码注释比写进 wiki 有用得多。
  • 热加载优先保稳。 DB 读失败时 _update_llm_router 宁可不更新也不清空(proxy_server.py:5843,把「配置面故障」和「数据面可用性」解耦。
  • 多 pod 记账靠选主而非分布式事务。 所有 pod 写 Redis,只有抢到锁的那个写 Postgres(db_spend_update_writer.py:892),用最土的办法绕开了 DB 死锁。
  • master key 不进任何下游标签。 命中后立刻换成别名常量(user_api_key_auth.py:1676-1691),从源头切断泄漏路径。

6.10 边界与局限

一个 17000 行的端点文件。 litellm/proxy/proxy_server.py 是 17644 行、84 个 @router.* 装饰点。加一个新端点意味着往这个文件里再塞一段;route_request 则是一条约 230 行的 if/elif 链(route_llm_request.py:446-674),common_processing_pre_call_logicroute_type 参数是个有 90 多个字面量Literal(common_request_processing.py:1558-1640)。可读性、类型检查开销和「改一处漏一处」的风险都记在这笔账上。

翻译层是有损的。 网关对外只讲 OpenAI 方言,供应商独有的能力没有对应字段。逃生口有两个,但都绕开了统一抽象:

  • provider-specific 参数(在 litellm_params 里直接写供应商字段);
  • pass_through_endpoints(pass_through_endpoints.py:1668)—— 原样转发,请求体不翻译,只保留认证与计费。

细节见 02 章

成本依赖外部价格表的时效性。 所有计费都来自 model_prices_and_context_window.json,默认从 GitHub 拉取,拉不到才退回包内备份(litellm/litellm_core_utils/get_model_cost_map.py:2-8、:54)。所以:模型刚发布还没进表 → response_costNone,_PROXY_track_cost_callback 会抛异常(proxy_track_cost_callback.py:334-347);表里价格滞后于供应商调价 → 账目静默偏差,网关自己发现不了。自托管模型必须手工配 input_cost_per_token 之类的字段。

强依赖 Redis + Postgres。 没有 Postgres 就没有虚拟 key(prisma_client is None 时直接抛 no_db_connection,user_api_key_auth.py:1716-1722);跨 pod 的限流、预算预留和记账都建立在 Redis 上,use_redis_transaction_buffer 开了却没配 Redis 会在启动时直接报错(proxy_server.py:8445)。换句话说,这不是一个能「单文件跑起来」的组件——它是一套需要运维的有状态服务。

预留会放大失败的代价。 预留是在调用前扣的,后续要靠成功/失败回调去回填或释放(_release_budget_reservation)。回调没跑到(进程被杀、异常路径没覆盖)就会留下「幽灵占用」,直到该周期预算重置。代码里为此写了多层 best-effort 释放(budget_reservation.py:813_release_applied_entries_best_effort),本身就说明这条路径不好走。


6.11 代码地图

主题文件路径关键符号
FastAPI app 与 lifespanlitellm/proxy/proxy_server.pyapp / proxy_startup_event
chat/completions 端点litellm/proxy/proxy_server.pychat_completion
配置加载litellm/proxy/proxy_server.pyProxyConfig / load_config / get_config
数据库热加载部署litellm/proxy/proxy_server.pyadd_deployment / _update_llm_router
后台定时任务litellm/proxy/proxy_server.pyProxyStartupEvent.initialize_scheduled_background_jobs
认证总入口litellm/proxy/auth/user_api_key_auth.pyuser_api_key_auth / _user_api_key_auth_builder
token 解析litellm/proxy/auth/user_api_key_auth.py_get_bearer_token / get_api_key
JWT 换虚拟 keylitellm/proxy/auth/user_api_key_auth.py_resolve_jwt_to_virtual_key / _auto_register_jwt_mapping
集中授权与预留litellm/proxy/auth/user_api_key_auth.py_run_centralized_common_checks / _reserve_budget_after_common_checks
key 对象解析litellm/proxy/auth/resolvers/store.pyIdentityStore
预算预留litellm/proxy/spend_tracking/budget_reservation.pyreserve_budget_for_request / _reserve_counter
统一请求处理litellm/proxy/common_request_processing.pyProxyBaseLLMRequestProcessing / common_processing_pre_call_logic / base_process_llm_request
响应头litellm/proxy/common_request_processing.pyget_custom_headers
分发litellm/proxy/route_llm_request.pyroute_request / ROUTE_ENDPOINT_MAPPING
钩子注册表litellm/proxy/hooks/__init__.pyPROXY_HOOKS / get_proxy_hook
钩子执行litellm/proxy/utils.pyProxyLogging._add_proxy_hooks / ProxyLogging.pre_call_hook
限流litellm/proxy/hooks/parallel_request_limiter_v3.py_PROXY_MaxParallelRequestsHandler_v3 / _create_rate_limit_descriptors / reserve_tpm_tokens
个人预算litellm/proxy/hooks/max_budget_limiter.py_PROXY_MaxBudgetLimiter
计费回调litellm/proxy/hooks/proxy_track_cost_callback.py_ProxyDBLogger / _PROXY_track_cost_callback
花费落库litellm/proxy/db/db_spend_update_writer.pyDBSpendUpdateWriter / db_update_spend_transaction_handler
数据表定义schema.prismaLiteLLM_VerificationToken / LiteLLM_TeamTable / LiteLLM_SpendLogs
泛型仓储litellm/repositories/base_repository.pyBaseRepository
key 仓储(归档删除)litellm/repositories/verification_token_repository.pyVerificationTokenRepository.delete_token
内容安全litellm/proxy/guardrails/init_guardrails_v2 / GuardrailRegistry
MCP 网关litellm/proxy/_experimental/mcp_server/mcp_server_manager.pyMCPServerManager
原生透传litellm/proxy/pass_through_endpoints/pass_through_endpoints.pycreate_pass_through_route
价格表litellm/litellm_core_utils/get_model_cost_map.pyGetModelCostMap

上一章: 05-router.md —— Router 怎么在模型组内选部署、冷却与回退。 回目录: index.md