LiteLLM Pass-Through Endpoint Guardrail Translation 源码解析与配置实战:让任意上游 API 也走统一护栏流水线
【免费下载链接】litellmThe fastest, litest AI Gateway. Rust core with Python SDK. Call 100+ LLM APIs in OpenAI (or native) format with cost tracking, guardrails, load balancing, and logging [Bedrock, Azure, OpenAI, Anthropic, OpenAI, VertexAI, vLLM, Nvidia NIM]项目地址: https://gitcode.com/GitHub_Trending/li/litellm
LiteLLM Proxy 的「透传端点(Pass-Through Endpoint)」允许把一个自定义路径原样转发到任意上游 API(例如 Cohere/v1/rerank),此时请求不再经过 chat/completion 这类标准 LLM 路由,护栏(Guardrail)默认不会生效。本文以仓库中 guardrail_translation/README.md 为骨架,结合litellm/llms/下的翻译层实现与 litellm/proxy/pass_through_endpoints 的主流程代码,讲解 LiteLLM 如何通过「护栏翻译映射(Guardrail Translation Mapping)」机制,把透传端点的请求/响应纳入统一护栏流水线——包括 JSONPath 字段定向提取、全载荷兜底、双层 handler 分发与 YAML 配置实战。
一、这套机制要解决的问题
透传端点本质上是一个「透明代理」:客户端请求/v1/rerank,Proxy 把它 1:1 转发给目标上游并原样回传响应。与标准 LLM 请求不同,这类端点:
- 没有
messages、model等 OpenAI 规范字段,护栏框架无法复用「从 messages 提取文本」的通用逻辑; - 请求/响应结构完全由上游 API 决定,可能是
{"query": "..."}、{"documents": [...]}、{"results": [...]}等任意形态; - 仍然存在内容安全、敏感信息泄露等风险,用户希望护栏在这里同样生效。
解决思路在模块 docstring 中写得很清楚(见 litellm/llms/pass_through/init.py):以配置文件里声明的字段表达式为指引,从请求/响应里定向取出需要检查的文本,再交给统一的护栏实例执行,且透传场景下护栏只做「检查(check)」而不改写请求文本。
二、模块为什么放在litellm/llms/而不是主透传代码里
README 首先解释了目录归属问题:该模块位于 litellm/llms/pass_through/guardrail_translation/,而不是与主透传实现放在一起,原因有二:
- 自动发现(Auto-discovery):
litellm/llms/__init__.py中的load_guardrail_translation_mappings()会递归扫描litellm/llms/下所有名为guardrail_translation且含__init__.py的目录,把其中导出的guardrail_translation_mappings字典聚合注册; - 一致性(Consistency):其他 provider 的护栏翻译 handler 都遵循同一目录模式(README 举例为
openai/chat/guardrail_translation/、anthropic/chat/guardrail_translation/),透传端点也照此办理,便于统一发现与加载。
对应源码位于 litellm/llms/init.py,其中核心是三个函数:
discover_guardrail_translation_mappings():遍历litellm/llms/目录树,凡os.path.basename(root) == "guardrail_translation"且有__init__.py的模块都会被导入,若模块含guardrail_translation_mappings属性则并入结果集;随后还会额外并入 MCP Server 相关映射(litellm/llms/init.py);load_guardrail_translation_mappings():带缓存的入口,首次调用触发全量发现,结果存入全局endpoint_guardrail_translation_mappings;get_guardrail_translation_mapping(call_type):按CallTypes取映射,若不存在会抛出异常并列出可用映射清单,方便排查「为什么某个透传类型没有护栏翻译 handler」。
三、本模块导出的映射注册表
模块的init.py 是自动发现机制的直接消费对象,它向注册表声明了两个CallTypes的映射:
guardrail_translation_mappings: Final = { CallTypes.pass_through: PassThroughEndpointHandler, CallTypes.allm_passthrough_route: LlmPassthroughRouteHandler, }其中:
CallTypes.pass_through→ 通用透传端点场景,由PassThroughEndpointHandler负责;CallTypes.allm_passthrough_route→ LLM 透传路由场景,由LlmPassthroughRouteHandler负责。allm_passthrough_route是比通用透传更接近「LLM 调用」的一种透传形态,请求体里带custom_llm_provider字段,因此需要按 provider 再分发。
两个 handler 都继承自 litellm/llms/base_llm/guardrail_translation/base_translation.py 中的抽象基类BaseTranslation,后者用@abstractmethod规定了两个必须实现的方法(base_translation.py):
process_input_messages(data, guardrail_to_apply, litellm_logging_obj):护栏处理请求入站内容;process_output_response(response, guardrail_to_apply, litellm_logging_obj, user_api_key_dict, request_data):护栏处理响应出站内容。
换句话说,任何希望接入护栏体系的端点类型,本质都是实现一个「把任意结构翻译成护栏可检查的GenericGuardrailAPIInputs」的翻译器。
四、PassThroughEndpointHandler:核心处理逻辑
该 handler 的实现集中在 handler.py,模块 docstring 明确它「使用 litellm_logging_obj 上的字段定向配置,提取指定字段交给护栏处理」。整体分三步:
1. 读取护栏设置(_get_guardrail_settings)
def _get_guardrail_settings(self, litellm_logging_obj, guardrail_name): passthrough_config = getattr(litellm_logging_obj, "passthrough_guardrails_config", None) if not passthrough_config or not guardrail_name: return None return PassthroughGuardrailHandler.get_settings(passthrough_config, guardrail_name)关键点:透传护栏的字段定向配置并不是临时从 YAML 现取的,而是由透传主流程在请求进入时把它存到litellm_logging_obj.passthrough_guardrails_config属性上(见后文数据流),handler 侧通过getattr取回,再按护栏名查设置。这就把「配置解析」与「护栏执行」解耦成两个阶段。
2. 字段定向或全载荷兜底(_extract_text_for_guardrail)
def _extract_text_for_guardrail(self, data, field_expressions): if field_expressions: text = JsonPathExtractor.extract_fields( data=data, jsonpath_expressions=field_expressions, ) return text # 未配置字段表达式 → 整包检查(剔除内部字段) payload_to_check = { k: v for k, v in data.items() if not k.startswith("_") and k not in ("metadata", "litellm_logging_obj") } return safe_dumps(payload_to_check)这里体现了 README 强调的两条设计:
- 字段定向(Field Targeting):若配置了
request_fields/response_fields(JSONPath 表达式列表),仅提取命中的字段拼成检查文本,降低护栏扫描成本与误报率; - 全载荷兜底(Full Payload Fallback):若未配置,就把整包 JSON 作为检查文本,但会剔除三类内部字段——下划线开头的键、
metadata、litellm_logging_obj,避免把代理自身的审计元数据塞给护栏(序列化走 safe_json_dumps,保证异常值不会炸掉序列化)。
3. 组装输入并执行护栏(仅检查、不改写)
process_input_messages与process_output_response的落点一致:把提取出的文本包装为GenericGuardrailAPIInputs(texts=[text_to_check]),若载荷含model字段则一并带上,然后调用guardrail_to_apply.apply_guardrail(inputs=..., input_type="request"/"response", ...)(handler.py)。
三个值得注意的实现细节:
- 纯检查语义:透传 handler 拿到
_guardrailed_inputs后并不用它回填/改写原始data,而是原样return data/return response——护栏在这里负责「拦截放行」,文本改写由各 provider 自己的翻译器完成; - 响应必须可解析:
process_output_response开头就检查isinstance(response, dict),非字典响应(如流式 chunk、二进制)直接跳过(handler.py); - 空文本短路:提取结果为空时直接跳过护栏(
No text to check, skipping guardrail),避免护栏空跑。
响应处理还额外做了上下文补全:当request_data为空(SDK / 直接调用路径)时自建{"response": ...}字典,并把user_api_key_dict通过基类transform_user_api_key_dict_to_metadata转成user_api_key_前缀键写入litellm_metadata,让护栏回调里能拿到密钥/团队身份信息。
五、主透传实现:litellm/proxy/pass_through_endpoints/
README 指出主透传端点实现并不在本模块,而在:
litellm/proxy/pass_through_endpoints/ ├── pass_through_endpoints.py # 核心透传路由逻辑 ├── passthrough_guardrails.py # 护栏收集与字段定向 ├── jsonpath_extractor.py # JSONPath 字段提取工具 └── ...目录下还有streaming_handler.py、success_handler.py、passthrough_endpoint_router.py等,共同构成透传子系统。三个与护栏翻译层关系最密切的文件如下。
1.passthrough_guardrails.py:护栏收集与执行助手
PassthroughGuardrailHandler是透传护栏的「总调度」,负责四件事(passthrough_guardrails.py):
normalize_config:把配置归一为字典。支持两种写法——简单列表["g1", "g2"]会被转成{"g1": None, "g2": None};带设置的字典原样保留;is_enabled/get_guardrail_names:判空、取名单。模块 docstring 强调透传是opt-in 模型——只有显式配置至少一个护栏才执行,绝不隐式启用;collect_guardrails:在透传启用后,合并「端点自己声明的护栏」与「继承自 org/team/key 元数据的护栏」。后者通过_add_guardrails_from_key_or_team_metadata从user_api_key_dict.metadata/team_metadata中提取并去重合并,实现「组织/团队/密钥级护栏自动继承」;execute:主入口,把护栏名写成request_data["metadata"]["guardrails"] = {name: True},并通过set_passthrough_guardrails_config写入请求级上下文,供后续阶段查询(上下文读写由 litellm/proxy/pass_through_endpoints/passthrough_context.py 提供)。
2.jsonpath_extractor.py:轻量 JSONPath 引擎
字段定向依赖JsonPathExtractor,它不引入外部 JSONPath 依赖,而是实现了一个极简、够用的子集(jsonpath_extractor.py):
- 简单键:
"query"→data["query"]; - 嵌套键:
"foo.bar"→data["foo"]["bar"](点号逐层取); - 数组通配:
"documents[*].text"/"items[*]"→ 遍历数组每项递归求值,结果扁平合并; - 多点路径:表达式列表逐一求值,最终所有命中值用换行
\n连接成一个字符串。
解析实现先把[*]归一为.[*]再按.切分,遇到[*]段时判断当前节点是 list,取出剩余路径对每个元素递归evaluate(jsonpath_extractor.py)。单字段求值失败只打 debug 日志、不影响其余字段,因此对上游形态变化有不错的容错性。
3.pass_through_endpoints.py:把护栏挂进透传请求生命周期
主路由文件中可看到护栏配置从入参到执行的全过程:
- 端点配置里
guardrails字段与path、target、headers并列(PassThroughGenericEndpoint模型见 litellm/proxy/_types.py),forward_request接收guardrails_config: dict | None; - 进入请求处理时调用
PassthroughGuardrailHandler.collect_guardrails(...),把guardrails_to_run写入_parsed_body["metadata"]["guardrails"]; - 随即「初始化 LOGGING OBJECT」,并把
logging_obj.passthrough_guardrails_config = guardrails_config存到日志对象上——这正是第四节PassThroughEndpointHandler._get_guardrail_settings反查配置的数据来源; - 响应侧在
response.status_code < 400且响应体可 JSON 解析、且存在待运行护栏时重新挂载guardrails(见 pass_through_endpoints.py),保证 post-call 护栏能看到它们,随后才把响应交还客户端。
于是整条调用链闭合为:
请求进入 → collect_guardrails(合并端点+org/team/key护栏) → metadata.guardrails 写入 + set_passthrough_guardrails_config → logging_obj.passthrough_guardrails_config 缓存 → PassThroughEndpointHandler.process_input_messages → _get_guardrail_settings 反查 request_fields/response_fields → JsonPathExtractor 定向提取 / 全载荷兜底 → apply_guardrail(检查+拦截) → 原样放行或抛 HTTPException 阻断六、YAML 配置实战与参数详解
README 给出了最小可运行配置,这里原样保留并做必要标注:
passthrough_endpoints: - path: "/v1/rerank" target: "https://api.cohere.com/v1/rerank" guardrails: bedrock-pre-guard: request_fields: ["query", "documents[*].text"] response_fields: ["results[*].text"]逐项拆解:
path:注册到 LiteLLM Proxy 上的对外路由;target:实际转发目标(这里是 Cohere rerank 服务);guardrails:opt-in 的护栏声明。名字(如bedrock-pre-guard)必须在 Proxy 的护栏注册表里存在(即配置文件中guardrails:顶层节里定义的预护栏实例),透传端点本身不定义护栏,只引用;request_fields:PassThroughGuardrailSettings的入站字段定向(见 litellm/proxy/_types.py)。接收 JSONPath 表达式列表,典型值:query、documents[*].text、messages[*].content;不填则护栏跑整个请求载荷;response_fields:出站字段定向,典型值:results[*].text、output;不填则护栏跑整个响应载荷。
guardrails还支持两种等价写法。PassthroughGuardrailHandler.normalize_config会把列表格式归一为字典格式:
# 写法 A:简单列表(无字段定向,整包检查) guardrails: ["bedrock-pre-guard"] # 写法 B:字典 + 每个护栏各自的 request_fields / response_fields guardrails: bedrock-pre-guard: request_fields: ["query", "documents[*].text"] response_fields: ["results[*].text"]使用建议:
- 字段定向能显著降低护栏扫描成本并减少误报,凡是明确知道敏感/待审内容落在哪个字段的场景都应优先配置;
- 数组通配
[*]语法只支持到单层数组递归,若上游返回嵌套更深的结构(数组套数组),需要评估该轻量引擎的覆盖范围(jsonpath_extractor.py 的实现即唯一事实依据); - 护栏名与字段设置一一对应意味着同一端点可为不同护栏配置不同的扫描面——例如入站文本安全护栏只扫
query,出站合规护栏只扫results[*].text。
七、LlmPassthroughRouteHandler:按 provider 二次分发
通用透传之外,同文件还定义了一个面向 LLM 透传路由的翻译器LlmPassthroughRouteHandler(handler.py)。它本身不做字段提取,而是充当分发器:
- 内部维护
_PROVIDER_HANDLERS注册表,目前仅注册{"bedrock": BedrockPassthroughGuardrailHandler}(provider 专属翻译器位于 litellm/llms/bedrock/passthrough/guardrail_translation/handler.py); process_input_messages/process_output_response依据data["custom_llm_provider"](响应侧从request_data取)选择 provider handler,找不到对应 provider 时只打 debug 日志并原样放行,保证「未知 provider 不阻塞透传」;- 额外暴露一批事件流(SSE)辅助静态方法:
is_event_stream_response、event_stream_media_type、supports_event_stream_de_anonymization、de_anonymize_event_stream等。这些方法委托给 provider handler 的对应实现,用于识别并「去匿名化」事件流内容——因为流式响应需要反序列化成可检查形态后才能跑护栏,检查完再还原给客户端(handler.py)。
这从源码结构上印证了 README 提到的设计一致性:每个端点/provider 形态各写一个guardrail_translationhandler,统一通过CallTypes映射注册,由统一框架发现和调用。
八、与其他 Provider 翻译层的关系
透传护栏翻译并不是孤例。README 明确指出所有护栏翻译 handler 遵循同一模式(openai/chat/guardrail_translation/、anthropic/chat/guardrail_translation/等),它们都:
- 继承同一个
BaseTranslation抽象基类,保证process_input_messages/process_output_response(以及可选的流式处理钩子)接口一致; - 在各自目录的
__init__.py导出guardrail_translation_mappings字典; - 由
litellm/llms/__init__.py的自动发现机制统一加载,按CallTypes检索。
因此,接入新形态(新 provider 透传、新端点类型)时最需要做的是补一个翻译器并注册CallTypes映射,而无需改动护栏执行框架本身——这也解释了本模块目录位置的选择是出于框架可扩展性的刻意安排(参见 litellm/llms/init.py 的扫描与聚合逻辑)。
九、小结
围绕透传端点护栏这条链路,LiteLLM 实际上建立了三层清晰的分工:
| 层次 | 位置 | 职责 |
|---|---|---|
| 配置层 | passthrough_endpoints[*].guardrails(YAML)与 PassThroughGuardrailSettings | 声明护栏名、request_fields/response_fieldsJSONPath 定向 |
| 执行层 | passthrough_guardrails.py + jsonpath_extractor.py | opt-in 收集(含 org/team/key 继承)、字段提取、写请求元数据 |
| 翻译层 | litellm/llms/pass_through/guardrail_translation/handler.py | 把透传载荷翻译成护栏输入,CallTypes→ handler 注册与自动发现 |
透传端点的护栏执行是opt-in、字段可定向、默认全载荷兜底的行为模型:不配置就完全不干预转发;配置了就在 pre-call 与 post-call 两处对目标字段做纯检查式拦截,拦截失败可抛异常阻断,通过则 1:1 放行上游响应。对需要「透明转发 Cohere、Bedrock 等非标准 API 又不想失去内容安全能力」的网关部署场景,这套翻译机制是值得直接照搬的最小实现范式。
【免费下载链接】litellmThe fastest, litest AI Gateway. Rust core with Python SDK. Call 100+ LLM APIs in OpenAI (or native) format with cost tracking, guardrails, load balancing, and logging [Bedrock, Azure, OpenAI, Anthropic, OpenAI, VertexAI, vLLM, Nvidia NIM]项目地址: https://gitcode.com/GitHub_Trending/li/litellm
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考