DSH Plugins Marketplace

DSH Plugins

Plugins

/

dsh-opencode-pin

d

dsh-opencode-pin

Manifest valid

Pin an OpenCode Go upstream route in DeepSeek Harness by keeping one stable x-opencode-session key, with an in-GUI route picker and drift checks.

UI (client)hasBundlePatch

dsh-opencode-pin · 把 OpenCode Go 的上游路由钉住

一个 DeepSeek Harness(DSH)Host 插件:把所有模型调用钉在 OpenCode Go 套餐上,并保证每个请求都带 稳定的 x-opencode-session,从而让 OpenCode 的「会话 → 上游提供商」绑定在整段对话里不漂移。


awesome · DSH plugin

源码:https://github.com/wobushiyoushen0214/dsh-opencode-pin

安装

# 官方 CLI(插件市场里的同一条命令)
dsh plugin --profile web add dsh-opencode-pin

# DSH 内置插件管理器
plugin_manager { action: "install_bundle", target: "dsh-opencode-pin" }

装好后:设置 → 内置插件 → 页签「OpenCode 上游钉选」。也可以从插件市场的卡片一键安装(搜 "OpenCode 上游钉选" 或 dsh-opencode-pin)。


0. 当前状态(本机已实测)

项结果
安装bundle dsh-opencode-pin,行 opencode-pin:enabled 且 fiberPhase: active
Config schemacordis_inspect_query Config.listConfigs → status: "schema",packageDir 指向 profile 链接;说明 @deepseek-ai/schemastery 的 peer 路由在重启后生效
路由钉(已在跑)会话日志只有两条 request/header:initial muyuan/deepseek-v4.1-flash → resume opencode-go/deepseek-v4.1-flash(effort high、maxTokens 384000);重启后 13 条 assistant/message 全部由 opencode-go 服务,且没有 provider 反复横跳
单测node --test test/*.test.mjs → 23 passed
观测路由GET /opencode-pin/status 曾 404:已定位为注册时机 bug 并修复(见下),需再重启一次生效

两个踩过的坑(都已在代码里修掉)

  1. agent/request 必须 prepend 才能钉住路由。 dsh-agent 自己的"会话模型选择"监听器也答 agent/request,它在外层 await next() 后用会话选择覆盖结果。插件原来注册在最内层,改写被吞掉,日志里表现为每步都 muyuan -> opencode-go 却钉不住;而且"会话选择 ≠ 最近 header"会被当成用户换模型,反复注入 [model changed: …] 通知。现在:{ global: true, prepend: true } + 通过 sessionController.selectForNextRequest() 把钉住的路由写成本会话 model/selection。
  2. ctx.on('webServer', …) 在 Cordis v4 里永不触发。 服务就绪只以 internal/service 事件广播(cordis/lib/index.js:849)。而 webServer 由 web 端 bundle 挂载,插件在 profile bundle 之后激活时它可能还不存在 → 路由永远注册不上。正确写法是 ctx.inject(['webServer'], (scoped) => …)。 现场对比(临时探针插件,两个路由同插件内并列):
    /opencode-pin-probe/inject   200  {"ok":true,"via":"ctx.inject"}
    /opencode-pin-probe/event    404
    

插件 JS 代码改动必须重启应用才会加载新一代模块(ESM 模块缓存);只有 cordis.patch.yml 的配置改动走 HMR。


1. 为什么需要它(实测结论,非推测)

用 scripts/evidence.mjs 打真实请求(每次 1 token),OpenCode Go 会在响应头里明说自己用了哪个上游:

x-opencode-endpoint-id:        novita-deepseek | luminal-dsv4.1flash | orcarouter-dsv4.1flash | ...
x-opencode-upstream-model-id:  deepseek/deepseek-v4.1-flash | deepseek-ai/DeepSeek-V4.1-Flash | ...
x-zen-model:                   deepseek-v4.1-flash
x-opencode-log-id:             98b1140f-5350-4bce-8c02-19e1afa1f438

三个关键事实:

  1. 同一个模型 id 由多个上游端点服务(novita-deepseek、luminal-dsv4.1flash、orcarouter-dsv4.1flash …), x-opencode-upstream-model-id 甚至分两种命名体系:deepseek-ai/...(官方权重那一路)与 deepseek/...(第三方托管)。这就是「降智」的来源——落到哪个上游不由你选。

  2. 路由由 x-opencode-session 决定,而且一旦分配就黏在那个 key 上:

    • 同一 key 连续请求 → 始终同一个端点;
    • 换新 key → 落到「当前默认」端点(会随时间/负载整体变化);
    • 官方文档明确要求:"Send a stable session ID in x-opencode-session for each conversation so we can optimize routing and prompt caching."
  3. 不带这个头,请求直接被拒:

    HTTP 400  Request is missing x-opencode-session and cannot be routed efficiently.
    

而 DSH 只有部分路径会带 session:agent 主循环 / 子代理 / 标题 / 压缩都带,但 @deepseek-ai/dsh-experimental-auto-review 这类直接调 ctx.llm.stream() 的路径不带(OpenCode 官方文档的 "Known problematic clients" 里点名 DSH:"Session information arrives on some model paths, but is missing on others.")。 DSH 也不会把响应头暴露给插件,所以「现在到底落在哪个上游」平时是看不见的。

2. 它怎么做

机制扩展点作用
路由钉选agent/request waterfall在请求被记入会话日志之前把 provider/model 换成配置的目标(默认 opencode-go/deepseek-v4.1-flash)。主循环与每个子代理都覆盖,用量归属与可回放 header 保持正确。
会话兜底注入llm/stream waterfall任何到达 LLM seam 却没有 sessionId 的调用,补齐为「发起该调用的对话身份」;落在 OpenCode 路由上的调用可改为共享一个被探测挑出来的 key(shared / hunt 模式)。
会话选择对齐sessionController.agents.selectForNextRequest()(facade 上若将来提升也接受;都没有则退回纯 waterfall,并在状态里报 selectionApi: false)把钉住的路由写成本会话的 model/selection。dsh-agent 会把"会话选择 ≠ 最近请求 header"当成用户换了模型,反复注入 [model changed: ...] 通知——对齐后不会误报,UI 的模型标签也与实际一致。
上游观测与挑选自带 1-token 探测因为 Harness 不给插件响应头,插件自己发探测请求,读出 x-opencode-endpoint-id,在 hunt 模式下挑一个落在偏好端点上的 key 并全程复用——这就是字面意义上的「钉住上游提供商」。

hunt 模式的做法:先用候选 key 并行探测(默认 4 个,每个 1 token),若某个 key 的端点命中 probe.prefer(例如 ["luminal-*"]),就把这个 key 固定下来给所有会话用;超过 probe.recheckMs 会重新验证一次,发现漂移就再挑一轮并告警。

钉住上游端点(本插件的主要用途)

实测结论(本机,跨 1 小时+、跨两次应用重启):

key现在落到x-opencode-upstream-model-id
probe-A / pin-live-aluminal-dsv4.1flashdeepseek-ai/DeepSeek-V4.1-Flash(官方权重命名那一路)
pin-6 / evidence-oldnovita-deepseekdeepseek/deepseek-v4.1-flash
pin-1orcarouter-dsv4.1flashdeepseek/deepseek-v4.1-flash
任何新 key当前默认(观察期内从 luminal → novita → orcarouter 变过)随默认

两条关键规律:

  1. 绑定黏在 key 上,且长期稳定;换新 key 只会拿到"当前默认"。
  2. 所以想要非默认端点(如 luminal),必须复用当初落在那里、且一直黏着的旧 key —— 这正是本插件 probe.pool / 记忆 key 的用途。

配置(cordis.patch.yml 里按行 id opencode-pin 覆盖,config 整体替换):

- id: opencode-pin
  name: "dsh-opencode-pin"
  config:
    pin:
      route: false                 # 不钉 provider 路由,只钉上游端点
    session:
      mode: shared                 # 所有 opencode 请求共用一个 key ⇒ 同一个上游
      models: [deepseek-v4.1-flash]  # 只钉这个模型(留空=该路由所有模型)
      key: probe-A                 # ← 换这一行就换上游:
                                   #   probe-A / pin-live-a → luminal-dsv4.1flash
                                   #   pin-6 / evidence-old → novita-deepseek
                                   #   pin-1               → orcarouter-dsv4.1flash
                                   #   任意新 key          → 当前默认端点
    probe:
      prefer: ["luminal-*"]        # hunt 模式的偏好端点(通配 *)
      pool: [probe-A, pin-live-a, pin-6, evidence-old, pin-1]  # 优先复用的历史 key

哪个端点算「DeepSeek 官方」?

同一模型 deepseek-v4.1-flash 在三个端点上的线上指纹(实测):

端点 (x-opencode-endpoint-id)x-opencode-upstream-model-id响应 id 形态判断
luminal-dsv4.1flashdeepseek-ai/DeepSeek-V4.1-Flashchatcmpl-ad9b3b1cHuggingFace 官方组织 deepseek-ai/ 的仓库命名 ⇒ 官方权重那一路
novita-deepseekdeepseek/deepseek-v4.1-flash32 位 hexOpenRouter 风格 vendor slug ⇒ 第三方托管
orcarouter-dsv4.1flashdeepseek/deepseek-v4.1-flash56 位 hex同上

另外:端点空间是按模型分配的 —— deepseek-v4-pro 只有 orcarouter-dsv4pro;deepseek-v4-flash 只有 novita-deepseek;唯一名字就叫 deepseek 的端点只服务 deepseek-v4-flash-vision-exp,deepseek-v4.1-flash 拿不到它(4 个不同 key 抽样均未出现)。

所以:在 opencode-go 体系内,「官方」的上限就是 luminal-dsv4.1flash(本插件的 key: probe-A 正指向它)。想要真正的 DeepSeek 官方 API 直连(api.deepseek.com),应当改用 DSH 自带路由而不是 opencode-go:

路由凭据说明
deepseek-accountDSH 账号授权(x-dsh-auth-token)登录即用;inferenceOrigin 默认 https://api.deepseek.com;失败以 ACCOUNT_QUOTA 呈现
deepseek-officialDEEPSEEK_API_KEY需在 设置→模型 里填官方 API Key

两条路由的默认目录都是 deepseek-flash(文本+图片,1M 上下文)与 deepseek-v4-pro(纯文本,1M),且模型 id 原样透传,任意官方模型 id 都能直接请求。

想让它自愈(定期校验绑定、漂移就自动换回一个命中的 key):把 session.mode 改成 hunt。hunt 会先验证 pool 里的历史 key(而不是只探新 key,因为新 key 拿不到非默认端点),命中即用,超过 probe.recheckMs(默认 30 分钟)重新校验,发现漂移会在日志里告警并重挑。

监控漂移:面板点「探测一轮」,或 curl "localhost:19387/opencode-pin/probe?keys=probe-A",看 endpoint 是否还是你钉的那个。

注意:session.mode: shared 会让该模型的所有请求(含标题/压缩/子代理)共用这一个 key —— 这是"钉上游"的代价与目的。想让别的模型保持原样,用 session.models 限定范围。

3. 安装

在 DSH 里用插件管理器安装(不会手写 profile 文件,也不需要 pnpm):

plugin_manager { action: "install_bundle", target: "/Users/lizhiwei/localProj/dsh-opencode-pin" }

安装后确认:

  • plugin_manager { action: "list_plugins" } 里能看到行 opencode-pin(application: applied 表示已热挂载);
  • GET http://127.0.0.1:19387/opencode-pin/status 返回 JSON;
  • 日志里出现 opencode-pin: route pin on (opencode-go/deepseek-v4.1-flash), session mode "conversation", hunt off。

修改插件代码后需要重启应用才会加载新的 JS(restart-required);只改 cordis.patch.yml 的配置会走 HMR。

4. 配置

配置走标准 DSH 方式:在 profile 的 cordis.patch.yml 里按行 id 覆盖。注意 patch 里的 config 是整体替换,不是深合并,要改哪层就把那层写全。

- id: opencode-pin
  name: "dsh-opencode-pin"
  config:
    pin:
      route: true                     # 强制所有调用走下面这个路由
      provider: opencode-go
      model: deepseek-v4.1-flash
      reasoningEffort: high           # 目标模型不支持调用方要的档位时用它兜底
      auxiliary: true                 # 标题/压缩/auto-review 这类绕过 agent loop 的调用也钉
      adoptSelection: true            # 同时把钉住的路由写成本会话的模型选择(UI 一致、不再注入"模型已切换"通知)
    session:
      mode: conversation              # conversation | shared | hunt
      routes: [opencode-go, opencode] # 只对这些路由改写 session key
      key: dsh-opencode-pin
    probe:
      prefer: ["luminal-*"]           # hunt 模式的偏好端点(空 = 不探测、不额外花 token)
      attempts: 4
      apiKeyRef: OPENCODE_GO_API_KEY

session.mode 三档

模式x-opencode-session额外流量适用
conversation(默认)对话自己的 session id;缺失时补「发起对话的身份」无最小干预:修掉缺头的路径,保持官方建议的「每会话稳定」。每个新对话仍可能被分配到不同上游。
shared所有请求共用一个固定 key无想彻底「全局一个上游」但又不想探测:所有对话用同一 key,首次分配后长期黏住。
hunt探测挑出的、落在 probe.prefer 端点上的 key,全程复用开启时每轮最多 attempts 个 1-token 请求(≈几百 token);命中后缓存 recheckMs真的要「钉住某个上游提供商」。

5. 在 GUI 里打开 & 验证

打开方式:设置 → 插件 → 顶部页签「OpenCode 上游钉选」(与官方「全部插件」「dshmarket」页签并列,order: 70)。这一页由 client.js 注册进 settings.plugins.tab 提供,显示:

  • 钉住的路由(provider/model/reasoningEffort)、pin.route 开关状态、session key 模式
  • config schema / 会话选择对齐 两个能力是否可用(绿=可用,黄=降级)
  • 当前钉住的 key 与它对应的上游端点(preferred)、见过的所有上游端点及次数
  • 会话 → key 分配表、最近探测记录、最近决定
  • 三个按钮:刷新、探测一轮(/opencode-pin/probe)、重新钉选(/opencode-pin/hunt)

日常怎么用(通常什么都不用做)

你想做的事怎么做
确认上游钉住了面板看 preferred / rememberedKeys,或 curl "localhost:19387/opencode-pin/probe?keys=<你配的 key>"
换一个上游端点改 session.key 一行(见上一节对照表),HMR 立即生效
确认在钉路由(可选)打开该页签看「钉住的路由」;或在会话里手动切到别的模型(如 muyuan),下一步会被自动拉回 opencode-go(recentDecisions 会出现一条 route-pin)
看当前上游端点页签点「探测一轮」,或 curl "localhost:19387/opencode-pin/probe?keys=manual-1";看 x-opencode-endpoint-id
钉到偏好端点profile 的 cordis.patch.yml 里按行 id opencode-pin 覆盖 session.mode: hunt + probe.prefer: ["luminal-*"](配置整体替换,要写全)
临时不钉设置→插件里停用 bundle dsh-opencode-pin;或 patch 里设 pin.route: false(只补 session、不强制路由)
只钉主循环、放弃标题/压缩patch 里设 pin.auxiliary: false
换钉的目标模型patch 里设 pin.provider / pin.model / pin.reasoningEffort

页签与面板需要 dsh.client 在启动时扫描进客户端模块图;加上主机侧的 ctx.inject 修复,重启一次应用后两者一起生效。面板如果显示「主机路由还不可用」,就是这一版还没被加载。

也可以直接用 HTTP(同样的数据,不需要页面):

# 1) 插件视角:当前 pin 的路由、每个对话分配到的 key、最近的决定与探测结果
curl -s http://127.0.0.1:19387/opencode-pin/status | jq

# 2) 现场探测若干候选 key,看各自会落到哪个上游
curl -s "http://127.0.0.1:19387/opencode-pin/probe?keys=check-a,check-b,check-c" | jq

# 3) 立刻重跑一轮 hunt(忽略缓存)
curl -s http://127.0.0.1:19387/opencode-pin/hunt | jq

# 4) 独立证据脚本:不依赖 Harness,直接看服务端路由行为
node scripts/evidence.mjs --keys 6

/opencode-pin/status 会给出 preferred(当前钉住的 key 与端点)、endpoints(累计见过的端点及次数)、assignments、recentDecisions、recentProbes。

6. 已知边界(重要)

  • 客户端无法指定端点:实测请求侧 x-opencode-endpoint-id 会被忽略,UA 也不影响路由。能影响路由的只有 session key,所以「钉」= 挑一个落在好端点上的 key 并复用,而不是点名某个上游。
  • 路由是服务端策略:端点的存在与默认落点会随时间变化(实测几分钟内默认端点就从 orcarouter 变到 novita)。被 key 黏住的绑定也会漂移,hunt 模式靠重新验证+重挑来应对,不是硬保证。
  • 探测会消耗极少额度:每个探测 max_tokens: 1、prompt 约 40 token。probe.prefer 为空时插件完全不发探测。
  • shared/hunt 会让所有对话共用路由身份,与官方「每会话一个稳定 session」的措辞不完全一致;换来的是上游不随对话漂移。二选一时官方建议优先 conversation。
  • 图片调用:plan 会检查目标模型是否支持图片;agent/request 阶段拿不到消息内容,所以把 pin.model 指到纯文本模型时,带图的主循环请求会被该模型按文本投影(DSH 行为)。默认目标 deepseek-v4.1-flash 支持图片。
  • 只对配置里 session.routes 的路由改写 session key:其他适配器(含官方 DeepSeek API 那条会按 sessionId 反查会话的扩展)保持真实会话身份。
  • 观测面需要 webServer:没挂 HTTP 服务时插件照常钉路由,只是没有 /opencode-pin/* 这三个端点。
  • 静态替代方案(不想装插件时):直接在 llm-pi-ai 的 opencode-go 路由上写死请求头即可全局恒定;pi-ai 发现已有该头就不再注入自己的值:
    opencode-go:
      headers:
        x-opencode-session: dsh-pinned
    
    代价是永远只有一个路由身份,且无法随端点变化重挑。

7. 开发

node --test test/*.test.mjs     # 27 项:决策逻辑、宿主接线、客户端页签冒烟
node scripts/evidence.mjs       # 打真实请求取证

目录:

index.js            宿主半:两个 waterfall 监听 + 观测路由
client.js           客户端半:设置→插件里的页签(Settings › Plugins,slot settings.plugins.tab)
lib/plan.mjs        纯决策函数(可单测,无 Harness 依赖)
lib/probe.mjs       凭据解析 + 1-token 路由探测
cordis.patch.yml    bundle 行(含默认配置)
locale/{zh,en}.json 插件卡片的显示名/描述
scripts/evidence.mjs 取证脚本
test/plan.test.mjs   纯决策单测
test/plugin.test.mjs 宿主半接线测试(假 ctx,含重派发防递归)
test/client.test.mjs 客户端半冒烟测试(React shim,无需浏览器)

8. 卸载

plugin_manager { action: "remove_bundle", target: "dsh-opencode-pin" }

Comments

Loading…