数据截至 (上游 commit 01f4282f1ffe)
插件框架:pipe 自定义模型与 filter 拦截函数
30 秒导读: Open WebUI 允许用户在后台粘贴一段纯 Python 代码,就地扩展这个聊天智能体——不用改源码、不用重启。两种插件:
Pipe类把自己伪装成一个"模型"接入模型下拉框(自定义后端);Filter类在请求进出的三个时刻拦截并改写数据。本章讲清"插件是什么、怎么被加载进来、怎么扩展",拦截的精确调用时机只做交叉引用,细节在 02 与 04。
本章属于 Open WebUI 系列。其它章见 index:01 请求生命周期、02 入口装配、03 工具系统、04 智能体循环与出口。
1. 这是什么(零基础也能懂)
一句话定义: 插件框架 = 让用户用一段 Python 代码给这个智能体加一个"新模型"或"一道拦截关卡",而无需碰主程序。
解决什么问题 / 给谁用。 假设你想:
- 接一个官方没内置的模型服务(比如某个私有网关、某个新出的 API);
- 或者在每条消息发给模型之前偷偷加一句系统提示、脱敏用户输入;
- 或者在模型答完之后给回复加个免责声明、翻译一遍。
改主程序太重。Open WebUI 的答案是:把这些扩展点开放成用户可编辑的 Python 源码,存进数据库,运行时动态加载执行。
它能做什么(两类插件):
| 插件类型 | 你要写的类 | 扮演什么角色 | 白话 |
|---|---|---|---|
| Pipe | Pipe | 一个"自定义模型"后端 | 模型下拉框里多出一个你的模型;选它,聊天请求就走进你的 pipe() 函数 |
| Filter | Filter | 进出请求的拦截器 | 在请求送模型前(inlet)、流式分片经过时(stream)、答完之后(outlet)改写数据 |
| Action | Action | 消息下方的按钮动作 | (本章不展开,加载机制同上,见 load_function_module_by_id) |
用起来什么样。 用户在管理后台粘贴的,就是一段带类的普通 Python。一个最小 Pipe:
# 示意,非源码 —— 这就是用户粘进后台的全部内容
"""
title: Echo Model
requirements:
"""
from pydantic import BaseModel
class Pipe:
class Valves(BaseModel): # 可配置项(见 §6)
prefix: str = "you said: "
def __init__(self):
self.valves = self.Valves()
def pipe(self, body: dict): # 核心:收到聊天请求 body,返回回复
last = body["messages"][-1]["content"]
return self.valves.prefix + last # 直接返回字符串 = 一条完整回复
保存后,模型列表里就会出现 "Echo Model";选它发消息,回复永远是 you said: <你说的>。没有注册表、没有插件清单——一个 Pipe 类就是一个模型。
一句话直觉/类比。 把插件想成给智能体的热插拔配件:Pipe 是"换一个大脑"(模型后端),Filter 是"在耳朵和嘴巴上各装一个滤网"(输入输出拦截)。配件本身是一张纸(源码字符串),用的时候才被"读活"(exec 成模块)。
2. 顶层全景(它大概怎么转)
插件的生命有三段:存 → 加载 → 接线。
用户在后台粘贴 Python 源码
│ 存进 function 表的 content 列(纯文本)
▼
┌──────────────────────────────────────────────┐
│ models/functions.py │
│ Function 表: id / type / content / valves / │
│ is_active / is_global │
└──────────────────────────────────────────────┘
│ 运行时按需取出 content 字符串
▼
┌──────────────────────────────────────────────┐
│ utils/plugin.py —— 把"字符串"读成"活模块" │
│ replace_imports → exec 进临时模块 → │
│ 找到 Pipe / Filter / Action 类并实例化 │
│ (结果缓存在 request.app.state.FUNCTIONS) │
└──────────────────────────────────────────────┘
│
├─ 是 Pipe ─────────────────────────────────┐
│ ▼
│ ┌────────────────────────────────┐
│ │ functions.py │
│ │ get_function_models() 把每个 │
│ │ pipe 变成一个"模型"塞进模型列表 │
│ │ generate_function_chat_ │
│ │ completion() 执行 pipe() │
│ └────────────────────────────────┘
│ ▲
│ utils/chat.py: model.get('pipe') 分支路由到这里
│
└─ 是 Filter ───────────────────────────────┐
▼
┌────────────────────────────────┐
│ utils/filter.py │
│ get_sorted_filter_ids() 排序 │
│ process_filter_functions() 执行 │
│ 被 middleware.py 在 inlet / │
│ outlet / stream 三处接线调用 │
└────────────────────────────────┘
部件一句话职责:
| 部件 | 干什么 | 在哪个文件 |
|---|---|---|
Function 表 | 存插件源码、类型、valves、激活状态 | models/functions.py:19 |
| 加载器 | 把源码字符串 exec 成模块并缓存 | utils/plugin.py:263 load_function_module_by_id |
| pipe→模型 | 把每个 pipe 注册成一个可选"模型" | functions.py:71 get_function_models |
| pipe 执行 | 路由进来后调用 pipe(),处理流式/非流式 | functions.py:150 generate_function_chat_completion |
| pipe 路由 | 判断选中的模型是不是 pipe,是就转过去 | utils/chat.py:282 model.get('pipe') 分支 |
| filter 排序 | 收集+按优先级排出该跑哪些 filter | utils/filter.py:97 get_sorted_filter_ids |
| filter 执行 | 逐个调用 filter 的 inlet/outlet/stream | utils/filter.py:197 process_filter_functions |
主线走一遍(高层)。 用户选了模型 A 发消息 → 请求进入 generate_chat_completion(utils/chat.py:151)→ 如果 A 是 pipe,走 model.get('pipe') 分支交给 generate_function_chat_completion;如果 A 是普通模型,则在送模型前后由 middleware 调 process_filter_functions 跑一圈 filter。pipe 决定"