首页
/ OmniRoute OpenCode 插件实战指南:动态模型目录、实时 Combo 发现与多实例接入

OmniRoute OpenCode 插件实战指南:动态模型目录、实时 Combo 发现与多实例接入

2026-09-07 11:33:52作者:段琳惟

@omniroute/opencode-plugin 是 OmniRoute 官方推荐的 OpenCode 集成方式,它把 OmniRoute 网关的「动态模型目录 + Combo 聚合路由 + 多上游统一鉴权」能力以 OpenCode 原生 Plugin 形态接入:启动时在 Node.js 侧实时拉取 /v1/models/api/combos,以能力交集(least-common-denominator)聚合 Combo,并支持开箱即用的多 OmniRoute 实例并存。读完本文,你将掌握从零配置、自动/手动刷新、模型筛选、MCP 自动发射到生产/预生产双实例隔离的完整实操路径,并理解其背后 provider/config/auth 三个 Plugin hook 的源码级实现原理。

本插件位于仓库 @omniroute/opencode-plugin(当前 package.json 版本为 0.2.1,MIT 协议),其设计与竞品对照、配置项定义与 Feature 开关全部围绕一个核心目标:让 OmniRoute 成为模型事实的唯一来源(source of truth),OpenCode 端不做任何客户端侧的能力拼装

为什么选 Plugin,而不是 @omniroute/opencode-provider

@omniroute/opencode-provider 是旧的「配置生成器」包:它会在构建/CLI 阶段把一段冻结的 provider.omniroute 配置块写入 opencode.json,模型清单是写死在源码里的 8 个模型(见 @omniroute/opencode-provider/src/index.ts 中的 OMNIROUTE_DEFAULT_OPENCODE_MODELS)。它在纯 CLI 环境下可以工作,但在 OpenCode Desktop / Web(Tauri / Electron) 构建中,运行时模型选择器会重新执行,静态配置块只能暴露出其中一小部分模型,并且会随 OmniRoute 在线目录的更新而逐渐失真。

本插件(Plugin)则完全改走「运行时集成」路线:

  • OpenCode 启动时、于 Node.js 进程内拉取 /v1/models/api/combos,天然绕开浏览器 WebView 的 CORS 限制;
  • 在插件自身的 provider/config hook 中动态发射 provider 配置块,因此 opencode.json 只需要一行插件入口,不再需要手写静态的 provider.omniroute
  • 支持按可配置 TTL(默认 5 分钟)按需重拉,同时提供后台自动再发现(autoSyncIntervalMs,默认 5 分钟),新模型与 Combo 变更无需重启 OpenCode 即可生效;
  • 暴露强刷路径(omniroute_sync_models tool + /omni-sync 命令模板),对齐 Pi 的 /omni sync
  • 对 Combo 的 limit.context 统一计算为 min(member.context_length)(取各成员上下文长度的最小值),杜绝 null 导致的 4K 截断问题;
  • 自动识别思考模型的 interleaved 能力(经 PR #3138 合并)。

源码层面,这两条路线的差异在 插件主文件 顶部注释中有明确论述:@omniroute/opencode-providerfetchLiveModels 返回的是被裁剪过的 {id, name, contextLength?} 结构,丢掉了插件做 ModelV2 透传所需的 capabilities / *_modalities / max_*_tokens,因此插件直接实现了一个约 30 行的轻量 fetcher 来保留完整字段。

如果当前 opencode.json 里还残留旧的 opencode-provider 静态块,把它替换成一行插件入口即可,其余配置(包括同一份 auth.json API Key)无需任何改动。

下面是两者的整体对比:

@omniroute/opencode-plugin(本插件) @omniroute/opencode-provider
类型 OC Plugin(运行时) 配置生成器(CLI / 构建期)
模型 /v1/models 实时获取 生成时冻结
Combo LCD 聚合、实时
Gemini schema 清洗 N/A
OC UI 集成 /connect/models
多实例 原生 手动

两种方案可以共存,按你的运行环境选择即可。

安装:一条命令(omniroute setup opencode

OmniRoute v3.8.23 起,插件随 omniroute npm 包预构建发布。只要安装了 OmniRoute,插件就已经在你的磁盘上,无需单独安装:

# 1. 一条命令——把插件复制进 OpenCode 并更新 opencode.json
omniroute setup opencode --auth

# 2. 跟随交互提示输入 OmniRoute API Key
# 3. 重启 OpenCode——/models 即列出完整实时目录

--auth 标志会自动执行 opencode auth login --provider opencode-omniroute。如果指向非默认的 OmniRoute 地址,用 --base-url 指定:

omniroute setup opencode --base-url https://or.example.com --auth

该命令实际做四件事:

  1. 定位 omniroute 安装目录内捆绑的插件;
  2. dist/package.json 复制到 ~/.config/opencode/plugins/omniroute/
  3. 把插件入口写入 / 更新 opencode.json(幂等操作,且会替换掉旧版条目);
  4. (配合 --auth)执行 opencode auth login 以持久化 API Key。

该命令可随时重跑,用于升级插件或更换 base URL;旧的 @omniroute/opencode-provider 条目与历史 opencode-omniroute-auth 包都会被自动清理。

手动安装(无 omniroute CLI 场景)

无法运行 omniroute setup opencode 时(本地开发、CI、离线/内网环境),可以直接构建并打包产物:

cd @omniroute/opencode-plugin && npm run build && npm pack
# 然后将产物解压到 ~/.config/opencode/plugins/omniroute-opencode-plugin/

再手工把插件入口写入 opencode.json(参考下文 Quick Start)。构建脚本由 tsup.config.ts 驱动,构建产物 main 指向 ./dist/index.jsfiles 字段只发布 distREADME.mdLICENSE

Peer 依赖为 @opencode-ai/plugin(由你的 OpenCode 安装负责管理)。插件的 engines.node 要求 >=22.22.3,官方在 Node 22 与 24 上做过测试。

单实例快速上手(手动配置)

最小可用配置只需一个指向插件 dist 的入口和必要的运行参数:

// opencode.json
{
  "$schema": "https://opencode.ai/config.json",
  "plugin": [
    [
      "./plugins/omniroute-opencode-plugin/dist/index.js",
      {
        "providerId": "omniroute",
        "baseURL": "https://or.example.com",
        // OpenCode 运行期间后台再发现(对齐 Pi)。
        // 默认 300000(5 分钟)。最小 60000。设为 0 可关闭。
        "autoSyncIntervalMs": 300000,
      },
    ],
  ],
}

随后写入 API Key:

opencode auth login --provider opencode-omniroute
# 提示输入 OmniRoute API Key,写入 ~/.local/share/opencode/auth.json

⚠ 请务必显式使用 --provider 标志。当前 OpenCode 版本(≤1.15.5)会把 opencode auth login omniroute 解析为位置参数 url,并以 fetch() URL is invalid 失败(上游已知问题)。

重启 OpenCode 后,/models 即列出完整实时目录。-low-medium-high-thinking 变体和 Combo 会以一等公民的模型 ID 出现——因为 OmniRoute 就是事实源,客户端不做任何二次合成。

从源码结构看,这一行为的核心是 createOmniRouteAuthHook 中的 loader:它通过 getAuth() 读取已存凭证,把 apiKey/baseURL 投影成 AI-SDK 的 openai-compatible 配置形态;只有 type === "api" 且 key 非空的凭证才会返回有效配置,否则返回 {} 让 OpenCode 弹出 /connect 流程——避免带着错误凭证发请求。

实时目录刷新:TTL 自动 + 后台定时 + 手动强刷

OpenCode 运行期间,插件用两条独立机制保持目录新鲜:

机制 默认值 作用
modelCacheTtl 300000(5 分钟) 按需 TTL:过期后下一次 provider/models hook 触发时重新拉取 /v1/models
autoSyncIntervalMs 300000(5 分钟) 后台定时器:harness 运行期间主动失效并重拉。最小 60000。设为 0 关闭后台轮询(TTL 仍然生效)

两个默认值在源码中以常量定义(DEFAULT_MODEL_CACHE_TTL_MS = 300_000DEFAULT_AUTO_SYNC_INTERVAL_MS = 300_000MIN_AUTO_SYNC_INTERVAL_MS = 60_000)。后台间隔会先经过 sanitizeAutoSyncIntervalMs 归一化:未设置/非法值回落到 300_000;显式 0 表示关闭;(0, 60000) 区间的值会被钳制到 60000。定时器的首个 tick 会推迟一个间隔执行,避免与会话开始时的 provider.models 冷启动拉取重叠;setInterval 的句柄还被 unref(),因此不会拖住 Node 进程退出。

立即强刷(等价于 Pi 的 /omni sync)——OpenCode 没有 Pi 风格的斜杠命令注册 API,因此插件同时接入了 tool 与命令模板两条路:

  1. Tool:omniroute_sync_models —— 失效内存 + 磁盘缓存,重新拉取 GET /v1/models(以及按开关拉取 combos / enrichment 等),返回 { ok, count, ... }
  2. 命令模板(在 OpenCode 中直接输入):
    • /omni-sync —— 让 agent 调用 omniroute_sync_models 并汇报结果;
    • /omni-autosync —— 让 agent 汇报当前 autoSyncIntervalMs / modelCacheTtl 状态。
/omni-sync
/omni-autosync

OmniRoutePluginconfig hook 实现里,两个命令模板被写进 cfg.command,模板文本中会内插实际的 provider id(如 omniroute)与解析后的间隔值;omniroute_sync_models 强刷逻辑由 forceSyncOmniRouteModels 提供——它先清空针对该 baseURL 的内存缓存与磁盘快照,再依次拉取 models、combos、auto-combos、enrichment、compression 元数据与连接表,最后把带 expiresAt 的结果重新写入缓存(并在 diskCache 开启时落盘)。sync 返回体里包含模型数、Combo 数、清除的内存条目数与是否清理了磁盘快照,便于用户与 agent 核对。

多实例:生产 + 预生产并存

⚠ OpenCode ≤1.15.5 按模块绝对路径对插件加载去重。两条 plugin: 条目若指向同一个 dist/index.js,会合并为一条(后者选项生效)。解决方法是把插件分别安装到不同目录,使每条入口解析到不同的模块文件。README 提到后续 v0.2.x 将引入 instances: [...] 形态,实现单次加载注册 N 个 provider(当前 package.json 版本为 0.2.1)。

双实例工作区方案(OpenCode ≤1.15.5 当下可用)

先打包插件,再解压成两份命名目录,每条 plugin: 入口指向各自副本:

# 1. 构建 + 打包(在插件工作区执行)
cd /path/to/OmniRoute/@omniroute/opencode-plugin
npm run build
npm pack
# 产出 omniroute-opencode-plugin-0.1.0.tgz(以实际版本号为准)

# 2. 每个 OmniRoute 端点解压一份
mkdir -p ~/.config/opencode/plugins/omniroute-opencode-plugin-prod
mkdir -p ~/.config/opencode/plugins/omniroute-opencode-plugin-preprod
tar -xzf omniroute-opencode-plugin-0.1.0.tgz -C ~/.config/opencode/plugins/omniroute-opencode-plugin-prod    --strip-components=1
tar -xzf omniroute-opencode-plugin-0.1.0.tgz -C ~/.config/opencode/plugins/omniroute-opencode-plugin-preprod --strip-components=1

然后在 ~/.config/opencode/opencode.json 中按绝对路径引用两个目录:

{
  "$schema": "https://opencode.ai/config.json",
  "plugin": [
    [
      "./plugins/omniroute-opencode-plugin-prod/dist/index.js",
      {
        "providerId": "omniroute",
        "displayName": "OmniRoute",
        "baseURL": "https://or.example.com",
      },
    ],
    [
      "./plugins/omniroute-opencode-plugin-preprod/dist/index.js",
      {
        "providerId": "omniroute-preprod",
        "displayName": "OmniRoute Preprod",
        "baseURL": "https://or-preprod.example.com",
      },
    ],
  ],
}

路径相对于 ~/.config/opencode/。此时每条入口都解析到不同的模块文件,OpenCode 会把它们当作两个独立插件实例。分别完成鉴权:

opencode auth login --provider opencode-omniroute
opencode auth login --provider opencode-omniroute-preprod

每条入口获得独立的 provider id、独立的模型选择器条目、auth.json 中独立槽位与独立 TTL 缓存。闭包按插件实例隔离——实例之间零串扰(源码层面,每次 OmniRoutePlugin(...) 调用都通过闭包创建自己的 sharedCache: new Map(),生产/预生产实例不会共用缓存键)。

发布后(@omniroute/opencode-plugin npm)

包发布后,双实例安装可从 tar -xzf 退化为两条 npm install --prefix

mkdir -p ~/.config/opencode/plugins/omniroute-opencode-plugin-prod
mkdir -p ~/.config/opencode/plugins/omniroute-opencode-plugin-preprod
npm install --prefix ~/.config/opencode/plugins/omniroute-opencode-plugin-prod    @omniroute/opencode-plugin
npm install --prefix ~/.config/opencode/plugins/omniroute-opencode-plugin-preprod @omniroute/opencode-plugin

此时 opencode.json 路径变为 ./plugins/omniroute-opencode-plugin-prod/node_modules/@omniroute/opencode-plugin/dist/index.js(preprod 同理)。

功能全景与各功能对应的 hook

插件把全部能力挂接在三个 OpenCode hook 之上,README 给出的功能矩阵如下:

功能 作用 Hook
动态 /v1/models 每次刷新拉取实时目录(生产上 455+ 条目),TTL 缓存 provider.models
变体透传 -low/-medium/-high/-thinking 以一等公民 ID 由 OmniRoute 提供(无客户端合成) provider.models
Combo LCD 聚合 Combo 以成员间的能力交集 + 上下文/输出最小值呈现 provider.models + config
combo/<slug> 命名空间 + Combo: 前缀 Combo 以 combo/claude-primary 形式(而非上游 UUID)出现,选择器显示 Combo: claude-primary 两个 hook
友好名称 + 成本 /api/pricing/models 显示名 + /api/pricing 每百万 token 价格叠加到实时目录 两个 hook
Canonical 孪生去重 + alias 回退 /v1/models 会同时暴露短别名(cc/claude-opus-4-7)与规范名(claude/claude-opus-4-7);存在别名孪生时丢弃规范孪生(选择器不出现重复行),并将规范名反向映射为 alias 以补齐 /api/pricing/models 只按规范名索引的 enrichment 两个 hook
压缩管线标签 features.compressionMetadata: true 时,Combo 名带压缩管线标签(如 Combo: claude-primary [rtk🟡 → caveman🟠]),强度用红绿灯 emoji 表达 两个 hook
Provider 标签前缀 富化名前置短上游供应商标签(如 Claude - Claude Opus 4.7 对比 Kiro - Claude Opus 4.7),默认开启,可用 features.providerTag: false 关闭 两个 hook
仅可用过滤 过滤到 /api/providers 中至少有一条健康连接的上游(features.usableOnly 开启) 两个 hook
模型白/黑名单 features.visibleModels(白)与 features.hiddenModels(黑)把选择器收敛到固定 ID 集合;裸后缀(如 claude-opus-4-7)匹配任意 {prefix}/claude-opus-4-7。与 usableOnly 全部 AND 叠加,黑名单优先(deny 优先) 两个 hook
磁盘缓存兜底 最近一次成功目录持久化到磁盘;冷启动时若 /v1/models 不可达则据此水合(默认开启,features.diskCache: false 关闭) config
Bearer 注入 + 后缀仿冒防护 仅在 baseURL 匹配的请求上加 Authorization auth.loader.fetch
Gemini schema 清洗 gemini-*/google-vertex-gemini/* 剥离 $schema/$ref/additionalProperties auth.loader.fetch 包装
多实例 每个插件条目绑定自己的 providerId;闭包隔离 factory
Config-hook shim OC ≤1.15.5 回退:把静态目录写入 config.provider[id](这些版本在 serve 模式下只有 config hook 会触发) config

结合仓库源码,其中几个关键行为的内部机制值得展开:

  • 模型映射与能力透传mapRawModelToModelV2/v1/models 原始条目映射为 AI-SDK 的 ModelV2capabilities.thinking 会并入 reasoning,同时单独置位 interleaved(这正是思考模型自动拾取的核心);capabilities.vision 按 OmniRoute 惯例映射为 attachment;输入/输出模态由 input_modalities/output_modalities 数组逐项点亮布尔位。/v1/models 不承载价格,因此插件先输出零值 cost 块,真实计费仍由 OmniRoute 在路由时负责。
  • schema 严格校验:插件选项经 parseOmniRoutePluginOptions 用 Zod 严格解析,未知键直接抛错,opencode.json 里的拼写错误会在插件构造期立即暴露,而不是被静默丢弃;空 providerId、非 URL 的 baseURL、负数 modelCacheTtl 都会得到可操作的报错信息。
  • providerId 前缀归一resolveOmniRoutePluginOptions 会为 OC ≥1.17.8 的原生适配器门禁自动添加 opencode- 前缀(门禁只接受 {openai, anthropic, opencode*}),而所有要交给 OmniRoute 服务端解析的位置(模型 ID 前缀、providerID、combo 目录键)一律使用无前缀omnirouteProviderId,避免 parseModel() 因无法解析 opencode-<x> 而报 "No credentials for opencode-"。

插件选项(Plugin options)

选项 类型 默认值 说明
providerId string "omniroute" OpenCode provider id;在多个插件条目间必须唯一
displayName string "OmniRoute"OmniRoute (<id>) OC UI 中显示的标签
modelCacheTtl number 300000(5 分钟) /v1/models TTL(毫秒)
baseURL string /connectauth.json 解析 覆盖 OmniRoute base URL
managementReadToken string 回落到 apiKey 管理目录 GET 用的只读 token;/v1 推理仍走已连接的 apiKey
features object 见下 功能开关(均可选/关,默认保持 v0.1.0 行为)

最小权限部署:managementReadToken

对于最小权限部署,可在顶层把 managementReadToken 设为一个只读管理 token。它只会被发往目录类读取接口:/api/combos/api/combos/auto/api/pricing/models/api/pricing/api/context/combos/api/providers/v1 下的推理请求(含 chat)仍使用 OpenCode 存储的 apiKeyfeatures.mcpToken 保持独立。若省略 managementReadToken,目录读取沿用原有 apiKey 行为。上述这些读取接口在仓库中均有对应实现,例如 src/app/api/combos/route.ts(含 combos/auto/ 子目录)、src/app/api/pricing/models/route.tssrc/app/api/pricing/route.ts

features

每个字段都可省略,默认值镜像 v0.1.0 行为,因此既有的 opencode.json 无需改动。

功能 类型 默认值 作用
combos boolean true 发现 /api/combos 并把 Combo 以 LCD 能力呈现为伪模型。Combo 在 combo/<slug> 命名空间下按键存储,选择器里以 Combo: <name> 标签呈现,与裸的 provider/模型组合区分开
enrichment boolean true /api/pricing/models 拉显示名、从 /api/pricing 拉每百万 token 价格(inputoutputcachedcacheReadcache_creationcacheWrite),叠加到实时目录上
compressionMetadata boolean false 拉取 /api/context/combos,给 Combo 名加压缩管线标签,如 Combo: claude-primary [rtk🟡 → caveman🟠]。强度 token 用红绿灯 emoji 渲染:🟢 lite/minimal · 🟡 standard · 🟠 aggressive/full · 🔴 ultra
providerTag boolean true 给富化显示名前置短上游标签并用 " - " 连接,如 cc/claude-opus-4-7 → Claude - Claude Opus 4.7,区别于 kr/claude-opus-4-7 → Kiro - Claude Opus 4.7。标签解析:/api/pricing/models[<alias>].name ≤8 字符时原文使用(如 ClaudeKiroCodexQwen),否则回退为 UPPER(alias)(如 GitHub ModelsGHMGeminiGEMINI)。幂等。Combo 有意跳过(Combo: 前缀已传达多上游语义)
usableOnly boolean false 读取 /api/providers,把目录过滤到至少有一条 isActive: truetestStatus: 'active' 连接的供应商。采用减集过滤语义:对 pricing-models 目录和连接表都未知的供应商(如 agentrouter/* 这类合成前缀)放行。拉取失败时该次刷新禁用过滤——绝不因此隐藏整个目录
visibleModels string[] 未设置 白名单——设置且非空时,只发射原始 /v1/models ID 匹配的模型。裸 ID(无斜杠,如 claude-opus-4-7)匹配任意 {prefix}/claude-opus-4-7;完整 ID(如 cc/claude-opus-4-7)精确匹配。与 usableOnlyhiddenModels 组合(所有过滤 AND)。未设置或空 = 不过滤
hiddenModels string[] 未设置 黑名单——原始 ID 匹配的模型被丢弃。匹配规则与 visibleModels 相同。模型同时出现在两份名单中时黑名单胜出(deny 优先)。组合规则同上
diskCache boolean true 把最近一次成功的 /v1/models + /api/combos + enrichment + 连接 + 压缩快照持久化到 ${OPENCODE_DATA_DIR ?? ~/.local/share/opencode}/plugins/omniroute-<providerId>.json。后续冷启动时若 /v1/models 抛错(断网 / IP 白名单掉线 / 5xx),静态块从快照水合,使 OC 模型选择器离线仍可用。读写失败软失败——绝不阻塞发布
geminiSanitization boolean true 当模型 ID 匹配 gemini 时,从工具参数中剥离 $schema/$ref/additionalProperties
mcpAutoEmit boolean false 自动把指向 <baseURL>/api/mcp/stream、携带解析后 Bearer token 的 mcp.<providerId> 远端条目写入 OC 配置
mcpToken string 未设置 自动发射的 MCP 条目使用的独立 Bearer。未设置时回退到 provider 的 apiKey(取自 auth.json
fetchInterceptor boolean true 对每个目标为 baseURL 的外发请求注入 Authorization: Bearer + 默认 Content-Type(带后缀仿冒防护)

示例:enrichment + 压缩标签 + MCP 自动发射

{
  "plugin": [
    [
      "@omniroute/opencode-plugin",
      {
        "providerId": "omniroute",
        "baseURL": "https://or.example.com",
        "managementReadToken": "<read-only-management-token>",
        "features": {
          "combos": true,
          "enrichment": true,
          "compressionMetadata": true,
          "mcpAutoEmit": true,
        },
      },
    ],
  ],
}

mcpAutoEmit: true 时,插件合成的 mcp.omniroute 条目等价于手工配置:

"mcp": {
  "omniroute": {
    "type": "remote",
    "url": "https://or.example.com/api/mcp/stream",
    "enabled": true,
    "headers": { "Authorization": "Bearer <apiKey-from-auth.json>" }
  }
}

若想为 MCP 使用更小权限范围的 Bearer(不同于聊天/推理 key),设置 features.mcpToken操作者手动配置优先:若你已在 opencode.json 中配置了 mcp.omniroute,插件不会覆盖它。

示例:生产取向默认(干净选择器 + 离线韧性)

{
  "plugin": [
    [
      "@omniroute/opencode-plugin",
      {
        "providerId": "omniroute",
        "baseURL": "https://or.example.com",
        "features": {
          "combos": true,
          "enrichment": true,
          "compressionMetadata": true,
          "usableOnly": true,
          "diskCache": true,
        },
      },
    ],
  ],
}
  • usableOnly: true 会把其规范 provider 在你的 OmniRoute 实例中没有健康连接的模型丢弃——/models 选择器只保留你真能调用的模型。
  • diskCache: true(默认)每次健康刷新时把快照写入 ${OPENCODE_DATA_DIR}/plugins/omniroute-<providerId>.json。冷启动时若 /v1/models 不可达(笔记本离线、IP 白名单掉线),快照会水合静态块,OC 依然展示完整目录而非占位 stub。
  • compressionMetadata: true 用红绿灯 emoji 把 Combo 显示名标注上压缩管线与强度(例如 Combo: claude-primary [rtk🟡 → caveman🟠])。调色板:🟢 lite/minimal · 🟡 standard · 🟠 aggressive/full · 🔴 ultra。未知强度回退为原始文本([rtk:custom-thing]),插件绝不隐藏 OmniRoute 已知而插件未知的值。
  • providerTag: true(默认)前置短上游标签,使选择器对 cc/claude-opus-4-7 显示 Claude - Claude Opus 4.7、对 kr/claude-opus-4-7 显示 Kiro - Claude Opus 4.7、对 ghm/gpt-5 显示 GHM - GPT 5slot.name GitHub Models 超过 8 字符因此缩写)。当同一模型 ID 经由多个成本/鉴权/限流画像不同的上游连接售卖时,这一点至关重要。设为 false 可回到 v3.8.3 之前的无前缀格式。

显示名的统一装配逻辑在 src/naming.tsbuildModelDisplayName 中实现,优先级为:Auto Combo → Auto: <variant> (<N>p);DB Combo → Combo: <name>;Free + enrichment + providerTag → [Free] <label> - <name> · <budget>;Free + raw → [Free] <rawId> · <budget>;Enrichment + providerTag → <label> - <name>;仅 enrichment → <name>;最终兜底 → normaliseFreeLabel(rawId)。免费预算后缀(如 25M tokens/day10M credits)由 formatFreeBudgetfreeType 生成,令牌量级(K/M/B)由 fmtTokens 处理边界进位。

示例:收敛模型选择器(白名单 + 黑名单)

典型 OmniRoute 实例提供 600+ 个模型。要在几百条记录里翻出实际使用的约 30 个模型时,OpenCode TUI/CLI 选择器会变得不可用。visibleModelshiddenModels 可以把选择器收敛到固定的模型 ID 集合,且该集合持久保存在 opencode.json 中,配置重置也不会丢:

{
  "plugin": [
    [
      "@omniroute/opencode-plugin",
      {
        "providerId": "omniroute",
        "baseURL": "https://or.example.com",
        "features": {
          "combos": true,
          "enrichment": true,
          "usableOnly": true,
          "visibleModels": [
            "claude-opus-4-7",     // 裸后缀:匹配 cc/claude-opus-4-7、kr/claude-opus-4-7 等
            "cc/claude-sonnet-4-6", // 精确:仅 cc/ 别名
            "gemini-2.5-pro",
            "gpt-5",
            "o3",
            "o3-pro",
            "o4-mini",
          ],
          "hiddenModels": [
            "o3-mini",  // 即使 visibleModels 未设置也隐藏 mini 变体
          ],
        },
      },
    ],
  ],
}
  • visibleModels 是白名单——只发射原始 ID 匹配的模型。裸 ID(无斜杠)匹配任意 provider 前缀;完整 ID(含斜杠)精确匹配。
  • hiddenModels 是黑名单——被列出的模型直接丢弃。当模型同时出现在两份名单里时,黑名单胜出(deny 优先)。
  • 两者都与 usableOnly 组合(全部 AND:模型必须同时通过 usableOnly、visibleModels 且不在 hiddenModels)。
  • 未设置或为空 = 不过滤(当前行为)。

这类过滤与 URL 门禁、模型 id 解析规则共同被 multi-instance.test.tsmodel-allowlist.test.tsusable-combo.test.ts 等 25 个测试文件覆盖,覆盖点包括 option schema 校验、Gemini 清洗、fetch 拦截、磁盘快照权限、feature 默认值、管理只读 token、自动同步、命名与免费预算等——它们都运行在 npm test 之下。

Gemini 清洗与 Bearer 注入的底层实现

README 的 Features 矩阵里「Bearer 注入」与「Gemini schema 清洗」都落在 auth.loader.fetch 上,其实现值得单列说明:

  • fetch 拦截器createOmniRouteFetchInterceptor)只对「源 == baseURL origin 且路径命中 /v1/chat/completions/v1/models」的请求注入 Authorization: Bearer 与默认 Content-Type。baseURL 无法被 new URL 解析时,注入会被禁用而非扩大凭证作用域——这是「后缀仿冒防护」的落点:伪造的相似 host 永远拿不到你的 key。
  • Gemini schema 清洗createGeminiSanitizingFetch)递归删除 $schema$refrefadditionalProperties 四类 Gemini 会拒绝的 JSON-Schema 关键字,覆盖 tools[].function.parameters(OpenAI chat-completions 形态)、tools[].function_declaration.parameters(Gemini 原生形态)与 tools[].input_schema(Responses API 形态),并处理 payload 顶层关键字。模型判定由 shouldSanitizeForGemini 完成:payload.model 命中大小写不敏感的 gemini 子串即触发(覆盖裸 gemini-*models/gemini-…google-vertex/gemini-…)。清洗整体fail-open:任何解析/清洗异常都回退为转发原始请求——清洗是尽力而为的护栏,绝不是硬失败路径。ReadableStream 流式 body 因无法安全克隆而跳过,只做一次性 console.warn 提示。
  • 两层通过 loader 按序组合:先包上 Bearer 注入拦截器,再包 Gemini 清洗层(两层均可由 features.fetchInterceptor / features.geminiSanitization 独立关闭);两者都关闭时回退到 SDK 默认 fetch(仅带 apiKey)。

运行环境要求与版本兼容

  • Node >=22.22.3(见 engines.node);已在 Node 22 与 24 上测试。
  • OpenCode:已针对 opencode@1.15.5 + @opencode-ai/plugin@1.15.6 做过端到端验证。
  • OC 插件 peer(@opencode-ai/plugin>=1.14.49 才能获得完整功能(provider hook 会把模型展现在 /models)。在 <=1.14.48 上,插件回退到自身 config hook,把静态目录快照写入 config.provider[id],模型依然可见。
  • 插件使用 OC v1 插件形态default: { id, server },见 index.ts 底部的 OmniRouteV1Plugin)。只遍历命名导出的旧版 OC 会拒绝它——请保持在 OC ≥1.15。

结语

@omniroute/opencode-plugin 把「运行时实时目录、Combo LCD 聚合、Gemini 兼容清洗、多实例隔离、离线磁盘兜底」完整收拢进 OpenCode 的原生 Plugin 契约中。对使用者而言,收益是零静态维护:/models 永远等于 OmniRoute 此刻的真实目录;对开发者而言,它同样是一个值得研读的参考实现——从 插件主源码命名管道25 个专项测试,完整展示了如何把 AI 网关的丰富目录模型以合规、安全、可离线的方式接入桌面级 Agent 客户端。

License

MIT。见 @omniroute/opencode-plugin/LICENSE

登录后查看全文
热门项目推荐
相关项目推荐