前言
本地 LiteLLM Proxy 接入第三方模型网关(兼容 OpenAI 协议),调用大语言模型。看起来是个很标准的需求——上游是兼容 OpenAI 接口,LiteLLM 天然支持。但实际上从配置到调通一共踩了四个坑,每个坑的报错信息都挺有迷惑性的。记录一下排查过程和最终方案,给同样在折腾 LiteLLM 的朋友参考。
环境介绍
- LiteLLM Proxy v1.93.0,pip 安装,Python 3.12 venv,运行在 macOS 上,监听端口 4000
- 上游平台是一个兼容 OpenAI 协议的第三方模型网关,提供多种大语言模型调用
- 下游是 Hermes Agent,通过标准的
chat/completions接口调用 LiteLLM
需求很简单:让 Hermes 通过 LiteLLM 调用上游平台的模型。
坑 1:model 前缀用错
问题
在 LiteLLM UI 里把模型配成 openai/xxx-model,测试连接返回 Your request was blocked。
排查
打开 debug 日志(litellm._turn_on_debug()),看到 LiteLLM 发给上游的 model 字段是 openai/xxx-model——原样发出,没去前缀。上游平台不认这个模型名,直接拒了。
原因
这里有个认知误区。兼容 OpenAI 协议的第三方网关虽然接口格式兼容,但它不是 OpenAI 官方 API。两者的关键区别:
| OpenAI 官方 API | 兼容协议的第三方网关 | |
|---|---|---|
| 端点 | 官方域名 | 自建域名 |
| 模型名 | 由官方定义 | 网关转发,名称可能不同 |
| 鉴权 | 官方签发的 key | 平台自签发的 key |
| 协议 | 原生 | 兼容格式,但风控策略不同 |
| SDK 支持 | 官方 SDK 直接用 | 需要指定自定义 api_base |
我一开始以为既然上游模型兼容 OpenAI,LiteLLM 里就应该用 openai/ 前缀,这样能保留原生 API 的交互逻辑。但 LiteLLM 的 openai/ 前缀意味着"我是 OpenAI 官方",它不会去掉前缀,直接把 openai/xxx-model 发给上游——上游当然不认。
LiteLLM 通过 model 字段的前缀来路由 provider,源码在 get_llm_provider_logic.py:
custom_llm_provider = model.split("/", 1)[0] # 取前缀作为 provider
model = model.split("/", 1)[1] # 取剩余部分作为实际模型名
openai/xxx-model→ provider =openai,走官方逻辑,不会去掉前缀custom_openai/xxx-model→ provider =custom_openai,自动去掉前缀,只发xxx-model
修复
model 字段改成 custom_openai/xxx-model。第三方兼容平台无论转发的是什么模型,在 LiteLLM 里都应使用 custom_openai/ 前缀——它告诉 LiteLLM "用兼容协议去调,但这是第三方,不是官方"。
坑 2:api_base 路径重复拼接
问题
修完坑 1 后报错变成 Invalid URL (POST /v1/chat/completions/chat/completions),路径重复了。
原因
排查坑 1 时我把 api_base 改成了完整路径 https://上游地址/v1/chat/completions,但 custom_openai provider 会自动在 api_base 后拼接 /chat/completions,导致路径变成双重。
修复
api_base 只填到 /v1,LiteLLM 会自己拼后面的部分:
api_base: "https://上游地址/v1"
坑 3:上游根据 User-Agent 拦截请求
问题
前两个坑修完,还是 Your request was blocked,但用 curl 直调上游完全正常。
排查
这是最隐蔽的一个。debug 日志显示 URL 和 body 都对了,于是开始逐个对比 header。用 curl 分别测试不同的 User-Agent:
# ✅ 正常
curl -H 'User-Agent: curl/8.0' ...
# ❌ blocked
curl -H 'User-Agent: OpenAI/Python 1.0' ...
定位到:上游平台的风控会拦截 User-Agent 中包含特定 SDK 标识的请求。 而 LiteLLM 的 custom_openai provider 底层用的是 OpenAI Python SDK,默认发送带有 SDK 标识的 User-Agent,正好被拦。
修复
在 litellm_params 里加 extra_headers 覆盖掉默认的 User-Agent:
litellm_params:
model: custom_openai/xxx-model
api_base: "https://上游地址/v1"
extra_headers:
User-Agent: "litellm/1.0"
至此测试连接和实际调用都通了。
坑 4:Tool Schema 缺少 required 字段
问题
LiteLLM 测试通了,但下游 Agent 带工具调用时报错:
Invalid schema for function 'browser_back': None is not of type 'array'.
排查
下游传过来的 tool 定义里,有些 function 的 parameters 没有 required 字段:
{
"name": "browser_back",
"parameters": {
"type": "object",
"properties": {}
// ← 缺少 "required" 字段
}
}
上游平台对 JSON Schema 校验比较严格,required 缺失时被当作 null,而它期望的是数组 []。官方 API 不管这个,所以下游之前一直没问题,换了上游才暴露。
修复
不想改下游代码,在 LiteLLM Proxy 层加了个 pre-call hook,请求发出前自动给所有缺 required 的 tool schema 补上 "required": []。
custom_hooks.py(放在 LiteLLM 安装目录下):
from typing import Optional, Union
from litellm.integrations.custom_logger import CustomLogger
from litellm.proxy._types import UserAPIKeyAuth
from litellm.caching import DualCache
from litellm.types.utils import CallTypesLiteral
class ToolSchemaFixerHandler(CustomLogger):
async def async_pre_call_hook(
self,
user_api_key_dict: UserAPIKeyAuth,
cache: DualCache,
data: dict,
call_type: CallTypesLiteral,
) -> Optional[Union[Exception, str, dict]]:
tools = data.get("tools")
if not tools or not isinstance(tools, list):
return data
for tool in tools:
if not isinstance(tool, dict):
continue
func = tool.get("function")
if func is None:
if "parameters" in tool:
func = tool
else:
continue
params = func.get("parameters")
if isinstance(params, dict) and "required" not in params:
params["required"] = []
return data
# LiteLLM 的 get_instance_fn() 不会自动实例化类,
# 必须在模块级别导出实例,配置里写实例名而不是类名。
tool_schema_fixer = ToolSchemaFixerHandler()
litellm_config.yaml 里加上 callback:
litellm_settings:
callbacks:
- custom_hooks.tool_schema_fixer # 模块名.实例名
重启 LiteLLM 生效。注意配置里写的是实例名 tool_schema_fixer,不是类名 ToolSchemaFixerHandler——LiteLLM 内部用 getattr(module, name) 取值后直接使用,不会帮你实例化,写类名会报 missing 1 required positional argument: 'self'。
最终配置汇总
模型配置
| 字段 | 值 |
|---|---|
model | custom_openai/xxx-model |
api_base | https://上游地址/v1 |
drop_params | true |
extra_headers | {"User-Agent": "litellm/1.0"} |
litellm_config.yaml
litellm_settings:
callbacks:
- custom_hooks.tool_schema_fixer
custom_hooks.py
与 litellm_config.yaml 同目录放置,内容如上。
排查技巧
- 开 debug 日志:
litellm._turn_on_debug()会打印实际发出的 HTTP 请求,URL、headers、body 全能看到,定位问题的第一步。 - curl 逐变量对比:LiteLLM 失败但 curl 成功时,逐个对比 header 差异,本次就是靠这个定位到 User-Agent 拦截的。
- 读源码理解路由:provider 路由逻辑在
get_llm_provider_logic.py,model.split("/")的前缀决定一切。 - Proxy 层做兼容:上下游协议严格程度不一致时,在 Proxy 层加 hook 自动修复,比改上游或下游更合理。