插件拦截钩子
插件可以在 manifest 里声明 hooks,在四个环节介入转发链路。
一、四个钩子点
| 钩子 | 运行时机 | 可以改什么 |
|---|---|---|
route | 每请求一次,在选型之前 | 被路由的模型名,或拒绝该请求 |
request | 每次渠道尝试一次,在上游请求体与端点定型之后 | 上游请求体与头部 |
response | 成功的非流式应答,在客户端看到之前 | 应答体、状态码与头部 |
event | 流式应答的每一帧,在客户端看到之前 | 该帧的 data: 载荷,或丢弃这一帧 |
流式:response 与 event 的分工
一份 SSE 应答没有完整文档可以重写,所以整包改写只发生在非流式应答上,流式应答逐帧交给 event。
- 只有单 data 行的 JSON 对象会送进插件。
[DONE]、纯文本、数组、多行 data 帧原样通过。 - 帧里的
event:/id:/retry:/ 注释行在改写时原样保留。 - 决定字段:
body替换载荷,drop: true丢弃整帧(什么都不写)。 - 只在
Content-Type: text/event-stream的应答上生效。客户端要流式但上游回了整包 JSON 的,仍走response点。
一次往返 = 一次插件调用。 所以
event点的默认超时是 100ms(其它点 800ms);一帧超过 1 MiB 就不再解析、后续按原样流式转发;一条应答最多送 4000 帧,超出照旧透传。审计上一条流只记一次决策(第一帧被改写或丢弃时),不会把每帧都写进日志行。
二、四条性质(每条都是承重的)
1. 未匹配的模型永远到不了插件
match_models 是内存内的 glob 匹配,用路由模式的语义(* / ?),所以普通流量零成本。
2. 全程 fail-open
超时、传输错误、非 200、畸形 JSON、panic —— 全部意味着「没有意见」,请求原样继续。
熔断器连续 5 次失败后打开 30 秒,然后放一个探测过去。
3. 不自拦截
网关在钩子调用上打两个头:
| 头 | 作用 |
|---|---|
X-Meta-Hook-Origin | 标记这次调用由哪个插件触发 |
X-Meta-Hook-Depth | 嵌套深度 |
插件把这两个头透传到嵌套的网关调用上时,它自己的钩子就不会作用于那个请求。深度 ≥ 3 时跳过所有钩子。
4. 显式权限
声明了 hooks 的 manifest 必须同时声明 relay:intercept,否则注册被拒。
理由:拦截会把匹配到的提示词与应答暴露给插件进程。
三、route 决策的连带效果
一次 route 改写会同时改路由键与请求体里的模型名,并记录在三处:
- 决策快照的
hook_decisions; X-Meta-Hook-Decision响应头;- 对虚拟模型名而言,
/v1/models目录。
只改前者会让上游收到
auto这类虚拟名。 两个字段必须一起改。
四、插件模型发现(models_path)
钩子可以声明 models_path:一个插件相对的 GET 端点,应答:
{"models": ["virtual-model-a", "virtual-model-b"]}OpenAI 形状的 {"data":[{"id":...}]} 也接受。
网关什么时候问它
注册、启用、保存配置、托管插件进程启动、网关启动,以及一个 30 秒的慢定时器上。用的是与钩子调用同样的凭据,包括 X-Plugin-Config。
报出的名字做什么
替换该 route 钩子声明的 match_models,并且就是 /v1/models 对外宣传的内容。
所以插件可以按自己的配置改名虚拟模型,而网关只需要发现答案。替换只发生在一处(热路径、目录与控制台共同读取的那个位置)。
失败怎么降级
| 情况 | 结果 |
|---|---|
| 端点不可达 | 保留上次已知列表 |
| 答案不可用(非 JSON / 为空 / 含通配符名 / 条目数超上限) | 保留上次已知列表 |
| 首次发现就失败 | 用声明的 match_models |
声明了 models_path 的钩子可以完全省略 match_models——报出的列表就是匹配列表。
省略从不意味着「匹配空」。 发现可以让改名变慢,永远不会扩大或清空钩子拦截的范围。
五、钩子在哪里实现
| 位置 | 职责 |
|---|---|
internal/proxy/hooks.go | 契约(Interceptor 接缝) |
internal/plugins/hooks.go | 宿主侧(声明解析、模型匹配、HTTP 调用、熔断) |
proxy 从不导入插件机制——与实时追踪观察者是同一种注入形状。这样转发核心不知道插件的存在,也就能在没有插件时保持零开销。
六、排查
| 现象 | 看什么 |
|---|---|
| 模型没被改选 | X-Meta-Hook-Decision 响应头、决策快照的 hook_decisions |
| 插件「没生效」 | 该插件的熔断是否打开(连续 5 次失败后停 30 秒) |
| 流式没被改写 | 应答是否真是 text/event-stream;帧是否为单 data 行的 JSON 对象;是否已超过 4000 帧 |
| 流变慢了 | event 点是每帧一次 HTTP 调用:插件必须便宜,必要时显式声明更小的 timeout_ms |
虚拟模型不在 /v1/models 里 | GET /admin/plugins/hooks 看声明;再看插件 models_path 是否可达 |
| 插件自己调用网关时行为异常 | 检查它是否透传了 X-Meta-Hook-Origin / X-Meta-Hook-Depth |