跳到主要内容

数据截至 (上游 commit 3f15dc32871c)

主线:一次 completion() 从调用到返回

30 秒导读: LiteLLM SDK 对外只有一个函数 completion()。这一章追这一个函数:它怎么判断"这个模型名归哪个供应商"、怎么把上百个杂七杂八的参数摆整齐、最后怎么把活交给具体供应商的处理函数。翻译、HTTP、流式、日志计费都不在这章。


1. 这一章讲的"调度骨架"是什么

一句话: completion() 是一个总机——它不打电话,只负责认领来电、整理工单、转接给正确的分机。

用起来是这样的,同一个函数换个模型名就换了一家供应商:

import litellm

# 打 OpenAI
litellm.completion(model="gpt-4o", messages=[{"role": "user", "content": "hi"}])
# 打 Anthropic —— 除了模型名,什么都没变
litellm.completion(model="anthropic/claude-sonnet-4", messages=[{"role": "user", "content": "hi"}])
# 打 Bedrock 上的同一个 Claude —— 还是没变
litellm.completion(model="bedrock/anthropic.claude-sonnet-4-v1:0", messages=[{"role": "user", "content": "hi"}])

要让这三行都成立,总机必须回答三个问题。这三个问题就是本章的全部内容:

问题白话负责的符号
这单归谁?"anthropic/claude-x" 这串字符到底是哪家的get_llm_provider
工单怎么整理?超时、定价、别名、模板、provider config 的固定顺序completion() 主体
转给哪个分机?62 个 _complete_* 函数里挑一个_CompletionDispatchContext + if/elif 链

本章不覆盖(避免和兄弟章重复):

内容去哪章
BaseConfig 的方法语义、参数映射规则02-translation-layer.md
BaseLLMHTTPHandler 怎么发请求、流式怎么拼装03-http-and-streaming.md
@client 装饰器里的日志、缓存、成本、异常04-cross-cutting-wrapper.md
多部署负载均衡、冷却、回退05-router.md

2. 顶层全景:一条直线

先看整条主线。这张图从上到下就是执行顺序,没有循环、没有回头——SDK 层的调度是一条直线。

用户调用

├── litellm.acompletion(...) ← 异步入口 (main.py:388)
│ │ 打标记 acompletion=True,把自己丢进线程池
│ ▼
└── litellm.completion(...) ← 唯一主体 (main.py:4901)

│ ① 认领:模型名 → 供应商

get_llm_provider() (get_llm_provider_logic.py:130)
│ 返回四元组 (model, provider, key, api_base)

│ ② 预处理:把工单摆整齐

别名 → 桥接判定 → 超时归一 → 定价注册
→ 提示模板 → provider_config → 角色改写 (main.py:5223-5324)

│ ③ 派发:选一个分机

_CompletionDispatchContext(冻结) (types/completion.py:215)


if/elif 链(约 64 个分支) (main.py:5575-5783)


_complete_anthropic / _complete_bedrock / …(62 个)


provider handler(第 03 章的地盘)

各段职责一句话:

阶段干什么关键符号位置
入口校验参数、args = locals() 存一份原始调用completionlitellm/main.py:4901
认领模型名 → (model, provider, key, api_base)get_llm_providerlitellm/litellm_core_utils/get_llm_provider_logic.py:130
预处理八九个固定顺序的归一化步骤CompletionTimeout.resolvelitellm/main.py:5223-5352
派发冻结 ctx + 显式长链_CompletionDispatchContextlitellm/types/completion.py:207
出口交给供应商处理函数_complete_*litellm/main.py:1241-4899

3. 一个入口,还是两个?

3.1 它要解决的小问题

LiteLLM 支持 100 多家供应商,每家都要能同步调用也能异步调用。最笨的做法是写两遍:completion() 一套 62 个分支,acompletion() 再来一套。那就是两份永远会不同步的代码。

3.2 思路:异步入口不复制主体,只做三件事

acompletion() 的做法很反直觉——它自己不实现任何调度逻辑,它调用同步的 completion():

  1. 在参数里塞一个标记 acompletion=True;
  2. 把同步的 completion() 丢进线程池执行(不阻塞事件循环);
  3. 拿回返回值:如果是协程就 await 它,如果已经是结果就直接返回。

关键在于标记 acompletion=True 会一路传到最底层的 provider handler,handler 看到这个标记就返回一个协程而不是真去发同步 HTTP 请求。所以线程池里那一趟其实不做 IO,只是把上百行调度逻辑跑一遍,产出一个"待执行的协程"。

用示意代码把这个想法演出来:

# 示意,非源码:异步入口如何复用同步主体
async def acompletion(model, messages, **kwargs):
kwargs["acompletion"] = True # ① 打标记,一路传到 handler
func = partial(completion, model=model, messages=messages, **kwargs)
init = await loop.run_in_executor(None, func) # ② 线程池里跑同步调度骨架
if asyncio.iscoroutine(init): # ③ handler 因为标记返回了协程
return await init # 真正的 IO 在这里发生
return init # 缓存命中等情况:已经是结果

重点看第 ①、③ 两步:标记决定了返回值的形态,所以第 ③ 步必须做类型判断。

3.3 真实实现

打标记发生在 completion_kwargs 字典里,一行注释写着"assuming this is a required parameter":

"acompletion": True, # assuming this is a required parameter

—— litellm/main.py:593,这个字典从 main.py:553 起把 acompletion() 的每个显式参数逐个抄进去(没有用 *args/**kwargs 转发,这样 IDE 和类型检查器能看见完整签名)。

进线程池的三行在 main.py:631-635:先 kwargs.pop("acompletion", None) 去掉重复键,再 partial(completion, **completion_kwargs, **kwargs),再用 contextvars.copy_context() 把当前上下文变量复制进去——没有这一步,线程池里的日志上下文(trace id 之类)就丢了

拿回结果的三态判断在 main.py:639-646:

返回值类型什么情况怎么处理
dict / ModelResponse缓存命中(源码注释直书 ## CACHING SCENARIO)直接用
协程正常异步路径,handler 返回了 coroutineawait
其它(如 CustomStreamWrapper)流式原样返回

同步入口 completion() 本体在 main.py:4899-4901——注意它头上叠了两个装饰器 @tracer.wrap()@client,@client 就是横切层(见 04-cross-cutting-wrapper.md)。

3.4 两个容易踩的细节

异步路径提前解析了一次 provider。 main.py:600-605 里,acompletion() 在还没进线程池之前就先调了一次 get_llm_provider,只为拿 custom_llm_provider。它的用途是异常映射——main.py:695-703except 里要用 provider 名把原始异常翻译成 OpenAI 风格异常。所以同一次调用里 get_llm_provider 会被执行两次(异步路径一次,completion() 主体一次)。

_async_streaming 不服务于 chat。 main.py:720 定义的 _async_streaming 是一个异步生成器:先判断 asyncio.iscoroutine(response) 决定要不要先 await,再逐行 yield,并把异常包进 exception_type。但在本 commit 里它只被 atext_completion 用到(main.py:7116,包进 TextCompletionStreamWrapper);chat completion 的异步流式走的是 CustomStreamWrapper,那条路在 03-http-and-streaming.md


4. 认领:模型名 → 供应商

4.1 它要解决的小问题

用户手里只有一个字符串。"gpt-4o""anthropic/claude-sonnet-4""azure/command-r-plus""openrouter/anthropic/claude-3.5-sonnet"——这些要落到同一个答案上:这单归哪家、用哪个 key、发到哪个 URL

get_llm_provider(litellm/litellm_core_utils/get_llm_provider_logic.py:130)返回一个四元组:

位置名字含义
1model剥掉前缀后的真实模型名(供应商 API 认的那个)
2custom_llm_provider供应商标识,派发链就是按它分支
3dynamic_api_key从环境变量现取的 key(只有部分规则会填)
4api_base推断出的 endpoint(只有部分规则会填)

4.2 决策顺序:命中即停

怎么读这张图:从上往下依次尝试,命中任何一条就立刻返回,不再往下走。 顺序本身就是优先级。

model 字符串


┌─────────────────────────────┐
│ 特例层(改写而非返回) │
│ azure/ + 非 OpenAI 模型 → openai :169
│ cohere/ + chat 模型 → cohere_chat :175
│ anthropic/ + text 模型 → anthropic_text :177
│ openrouter/ 前缀剥离 :189
└───────────┬─────────────────┘

┌─────────────────────────────┐
│ 前缀层(有 "/" 才生效) │
│ ① JSON 注册表 exists? :196 ← 优先
│ ② 前缀 ∈ provider_list? :210 / :221
└───────────┬─────────────────┘
│ 无前缀 / 未命中

┌─────────────────────────────┐
│ api_base 反查层 :233 │
│ 逐个匹配 openai_compatible_endpoints │
│ 命中则连 env 里的 key 一起取出 │
└───────────┬─────────────────┘


┌─────────────────────────────┐
│ 模型名表层 :358-470 │
│ model ∈ open_ai_chat_completion_models … │
│ 或 startswith("bytez/") 之类兜底前缀 │
└───────────┬─────────────────┘
│ 还没命中

fallback 泛化规则 :477 → 仍没有 → BadRequestError :488

4.3 逐条看几个不显然的规则

azure 特例:azure/ 不等于 Azure OpenAI。 Azure AI Studio 上也托管 Cohere 和 Mistral。_is_non_openai_azure_model(get_llm_provider_logic.py:52)切出 / 后的模型名,如果它在 litellm.cohere_chat_models 里、或者 mistral/{name}litellm.mistral_chat_models 里,就把 provider 定成 "openai"(走 OpenAI 兼容协议),而且保留完整的 azure/xxx 模型名不剥前缀(:169-172)。

openrouter 前缀剥离是有条件的。 OpenRouter 自己的模型 ID 长得像 anthropic/claude-3.5-sonnet,加上 LiteLLM 的路由前缀就变成 openrouter/anthropic/claude-3.5-sonnet。剥离规则(:189-194)是:去掉 openrouter/ 后,剩下的部分里还有 / 才剥;像 openrouter/auto 这种原生 ID 剩下 auto 没有斜杠,就原样保留。源码注释把这两种情况都写明了。

JSON 注册表优先于枚举列表。 :196-204 先问 JSONProviderRegistry.exists(provider_prefix),命中就走 _get_openai_compatible_provider_info;之后才是 model.split("/")[0] in litellm.provider_list 的枚举判断(:210:221)。这让"用一份 JSON 配置新增一个 OpenAI 兼容供应商"不需要动枚举。

api_base 反查会顺手把 key 从环境变量里取出来。 :233 起是一条长长的 if/elif:用户没写前缀但写了 api_base="https://api.groq.com/openai/v1",就认成 groq 并且 dynamic_api_key = get_secret_str("GROQ_API_KEY")

这条反查路径修过一个真实的凭据外泄漏洞。 匹配函数 _endpoint_matches_api_base(:15)现在用 urlparse主机名精确相等 + 路径按段前缀的判断,而不是朴素的子串包含。函数 docstring 直接写出了攻击形态:朴素写法下,攻击者传 https://attacker.com/api.groq.com/openai/v1 就能骗代理去读服务端的 GROQ_API_KEY 并作为 Bearer 凭据发给攻击者的主机。

兜底与失败。 全都没命中时,match_fallback_generalization(model)(:477)做一次声明式泛化(源码注释举的例子是把未来的 claude-* 路由到 anthropic);还是没有,就抛 BadRequestError,消息里附上供应商文档链接(:488-495)。


5. 预处理:一条顺序敏感的流水线

5.1 先看顺序

completion() 主体在拿到 provider 后,按固定顺序做一串归一化。顺序不是随意的:后一步经常依赖前一步的产物(比如超时归一需要先知道 provider 才能判断它支不支持 httpx.Timeout)。

#步骤干什么位置
0参数校验修正 messages / tools / tool_choice / thinking 的形状main.py:5003-5011
0args = _locals_snapshot(locals())存一份未经改动的原始调用参数main.py:5014
1fallbacks / model_list有就直接转给回退或批量执行器,主线到此为止main.py:5213-5221
2model_alias_map把用户自定义别名换成真实模型名main.py:5223-5226
3get_llm_provider认领(第 4 节)main.py:5237-5245
4responses_api_bridge_check这个模型该不该改走 Responses APImain.py:5248-5253
5CompletionTimeout.resolve超时归一main.py:5273-5279
6自定义定价注册注册本次调用带来的自定义定价main.py:5282-5290
7custom_prompt_dict组装自定义提示模板main.py:5292-5305
8get_provider_chat_config拿到该 provider 的 BaseConfigmain.py:5315-5321
9translate_developer_role_to_system_role用 config 改写消息角色main.py:5324
10get_optional_params参数映射(第 02 章的主题)main.py:5383
11mock 出口有 mock 参数就在这里返回,不发请求main.py:5462-5477

args = _locals_snapshot(locals()) 这一行值得单独记:它在所有改写之前抓快照,后面 completion_with_fallbacks(**args)(:5215)、batch_completion_models(**args)(:5220)和最外层的异常映射 completion_kwargs=args(:5791)都靠它拿到"用户原本传了什么"。

5.2 讲透三个

超时归一:四级优先级,外加一次类型降级。

CompletionTimeout.resolve(litellm/litellm_core_utils/completion_timeout.py:33)把散落各处的超时收敛成一个值。docstring 把优先级写死了:

优先级来源
1调用参数 timeout(或 Router 合并进来的 litellm_params)
2kwargs["timeout"]
3kwargs["request_timeout"]
4全局 litellm.request_timeout;从未配置过则用 COMPLETION_HTTP_FALLBACK_SECONDS

第四级有个微妙处:_fallback_when_no_explicit_timeout(completion_timeout.py:17)区分"没配置过"(None)和"显式配了一个值",显式值哪怕等于默认数字也照样尊重。

解析完还有一次类型降级:如果结果是 httpx.Timeout 但该 provider 不支持(supports_httpx_timeout(provider) 为假),就退化成 resolved.read 那个浮点秒数(completion_timeout.py:62-70)。

Responses API 桥接要检查两次。 第一次在解析完 provider 后立刻做(main.py:5248),负责"模型名带 responses/ 前缀"和"model_cost 里标了 mode: responses"这类光看模型名就能判定的情况,顺便把前缀从模型名里抹掉。第二次在 optional_params 算好之后(main.py:5482-5494),而且只在第一次没判定成 responses 时才跑——因为它要看参数组合:源码注释说明的是 gpt-5.4+ 带 tools + reasoning_effort、或者带 reasoning summary 别名的情况(responses_api_bridge_checkmain.py:1001,判定逻辑在 main.py:1045-1060)。判定成立就整条走 responses_api_bridge.completion(...) 返回,根本不进派发链(main.py:5520-5536)。

自定义定价是"注册到全局表",不是"塞进这次请求"。 用户传 input_cost_per_token / output_cost_per_token / input_cost_per_second 时,经 _register_custom_pricing_for_request(main.py:1188)把 CustomPricingLiteLLMParams 里出现的字段都收集进 _build_custom_pricing_entry(main.py:1148),再从 model_info 补上 modesupports_prompt_cachingmax_tokens,最后调 litellm.register_modelf"{custom_llm_provider}/{model}" 共享键写入(直连调用如此;Router 部署则改按 deployment id 单独注册,main.py:5282-5290 是调用点)。也就是说这是写进进程级模型成本表的副作用,后续同名调用都会看到。


6. 派发:一个冻结的信封 + 一条长链

6.1 它要解决的小问题

预处理结束时,手里攒了三十来个变量:modelapi_keyapi_baseheadersoptional_paramslitellm_paramsloggingtimeoutstream……62 个 _complete_* 函数各自需要其中一个子集。怎么把这堆东西交出去,而不写 62 遍三十行参数列表?

6.2 答案:打包成不可变的 ctx

_CompletionDispatchContext(litellm/types/completion.py:207)是一个 @dataclass(frozen=True, slots=True),30 个字段:

@dataclass(frozen=True, slots=True)
class _CompletionDispatchContext:

—— litellm/types/completion.py:206-207。两个修饰各有用意:

  • frozen=True:handler 不能改 ctx。62 个函数共享同一个对象,任何一个偷偷改字段都会变成跨 provider 的幽灵 bug。
  • slots=True:去掉 __dict__,省内存、字段名打错会直接报错。

返回类型也被收敛成一个三态联合(litellm/types/completion.py:240):

_CompletionDispatchResult = Union[
Coroutine[Any, Any, Union["ModelResponse", "CustomStreamWrapper"]],
"ModelResponse",
"CustomStreamWrapper",
]

这三态正好对上第 3.3 节 acompletion() 的三分支判断——协程对应异步、ModelResponse 对应同步、CustomStreamWrapper 对应流式。这是同步/异步共用一条路径能成立的类型层保证。

6.3 handler 长什么样

每个 _complete_* 都是同一个形状:开头把 ctx 拆包成局部变量,中间补 provider 特有的默认值,最后调 handler 对象。以 _complete_anthropic(main.py:2731)为例,它的 provider 特有工作就三件:

  1. key 的多级兜底:api_key or litellm.anthropic_key or litellm.api_key or os.environ.get("ANTHROPIC_API_KEY")(main.py:2748);
  2. api_base 兜底到 https://api.anthropic.com/v1/messages,并在不以 /v1/messages 结尾时自动补后缀——除非环境变量 LITELLM_ANTHROPIC_DISABLE_URL_SUFFIX 关掉这个行为(main.py:2752-2766);
  3. anthropic_chat_completions.completion(...)(模块级单例,建在 main.py:288),并把 acompletion 标记透传下去。

标记透传到这里就兑现了。 以共享 HTTP 处理器为例,litellm/llms/custom_httpx/llm_http_handler.py:549if acompletion is True: 分支直接 return self.acompletion_stream_function(...)self.async_completion(...)——返回的是协程对象,没有真正发起 IO。这就是为什么线程池里跑一趟同步主体不会阻塞。

6.4 为什么是显式长链,而不是字典表

第一反应总是:HANDLERS = {"anthropic": _complete_anthropic, ...} 一行搞定,何必写 200 行 elif?

因为分支条件根本不都是等值比较。 看几个真实分支:

分支形态例子位置
纯等值custom_llm_provider == "anthropic"main.py:5670
大集合 or 谓词model in litellm.open_ai_chat_completion_models provider in litellm.openai_compatible_providers JSONProviderRegistry.exists(provider) "ft:gpt-3.5-turbo" in modelmain.py:5632-5656
子串匹配"replicate" in model or ...main.py:5663
组合条件provider in litellm.openai_text_completion_compatible_providers kwargs.get("text_completion") is Truemain.py:5589-5595
两个 provider 共用一个 handlerprovider == "cohere_chat" or provider == "cohere"main.py:5676
运行期注册表custom_llm_provider in litellm._custom_providersmain.py:5770
废弃占位elif ...: pass(clarifai、together_ai 已被前面的 OpenAI 兼容分支接走)main.py:5666-5667main.py:5699-5706

字典表只能表达第一行那种。而且 elif 的顺序本身就是语义:custom_openai 那个大分支必须排在 mistralreplicate 之前,否则会把它们抢走;custom_custom_providers 必须排在所有内置 provider 之后,才能保证内置优先。

代价也很实在,不必粉饰:

  • 一个函数从 main.py:4901 拉到 main.py:5793,近 900 行;派发链本身 main.py:5575-5783 约 64 个分支。
  • 新增供应商要改三处:写 _complete_xxx、在链上插一个 elif、把 provider 加进枚举/JSON 注册表。
  • 分支覆盖只能靠测试保证,类型检查器帮不上忙——链里到处是 # pyright: ignore 注释,因为返回类型比声明的 Union[ModelResponse, CustomStreamWrapper] 更宽。

收口是一致的。 链尾 else: raise LiteLLMUnknownProvider(...)(main.py:5783),然后 return response(main.py:5784);整个 try 块的 except 把任何异常交给 exception_type 翻译成 OpenAI 风格异常(main.py:5785-5793)。在这条主线上,"不认识的 provider"和"供应商返回 500"最终走的是同一个异常出口。


7. 两个特殊出口

7.1 自定义供应商:两条完全不同的路

名字很像,机制完全不同:

custom_llm_provider == "custom"custom_llm_provider in litellm._custom_providers
出口函数_complete_custom(main.py:4669)_complete_custom_providers(main.py:4738)
用户要提供什么一个 api_base URL一个 CustomLLM 子类实例
请求格式LiteLLM 规定死的 {"model": ..., "params": {...}}完全由用户的类决定
响应格式规定死的 response_json["data"][0]["output"][0]由用户的类返回
支持流式/异步否(只有同步 post)是,四种组合都支持

_complete_custom 是最朴素的那条:把所有 message 的 content 用空格拼成一个 prompt,litellm.module_level_client.post(...) 打过去,按固定路径挖出字符串塞进 model_response.choices[0].message.content(main.py:4701-4735)。约定的请求/响应格式就写在函数体内的两段 docstring 里。

_complete_custom_providers 才是正经扩展点。它先在 litellm.custom_provider_map 里按 provider 名找到 CustomLLM 实例(main.py:4759-4763),找不到就抛 LiteLLMUnknownProvider(main.py:4764);然后调用一个2×2 的路由函数:

handler_fn = custom_chat_llm_router(async_fn=acompletion, stream=stream, custom_llm=custom_handler)

—— main.py:4767custom_chat_llm_router 本体在 litellm/llms/custom_llm.py:218,做的就是一张真值表:

async_fnstream返回的方法
custom_llm.astreaming
custom_llm.acompletion
custom_llm.streaming
custom_llm.completion

这张表是"同步/异步共用一条路径"在扩展点上的镜像:上层只传一个 acompletion 标记,选方法这件事收在一个地方。流式时外面再包一层 CustomStreamWrapper(main.py:4791-4795)。

7.2 离线测试通路:mock

mock_completion(main.py:826)让整条主线可以完全不发网络请求地跑完。触发点在预处理末尾(main.py:5462):只要 mock_responsemock_tool_callsmock_timeout 有任何一个,就直接返回,派发链根本不执行

它模拟的东西比"返回一段假文本"多得多:

传入行为位置
普通字符串填进 choices[0].message.content,并附上固定的 mock usagemain.py:931-959
dict / ModelResponse直接当响应用;stream=True 时转成流式main.py:897-904
异常实例原样抛出(openai.APIError 直接抛,其余包成 litellm.MockException)main.py:742-750
"litellm.RateLimitError" 等字符串抛出对应的真实异常类型main.py:752-777
"Exception: mock_streaming_error"抛出流中途的 529 错误main.py:886-892
n=k造 k 个 choicemain.py:935-942

超时模拟单独一对函数:_handle_mock_timeout(main.py:780)在 mock_timeout is True 且 timeout 不为 None 时,先真的 sleep 掉那段超时(_sleep_for_timeout,main.py:808,能处理 float / str / httpx.Timeout 三种形态),再抛 litellm.Timeout。异步版是 _handle_mock_timeout_async(main.py:794),在 acompletion()最开头就执行(main.py:487-488),比线程池那一趟还早。

mock_delay 的处理分两处,是个容易看漏的细节:同步路径在 mock_completiontime.sleep(main.py:893-895,条件里明确排除了 is_acompletion),异步路径在 acompletion()await asyncio.sleep(main.py:622-627,先用 should_run_mock_completion 确认确实会走 mock 才睡)。


8. 巧妙之处

一个标记换掉一整套异步代码。 acompletion=Truemain.py:593 一路传到 llm_http_handler.py:549,途中经过 62 个 handler 中的一个。上层不需要任何 async def 版本的调度逻辑,底层不需要知道自己是被谁调的。代价是异步路径要走一次线程池,以及返回值三态判断散在几处。

冻结 ctx 把"共享可变状态"这个大坑提前堵死。 frozen=True, slots=True(types/completion.py:206)意味着 62 个 handler 拿到的是同一份只读快照。这在一个 900 行的函数里尤其重要——否则"某个 provider 改了 ctx.headers 导致别的 provider 出错"这类 bug 几乎必然发生。

顺序即优先级,并且写在明面上。 get_llm_provider 的规则链(get_llm_provider_logic.py:169-494)和派发链(main.py:5575-5783)都是"命中即停"的线性结构。这种写法难看,但任何人读一遍就知道谁盖过谁;换成注册表 + 优先级数字反而更难推理。

安全修复留在了 docstring 里。 _endpoint_matches_api_base(get_llm_provider_logic.py:15)不只改了实现,还把攻击场景原样写进函数说明。这让后来者不会"顺手优化"回子串匹配。

mock 出口卡在正确的位置。 它在所有归一化之后、派发之前(main.py:5462)。往前挪就测不到预处理,往后挪就得进 handler。这个位置让测试既覆盖了完整的调度骨架,又完全不碰网络。


9. 边界与局限

completion() 是一个近 900 行的函数。 main.py:4901-5793,加上 62 个 _complete_*(main.py:1241-4899),整个 main.py 8954 行。新增供应商要动三个地方,合并冲突高发。

get_llm_provider 在一次异步调用里跑两遍。 main.py:600 一次(为异常映射),main.py:5237 一次(为真正的调度)。前者用的参数更少(没传 api_key、没传 litellm_params),所以两次结果在边缘情况下未必一致——代码里看不出有对齐机制。

Responses API 桥接判定也是两遍,且第二遍的条件依赖 optional_params 已经算好。这意味着"该不该桥接"这件事被拆在了预处理的头尾两端,读代码时容易只看到一半(main.py:5248main.py:5486)。

派发链里有 pass 分支。 clarifai(main.py:5666-5667)和 together_ai(main.py:5699-5706)被上面的 OpenAI 兼容大分支接走了,链上留的是空壳加注释。palm 则是直接抛 ValueError 告知已下线(main.py:5707-5710)。这些是历史包袱的可见痕迹。

类型检查是被局部关掉的。 派发链和几个提前返回点带着 # pyright: ignore[reportReturnType] 注释(如 main.py:5215main.py:5220main.py:5520),原因是实际返回类型(协程、列表)比 completion() 声明的返回类型宽。也就是说这条主线的返回契约靠约定和测试维持,不靠静态检查


10. 代码地图

主题文件路径符号名
同步入口(唯一主体)litellm/main.py:4901completion
异步入口(打标记 + 线程池)litellm/main.py:388acompletion
异步流式生成器(服务 text completion)litellm/main.py:720_async_streaming
模型名 → 供应商解析litellm/litellm_core_utils/get_llm_provider_logic.py:130get_llm_provider
Azure 非 OpenAI 模型特例litellm/litellm_core_utils/get_llm_provider_logic.py:52_is_non_openai_azure_model
api_base 安全匹配litellm/litellm_core_utils/get_llm_provider_logic.py:15_endpoint_matches_api_base
cohere / anthropic_text 改写litellm/litellm_core_utils/get_llm_provider_logic.py:74:100handle_cohere_chat_model_custom_llm_providerhandle_anthropic_text_model_custom_llm_provider
OpenAI 兼容供应商信息补全litellm/litellm_core_utils/get_llm_provider_logic.py:513_get_openai_compatible_provider_info
超时归一litellm/litellm_core_utils/completion_timeout.py:33CompletionTimeout.resolve
Responses API 桥接判定litellm/main.py:1001responses_api_bridge_check
自定义定价条目组装litellm/main.py:1148_build_custom_pricing_entry
provider config 查找litellm/utils.py:8030(类在 :7531)ProviderConfigManager.get_provider_chat_config
派发上下文(冻结 dataclass)litellm/types/completion.py:207_CompletionDispatchContext
派发返回类型三态litellm/types/completion.py:240_CompletionDispatchResult
派发链litellm/main.py:5575-5783completion 内的 if/elif
供应商出口(示例)litellm/main.py:2731:3939:1241_complete_anthropic_complete_bedrock_complete_azure
裸 api_base 自定义出口litellm/main.py:4669_complete_custom
CustomLLM 扩展出口litellm/main.py:4738_complete_custom_providers
CustomLLM 四向路由litellm/llms/custom_llm.py:218custom_chat_llm_router
离线 mocklitellm/main.py:826mock_completion
mock 超时litellm/main.py:780:794_handle_mock_timeout_handle_mock_timeout_async
mock 异常映射litellm/main.py:737_handle_mock_potential_exceptions
未知供应商异常litellm/main.py:5783LiteLLMUnknownProvider

下一步该读哪章: