DeepSeek Harness(DSH)插件:在设置页新增一栏 「OpenCode Go 模型同步」,一键拉取 OpenCode Go 服务端的全部模型、逐个实测可用性与视觉能力,并把可用模型写回 llm-pi-ai 的模型列表。
dsh 设置里内置的「获取可用模型」按钮对 catalog 里的 provider 不发网络请求——它直接返回打包在 pi-ai npm 包里的静态快照(opencode-go 是 19 个模型)。OpenCode Go 服务端实际模型列表变动频繁(上新、下线、暂不可用),静态快照永远滞后。
本插件走真实路径:GET /models 拉全集 → 逐个实测 → 写回 settings.yaml。
npx -y @deepseek-ai/dsh plugin --profile web add https://github.com/Retr67/dsh-opencode-models安装后重启 dsh web 并刷新页面。
插件做的是"把路由的实际模型列表如实同步进配置"。路由不存在时它会自动创建(仅限它认识的 provider,默认 opencode-go),所以你只需要准备:
- 凭据可用:
OPENCODE_GO_API_KEY已在 dsh 凭据里配好(这是唯一硬性前提); - 路由本身无需手写 —— 缺了就自动建,只写
baseURL+apiKeyEnv。
为什么这么少:dsh 对 catalog 支持的 provider 只需要一个凭据引用就能工作 —— 端点、协议、以及网关要求的
x-opencode-session头都由内置 catalog 提供。你现在的配置正是一行apiKeyEnv: OPENCODE_GO_API_KEY,聊天一直正常。插件自己发 HTTP 请求,所以它内部会补上同样的默认值(端点 + 探测用 session 头),但不会把这些写进你的配置 —— 写死一个固定 session 头反而会顶掉聊天用的按会话路由。
只有插件不认识的 provider(自定义聚合站)才需要你手写路由,那时它会告诉你缺什么、现有路由有哪些。
| 情况 | 行为 |
|---|---|
| 没有目标路由,且是它认识的 provider | 自动创建(只写 baseURL + apiKeyEnv),然后正常同步 |
| 没有目标路由,且不认识这个 provider | 报错并列出当前实际存在的路由名 |
路由存在但没有 baseURL,是它认识的 provider |
用内置默认端点,正常同步 |
同上,但路由声明了 api |
顺带把 baseURL 补写进路由 —— 否则 dsh 会拒绝未收录模型(needs a baseURL) |
路由存在但没有 baseURL,是不认识的 provider |
指出缺哪个字段 |
读不到 llm-pi-ai 配置节 |
提示适配器可能没加载 |
- 打开 dsh 设置 → OpenCode Go 模型同步;
- 点击 「更新最新 OpenCode Go 模型列表」;
- 等待进度完成(约 1 分钟:拉列表 + 全部模型两轮探测),页面会显示:
- 新增:服务端有、配置里没有的模型;
- 保留:继续可用的模型;
- 移除:配置里有、服务端已下线的模型;
- 服务端拒绝:在
/models里但实测不可用的模型(地区封锁 / 需 opt-in / 明确拒绝); - 未纳入(catalog 未收录):可用但 dsh 内置 catalog 不认识、且路由未声明
api的模型(见下文如何纳入)。
对每个模型发微型真实请求(max_tokens: 1,并发 6)。
网关上的模型分属三种 wire 协议。dsh 按 catalog 给每个模型定的协议自动路由,插件不干预也不该干预 —— 但插件必须"问得通",才能判断模型是否真的可用:
| 协议 | 端点 | 鉴权 |
|---|---|---|
openai-completions |
POST {baseURL}/chat/completions |
Authorization: Bearer |
openai-responses |
POST {baseURL}/responses |
Authorization: Bearer |
anthropic-messages |
POST {baseURL}/messages |
x-api-key + anthropic-version(Bearer 会被拒) |
插件先试 openai-completions;只有当失败信息表明「协议不对」(ModelProtocolUnsupported / 404 / 405)时,才换下一个协议重试。地区封锁、opt-in、5xx 等是关于模型的结论,不会白跑另外两个协议。
为什么必须这样做:2026-10-22 在真实网关上实测,
grok-4.6/grok-4.7在/chat/completions返回 400ModelProtocolUnsupported,但在/responses返回 200;claude-haiku-5-5/minimax-m2.7在 completions 返回 400,在/messages(配x-api-key)返回 200。只探一种协议的旧版本会把这几只完全可用的模型判成「服务端拒绝」并从你自己的配置里删掉。
| 探测结果 | 判定 |
|---|---|
| 任一协议 200 | 可用 |
403 RegionError「not available in your country」 |
地区不可用(移除) |
| 400 含数据政策字样(如「trains on request data」) | 需 opt-in(移除) |
三协议均 ModelProtocolUnsupported / 400 / 401 |
服务端明确拒绝(移除) |
| 402「Endpoint is unavailable」 | 无法判定(保留) |
| 5xx | 暂时故障(保留) |
| 网络错误 | 无法判定(保留) |
| 图像 ping(1×1 PNG,走该模型已答通的协议) | 200 → 标注 input: [text, image] |
旧版本把 403 一律当成「需要数据政策 opt-in」,但真实网关的 403 主要是地区封锁,而真正的 opt-in 反而返回 400。现在按响应体区分。
写回保护:
- 无变化不写:列表与当前配置一致时不触碰
settings.yaml(含「再同步一次同样内容」也完全不写;视觉标注input只在真正变化时才改,不会因为字段顺序变动而误判为有变化); - 大量失败中止:超过一半模型"无法判定"(网络层异常)时放弃本次写回,绝不清空列表;
- 不写空列表:若没有任何模型能写入(例如全部被拒、或全部未被 catalog 收录),拒绝写入并说明真实原因——空列表会把你已配置的模型丢回 catalog 默认值;
- 系统性误删保护:如果大部分"服务端仍在售卖"的已配置模型同时被拒(≥3 个且占一半以上),这更像系统性故障(密钥失效、配额到期、网关新增了必需请求头)而不是真的下线,插件会保留这些条目并继续同步其余部分。服务端不再列出的模型才是真下线,照常移除。「下线模型自动移除」因此不受影响;
- 保留手工元数据:已有条目的
name/contextWindow/maxTokens原样保留——你手填过的规格永远不会被同步覆盖; - 新条目不写死规格:新模型只写
id/name(+ 实测的视觉、思考档位),contextWindow与maxTokens交给 dsh 自己解析——dsh 会从内置的 pi-ai catalog 取该模型的真实规格(例如 kimi-k3 → 1048576 / 131072),只有 catalog 也不认识的模型才回落到路由默认值。写死一个猜测值反而会把错误数字永久冻结(因为同步保留已设置的字段); - 视觉能力以实测为准(覆盖手工标注)。
模型条目没有声明 contextWindow / maxTokens 时,dsh 按这个顺序解析:
- 内置 pi-ai catalog 里该模型的真实条目(例如
kimi-k3→ 上下文 1048576、输出 131072); - catalog 不认识这个模型时,回落到路由的
defaultContextWindow/defaultMaxTokens(dsh 默认 262144 / 32768)。
由此推论:
- 不要手填猜的值——一旦条目里写了
contextWindow,它就永久覆盖 catalog 的真实规格,而同步又会一直保留你的写法(看起来"怎么都不是尽可能大",就是这个原因); - 确实想调大/调小时才显式写,例如
{ id: kimi-k2.6, contextWindow: 1048576 }; - 服务器
/models只返回id,不提供任何规格元数据,所以本插件不猜、也不写死规格,一律留给 dsh 的 catalog 解析。
- 宿主端(
dsh/index.js,零依赖):注册POST /opencode-models/refresh(启动后台刷新)与GET /opencode-models/status(进度/结果)两个路由;从llm-pi-aisettings 读取路由配置(baseURL/apiKeyEnv),经 dsh 凭据服务解析 API Key,探测完成后用settings.mutate原子写回模型数组。 - 浏览器端(
dsh/client.js,零依赖手写懒加载 CJS bundle):注册settings.sectionslot,在设置面板新增独立一栏;按钮点击后轮询状态端点,实时渲染进度与结果。
默认目标 provider 是 opencode-go(llm-pi-ai 路由里的 opencode-go 条目);任何 OpenAI 兼容聚合站只要按同样方式配置了路由,改一个配置项即可复用(见下)。
探测请求与列表请求会携带该 provider 路由配置里的
headers(llm-pi-ai 路由可配headers: { ... })。部分聚合网关(如 OpenCode Go)要求x-opencode-session请求头——确保它在你的路由 headers 里,否则探测会被网关判为拒绝。
服务端会先于 catalog 上新,所以同步时可能有一部分模型写不进去。这是设计使然,不是故障。
dsh 给一个模型定 wire 协议时按这个顺序解析:
路由的 api → catalog 里该模型的 api → 该路由 catalog 唯一一致的协议
opencode-go 的 catalog 同时包含三种协议(anthropic-messages / openai-completions / openai-responses),所以第三条永远没有值。于是 catalog 不认识的模型无从定协议。
| 情况 | 行为 |
|---|---|
路由已声明 api |
全部模型照常同步(api 能解析不认识的模型) |
路由未声明 api,但有 catalog 认识的模型 |
认识的照常写入;不认识的列在「未纳入(catalog 未收录)」里,不写入 |
路由未声明 api,且一个认识的都没有 |
拒绝写入并报错(写空列表会把你已配置的模型丢回 catalog 默认值) |
也就是说:同步能推进多少就推进多少,不会再因为一个新模型导致整份列表写不进去。未纳入的模型会明确列出来,而不是悄悄丢掉。
在路由下加一行 api:
llm-pi-ai:
providers:
opencode-go:
api: openai-completions # ← 加这一行
baseURL: https://opencode.ai/zen/go/v1
apiKeyEnv: OPENCODE_GO_API_KEY加了之后刷新,未收录的模型也会被写入。
注意(重要):catalog 路由一旦显式声明
api,dsh 会改用协议实现(createProvider)而不是复用 catalog provider 来构造该路由。catalog provider 会按会话自动注入x-opencode-session,改走协议实现后这个自动注入就没了。所以如果你的网关要求该头(OpenCode Go 就是),请把 header 一并显式写上:headers: x-opencode-session: <你的会话标识>否则可能出现「同步成功了,但聊天全部被网关拒绝」。
本插件不会替你自动写这行
api:自动写会修好同步、却可能悄悄弄坏你原本正常的聊天请求(丢掉上面那个自动注入的 header)。这个取舍交给你决定。
已显式声明 api 的路由不受影响,刷新照常工作。
无需配置即可使用(默认目标 opencode-go 路由)。如需调整,在 profile 的 cordis.patch.yml 里按 entry id 覆盖:
# ~/.dsh/profiles/web/cordis.patch.yml
- id: opencode-models # 本插件在 patch 层的 entry id
config:
provider: opencode-go # 目标 llm-pi-ai provider 路由名
concurrency: 6 # 探测并发数(1-12)
timeoutMs: 25000 # 单请求超时(5s-60s)- dsh ≥ 0.1.0-rc.7(web profile;在 0.1.2-alpha.x 上开发验证,0.2.0-rc.2 上修复验证)
- 零构建、零运行时依赖(宿主与浏览器端均为 node/DOM 内置能力)
- 读配置走
settings.describe():dsh 0.1.x 的settings.get(ns)在 0.2.0 已被移除(v0.2.1 起改用describe().find(ns).value,新旧版本语义等价,双向兼容) - 已验证的场景:模型上新自动补齐、下线模型自动移除、视觉模型自动标注、5xx 故障不误删
- 已知限制:服务端出现 catalog 未收录的模型、且路由未声明
api时,这部分模型不会被写入(会列在结果里)。本插件不会擅自替你声明api(见上文「刷新失败 / 模型未被纳入」一节) - 思考强度:同步的模型带
reasoningEfforts声明,WebUI 出现思考强度选择器(关闭 / Low / Medium / High / Max)。选择器停在"关闭"时完全不发送思考参数。少数模型的上游拒绝 Max(qwen3.7 系列、grok-4.5、gpt-5.6-luna、minimax-m2.7、mimo-v2.5-pro 已实测剔除,只到 High);不想给某个模型开,把它的reasoningEfforts手动设为false,同步会保留你的设置 reasoningEfforts取值规则(dsh 0.2.x):想隐藏某一档就不要声明那个 key。除off外,任何档位的值都不能是null或空字符串,否则 dsh 会拒绝整份写入并报reasoningEfforts.<level> needs the wire value dispatch should send。旧版本曾用minimal: null表达"隐藏 Minimal",该写法已作废;同步会自动清理历史配置里的这种值,不需要你手改 YAML
dsh 0.2.0 移除了 settings 服务的 get(ns) 方法(0.1.x 有,返回该命名空间的解析值),插件 v0.2.0 及更早版本因此在该版本上完全无法读取配置,点刷新立即失败:
刷新失败:ctx.settings.get is not a function
v0.2.1 已修复:改用 settings.describe().find(ns).value。这个方法在 0.1.2 起的每个版本都存在,返回的 value 与旧 get() 返回的解析值相同,所以同一个调用在新旧 dsh 上都能用。
修复后需要重启 dsh(宿主端模块在启动时加载,热重载不会重新执行已缓存的模块),再刷新页面。
MIT