dsh-opencode-pin
Manifest validPin 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.
dsh-opencode-pin · 把 OpenCode Go 的上游路由钉住
一个 DeepSeek Harness(DSH)Host 插件:把所有模型调用钉在 OpenCode Go 套餐上,并保证每个请求都带 稳定的 x-opencode-session,从而让 OpenCode 的「会话 → 上游提供商」绑定在整段对话里不漂移。
源码: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 schema | cordis_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 并修复(见下),需再重启一次生效 |
两个踩过的坑(都已在代码里修掉)
agent/request必须prepend才能钉住路由。dsh-agent自己的"会话模型选择"监听器也答agent/request,它在外层await next()后用会话选择覆盖结果。插件原来注册在最内层,改写被吞掉,日志里表现为每步都muyuan -> opencode-go却钉不住;而且"会话选择 ≠ 最近 header"会被当成用户换模型,反复注入[model changed: …]通知。现在:{ global: true, prepend: true }+ 通过sessionController.selectForNextRequest()把钉住的路由写成本会话model/selection。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
三个关键事实:
-
同一个模型 id 由多个上游端点服务(
novita-deepseek、luminal-dsv4.1flash、orcarouter-dsv4.1flash…),x-opencode-upstream-model-id甚至分两种命名体系:deepseek-ai/...(官方权重那一路)与deepseek/...(第三方托管)。这就是「降智」的来源——落到哪个上游不由你选。 -
路由由
x-opencode-session决定,而且一旦分配就黏在那个 key 上:- 同一 key 连续请求 → 始终同一个端点;
- 换新 key → 落到「当前默认」端点(会随时间/负载整体变化);
- 官方文档明确要求:"Send a stable session ID in
x-opencode-sessionfor each conversation so we can optimize routing and prompt caching."
-
不带这个头,请求直接被拒:
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-a | luminal-dsv4.1flash | deepseek-ai/DeepSeek-V4.1-Flash(官方权重命名那一路) |
pin-6 / evidence-old | novita-deepseek | deepseek/deepseek-v4.1-flash |
pin-1 | orcarouter-dsv4.1flash | deepseek/deepseek-v4.1-flash |
| 任何新 key | 当前默认(观察期内从 luminal → novita → orcarouter 变过) | 随默认 |
两条关键规律:
- 绑定黏在 key 上,且长期稳定;换新 key 只会拿到"当前默认"。
- 所以想要非默认端点(如 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.1flash | deepseek-ai/DeepSeek-V4.1-Flash | chatcmpl-ad9b3b1c | HuggingFace 官方组织 deepseek-ai/ 的仓库命名 ⇒ 官方权重那一路 |
novita-deepseek | deepseek/deepseek-v4.1-flash | 32 位 hex | OpenRouter 风格 vendor slug ⇒ 第三方托管 |
orcarouter-dsv4.1flash | deepseek/deepseek-v4.1-flash | 56 位 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-account | DSH 账号授权(x-dsh-auth-token) | 登录即用;inferenceOrigin 默认 https://api.deepseek.com;失败以 ACCOUNT_QUOTA 呈现 |
deepseek-official | DEEPSEEK_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…