Repository navigation
Releases
v0.9.0-rc.2 — messages 端面不再被一条畸形历史打死 + 图片边界说法纠错 (preview)
v0.9.0-rc.2 — messages 端面不再被一条畸形历史打死 + 图片边界说法纠错
Pre-release(预览版)。修掉 v0.9.0-rc.1 之后发现的一处「一条持久历史打死整会话」
(#3 同一类),并订正上一版对图片
体积的描述。
修复
- messages 端面:历史里的畸形 tool-call 不再让之后每一轮都失败。
持久化的 assistanttool-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。
- 非法 JSON(
- 创作侧保持严格:
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 assistanttool-callhadargumentsthat were not valid JSON,
toAnthropicMessagesthrewPROVIDER_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 passarguments
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 newtoolInput()maps a historicalinputto{}for:- unparseable JSON (
not-json{, the empty string); - anything that parses to a non-object —
"5","[1]","null",'"text"'. The
Messages API requiresinputto be an object, so these failed exactly as hard, while the
previous release sent them verbatim: an upstream 400 naming no source.
- unparseable JSON (
- Creation stays strict.
pumpMessagesstill 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-deepseekmessages 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), andimageHostPath
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 ismaxRequestImageBytes,
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
maxRequestImageBytesentries inREADME.md/README.en.md, andAGENTS.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{}whileid/namestill replay; andpumpMessagesstill rejects a freshly
streamed non-JSON call withPROVIDER_PROTOCOL_ERROR.node test/host-compat.mjs: 8 tests, 8 pass, 0 skip (realdsh-llm+ real stream
invariant).npm run build:client: the browser half is unchanged in this release and
lib/client.jsstays in sync withsrc/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
inputto{}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 thetool_resulttext that follows. imageRequestPricingis 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.readImageRequestrequest-version downscaling (it needs another set of
pixel/byte targets that could only be guessed, plus a re-encode fidelity tradeoff), so
maxRequestImageBytesremains 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
maxRequestImageByteskeeps 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)
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的默认/校验/传导;RPCconfigError的三种情形;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
offloadedalways travel as placeholder text, projected before
any read. Dropping images is the harness's durable session decision
(dsh-compaction-image-offloadrecords 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(plusoffloadImages)
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 ownbytes.
- Occurrences marked
- Peer range corrected: all six
@deepseek-ai/dsh-*peers move from^0.1.5-rc.1to
^0.1.6-alpha.1. The old range never matches0.1.6-alpha.1under semver's prerelease
rule (a range admits a prerelease only on its own tuple), so a fresh install resolved
dsh-llm0.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/knownand
models/refreshnow carryconfigError, 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
surfaceandefforts
as a pair, so the card conforms the levels in the same settings write that changes
the surface (new atomic multi-fieldsetModelCapFields):- 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
defaultreconciles 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-onlymaxpair in the first place.
Verification
node test/smoke.mjs: 55 tests, 54 pass + 1 skip (the key-gated live probe). New
coverage:maxRequestImageBytesdefaults/validation/plumbing; the threeconfigError
cases; offloaded occurrences never read back and their placeholder reaching the body; an
over-budget request failing before dispatch with the rightoffloadImagescount; and three
card beats for the surface/level reconciliation (partial keep / no vocabulary / follow the
default / fully compatible left untouched), comparing the persistedmodelCapspayload
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/cordisfromnode_modulesand drives
the realllmservice plus the real stream-grammar invariant (onlyconnection/
webServer/settings/credentials/attachments/fsare 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.jsstays in sync withsrc/client/*(asserted by the
suite).- Live GUI check (isolated DSH_HOME; the user's config was not modified): switching
deepseek-flashfrom the responses surface to chat conformed its levels from
minimal/low/medium/high/xhightolow/medium/high, reported "dropped minimal / xhigh",
persisted a valid pair, and left no refusal banner. - Live host check:
/zen-go-rpcmodels/knownandusage/summaryboth answer 200 on
0.1.6-alpha.1, andmodels/knownreports the exact reason for a refused section.
Known / not covered
- messages surface: a persisted tool-call
argumentsthat 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. imageRequestPricingis 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, somaxRequestImageBytesis
the only request-body guard; the harness-sideimagePixelBudgetstyle 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 importsprojectOffloadedImages/
requiredImageOffload/IMAGE_OFFLOAD_REQUIRED, which exist ...
v0.8.9-rc.2 — 未知模型可手动归类 + 参数面板 (preview)
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
modelCapsfields:surface(chat / responses / messages),image
(boolean) andefforts(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 sendsreasoning_effort,
responses sendsreasoning.effort, messages has none — so you cannot be offered a level the
surface would reject. - A declared
imagealso rewrites the image-admission check: declareimage: trueon 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/knownendpoint: 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 throwUNSUPPORTED_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;imagestill fails loudly. The assistant side got the
same treatment, and the messages surface no longer emits an emptycontent: []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
modelCapsfields through aConfiground-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: []andimage: falsenot being treated as resets, and clearing an entry's last field
dropping it).npm run build:client:lib/client.jsis in sync withsrc/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
argumentsthat 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 pageUsage: 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)
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 raisedUNSUPPORTED_CONTENT. - The producer is not exotic input. A continuable subagent's settlement notice is a user message,
andcreateSettlementMessageexpandsterminal.outputinto it — which is the child's raw final
assistantcontentarray (reasoning, and sometimes tool-call, included). - One settled subagent therefore poisoned the history, and
ctx.llm.streamreplays the whole
history every turn, so the failure repeated at request-assembly time forever. The automatic
rescue is blocked too:dsh-compaction-basicsends[...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'sflattenTextkeeps text only andllm-pi-ai'suserContent
falls through viadefault: break; both reserveUNSUPPORTED_CONTENTfor 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 thancontent: []. - Images still fail loudly: uploaded binary content must not vanish silently.
- The resulting policy is documented at the top of
lib/protocol.jsand 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.jsis in sync withsrc/client/*(no diff).- Producer confirmed: scanning 123 local session logs, 4103 of 7842 non-empty assistant
messages carry areasoningblock — this input is the norm, not a corner case.
Known / not covered here
- Messages surface: stored tool-call
argumentsthat 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 pageLocal-source installs: pull main and restart dsh web.
v0.8.8 — 适配 DSH 0.1.5-rc.1
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 web0.1.5-rc.1 上加载本插件(link:安装),同源
POST /zen-go-rpc/usage/summary命中插件的requestRejection围栏返回 401(路由确实
挂载、fence 生效),而不是 SPA fallback 的 405。
收录 v0.8.7-rc.1 的修复
/zen-go-rpcHTTP 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-timeoutmove from^0.1.2-rc.1to^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 in0.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"}), andconnection.requestRejection. - Client injection surface intact:
dsh-client-connection/dsh-client-locale/
dsh-client-ui-settings/dsh-api-remotesare all still present in0.1.5-rc.1, so
client.injectneeds no change. - Verified live: loaded on a
dsh webrunning 0.1.5-rc.1 (vialink:), a same-origin
POST /zen-go-rpc/usage/summaryhits the plugin'srequestRejectionfence and answers 401
(route mounted, fence active) instead of the SPA fallback's 405.
Carried over from v0.8.7-rc.1
/zen-go-rpcHTTP 405 root cause:connection.rpc.handle("/zen-go-rpc", …)resolved
owner.webServerinside the connection service's own context; Cordis threw
cannot get property "webServer" without injectand 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"]viawebServer.register({kind:"prefix", path:"/zen-go-rpc", handler}),
reimplementing the/api-shaped fetch handler (buffered body,client-requestenvelope,
method==endpoint,{ok}/failresults,requestRejectionfence). - Also fixed: browser
rpc.callpasses endpoint args underpayload.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.jsis in sync withsrc/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 pageLocal-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)
v0.8.7-rc.1: fix /zen-go-rpc HTTP 405 (route never mounted)
v0.8.6
流式链路加固 Stream hardening
- 提前退出也记账:消费者中途
break(for await … return())不再丢行,账本记aborted。| Early-exit now recorded: consumerbreakout offor awaitno longer drops the row — the ledger recordsaborted. - SSE 连接不泄漏:
finally释放 reader 锁,提前退出不再把 socket 挂到 GC。| No SSE socket leak: the reader lock is released infinally, so early exits no longer hold the socket until GC. - chat 面错误直报:网关
error事件直接抛PROVIDER_ERROR(带原文),不再吞成“无 finish reason”。| Chat-surface errors surface directly: gatewayerrorevents now throwPROVIDER_ERRORwith 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 throwPROVIDER_PROTOCOL_ERRORinstead of leaking a rawSyntaxError. - 单一路分帧:
pumpChunks复用共享ssePayloads泵,三端面不再各写一套切行/看门狗。| Single framing path:pumpChunksreuses the sharedssePayloadspump — one line-splitting/watchdog implementation for all three surfaces.
身份与配置 Identity & config
- 身份头统一:新增
apiAttributionHeaders(),推理与目录刷新共用deepseek-harness/<v> dsh-opencode-go/<v>。| Unified identity headers: newapiAttributionHeaders()shared by inference and catalog refresh. apiBase预检:设置加载时new URL()校验,配错直接报字段名,不用到fetch里猜。|apiBasevalidated early: checked withnew URL()at settings load, misconfiguration names the field instead of failing cryptically insidefetch.- 刷新超时不卡进程:
refreshModels改用可unref的AbortController,与推理面同构。| Refresh timeout no longer holds the process:refreshModelsuses an unref'dAbortController, mirroring the inference path.
显示与工程 UX & DX
- 模型分组改名:选择器显示
zen-go (OpenCode Go),搜zen-go直达;路由仍是zen-go,已有会话不受影响。| Provider renamed in picker: group now showszen-go (OpenCode Go)so it matches thezen-goroute; existing sessions unaffected. - 浏览器半构建门:
build-client.cjs加缺件检查 +vm语法门(先检查后落盘)+npm run build:client/npm test;README 补构建说明。| Client build gate:build-client.cjsgains missing-part +vmsyntax checks (check-before-write), plusnpm 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,apiBasevalidation, and identity consistency.
升级:重启 dsh web 即生效(纯 Host 改动,无需重新 add)。| Upgrade: restart dsh web to apply (host-side changes only, no re-add needed).
v0.8.5
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
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
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.