Repository navigation
Releases: Luca4Don3/dsh-providers-live-models
Release list
v0.6.0
Stability release; catalog merging and gateway behavior unchanged.
- Abort in-flight catalog requests on unload (probe i)
- A provider registered while a refresh is in flight refreshes immediately instead of waiting for the next poll (probe j)
- Late-refresh callback guarded against cross-generation HMR takeover (probe k)
- Disable semantics scenario (l): poll timer cleared, no events after dispose
- Publish-count flake hardened: in-flight cache writes drain before scenes that count emits, with a tripwire assertion
- Quota parser regressions (19 assertions) run in CI via npm test
- Docs: P0 compatibility/E2E/P0-Q reports, approved scoped implementation gate, HMR evidence
0.5.0
What changed
Keeping up with OpenCode no longer covers only one of its two endpoints.
`opencode` (OpenCode Zen) and `opencode-go` are different wire endpoints, so the
plugin now polls both, each with its own catalog URL, live roster, protocol table
and cache entry.
- Zen's model list is live. It used to be whatever catalog the installed
pi-ai shipped with, so a model Zen added after the DSH build was selectable
nowhere. Zen's fourth protocol rides pi-ai's own Google transport at the base
URL Pi publishes for it. - Catalog rows that are not chat models are skipped. Pi lists Zen's two
`type: "classifier"` entries beside the chat models with no `maxTokens`; they are
named once and left to the installed catalog instead of being published or
taking the whole catalog down with them. - One cache file, keyed by provider id, each entry age-checked on its own.
The schema 1 file is still read as Go, so the conditional first request
survives this upgrade. - A route that only just appeared refreshes itself, instead of waiting for
the next five-minute tick because its first poll was joined to one that ran
against an empty route list. - Only routes DSH actually registered are polled.
Verification
- `npm test`: 174/174 assertions.
- Live end-to-end against the real endpoints: Go 30 → 35 models, Zen 73 → 84,
both catalogs on disk.
Upgrade
Reinstall the plugin in your profile and restart DSH:
```sh
dsh plugin --profile desktop add github:Luca4Don3/dsh-opencode-live-models
```
An install still on 0.3.3 keeps the old code until it is reinstalled.
0.4.0
0.4.0 是功能性版本。插件不再只维护模型目录:它还接管了两件网关要求的事,因此第三方插件 BrackRat/dsh-opencode-session 可以卸载了。
第一条是缓存保活。pi-ai 把 cacheRetention 默认解析为 "short",而 "short" 在 openai-completions 路径上既不发送 prompt_cache_key 也不发送 prompt_cache_retention,于是一轮对话只能靠网关自己约 5 分钟的自动窗口命中,而 DeepSeek 官方端点的磁盘缓存可以存活数小时到数天。这就是同一个会话走 opencode-go 的命中率低于走官方 API 的原因。该设置在 pi-ai 里是提供方级字段,compat 是唯一的按模型字段,所以无法用配置点名具体模型;插件在既有的 provider 包装里合并这个选项,只对匹配 /^deepseek/i 的 id 生效。kimi-k2.6 是上游唯一一个已通过 compat.supportsLongCacheRetention 主动退出的模型,而这条路由上没有任何模型声明 cacheControlFormat,所以行为会变的恰好是正则匹配到的那些。
第二条是会话头。opencode.ai 会拒绝不带 x-opencode-session 的请求(400 MissingSessionID),并要求客户端自报身份。pi-ai 0.87.1 确实会注入这个头,但只从它自带的两个 provider 描述符出发:为自定义 id 配置的路由,或因为写了 api 而走协议表重建的路由,都拿不到它。x-opencode-client: dsh 则没有任何一方在发,因为 pi-ai 在每条路由上带的是 pi 自己的 user-agent。两者现在合并进 Models 原型——这是唯一能覆盖插件不认识的 provider id、又能在集合重建后存活的一层:adapter.current() 每次 profiles 变化都会重建集合,它背后的类不会。已经在请求选项或模型描述符上显式配置的头始终优先,且大小写不敏感。
两条策略都只改写匹配到的请求,其余模型拿回同一个 options 对象;测试按对象标识断言,多出一个键就会转红。卸载会把原型、adapter 钩子与包装过的 provider 一起还原,各自带守卫,不会剥掉更新实例装的那一层。
调用链是读安装中的 pi-ai 得到的,不是假设:buildBaseOptions 显式透传 cacheRetention 与 sessionId,applyAuth 只剥掉 transformHeaders,withOpenCodeSessionHeader 是合并而非替换。实机验证:0.4.0 加载后 Models 原型上的包装数为 1,用 deepseek-v4.1-flash 的请求全部成功,整份会话记录里错误事件为 0,命中率 99.9%。
两份 README 都新增了这两件事的说明与各自的限制。probe 从 148 条增至 166 条。
user-agent 的版本号与 package.json 一起改为 0.4.0。probe 会比对这两处,只改一边会让测试转红。
0.3.3
0.3.3 不带运行时变化。lib/index.js 只动了 user-agent 的版本号,其余改动落在测试与两份 README 上。
修掉了一个只在 node 18 上出现的测试竞态。并行取数那个场景 mock 了 300ms 的往返延迟,而挂载辅助函数固定等 300ms,两者等长,刷新是否落在窗口内成了掷硬币。node 24 的 runner 一直赢,node 18 这次输了,断言读到只有已安装目录的 30 个模型,而期望是 33。现在改为轮询等待目录落定,最多十个 tick,断言本身保持严格:刷新永远不落定仍然会失败。复现方式是把 mock 延迟抬到 500ms,修复前稳定转红,修复后通过。
README 的安装一节补上了 Desktop 的步骤:在侧边栏打开 Plugins,点 Add plugin,粘贴 spec,选择 Enable now 并重启 DSH。CLI 的写法保持不变。同一节还写明 github: 形式的 spec 是经 HTTPS 从 codeload.github.com 下载的,npm 镜像不会代理它,所以主机必须能访问 GitHub。
user-agent 的版本号与 package.json 一起改。probe 会比对这两处,只改一边会让测试转红。
0.3.2
0.3.2 不带运行时变化。改动全部落在注释、文档和测试上,lib/index.js 的可执行代码只多了一个导出。
probe.mjs 是改动最多的部分。有五处局部量写了却从不读,包括 ETag 场景里的 events 与 poll、ctx._on,以及三个场景工厂返回的 models() 访问器。有四条断言不可能失败。计费字段那条与写死在测试里的列表比较,所以 RATE_FIELDS 在插件里漂移它看不见,现在这个常量被导出,两边真正比对。缓存那条断言 headers === undefined,而 fixture 里的描述符从来没有这个字段。冷启动那条只问目录里的 id 来自已安装目录还是 Pi,这是合并的结构不变量,名单有没有被拒都成立,现在改为断言拒绝之后的目录规模。离线重启那条检查的 deepseek-v4.1-flash,而它本来就在已安装目录里。后两条的替代写法都实际破坏验证过,各自会转红。另有两处数字是错的,同一行里说无描述符的模型有 14 个,实际是 10 个,说 pi-ai 版本是 0.85.1,而 fixture 文件名与 CATALOG_SOURCE 都是 0.87.1。注释里的 CACHE_DIR 在 lib/index.js 中不存在。测试数从 149 变成 148,少的那条与卸载场景里的断言完全相同。
lib/index.js 里,mergeModels 构造了一个 Set 交给 buildOverlay,而那个函数不接收参数。buildOverlay 又逐条拷贝 state.remoteCatalog,与 new Map(...) 等价。继承状态上方那段说明把同一个 0.1.5 到 0.1.6 的故障讲了两遍,apply 的文档飘到了 applyDeferredFeeds 头上,另一处注释声称单条名单会删掉 5 个模型,而 installed-minus-Pi 是 4 个,这个数字会随 Pi 目录浮动,所以那句话改成点名它所指的那组。
两份 README 都把 space-bunny-free 说成 OCG 已下线而 Pi 仍在发布的例子。Pi 在同一个窗口跟着迁移,这个状态已经不存在,规则保留,例子删除。
user-agent 的版本号与 package.json 一起改。probe 会比对这两处,只改一边会让测试转红。
0.3.1
0.3.1 不带运行时变化。lib/index.js 相对 0.3.0 只动了一行版本常量,其余改动落在文档、测试 fixture 和 CI 上,测试仍为 149/149。
两个远端端点在同一次刷新里换掉了同一个 id。OCG 让免费的 Space Bunny 下线,Pi 跟着迁到付费的 space-bunny,所以 roster 和 Pi 目录现在都带 space-bunny 而不带 space-bunny-free。0.3.0 就是为此发布的:内置描述符会继续发布一个每次派发都失败的 id。probe.mjs 里有 12 处写着旧 id,多数只是改名,其中三处已经测不到任何东西。space-bunny-free 在两个 fixture 里都不存在,于是「Pi 独有的模型在联网前缺席」这类断言说的是一段哪都不出现的字符串,现在它们改指 space-bunny,这个 id 在 roster 和 Pi 目录里,不在已安装目录里。trulyUnknown 里的排除子句有相反方向的同一问题,它跳过的两个模型如今都由 Pi 覆盖,子句什么都没排除,只会让人以为它们仍需要特殊处理,已经删掉。
fixture 的刷新步骤此前无处可寻。probe.mjs 两处指向「README 的 Probe 章节」,而那一节并不存在,于是步骤在该用的时候消失了。现在记录在协议探测文档里,包含 OCG_FIXTURES,下一次刷新可以先试再提交。第三个 fixture 来自桌面客户端 app.asar 内部,指向已有的读取器,而不是重复描述偏移量换算。
探测笔记里 installed-only 的计数错了。原文把 omen-alpha 算进已安装 pi-ai 覆盖的那一组,那在 pi-ai 0.85.1 成立,到 0.87.1 不再成立,0.1.9 刷新 fixture 时丢掉了它而笔记没跟上。Pi 不发布的 14 个模型里,四个有描述符、十个没有,omen-alpha 属于后者。两处 orderModels 注释随之改掉。
CI 的 pin 落后了三个大版本而无人察觉。checkout 和 setup-node 都从 v4 升到 v7,因为 v4 内嵌的 Node 20 已被 GitHub 弃用。新增 concurrency,取消被后续推送取代的运行。job 加上 5 分钟上限,挂起的 fetch 会失败而不是占住 runner 360 分钟。新增 dependabot 按月跟进 github-actions 的 pin。pull_request 触发器保留。此前有一次改动删掉了它,理由是 19 次运行没有一次来自 PR,这忽略了 main 上的保护规则要求改动经过 PR。enforce_admins 关闭,owner 直推成功,这个触发器从未触发,但一个无法绕过该规则的贡献者提交 PR 时,工作流没有跑过,合并就少了一道检查。
user-agent 的版本号与 package.json 一起改。probe 会比对这两处,只改一边会让测试转红,已实测。
0.3.0
不再内置模型描述符,并修正几处只写在文档里的值。
两个手写描述符已经删除。已安装目录没有的模型,现在只有 Pi 可达时才会出现。用 fixture 核对过:deepseek-v4.1-flash 在已安装目录里,本来就由它覆盖;space-bunny-free 不在,所以完全离线的首次启动在 0.2.1 会从内置副本里列出它,0.3.0 不会。删除的原因就是 space-bunny-free:OCG 已经把它下线,内置副本会继续发布一个每次派发都返回 ModelError 的 id,而文件里没有任何东西会察觉。两份 README 开头都写明了这个变化。
user-agent 里的版本号原本是 header 对象中的字面量,发版时容易漏改,请求会声称一个包已经不存在的版本。现在改成具名常量,每次 probe 都会与 package.json 比对。这次是它经历的第一个版本号变更,两边必须一起改才能保持测试通过。
roster 和 catalog 原本各自解析自己的 envelope,OCG 可能改变的字段形状记录在两处,roster 那份知道得更少。现在共用一个解析器。写这条测试时发现一个陷阱:被拒绝的 roster 会让 liveModelIds 保持 null,此时不进行任何过滤,而这里所有已安装模型都是网关提供的,所以误读 envelope 和正确解析会得到同样的 33 个 id。断言必须检查 roster 已被接受,而不是看最终结果。
RATE_FIELDS 保持字面量,这是有意的。如果从已安装目录的 cost 键推导,任何描述符携带的数值字段都会扩大这条规则,将来多一个 cost.currency,插件就会开始拒绝计费正常的目录。注释里写明了该回看哪个 pi-ai 版本的字段,以及两种漂移各自怎么坏:费率改名会整份拒绝目录并报警,新增费率则静默计成 NaN。另有一条 fixture 断言盯着已安装目录的 cost 键。
模块头部原文写 profile 插件无法解析 DSH 包,导入会以 MODULE_NOT_FOUND 失败。这是错的。dsh-app-boot 给 Node 的 resolver 打了补丁,@deepseek-ai/* 会路由到 app.asar 里的副本,dsh-shell 从同一个 profile 且不声明任何依赖就能证明这一点。准确的说法是:在没有 node_modules 的 checkout 里解析不了,而 probe.mjs 就在那里加载本模块。所以保持零 import 的原因是测试套件,宿主环境其实可以解析这些包。
Config schema 据此被否决,否决理由是真实的那条。Schemastery 在插件里可达,在这个 checkout 里不可达,导出 Config 就得在 probe 里打桩,而桩的默认值必须和真实实现一致,否则测试会在生产有默认值的地方看到 undefined。
probe.mjs 141/141 → 149/149。roster、user-agent 和 fixture 三处守卫都验证过:回退修复后测试会转红。
0.2.1
校验 cost.tiers,并修正刷新判定过早的问题。
两个问题都关于过早读取信号。
cost 的注释声称分层定价按同样方式校验,实际没有,只检查了基础四个费率。pi-ai 在用量跨过阈值后整体换掉费率对象,所以某个 tier 缺费率会产生同样的 NaN,只是发生得更晚、只在长对话里出现,比一开始就出错更难排查。现在每个 tier 也要带齐四个费率。两个 OCG fixture 都没有 tiers,所以这是对一条路径的防护,不是对某次报告的响应,代码里也这么写明了。
对应的测试同样标注为合成:它们用手写的条目驱动真实的采纳路径,其中检查完整 tier 的那条是为了证明这个守卫不会拒绝正常的分层定价,那是更糟的错误。
README 原本让读者在重启后查看「没有 pi.dev catalog refresh failed」。两个请求并行、各有 8 秒超时,失败要等两边都落定才记录,所以这项检查可能在刷新还来不及失败时就跑完并通过。现在说明要先等过超时窗口。
另记一个编辑测试时的坑。场景标签里的非 ASCII 字符会成为 import 说明符的一部分,动态 import 会在插件加载之前就以 Cannot convert argument to a ByteString 失败,看起来像插件告警。
probe.mjs 137/137 → 141/141。tier 断言验证过:回退守卫后转红。
0.2.0
四个费率全部纳入校验,内置兜底不再覆盖 pi-ai 的描述符。
两个缺陷都只在上游出问题时才会走到,都是在 pi-ai 0.87.1 的 fixture 上发现的。
第一个是只带 cost.input 的描述符会被接受。pi-ai 的 calculateCost 从同一个对象上读 input、output、cacheRead、cacheWrite,四个都没有默认值,所以缺的那三个会按 NaN 计费而不是零,而校验只检查了四个字段中的一个。现在四个都是必需字段,与实时响应和缓存走同一个 REQUIRED_DESCRIPTOR_FIELDS 条目,缓存文件无法夹带响应本身不被接受的内容。分层定价会整体替换费率对象,同样按这条规则校验。
第二个是内置兜底会静默覆盖已安装的描述符。deepseek-v4.1-flash 已经内置进 pi-ai 并带有 inputLimits,而按 0.85.1 写的那份内置副本没有这个字段,于是在 Pi 不可达时会覆盖它。兜底的作用是补缺口,不是覆盖目录,所以现在只在已安装目录无从提供该模型时才生效。Pi 自己的描述符仍会更新已安装模型,这是插件的目的。
兜底那条断言值得记一下。在回退修复的情况下它起初看起来是通过的,因为 Pi 可达时 Pi 自己的条目本来就赢,内置副本没有机会生效。真正走到这条路径的场景必须让 Pi 不可达,所以测试现在会切断网络。
probe.mjs 129/129 → 137/137。两处修复都验证过:回退后测试转红。
0.1.9
把已安装目录的 fixture 更新到 pi-ai 0.87.1,并给 probe 隔离缓存目录。
测试套件有两个问题,它们都可能在什么都没检查的情况下报告成功。
fixture 原本是 pi-ai 0.85.1,也就是 DSH 0.1.7 附带的版本。现在 DSH 到 0.2.0-rc.2,附带 pi-ai 0.87.1,于是所有关于「已安装目录」的断言都是在说一份已经不存在的目录。替换为真实的 0.87.1 数据后,模型数从 27 变成 30:deepseek-v4.1-flash、mimo-v2.6-flash、mimo-v2.6-pro、grok-4.7 变成内置,omen-alpha 被移除。合并后的选择器因此从 34 变成 33,无描述符的数量从 9 变成 10。相关规模现在从 fixture 推导,不再写死在断言里,这样下一次 pi-ai 升级不会因为一个没人写过理由的数字而失败。
更严重的是,测试套件在读写开发者真实的 ~/.dsh 缓存。缓存位置按模块实例记忆化,所以某个场景重新赋值 DSH_PROFILE_DIR 只能影响之后加载的实例,更早的实例早已在跟真实目录打交道。结果是套件的成败取决于 DSH 是否正在运行:有残留目录时 129/129,没有时 125/129。两个数字都不属于套件本身,较高的那个是污染造成的。现在 probe 在加载任何实例之前换成一次性目录,并在场景之间清空,因此真实缓存、空沙箱、不设变量这三种环境下都是同样的 129/129,真实缓存不被触碰。
另外补了三条兼容性断言,因为对着过期 fixture 跑绿并不能说明实际安装的版本没问题。fixture 需要带齐五个必需字段、只使用插件支持的三种协议、规模与 Pi 目录相差不超过一倍,以免缩水保护把一次升级当成截断。
probe.mjs 129/129 → 132/132。