Skip to content
This repository was archived by the owner on Nov 9, 2026. It is now read-only.

Releases

v0.9.0-rc.2 — messages 端面不再被一条畸形历史打死 + 图片边界说法纠错 (preview)

Choose a tag to compare

@xia-sc xia-sc released this 16 Sep 05:25

v0.9.0-rc.2 — messages 端面不再被一条畸形历史打死 + 图片边界说法纠错

Pre-release(预览版)。修掉 v0.9.0-rc.1 之后发现的一处「一条持久历史打死整会话」
(#3 同一类),并订正上一版对图片
体积的描述。

修复

  • messages 端面:历史里的畸形 tool-call 不再让之后每一轮都失败。
    持久化的 assistant tool-call 只要 arguments 不是合法 JSON,toAnthropicMessages
    就抛 PROVIDER_PROTOCOL_ERROR,于是该会话在 messages 系模型上之后每一轮都失败,
    且错误里不说是哪一条历史。触发者甚至不一定是本插件:chat / responses 两个端面把
    arguments 原样透传(线上本来就是字符串),所以一条畸形调用在 chat 端面落盘后,
    要等会话切到 messages 系模型才引爆。
  • 降级而不是拒绝:这个调用**早就执行过、也有 tool_result**了,模型只需要看见
    「它当时调过这个工具」,所以拒发它毫无收益、代价却是整段会话。新增 toolInput(),
    历史 input 一律退成 {}:
    • 非法 JSON(not-json{、空串);
    • 能解析但不是对象——"5" / "[1]" / "null" / '"text"'。Messages API 要求
      input 是对象,所以这类与非法 JSON 一样会被对端拒绝,而上一版是原样发出去的,
      表现为一条看不出问题来源的上游 400。
  • 创作侧保持严格:pumpMessages 对刚刚流出来的调用仍然抛错。参数不是 JSON 是
    上游违反协议,一个无法执行的调用不该被保留或派发——宿主的 max-tokens 与中断规则也是
    这个取向(宁可丢掉被截断的 tool-call)。新增断言锁住这个方向,防止以后「统一成宽容」
    把两边都改掉。
  • 宿主自己也容忍这种畸形(dsh-agent-loop 的工具参数解析器保留原文、dsh-llm-pi-ai
    的回放解析器退成 {}),只有 dsh-llm-deepseek 的 messages 实现选择抛错——而它没有
    跨端面回放这一层要照顾。

文档

上一版 notes 与 README 把图片体积说大了,本版改准(只改说法,没有改行为):

  • 单图大小的真实边界是 attachment 的准入归一化,不是本插件:本部署在准入时就把每张图
    压到 ≤ 4 MiB / ≤ 2048×2048 像素(单边 ≤ 8192;上传侧单图 ≤ 20 MiB、单消息 ≤ 200 MiB),
    而 imageHostPath 给的就是那份归一化副本——所以本插件从不内联原始上传。
  • 插件自己那条 20 MiB 单图检查只对不做归一化的 attachment provider 生效,是兜底;
    真正管用的请求级保护是 maxRequestImageBytes,而且它按张数才会撞线:归一化后单图
    base64 约 5.4 MiB,64 MiB 预算约合十余张同请求。
  • lib/models.js 的常量注释、README.md / README.en.md 的多模态与
    maxRequestImageBytes 条目、AGENTS.md(约定 13 补边界事实、新增约定 14 记下
    「回放降级、创作严格」)同步更新。

验证

  • node test/smoke.mjs:56 项,55 pass + 1 skip(无 key 的活探针)。本版新增:
    6 种畸形历史载荷(非法 JSON、空串、数组、null、数字、字符串)都退成 {} 且
    id / name 照旧回放;以及 pumpMessages 对刚流出的非 JSON 调用仍然抛
    PROVIDER_PROTOCOL_ERROR。
  • node test/host-compat.mjs:8 项 8 pass,0 skip(真 dsh-llm + 真流语法不变量)。
  • npm run build:client:客户端半本版没有改动,lib/client.js 与 src/client/* 仍同步
    (测试里有断言守卫),所以浏览器侧无需刷新也不必重启。
  • 无联网断言:本版改动是纯转译层,不涉及上游行为,故未消耗额度做真机探针。

已知 / 未覆盖

  • 消息端面的 tool-call 参数只做「可发送」保证,不保证语义保真:input 降级成 {} 后,
    模型看到的是「调过这个工具」但参数为空。这是刻意的取舍(pi-ai 同款):保住会话
    可用性优先于历史参数保真,而真正的结果在紧随其后的 tool_result 文本里。
  • imageRequestPricing 仍未实现:图片 token 计入走 token meter 的启发式估值(不报错,
    只是不够准)。上游没有公布视觉计费口径,编一个公式会比不实现更差——token meter
    一旦拿到定价就整体替换掉自己的启发式。
  • 仍不参与 attachments.readImageRequest 的请求版降采样(那需要另一组只能靠猜的像素/字节
    目标,且有重编码的画质取舍);因此请求体侧唯一的保护仍是 maxRequestImageBytes。
  • 上游网关的请求体上限未公布,maxRequestImageBytes 的默认值是部署取值而非实测值。
  • 目录里没有 .github/,没有 CI;门禁就是这两个测试入口 + 构建脚本的语法检查。

升级

dsh plugin --profile web add github xia-sc/dsh-opencode-go
# 重启 dsh web(宿主侧改动必须重启才生效;客户端半本版没变)
  • 运行环境下限仍是 DeepSeek Harness 0.1.6-alpha.1(peer 依赖 ^0.1.6-alpha.1)。
  • 无需改任何配置:本版没有新增或改动配置字段,maxRequestImageBytes 的默认值不变。
  • 如果你的会话里有 messages 系模型(minimax / qwen)一直报
    stored tool-call arguments are not valid JSON,本版之后它就会正常继续——不需要清历史。

v0.9.0-rc.2 — the messages surface is no longer killed by one malformed history entry + image-bound corrections

Pre-release. Fixes another instance of "one durable history entry kills the session" (the same
class as #3) found after
v0.9.0-rc.1, and corrects the previous release's description of image sizes.

Fixes

  • messages surface: a malformed tool call in history no longer fails every later turn.
    Whenever a persisted assistant tool-call had arguments that were not valid JSON,
    toAnthropicMessages threw PROVIDER_PROTOCOL_ERROR, so the conversation failed on every
    later turn
    with a messages-surface model, in an error that named no history entry. The
    culprit need not be this plugin: the chat and responses translations pass arguments
    through untouched (the wire takes a string), so a malformed call persisted on one of those
    surfaces only detonates once the session is switched to a messages-surface model.
  • Degrade instead of refusing. The call already ran and already has its tool_result; the
    model only needs to see that it made the call, so refusing to re-send it bought nothing and
    cost the whole conversation. A new toolInput() maps a historical input to {} for:
    • unparseable JSON (not-json{, the empty string);
    • anything that parses to a non-object — "5", "[1]", "null", '"text"'. The
      Messages API requires input to be an object, so these failed exactly as hard, while the
      previous release sent them verbatim: an upstream 400 naming no source.
  • Creation stays strict. pumpMessages still throws for a freshly streamed call whose
    arguments are not JSON. That is an upstream protocol violation, and an unexecutable call
    must not be retained or dispatched — the harness's own max-tokens and interruption rules take
    the same line (dropping truncated tool calls rather than keeping them). A new assertion locks
    that direction against a future "let us be tolerant everywhere" edit.
  • The harness itself already tolerates this malformation (dsh-agent-loop's tool-argument
    parser keeps the raw text, dsh-llm-pi-ai's replay parser returns {}); only the
    dsh-llm-deepseek messages implementation throws, and it has no cross-surface replay to
    worry about.

Documentation

The previous release's notes and the README overstated image sizes. This release corrects the
wording (no behavior change):

  • The real bound on one image is the attachment store's admission-time normalization, not
    this plugin
    : this deployment reduces every image to <= 4 MiB and <= 2048x2048 px (<= 8192
    per side; uploads are capped at 20 MiB each and 200 MiB per message), and imageHostPath
    returns that normalized copy — so nothing here ever inlines the original upload.
  • The plugin's own 20 MiB per-image check therefore only fires for an attachment provider that
    does no normalization: it is a fallback. The guard that matters is maxRequestImageBytes,
    and it triggers by image count — a normalized image is ~5.4 MiB once base64-encoded, so
    the 64 MiB default is roughly a dozen in one request.
  • Updated together: the constant's comment in lib/models.js, the multimodal and
    maxRequestImageBytes entries in README.md / README.en.md, and AGENTS.md (rule 13
    gains the bounds; new rule 14 records the replay-degrades / creation-strict asymmetry).

Verification

  • node test/smoke.mjs: 56 tests, 55 pass + 1 skip (the key-gated live probe). New: six
    malformed history payloads (invalid JSON, empty, array, null, number, string) all degrade
    to {} while id / name still replay; and pumpMessages still rejects a freshly
    streamed non-JSON call with PROVIDER_PROTOCOL_ERROR.
  • node test/host-compat.mjs: 8 tests, 8 pass, 0 skip (real dsh-llm + real stream
    invariant).
  • npm run build:client: the browser half is unchanged in this release and
    lib/client.js stays in sync with src/client/* (asserted by the suite), so no page
    refresh and no client-side restart is needed.
  • No live probe: the change is pure translation and involves no upstream behavior, so no quota
    was spent.

Known / not covered

  • Tool-call arguments are only guaranteed sendable, not semantically faithful: degrading
    input to {} tells the model that the call happened with no arguments. That is a
    deliberate tradeoff (same as pi-ai) — keeping the conversation usable outranks history
    fidelity, and the real outcome lives in the tool_result text that follows.
  • imageRequestPricing is still unimplemented: image tokens are priced by the token meter's
    heuristic (no failure, just less exact). The operator publishes no vision pricing, and
    inventing a formula would be worse than not implementing it — the meter replaces its own
    heuristic wholesale once pricing exists.
  • Still no attachments.readImageRequest request-version downscaling (it needs another set of
    pixel/byte targets that could only be guessed, plus a re-encode fidelity tradeoff), so
    maxRequestImageBytes remains the only request-body guard.
  • The upstream gateway's request-body limit is not published, so maxRequestImageBytes's
    default is a deployment choice, not a measured value.
  • There is no .github/ and no CI: the gate is these two test entries plus the build script's
    syntax check.

Upgrade

dsh plugin --profile web add github xia-sc/dsh-opencode-go
# restart dsh web (host-side changes need a restart; the browser half is unchanged)
  • The runtime floor is still DeepSeek Harness 0.1.6-alpha.1 (peer ^0.1.6-alpha.1).
  • No configuration change: no new or altered config field, and maxRequestImageBytes keeps its
    default.
  • If a session with a messages-surface model (minimax / qwen) kept failing with
    stored tool-call arguments are not valid JSON, it simply continues after this release — no
    history cleanup needed.

v0.9.0-rc.1 — 适配 harness 0.1.6:adapter 侧图片 offload + 端面/档位收敛 (preview)

Choose a tag to compare

@xia-sc xia-sc released this 16 Sep 03:09

v0.9.0-rc.1 — 适配 harness 0.1.6:adapter 侧图片 offload + 端面/档位收敛

Pre-release(预览版)。本版是兼容性版本:DeepSeek Harness 0.1.6-alpha.1 把图片
卸载(image offload)的职责从 runtime 挪进了 adapter,本插件原来两边都没实现,于是
「已经为塞进请求上限而丢掉的图片」会被重新内联、超限请求也不会走 harness 的卸载回路。
同时补上两处「卡上能填、请求不生效」的缺口。下限抬到 0.1.6,见文末「升级」。

修复

  • 图片 offload 契约(0.1.6 新增的硬契约):
    • 已标记 offloaded 的图片永远按占位文本发送,在所有读盘之前就投影好。丢图是
      harness 的持久会话决定(dsh-compaction-image-offload 会把它记进会话历史),
      路由无权撤销——原来的行为等于把已经丢掉的字节又塞了回去。
    • 超预算时抛 IMAGE_OFFLOAD_REQUIRED(带 offloadImages),而不是自己丢图。
      harness 据此记录选择并重试该步;自己丢图则没有任何记录,下一次请求又会把图带回来。
    • 判定一定发生在发请求之前,且不读盘:账目用 durable ref 自带的 bytes。
  • peer 区间修正:6 个 @deepseek-ai/dsh-* 从 ^0.1.5-rc.1 抬到 ^0.1.6-alpha.1。
    旧区间在 semver 的预发布规则下根本不匹配 0.1.6-alpha.1(区间只有在同一 tuple 上
    才接受预发布),所以新装会把 dsh-llm 解析成 0.1.5-rc.x,而代码现在 import 的是
    0.1.6 才有的 API。
  • 设置段被拒不再只有日志:被拒的段会被整段丢弃、adapter 沿用组合层默认值,于是卡片
    的勾选数与实际路由各说各话且用户看不到原因。models/known / models/refresh 的
    成功回包现在附带 configError,卡片在模型列表上方显示本地化标题 + 宿主原文。

新功能

  • maxRequestImageBytes(默认 64 MiB):单请求内联图片总量上限,按进请求体的
    base64 字节
    计。上游没公布请求体上限,所以这是部署取值——默认远高于日常截图流量,
    代理有更小的 body cap 就往下调。

  • 端面与档位一起生效:宿主把 surface 与 efforts 当成一个组合校验,所以卡片
    改端面时在同一次设置写入里就把档位收敛好(新增 setModelCapFields 原子多字段写):

    • 新端面能带走的档位保留,带不走的去掉,并明确提示去掉了哪些;
    • 切到 messages 这类没有档位词汇的端面 → 整体清空;
    • 交回「跟随默认」→ 按该模型自身的默认档位表收敛(defaultEfforts 由宿主给出:
      未归类模型跟随 chat 端面、但本身不提供任何档位,所以诚实的答案是档位列表而不是端面)。

    分两次提交这两个字段,正是当初能写出 surface: responses + chat 才有的 max 这种
    被拒组合的原因。

验证

  • node test/smoke.mjs:55 项,54 pass + 1 skip(无 key 的活探针)。本版新增:
    maxRequestImageBytes 的默认/校验/传导;RPC configError 的三种情形;offloaded 图片
    不读盘、占位文本进请求体;超预算在 dispatch 之前抛错且 offloadImages 计数正确;
    卡片端面收敛的三组断言(部分保留 / 无词汇清空 / 跟随默认收敛 / 完全可容纳时不动),
    并逐字比对落盘的 modelCaps 载荷与写入次数。
  • node test/host-compat.mjs(新增测试入口):8 项 8 pass,0 skip。它从
    node_modules 直接 import 真实的 @deepseek-ai/dsh-llm / dsh-invariants / cordis,
    起真 cordis app 走真 llm 服务与真流语法不变量(只打桩 connection / webServer /
    settings / credentials / attachments / fs)。smoke.mjs 把宿主面全打桩,看不到
    宿主收紧契约
    ——0.1.6 这次搬迁正是只有这一层能发现。两条缺陷断言先以 { skip } 写就,
    修复后去掉 skip 即成为验收测试。
  • npm run build:client:lib/client.js 与 src/client/* 同步(测试里有断言守卫)。
  • 真机 GUI 核对(隔离 DSH_HOME,未改动用户配置):把 deepseek-flash 的端面从
    responses 切成 chat —— 档位由 minimal/low/medium/high/xhigh 自动收敛为
    low/medium/high,提示「已去掉 minimal / xhigh」,落盘为合法组合,横幅不出现。
  • 真机宿主核对:/zen-go-rpc 的 models/known、usage/summary 在 0.1.6-alpha.1 上
    均为 200;models/known 能回报被拒段的原文原因。

已知 / 未覆盖

  • messages 端面:持久化的 tool-call arguments 不是合法 JSON 时仍抛
    PROVIDER_PROTOCOL_ERROR(与 #3 同类的「一条历史打死整会话」)。它有现存测试刻意断言
    (宁可响也不降级),本版未改动。
  • 未实现 imageRequestPricing:图片 token 计入走 token meter 的启发式估值(不报错,
    只是不够准)。上游未公布视觉计费口径。
  • 本插件读的是原始图片字节,不经过 attachments.readImageRequest 的请求版降采样,因此
    maxRequestImageBytes 是唯一的请求体保护;harness 侧 imagePixelBudget 那类按像素
    生成的请求版本本插件不参与。
  • 上游网关的请求体上限未公布,maxRequestImageBytes 的默认值是部署取值而非实测值。
  • 目录里没有 .github/,没有 CI;门禁就是这两个测试入口 + 构建脚本的语法检查。

升级

dsh plugin --profile web add github xia-sc/dsh-opencode-go
# 重启 dsh web(宿主侧改动必须重启才生效)
  • 运行环境下限抬到 DeepSeek Harness 0.1.6-alpha.1。装在 0.1.5 系宿主上时,pnpm
    会报 peer 警告(本插件 import 了 0.1.6 才有的 projectOffloadedImages /
    requiredImageOffload / IMAGE_OFFLOAD_REQUIRED),所以不要降级宿主。
  • 想给图片留更大余量(或让代理更早拦截),在 llm-opencode-go 段设
    maxRequestImageBytes;默认 64 MiB。
  • 如果你的 settings.yaml 里 modelCaps 的 surface 与 efforts 不匹配(例如
    surface: responses 配了 chat 才有的 max),该段会被整段拒绝:现在卡片会直接告诉你
    原因,点开该行「参数」改一次端面即自动收敛。

v0.9.0-rc.1 — harness 0.1.6 compatibility: adapter-owned image offload + surface/level conformance

Pre-release. This is a compatibility release: DeepSeek Harness 0.1.6-alpha.1 moved
image offloading out of the runtime and into the adapter. This plugin implemented neither
half, so occurrences the harness had already dropped for size travelled as bytes again, and
an over-budget request never took the harness's offload-and-retry path. Two
"the card accepts it, the request ignores it" gaps are closed as well. The floor rises to
0.1.6
— see Upgrade at the end.

Fixes

  • The image-offload contract (new in 0.1.6, and mandatory):
    • Occurrences marked offloaded always travel as placeholder text, projected before
      any read. Dropping images is the harness's durable session decision
      (dsh-compaction-image-offload records it in the transcript); a route has no business
      reversing it by re-inlining the bytes.
    • An over-budget request fails with IMAGE_OFFLOAD_REQUIRED (plus offloadImages)
      instead of dropping images locally. The harness records that selection and retries the
      step; a local drop records nothing, so the next request carries the images again.
    • The budget is decided before dispatch and without I/O: accounting uses the durable
      ref's own bytes.
  • Peer range corrected: all six @deepseek-ai/dsh-* peers move from ^0.1.5-rc.1 to
    ^0.1.6-alpha.1. The old range never matches 0.1.6-alpha.1 under semver's prerelease
    rule (a range admits a prerelease only on its own tuple), so a fresh install resolved
    dsh-llm 0.1.5-rc.x while the code imports 0.1.6-only APIs.
  • A refused settings section is no longer log-only: a rejected section is dropped whole
    and the adapter keeps the composition default, leaving the card's selection count and the
    request path each telling their own story, with no visible reason. models/known and
    models/refresh now carry configError, and the card shows it above the model rows.

Features

  • maxRequestImageBytes (64 MiB by default): the inline image budget for one request,
    counted in the base64 bytes the body carries. The operator publishes no request-size
    limit, so this is a deployment value — the default sits far above ordinary screenshot
    traffic; lower it toward a proxy's body cap.

  • Surface and levels take effect together: the host validates surface and efforts
    as a pair, so the card conforms the levels in the same settings write that changes
    the surface (new atomic multi-field setModelCapFields):

    • levels the new surface can carry are kept, the rest are dropped and named in a notice;
    • a surface with no level vocabulary (messages) clears them;
    • handing the choice back to default reconciles against that model's own default level
      list
      (defaultEfforts, supplied by the host: an unclassified id follows the chat
      surface yet offers no levels, so the honest answer is the level list, not the surface).

    Committing the two fields apart is exactly what allowed the refused
    surface: responses + chat-only max pair in the first place.

Verification

  • node test/smoke.mjs: 55 tests, 54 pass + 1 skip (the key-gated live probe). New
    coverage: maxRequestImageBytes defaults/validation/plumbing; the three configError
    cases; offloaded occurrences never read back and their placeholder reaching the body; an
    over-budget request failing before dispatch with the right offloadImages count; and three
    card beats for the surface/level reconciliation (partial keep / no vocabulary / follow the
    default / fully compatible left untouched), comparing the persisted modelCaps payload
    byte for byte along with the write count.
  • node test/host-compat.mjs (new test entry): 8 tests, 8 pass, 0 skip. It imports
    the real @deepseek-ai/dsh-llm / dsh-invariants / cordis from node_modules and drives
    the real llm service plus the real stream-grammar invariant (only connection /
    webServer / settings / credentials / attachments / fs are stubbed). smoke.mjs
    stubs every host surface and cannot see the host tightening a contract — which is the
    only way this 0.1.6 move was discoverable. Its two defect assertions were written as
    { skip } cases first and unskipped as the fix's acceptance test.
  • npm run build:client: lib/client.js stays in sync with src/client/* (asserted by the
    suite).
  • Live GUI check (isolated DSH_HOME; the user's config was not modified): switching
    deepseek-flash from the responses surface to chat conformed its levels from
    minimal/low/medium/high/xhigh to low/medium/high, reported "dropped minimal / xhigh",
    persisted a valid pair, and left no refusal banner.
  • Live host check: /zen-go-rpc models/known and usage/summary both answer 200 on
    0.1.6-alpha.1, and models/known reports the exact reason for a refused section.

Known / not covered

  • messages surface: a persisted tool-call arguments that is not valid JSON still throws
    PROVIDER_PROTOCOL_ERROR (the same class as #3, "one history entry kills the session").
    Existing tests assert that deliberately (loudly rather than degrade); unchanged here.
  • imageRequestPricing is not implemented: image tokens are priced by the token meter's
    heuristic (no failure, just less exact). The operator publishes no vision pricing.
  • This plugin inlines raw image bytes rather than going through
    attachments.readImageRequest's request-version downscaling, so maxRequestImageBytes is
    the only request-body guard; the harness-side imagePixelBudget style request versions are
    not involved.
  • The upstream gateway's request-body limit is not published, so
    maxRequestImageBytes's default is a deployment choice, not a measured value.
  • There is no .github/ and no CI: the gate is these two test entries plus the build
    script's syntax check.

Upgrade

dsh plugin --profile web add github xia-sc/dsh-opencode-go
# restart dsh web (host-side changes only take effect on restart)
  • The runtime floor rises to DeepSeek Harness 0.1.6-alpha.1. Installed beside a 0.1.5
    host, pnpm will warn about the peer: this plugin imports projectOffloadedImages /
    requiredImageOffload / IMAGE_OFFLOAD_REQUIRED, which exist ...
Read more

v0.8.9-rc.2 — 未知模型可手动归类 + 参数面板 (preview)

Choose a tag to compare

@xia-sc xia-sc released this 14 Sep 05:36

v0.8.9-rc.2 — 未知模型可手动归类 + 参数面板

Pre-release(预览版)。在 v0.8.9-rc.1 的 #3 修复之上,收录
#4:官方目录只给 id,新上线的模型
归类为 unknown,于是既选不了思考等级、也定不了多模态 —— 现在可以在设置卡里按模型手填。

新功能(#4)

  • modelCaps 新增三个可选字段:surface(chat / responses / messages)、image
    (布尔)、efforts(档位 id 列表)。能力解析收拢到一处真源 capabilitiesOf():
    用户声明 > 内置表 > 保守 unknown,未知 id 在没有声明时行为完全不变。
  • 端面决定路由,也决定档位词表:stream() 按声明的端面发请求;词表随端面走 ——
    chat 落 reasoning_effort、responses 落 reasoning.effort、messages 没有档位词汇,
    所以设置卡里的档位选项会跟着端面变化,不会给你一个该端面收不了的档位。
  • 声明 image 会同时改写图片准入判定:给一个内置表里 text-only 的 id 声明
    image: true 后,runtime 会凭同一声明把图片放行,插件侧的准入检查不会再反手报
    UNSUPPORTED_CONTENT(那正是 #3 那类「自己把自己打死」)。
  • 设置卡改成折叠面板:每行一行(勾选 + 模型名 + 标签 + 手填内容摘要 + 「参数」),
    点开后在该行下方展开带标签的网格面板(端面 / 思考档位 / 多模态 / 上下文 / 输出上限)。
    展开时会把该行带进视野(模型列表自己有滚动区)。
  • 新增 models/known 端点:把内置表对已勾选模型的静态归类(含你的覆盖)返回给设置卡,
    不要 key、不联网。这样即使不点「刷新目录」(或刷新联网失败),标签与档位词表也
    和实际请求路径一致 —— 否则卡里会把未知端面猜成 chat,给 responses 模型列出含 Max 的
    chat 档位,存下去被宿主拒掉。

收录 v0.8.9-rc.1

  • #3:user 消息里的 harness 注解块(reasoning / tool-call 等)不再让整条请求在
    组装阶段抛 UNSUPPORTED_CONTENT。子代理结算通知会把子会话最后的 assistant 内容整段
    展开进 user 消息,而该块是持久历史 —— 原来一旦污染,该会话之后每一轮都失败且不自愈。
    现在三端面统一「带得了的带、带不了的丢」,image 仍硬报错;assistant 侧同样处理,
    并修掉 messages 端面「纯 reasoning 的 assistant 轮产出空 content: []」的问题。

验证

  • node test/smoke.mjs:51 项,50 pass + 1 skip(无 key 的活探针)。本版新增:
    能力解析与校验、端面覆盖真的改路由、models/known(并断言它不碰网络)、
    modelCaps 新字段过 Config 往返,以及一个用 stub React 驱动客户端 bundle 的用例
    (逐行展开/收起、按端面给词表、efforts: []/image: false 这类「有含义的空值」不被
    当成重置、清空最后一个字段会删掉条目)。
  • npm run build:client:lib/client.js 与 src/client/* 同步(测试里也有断言守卫)。
  • 真机 GUI 核对(客户端部分):面板排布、间距、展开自动进入视野,以及刷新目录后
    responses 模型只给 responses 档位词表(不再出现 Max)。

已知 / 未覆盖

  • messages 端面:持久化的 tool-call arguments 不是合法 JSON 时仍抛
    PROVIDER_PROTOCOL_ERROR(与 #3 同类的「一条历史打死整会话」)。它有现存测试刻意断言
    (宁可响也不降级),本版未改动。
  • 「空文本块」在 responses / messages 上游是否被接受,未做真机实测(探针需要 key)。

升级

宿主侧改动需要重启 dsh web 才生效(插件的主进程代码在启动时加载);客户端 bundle
刷新页面即可。

dsh plugin --profile web add github xia-sc/dsh-opencode-go
# 然后重启 dsh web,刷新页面

用法:在设置卡里勾选模型 → 点该行「参数」→ 手填端面 / 思考档位 / 多模态;
留空即跟随默认。标签为 unknown 的模型就是服务端没给归类的那些。


v0.8.9-rc.2 — describe an unclassified model by hand, plus an options panel

Pre-release. On top of the #3 fix in v0.8.9-rc.1, this carries
#4: the operator's catalog discloses ids
only, so a newly served model classifies as unknown — no reasoning level and no modality can be
set for it. The settings card now lets you state both by hand.

New (#4)

  • Three optional modelCaps fields: surface (chat / responses / messages), image
    (boolean) and efforts (level ids). Capability resolution now has a single source of truth,
    capabilitiesOf(): user override > static table > conservative unknown, so an id you say
    nothing about behaves exactly as before.
  • The surface drives routing and the level vocabulary: stream() sends to the declared
    surface, and the picker offers that surface's vocabulary — chat sends reasoning_effort,
    responses sends reasoning.effort, messages has none — so you cannot be offered a level the
    surface would reject.
  • A declared image also rewrites the image-admission check: declare image: true on an id
    the table calls text-only and the runtime passes images through on the strength of that same
    declaration, so the plugin's own guard no longer fails the request (the #3 class of
    self-inflicted failure).
  • The settings card is now a collapsible panel: one line per model (checkbox, id, tags, a
    summary of what you set, and an "options" toggle) that opens a labelled grid underneath
    (surface / levels / modality / context / max output). Opening a row scrolls it into view, since
    the model list is its own scroll box.
  • New models/known endpoint: hands the card the static classification (plus your overrides)
    for the enabled ids — no key, no network — so tags and level presets match the request path
    even without a catalog refresh (or when that refresh fails). Without it the card guessed the
    surface as chat, offered a responses model the chat preset including Max, and the host then
    refused the saved value.

Carried over from v0.8.9-rc.1

  • #3: harness annotation blocks (reasoning / tool-call) in a user message no longer make
    request assembly throw UNSUPPORTED_CONTENT. A settled subagent expands the child's final
    assistant content into a user-role notice, and that block is durable history — so a single
    settled subagent used to fail every later turn, unrecoverably. All three surfaces now carry
    what they can and drop what they cannot; image still fails loudly. The assistant side got the
    same treatment, and the messages surface no longer emits an empty content: [] for a
    reasoning-only assistant turn.

Verified

  • node test/smoke.mjs: 51 tests, 50 pass + 1 skip (live probe needs a key). This release
    adds coverage for capability resolution and validation, the surface override actually changing
    the routed endpoint, models/known (asserting it never touches the network), the new
    modelCaps fields through a Config round-trip, and a client case that drives the built bundle
    with a stub React (opening/closing a row, per-surface presets, meaningful empties like
    efforts: [] and image: false not being treated as resets, and clearing an entry's last field
    dropping it).
  • npm run build:client: lib/client.js is in sync with src/client/* (a test asserts this).
  • Checked live in the GUI (client half): panel layout and spacing, scroll-into-view on open,
    and — after a catalog refresh — a responses model offering only the responses vocabulary.

Known / not covered here

  • Messages surface: stored tool-call arguments that are not valid JSON still throw
    PROVIDER_PROTOCOL_ERROR (the same "one history entry kills the conversation" class). An
    existing test deliberately asserts that throw, so this release leaves it alone.
  • Whether responses / messages upstreams accept an empty text block was not verified live.

Upgrade

Host-side changes need a dsh web restart (the plugin's main-process code loads at startup);
the client bundle only needs a page refresh.

dsh plugin --profile web add github xia-sc/dsh-opencode-go
# then restart dsh web and refresh the page

Usage: tick a model in the settings card, open its "options", and set surface / levels / modality
by hand; empty means follow the default. Rows tagged unknown are the ones the server did not
classify.

v0.8.9-rc.1 — 不再让不可转译的内容块打死会话 (preview)

Choose a tag to compare

@xia-sc xia-sc released this 14 Sep 01:44

v0.8.9-rc.1 — 不再让不可转译的内容块打死会话

Pre-release(预览版)。修的是 #3:
user 消息里只要出现过 harness 注解块(reasoning / tool-call),整条请求会在发出去
之前就失败;而该块已经在持久历史里,于是之后每一轮都失败、且不会自愈——用户侧表现为
「对话框完全不能用了」。

根因

  • lib/protocol.js 三端面的 user 分支都是「不认识就抛」:只认 text / image,
    其余一律 UNSUPPORTED_CONTENT。
  • 触发源不是异常输入:continuable 子代理的结算通知是 user 角色,
    createSettlementMessage 会把 terminal.output 整段展开进去,而
    finalAssistantOutput() 返回的是子会话最后一条 assistant 消息的原始 content 数组
    (含 reasoning,有时含 tool-call)。
  • 因此一条结算通知就能把历史写脏;ctx.llm.stream 每轮重放整段历史 → 每轮都在请求组装阶段抛错。
    自动救回路径也堵死:dsh-compaction-basic 的压缩请求同样是
    [...input.messages, 压缩指令],走的还是本 adapter。

修复

  • 丢弃而非抛错:user 消息只承载用户载荷(text / image),注解块与 merge-extensible
    的新类型一律丢弃。这与官方适配器一致——llm-deepseek 的 flattenText 只留 text、
    llm-pi-ai 的 userContent 走 default: break,两者都把 UNSUPPORTED_CONTENT
    留给 image。
  • assistant 侧同样处理:原来对未知类型也抛,同样会打死会话。
  • messages 端面的空 content:纯 reasoning 的 assistant 轮原本会产出 content: [],
    Anthropic 拒空数组;现在改为省略该轮(与 responses 端面既有做法一致)。
  • 空 user 轮兜底:块被全部丢弃时不再产出 content: [],而是回退成一条空 turn
    (chat 为 "",responses / messages 为一个空文本块)。
  • image 仍然硬报错:用户上传的二进制内容不能被悄悄抹掉。
  • 判定标准写进了 lib/protocol.js 顶部的块策略注释,三端面共用。

验证

  • node test/smoke.mjs:47 项,46 pass + 1 skip(无 key 的活探针)。新增 2 项回归用例,
    用 #3 里的结算通知形状断言:
    注解块被丢弃且正文保留、全丢后是空 turn 而非空数组、污染历史后三端面组装均不再抛、
    image 仍然报错。
  • npm run build:client:产物 lib/client.js 与 src/client/* 同步(无差异)。
  • 触发源确证:本机 123 个会话日志扫描,7842 条非空 assistant 消息中 4103 条带
    reasoning 块——这类消息不是边角情况。

已知 / 本版未覆盖

  • messages 端面:持久化的 tool-call arguments 不是合法 JSON 时仍抛
    PROVIDER_PROTOCOL_ERROR,属于同一类「一条历史打死整会话」的风险。它有现存测试刻意断言
    (宁可响也不降级),本版未改动,留待单独决定。
  • 「空文本块」在 responses / messages 上游是否被接受,未做真机实测(探针需要 key)。
    若上游拒收,改成「整条省略该 user 轮」是一行的事。

升级

仓库安装(link: 方式)重启即生效:

dsh plugin --profile web add github xia-sc/dsh-opencode-go
# 然后重启 dsh web,刷新页面

本地源码安装用户:拉取 main 后重启 dsh web。


v0.8.9-rc.1 — unrepresentable content blocks no longer brick a conversation

Pre-release. Fixes #3: once a user message
carried a harness annotation block (reasoning / tool-call), every request failed before it
was ever sent — and because that block is durable history, every later turn failed too, with no
self-healing. On the user's side the conversation simply stopped working.

Root cause

  • All three surfaces' user branches were "throw on anything unrecognised": only text / image
    were carried, everything else raised UNSUPPORTED_CONTENT.
  • The producer is not exotic input. A continuable subagent's settlement notice is a user message,
    and createSettlementMessage expands terminal.output into it — which is the child's raw final
    assistant content array (reasoning, and sometimes tool-call, included).
  • One settled subagent therefore poisoned the history, and ctx.llm.stream replays the whole
    history every turn, so the failure repeated at request-assembly time forever. The automatic
    rescue is blocked too: dsh-compaction-basic sends [...input.messages, instruction] through
    the same adapter.

Fix

  • Drop instead of throw: a user message carries user payload (text / image) only;
    annotation blocks and merge-extensible additions are dropped. This matches the official
    adapters — llm-deepseek's flattenText keeps text only and llm-pi-ai's userContent
    falls through via default: break; both reserve UNSUPPORTED_CONTENT for images.
  • Assistant branches too: unknown types used to throw there as well, with the same
    conversation-killing effect.
  • Empty content on the messages surface: a reasoning-only assistant turn used to emit
    content: [], which the Messages API rejects; the turn is now omitted, matching what the
    responses surface already did.
  • Empty-turn fallback: a user turn whose blocks were all dropped becomes one empty turn
    ("" on chat, one empty text block on responses / messages) rather than content: [].
  • Images still fail loudly: uploaded binary content must not vanish silently.
  • The resulting policy is documented at the top of lib/protocol.js and shared by all surfaces.

Verified

  • node test/smoke.mjs: 47 tests, 46 pass + 1 skip (live probe needs a key). Two new regression
    tests assert, using the settlement-notice shape from
    #3: annotations are dropped while the
    payload survives, an all-dropped turn becomes an empty turn rather than an empty array, poisoned
    history no longer throws on any surface, and images still fail.
  • npm run build:client: lib/client.js is in sync with src/client/* (no diff).
  • Producer confirmed: scanning 123 local session logs, 4103 of 7842 non-empty assistant
    messages carry a reasoning block — this input is the norm, not a corner case.

Known / not covered here

  • Messages surface: stored tool-call arguments that are not valid JSON still throw
    PROVIDER_PROTOCOL_ERROR — the same "one history entry kills the conversation" class. It is
    covered by an existing test that deliberately asserts the throw, so this release leaves it alone
    pending a separate decision.
  • Whether responses / messages upstreams accept an empty text block was not verified live (the
    probe needs a key). If they reject it, omitting the whole user turn instead is a one-line change.

Upgrade

Repo installs (link: style) apply on restart:

dsh plugin --profile web add github xia-sc/dsh-opencode-go
# then restart dsh web and refresh the page

Local-source installs: pull main and restart dsh web.

v0.8.8 — 适配 DSH 0.1.5-rc.1

Choose a tag to compare

@xia-sc xia-sc released this 10 Sep 06:11

v0.8.8 — 适配 DSH 0.1.5-rc.1

正式版(Latest)。本版把插件对齐到 DeepSeek Harness 0.1.5-rc.1,并收录此前只存在于
Pre-release 的 /zen-go-rpc HTTP 405 修复。

适配 DSH 0.1.5-rc.1

  • peer 依赖对齐:@deepseek-ai/dsh-credentials / dsh-launch-environment / dsh-llm /
    dsh-settings / dsh-timeout 由 ^0.1.2-rc.1 升到 ^0.1.5-rc.1。0.1.x 的预发布版
    caret 不跨 patch 匹配(^0.1.2-rc.1 不覆盖 0.1.5-rc.1),旧声明已与实际运行环境不符。
  • 宿主 API 逐项复核:插件用到的每个接缝在 0.1.5-rc.1 上仍存在且语义一致 ——
    LlmAdapter(providerInfo / listModels / resolveModel / prepareCall / stream)、
    llm.registerAdapter / llm.registerConfigurableProviders、settings.installSection、
    credentials.credentialRef / credentials.resolve、launchEnvironmentOf、
    MAX_TIMER_DELAY_MS、attributionHeaders / assertUsableApiKey / LlmError、
    webServer.register({kind:"prefix"})、connection.requestRejection。
  • 客户端注入面同样在位:dsh-client-connection / dsh-client-locale /
    dsh-client-ui-settings / dsh-api-remotes 在 0.1.5-rc.1 中均存在,client.inject 无需改动。
  • 真机验证:在 dsh web 0.1.5-rc.1 上加载本插件(link: 安装),同源
    POST /zen-go-rpc/usage/summary 命中插件的 requestRejection 围栏返回 401(路由确实
    挂载、fence 生效),而不是 SPA fallback 的 405。

收录 v0.8.7-rc.1 的修复

  • /zen-go-rpc HTTP 405 根因:connection.rpc.handle("/zen-go-rpc", …) 在 connection 服务
    自己的上下文里解析 owner.webServer,Cordis 抛 cannot get property "webServer" without inject
    并被 effect 静默吞掉 → 路由从未挂载,浏览器 POST 落到 SPA fallback(非 GET/HEAD 一律 405)。
  • 修复:改为在注入 ["connection","webServer"] 的 fiber 上直接
    webServer.register({kind:"prefix", path:"/zen-go-rpc", handler}),自实现与 /api 同形的
    fetch-shaped RPC handler(buffered body、client-request 信封校验、method==endpoint、
    {ok}/fail 结果、requestRejection 安全围栏)。
  • 连带修复:浏览器 rpc.call 的参数位于 payload.args 而非 envelope.args,已对齐。

验证

  • node test/smoke.mjs:45 项,44 pass + 1 skip(无 key 的活探针)。
  • npm run build:client:产物 lib/client.js 与 src/client/* 同步(无差异)。
  • 真实 dsh-host-webserver + dsh-client-connection(0.1.5-rc.1)挂载下 RPC 通道可用。

升级

仓库安装(link: 方式)重启即生效:

dsh plugin --profile web add github xia-sc/dsh-opencode-go
# 然后重启 dsh web,刷新页面

本地源码安装用户:拉取 main 后重启 dsh web(无需重跑 setup-local-deps.cjs)。
v0.8.7-rc.1 用户可直接升到本版:功能内容一致,依赖声明与版本号转正。


v0.8.8 — adapted to DSH 0.1.5-rc.1

Stable release (Latest). This release aligns the plugin with DeepSeek Harness 0.1.5-rc.1 and
carries the /zen-go-rpc HTTP 405 fix that until now existed only in a Pre-release.

Adapted to DSH 0.1.5-rc.1

  • Peer dependencies aligned: @deepseek-ai/dsh-credentials / dsh-launch-environment /
    dsh-llm / dsh-settings / dsh-timeout move from ^0.1.2-rc.1 to ^0.1.5-rc.1. A caret on a
    0.1.x prerelease does not match a different patch tuple, so the old declaration no longer
    described the environment the plugin actually runs in.
  • Every host seam re-checked: all APIs this plugin consumes still exist with the same
    semantics in 0.1.5-rc.1 — LlmAdapter (providerInfo / listModels / resolveModel /
    prepareCall / stream), llm.registerAdapter / llm.registerConfigurableProviders,
    settings.installSection, credentials.credentialRef / credentials.resolve,
    launchEnvironmentOf, MAX_TIMER_DELAY_MS, attributionHeaders / assertUsableApiKey /
    LlmError, webServer.register({kind:"prefix"}), and connection.requestRejection.
  • Client injection surface intact: dsh-client-connection / dsh-client-locale /
    dsh-client-ui-settings / dsh-api-remotes are all still present in 0.1.5-rc.1, so
    client.inject needs no change.
  • Verified live: loaded on a dsh web running 0.1.5-rc.1 (via link:), a same-origin
    POST /zen-go-rpc/usage/summary hits the plugin's requestRejection fence and answers 401
    (route mounted, fence active) instead of the SPA fallback's 405.

Carried over from v0.8.7-rc.1

  • /zen-go-rpc HTTP 405 root cause: connection.rpc.handle("/zen-go-rpc", …) resolved
    owner.webServer inside the connection service's own context; Cordis threw
    cannot get property "webServer" without inject and the effect silently swallowed it → the
    route never mounted and every browser POST fell through to the SPA fallback, which answers
    non-GET/HEAD with 405.
  • Fix: register the prefix route directly on the fiber that injects
    ["connection","webServer"] via webServer.register({kind:"prefix", path:"/zen-go-rpc", handler}),
    reimplementing the /api-shaped fetch handler (buffered body, client-request envelope,
    method==endpoint, {ok}/fail results, requestRejection fence).
  • Also fixed: browser rpc.call passes endpoint args under payload.args, not
    envelope.args.

Verified

  • node test/smoke.mjs: 45 tests, 44 pass + 1 skip (live probe needs a key).
  • npm run build:client: lib/client.js is in sync with src/client/* (no diff).
  • RPC channel exercised against real dsh-host-webserver + dsh-client-connection (0.1.5-rc.1).

Upgrade

Repo installs (link: style) apply on restart:

dsh plugin --profile web add github xia-sc/dsh-opencode-go
# then restart dsh web and refresh the page

Local-source installs: pull main and restart dsh web (no need to re-run
setup-local-deps.cjs). v0.8.7-rc.1 users can move straight to this release: same functional
content, with the dependency declaration and version number promoted to stable.

v0.8.7-rc.1 — fix /zen-go-rpc HTTP 405 (preview)

Choose a tag to compare

@xia-sc xia-sc released this 10 Sep 02:59
v0.8.7-rc.1: fix /zen-go-rpc HTTP 405 (route never mounted)

v0.8.6

Choose a tag to compare

@xia-sc xia-sc released this 08 Sep 10:45

流式链路加固 Stream hardening

  • 提前退出也记账:消费者中途 break(for await … return())不再丢行,账本记 aborted。| Early-exit now recorded: consumer break out of for await no longer drops the row — the ledger records aborted.
  • SSE 连接不泄漏:finally 释放 reader 锁,提前退出不再把 socket 挂到 GC。| No SSE socket leak: the reader lock is released in finally, so early exits no longer hold the socket until GC.
  • chat 面错误直报:网关 error 事件直接抛 PROVIDER_ERROR(带原文),不再吞成“无 finish reason”。| Chat-surface errors surface directly: gateway error events now throw PROVIDER_ERROR with the original message instead of collapsing into “no finish reason”.
  • 坏 SSE 行有分类:非 JSON keepalive / HTML 错误行抛 PROVIDER_PROTOCOL_ERROR,不再漏原生 SyntaxError。| Malformed SSE lines classified: non-JSON keepalives / HTML error lines throw PROVIDER_PROTOCOL_ERROR instead of leaking a raw SyntaxError.
  • 单一路分帧:pumpChunks 复用共享 ssePayloads 泵,三端面不再各写一套切行/看门狗。| Single framing path: pumpChunks reuses the shared ssePayloads pump — one line-splitting/watchdog implementation for all three surfaces.

身份与配置 Identity & config

  • 身份头统一:新增 apiAttributionHeaders(),推理与目录刷新共用 deepseek-harness/<v> dsh-opencode-go/<v>。| Unified identity headers: new apiAttributionHeaders() shared by inference and catalog refresh.
  • apiBase 预检:设置加载时 new URL() 校验,配错直接报字段名,不用到 fetch 里猜。| apiBase validated early: checked with new URL() at settings load, misconfiguration names the field instead of failing cryptically inside fetch.
  • 刷新超时不卡进程:refreshModels 改用可 unref 的 AbortController,与推理面同构。| Refresh timeout no longer holds the process: refreshModels uses an unref'd AbortController, mirroring the inference path.

显示与工程 UX & DX

  • 模型分组改名:选择器显示 zen-go (OpenCode Go),搜 zen-go 直达;路由仍是 zen-go,已有会话不受影响。| Provider renamed in picker: group now shows zen-go (OpenCode Go) so it matches the zen-go route; existing sessions unaffected.
  • 浏览器半构建门:build-client.cjs 加缺件检查 + vm 语法门(先检查后落盘)+ npm run build:client / npm test;README 补构建说明。| Client build gate: build-client.cjs gains missing-part + vm syntax checks (check-before-write), plus npm run build:client / npm test; README documents the client build.

测试 Tests

  • node test/smoke.mjs:45 项,44 pass + 1 skip(无 key 活探针);新增提前退出记账、chat error 事件、坏 SSE 行、apiBase 校验、身份一致性回归。| node test/smoke.mjs: 45 tests, 44 pass + 1 skip (live probe needs a key); new regressions for early-exit metering, chat error events, malformed SSE, apiBase validation, and identity consistency.

升级:重启 dsh web 即生效(纯 Host 改动,无需重新 add)。| Upgrade: restart dsh web to apply (host-side changes only, no re-add needed).

v0.8.5

Choose a tag to compare

@xia-sc xia-sc released this 08 Sep 05:13

Community contribution: deepseek-v4-flash, deepseek-v4-pro and deepseek-v4-flash-vision-exp gain a Max reasoning tier (reasoning_effort=max, verified live against the upstream gateway). Other chat models keep low/medium/high; responses and messages surfaces are untouched. Thanks to 34262315716 (Critical Natural) for the live verification and the PR (pull 1). README zh/en add an Acknowledgements section.

v0.8.4

Choose a tag to compare

@xia-sc xia-sc released this 08 Sep 05:04

Two fixes in one. Timeout no longer kills long streams: requestTimeoutMs used to cover the whole fetch, so long reasoning turns on muse-spark tripped the 60s budget in batches (The operation was aborted due to timeout, ledger rows error:23). It now guards connect plus response headers only; the timer stops at headers and the body phase is watched by the stream idle watchdog alone. Ledger rows for DOMException aborts now read error:TimeoutError instead of error:23. Local-source install no longer crashes boot (issue 2): pnpm link installs left the host-half peer imports unresolvable (ERR_MODULE_NOT_FOUND, fatal at loader import). New scripts/setup-local-deps.cjs bridges node_modules/@deepseek-ai to the host tree (auto-detected, --host override, --dry-run probe); README zh/en document the extra step.

v0.8.3

Choose a tag to compare

@xia-sc xia-sc released this 07 Sep 07:16

Fix a ReferenceError that masked real provider errors: streamSurface called httpError/parseRetryAfterMs without importing them from protocol.js, so any non-2xx response (e.g. 401/429/400) surfaced as 'httpError is not defined' instead of the mapped AUTH/RATE_LIMIT/INVALID_REQUEST error. Adds a regression test stubbing a 401 fetch through the adapter stream path.