400 MissingSessionID 排障实录:OpenCode Go 会话路由、DSH 插件补丁与双环境落地
一句话摘要:OpenCode Go 要求每个推理请求携带稳定的 x-opencode-session 头用于路由与提示词缓存,而 DeepSeek Harness(DSH)≤ 0.1.2-rc.1 在所有适配器路径上都不发这个头;本地装一个按会话注入 header 的插件即可修复。但"装上了却没生效"往往不是插件的问题——DSH 存在多套互相独立的数据目录(DSH_HOME),插件可能装到了 Desktop 永远读不到的那一套里。
1. 问题现象:一请求就 400
在 DeepSeek Harness 里切换到 OpenCode Go 模型后,所有推理请求直接失败,返回:
400: {
"type": "MissingSessionID",
"message": "Error from provider (Console Go): Request is missing x-opencode-session and cannot be routed efficiently. Please see https://opencode.ai/docs/go/#where-can-i-use-it"
}
特征:
- 不是凭据问题:API key 有效、模型列表能拉到(GET https://opencode.ai/zen/go/v1/models 正常);
- 不是偶发:100% 复现,任何 opencode-go 模型(opencode-go/<model-id>)都失败;
- 报错即指路:OpenCode Go 官方文档明确要求客户端"为每个会话发送稳定的 x-opencode-session"(见 Where can I use it?)。
2. 背景分析:为什么必须带这个头?
2.1 OpenCode Go 是什么
OpenCode Go 是 OpenCode 推出的 $10/月的订阅,以接近批发成本的价格提供一批经过选型与压测的开源编码模型(Grok、GLM、Kimi、Qwen、DeepSeek V4、MiMo、MiniMax 等),主要面向国际用户并提供稳定访问。它本质上是"又一个 provider",只是对请求形态有约束:
2.2 缺失 session id 的两种代价
| 显式失败 | 不带 header → 400 MissingSessionID | 全部请求不可用 |
| 隐式劣化 | 带了但 id 不稳定/全局共用 | 无报错,但路由不优、提示词缓存全部落空 |
第二种情况下钱是实打实多花的:以 DeepSeek V4 Flash 为例,官方价格 输入 $0.22/1M、缓存读取仅 $0.007/1M(约 3%);GLM-5.3-Flash 输入 $0.15 vs 缓存读取 $0.03。会话 id 不稳定 = 每次请求都按全价输入计费。所以"把 id 修对"不只是过网关,还直接影响成本与延迟。
2.3 为什么 DSH 会缺这个头
OpenCode 官方文档的 Known Problematic Clients 表里赫然列着:
DeepSeek Harness — Session information arrives on some model paths, but is missing on others. We recognize its native header; the remaining work is to send it across all adapters. Discussion #5495
即:DSH 部分模型路径带会话信息、其他路径不带;上游已识别 DSH 原生头,但尚未在所有适配器路径统一发送(社区插件作者记录为 2026-09-05 起 OpenCode Go 开始强制执行,而 DSH ≤ 0.1.2-rc.1 未覆盖)。修复在 upstream 发布前,需要本地补丁。
2.4 候选方案的取舍
| settings.yaml 静态 headers | ✅ | ❌ 所有会话共用一个 id → 缓存落空、变慢变贵 | ✅ | — | — |
| 全局 opencode_zen profile(第三方 custom-header 插件) | ✅ | ✅ | ❌ 对所有主机改写 UA + x-opencode-* | ❌ | 第三方插件 + 客户端 bundle |
| dsh-opencode-session-header 插件 | ✅ | ✅ 直接用 DSH 真实会话 id | ✅ 仅 opencode.ai | ✅ 改 JSON 立即生效 | 无 |
结论:静态 header 治标不治本(缓存经济学上亏);全局改写会污染其他 provider(包括自建网关的抓包排查);按会话、按域名白名单注入的专用插件是正确解。
3. 解决方案:按会话注入 x-opencode-session
3.1 插件机制
dsh-opencode-session-header(npm: dsh-opencode-session-header,零依赖)在 fetch 传输层做手脚,不触碰任何模型适配器代码:
llm/stream 流式迭代
│ 每个流迭代包在 AsyncLocalStorage 里,携带 GenerateOptions.sessionId
│ (稳定:同一会话/分支/子代理共用;换会话/分支/fork 则新 id)
▼
fetch middleware 链(vendored 自 pi-fetch-pipeline, MIT,用独立 Symbol 命名空间)
│ host 命中白名单(默认 opencode.ai + 子域)→ 盖上 x-opencode-session
│ 其他 host → 字节级原样放行
▼
底层 globalThis.fetch
三个关键设计:
- 会话语义正确:直接用 DSH 的会话 id(fresh per conversation / fork / subagent),保证 prompt-cache 命中;
- 作用域严格:默认只对 opencode.ai 生效,其他 provider、内网网关、抓包代理流量完全不变;
- 运行时开关:状态文件 <DSH_HOME>/plugins/dsh-opencode-session-header.json,{"enabled": false} 即关,每次 LLM 请求前重读、无需重启;文件缺失 = 启用。兜底 id 为 dsh-default。
3.2 安装(以 CLI 环境为例)
dsh plugin –profile web add dsh-opencode-session-header
# 或显式指定版本 / 本地目录
dsh plugin –profile web install "file:/path/to/dsh-opencode-session-header"
装完手动重启一次 dsh web(bundle 层启动时合成),启动日志应出现加载行;之后正常发请求即可。
3.3 真实排障案例:装上了,但 Desktop 就是加载不到
这是本博客最有踩坑价值的一段。现象:~/.dsh/profiles/web/node_modules/dsh-opencode-session-header 明明存在,DSH Desktop 却始终不加载。
根因:两套完全独立的 DSH 环境。
| dsh 本体 | /usr/local/lib/node_modules/@deepseek-ai/dsh(v0.1.2-rc.1) | /Applications/DSH Desktop.app(内置 v0.1.2-alpha.1) |
| DSH_HOME | ~/.dsh | ~/Library/Application Support/dsh-desktop/harness |
| web profile | ~/.dsh/profiles/web | <DSH_HOME>/profiles/web |
证据链(Desktop 自身日志 ~/Library/Logs/DSH Desktop/harness.log):
[stdout] [harness-node] DSH_HOME=/Users/hello/Library/Application Support/dsh-desktop/harness
[stdout] [harness-node] loading=/Applications/DSH Desktop.app/Contents/Resources/app/node_modules/@deepseek-ai/dsh/lib/bin.js
而 Desktop 的 profiles/web/package.json 里:dependencies 没有该插件、dsh.profile.bundles 没有、cordis.patch.yml 没有 insert、node_modules grep 为空——插件从未被装进 Desktop 的 profile。它不是"加载失败",是"无物可载"。CLI 的 ~/.dsh 对它来说形同虚设。
修复要点:必须用 Desktop 自己的工具链装进 Desktop 自己的 DSH_HOME。
export DSH_HOME="$HOME/Library/Application Support/dsh-desktop/harness"
export PATH="$DSH_HOME/.desktop-bin:$PATH" # Desktop 的 node/pnpm shim
"$DSH_HOME/.desktop-bin/node" \\
"/Applications/DSH Desktop.app/Contents/Resources/app/node_modules/@deepseek-ai/dsh/lib/bin.js" \\
plugin –profile web install dsh-opencode-session-header
plugin install 会在 pnpm 成功后自动把声明了 dsh.bundle.patch 的包追加进 dsh.profile.bundles,于是它成为真正的 profile 层。
过程中踩到的三个次生坑:
验证三连(避免"以为好了"):
# 1) 组合配置树里出现该 entry:
dsh web –dump-config # → # == dsh-opencode-session-header
# – id: dsh-opencode-session-header
# name: dsh-opencode-session-header
# 2) 启动日志无报错:grep -E "failed to load|entry did not activate|Failed to load plugins"
# 3) 模块冒烟:desktop node -e "import('dsh-opencode-session-header')…" 打印 exports 齐全
4. 场景分类(决策速查)
同一个问题在不同形态下的处置差异很大,归纳为四维:
维度 A:按报错形态
| A1 显式 400 | 所有 opencode-go 模型请求返回 MissingSessionID | 装注入插件;确认 hosts 白名单含 opencode.ai、开关文件未禁用 |
| A2 隐式劣化 | 请求成功,但 id 每次/每会话变化或全局共用 | 无报错也要查:对照官方 Validated Clients 表,确保自己的客户端路径发送稳定会话 id;观察缓存命中率与账单 |
| A3 部分路径失败 | 有的模型路径带 session、有的不带(DSH 当前状态) | 这正是 upstream Discussion #5495 跟踪的问题;发布前用插件在传输层兜底 |
维度 B:按运行载体(DSH_HOME 决定一切)
| 纯 CLI | ~/.dsh | dsh plugin –profile web add <pkg> | 无 |
| DSH Desktop | ~/Library/Application Support/dsh-desktop/harness | 必须用 Desktop 内置 shim + bin.js + 指向其 DSH_HOME;或 App 内插件入口/市场 | 最容易中招:把 ~/.dsh 当共享目录 |
| 打包可执行 / 多用户机器 | 随启动参数 | 注意模块后备机制差异(普通 Node 用 symlink,pkg/Electron 用 ESM 代理,见 .dsh-module-fallback);关注文件属主而非只看"文件存在" |
维度 C:按安装来源
| npm registry | plugin … add <name> | 版本未锁定则写 ^x.y.z;官方提示暂用 install –no-frozen-lockfile 与 pnpm 10 不兼容 |
| 本地/仓库目录 | plugin … install "file:/abs/path" 或相对路径 | pnpm 以 profile 目录为 cwd,相对路径会按调用目录锚定(anchorPathSpec) |
| 插件市场(dshmarket) | App 内入口 | Desktop 场景注意 allowRestart: false 补丁;商业场景慎用自动重启 |
维度 D:按治理/安全边界
| header 作用域 | 只对白名单域名注入(默认 opencode.ai),避免把 x-opencode-session/UA 改写到自建网关或其他 provider |
| 会话语义 | 每会话稳定、fork/子代理刷新新 id;不要用全局固定值(缓存失效 + 路由不优) |
| 灰度开关 | 用运行时开关 JSON,改文件即生效、免重启,故障时可秒级关闭 |
| 多环境漂移 | CLI 与 Desktop 各装各的;用 –dump-config、启动日志、模块冒烟做三方验证,而不是"目录里看到了文件" |
5. 复盘:三条经验
参考链接
- OpenCode Go 官方文档:https://opencode.ai/docs/go/(会话要求见 #where-can-i-use-it)
- 上游跟踪:DeepSeek Harness Discussion #5495
- 插件:dsh-opencode-session-header(MIT,零依赖)




