数据截至 (上游 commit 3f15dc32871c)
翻译层:BaseConfig 契约与参数映射
30 秒导读: LiteLLM 的卖点是"一套 OpenAI 代码调 100+ 家模型"。这一章讲这个"统一"到底是怎么做出来的: 每家供应商写一个
BaseConfig子类,实现 6 个必答题(参数白名单、参数映射、鉴权、请求组装、响应还原、错误分类); 上层只认这个接口,永远不知道下面是 Anthropic 还是 Bedrock。
本章只讲翻译。请求实际怎么发出去、流式增量怎么拼,见 03-http-and-streaming; 一次调用的完整时间线见 01-request-lifecycle。
1. 这是什么(零基础也能懂)
一句话定义: 翻译层 = 一个抽象基类 BaseConfig(litellm/llms/base_llm/chat/transformation.py:65)+ 每家供应商一个子类,
负责把 OpenAI 格式的请求翻成供应商格式、再把供应商的响应翻回 OpenAI 格式。
它解决的问题。 你写好了一段 OpenAI 代码,想换成 Claude。麻烦不在"换个 URL":
| 你写的(OpenAI) | Anthropic 那边叫什么 / 长什么样 |
|---|---|
max_completion_tokens=100 | max_tokens=100(名字不同) |
stop=["\n\n"] | stop_sequences=["\n\n"] |
messages=[{"role":"system",...}, ...] | system 必须从 messages 里抽出来放顶层 system 字段 |
tools=[{"type":"function","function":{...,"parameters":{...}}}] | tools=[{"name":..., "input_schema":{...}}] |
user="alice" | metadata={"user_id":"alice"} |
response_format={"json_schema":...} | 老模型不支持,得假装成一次工具调用去逼出 JSON |
一句话直觉。 把它当成一个双向翻译官:进门时把中文翻成对方的语言,出门时再翻回中文。 调用方进出两侧看到的都是同一种语言(OpenAI 格式),中间那段外语它压根不知道。
用起来什么样。 用户视角只有一行差别:
# 同一段代码,只换 model 字符串
litellm.completion(model="gpt-4o", messages=msgs, max_tokens=100, tools=tools)
litellm.completion(model="anthropic/claude-sonnet-4-5", messages=msgs, max_tokens=100, tools=tools)
litellm.completion(model="bedrock/anthropic.claude-3-5-sonnet-20241022-v2:0", messages=msgs, max_tokens=100, tools=tools)
三行走的是三个不同的 BaseConfig 子类,返回的都是同一个 ModelResponse。
2. 顶层全景(它大概怎么转)
怎么读这张图: 从上往下是一次请求的时间顺序。灰色的第 ③ 步之前只动参数字典,之后才动 messages / URL / header。 注意最上和最下都是 OpenAI 格式——这就是"统一接口"的全部含义。
调用方 ← 永远只写 OpenAI 格式 →
completion(model="anthropic/claude-...", messages=[...], max_completion_tokens=100, tools=[...])
│
▼
┌────────────────────────────────────────────────────┐
│ ① 选 config:按 provider + model + base_model 挑一个 │
│ 子类实例 ProviderConfigManager │
└────────────────────────────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────┐
│ ② 参数映射(只碰参数字典,不碰 messages) │
│ 能不能收? get_supported_openai_params │
│ 怎么改名? map_openai_params │
│ 产出 optional_params │
└────────────────────────────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────┐
│ ③ 请求组装(碰 messages / URL / header) │
│ 鉴权头 validate_environment │
│ 真实URL get_complete_url │
│ 请求体 transform_request │
│ 签名 sign_request(Bedrock 之类才用) │
└────────────────────────────────────────────────────┘
│ 供应商私有格式的 HTTP 请求 → 见 03 章
│ 供应商私有格式的 HTTP 响应 ←
▼
┌────────────────────────────────────────────────────┐
│ ④ transform_response → ModelResponse │
│ 非 2xx → get_error_class 转成供应商专属异常 │
└────────────────────────────────────────────────────┘
│
▼
调用方 ← 拿到的仍是 OpenAI 格式(ModelResponse) →
第 ② 步发生在 litellm.completion() 内部的参数预处理阶段;第 ③④ 步发生在 HTTP 编排器里,
按顺序在 litellm/llms/custom_httpx/llm_http_handler.py 的 completion(:455)中依次调用:
should_fake_stream(:488)→ validate_environment(:492)→ get_complete_url(:502)→
transform_request(:511)→ sign_request(:522)。
部件职责一览:
| 部件 | 干什么 | 在哪个文件 |
|---|---|---|
BaseConfig | 定义契约:6 个必答题 + 一堆可选钩子 | litellm/llms/base_llm/chat/transformation.py:65 |
get_optional_params | 参数过滤 + 分发到某个 config 的 map_openai_params | litellm/utils.py:3934 |
ProviderConfigManager | 按 provider/model 选出该用哪个 config | litellm/utils.py:7780 |
| 各供应商子类 | 真正的翻译逻辑 | 如 litellm/llms/anthropic/chat/transformation.py:230 |
JSONProviderRegistry | 纯声明式接入 OpenAI 兼容供应商,零 Python 代码 | litellm/llms/openai_like/json_loader.py:27 |
CustomLLM | 用户自带整套 handler,完全绕开翻译层 | litellm/llms/custom_llm.py:41 |
3. 契约:BaseConfig 的必答题
BaseConfig 是 ABC。带 @abstractmethod 的一共 6 个——不实现就实例化不了,这就是"接一家新供应商的最小工作量"。
3.1 六个必答题
| 方法(行号) | 一句话职责 | 谁在什么时候调它 |
|---|---|---|
get_supported_openai_params(:180) | 这个 model 能收哪些 OpenAI 参数(返回名字列表) | 参数校验前,经 litellm/litellm_core_utils/get_supported_openai_params.py:8 汇总 |
map_openai_params(:230) | 把 OpenAI 参数名/形状翻成供应商的 | litellm/utils.py:4059 起的分发链里 |
validate_environment(:240) | 取 API key、填鉴权 header;key 缺失就在这里报错 | 发请求最前面(llm_http_handler.py:492) |
transform_request(:298) | messages + 参数 → 供应商请求体 dict | URL 定好之后(llm_http_handler.py:511) |
transform_response(:330) | 供应商响应 → ModelResponse | 拿到响应后(llm_http_handler.py:657、:715) |
get_error_class(:358) | HTTP 错误 → 供应商专属异常(BaseLLMException,:40) | 非 2xx 时(llm_http_handler.py:324、:374) |
关键分工:map_openai_params 只碰参数,transform_request 才碰 messages。
Anthropic 的实现里专门写了一段注释说明为什么工具名改写要放在 transform_request 而不是 map_openai_params——
因为前者是 Anthropic / Bedrock-Anthropic / Vertex-Anthropic 三条路径共用的唯一咽喉
(litellm/llms/anthropic/chat/transformation.py:1403-1413),而且 optional_params 会被整个展开进请求体,
放内部状态进去会被供应商判成非法字段。
3.2 可选钩子:只有需要的供应商才覆盖
| 钩子(行号) | 什么时候需要 | 真实例子 |
|---|---|---|
sign_request(:252) | 请求体组装完还要整体签名 | Bedrock SigV4:litellm/llms/bedrock/chat/invoke_transformations/base_invoke_transformation.py:144 |
get_complete_url(:277) | URL 里要带 model 或固定路径 | Ollama 拼 /api/generate:litellm/llms/ollama/completion/transformation.py:408 |
async_transform_request(:308) | 异步组装时要发 HTTP(如把图片 URL 抓下来转 base64) | 目前只有 OpenAI 路径调用(litellm/llms/openai/openai.py:854,实现在 litellm/llms/openai/chat/gpt_transformation.py:445) |
transform_parsed_response_dict(:346) | 走 OpenAI SDK 的供应商绕过了 transform_response,需要在这里修畸形响应 | github_copilot 返回空 choices 时补救:litellm/llms/github_copilot/chat/transformation.py:263 |
get_model_response_iterator(:361) | 提供流式增量解析器 | 流式路径调用(llm_http_handler.py:730),细节见 03 章 |
has_custom_stream_wrapper(:404) | 声明"我自带整套流式包装,别走通用管线" | litellm/llms/oci/chat/transformation.py:250;判定点 llm_http_handler.py:602 |
should_fake_stream(:119) | 供应商不支持真流式,先拿完整响应再切成假增量 | Azure o-series:litellm/llms/azure/chat/o_series_transformation.py:69;判定点 llm_http_handler.py:488 |
_add_response_format_to_tools(:183) | 供应商不支持 response_format,用 tool calling 模拟 JSON schema | Fireworks AI:litellm/llms/fireworks_ai/chat/transformation.py:312 |
calculate_additional_costs(:428) | token 之外还有基础设施费/路由费 | Azure AI model router:litellm/llms/azure_ai/azure_model_router/transformation.py:91;调用点 litellm/cost_calculator.py:269 |
_add_response_format_to_tools 值得单独看一眼——这是"用工具调用假装结构化输出"的标准做法:
把 JSON schema 塞进一个名叫 json_tool_call 的假工具(常量 RESPONSE_FORMAT_TOOL_NAME,litellm/constants.py:1313),
再用 tool_choice 逼模型必须调它,最后打上 optional_params["json_mode"] = True
(base_llm/chat/transformation.py:208-224)。响应侧看到 json_mode 就把这次"工具调用的参数"当作正文内容还回去。
3.3 一个最小 config 长什么样
# 示意,非源码:接一家新供应商的最小骨架
class MyProviderConfig(BaseConfig):
def get_supported_openai_params(self, model): # 我只认这三个
return ["max_tokens", "temperature", "stream"]
def map_openai_params(self, non_default_params, optional_params, model, drop_params):
for k, v in non_default_params.items():
if k == "max_tokens":
optional_params["maxOutputTokens"] = v # 改名
elif k in ("temperature", "stream"):
optional_params[k] = v
return optional_params
def validate_environment(self, headers, model, messages, optional_params,
litellm_params, api_key=None, api_base=None):
headers["X-Api-Key"] = api_key or os.environ["MYPROVIDER_API_KEY"]
return headers
# transform_request / transform_response / get_error_class 同理
重点看:map_openai_params 是一个纯字典 → 字典的函数,没有 IO、没有 messages。
4. 参数过滤与映射:get_optional_params
入口是 litellm/utils.py:3934 的 get_optional_params。它做四件事,顺序固定。
4.1 第一刀:passed_params 与 non_default_params
passed_params= 调用方传进来的全部东西,用locals().copy()一把抓(utils.py:3980), 包括**kwargs里那些非 OpenAI 的私货(top_k、aws_region_name…)。non_default_params= 从中筛出"值和 OpenAI 默认值不一样"的那些 (PreProcessNonDefaultParams.base_pre_process_non_default_params,utils.py:3715; 默认值表DEFAULT_CHAT_COMPLETION_PARAM_VALUES在litellm/constants.py:669)。
为什么要分这两个? 因为"用户没传 temperature"和"用户显式传了 temperature=None"必须区分。 只有真正被改过的参数才需要翻译,也才需要被校验——用户没传的东西,不该因为供应商不支持就报错。
被排除在 non_default_params 之外的还有控制类参数本身(drop_params、allowed_openai_params、
additional_drop_params、api_version)和 messages(utils.py:3737-3753)——它们是指令,不是要转发的参数。
紧接着 pre_process_non_default_params(utils.py:3781)做两处规整:
Pydantic 模型形式的 response_format 转成 JSON schema(:3805-3812);
清掉 tools 里会让 Gemini 报错的 additionalProperties: False(:3819-3826)。
另有一个 pre_process_optional_params(utils.py:3856)专管云厂商的鉴权参数
(project / region_name / token 翻成 azure / vertex / watsonx / aws 各自的写法),
以及"供应商根本不支持 function calling 时改写成 prompt"的退路(:3883-3924)。
4.2 第二刀:_check_valid_arg 与 UnsupportedParamsError
if unsupported_params:
if litellm.drop_params is True or (drop_params is not None and drop_params is True):
for k in unsupported_params.keys():
non_default_params.pop(k, None)
else:
raise UnsupportedParamsError(...)
—— litellm/utils.py:4033-4041,内嵌函数 _check_valid_arg(:4007)的收尾。默认行为是报错而不是静默丢弃:
你以为传了 logit_bias,结果供应商根本不支持,LiteLLM 宁可让你知道。异常类是 UnsupportedParamsError
(litellm/exceptions.py:911),它继承 BadRequestError,所以对调用方而言就是一个普通的 400 族错误。
有三个例外会被无条件放行:user / stream_options / stream、LangChain 习惯性传的 n=1、
以及 max_retries(utils.py:4020-4026,max_retries 那处源码里明确标了 TODO: This is a patch)。
4.3 四个逃生阀,语义各不相同
| 逃生阀 | 写在哪 | 作用域 | 语义 | 依据 |
|---|---|---|---|---|
litellm.drop_params | 全局模块变量 | 整个进程 | 不支持的参数静默丢掉 | litellm/__init__.py:237(可由 LITELLM_DROP_PARAMS 环境变量设置);判定 utils.py:4033 |
drop_params=True | 单次 completion() 入参 | 这一次请求 | 同上,只影响本次 | utils.py:3966;判定 utils.py:4033 |
additional_drop_params=[...] | 单次 completion() 入参 | 这一次请求 | 按名字点名丢掉,不管供应商支不支持 | _should_drop_param,utils.py:2982 |
allowed_openai_params=[...] | 单次 completion() 入参 | 这一次请求 | 反向:强行把参数加进白名单,原样透传 | utils.py:4050,回填 _apply_openai_param_overrides,utils.py:4576 |
三个"丢"里最容易混的是前两个 vs 第三个:
drop_params是条件性的——只丢"供应商不支持的";供应商支持的照样发出去。additional_drop_params是无条件的——名字对上就丢,而且丢得更早:它在non_default_params构造阶段 就把参数剔除了(utils.py:3753),所以_check_valid_arg根本看不到它,自然也不会报错。 它还支持a.b.c这种嵌套路径,在最后一步再做一次深层删除(utils.py:4524-4530)。allowed_openai_params是唯一往里加的阀门:把参数接到supported_params尾巴上让校验放行, 末了再把它塞回optional_params。这里有个真实教训——早期实现会给"白名单里但用户没传"的参数写None, 结果 OpenAI SDK 收到不认识的顶层 kwarg 直接炸,修法是只回填用户真传了的(utils.py:4576-4595,附 issue #25697)。
UnsupportedParamsError 的报错文案会把这四个阀门里的三个直接写给用户看(utils.py:4039)——错误信息即文档。
4.4 每种端点各有一套平行实现
聊天不是唯一入口。嵌入、图像、转写各自复制了一份同样结构的流程:
| 端点 | 函数 | 位置 | 自带的校验函数 |
|---|---|---|---|
| chat | get_optional_params | litellm/utils.py:3934 | _check_valid_arg,:4007 |
| embedding | get_optional_params_embeddings | litellm/utils.py:3233 | _check_valid_arg,:3165 |
| image | get_optional_params_image_gen | litellm/utils.py:3108 | _check_valid_arg,:3037 |
| transcription | get_optional_params_transcription | litellm/utils.py:3002 | _check_valid_arg,:3259 |
四份逻辑高度重复,只有 chat 那份被抽出了 PreProcessNonDefaultParams(utils.py:3713)供 embedding 复用
(embedding_pre_process_non_default_params,:3760)。复制的代价也看得见:transcription 的报错文案写死成
"Setting user/encoding format is not supported by …"(utils.py:3048),不管实际不支持的是哪个参数。
4.5 分发:一条几百行的 if/elif,和它旁边的通用通道
从 utils.py:4057 开始是一条按 custom_llm_provider 逐个 elif 的长链,一直排到 :4502。
但链条末尾有两个"通用出口":
elif provider_config is not None:(utils.py:4495)—— 只要ProviderConfigManager给出了 config,就直接调它的map_openai_params。else:兜底成OpenAILikeChatConfig(utils.py:4502)。
也就是说新接的供应商根本不需要往这条长链里加分支,只要能被 ProviderConfigManager 认出来即可 。
前面那几十个显式分支是历史包袱(有些还带着 Bedrock 路由、Azure o-series 判定这类无法通用化的逻辑)。
最后一步 add_provider_specific_params_to_optional_params(utils.py:4535)处理"不在 OpenAI 规范里的私货":
OpenAI 兼容供应商塞进 extra_body,其他供应商直接平铺到 optional_params。
5. 用哪个 config?ProviderConfigManager 说了算
ProviderConfigManager.get_provider_chat_config(model, provider, base_model)(litellm/utils.py:8030)
是选型的唯一入口。它按三层优先级挑:
provider = "openai"? ──→ o-series 模型 → openaiOSeriesConfig
│ GPT-5 模型 → OpenAIGPT5Config
│
provider = "azure"? ──→ 用 base_model(而非部署名)判类型 → _get_azure_config(:7954)
│
查 _PROVIDER_CONFIG_MAP(懒加载,O(1) 字典)
│ 命中 → Python 类(有定制逻辑,优先级最高)
│ 未命中 ↓
查 JSONProviderRegistry → 动态生成一个 OpenAI 兼容 config
│ 未命中 ↓
返回 None(上层退回 OpenAILikeChatConfig 兜底)
三个设计点值得记:
- 同一个 provider 可以给出不同 config,取决于
model字符串。OpenAI 的 o-series 和 GPT-5 参数集就和普通模型不同。 base_model是能力提示,不是路由键。 Azure 部署名可以随便起(my-gpt4-prod),从名字看不出模型能力; 传base_model后按它判类型。注意它是加法:最终 supported params 是 model 与 base_model 两者的并集, 只会加能力、不会减(get_supported_openai_params.py:53-58的注释与实现)。- Python 类优先于 JSON(
utils.py:8057附近注释,懒加载_PROVIDER_CONFIG_MAP),因为 Python 类才有定制覆盖。
6. 真实样例:AnthropicConfig 全流程
AnthropicConfig(litellm/llms/anthropic/chat/transformation.py:230)是最值得读的样例——
它同时被直连 Anthropic、Bedrock invoke、Vertex Anthropic、Azure Anthropic 四条路径复用。
6.1 白名单是动态的
get_supported_openai_params(:434)返回一个固定列表,但会按模型能力追加:
if ("claude-3-7-sonnet" in model
or AnthropicConfig._is_adaptive_thinking_model(model)
or supports_reasoning(model=model, custom_llm_provider=self.custom_llm_provider)):
params.append("thinking")
params.append("reasoning_effort")
—— :454-465。同一家供应商、不同模型,能收的参数不一样,所以这个方法的入参是 model 而不是无参。