Skip to content

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

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 加头;插件改为:

  1. 从瀑布 options 上读取 provider + sessionId;
  2. 把下游流的每次 pull 放进一个 AsyncLocalStorage store 里执行;
  3. 一次性 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 的请求原样放行)。

退役(可核验流程)

不要凭感觉退役本插件,按顺序核验:

  1. 读 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 升级后再查。
  2. 若该版本 dist/ 里含 x-opencode-session (grep -rl x-opencode-session <该包>/dist),判断它的注入是按提供方 id (opencode / opencode-go)还是按 base URL(opencode.ai)触发。
  3. 结局:按 base URL → 本插件已冗余,卸载;只按提供方 id → 自定义路由 opencode-go-self 仍不被覆盖,要么继续用本插件,要么先把该路由迁到内置 opencode-go。
  4. 没有新版本 / 仍未命中 → 继续用本插件;等 DSH 升了 @earendil-works/pi-ai 依赖后 重跑本流程(npm view "@earendil-works/pi-ai@<声明的范围>" version 可查该范围允许的 最新版本)。

许可证

MIT

参与贡献

见 CONTRIBUTING.md。