
OmniRoute Plugin SDK 开发指南事件钩子、权限沙箱与插件生命周期【免费下载链接】OmniRouteNever stop coding. Free MIT AI gateway: one endpoint, 352 providers (150 free), 1200 models Kimi, Claude, GPT, Gemini, GLM, DeepSeek, MiniMax. Works with Claude Code, Codex, Cursor, OpenCode, Cline Copilot. Quota-aware auto-fallback, RTKCaveman compression saves 15-95% tokens, MCP/A2A, Desktop/PWA. Built by 550 contributors项目地址: https://gitcode.com/GitHub_Trending/om/OmniRoute导读OmniRoute Plugin SDK 是构建在 src/lib/plugins 之上的官方插件开发接口让你能够以 TypeScript 编写自定义逻辑介入 AI 网关中每一次聊天请求的完整生命周期——从请求进入、模型路由、provider 调用、SSE 流式响应到错误处理。读完本文你将掌握definePlugin的核心 API、plugin.json清单的完整字段语义、钩子优先级与链式执行规则、权限模型以及一个插件从安装、激活到卸载的完整生命周期并能对照仓库中的真实示例examples/plugins直接动手开发。快速开始五步写出第一个插件SDK 的核心入口是一个工厂函数definePlugin。它在 src/lib/plugins/sdk.ts 中实现功能是把你声明的插件定义归一化为标准Plugin对象并填充默认值priority默认 100、enabled默认 true。import { definePlugin } from omniroute/plugins/sdk; export default definePlugin({ name: my-plugin, priority: 50, onRequest: async (ctx) { console.log(Request ${ctx.requestId} for ${ctx.model}); }, onResponse: async (ctx, response) { console.log(Response for ${ctx.requestId}); return response; }, onError: async (ctx, error) { console.error(Error: ${error.message}); }, });把这个文件连同下面的plugin.json放进插件目录后插件便可以在网关启动时被自动扫描、安装并激活。name必须使用 kebab-case小写字母与连字符这一约束在 manifest.ts 的 Zod schema 中通过正则/^[a-z0-9-]$/强制校验。核心 API 参考definePlugin(def: PluginDefinition): Plugin类型安全的插件定义工厂参数与默认值如下参数类型必填默认值说明namestring是—插件名kebab-case例如my-pluginprioritynumber否100钩子执行优先级数值越小越先执行enabledboolean否true是否在启动时启用onRequestfunction否—在 chat handler 之前运行可阻断或修改请求onResponsefunction否—在 chat handler 之后运行可修改响应onErrorfunction否—在 handler 出错时运行可恢复或重新抛出三个钩子的类型签名定义在 sdk.tsonRequest返回PluginResult | voidonResponse接收(ctx, response)并返回修改后的响应onError接收(ctx, error)且错误会被记录但不会阻断其他钩子。blockRequest(response?)阻断请求在onRequest中调用blockRequest可以直接终止本次请求并可选地返回自定义响应体。其实现sdk.ts只是返回{ blocked: true, response }随后由emitHookBlocking检测到blocked标记后短路返回。onRequest: (ctx) { if (!ctx.headers[authorization]) { return blockRequest({ error: Unauthorized, status: 401 }); } };这是实现鉴权、风控、额度拦截等场景的基础设施。注意钩子体系是 fail-open 的见下文生命周期小节因此用于安全阻断的插件必须保证自身进程存活。modifyBody(body)改写请求体在请求到达 provider 之前修改 body。实现返回{ body }sdk.tsemitHookBlocking会把它链式合并进后续钩子可见的上下文。onRequest: (ctx) { return modifyBody({ ...ctx.body, temperature: 0.7 }); };addMetadata(metadata)附加上下文元数据向请求上下文附加可被下游钩子读取的可变元数据。实现返回{ metadata }sdk.ts。元数据会在多个钩子之间累积合并见下文执行语义非常适合在onRequest记录起始时间戳、追踪 ID供onResponse计算耗时——examples/plugins/request-logger/index.mjs 正是用ctx.metadata.__requestStart实现计时。onRequest: (ctx) { return addMetadata({ source: my-plugin, version: 1.0.0 }); };插件上下文PluginContext每个钩子收到的第一个参数ctx携带本次请求的上下文信息。文档定义的字段如下字段类型说明requestIdstring唯一请求标识符modelstring请求的模型名providerstring目标 provider 的 IDbodyRecordstring, unknown请求体headersRecordstring, string请求头metadataRecordstring, unknown可变元数据timestampnumber请求时间戳从当前源码的接口定义hooks.ts看PluginContext还额外暴露了apiKeyInfo字段并且headers的类型被放宽为Recordstring, string | string[] | undefined——这是因为并非所有调用方都能拿到原始请求头例如内部触发、重试路径该字段专门用于让可观测性 / 追踪导出类插件读取客户端透传的 trace id、correlation id 等请求级上下文。插件清单plugin.json每个磁盘插件都必须带一个plugin.json清单它由 manifest.ts 的 Zod schema 解析并校验。一个完整的示例{ name: my-plugin, version: 1.0.0, description: A sample plugin, author: your-name, main: index.js, hooks: { onRequest: { enabled: true, priority: 50 }, onResponse: true, onError: false }, requires: { permissions: [network, file-read] }, enabledByDefault: false, configSchema: { apiKey: { type: string, description: API key for external service }, maxRetries: { type: number, min: 1, max: 10, default: 3 }, debug: { type: boolean, default: false }, mode: { type: string, enum: [fast, slow], default: fast } } }要点说明name必须 kebab-caseversion必须严格MAJOR.MINOR.PATCH语义化版本正则/^\d\.\d\.\d$/。main默认index.js是插件入口文件source默认local取值local或marketplace。hooks中的每个字段声明插件是否实现对应事件未声明的钩子不会被注册见 loader 的注册逻辑 loader.ts。requires.permissions声明插件运行所需的权限requires.omniroute可声明对 OmniRoute 版本的约束。可选字段还有license默认MIT、tags、skills插件可随附的技能定义、enabledByDefault默认false以及可选的integrity字段——这是一个 SRI 格式的sha256-base64入口文件哈希若声明loader 在激活时会校验入口文件哈希不匹配则拒绝激活loader.ts用于对从 marketplace 获取的插件做篡改检测。钩子优先级Hook Priority钩子既可以配置优先级也可以直接用布尔值声明。优先级越低执行越早注册时会按优先级对同一事件的处理器排序hooks.ts。{ hooks: { onRequest: { enabled: true, priority: 10 }, onResponse: { enabled: true, priority: 100 } } }或者写成简单布尔值等价于使用默认优先级 100{ hooks: { onRequest: true, onResponse: true } }仓库中的真实示例 examples/plugins/request-logger/plugin.json 就采用了纯布尔形式声明了onRequest、onResponse、onError三个钩子。权限系统最小化沙箱暴露插件运行在隔离的执行环境中访问外部资源必须显式声明权限未声明对应权限时相关全局对象在沙箱中根本不可用权限授予的能力networkfetch、AbortController、Headers、Request、Responsefile-readfs.readFile、fs.readdir、fs.statfile-writefs.writeFile、fs.mkdir、fs.rmenv只读的process.env代理execchild_process.exec、child_process.execSync从源码实现看当前版本采用的是子进程隔离方案每个插件由 loader.ts 通过child_process.spawn生成独立的 Node.js 进程通过 IPC 通道调用钩子而非eval/new Function。环境变量使用白名单过滤loader.ts默认仅透传PATH、HOME、USER、LANG、LC_ALL、NODE_ENV等安全变量Windows 上额外追加SystemRoot、windir只有声明了env权限才会追加PORT、HOSTNAME、TZ、TMPDIR。这意味着不声明env权限的插件无法读取宿主进程的环境变量从机制上隔离了密钥泄露面。另外子进程的stdout/stderr会被转发到父进程的结构化日志loader.ts因此插件内使用console.log是官方推荐的日志方式为防止失控插件刷屏每个流最多转发 500 行、单行最长 4000 字符。内置事件与执行语义文档列出的内置事件覆盖了请求生命周期、路由决策与限流等场景事件触发时机PayloadonRequestchat handler 之前请求上下文onResponsechat handler 之后响应数据onErrorhandler 出错时Error 对象onModelSelect模型被路由选中时模型信息onComboResolve组合combo路由解析完成时组合目标onRateLimit命中限流时限流信息onQuotaExhaust配额耗尽时配额信息onProviderErrorprovider 返回错误时错误详情onStreamStartSSE 流开始时流信息onStreamEndSSE 流结束时流统计onInstall插件被安装时{ name, version, manifest }onActivate插件被激活时{ name, version, manifest }onDeactivate插件被停用时{ name, version, manifest }onUninstall插件被卸载时删除文件之前{ name, version, manifest }从当前源码看hooks.ts 中BUILTIN_EVENTS实际注册并发射的核心事件集合为 8 个onRequest、onResponse、onError、onInstall、onActivate、onDeactivate、onUninstall以及新增的onStreamComplete。onStreamComplete是流式响应被完整消费后触发的通知型事件携带requestId、token 用量prompt_tokens、completion_tokens、reasoning_tokens、cache_read_input_tokens、cache_creation_input_tokens以及耗时统计latencyMs、ttft是成本统计与用量上报插件的关键入口类型定义见 hooks.ts。执行语义上有几个关键点均可在 hooks.ts 中找到实现依据链式上下文合并emitHookBlocking在处理onRequest时会把每个处理器返回的body、metadata合并进后续处理器的可见上下文currentPayload { ...ctx, body: mergedBody, metadata: mergedMetadata }保证插件 B 能看到插件 A 的修改。短路阻断任一处理器返回blocked: true即立即返回阻断结果。单插件限流每个插件每秒最多被调用 100 次RATE_LIMIT_MAX 100/ 窗口 1000ms超出后该插件的后续调用被跳过并记录告警防止失控插件拖垮网关。错误隔离单个处理器抛错只会被记录日志hook.handler_error不会影响同事件的其他处理器。超时与隔离阻塞类钩子onRequest/onResponse/onError每次 IPC 调用默认超时 10 秒DEFAULT_HOOK_TIMEOUT超时后发送SIGTERM并在 3 秒宽限期后升级为SIGKILL而通知型钩子onStreamComplete超时只丢弃本次投递、保留进程存活loader.ts。onResponse采用链式管道执行runOnResponsehooks.ts让每个插件基于上一个插件返回的响应继续加工最终结果返回给调用方。插件生命周期安装、激活、停用、卸载插件管理器manager.ts以单例形式协调扫描器、加载器、数据库与钩子注册表整个生命周期如下安装install从源目录复制到临时 staging 目录校验manifest.main必须解析在插件目录内部assertEntryPointWithinDest然后原子rename到位并写入数据库版本已存在且源版本更新时自动走upgrade流程。安装完成后触发onInstall若enabledByDefault为 true 则自动激活。激活activate重新读取磁盘上的plugin.json刷新清单避免旧安装缺失新版 schema 字段做符号链接解析与路径逃逸校验assertEntryPointWithinPluginDir防止main逃逸插件目录随后加载子进程、按清单注册钩子、更新 DB 状态为active最后触发onActivate。停用deactivate先触发onDeactivate此时钩子仍注册着插件可以执行清理逻辑再注销钩子、终止子进程、更新状态为inactive。卸载uninstall先停用再触发onUninstall发生在删除文件之前便于插件做外部资源清理最后在路径包含性断言保护下递归删除目录并从数据库移除。值得注意的健壮性设计钩子注册表是模块级内存状态重启后不保留而 DB 中状态仍为active。因此 hooks.ts 在请求路径上实现了ensurePluginsLoaded启动引导每个进程首次收到请求时调用pluginManager.loadAll()重新加载所有 active 插件且加载失败只记录日志并在下次请求重试不会阻塞后续调用。此外安装、升级、卸载过程中的目录删除路径都经过assertWithinPluginDir包含性断言manager.ts防止被篡改的数据库pluginDir指向插件目录之外导致任意路径被递归删除——这是文档中权限体系之外的又一层路径级安全边界。配置 Schema 与运行时校验在configSchema中声明插件的可配置项OmniRoute 会在 dashboard 的插件配置页渲染表单并把用户保存的配置持久化到数据库中。字段支持的类型为string、number、boolean、select字段选项包括default、min、max、enum、description。{ configSchema: { apiKey: { type: string, description: External API key }, maxRetries: { type: number, min: 1, max: 10, default: 3 }, debug: { type: boolean, default: false }, mode: { type: string, enum: [fast, slow], default: fast } } }运行时校验逻辑在 manifest.ts 的validatePluginConfig中实现仅校验配置中实际出现的键未提供的键使用默认值number类型检查min/max边界select类型要求值必须在enum列表内未知的配置键会被报告为Unknown config key。插件代码内可通过ctx.config读取配置见 examples/plugins/request-logger/index.mjs 中ctx?.config.logLevel的用法。实战示例Request Logger请求日志日志插件是最经典的入门案例。SDK 文档给出了基础版import { definePlugin } from omniroute/plugins/sdk; export default definePlugin({ name: request-logger, onRequest: async (ctx) { console.log([${new Date().toISOString()}] ${ctx.method} ${ctx.model} - ${ctx.provider}); }, });仓库 examples/plugins/request-logger 提供了完整可运行的生产级版本它额外实现了通过ctx.metadata.__requestStart记录起始时间并在onResponse中计算耗时durationMs借助configSchema暴露logLevelselect类型枚举debug/info/warn/error、includeBodyboolean、maxBodyLengthnumber100~10000三个可配置项对响应体做截断与 JSON 序列化保护避免日志刷屏与循环引用崩溃。Rate Limiter基于 API Key 的限流在onRequest中用blockRequest实现按 API Key 的滑动窗口限流优先级设为 10 以保证先于其他插件执行import { definePlugin, blockRequest } from omniroute/plugins/sdk; const requests new Mapstring, number[](); export default definePlugin({ name: rate-limiter, priority: 10, onRequest: async (ctx) { const key ctx.headers[x-api-key] || anonymous; const now Date.now(); const window 60000; // 1 minute const maxRequests 100; const timestamps (requests.get(key) || []).filter((t) t now - window); timestamps.push(now); requests.set(key, timestamps); if (timestamps.length maxRequests) { return blockRequest({ error: Rate limit exceeded, status: 429 }); } }, });注意该示例的状态保存在插件子进程内存中适合单实例部署多实例或需要持久化的场景应改用外部存储如 Redis。Response Transformer响应改写onResponse钩子返回修改后的响应对象即可对 provider 的响应做统一清洗——下面的例子会去掉每个 choice 中消息内容的首尾空白import { definePlugin } from omniroute/plugins/sdk; export default definePlugin({ name: response-transformer, onResponse: async (ctx, response) { if (response.choices) { response.choices response.choices.map((c: any) ({ ...c, message: { ...c.message, content: c.message.content.trim() }, })); } return response; }, });延伸阅读插件系统总览与安装方式PLUGINS.md、OPENCODE-V2-PLUGIN.mdSDK 类型定义与实现sdk.ts、hooks.ts子进程隔离加载与超时机制loader.ts清单 schema 与配置校验manifest.ts生命周期管理manager.ts可直接运行的插件示例examples/plugins【免费下载链接】OmniRouteNever stop coding. Free MIT AI gateway: one endpoint, 352 providers (150 free), 1200 models Kimi, Claude, GPT, Gemini, GLM, DeepSeek, MiniMax. Works with Claude Code, Codex, Cursor, OpenCode, Cline Copilot. Quota-aware auto-fallback, RTKCaveman compression saves 15-95% tokens, MCP/A2A, Desktop/PWA. Built by 550 contributors项目地址: https://gitcode.com/GitHub_Trending/om/OmniRoute创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考