一个融合 动态模型发现 + 账号池 failover 的 OpenCode 插件: 自动兼容任意 OpenAI 兼容第三方中转站(
/v1),把中转站的真实模型动态同步进/models列表, 并为每个中转站维护多 Key 账号池 —— 限流自动隔离、失效自动禁用、加权轮询切换,零人工干预。
本插件是以下两个插件功能的合体:
| 功能 | 来源 |
|---|---|
| 动态模型发现(模型列表随中转站真实变化) | opencode-models-discovery |
| 多 Key 账号池 / failover 轮换 | opencode-failover |
- 任意 OpenAI 兼容中转站:只需
baseURL+ 一个或多个 key,无需额外代码 - 模型动态发现:启动时拉取中转站
/v1/models,注入 OpenCode provider 配置 —— 中转站增删模型,/models跟着变;并用 models.dev 元数据自动补充新模型的上下文窗口与 reasoning 标记(24h 缓存,网络不通自动跳过) - 账号池 failover:多 key 加权轮询;
429尊重Retry-After退避重试(长限流自动隔离、指数退避),401/403/402永久禁用并切换,5xx/过载退避后自动重试(最多 3 次) - 请求拦截轮换:fetch 补丁用请求头识别池子 key(不匹配则原样放行,不读取 Request body);出错时透明切换下一个 key,并把激活 key 写入
auth.json;支持fetch(url, init)与fetch(new Request(...))两种调用形式 - 流式零缓冲:2xx 响应原样直通,不 clone 不缓冲 —— SSE 流式输出零延迟
- 安全落盘:
.env/auth.json写入即chmod 0600,共享状态文件中的 key 已脱敏 - 模型过滤:正则
includeRegex/excludeRegex、字段级includeBy/excludeBy、自动剔除 embedding 模型 - 模型名称增强:
smartModelName把owned_by拼进显示名(如openai gpt-5.2) - 发现结果缓存:可选(默认关闭),开启后默认 24h TTL
- 可视化工具:
relaypool-status/relaypool-setup/relaypool-remove/relaypool-reset/relaypool-switch/relaypool-refresh/relaypool-import;relaypool-refresh会把新模型写回当前会话的/models列表 - 配置兼容:同时兼容
relayPool、modelsDiscovery两种配置块,以及opencode-failover风格的*_API_KEYS环境变量和.env
npm install -g opencode-relay-pool
# 或
opencode plugin opencode-relay-pool在 opencode.json 中启用:
{
"plugin": ["opencode-relay-pool@latest"]
}你只需要按 OpenCode 原生方式配置中转站,插件自动接管一切:
{
"$schema": "https://opencode.ai/config.json",
"plugin": ["opencode-relay-pool@latest"],
"provider": {
"myrelay": {
"npm": "@ai-sdk/openai-compatible",
"name": "我的中转站",
"options": {
"baseURL": "https://relay.example.com/v1",
"apiKey": "sk-xxx"
}
}
}
}重启 OpenCode 后 /models 里自动出现该中转站的全部真实模型(随中转站增删自动变化)。想要多 key 轮换,把 apiKey 换成数组:
"options": {
"baseURL": "https://relay.example.com/v1",
"apiKeys": ["sk-1", "sk-2", "sk-3"]
}三个 key 自动组成账号池:限流的隔离、失效的禁用、按权重轮询切换。
对话中直接说:
Add these API keys for myrelay: sk-xxx, sk-yyy, sk-zzz
或用工具:
relaypool-setup(provider="myrelay", keys="sk-xxx,sk-yyy", base_url="https://relay.example.com/v1")
key 会保存到项目 .env,插件自动注册账号池和模型发现。
MYRELAY_API_KEYS="sk-xxx,sk-yyy,sk-zzz"
MYRELAY_BASE_URL="https://relay.example.com/v1"<ID>_API_KEYS 与 <ID>_BASE_URL 会从进程环境变量和项目 .env 读取(.env 不覆盖已经存在的环境变量)。provider id 里的非字母数字会变成下划线,例如 myrelay → MYRELAY_BASE_URL。也可以只在 opencode.json 里写 baseURL,把 key 放在 MYRELAY_API_KEYS 里。
若还要自动发现模型并出现在 /models 里,opencode.json 中仍需要对应的 provider.<id>(至少包含 npm 与 options.baseURL)。仅环境变量可以注册账号池,但不能凭空创建一个 OpenCode provider。
想精细控制过滤/缓存/权重时,用 relayPool 配置块:
{
"provider": {
"myrelay": {
"npm": "@ai-sdk/openai-compatible",
"name": "我的中转站",
"options": {
"baseURL": "https://relay.example.com/v1",
"relayPool": {
"apiKeys": ["sk-key1", "sk-key2", "sk-key3"],
"weight": { "sk-key1": 3 },
"header": "Authorization",
"scheme": "Bearer",
"discovery": {
"enabled": true,
"timeoutMs": 5000,
"models": {
"excludeRegex": ["embedding", "^tts"]
}
}
}
}
}
}
}| 字段 | 类型 | 默认 | 说明 |
|---|---|---|---|
enabled |
boolean | true |
是否启用该中转站的 relay-pool |
apiKeys |
string[] | [] |
账号池 key 列表(多 key 自动 failover) |
weight |
Record<string, number> | {} |
加权轮询,如 {"sk-1": 3} 表示该 key 权重 3x |
header |
string | Authorization |
认证头名称 |
scheme |
string | Bearer |
认证 scheme,设为空字符串可传裸 key |
discovery |
RelayDiscoveryConfig | — | 模型发现配置 |
| 字段 | 类型 | 默认 | 说明 |
|---|---|---|---|
enabled |
boolean | true |
是否动态拉取模型 |
endpoint |
string | /v1/models |
模型列表端点 |
timeoutMs |
number | 3000 |
发现请求超时 |
smartModelName |
boolean | false |
用 owned_by + id 生成显示名 |
filterNonChat |
boolean | true |
过滤非聊天模型(当前仅自动剔除 embedding 类;image/audio/tts 等保留) |
includeRegex / excludeRegex |
string[] | [] |
模型 ID 正则过滤 |
models.includeBy / models.excludeBy |
{field, match|equals}[] | [] |
模型字段级过滤 |
cache.enabled |
boolean | false |
开启发现结果缓存 |
cache.ttlSeconds |
number | 86400 |
缓存 TTL |
modelsDiscovery 配置块同样被识别(与 relayPool 等价):
{
"options": {
"baseURL": "https://relay.example.com/v1",
"modelsDiscovery": {
"enabled": true,
"models": { "includeRegex": ["^gpt|^deepseek"] }
}
}
}| 工具 | 作用 |
|---|---|
relaypool-status |
查看所有账号池状态:active / QUAR / DISABLED、权重、退避倒计时 |
relaypool-setup |
保存 key(provider, keys 逗号/换行分隔, base_url) |
relaypool-switch |
手动切换当前激活 key(不传 key 轮换到下一个,传 key 指定) |
relaypool-remove |
移除 key(不传 key 参数则清空该 provider 全部 key) |
relaypool-reset |
把所有隔离/禁用的 key 重置为 active |
relaypool-refresh |
立即重新拉取某 provider 的模型列表,并写回当前会话的 /models(用户手写的 models 会保留,中转站已下线的模型会去掉) |
relaypool-import |
从 opencode auth.json 导入已有 key |
- key 池:项目
.env(<ID>_API_KEYS+OPENCODE_RELAY_POOL_KEYS,写入后自动chmod 0600) - 共享状态:
~/.config/opencode/relay-pool-state.json(key 已脱敏) - 激活 key:写入
auth.json(自动chmod 0600),OpenCode 请求直接带上当前激活 key;出错时由 fetch 补丁透明切换 - 发现缓存:
~/.local/share/opencode/relay-pool-discovery/<id>.json
| 变量 | 说明 |
|---|---|
OPENCODE_RELAY_POOL_DEBUG |
设为 1 输出调试日志 |
OPENCODE_RELAY_POOL_ENV_FILE |
自定义 .env 路径 |
<ID>_API_KEYS |
该 provider 的 key 列表,逗号分隔,如 MYRELAY_API_KEYS |
<ID>_BASE_URL |
该 provider 的中转站地址,如 MYRELAY_BASE_URL |
OPENCODE_RELAY_POOL_KEYS |
keychain JSON(内部使用) |
OPENCODE_RELAY_POOL_PROVIDERS |
provider 配置 JSON(内部使用) |
启动 → 解析 provider.relayPool / options.apiKeys / <ID>_API_KEYS + <ID>_BASE_URL
→ KeyPool 注册多 key(再次注册时保留隔离/禁用状态)
→ config hook: 请求 {baseURL}/v1/models → 过滤 → models.dev 补 limit/reasoning
→ 注入 config.provider.<id>.models(用户手写的 models 优先保留)
→ fetch 补丁: 先用请求头匹配池子 key;不匹配则原样放行(不读 Request body)
├─ 匹配成功后才物化 Request,以便出错重试
├─ 2xx → 原样直通(不 clone、不缓冲,SSE 流式输出零延迟)
├─ 401/403/402 → 禁用该 key, 切换下一个, 写入 auth.json(无需等待)
├─ 429/5xx/overload → 按 Retry-After(无则 2s)退避等待后重试
│ Retry-After ≥ 10s → 隔离该 key(指数退避 60s→300s)并切换
└─ 全部 key 不可用 → 降级用当前 key,请求绝不抛异常
relaypool-refresh → 强制重新发现并写回当前会话的 provider.models
错误响应 body 仅读取前 2KB 用于分类,不影响原响应
兼容 fetch(url, init) 与 fetch(new Request(...)) 两种调用形式
/models 里没有出现中转站的模型?
按顺序排查:discovery.enabled 是否被设为 false;excludeRegex / filterNonChat 是否把模型过滤掉了;中转站 /v1/models 是否可达。打开 OPENCODE_RELAY_POOL_DEBUG=1 可看到发现请求的日志。
模型列表不更新?
开启 discovery.cache.enabled 后默认缓存 24h;用 relaypool-refresh 会跳过缓存、重新拉取,并把结果写回当前会话的 provider.<id>.models(用户在配置里手写的模型会保留,中转站已下线的模型会去掉)。如果 TUI 的 /models 面板还显示旧列表,关掉再打开一次即可;仍没有则重启 OpenCode。
key 全部变成 DISABLED / QUAR?
用 relaypool-status 查看每个 key 的禁用原因和退避倒计时;relaypool-reset 一键全部重置回 active。
中转站不支持 /v1/models?
把 discovery.enabled 设为 false,然后在 provider 里手动写 models 即可,账号池 failover 不受影响。
流式输出很卡 / 半天才出第一个字?
这是旧版本 clone().text() 缓冲整个响应导致的,升级到含 fetch 补丁修复的版本即可(2xx 响应现在零缓冲直通)。
401/403 报错但 key 没被禁用?
这是旧版本的已知 bug:错误分类器在检查 HTTP 状态码之前先做响应体关键字匹配,quota/capacity/exhausted 等字样会把 401/403 误判成"服务器过载",导致坏 key 永不剔除、一直轮换重试。升级到最新版(classify 先判状态码再判关键字)即可。
429 之后请求为什么反而变慢了?
这是有意的:旧的实现收到 429 后不等待就直接换 key 重试,等于对中转站连续开火,只会招致更狠的限流;现在会尊重 Retry-After(没有则默认 2s 退避,上限 9s)再重试。长限流(≥10s)则把该 key 隔离起来,用其它 key 继续。
key 会明文暴露吗?
.env 与 auth.json 写入后自动 chmod 0600(仅当前用户可读写);共享状态文件 relay-pool-state.json 中的 key 已脱敏。
npm install
npm run typecheck # tsc --noEmit
npm test # node --experimental-strip-types --test推荐方式(打 tag 触发 CI 自动发布 npm,需仓库配置 secrets.NPM_TOKEN):
npm version patch
git push --follow-tagsCI(.github/workflows/publish.yml)会在 tag 上自动跑 typecheck + 全量测试并 npm publish --provenance。
也可以手动发布:
npm login
npm version patch
npm publish