LiteLLM Proxy 踩坑记录:接入兼容 OpenAI 协议的第三方模型网关

637 阅读5分钟

前言

本地 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'。


最终配置汇总

模型配置

字段值
modelcustom_openai/xxx-model
api_basehttps://上游地址/v1
drop_paramstrue
extra_headers{"User-Agent": "litellm/1.0"}

litellm_config.yaml

litellm_settings:
  callbacks:
    - custom_hooks.tool_schema_fixer

custom_hooks.py

与 litellm_config.yaml 同目录放置,内容如上。


排查技巧

  1. 开 debug 日志:litellm._turn_on_debug() 会打印实际发出的 HTTP 请求,URL、headers、body 全能看到,定位问题的第一步。
  2. curl 逐变量对比:LiteLLM 失败但 curl 成功时,逐个对比 header 差异,本次就是靠这个定位到 User-Agent 拦截的。
  3. 读源码理解路由:provider 路由逻辑在 get_llm_provider_logic.py,model.split("/") 的前缀决定一切。
  4. Proxy 层做兼容:上下游协议严格程度不一致时,在 Proxy 层加 hook 自动修复,比改上游或下游更合理。