dsh-opencode-go 0.1.18 → 0.1.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.en.md CHANGED
@@ -86,7 +86,7 @@ To build from source and install a local package:
86
86
  npm ci
87
87
  npm run compile
88
88
  npm pack
89
- dsh plugin --profile web add ./dsh-opencode-go-0.1.18.tgz
89
+ dsh plugin --profile web add ./dsh-opencode-go-0.1.20.tgz
90
90
  ```
91
91
 
92
92
  Source development builds the plugin explicitly with `npm run compile`; `--legacy-peer-deps` is not required. Multi-version DSH compatibility tests use independent environments and are not installed during ordinary installation or builds. For Headless, replace `web` with `headless`.
@@ -97,7 +97,7 @@ Source development builds the plugin explicitly with `npm run compile`; `--legac
97
97
  npm run compile # Regenerate lib/ and commit it with source changes
98
98
  npm test # Core tests, rebuilding the plugin first
99
99
  npm run check:dist # Verify shipped artifacts match the source
100
- npm run test:compat # One tarball tested in 8 independent DSH environments
100
+ npm run test:compat # One tarball tested in 9 independent DSH environments
101
101
  npm run test:install # npm / pnpm Git installs without plugin build approval
102
102
  npm run verify # All of the above
103
103
  ```
@@ -114,8 +114,12 @@ dsh plugin --profile web update dsh-opencode-go --latest
114
114
 
115
115
  Restart `dsh web` and refresh the browser afterwards. For Headless, replace `web` with `headless`; if both profiles have the plugin installed, update each one separately.
116
116
 
117
+ After updating the desktop plugin, fully quit and reopen DeepSeek Harness to load the updated plugin code.
118
+
117
119
  ## Subscription usage display
118
120
 
121
+ Under **Settings → OpenCode Go → Advanced settings → Usage display**, choose a mode and save: **Auto (default)** shows the pill only for this plugin's DSH OpenCode Go models; **Always** keeps it visible with other models, including the built-in `opencode-go` provider; **Off** hides it and stops polling. Disabling OpenCode Go hides the pill in every mode. Click the pill to open the usage panel.
122
+
119
123
  Usage refreshes every minute. Temporary network or service errors retain the last reading for the same account, with a failure notice, timestamp, and reason; the usage panel offers an immediate retry. Initial and authentication failures do not show old usage. Catalog, metadata, and usage JSON requests retry a transient connection reset once within the original timeout budget; this cannot guarantee recovery while the network is failing.
120
124
 
121
125
  ![OpenCode Go usage display](image.png)
package/README.md CHANGED
@@ -86,7 +86,7 @@ dsh --profile headless --patch ./headless.patch.yml "你好"
86
86
  npm ci
87
87
  npm run compile
88
88
  npm pack
89
- dsh plugin --profile web add ./dsh-opencode-go-0.1.18.tgz
89
+ dsh plugin --profile web add ./dsh-opencode-go-0.1.20.tgz
90
90
  ```
91
91
 
92
92
  源码开发时显式运行 `npm run compile`;普通用户直接安装预编译产物,无需 `--legacy-peer-deps`。多版本 DSH 兼容性测试使用独立环境,不参与普通安装或构建。Headless 用户将 `web` 换成 `headless`。
@@ -97,7 +97,7 @@ dsh plugin --profile web add ./dsh-opencode-go-0.1.18.tgz
97
97
  npm run compile # 更新 lib/,与源码一起提交
98
98
  npm test # 基础功能测试(自动重新构建)
99
99
  npm run check:dist # 检查已提交产物是否与源码一致
100
- npm run test:compat # 同一安装包在 8 套独立 DSH 环境中测试
100
+ npm run test:compat # 同一安装包在 9 套独立 DSH 环境中测试
101
101
  npm run test:install # npm / pnpm 无额外构建授权的 Git 安装检查
102
102
  npm run verify # 执行以上全部检查
103
103
  ```
@@ -114,8 +114,12 @@ dsh plugin --profile web update dsh-opencode-go --latest
114
114
 
115
115
  完成后重启 `dsh web` 并刷新浏览器。Headless 用户将 `web` 换成 `headless`;如果两个 profile 都安装了插件,需要分别升级。
116
116
 
117
+ 桌面版升级后,请完全退出并重新打开 DeepSeek Harness,以加载更新后的插件代码。
118
+
117
119
  ## 订阅用量显示
118
120
 
121
+ 在「设置 → OpenCode Go → 高级设置 → 额度显示」中选择并保存:**自动(默认)**仅在选中本插件的 DSH OpenCode Go 模型时显示;**常驻**在使用其他模型(包括宿主自带的 `opencode-go`)时也显示;**关闭**隐藏胶囊并停止轮询。停用 OpenCode Go 后所有模式均隐藏。显示时点击胶囊展开用量面板。
122
+
119
123
  用量每分钟刷新。临时网络或服务错误会保留同一账号的上次数据,并标明刷新失败、更新时间和错误原因;可在用量弹层中立即重试。首次获取失败或鉴权失败时不显示旧额度。模型目录、模型配置和用量的 JSON 请求遇到短暂连接重置时会在原有超时范围内额外重试一次;这不能保证故障中的网络恢复可用。
120
124
 
121
125
  ![OpenCode Go usage display](image.png)
@@ -0,0 +1,116 @@
1
+ # 与 DeepSeek Harness v0.2.0-rc.2 原生调用的对照
2
+
3
+ 研究日期:2026-09-30。插件:当前工作区 `dsh-opencode-go@0.1.17`,基线提交 `03c482369f400ae786a251a5df265309e5341bb1`。原生:固定 tag `dsh-v0.2.0-rc.2`,提交 `639ed015397290b3745d163aafe02ffee4aa3f84`。上游源码只读检出在系统临时目录;没有读取真实 API key,也没有发出收费推理请求。版本出处:[官方 release](https://github.com/deepseek-ai/deepseek-harness/releases/tag/dsh-v0.2.0-rc.2)、[插件 package.json](../package.json)。
4
+
5
+ 后续修复(2026-09-30):当前工作区已修复下文基线中的配置/credential 引用漂移与 Anthropic alias 回放缺陷,并重建 `lib/`。请求在目录等待前复制配置,准备与发送使用同一份模型/目录/密钥引用;回放以请求模型 ID 匹配签名,返回别名仅作为说明信息保留。371 项测试、32 个产物检查、九个宿主版本兼容检查及原始请求对照均通过,见[修复验证](verification.md#prepared-call-configuration-and-anthropic-alias-replay-2026-09-30)。下文保留修复前的对照证据;默认思考和输出上限等策略差异仍适用。
6
+
7
+ ## 结论
8
+
9
+ 插件与 rc.2 的原生 OpenCode Go 使用同一版 pi-ai 0.87.1,常规文本、工具、流式与用量转换大体一致,但不是完全等价的调用。原生现在也会自动添加会话路由 header;这已不再是插件独有能力。插件的主要额外价值是在线模型目录、独立设置与额度界面,以及针对部分模型自动启用思考的默认策略。
10
+
11
+ 本地 HTTP mock 已复现两个应优先修复的问题:**插件没有完整冻结一次调用的配置,准备后改 endpoint 会从 A 改发 B,目录刷新期间改 endpoint 与 key 引用还会组合旧 endpoint A 和新 fixture key B;Anthropic 响应 model 与请求 alias 不同时,原生下一轮保留 thinking 签名,插件把 thinking 降为普通文本并丢失签名。** 另有两项明确策略差异:未选思考档位时插件为部分模型默认启用 high;配置输出上限 128、显式请求 512 时,原生发送 512,插件发送 128。统一 effort、maxTokens 与 temperature 后,DeepSeek V4 Flash、Qwen3.6-plus、GLM5.3 的完整请求 JSON、会话 header、User-Agent 均一致。详见后文证据。
12
+
13
+ 这里有两种“原生”:
14
+
15
+ | 对照对象 | 调用路径 | 是否与插件使用同一网关 |
16
+ | --- | --- | --- |
17
+ | 原生 OpenCode Go | `@deepseek-ai/dsh-llm-pi-ai` → pi-ai `opencode-go` provider → OpenCode Go | 是;这是主要对照对象 |
18
+ | 原生 DeepSeek 官方直连 | `@deepseek-ai/dsh-llm-deepseek-api-key` / account → `dsh-llm-deepseek` → DeepSeek Messages API | 否;协议、认证和能力均有差异 |
19
+
20
+ 原生 pi-ai 会复用安装包内置 provider,插件则构建私有的 `opencode-go` SDK provider,并以 `dsh-opencode-go` 注册到 DSH。官方直连默认端点为 `https://api.deepseek.com/anthropic`。来源:[原生 provider 构建](https://github.com/deepseek-ai/deepseek-harness/blob/dsh-v0.2.0-rc.2/packages/llm/llm-pi-ai/src/provider.ts#L144-L172)、[插件 provider 构建](../src/catalog.ts#L125)、[插件身份](../src/provider-identity.ts)、[官方直连配置](https://github.com/deepseek-ai/deepseek-harness/blob/dsh-v0.2.0-rc.2/packages/llm/llm-deepseek/src/config.ts#L106)。
21
+
22
+ ## 原生 OpenCode Go 与插件
23
+
24
+ ### 请求协议、会话 header 与认证
25
+
26
+ | 项目 | 插件 | rc.2 原生 OpenCode Go | 影响 |
27
+ | --- | --- | --- | --- |
28
+ | SDK | 私有 npm alias `opencode-go-pi-ai` → pi-ai 0.87.1 | `@earendil-works/pi-ai` 0.87.1 | 同版基线;原生源码构建另有流式性能补丁,见后文 |
29
+ | DSH provider ID | `dsh-opencode-go` | `opencode-go` | 可以共存,DSH 持久历史来源名称不同 |
30
+ | 带 sessionId | 显式发 `x-opencode-session: sessionId` | 内置 `withOpenCodeSessionHeader` 自动补同一值 | 常规 DSH 会话一致 |
31
+ | 无 sessionId | 每个请求生成随机 UUID header | 不自动补 header | 插件为无会话调用提供隔离的路由值 |
32
+ | User-Agent | `attributionHeaders()` | `attributionHeaders()`,覆盖 profile 中同名 header | 都归因到当前 Harness |
33
+ | API key | 每次通过 DSH credentials / launch environment 解析 `OPENCODE_API_KEY` 引用 | 可通过 profile 显式引用,也可使用 pi-ai 原生认证、存储与登录机制 | OpenCode Go 都能用同一环境引用;原生通用 adapter 认证范围更广 |
34
+ | 自定义 HTTP header | 没有对应配置项 | profile `headers` | 原生部署可配置附加 header |
35
+
36
+ 来源:[插件依赖](../package.json)、[原生依赖](https://github.com/deepseek-ai/deepseek-harness/blob/dsh-v0.2.0-rc.2/packages/llm/llm-pi-ai/package.json)、[插件请求选项](../src/adapter.ts#L317)、[插件 credential resolver](../src/index.ts#L92)、[原生 header 合并](https://github.com/deepseek-ai/deepseek-harness/blob/dsh-v0.2.0-rc.2/packages/llm/llm-pi-ai/src/adapter.ts#L204-L211)、[原生 auth](https://github.com/deepseek-ai/deepseek-harness/blob/dsh-v0.2.0-rc.2/packages/llm/llm-pi-ai/src/auth.ts)。SDK 原始代码:[本地 pi-ai 0.87.1 provider](../node_modules/opencode-go-pi-ai/dist/providers/opencode-go.js)、[会话 header wrapper](../node_modules/opencode-go-pi-ai/dist/providers/opencode-headers.js)。
37
+
38
+ 插件 [src/index.ts](../src/index.ts#L7) 仍称 generic pi-ai 不能表达该路由 header,这段说明对 rc.2 的内置 `opencode-go` 已过时。只有手工定义通用协议 provider、没有复用内置 OpenCode provider 时,不能直接套用上述自动 header 结论。
39
+
40
+ ### 模型目录与思考默认值
41
+
42
+ 原生使用 pi-ai 随安装包发布的模型目录;显式 `models` 替换目录,`modelOverrides` 修正单个条目。原生对已知 provider 的“发现模型”直接返回内置目录,不查询网关。插件则以网关 `${baseURL}/models` 决定可用 ID,以 `https://models.dev/api.json` 的 `opencode-go` 条目决定名称、协议、容量、模态和价格;内置目录作为兼容提示,已知历史数据用于故障时保留。插件还有磁盘元数据缓存、ETag、刷新与可见性开关。首次网关 listing 失败且没有已服务快照时,不会仅靠内置目录猜测可用模型。来源:[原生配置](https://github.com/deepseek-ai/deepseek-harness/blob/dsh-v0.2.0-rc.2/packages/llm/llm-pi-ai/src/config.ts#L105-L127)、[原生 discovery](https://github.com/deepseek-ai/deepseek-harness/blob/dsh-v0.2.0-rc.2/packages/llm/llm-pi-ai/src/discovery.ts#L269-L285)、[插件目录](../src/catalog.ts#L162)、[插件 membership 与故障策略](../src/catalog.ts#L263)、[插件元数据转换](../src/model-metadata.ts#L73)。
43
+
44
+ 插件接受线上目录中的新 ID,前提是能解析为三种支持协议之一,且容量、模态、思考声明可用;缺少元数据的模型会显示诊断,不进入可选模型列表。这也意味着同一 pi-ai 版本并不保证同一模型描述:线上更新可以让插件与原生在容量、协议或思考选项上发生差别。插件只读取 `opencode-go` 元数据,忽略在线端点、header 与 credential,实际模型流量仍使用配置的网关。来源:[插件目录构建](../src/catalog.ts#L285)、[metadata 读取规则](../src/model-metadata.ts#L68)、[picker 过滤](../src/adapter.ts#L166)。
45
+
46
+ 默认思考是明确的策略差异。原生只在 profile 指定 `reasoning` 时给模型提供 `defaultEffort`;插件遇到 `deepseek`、`zai`、`qwen`、`qwen-chat-template` 这类未指定 effort 会显式关闭思考的格式,会提供 `high`,或模型支持的最高非 off 档。DSH 正常调用会把该默认值补入请求。本地 mock 确认:DeepSeek V4 Flash 原生发 `thinking: disabled`,插件发 `thinking: enabled` 与 high;Qwen3.6-plus 在 fixture 声明 toggle 时原生发 `enable_thinking: false`,插件发 true 与 high;GLM5.3 两边都未默认指定 effort。该结果验证默认策略,不能视为真实服务推理质量对比。若直接调用 adapter.stream 而绕过 DSH 的默认值补齐,两边默认行为还会有所不同。来源:[插件默认 effort](../src/adapter.ts#L198)、[原生 reasoningInfo](https://github.com/deepseek-ai/deepseek-harness/blob/dsh-v0.2.0-rc.2/packages/llm/llm-pi-ai/src/adapter.ts#L187-L201)、[DSH 默认值补齐](https://github.com/deepseek-ai/deepseek-harness/blob/dsh-v0.2.0-rc.2/packages/llm/llm/src/index.ts#L892-L924)、[pi-ai thinking 序列化](../node_modules/opencode-go-pi-ai/dist/api/openai-completions.js#L619)、本文验证表。
47
+
48
+ ### 输出 cap 与调用配置冻结
49
+
50
+ 原生将显式配置的 model `maxTokens` 暴露为 `defaultMaxTokens`:只有请求没有 cap 时 DSH 才补默认值,调用方显式值优先。插件不暴露 `defaultMaxTokens`,而在发送时对 modelLimits 的 `maxTokens` 做 `Math.min(requested, configured)`,因此它是强制上限。本地 mock 设配置 128、请求 512,原生实际发送 512,插件实际发送 128;原生模型信息返回 defaultMaxTokens=128,插件没有该字段。这属于可见语义差异,且 DSH 准备的调用配置可能没有反映插件最终上限。两边都不会仅因为内置目录有 `maxTokens` 就在 adapter 层自动填入整个容量;SDK 在无显式 cap 时仍可能自行采用模型容量。本次默认请求中原生与插件的容量数值来自不同 fixture 目录,不能用那组数值认定额外缺陷。来源:[原生 modelInfo](https://github.com/deepseek-ai/deepseek-harness/blob/dsh-v0.2.0-rc.2/packages/llm/llm-pi-ai/src/adapter.ts#L300-L315)、[DSH defaultMaxTokens](https://github.com/deepseek-ai/deepseek-harness/blob/dsh-v0.2.0-rc.2/packages/llm/llm/src/index.ts#L896-L898)、[插件 cap](../src/adapter.ts#L266)、[插件 modelInfo](../src/adapter.ts#L223)、本文验证表。
51
+
52
+ rc.2 原生 `PiAiAdapter.prepareCall` 绑定不可变 profile/models 快照;`DeepSeekAdapter.prepareCall` 也绑定 connection。插件继承 base `LlmAdapter.prepareCall`,仅先 resolveModel,随后调用普通 stream 再读配置。本地 mock 已验证:在 endpoint A prepareCall 后将配置改到 B,原生发送到 A,插件发送到 B。这违背 rc.2 动态 adapter 应把能力解析与发送绑定同一配置代的要求;普通 stream 入口开始后捕获一次 config,不能覆盖准备到发送的时间段。来源:[rc.2 base prepareCall 要求](https://github.com/deepseek-ai/deepseek-harness/blob/dsh-v0.2.0-rc.2/packages/llm/llm/src/index.ts#L269-L282)、[原生 pi-ai prepareCall](https://github.com/deepseek-ai/deepseek-harness/blob/dsh-v0.2.0-rc.2/packages/llm/llm-pi-ai/src/adapter.ts#L318-L347)、[原生 DeepSeek prepareCall](https://github.com/deepseek-ai/deepseek-harness/blob/dsh-v0.2.0-rc.2/packages/llm/llm-deepseek/src/adapter.ts#L43-L48)、[插件 stream](../src/adapter.ts#L247)、本文验证表。
53
+
54
+ 另一个独立窗口是 credential 引用。插件 capture config 后先 await catalog,再调用零参数 resolveApiKey;该 resolver 重新读取 `current().apiKeyEnv`。本地 mock 使用真实 plugin.apply、live config 和两个固定假 key:阻塞 models.dev 元数据请求,期间将 endpoint A→B、key 引用 A→B,再释放目录请求,实际捕获到 **endpoint A 与 fixture key B**。这证明旧 endpoint 与新 credential 引用会组合,修复应把 credential 引用也绑定到同一次调用配置。原生 pi-ai 将已捕获的 profile 传给 credential resolver,官方直连将 connection 传给 resolveAuth。没有读取或使用真实 credential。来源:[插件等待与 credential](../src/adapter.ts#L251)、[插件 resolver](../src/index.ts#L92)、[原生 resolver](https://github.com/deepseek-ai/deepseek-harness/blob/dsh-v0.2.0-rc.2/packages/llm/llm-pi-ai/src/index.ts#L187-L205)、本文验证表。
55
+
56
+ ### 历史、回放与流式
57
+
58
+ 插件 context 基于原生 pi-ai 转换:相同的单一 systemPrompt 分离、assistant text/reasoning/tool-call、工具名恢复与图像引用转换。插件保留旧 DSH user 内嵌 `tool-result` 的兼容处理;rc.2 原生使用独立 `tool` role。两边都拒绝 developer 历史、tool-addition/tool-removal、deferred tool loading、assistant/system 图片以及 `GenerateOptions.stop`。来源:[插件 context](../src/conversion/context.ts#L51)、[旧 tool result 兼容](../src/conversion/context.ts#L232)、[原生 context](https://github.com/deepseek-ai/deepseek-harness/blob/dsh-v0.2.0-rc.2/packages/llm/llm-pi-ai/src/context.ts#L49-L64)、[原生 tools](https://github.com/deepseek-ai/deepseek-harness/blob/dsh-v0.2.0-rc.2/packages/llm/llm-pi-ai/src/context.ts#L129-L141)、[原生 stop](https://github.com/deepseek-ai/deepseek-harness/blob/dsh-v0.2.0-rc.2/packages/llm/llm-pi-ai/src/adapter.ts#L334-L336)。
59
+
60
+ 两边都用 `kind: pi-ai, version: 2` 回放,保存原生 responseId、thinking/text/tool 签名、providerThinkingLevel;不再可用的回放元数据会降级为 provider-neutral 内容并告警。插件额外保存 `sdkProvider`:持久来源为 `dsh-opencode-go`,SDK 同模型判断时恢复 `opencode-go`,解决私有 DSH 路由与 SDK provider 的身份差别。来源:[插件 replay](../src/conversion/replay.ts#L82)、[插件恢复](../src/conversion/replay.ts#L196)、[原生 replay](https://github.com/deepseek-ai/deepseek-harness/blob/dsh-v0.2.0-rc.2/packages/llm/llm-pi-ai/src/replay.ts#L79)。
61
+
62
+ **Anthropic model 别名导致思考签名回放丢失,已用 mock 复现。** 插件将 Anthropic 响应的 native `message.model` 存为 responseModel,回放时以 responseModel 恢复 assistant.model;原生始终以请求 model 恢复。pi-ai `transformMessages` 的同模型判断要求 provider、api 与 model.id 三者相同,跨模型时会将普通 thinking 转 text、去掉签名。本地 mock 请求 minimax-m3、响应 minimax-m3-returned-alias,将回放 envelope 经 JSON 往返后续聊同一请求 ID:原生发送 thinking 与 fixture-signature,插件发送包含相同思考文本的普通 text,签名消失。该验证证明明确条件下的回放缺陷,未证明真实 OpenCode 网关当前是否返回不同 alias。来源:[插件 responseModel 与恢复](../src/conversion/replay.ts#L82)、[插件恢复 model](../src/conversion/replay.ts#L231)、[原生恢复 model](https://github.com/deepseek-ai/deepseek-harness/blob/dsh-v0.2.0-rc.2/packages/llm/llm-pi-ai/src/replay.ts#L218-L226)、[SDK 同模型判断](../node_modules/opencode-go-pi-ai/dist/api/transform-messages.js#L65)、本文验证表。
63
+
64
+ 流式与用量映射的代码对照显示:除传入 requestedProvider 的回放差别,两边相同地发 text/reasoning/tool 参数增量、block-end、usage、finish;相同地处理空输出、上下文超限、401/403、quota/rate-limit、413、服务器错误、超时、截断和 abort。插件与原生都按 pi-ai 原值映射 input/output/total/cacheRead/cacheWrite,reasoning token 由 pi-ai 计入 output;不能把 cacheRead 字段等同于一定命中网关缓存。来源:[插件 stream](../src/conversion/stream.ts)、[原生 stream](https://github.com/deepseek-ai/deepseek-harness/blob/dsh-v0.2.0-rc.2/packages/llm/llm-pi-ai/src/stream.ts)。
65
+
66
+ 同版 SDK 仍有构建差异:上游源码维护 pi-ai 0.87.1 patch,删除工具参数每个 delta 对累计 JSON 的重复解析,保留完成时处理;插件私有 npm alias 不带该 patch。源码/应用构建采用此补丁时,长工具参数流式有额外性能优化;本次 npm 临时 host 不能自动代表补丁已应用,也没有进行性能基准测试。该项不能定性为正确性差异。来源:[上游 pi-ai patch](https://github.com/deepseek-ai/deepseek-harness/blob/dsh-v0.2.0-rc.2/patches/%40earendil-works__pi-ai%400.87.1.patch)、[插件依赖](../package.json)。
67
+
68
+ ### 图片、超时、重试与额度
69
+
70
+ 两边均只允许模型声明 image modality 且挂载 durable attachment service 时发送图片,以 base64 内联,每张默认 2048×2048 像素预算、1 MiB 原始编码目标,整个请求 20 MiB base64 上限。在 rc.2 上,超预算都会发 `IMAGE_OFFLOAD_REQUIRED`,由宿主记录卸载后重试,已卸载图片转文字占位。插件额外可配置 `maxImages`,并保留 0.1.5 旧宿主临时投影逻辑;这不是 rc.2 原生图像方案缺失。来源:[插件预算](../src/conversion/config.ts)、[插件 offload bridge](../src/conversion/image-offload.ts)、[原生预算](https://github.com/deepseek-ai/deepseek-harness/blob/dsh-v0.2.0-rc.2/packages/llm/llm-pi-ai/src/config.ts#L46-L62)、[原生图像处理](https://github.com/deepseek-ai/deepseek-harness/blob/dsh-v0.2.0-rc.2/packages/llm/llm-pi-ai/src/context.ts#L294-L338)。
71
+
72
+ 两边 stream idle timeout 默认均为 300,000 ms,并把 SDK `maxRetries` 设为 0,由 Harness agent recovery 拥有可见重试。原生额外配置 `timeoutMs`、`websocketConnectTimeoutMs`、`transport`、`cacheRetention`、`thinkingBudgets` 和 provider `retryPolicy`;插件只暴露 streamIdleTimeoutMs 等专用选项,没有覆写 providerRetryPolicy,使用 DSH base 的正常默认策略。因此 SDK retry=0 不意味着整个 agent 不重试。来源:[原生配置选项](https://github.com/deepseek-ai/deepseek-harness/blob/dsh-v0.2.0-rc.2/packages/llm/llm-pi-ai/src/config.ts#L153-L182)、[原生 stream options](https://github.com/deepseek-ai/deepseek-harness/blob/dsh-v0.2.0-rc.2/packages/llm/llm-pi-ai/src/adapter.ts#L114-L131)、[插件选项](../src/adapter.ts#L317)、[base retryPolicy](https://github.com/deepseek-ai/deepseek-harness/blob/dsh-v0.2.0-rc.2/packages/llm/llm/src/index.ts#L218-L224)。
73
+
74
+ 插件额度服务另外读取 `${baseURL}/usage`,在 Host 发送 Bearer key,展示账户额度;这是账户统计,与每次推理的 token usage 分开。原生 llm-pi-ai 没有这个专用 GoUsageService。来源:[插件 usage](../src/usage.ts#L24)、[usage UI](../src/client/UsagePill.tsx)。
75
+
76
+ ## 与 DeepSeek 官方直连的补充对照
77
+
78
+ rc.2 `dsh-llm-deepseek` 是独立 Messages transport,API key 官方路由由 `dsh-llm-deepseek-api-key` 挂载为 `deepseek-official`,使用 `x-api-key`;另外有 account 认证入口。它直接 fetch `/messages`,发送 `anthropic-version`、匿名 Harness user ID、Harness session ID 与 compaction header,支持官方 API extensions。插件通过 OpenCode Go 调用 DeepSeek 模型时走 OpenAI Completions 兼容协议,因此请求并不相同。来源:[官方 route 与 auth](https://github.com/deepseek-ai/deepseek-harness/blob/dsh-v0.2.0-rc.2/packages/llm/llm-deepseek-api-key/src/index.ts#L15-L41)、[Messages fetch](https://github.com/deepseek-ai/deepseek-harness/blob/dsh-v0.2.0-rc.2/packages/llm/llm-deepseek/src/adapter.ts#L103-L147)。
79
+
80
+ | 能力 | 插件 / 原生 pi-ai | 官方 Messages 直连 |
81
+ | --- | --- | --- |
82
+ | 思考默认 | 插件针对指定格式补 high;原生由 profile 控制 | 默认 high,支持 off / low / high / max;session-title 强制 off |
83
+ | 默认输出 cap | 取决于显式配置,不自动使用全目录容量 | 连接默认 256,000,模型项和显式请求值优先;对 DSH 暴露 defaultMaxTokens |
84
+ | 中途 system/developer 更新 | context 转换拒绝 developer / tool-change | 按模型声明处理 system in-history 与 developer 更新 |
85
+ | 延迟工具加载与增删 | 拒绝 deferred loading | 序列化 defer_loading、tool_addition、tool_removal |
86
+ | stop | adapter 拒绝 | 序列化 stop_sequences |
87
+ | 图片上传与复用 | base64 内联 | 优先 Files API,保存/复用 file_id;文件解析失败可退回 base64 |
88
+ | 图像 token 估算 | 未声明 provider 专用 pricing | DeepSeek token-grid pricing 与模型像素策略 |
89
+ | 原生回放 | pi-ai v2,多 provider 格式 | deepseek-messages v1,thinking signature |
90
+
91
+ 来源:[官方 serialize](https://github.com/deepseek-ai/deepseek-harness/blob/dsh-v0.2.0-rc.2/packages/llm/llm-deepseek/src/serialize.ts#L56-L167)、[官方 modelInfo](https://github.com/deepseek-ai/deepseek-harness/blob/dsh-v0.2.0-rc.2/packages/llm/llm-deepseek/src/model-info.ts#L61-L99)、[官方图片](https://github.com/deepseek-ai/deepseek-harness/blob/dsh-v0.2.0-rc.2/packages/llm/llm-deepseek/src/images.ts#L38-L110)、[file fallback](https://github.com/deepseek-ai/deepseek-harness/blob/dsh-v0.2.0-rc.2/packages/llm/llm-deepseek/src/adapter.ts#L84-L105)、[官方 image pricing](https://github.com/deepseek-ai/deepseek-harness/blob/dsh-v0.2.0-rc.2/packages/llm/llm-deepseek/src/request-pricing.ts)、[官方 replay](https://github.com/deepseek-ai/deepseek-harness/blob/dsh-v0.2.0-rc.2/packages/llm/llm-deepseek/src/replay.ts#L29-L64)。
92
+
93
+ 官方 Messages 流式解析独立校验块、JSON 与 SSE 结束事件;其 totalTokens 在 translator 中按输入、输出、cache read/write 相加。插件/原生 pi-ai 使用 SDK 提供的 usage.totalTokens。不同协议可能把 cache 字段包含在哪个输入字段中的语义不同,不能直接凭字段名推断账单差异;需要同一服务实际 usage 证据。来源:[官方 translator](https://github.com/deepseek-ai/deepseek-harness/blob/dsh-v0.2.0-rc.2/packages/llm/llm-deepseek/src/translate.ts#L143-L165)、[插件 mapUsage](../src/conversion/stream.ts#L24)。
94
+
95
+ ## 验证与建议
96
+
97
+ 已完成固定 tag 源码和本地 npm 0.87.1 实现的只读对照。`npm run test:compat -- v020-rc2` 全部通过,`npm run check:dist` 32 个产物通过;另在临时目录安装真实 rc.2 npm 包与插件 tarball,启动两个 loopback HTTP mock,以假 key 和固定 SSE 响应运行对照,实际捕获 URL、body、会话 header 与 User-Agent。模型目录和 models.dev 响应由 fixture 提供,probe 阻止其他网络推理请求。所有断言通过,结果如下。
98
+
99
+ | 验证场景 | 原生结果 | 插件结果 |
100
+ | --- | --- | --- |
101
+ | DeepSeek V4 Flash 不选 effort | thinking disabled | thinking enabled / high |
102
+ | Qwen3.6-plus,fixture 声明 toggle | enable_thinking=false | enable_thinking=true / high |
103
+ | GLM5.3 不选 effort | 无默认 effort | 无默认 effort |
104
+ | 上述三模型统一显式 effort / maxTokens=512 / temperature=0.2 | 完整 body、session header、User-Agent | 与原生 deepEqual |
105
+ | 指定 comparison-session | x-opencode-session 同值、Harness User-Agent | 两项完全一致 |
106
+ | 不指定 sessionId | 无会话 header | 随机 UUID 会话 header |
107
+ | 配置 cap=128,请求 cap=512 | 模型 defaultMaxTokens=128,实际 512 | 无 defaultMaxTokens,实际 128 |
108
+ | prepareCall 后 A→B | 实际请求 A | 实际请求 B |
109
+ | 插件目录请求期间改 endpoint/key 引用 | 原生源码绑定已捕获 profile | 实际组合旧 endpoint A 与 fixture key B |
110
+ | Anthropic model alias,经 JSON 往返后续聊 | thinking 与签名保留 | thinking 变 text,签名丢失 |
111
+
112
+ 临时验证证据:[results.json](/tmp/dsh-native-call-probe.b3lfUG/results.json)、[probe.mjs](/tmp/dsh-native-call-probe.b3lfUG/probe.mjs)。这些是本次运行的临时诊断材料,没有加入永久测试;临时目录清理后链接可能失效。没有真实网关推理质量、缓存命中率、延迟或计费对比结果。
113
+
114
+ 验证准备过程中,首轮临时 probe 未设置隔离的 DSH_HOME,曾将默认公共 models.dev 元数据缓存写为仅含 4 个模型的 fixture。随后脚本改为按 PID 隔离临时目录,并从官方 `https://models.dev/api.json` 成功恢复公共缓存,校验源 URL、33 个 OpenCode Go 模型、ETag 存在与正确缓存格式。用户配置和凭据未修改。
115
+
116
+ 建议优先修复调用配置及 credential 引用冻结与 Anthropic alias replay;随后决定 modelLimits.maxTokens 是强制 cap 还是默认值,并确保 DSH 记录与实际发送一致。文档应更新原生 OpenCode Go 已支持 x-opencode-session 的事实。是否继续使用插件主要取决于是否需要动态目录和额度/专用设置;仅为添加会话 header,rc.2 已无需单独插件。
@@ -1,5 +1,29 @@
1
1
  # Verification
2
2
 
3
+ ## Release 0.1.20: usage pill and settings draft fixes (2026-10-02)
4
+
5
+ The release includes the Host scope receiver fix and shared no-op draft predicate from PR #34. The installed-client compatibility fixture now reads instance state and exercises both SSR snapshot reads and client subscription cleanup. Desktop follow-up confirmed that saving Always works after restarting a Host process that predated the installed plugin update; the upgrade instructions now explicitly require quitting and reopening desktop Harness.
6
+
7
+ On macOS / Node 24.14.1, `DSH_COMPAT_CONCURRENCY=4 npm run verify` passed **401 tests in 28 files**, freshness checks for **34 shipped build files**, **all nine installed-host generations**, and both **npm and pnpm Git installation checks**. The compatibility run took **87.9 seconds** including cleanup; installation checks took **11.8 seconds**. Both installation paths loaded the shipped artifacts without source builds or plugin build approval and retained the private pi-ai 0.87.1 alongside the independent peer's 0.85.1. The test suite used local gateways and isolated consumers.
8
+
9
+ ## Usage pill rendering on a real Host and the staged override badge (2026-10-02)
10
+
11
+ The browser half no longer hands the Host settings scope's methods over as plain values. `UsagePill` subscribed with `useSyncExternalStore(settings.subscribe, settings.getSnapshot, ...)`, and a Host scope is a class instance whose methods read their own state (`ConfigFormController` behind `ctx.configForms.get(entryId)`, `SettingsScopeController` behind `ctx.settingsScope.bind(spec)`); both providers return that instance as-is. The detached reference lost its receiver, so the pill threw `TypeError: Cannot read properties of undefined (reading 'store')` on its first render, the slot's per-entry error boundary removed it, and no usage mode could ever show a pill — including Always, which is why a saved `usageDisplay: always` had no visible effect. React now reaches the scope through `useCallback` wrappers, the same shape the Host's own slot outlet uses.
12
+
13
+ The staged form's override badge now answers for the same state its save plan does. `plan` skipped a draft equal to the field's effective value while `field` reported it as an override, so a value typed back to its original (`60 → 59 → 60`) disabled Save and Discard yet kept the "overridden" badge, its reset button, and the advanced card's badge. Both paths now share one `unchangedDraft` predicate: a no-op draft reports whatever the user layer already holds, a clear still previews as not overridden, and an invalid draft still blocks the save.
14
+
15
+ Before the fix, `npx vitest run tests/usage-pill.client.spec.tsx tests/stores.client.spec.ts` failed 25 of 54 cases: all **24** usage-pill cases (including the new receiver case) with the detached-call `TypeError`, and the new no-op-draft case on `overridden`. The test double now keeps its scope methods on the prototype (`tests/support/settings-scope.ts`) and the installed-host fixture builds the same class shape, so a method passed as a value fails in tests exactly as it failed in a browser; before this change every double used arrow-function literals, which is what hid the regression from the suite and from the `v020-rc2` compatibility row.
16
+
17
+ Host/Client type checks and production compilation passed; `npm run test:ci` passed **401 tests in 28 files** and freshness checks for all **34 shipped artifacts** on Windows / Node 24.15.0. `DSH_COMPAT_CONCURRENCY=1 npm run test:compat -- v020-rc2 v017-rc2` passed both installed generations — 0.2.0-rc.2 and 0.1.7-rc.2 (both use the `configForms` path) — including the client fixture's module table, settings binding, rendering, CSS and cleanup. On this machine the client fixture also exceeded the runner's 30-second per-fixture limit before this change, so the two local rows ran with that limit temporarily raised to 180 seconds; the committed limit is unchanged. The other seven host generations, macOS and Linux, and a live Desktop session were not exercised. Every check used loopback fixtures; no installed user profile, GitHub issue or npm publication was changed.
18
+
19
+ Follow-up on macOS / Node 24.14.1: the installed-host fixture now reads `this.snapshot` and `this.listeners`; prototype methods that only closed over local variables still allowed detached calls. Its SSR checks exercise the snapshot reader, and a client mount/unmount now checks the scope subscription and cleanup while Auto keeps the pill hidden. The receiver unit case also checks a live display change and subscription disposal. Two temporary negative-control bundles failed as expected: the original detached snapshot reader threw on `snapshot`, and a client with only the subscription detached threw on `listeners`; the fixed bundle passed. Both type checks, **401 tests in 28 files**, all **34 shipped artifacts**, and **all nine installed-host generations** passed. The compatibility run took **88.2 seconds** including cleanup, with the committed 30-second fixture limit unchanged. The matrix exercises legacy `settingsScope` on 0.1.5/0.1.6 and `configForms` on 0.1.7/0.2. No live Desktop UI or installed user profile was exercised.
20
+
21
+ ## Release 0.1.19: consolidate pending features (2026-10-02)
22
+
23
+ The prepared-call configuration, Anthropic alias replay and usage-display changes are now integrated with 0.1.18's MiMo controls. Prepared calls retain the captured endpoint, credential reference and nested capacities; later calls use updated settings. Anthropic replay preserves signed thinking against the requested model ID. Settings now offer Auto / Always / Off usage visibility, with Off and a disabled plugin stopping the poller. The existing default remains Auto.
24
+
25
+ Host/Client type checks, production compilation, **398 tests in 28 files**, and freshness checks for **34 shipped artifacts** passed on macOS / Node 24.14.1. All **nine installed-host generations** passed, including DSH 0.2.0-rc.2, prepared-call dispatch, signed alias replay, MiMo Default/Off controls and usage-display settings. The complete compatibility run took 73.8 seconds including cleanup. Both npm and pnpm 11.7.0 Git installations passed without plugin build approval, retaining the plugin's private pi-ai 0.87.1 and the independent peer's 0.85.1. This integration uses isolated local gateway fixtures; the earlier live MiMo observations are recorded in the 0.1.18 entry.
26
+
3
27
  ## Release 0.1.18 validation (2026-10-02)
4
28
 
5
29
  The 0.1.18 manifest passed Host/Client type checks, production compilation, all **381 tests in 27 files**, and freshness checks for **33 shipped artifacts** on macOS / Node 24.14.1. Both npm and pnpm 11.7.0 Git installations passed with the new version, without plugin build approval; the plugin retains private pi-ai 0.87.1 and the independent peer retains public pi-ai 0.85.1. The nine-host compatibility checks and live OpenCode Go observations for this reasoning fix are recorded below.
@@ -23,6 +47,18 @@ Two further live calls went through the rebuilt plugin, DSH LLM runtime and actu
23
47
 
24
48
  Host/Client type checks and production compilation passed. `npm run test:ci` passed **381 tests in 27 files** and freshness checks for all **33 shipped artifacts**. New regressions cover online and outage paths, all four supported choices, rejection of unsupported choices before inference, Default/Off alternation without shared-map mutation, exact-model/protocol boundaries, future advertised `none`, and native controls. `DSH_COMPAT_CONCURRENCY=3 npm run test:compat` passed all **nine isolated installed-host generations**, from 0.1.5-rc.1 through **0.2.0-rc.2**, with 13 reasoning streams per generation plus the existing Host/Client/transcript/profile/bundle checks; total time including cleanup was 88.2 seconds. Other operating systems and a Desktop GUI session were not exercised for this change.
25
49
 
50
+ ## Prepared-call configuration and Anthropic alias replay (2026-09-30)
51
+
52
+ The adapter now copies its configuration, including nested model limits, before catalog discovery. Its `prepareCall` binds the resolved model, catalog provider and that configuration to the eventual stream. Credential resolution receives the captured configuration instead of re-reading the current key reference. This keeps an already prepared request on its original endpoint and credential reference while later requests use changed settings. Direct streams use the same capture path. Existing credential values still resolve through the Host service at dispatch; the reference is the frozen fact.
53
+
54
+ Anthropic replay now reconstructs the assistant with the requested model ID. The provider-reported alias remains in `responseModel` as informational metadata, preserving envelope version 2 and previously saved history. pi-ai therefore retains signed thinking when continuing the same requested model, while an actual switch to another model still strips those signatures.
55
+
56
+ Before the fix, `npx vitest run tests/call-snapshot.spec.ts tests/provider-identity.spec.ts` failed all four new regressions: nested limits changed during discovery, a prepared request went to the new endpoint, a direct stream combined the old endpoint with the new key reference, and JSON-restored Anthropic alias history lost its thinking signature. The same tests pass after the fix, also checking that subsequent requests use the new endpoint/key/capacities and that actual model changes still remove incompatible signatures.
57
+
58
+ `npm test` passed Host/Client type checks, rebuilt the distributed artifacts and passed **371 tests in 27 files**. `npm run check:dist` confirmed all **32** shipped files match source. The installed-package matrix passed all **nine** host generations from `0.1.5-rc.1` through `0.2.0-rc.2`; each now checks prepared endpoint/key/limit retention and signed Anthropic alias replay through a streamed tool call, JSON restoration and tool-result continuation.
59
+
60
+ The original differential probe was rerun with the rebuilt tarball and real `0.2.0-rc.2` dependencies. Both native and plugin prepared requests stayed on endpoint A, the plugin paired endpoint A with fixture key A during a held catalog refresh, and both retained Anthropic thinking signatures. Existing reasoning defaults and forced output-cap behavior remained unchanged. All requests used isolated temporary caches, loopback gateways and dummy credentials; no paid inference or installed user profile was changed during this fix.
61
+
26
62
  ## Release 0.1.17 validation (2026-09-30)
27
63
 
28
64
  After integrating the npm 10 lockfile correction, the 0.1.17 manifest passed Host/Client type checks, all **367 tests in 26 files**, and freshness checks for **32 shipped artifacts** on macOS / Node 24.14.1. Both npm and pnpm 11.7.0 Git installs passed without plugin build approval; the installed package uses private pi-ai 0.87.1 while the independent peer probe keeps public pi-ai 0.85.1. The nine-generation runtime compatibility results below remain applicable: the subsequent integration changes only lockfile metadata, documentation and the package version.
package/lib/client.js CHANGED
@@ -375,6 +375,10 @@ function Capacity({ model, field, limit, disabled, t, locale, onChange }) {
375
375
  ] });
376
376
  }
377
377
 
378
+ // src/usage-display.ts
379
+ var USAGE_DISPLAY_MODES = ["auto", "always", "off"];
380
+ var DEFAULT_USAGE_DISPLAY = "auto";
381
+
378
382
  // src/client/Section.tsx
379
383
  var import_jsx_runtime2 = require("react/jsx-runtime");
380
384
  var ChevronDown = primitives.IconChevronDownOutlineRegular ?? primitives.IconChevronDownOutline14;
@@ -397,7 +401,22 @@ function ValueField(props) {
397
401
  )
398
402
  ] }) : null
399
403
  ] }),
400
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
404
+ props.options ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
405
+ "select",
406
+ {
407
+ id: props.id,
408
+ name: props.id,
409
+ "aria-describedby": hintId,
410
+ className: props.field.invalid ? Section_default.inputInvalid : Section_default.input,
411
+ "aria-invalid": props.field.invalid || void 0,
412
+ value: props.field.text,
413
+ disabled: props.disabled,
414
+ onChange: (event) => {
415
+ props.onEdit(event.target.value);
416
+ },
417
+ children: props.options.map((option) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("option", { value: option.value, children: option.label }, option.value))
418
+ }
419
+ ) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
401
420
  "input",
402
421
  {
403
422
  id: props.id,
@@ -492,7 +511,7 @@ function Loaded(props) {
492
511
  resetLabel: t("reset"),
493
512
  disabled
494
513
  };
495
- const advancedOverridden = state.apiKeyEnv.overridden || state.baseURL.overridden || state.refreshMinutes.overridden || state.streamIdleTimeoutMs.overridden || state.maxImages.overridden || state.maxRequestImageBytes.overridden || state.requestImagePixelBudget.overridden || state.requestImageMaxBytes.overridden;
514
+ const advancedOverridden = state.usageDisplay.overridden || state.apiKeyEnv.overridden || state.baseURL.overridden || state.refreshMinutes.overridden || state.streamIdleTimeoutMs.overridden || state.maxImages.overridden || state.maxRequestImageBytes.overridden || state.requestImagePixelBudget.overridden || state.requestImageMaxBytes.overridden;
496
515
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: Section_default.page, children: [
497
516
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: Section_default.pageBody, children: [
498
517
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: Section_default.pageTop, children: [
@@ -611,6 +630,23 @@ function Loaded(props) {
611
630
  advanced ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { id: "opencode-go-advanced", className: Section_default.cardBody, children: [
612
631
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: Section_default.hint, children: t("advancedHint") }),
613
632
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: Section_default.advancedGrid, children: [
633
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
634
+ ValueField,
635
+ {
636
+ id: "opencode-go-usage-display",
637
+ label: t("usageDisplayLabel"),
638
+ hint: t("usageDisplayHint"),
639
+ field: state.usageDisplay,
640
+ options: USAGE_DISPLAY_MODES.map((value) => ({ value, label: t(`usageDisplay_${value}`) })),
641
+ ...fieldProps,
642
+ onEdit: (text) => {
643
+ props.edit("usageDisplay", text);
644
+ },
645
+ onReset: () => {
646
+ props.resetField("usageDisplay");
647
+ }
648
+ }
649
+ ),
614
650
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
615
651
  ValueField,
616
652
  {
@@ -896,7 +932,9 @@ var StagedForm = class {
896
932
  const write = staged.clear ? { kind: "clear" } : spec.parse(staged.text);
897
933
  return {
898
934
  text: staged.text,
899
- overridden: write?.kind === "set",
935
+ // A draft a save would skip leaves the user layer exactly as it stands, so
936
+ // the badge answers for that standing override instead of the edit.
937
+ overridden: this.unchangedDraft(field, staged) ? this.stored(field) : write?.kind === "set",
900
938
  invalid: write === void 0
901
939
  };
902
940
  }
@@ -967,7 +1005,7 @@ var StagedForm = class {
967
1005
  if (this.stored(field)) plan.push({ field, run: () => this.clear(field) });
968
1006
  continue;
969
1007
  }
970
- if (staged.text === spec.format(this.sectionValue(field))) continue;
1008
+ if (this.unchangedDraft(field, staged)) continue;
971
1009
  const write = spec.parse(staged.text);
972
1010
  if (write === void 0) plan.push({ field, run: void 0 });
973
1011
  else if (write.kind === "clear") plan.push({ field, run: () => this.clear(field) });
@@ -1009,6 +1047,16 @@ var StagedForm = class {
1009
1047
  const user = this.userLayer();
1010
1048
  return user !== void 0 && Object.hasOwn(user, field);
1011
1049
  }
1050
+ /**
1051
+ * Whether a draft already equals the field's effective value, so a save would
1052
+ * write nothing. The save plan and the override badge both answer for it.
1053
+ * @param field - field name inside the namespace section.
1054
+ * @param staged - the staged edit to test.
1055
+ * @returns true when the draft is a no-op; a clear is never one.
1056
+ */
1057
+ unchangedDraft(field, staged) {
1058
+ return !staged.clear && staged.text === this.spec(field).format(this.sectionValue(field));
1059
+ }
1012
1060
  publish() {
1013
1061
  for (const listener of this.listeners) listener();
1014
1062
  }
@@ -1050,6 +1098,11 @@ var OpencodeGoSectionController = class {
1050
1098
  // Present so the shared override/reset machinery tracks the field; the
1051
1099
  // page's switch writes it directly instead of staging it.
1052
1100
  booleanField("enabled"),
1101
+ {
1102
+ field: "usageDisplay",
1103
+ format: (value) => typeof value === "string" ? value : DEFAULT_USAGE_DISPLAY,
1104
+ parse: (text) => USAGE_DISPLAY_MODES.some((mode) => mode === text) ? { kind: "set", value: text } : void 0
1105
+ },
1053
1106
  textField("apiKeyEnv"),
1054
1107
  textField("baseURL"),
1055
1108
  numberField("refreshMinutes"),
@@ -1095,6 +1148,7 @@ var OpencodeGoSectionController = class {
1095
1148
  return {
1096
1149
  ...this.form.shell(),
1097
1150
  enabled: this.enabled(),
1151
+ usageDisplay: this.form.field("usageDisplay"),
1098
1152
  modelVisibility: this.scope.getSnapshot().value?.modelVisibility ?? {},
1099
1153
  pickerSaving: this.pickerSaving,
1100
1154
  pickerFailed: this.pickerFailed,
@@ -1411,9 +1465,14 @@ var en = {
1411
1465
  limitsEmpty: "Read the model list first; capacities are edited per model the gateway serves.",
1412
1466
  limitsContextLabel: "Context window for {name}",
1413
1467
  limitsOutputLabel: "Max output for {name}",
1414
- advancedSummary: "Credential name \xB7 Gateway URL \xB7 Refresh \xB7 Idle timeout \xB7 Image limits",
1468
+ advancedSummary: "Usage display \xB7 Credential name \xB7 Gateway URL \xB7 Refresh \xB7 Idle timeout \xB7 Image limits",
1415
1469
  advancedLabel: "Advanced settings",
1416
- advancedHint: "Credential name, gateway URL, and adapter tuning. The inherited values work as they are; change them only if you know you need to.",
1470
+ advancedHint: "Usage display, credential name, gateway URL, and adapter tuning. The inherited values work as they are; change them only if you know you need to.",
1471
+ usageDisplayLabel: "Usage display",
1472
+ usageDisplayHint: "Auto shows usage only when a DSH OpenCode Go model is selected. Always keeps it visible with any model, including the built-in OpenCode Go provider. Off hides it and stops polling. While visible, usage refreshes every minute and the panel opens on click. Disabling OpenCode Go hides it in every mode. Save to apply.",
1473
+ usageDisplay_auto: "Auto (default)",
1474
+ usageDisplay_always: "Always",
1475
+ usageDisplay_off: "Off",
1417
1476
  apiKeyEnvLabel: "Credential reference",
1418
1477
  apiKeyEnvHint: "The name the key is resolved from, addressed through the credentials service.",
1419
1478
  baseURLLabel: "Gateway URL",
@@ -1530,9 +1589,14 @@ var zh = {
1530
1589
  limitsEmpty: "\u8BF7\u5148\u8BFB\u53D6\u6A21\u578B\u5217\u8868\uFF1B\u5BB9\u91CF\u6309\u7F51\u5173\u63D0\u4F9B\u7684\u6A21\u578B\u9010\u4E2A\u7F16\u8F91\u3002",
1531
1590
  limitsContextLabel: "{name} \u7684\u4E0A\u4E0B\u6587\u7A97\u53E3",
1532
1591
  limitsOutputLabel: "{name} \u7684\u6700\u5927\u8F93\u51FA",
1533
- advancedSummary: "\u51ED\u8BC1\u5F15\u7528\u540D \xB7 \u7F51\u5173\u5730\u5740 \xB7 \u76EE\u5F55\u5237\u65B0 \xB7 \u6D41\u7A7A\u95F2\u8D85\u65F6 \xB7 \u56FE\u7247 4 \u9879",
1592
+ advancedSummary: "\u989D\u5EA6\u663E\u793A \xB7 \u51ED\u8BC1\u5F15\u7528\u540D \xB7 \u7F51\u5173\u5730\u5740 \xB7 \u76EE\u5F55\u5237\u65B0 \xB7 \u6D41\u7A7A\u95F2\u8D85\u65F6 \xB7 \u56FE\u7247 4 \u9879",
1534
1593
  advancedLabel: "\u9AD8\u7EA7\u8BBE\u7F6E",
1535
- advancedHint: "\u51ED\u8BC1\u540D\u79F0\u3001\u7F51\u5173\u5730\u5740\u4E0E\u9002\u914D\u5668\u8C03\u4F18\u53C2\u6570\u3002\u7EE7\u627F\u6765\u7684\u53D6\u503C\u5373\u53EF\u6B63\u5E38\u4F7F\u7528\uFF0C\u9664\u975E\u4F60\u660E\u786E\u77E5\u9053\u9700\u8981\u4FEE\u6539\uFF0C\u5426\u5219\u4E0D\u5FC5\u52A8\u8FD9\u91CC\u3002",
1594
+ advancedHint: "\u989D\u5EA6\u663E\u793A\u3001\u51ED\u8BC1\u540D\u79F0\u3001\u7F51\u5173\u5730\u5740\u4E0E\u9002\u914D\u5668\u8C03\u4F18\u53C2\u6570\u3002\u7EE7\u627F\u6765\u7684\u53D6\u503C\u5373\u53EF\u6B63\u5E38\u4F7F\u7528\uFF0C\u6309\u9700\u4FEE\u6539\u3002",
1595
+ usageDisplayLabel: "\u989D\u5EA6\u663E\u793A",
1596
+ usageDisplayHint: "\u81EA\u52A8\uFF1A\u4EC5\u9009\u4E2D DSH OpenCode Go \u6A21\u578B\u65F6\u663E\u793A\u3002\u5E38\u9A7B\uFF1A\u4F7F\u7528\u4EFB\u610F\u6A21\u578B\u65F6\u5747\u663E\u793A\uFF0C\u5305\u62EC\u5BBF\u4E3B\u81EA\u5E26\u7684 OpenCode Go\u3002\u5173\u95ED\uFF1A\u9690\u85CF\u5E76\u505C\u6B62\u8F6E\u8BE2\u3002\u663E\u793A\u65F6\u6BCF\u5206\u949F\u5237\u65B0\uFF0C\u70B9\u51FB\u80F6\u56CA\u624D\u5C55\u5F00\u9762\u677F\uFF1B\u505C\u7528 OpenCode Go \u540E\u6240\u6709\u6A21\u5F0F\u5747\u9690\u85CF\u3002\u4FDD\u5B58\u540E\u751F\u6548\u3002",
1597
+ usageDisplay_auto: "\u81EA\u52A8\uFF08\u9ED8\u8BA4\uFF09",
1598
+ usageDisplay_always: "\u5E38\u9A7B",
1599
+ usageDisplay_off: "\u5173\u95ED",
1536
1600
  apiKeyEnvLabel: "\u51ED\u8BC1\u5F15\u7528\u540D",
1537
1601
  apiKeyEnvHint: "key \u89E3\u6790\u6240\u4F9D\u636E\u7684\u5F15\u7528\u540D\uFF0C\u7ECF\u51ED\u8BC1\u670D\u52A1\u5BFB\u5740\u3002",
1538
1602
  baseURLLabel: "\u7F51\u5173\u5730\u5740",
@@ -1636,9 +1700,14 @@ function usageLevel(window) {
1636
1700
  if (window.status === "rate-limited" || window.percent >= 100) return UsagePill_default.limited;
1637
1701
  return window.percent >= 80 ? UsagePill_default.high : void 0;
1638
1702
  }
1639
- function UsagePill({ directory, ...props }) {
1703
+ function UsagePill({ directory, settings, ...props }) {
1640
1704
  const state = (0, import_react3.useSyncExternalStore)(directory.subscribe, directory.getSnapshot, directory.getSnapshot);
1641
- return state.current?.provider === PROVIDER_ID ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ActiveUsage, { ...props }) : null;
1705
+ const readSettings = (0, import_react3.useCallback)(() => settings.getSnapshot(), [settings]);
1706
+ const subscribeSettings = (0, import_react3.useCallback)((listener) => settings.subscribe(listener), [settings]);
1707
+ const config = (0, import_react3.useSyncExternalStore)(subscribeSettings, readSettings, readSettings);
1708
+ const mode = config.value?.usageDisplay ?? DEFAULT_USAGE_DISPLAY;
1709
+ const visible = config.status === "ready" && config.value?.enabled !== false && (mode === "always" || mode === "auto" && state.current?.provider === PROVIDER_ID);
1710
+ return visible ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ActiveUsage, { ...props }) : null;
1642
1711
  }
1643
1712
  function ActiveUsage({ readUsage, t, getLocale }) {
1644
1713
  const [snapshot, setSnapshot] = (0, import_react3.useState)(null);
@@ -1774,7 +1843,7 @@ function ActiveUsage({ readUsage, t, getLocale }) {
1774
1843
  }
1775
1844
 
1776
1845
  // src/client/usage.ts
1777
- function registerUsagePill(ctx) {
1846
+ function registerUsagePill(ctx, settings) {
1778
1847
  ctx.inject(["modelDirectories", "sessions", "remote.session"], (scope) => {
1779
1848
  scope.inject(["remote.opencodeGoUsage"], (ready) => {
1780
1849
  const readUsage = async () => {
@@ -1789,6 +1858,7 @@ function registerUsagePill(ctx) {
1789
1858
  order: 1e3,
1790
1859
  inject: (sessionId) => ({
1791
1860
  directory: ready.modelDirectories.directoryFor(sessionId).store,
1861
+ settings,
1792
1862
  readUsage,
1793
1863
  getLocale: () => ready.locale.getLocale().active,
1794
1864
  t: (key) => translate(key)
@@ -1803,7 +1873,6 @@ var NS = "settings.opencode-go";
1803
1873
  var inject = ["slots", "locale", "remote", "remote.credentials", "remote.llm"];
1804
1874
  function apply(ctx) {
1805
1875
  ctx.effect(() => ctx.locale.register(NS, { zh, en }), "llm-opencode-go: copy dictionaries");
1806
- registerUsagePill(ctx);
1807
1876
  const modelsReady = ctx.remote.$mount(goRemote);
1808
1877
  ctx.effect(async () => await modelsReady);
1809
1878
  ctx.inject(["configForms", "remote.opencodeGoModels"], (child) => {
@@ -1818,6 +1887,7 @@ function apply(ctx) {
1818
1887
  });
1819
1888
  }
1820
1889
  function mountSettings(ctx, scope, modelsReady) {
1890
+ registerUsagePill(ctx, scope);
1821
1891
  const controller = new OpencodeGoSectionController(scope, ctx, async () => {
1822
1892
  await modelsReady;
1823
1893
  return ctx.remote.opencodeGoModels.read();
package/lib/index.js CHANGED
@@ -241,8 +241,8 @@ function replayedAssistant(message, source, rawState) {
241
241
  content,
242
242
  api: state.response.api,
243
243
  provider: state.response.sdkProvider ?? state.response.provider,
244
- // Anthropic reports aliases and fallbacks as model, unlike Completions' informational responseModel.
245
- model: state.response.api === "anthropic-messages" ? state.response.responseModel ?? state.response.model : state.response.model,
244
+ // SDK signature matching uses the requested identity even when Anthropic reports an alias.
245
+ model: state.response.model,
246
246
  ...state.response.responseModel === void 0 ? {} : { responseModel: state.response.responseModel },
247
247
  ...state.response.responseId === void 0 ? {} : { responseId: state.response.responseId },
248
248
  ...state.response.providerThinkingLevel === void 0 ? {} : { providerThinkingLevel: state.response.providerThinkingLevel },
@@ -1459,11 +1459,18 @@ function metadataFailureMessage(error) {
1459
1459
  // src/config.ts
1460
1460
  import { MAX_TIMER_DELAY_MS } from "@deepseek-ai/dsh-timeout";
1461
1461
  import z from "@deepseek-ai/schemastery";
1462
+
1463
+ // src/usage-display.ts
1464
+ var USAGE_DISPLAY_MODES = ["auto", "always", "off"];
1465
+ var DEFAULT_USAGE_DISPLAY = "auto";
1466
+
1467
+ // src/config.ts
1462
1468
  var DEFAULT_API_KEY_ENV = "OPENCODE_API_KEY";
1463
1469
  var DEFAULT_REFRESH_MINUTES = 60;
1464
1470
  var DEFAULT_STREAM_IDLE_TIMEOUT_MS = 3e5;
1465
1471
  var fields = {
1466
1472
  enabled: z.boolean().default(true),
1473
+ usageDisplay: z.union(USAGE_DISPLAY_MODES.map((mode) => z.const(mode))).default(DEFAULT_USAGE_DISPLAY),
1467
1474
  modelVisibility: z.dict(z.boolean().required()).default({}),
1468
1475
  apiKeyEnv: z.string().role("credential-ref").default(DEFAULT_API_KEY_ENV),
1469
1476
  baseURL: z.string().default(DEFAULT_BASE_URL),
@@ -1570,13 +1577,25 @@ var OpencodeGoAdapter = class extends LlmAdapter {
1570
1577
  }));
1571
1578
  }
1572
1579
  async resolveModel(_provider, model, signal) {
1573
- const config = this.options.config();
1574
- const snapshot = await this.catalogOf(config).forModel(model, signal);
1575
- const resolved = snapshot.models.get(model);
1580
+ return this.modelInfo((await this.callSnapshot(model, signal)).model);
1581
+ }
1582
+ /** Copy nested limits before discovery can yield to a settings update. */
1583
+ async callSnapshot(model, signal) {
1584
+ const config = structuredClone(this.options.config());
1585
+ const catalog = await this.catalogOf(config).forModel(model, signal);
1586
+ const resolved = catalog.models.get(model);
1576
1587
  if (resolved === void 0) {
1577
1588
  throw new LlmError6(`opencode-go has no model "${model}"`, "UNKNOWN_MODEL");
1578
1589
  }
1579
- return this.modelInfo(withModelLimit(resolved, config.modelLimits));
1590
+ return { config, catalog, model: withModelLimit(resolved, config.modelLimits) };
1591
+ }
1592
+ /** Keep capability resolution and eventual dispatch on the same configuration. */
1593
+ async prepareCall(_provider, model, signal) {
1594
+ const snapshot = await this.callSnapshot(model, signal);
1595
+ return {
1596
+ model: this.modelInfo(snapshot.model),
1597
+ stream: (options) => this.streamWithSnapshot(options, snapshot)
1598
+ };
1580
1599
  }
1581
1600
  /** Describe one model: capacities plus the reasoning levels it actually offers. */
1582
1601
  modelInfo(model) {
@@ -1613,31 +1632,32 @@ var OpencodeGoAdapter = class extends LlmAdapter {
1613
1632
  );
1614
1633
  }
1615
1634
  async *stream(options) {
1635
+ if (options.stop !== void 0) {
1636
+ throw new LlmError6("llm-opencode-go does not support GenerateOptions.stop", "UNSUPPORTED_OPTION");
1637
+ }
1638
+ let snapshot;
1639
+ try {
1640
+ snapshot = await this.callSnapshot(options.model, options.signal);
1641
+ } catch (error) {
1642
+ if (!options.signal?.aborted) throw error;
1643
+ yield { type: "finish", reason: {
1644
+ kind: "aborted",
1645
+ failure: { code: "ABORTED", message: "opencode-go request aborted by caller" }
1646
+ } };
1647
+ return;
1648
+ }
1649
+ yield* this.streamWithSnapshot(options, snapshot);
1650
+ }
1651
+ async *streamWithSnapshot(options, snapshot) {
1616
1652
  var _stack = [];
1617
1653
  try {
1618
1654
  if (options.stop !== void 0) {
1619
1655
  throw new LlmError6("llm-opencode-go does not support GenerateOptions.stop", "UNSUPPORTED_OPTION");
1620
1656
  }
1621
- const config = this.options.config();
1622
- let snapshot;
1623
- try {
1624
- snapshot = await this.catalogOf(config).forModel(options.model, options.signal);
1625
- } catch (error) {
1626
- if (!options.signal?.aborted) throw error;
1627
- yield { type: "finish", reason: {
1628
- kind: "aborted",
1629
- failure: { code: "ABORTED", message: "opencode-go request aborted by caller" }
1630
- } };
1631
- return;
1632
- }
1633
- const advertised = snapshot.models.get(options.model);
1634
- if (advertised === void 0) {
1635
- throw new LlmError6(`opencode-go has no model "${options.model}"`, "UNKNOWN_MODEL");
1636
- }
1637
- const model = withModelLimit(advertised, config.modelLimits);
1657
+ const { config, catalog, model } = snapshot;
1638
1658
  const outputLimit = config.modelLimits[model.id]?.maxTokens;
1639
1659
  const maxTokens = outputLimit == null ? options.maxTokens : Math.min(options.maxTokens ?? outputLimit, outputLimit);
1640
- const apiKey = await this.options.resolveApiKey();
1660
+ const apiKey = await this.options.resolveApiKey(config);
1641
1661
  if (apiKey === void 0 || apiKey.length === 0) {
1642
1662
  throw new LlmError6("llm-opencode-go: no credential resolved for the route", "MISSING_CREDENTIAL");
1643
1663
  }
@@ -1669,7 +1689,7 @@ var OpencodeGoAdapter = class extends LlmAdapter {
1669
1689
  };
1670
1690
  }
1671
1691
  const context = imageRequest === void 0 ? toPiContext(options, void 0, this.options.onReplayDegrade) : await toPiContext({ ...options, signal: watchdog.signal }, imageRequest, this.options.onReplayDegrade);
1672
- const events = snapshot.provider.streamSimple(withRequestReasoning(model, reasoning), normalizeContext(context), {
1692
+ const events = catalog.provider.streamSimple(withRequestReasoning(model, reasoning), normalizeContext(context), {
1673
1693
  apiKey,
1674
1694
  ...reasoning === void 0 || reasoning === "off" ? {} : { reasoning },
1675
1695
  ...options.temperature === void 0 ? {} : { temperature: options.temperature },
@@ -1886,8 +1906,8 @@ function apply(ctx, raw) {
1886
1906
  const entry = readConfig(config);
1887
1907
  assertBaseURL(entry.baseURL);
1888
1908
  let current = () => readConfig(config);
1889
- const resolveApiKey = async () => {
1890
- const ref = current().apiKeyEnv;
1909
+ const resolveApiKey = async (config2 = current()) => {
1910
+ const ref = config2.apiKeyEnv;
1891
1911
  const credentials = ctx.get("credentials");
1892
1912
  const hit = credentials !== void 0 ? (await credentials.resolve(credentialRef(ref)))?.value : launchEnvironmentOf(ctx).get(ref)?.value;
1893
1913
  if (hit !== void 0 && hit.length > 0) return assertUsableApiKey(hit, name, ref);
@@ -45,8 +45,8 @@ export interface OpencodeGoAdapterOptions {
45
45
  * two configuration generations.
46
46
  */
47
47
  config: () => OpencodeGoConfig;
48
- /** Resolve the route's credential per call; missing must fail loud. */
49
- resolveApiKey: () => Promise<string | undefined>;
48
+ /** Resolve the credential reference captured with this call's endpoint; missing must fail loud. */
49
+ resolveApiKey: (config: OpencodeGoConfig) => Promise<string | undefined>;
50
50
  /**
51
51
  * Image input machinery; absent refuses image content, which is the posture
52
52
  * for direct construction without a durable attachment service behind it.
@@ -94,9 +94,17 @@ export declare class OpencodeGoAdapter extends LlmAdapter {
94
94
  };
95
95
  listModels(_provider: string): Promise<readonly LlmModelInfo[]>;
96
96
  resolveModel(_provider: string, model: string, signal?: AbortSignal): Promise<LlmResolvedModelInfo>;
97
+ /** Copy nested limits before discovery can yield to a settings update. */
98
+ private callSnapshot;
99
+ /** Keep capability resolution and eventual dispatch on the same configuration. */
100
+ prepareCall(_provider: string, model: string, signal?: AbortSignal): Promise<{
101
+ model: LlmResolvedModelInfo;
102
+ stream: (options: GenerateOptions) => AsyncIterable<StreamChunk>;
103
+ }>;
97
104
  /** Describe one model: capacities plus the reasoning levels it actually offers. */
98
105
  private modelInfo;
99
106
  /** Validate an explicit effort against the model's own levels, without clamping. */
100
107
  private resolveReasoningLevel;
101
108
  stream(options: GenerateOptions): AsyncIterable<StreamChunk>;
109
+ private streamWithSnapshot;
102
110
  }
@@ -1,11 +1,14 @@
1
1
  import type { SnapshotStore } from '@deepseek-ai/dsh-client-store';
2
2
  import type { ModelDirectoryState } from '@deepseek-ai/dsh-client-ui-model-selection/client';
3
3
  import type { GoUsage } from '../usage-contract.ts';
4
+ import type { SettingsScope } from './settings.ts';
5
+ import type { OpencodeGoSettings } from './section-controller.ts';
4
6
  export interface UsagePillProps {
5
7
  directory: SnapshotStore<ModelDirectoryState>;
8
+ settings: SettingsScope<OpencodeGoSettings>;
6
9
  readUsage: () => Promise<GoUsage>;
7
10
  t: (key: string) => string;
8
11
  getLocale?: () => string;
9
12
  }
10
- /** Only the selected Go provider mounts a poller, so other models send no usage traffic. */
11
- export declare function UsagePill({ directory, ...props }: UsagePillProps): import("react").JSX.Element | null;
13
+ /** Only a visible, enabled pill mounts the usage poller. */
14
+ export declare function UsagePill({ directory, settings, ...props }: UsagePillProps): import("react").JSX.Element | null;
@@ -92,6 +92,11 @@ export declare const en: {
92
92
  advancedSummary: string;
93
93
  advancedLabel: string;
94
94
  advancedHint: string;
95
+ usageDisplayLabel: string;
96
+ usageDisplayHint: string;
97
+ usageDisplay_auto: string;
98
+ usageDisplay_always: string;
99
+ usageDisplay_off: string;
95
100
  apiKeyEnvLabel: string;
96
101
  apiKeyEnvHint: string;
97
102
  baseURLLabel: string;
@@ -11,6 +11,7 @@
11
11
  import type { Context as ClientContext } from '@deepseek-ai/cordis';
12
12
  import type { RemoteResult } from '@deepseek-ai/dsh-typert-protocol';
13
13
  import type { GoModel, GoModelCatalog } from '../models-contract.ts';
14
+ import { type UsageDisplayMode } from '../usage-display.ts';
14
15
  import type { SnapshotStore } from '@deepseek-ai/dsh-client-store';
15
16
  import type { SettingsScope } from './settings.ts';
16
17
  import { type FieldState, type FormActions, type FormShell } from './staged-form.ts';
@@ -20,6 +21,8 @@ export declare const OPENCODE_GO_NS = "llm-opencode-go";
20
21
  export interface OpencodeGoSettings {
21
22
  /** Whether the adapter serves its route; false withdraws it from every picker. */
22
23
  enabled?: boolean;
24
+ /** Usage pill visibility, independent of the selected model in always mode. */
25
+ usageDisplay?: UsageDisplayMode;
23
26
  /** Per-model switches; normal models default on, deprecated models default off. */
24
27
  modelVisibility?: Record<string, boolean>;
25
28
  /** Credential reference naming the environment key. */
@@ -85,6 +88,7 @@ export interface OpencodeGoSectionState extends FormShell {
85
88
  * a withdrawn route is what the user is trying to observe.
86
89
  */
87
90
  enabled: boolean;
91
+ usageDisplay: FieldState;
88
92
  modelVisibility: Readonly<Record<string, boolean>>;
89
93
  pickerSaving: boolean;
90
94
  pickerFailed: boolean;
@@ -191,5 +191,13 @@ export declare class StagedForm {
191
191
  private baseValue;
192
192
  private userLayer;
193
193
  private stored;
194
+ /**
195
+ * Whether a draft already equals the field's effective value, so a save would
196
+ * write nothing. The save plan and the override badge both answer for it.
197
+ * @param field - field name inside the namespace section.
198
+ * @param staged - the staged edit to test.
199
+ * @returns true when the draft is a no-op; a clear is never one.
200
+ */
201
+ private unchangedDraft;
194
202
  private publish;
195
203
  }
@@ -1,2 +1,4 @@
1
1
  import type { Context } from '@deepseek-ai/cordis';
2
- export declare function registerUsagePill(ctx: Context): void;
2
+ import type { SettingsScope } from './settings.ts';
3
+ import type { OpencodeGoSettings } from './section-controller.ts';
4
+ export declare function registerUsagePill(ctx: Context, settings: SettingsScope<OpencodeGoSettings>): void;
@@ -9,6 +9,7 @@
9
9
  * @module dsh-llm-opencode-go/config
10
10
  */
11
11
  import z from '@deepseek-ai/schemastery';
12
+ import { type UsageDisplayMode } from './usage-display.ts';
12
13
  /** Environment variable resolving the OpenCode API key. */
13
14
  export declare const DEFAULT_API_KEY_ENV = "OPENCODE_API_KEY";
14
15
  /** Successful refresh lifetime; failed refreshes retry sooner and explicit discovery revalidates immediately. */
@@ -38,6 +39,8 @@ export interface OpencodeGoConfig {
38
39
  * false registers nothing.
39
40
  */
40
41
  enabled: boolean;
42
+ /** Usage pill visibility; auto follows this plugin's selected provider. */
43
+ usageDisplay: UsageDisplayMode;
41
44
  /** Per-model picker switches; absent entries default to enabled unless deprecated. */
42
45
  modelVisibility?: Record<string, boolean>;
43
46
  /** Credential reference: the environment variable the key resolves from. */
@@ -32,7 +32,7 @@ export interface PiAiReplayResponse {
32
32
  sdkProvider?: string;
33
33
  /** Requested model identity, matching the durable assistant source. */
34
34
  model: string;
35
- /** Provider-reported model; only Anthropic replays it as the native model (reported in `message.model`, not `message.responseModel`). */
35
+ /** Provider-reported model; informational only, so requested identity still matches replay signatures. */
36
36
  responseModel?: string;
37
37
  responseId?: string;
38
38
  /** Provider-native effort for historical replay; absence is preserved. */
@@ -0,0 +1,4 @@
1
+ /** Usage visibility shared by the Host schema and the browser settings. */
2
+ export declare const USAGE_DISPLAY_MODES: readonly ["auto", "always", "off"];
3
+ export type UsageDisplayMode = typeof USAGE_DISPLAY_MODES[number];
4
+ export declare const DEFAULT_USAGE_DISPLAY: UsageDisplayMode;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-opencode-go",
3
- "version": "0.1.18",
3
+ "version": "0.1.20",
4
4
  "description": "OpenCode Go model provider and settings UI for DeepSeek Harness",
5
5
  "type": "module",
6
6
  "license": "MIT",