症状:DSH(DeepSeek Harness)把对话转发到 OpenCode Go 网关(
opencode.ai/zen/go/v1)时返回 HTTP 400 MissingSessionID,任何模型都发不出去。 结论:不是 Key 配错、也不是模型问题。OpenCode Go 从 2026-09-05 前后开始强制要求每个请求带x-opencode-session头,而 DSH 当前版本(直到 0.1.2-rc.1)都没有在出站请求里带这个头。装一个负责补头的社区插件、重启一次服务即可,零配置。
一、修复:两条命令
# 1) 给 DSH 的 web profile 装上插件(会自动追加进 bundle 层)
dsh plugin --profile web add "@gausszhou/dsh-opencode-session-id"
# 2) 重启 dsh,让插件随服务加载
重启命令取决于你 DSH 的部署方式,不要照抄网上到处写的 systemctl --user restart dsh-web:
| 部署方式 | 重启命令 |
|---|---|
系统级 systemd 服务(unit 在 /etc/systemd/system/) | sudo systemctl restart dsh-web |
| 用户级 systemd 服务 | systemctl --user restart dsh-web |
| 手动 nohup/setsid 起的孤儿进程 | 找到原启动参数重新拉起(`ps -ef |
插件默认配置即可工作,重启后日志里应能看到挂载信息:
[opencode-session-id] mounted: providers=[opencode, opencode-go]
headers=[x-opencode-session, x-session-affinity, x-client-request-id, x-session-id]
hosts=[opencode.ai] sessionId=nanoid8(alphanumeric)
二、根因:DSH 的适配器把这个头「故意」漏掉了
OpenCode Go 网关从 2026-09-05 左右开始,把 x-opencode-session 从「建议携带」升级成「强制要求」——缺了就拒绝路由:
{
"type": "error",
"error": {
"type": "MissingSessionID",
"message": "Request is missing x-opencode-session and cannot be routed efficiently. Please see https://opencode.ai/docs/go/#where-can-i-use-it"
}
}
DSH 这边其实是「接得住」这个能力的:它的 agent 循环会给每次对话填一个会话 ID(session-<uuid>),llm-pi-ai 适配器也会把这个 ID 转发到内部。但适配器在生成 provider 兼容配置时刻意不下发 sendSessionAffinityHeaders / sessionAffinityFormat,同时也丢掉了请求级的 options.headers——于是会话 ID 到了 HTTP 层就断了,请求空手出站。
我核对过:上游至今没有内置修复。当前最新版 @deepseek-ai/dsh@0.1.2-rc.1(2026-09-03 发布)的打包代码里搜不到任何 x-opencode-session / session-affinity 逻辑。所以短期内不是「升级 DSH」能解决的,装插件是目前最省事且可回滚的路。
三、插件做了什么
@gausszhou/dsh-opencode-session-id 从两个层面补上这个洞,且只动请求头:
- 监听
llm/stream这条官方拦截链路,用AsyncLocalStorage把每个流式调用按会话 ID 圈起来——多个会话并发时取到的 ID 也不会串; - 包装
globalThis.fetch,只对 host 匹配opencode.ai(默认)的请求补头。URL、方法、请求体、响应一律不动,不匹配的请求原样透传。
补的头默认是:
x-opencode-session—— 网关真正 key 的那个头;x-session-affinity/x-client-request-id/x-session-id—— 顺手带上,无副作用。
头值默认取会话 uuid 去掉 session- 前缀后做 SHA-256,再映射成 纯字母数字的 nanoid(8)(如 0RpJJnxJ)。同样的会话永远得到同样的 token,进程重启也不变,所以网关可以把多轮请求归到同一个会话。
四、验证:网关其实只查「有没有」
我用 Key 直接 curl OpenCode Go 的 /zen/go/v1/responses 做了对照:
| 请求 | 结果 |
|---|---|
不带 x-opencode-session | HTTP 400 MissingSessionID |
带 x-opencode-session: TESTdsh01(任意合法值) | HTTP 200,正常返回模型输出 |
关键结论:网关只校验「头存在且值格式合法」,并不校验会话是否真的注册过。所以插件派生出来的确定性 token 完全够用,装完即通。
五、排障方法与其他备注
- 先别怀疑 Key:这类错误发生在鉴权之后。如果 Key 真的无效会先收到 401
Missing API key。400MissingSessionID说明鉴权已过,纯粹是这个头的事。 - 改 profile 前先备份:
dsh plugin ... add会改写~/.dsh/profiles/<name>/package.json并把包追加进dsh.profile.bundles。动手前把package.json/cordis.patch.yml/ 锁文件拷一份到自己的备份目录,方便回滚。 - 想盯着每次注入看:在 profile 的
cordis.patch.yml里给id: opencode-session-id加config.verbose: true,重启后journalctl -u dsh-web -f就能看到每次请求实际补了什么头(会多打日志,验证完建议关掉)。 - Key 的存放:如果 DSH 以 systemd 跑但 unit 里没写 API Key,别在
/proc/<pid>/environ里找——DSH 启动时会把~/.dsh/.credentials.yaml读进进程内环境,运行后写入的 env 不会出现在 environ 快照里。
关联阅读:Claude Code 接入 OpenCode Go:可直接抄的配置(附踩坑清单) —— 同一网关,协议、认证头、Base URL 三处坑的对照表。