dsh-smoothly-opencode-session
Verified@karoc/dsh-smoothly-opencode-session · v0.2.1 · MIT
Smoothly OpenCode Session (Smoothly OCS) — 思磨力 OpenCode 会话头: external DeepSeek Harness host plugin that attaches the OpenCode-required x-opencode-session header to model calls routed to OpenCode / OpenCode Go provider routes (stable per-conversation id; f
Install
dsh plugin add @karoc/dsh-smoothly-opencode-session Confirm the layer applied with dsh --profile default --dump-config — see the install guide.
Source
- github/karoc/dsh-smoothly-opencode-session 0 0 archived
Tags
Readme
思磨力 OpenCode 会话头(Smoothly OCS)
Smoothly OpenCode Session (Smoothly OCS) — 一个外部 DeepSeek Harness
(DSH) 宿主插件:为路由到 OpenCode / OpenCode Go 提供方的模型请求自动附加
OpenCode 要求的 x-opencode-session 请求头,取值是稳定的按会话 id。
自 2026-09-05 起,OpenCode 的中继会拒绝任何缺少该头的推理请求
(400 MissingSessionID);该值同时把一段会话固定到同一上游后端,并让
OpenCode 的提示词缓存在该会话各轮之间保持命中(上游跟踪见
deepseek-harness discussion #5495)。
它做什么
- 修掉 400:凡是路由到允许 host 上的 OpenCode(Go) 提供方的请求,一律带上
x-opencode-session(由 host 门控判定,无需配置提供方列表)。 - 保住缓存/亲和收益:取值按会话唯一、且在该会话各轮、压缩、重试与进程重启
之间保持稳定(默认直接复用 DSH 会话 id——官方 DeepSeek 适配器已作为
x-deepseek-harness-session-id发送的同一个身份)。 - 其余一概不动:初始 URL 不在允许主机内的请求永不被修改(host 门控,默认
https://opencode.ai);已自带该头的请求、以及没有会话 id 的调用同样原样放行。 提供方是按目标 host 覆盖的、不是按名字,所以要把注入限制在特定路由上,需要显式写providers列表。见 hosts 与 providers。
工作原理
插件监听 llm/stream 瀑布——一个文档化的 DSH 扩展缝("围绕每次流式模型调用的
Waterfall")。LOOP 构建的请求 options 是深度冻结的(改写即抛错),所以不能靠改写
options 加头;插件改为:
- 从瀑布 options 上读取
provider+sessionId; - 把下游流的每次 pull 放进一个
AsyncLocalStoragestore 里执行; - 一次性 patch
globalThis.fetch,当有 store 处于激活时,把x-opencode-session: <值>合入出站请求(除非请求已自带该头——已有值永远优先)。
两处注册都是 fiber 作用域的(ctx.on 监听 + ctx.effect 清理函数),因此
停止 / 更新 / 卸载插件时会恢复原始 fetch 并移除监听。
为什么在 fetch 层注入
外部插件没有任何官方缝能给适配器请求逐条加请求头(options 深度冻结;provider 的
headers 配置是静态的、归属部署方)。fetch shim 是外部插件唯一能附加按会话值的
机制。正确的长期修复在提供方适配器本身(pi-ai);本插件只是它上线前的过渡方案。见
说明 / 限制。
配置
行的 config(写在插件的 cordis.patch.yml,或按 profile 覆盖)全部可选——
缺失的键由代码填默认值。
- insert:
- id: dsh-smoothly-opencode-session
name: '@karoc/dsh-smoothly-opencode-session'
config:
# providers: [opencode, opencode-go] # 可选收窄;不写 = 由 host 门控决定
mode: session-id
debug: false
providers
可选收窄——通常你根本不需要配这一项。 权威判据是请求的目标 host(见
hosts):不设 providers 时,凡目标 host 通过门控的提供方都被覆盖,因此自定义
路由键(例如 opencode-go-self)无需登记,被改名也不会静默失效。只有想把注入限制在
特定路由键上时才设它(届时 pi-ai 目录 id opencode / opencode-go 只是两个普通条目)。
匹配用的是路由键——提供方的显示名(例如界面上的 "OC Go" 标签)在这里永远看不到。设了列表
时,插件会在第一次模型调用把列表与实际注册的路由比对,缺哪个就告警一次(配置了
debugFile 时同样写入该文件——服务化部署可能不捕获控制台)。/ocgo 命令会打印生效策略
(门控 / 收窄 / mode / 已注册路由),无需任何客户端界面。
hosts
host 门控:只有目标主机在允许列表内(默认 ['https://opencode.ai'],含子域)的请求
才会被修改。条目可写 host(默认按 https 处理)或 scheme://host[:port];开头的
*. 会被忽略;IDN 条目会规范化为 punycode;不可用条目会在启动日志里报告而不是被
静默忽略,trim 后为空的条目不视为已提供——最终没有任何可用条目时,回到上面的默认
规则。['*'] 完全关闭门控——只对完全信任的镜像使用。如果你通过反向代理或镜像
访问 OpenCode,必须把该主机写进来(写全 scheme;裸 host 条目意味着 https),
否则头会静默不加;插件会按「提供方/host」对被挡下的情况各告警一次。
mode
session-id(默认)——头值 = 该次模型调用的 DSH 会话 id。每会话唯一, 跨轮次 / 压缩 / 重试 / 重启稳定。uuid——按 DSH 会话 id 派生的进程内稳定随机 UUID(不透明;进程重启后重置)。
debug / debugFile
debug: true——把每个进入注入流程的流式调用经ctx.logger(dsh 进程控制台) 打日志,并在请求级记录里显示原始值。debugFile: <绝对路径>——每次流式调用追加一行 JSON(kind: "stream",含provider/model/session/value);每个被注入或被 host 门控挡下的请求也各追加 一行(kind: "inject" | "skip",只要配了该文件就会写;debugRequests加的是控制台 那行,不是文件记录)。配置了但没有任何适配器注册的路由键会追加一条kind: "diagnostic"记录(见 providers)。流级记录只表示 "进入了注入流程",只有请求级记录能证明实际是否带上了头。流级记录携带原始 会话 id 与值(沿用 0.1.0 的格式以兼容),默认哈希的只有请求级记录。 该文件只追加、 不轮转(一次被注入的调用写两条:流级一条、请求级一条),且是 fire-and-forget—— 短命进程可能丢尾部记录。
debugRequests
debugRequests: true 在真实 fetch 时刻经 ctx.logger(dsh 进程控制台)为每条
请求级记录打一行日志;记录本身在配置了 debugFile 时追加进该文件(见上)。记录的
reason 取 session / discovery / host-not-allowed / already-present。被注入的
记录携带 {"ts","kind","reason","host","provider","valueHash","valueLen"};skip
记录只描述请求、不含指纹(ts/kind/reason/host/provider,URL 无法解析时没有
host),discovery 记录没有 provider(纯发现请求不属于任何路由)。值默认哈希:
只写值的 12 位十六进制 SHA-256 前缀;只有同时开 debug: true 才写原始值。
(上面单独说明的流级记录仍保留原始会话 id——见其注记。)
discoveryFallback
discoveryFallback: true(默认关)会为一个没有会话 id 的纯发现请求附加
进程内稳定 UUID:仅限 GET 且路径以 /models 结尾、且 host 通过门控的请求。
今天的 Models 页列表不需要它;只有当你的 OpenCode 端点开始拒绝该列表时才开启。
想在不改本包的情况下按 profile 覆盖配置:在 profile 自己的 cordis.patch.yml 里加一条
同 id 的 patch 条目(它整体替换 config,所以需要的键都要重写)。patch 条目用
id-targeted 形式,不是 insert——见 安装。
安装
兼容性: 本插件所需 seam(llm/stream、GenerateOptions.sessionId、commands 服务)最早出现的 dsh 版本是 0.1.0-rc.7——这就是声明的最低版本,依据是 seam 可用性,而不是对中间每个版本都跑过测试。该下限声明为对 @deepseek-ai/dsh-llm: ">=0.1.0-rc.7" 的可选 peer 依赖,DSH 会拿它对照自身运行时版本:不在区间内的运行时会拒绝加载——跳过该插件层,并打印 dsh plugin allow-version 的具体解法。这项检查本身自 dsh 0.1.7-rc.1 起才有,而所有带它的版本都已满足本下限,所以这条声明目前是写明契约而不是强制拦截;早于 0.1.7-rc.1 的 dsh 会照常加载本插件。peer 标记为 peerDependenciesMeta.optional,因为 dsh-llm 由宿主在运行时提供,npm 不会额外安装任何东西。区间里显式写出预发布号(>=0.1.0-rc.7 而非 >=0.1.0)是刻意的:面向正式版的区间不会匹配预发布运行时。
从 npm 安装(推荐)
dsh plugin --profile web add @karoc/dsh-smoothly-opencode-session
然后完全重启 dsh profile(bundle 层启动时才读取)。启动日志应出现:
[dsh-smoothly-opencode-session] active with mode session-id; provider narrowing: (none — the host gate decides)
[dsh-smoothly-opencode-session] host gate: https://opencode.ai
如果你从源码 checkout 运行 dsh,也可以作为 overlay 加载:
pnpm dsh web --patch ./cordis.patch.yml。
从 git 安装
dsh plugin --profile web add git+https://github.com/karoc/dsh-smoothly-opencode-session.git
更新
dsh plugin --profile web update @karoc/dsh-smoothly-opencode-session
之后重启 dsh web。
卸载
dsh plugin --profile web remove @karoc/dsh-smoothly-opencode-session
之后重启 dsh web。卸载是安全的:卸载时恢复原始 fetch,不影响其他提供方。
目录结构
src/index.ts 宿主插件:llm/stream 监听 + fetch shim
cordis.patch.yml bundle 层(插入带默认配置的插件行)
scripts/ 发布门禁 + 发布后校验
tests/ node --test 单元测试 + fake cordis 集成测试
lib/ 构建产物(npm 包入口)
构建与测试
npm install # 安装开发依赖(tsdown、@deepseek-ai/cordis 类型、@types/node)
npm run bundle # 产出 lib/index.js
npm test # node --test tests/*.test.ts(直接运行 TypeScript)+ 负向保证门禁
pnpm 同样可用(pnpm bundle、pnpm test);本仓库文档写 npm 路径,因为 pnpm 不总在
PATH 上。
说明 / 限制
- 范围:只有在
llm/stream调用内发出的请求会收到该头——判定依据是这个上下文, 不是 URL 路径。Models 页使用的一次性模型列表(GET <baseURL>/models)发生在该上下文之外, 因此默认不会收到该头(除非开启discoveryFallback)。 - 重定向不再过门控:门控判定的是初始请求 URL。Node 的 fetch 会跟随重定向,且 (在 Node 24 实测)跨 origin 保留自定义头——因此从允许主机开始、再跳到别处的请求, 会把该头带到重定向目标。
- 实现依赖:注入依赖 Node 的全局
fetch。如果未来某个 DSH 版本换掉网络栈, 该头会静默不再发送(400 复现)——届时卸载即可。这是提供方适配器(pi-ai)自身 发送该头之前的过渡方案。 - 不是官方修复:DSH 维护者的立场是提供方特殊性应归 pi-ai 包处理(见 discussion #5495 与 earendil-works/pi #9326)。本插件是它上线前的过渡方案——移除前请先走下面的 退役流程。
- 发现探测默认不受影响(没有会话 id 的请求原样放行)。
退役(可核验流程)
不要凭感觉退役本插件,按顺序核验:
- 读 DSH 实际加载的版本——pnpm 严格布局下真实路径是
<dsh 根>/packages/llm/llm-pi-ai/node_modules/@earendil-works/pi-ai/package.json(扁平布局<dsh 根>/node_modules/@earendil-works/pi-ai/package.json是备选;该包 不导出package.json,所以require.resolve找不到它)。定位不到就保持插件, 等下次 DSH 升级后再查。 - 若该版本
dist/里含x-opencode-session(grep -rl x-opencode-session <该包>/dist),判断它的注入是按提供方 id (opencode/opencode-go)还是按 base URL(opencode.ai)触发。 - 结局:按 base URL → 本插件已冗余,卸载;只按提供方 id → 自定义路由
opencode-go-self仍不被覆盖,要么继续用本插件,要么先把该路由迁到内置opencode-go。 - 没有新版本 / 仍未命中 → 继续用本插件;等 DSH 升了
@earendil-works/pi-ai依赖后 重跑本流程(npm view "@earendil-works/pi-ai@<声明的范围>" version可查该范围允许的 最新版本)。
许可证
MIT