dsh-opencode-go 0.1.0 → 0.1.1

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.md CHANGED
@@ -1,26 +1,36 @@
1
1
  # dsh-opencode-go
2
2
 
3
- 可独立构建、打包和安装的 DeepSeek Harness OpenCode Go 插件。包含模型适配器和 Web 设置页,无需修改 DSH 源码或重新构建 DSH。
3
+ 在 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 中使用 OpenCode Go 订阅模型,支持流式回复、工具调用、图片输入和 Web 设置页。
4
4
 
5
- ## 兼容版本
5
+ 插件自动添加 OpenCode Go 所需的会话请求头,并从网关获取可用模型目录。通过 DSH 插件命令安装,无需修改 DSH 源码。
6
6
 
7
- 当前适配并验证 **DSH `0.1.6-alpha.1`**,依赖使用明确版本。npm 的 DSH `latest` 标签仍指向较旧版本,请勿省略版本号后假定兼容。未验证其他 DSH 版本、Desktop 或其他操作系统。
7
+ ## 安装与使用
8
8
 
9
- 插件依赖 DSH 的公开服务接口;OpenCode Go 的请求头、模型目录和转换逻辑由本项目维护。无需安装或挂载 `dsh-llm-pi-ai` 才能使用本适配器;DSH 默认组合可以继续挂载它以服务其他提供方。
9
+ 已验证兼容 **DSH `0.1.6-alpha.1`**。其他版本尚未验证。
10
10
 
11
- ## 安装
12
-
13
- 在已经安装兼容版本 DSH 的环境执行:
11
+ ### Web
14
12
 
15
13
  ```sh
16
14
  dsh plugin --profile web add dsh-opencode-go
17
- dsh --profile web --dump-config
18
- dsh web
19
15
  ```
20
16
 
21
- 配置树应出现 `dsh-opencode-go` 层和 `id: opencode-go`。Web 设置中打开 **OpenCode Go**,保存 API Key,再在模型选择器选择该提供方的模型。安装插件不会自动更改默认模型。
17
+ 安装后启动或重启 `dsh web`:
18
+
19
+ 1. 打开 **设置 → OpenCode Go**。
20
+ 2. 填入 OpenCode Go API Key 并保存。
21
+ 3. 在会话的模型选择器中选择 OpenCode Go 模型。
22
+
23
+ API Key 来自你的 OpenCode Go 订阅。安装插件不会自动更改默认模型。
24
+
25
+ ### Headless
26
+
27
+ 安装到 Headless profile:
22
28
 
23
- Headless 用户先把以下内容保存为当前目录下的 `headless.patch.yml`,选择默认模型:
29
+ ```sh
30
+ dsh plugin --profile headless add dsh-opencode-go
31
+ ```
32
+
33
+ 将以下内容保存为 `headless.patch.yml`,选择默认模型:
24
34
 
25
35
  ```yaml
26
36
  - id: agent-default-model
@@ -29,35 +39,31 @@ Headless 用户先把以下内容保存为当前目录下的 `headless.patch.yml
29
39
  model: deepseek-v4.1-flash
30
40
  ```
31
41
 
32
- 再独立安装到 Headless profile:
42
+ 在 Bash 或 Zsh 中读取 API Key,然后运行任务:
33
43
 
34
44
  ```sh
35
- dsh plugin --profile headless add dsh-opencode-go
36
45
  read -s OPENCODE_API_KEY
37
46
  export OPENCODE_API_KEY
38
47
  dsh --profile headless --patch ./headless.patch.yml "你好"
39
48
  ```
40
49
 
41
- 安装器若提示 pnpm 拦截依赖构建脚本,按提示在该 profile 的 `pnpm-workspace.yaml` 中逐项声明 `allowBuilds`。本次测试对 `@google/genai` 和 `protobufjs` 均设置为 `false`,未执行这些脚本;OpenCode Go 测试路径不需要它们。无需全局放开安装脚本。
50
+ 模型 ID 须在当前网关目录中可用。Web 和 Headless 使用各自的 profile,需要分别安装插件。
42
51
 
43
- ### 从已有二次开发版本迁移
52
+ ## 订阅用量
44
53
 
45
- 本插件使用配置行 `id: opencode-go`,设置命名空间仍是 `llm-opencode-go`,凭据引用仍是 `OPENCODE_API_KEY`。现有二次开发版内置行 `id: llm-opencode-go` 必须停用,避免两个适配器竞争同一个提供方路由。
54
+ Web 中选择 `opencode-go` 提供方的模型后,模型名左侧显示 **Go · 5h 已用百分比 · 周已用百分比**。点击可查看 5 小时、每周、每月用量和本地时区的重置时间。
46
55
 
47
- 把以下停用行合并进对应 profile 的 `cordis.patch.yml`,保留已有的其他配置行:
56
+ 数据通过 Host 使用已配置的 API Key 查询 `GET /zen/go/v1/usage`,每分钟刷新;浏览器不接收 API Key。切换到其他提供方后隐藏,页面不可见时暂停查询。接口失败或响应缺失时显示“暂不可用”,不会填成 0%。接口返回的是账号订阅额度,不是当前会话 token 数。
48
57
 
49
- ```yaml
50
- - id: llm-opencode-go
51
- disabled: true
52
- ```
53
-
54
- 无需删除原仓库代码;停用旧行后由外部安装包提供功能。
58
+ 这个接口可在 [OpenCode 官方源码](https://github.com/anomalyco/opencode/blob/dev/packages/console/app/src/routes/zen/go/v1/usage.ts) 中查到;当前返回 `rolling`、`weekly`、`monthly` 各窗口的 `status`、`percent` 和 `resetsAt`,不返回缓存命中率。
55
59
 
56
- 如果通用 pi-ai 的配置也声明了 `opencode-go` 路由,请移除该路由配置。其他提供方可继续使用。
60
+ 底部的缓存命中率由每次模型响应的缓存 token 统计计算。插件支持 `prompt_tokens_details.cached_tokens`、`prompt_cache_hit_tokens` 和顶层 `cached_tokens`。上游未返回缓存统计时,当前 pi-ai / Harness 统计链路也可能显示 0;可在 OpenCode 控制台的使用历史中展开对应请求的输入明细进行核对。新会话首次请求出现 0 命中是可能的,同一会话后续有可复用的提示前缀才可能命中。
57
61
 
58
62
  ## 配置
59
63
 
60
- 修改对应 profile 的 `cordis.patch.yml`:
64
+ Web 用户可直接在 **设置 → OpenCode Go** 中修改配置。启用开关立即生效;保存其他配置后,后续请求使用新值。
65
+
66
+ 需要通过文件配置时,在对应 profile 的 `cordis.patch.yml` 中添加以下内容。所有字段均有默认值,通常只需配置 API Key。
61
67
 
62
68
  ```yaml
63
69
  - id: opencode-go
@@ -72,50 +78,64 @@ dsh --profile headless --patch ./headless.patch.yml "你好"
72
78
  requestImageMaxBytes: 1048576
73
79
  ```
74
80
 
75
- 所有字段均有默认值。`apiKeyEnv` 是凭据引用名,不是密钥;存在 credentials 服务时使用该服务,否则读取进程环境。Web 设置可覆盖基础配置,后续请求读取新值。禁用开关或移除密钥会撤销路由,设置页仍可访问。
81
+ `apiKeyEnv` 是凭据引用名,不是密钥。插件通过 DSH credentials 服务解析凭据;未挂载该服务时读取进程环境。Web 保存的设置可覆盖文件中的基础配置。
76
82
 
77
- ## 请求与模型行为
83
+ 关闭插件开关或移除密钥会撤下提供方路由,设置页仍可访问。
78
84
 
79
- - 每次请求包含 Harness User-Agent 和 `x-opencode-session`;同一会话保持相同值,无会话 ID 的请求使用独立随机值。
80
- - 模型目录取 pi-ai 目录及本项目补充表与网关 `/models` 的交集。未知协议的模型不会猜测加入。
81
- - 实时目录获取失败时,请求目录可回退到本地表;显式模型发现会报告失败。
82
- - 保留流式输出、工具调用、历史回放及支持图片的模型转换;输入图片须有 DSH attachment 服务。
83
- - 模型看到会话内容和工具定义,不增加隐藏系统提示。会话请求头用于网关路由;图片编码和历史前缀影响缓存复用。
85
+ ## 常见问题
84
86
 
85
- ## 从源码构建与打包
87
+ ### 安装提示依赖构建脚本被拦截
86
88
 
87
- 需要 Node.js `^22.19.0 || >=24.0.0` 和 npm。所有依赖从 npm 获取,不使用 `workspace:`、本机路径或 DSH 源码别名。
89
+ 按安装器提示,在该 profile 的 `pnpm-workspace.yaml` 中逐项设置 `allowBuilds`。OpenCode Go 已验证的运行路径不需要执行 `@google/genai` 和 `protobufjs` 的安装脚本,可以将这两项设为 `false` 后重试。保留文件中的其他配置,无需全局放开安装脚本。
88
90
 
89
- ```sh
90
- npm ci
91
- npm test
92
- npm pack
93
- ```
91
+ ### 提示 `opencode-go` 路由已被占用
94
92
 
95
- `npm test` 先构建,再执行测试;`npm pack` 也会构建。产物是 `dsh-opencode-go-0.1.0.tgz`,包含 Host、Client、声明文件及 bundle 配置,不包含源码、测试或密钥。
93
+ 同一 profile 中只能有一个适配器提供 `opencode-go` 路由。如果已经通过其他插件或通用 pi-ai 配置接入 OpenCode Go,请先停用那一项配置。其他提供方可以继续使用。
96
94
 
97
- ## 验证与维护
95
+ ### 没有出现预期的模型
98
96
 
99
- ```sh
100
- npm run typecheck
101
- npm test
102
- npm run verify:installed -- /path/to/isolated-consumer
103
- npm run verify:headless -- /path/to/isolated-consumer
104
- ```
97
+ 先确认插件已启用且 API Key 已配置,再刷新设置页中的模型列表。插件仅展示本地适配表与网关实时目录的交集;尚未适配协议的新模型不会自动加入。
98
+
99
+ 实时目录获取失败时,适配器可使用本地表继续处理请求;设置页中的模型发现会显示失败,便于重新尝试。
105
100
 
106
- 后两项使用隔离目录中的已安装包。准备方式和实际验证记录见 [验证说明](docs/verification.md)。架构与代码来源见 [独立发布决策](docs/independent-package.md)和 [第三方声明](THIRD_PARTY_NOTICES.md)。
101
+ ## 功能说明
107
102
 
108
- 测试使用本地模拟网关,不会发送真实 OpenCode Go API 请求。真实账号、额度和网关当前模型可用性须在配置自己的密钥后验证。
103
+ - **会话请求头**:每次请求包含 Harness User-Agent 和 `x-opencode-session`。同一会话保持相同 ID,无会话 ID 的请求使用独立随机值。
104
+ - **流式与历史**:支持流式输出、工具调用及历史回放,协议请求由 pi-ai 执行。
105
+ - **图片输入**:支持目录中声明图片能力的模型,需要 DSH attachment 服务。
106
+ - **提示与缓存**:插件不增加隐藏系统提示;会话 ID 用于网关路由,历史前缀及图片编码影响缓存复用。
109
107
 
110
108
  ## 卸载
111
109
 
110
+ 从对应 profile 移除插件,再重启应用:
111
+
112
112
  ```sh
113
113
  dsh plugin --profile web remove dsh-opencode-go
114
+ # 或
114
115
  dsh plugin --profile headless remove dsh-opencode-go
115
116
  ```
116
117
 
117
- 重启相应应用后移除插件;如默认模型仍指向 `opencode-go`,请选择其他提供方。凭据和历史会话由 DSH 管理。
118
+ 如果默认模型仍指向 `opencode-go`,请改选其他提供方。凭据和历史会话由 DSH 管理。
119
+
120
+ ## 开发
121
+
122
+ 需要 Node.js `^22.19.0 || >=24.0.0` 和 npm。
123
+
124
+ ```sh
125
+ npm ci
126
+ npm run typecheck
127
+ npm test
128
+ npm pack
129
+ ```
130
+
131
+ `npm test` 会先构建再执行测试;`npm pack` 生成可安装的 `.tgz` 包。依赖均通过 npm 安装,不需要 DSH 源码仓库。
132
+
133
+ - [验证说明](docs/verification.md):测试范围、隔离安装与 Headless 验证方法。
134
+ - [架构与依赖](docs/independent-package.md):Host、Client 构建方式和转换代码的维护方式。
135
+ - [第三方声明](THIRD_PARTY_NOTICES.md):代码来源与许可。
136
+
137
+ 自动化测试使用本地模拟网关,不会发起付费模型请求。真实账号、Desktop 和其他操作系统尚未验证。
118
138
 
119
- ## 发布
139
+ ## 许可证
120
140
 
121
- 维护者在有发布权限的 npm 账号下执行 `npm publish --access public`。发布前执行 `npm run typecheck`、`npm test` 和 `npm publish --dry-run`;发布后检查 registry 版本并用包名验证安装。每次更新递增版本号,已发布的版本不可覆盖。构建脚本从 `package.json` 读取 Client 模块 ID。
141
+ [MIT](LICENSE)
@@ -45,6 +45,12 @@ The tarball was installed into a separate Web profile in the same temporary Harn
45
45
 
46
46
  The browser-factory test additionally evaluates the distributed `lib/client.js` against the published React/store/UI module table, checks the package ID, mounts the settings section, and verifies CSS insertion.
47
47
 
48
- ## Limits
48
+ ## Subscription usage verification (0.1.1)
49
+
50
+ The usage tests invoke the actual published Typert Host gateway against a local HTTP fixture, checking the Bearer credential, credential changes, all three windows, and failure handling. Browser component tests check provider switching, polling cleanup, monthly details, and failed-refresh behavior. Adapter tests cover non-zero cached input in OpenAI, DeepSeek, and Kimi usage fields.
51
+
52
+ The local Web profile was also checked against the real read-only OpenCode Go usage endpoint. The button renders immediately before the model selector, and its details show rolling, weekly, and monthly percentages and local reset times. This check sends no model-generation request. The manual Remote codec supports both the published DSH `schema` shape and the current source build's `create()` shape.
53
+
54
+ ## Remaining limits
49
55
 
50
56
  Real paid OpenCode Go completions, Desktop, non-macOS platforms, and other DSH releases are not verified. The automated gateway tests preserve the adapter's request and replay semantics but cannot establish account validity or live provider availability.
package/lib/client.js CHANGED
@@ -32,7 +32,7 @@ var import_react = require("react");
32
32
  var import_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
33
33
 
34
34
  // plugin-css:/Users/tanglei/Desktop/dsh-opencode-go/src/client/Section.module.css
35
- var id = "dsh-opencode-go/settings";
35
+ var id = "dsh-opencode-go/Section.module.css";
36
36
  if (!document.querySelector("style[data-plugin-css=" + JSON.stringify(id) + "]")) {
37
37
  const style = document.createElement("style");
38
38
  style.dataset.plugin = "dsh-opencode-go";
@@ -40,7 +40,7 @@ if (!document.querySelector("style[data-plugin-css=" + JSON.stringify(id) + "]")
40
40
  style.textContent = ".BtKYva_field{flex-direction:column;gap:6px;padding:12px 0;display:flex}.BtKYva_field+.BtKYva_field{border-top:.5px solid var(--dsw-alias-border-l2)}.BtKYva_head{align-items:center;gap:8px;display:flex}.BtKYva_label{min-width:0;color:var(--dsw-alias-label-primary);flex:1;font-size:13px;font-weight:500;line-height:1.5}.BtKYva_badges{align-items:center;gap:8px;display:inline-flex}.BtKYva_reset{font:inherit;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;padding:0;font-size:12px;line-height:1.5}.BtKYva_reset:hover:not(:disabled){color:var(--dsw-alias-label-primary)}.BtKYva_reset:disabled{cursor:default}.BtKYva_input{border:.5px solid var(--dsw-alias-border-l4);background:var(--dsw-alias-bg-layer-3);height:34px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font-size:13px;line-height:1.5}.BtKYva_input:focus-visible{border-color:var(--dsw-alias-brand-primary);outline:none}.BtKYva_input:disabled{color:var(--dsw-alias-label-tertiary);cursor:default}.BtKYva_inputInvalid{border-color:var(--dsw-alias-label-error);}.BtKYva_invalid{color:var(--dsw-alias-label-error);margin:0;font-size:12px;line-height:1.5}.BtKYva_hint{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:1.5}.BtKYva_intro{color:var(--dsw-alias-label-secondary);margin:0 0 8px;font-size:13px;line-height:1.5}.BtKYva_disclosure{font:inherit;text-align:left;cursor:pointer;background:0 0;border:none;align-items:center;gap:8px;margin:0;padding:0;display:flex}.BtKYva_disclosure:hover .BtKYva_label{color:var(--dsw-alias-brand-primary)}.BtKYva_chevron,.BtKYva_chevronOpen{color:var(--dsw-alias-label-tertiary);flex:none}.BtKYva_chevron{transform:rotate(-90deg)}.BtKYva_advanced{flex-direction:column;gap:4px;padding:4px 0 0 22px;display:flex}.BtKYva_advanced .BtKYva_field+.BtKYva_field{border-top:none}.BtKYva_chips{flex-wrap:wrap;gap:6px;display:flex}.BtKYva_actions{align-items:center;gap:8px;padding-top:12px;display:flex}.BtKYva_failedNote{color:var(--dsw-alias-label-error);margin:0;font-size:12px;line-height:1.5}";
41
41
  document.head.appendChild(style);
42
42
  }
43
- var Section_default = { "reset": "BtKYva_reset", "chevronOpen": "BtKYva_chevronOpen", "advanced": "BtKYva_advanced", "head": "BtKYva_head", "hint": "BtKYva_hint", "chevron": "BtKYva_chevron", "actions": "BtKYva_actions", "label": "BtKYva_label", "badges": "BtKYva_badges", "input": "BtKYva_input", "invalid": "BtKYva_invalid", "disclosure": "BtKYva_disclosure", "inputInvalid": "BtKYva_inputInvalid", "failedNote": "BtKYva_failedNote", "chips": "BtKYva_chips", "field": "BtKYva_field", "intro": "BtKYva_intro" };
43
+ var Section_default = { "reset": "BtKYva_reset", "badges": "BtKYva_badges", "inputInvalid": "BtKYva_inputInvalid", "label": "BtKYva_label", "invalid": "BtKYva_invalid", "failedNote": "BtKYva_failedNote", "input": "BtKYva_input", "disclosure": "BtKYva_disclosure", "intro": "BtKYva_intro", "field": "BtKYva_field", "advanced": "BtKYva_advanced", "actions": "BtKYva_actions", "head": "BtKYva_head", "hint": "BtKYva_hint", "chevron": "BtKYva_chevron", "chevronOpen": "BtKYva_chevronOpen", "chips": "BtKYva_chips" };
44
44
 
45
45
  // src/client/Section.tsx
46
46
  var import_jsx_runtime = require("react/jsx-runtime");
@@ -731,6 +731,16 @@ function refOf(snapshot) {
731
731
 
732
732
  // src/client/locales.ts
733
733
  var en = {
734
+ usageTitle: "OpenCode Go usage",
735
+ usageHint: "Account usage \xB7 used percentage \xB7 refreshes every minute",
736
+ usageWeekShort: "week",
737
+ usage_rolling: "5 hours",
738
+ usage_weekly: "Weekly",
739
+ usage_monthly: "Monthly",
740
+ usageResets: "Resets",
741
+ usageLimited: "Limit reached",
742
+ usageLoading: "Loading usage\u2026",
743
+ usageUnavailable: "Unavailable",
734
744
  nav: "OpenCode Go",
735
745
  title: "OpenCode Go",
736
746
  intro: "Use your OpenCode Go subscription in the harness. Paste the API key below and save; the models the gateway currently serves are listed underneath.",
@@ -776,6 +786,16 @@ var en = {
776
786
  unavailable: "The OpenCode Go settings are not served in this deployment."
777
787
  };
778
788
  var zh = {
789
+ usageTitle: "OpenCode Go \u7528\u91CF",
790
+ usageHint: "\u8D26\u53F7\u989D\u5EA6 \xB7 \u5DF2\u7528\u767E\u5206\u6BD4 \xB7 \u6BCF\u5206\u949F\u5237\u65B0",
791
+ usageWeekShort: "\u5468",
792
+ usage_rolling: "5 \u5C0F\u65F6",
793
+ usage_weekly: "\u6BCF\u5468",
794
+ usage_monthly: "\u6BCF\u6708",
795
+ usageResets: "\u91CD\u7F6E\u4E8E",
796
+ usageLimited: "\u5DF2\u8FBE\u9650\u989D",
797
+ usageLoading: "\u6B63\u5728\u8BFB\u53D6\u7528\u91CF\u2026",
798
+ usageUnavailable: "\u6682\u4E0D\u53EF\u7528",
779
799
  nav: "OpenCode Go",
780
800
  title: "OpenCode Go",
781
801
  intro: "\u5728 Harness \u4E2D\u4F7F\u7528\u4F60\u7684 OpenCode Go \u8BA2\u9605\u3002\u586B\u5165\u4E0B\u65B9 API key \u5E76\u4FDD\u5B58\u5373\u53EF\uFF1B\u7F51\u5173\u5F53\u524D\u63D0\u4F9B\u7684\u6A21\u578B\u5217\u5728\u4E0B\u9762\u3002",
@@ -821,11 +841,187 @@ var zh = {
821
841
  unavailable: "\u6B64\u90E8\u7F72\u672A\u63D0\u4F9B OpenCode Go \u8BBE\u7F6E\u3002"
822
842
  };
823
843
 
844
+ // src/usage-contract.ts
845
+ function parseGoUsage(value) {
846
+ if (!value || typeof value !== "object") throw new Error("Invalid OpenCode Go usage response");
847
+ const source = value;
848
+ const result = {};
849
+ for (const key of ["rolling", "weekly", "monthly"]) {
850
+ const row = source[key];
851
+ if (!row || row.status !== "ok" && row.status !== "rate-limited" || typeof row.percent !== "number" || !Number.isFinite(row.percent) || row.percent < 0 || typeof row.resetsAt !== "string" || !Number.isFinite(Date.parse(row.resetsAt))) {
852
+ throw new Error("Invalid OpenCode Go usage response");
853
+ }
854
+ result[key] = { status: row.status, percent: row.percent, resetsAt: row.resetsAt };
855
+ }
856
+ return result;
857
+ }
858
+ var usageCodec = {
859
+ mode: "strict",
860
+ typeSymbol: "dsh-opencode-go#GoUsage",
861
+ schema: { parse: parseGoUsage },
862
+ create: () => ({ parse: parseGoUsage })
863
+ };
864
+ var usageRemote = {
865
+ package: "dsh-opencode-go",
866
+ descriptors: [{
867
+ id: "dsh-opencode-go#opencodeGoUsage/read",
868
+ service: "opencodeGoUsage",
869
+ namespace: "opencodeGoUsage",
870
+ method: "read",
871
+ invocation: { kind: "direct" },
872
+ parameters: [],
873
+ result: usageCodec
874
+ }]
875
+ };
876
+
877
+ // src/client/UsagePill.tsx
878
+ var import_react2 = require("react");
879
+
880
+ // plugin-css:/Users/tanglei/Desktop/dsh-opencode-go/src/client/UsagePill.module.css
881
+ var id2 = "dsh-opencode-go/UsagePill.module.css";
882
+ if (!document.querySelector("style[data-plugin-css=" + JSON.stringify(id2) + "]")) {
883
+ const style = document.createElement("style");
884
+ style.dataset.plugin = "dsh-opencode-go";
885
+ style.dataset.pluginCss = id2;
886
+ style.textContent = ".dmPm3q_root{min-width:0;display:inline-flex;position:relative}.dmPm3q_trigger{color:inherit;opacity:.75;font:inherit;cursor:pointer;white-space:nowrap;background:0 0;border:0;border-radius:6px;padding:4px 6px;font-size:12px}.dmPm3q_trigger:hover,.dmPm3q_trigger:focus-visible{opacity:1;background:color-mix(in srgb, currentColor 8%, transparent)}.dmPm3q_panel{z-index:100;border:1px solid color-mix(in srgb, currentColor 18%, transparent);width:280px;max-width:calc(100vw - 32px);color:var(--dsw-alias-label-primary,CanvasText);background:var(--dsw-specific-menu,Canvas);border-radius:12px;padding:16px;font-size:12px;position:absolute;bottom:calc(100% + 12px);right:0;box-shadow:0 8px 30px #0002}.dmPm3q_hint{opacity:.65;font-size:11px;line-height:1.6}.dmPm3q_window{margin-top:14px}.dmPm3q_row{justify-content:space-between;margin-bottom:5px;display:flex}.dmPm3q_window progress{accent-color:#39a878;width:100%;height:5px;margin-bottom:5px;display:block}.dmPm3q_window progress::-webkit-progress-bar{background:color-mix(in srgb, currentColor 15%, transparent);border-radius:4px}.dmPm3q_window progress::-webkit-progress-value{background:#39a878;border-radius:4px}.dmPm3q_window progress::-moz-progress-bar{background:#39a878;border-radius:4px}";
887
+ document.head.appendChild(style);
888
+ }
889
+ var UsagePill_default = { "root": "dmPm3q_root", "window": "dmPm3q_window", "panel": "dmPm3q_panel", "row": "dmPm3q_row", "hint": "dmPm3q_hint", "trigger": "dmPm3q_trigger" };
890
+
891
+ // src/client/UsagePill.tsx
892
+ var import_jsx_runtime2 = require("react/jsx-runtime");
893
+ function UsagePill({ directory, ...props }) {
894
+ const state = (0, import_react2.useSyncExternalStore)(directory.subscribe, directory.getSnapshot, directory.getSnapshot);
895
+ return state.current?.provider === "opencode-go" ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(ActiveUsage, { ...props }) : null;
896
+ }
897
+ function ActiveUsage({ readUsage, t }) {
898
+ const [usage, setUsage] = (0, import_react2.useState)(null);
899
+ const [failed, setFailed] = (0, import_react2.useState)(false);
900
+ const [open, setOpen] = (0, import_react2.useState)(false);
901
+ const root = (0, import_react2.useRef)(null);
902
+ (0, import_react2.useEffect)(() => {
903
+ let alive = true;
904
+ let busy = false;
905
+ const refresh = async () => {
906
+ if (busy || document.visibilityState === "hidden") return;
907
+ busy = true;
908
+ try {
909
+ const value = await readUsage();
910
+ if (alive) {
911
+ setUsage(value);
912
+ setFailed(false);
913
+ }
914
+ } catch {
915
+ if (alive) {
916
+ setUsage(null);
917
+ setFailed(true);
918
+ }
919
+ } finally {
920
+ busy = false;
921
+ }
922
+ };
923
+ void refresh();
924
+ const timer = setInterval(() => {
925
+ void refresh();
926
+ }, 6e4);
927
+ const visible = () => {
928
+ void refresh();
929
+ };
930
+ document.addEventListener("visibilitychange", visible);
931
+ return () => {
932
+ alive = false;
933
+ clearInterval(timer);
934
+ document.removeEventListener("visibilitychange", visible);
935
+ };
936
+ }, [readUsage]);
937
+ (0, import_react2.useEffect)(() => {
938
+ if (!open) return;
939
+ const click = (event) => {
940
+ if (!root.current?.contains(event.target)) setOpen(false);
941
+ };
942
+ const key = (event) => {
943
+ if (event.key === "Escape") setOpen(false);
944
+ };
945
+ document.addEventListener("mousedown", click);
946
+ document.addEventListener("keydown", key);
947
+ return () => {
948
+ document.removeEventListener("mousedown", click);
949
+ document.removeEventListener("keydown", key);
950
+ };
951
+ }, [open]);
952
+ const label = usage ? `Go \xB7 5h ${usage.rolling.percent}% \xB7 ${t("usageWeekShort")} ${usage.weekly.percent}%` : `Go \xB7 ${failed ? t("usageUnavailable") : "\u2026"}`;
953
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: UsagePill_default.root, ref: root, children: [
954
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
955
+ "button",
956
+ {
957
+ type: "button",
958
+ className: UsagePill_default.trigger,
959
+ "aria-expanded": open,
960
+ "aria-haspopup": "dialog",
961
+ "aria-label": `${t("usageTitle")}: ${label}`,
962
+ onClick: () => {
963
+ setOpen(!open);
964
+ },
965
+ children: label
966
+ }
967
+ ),
968
+ open && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: UsagePill_default.panel, role: "dialog", "aria-label": t("usageTitle"), children: [
969
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("strong", { children: t("usageTitle") }),
970
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: UsagePill_default.hint, children: t("usageHint") }),
971
+ usage ? ["rolling", "weekly", "monthly"].map((key) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: UsagePill_default.window, children: [
972
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: UsagePill_default.row, children: [
973
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { children: t(`usage_${key}`) }),
974
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("strong", { children: [
975
+ usage[key].percent,
976
+ "%"
977
+ ] })
978
+ ] }),
979
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("progress", { "aria-label": t(`usage_${key}`), max: 100, value: Math.min(100, usage[key].percent) }),
980
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: UsagePill_default.hint, children: [
981
+ t("usageResets"),
982
+ " ",
983
+ new Date(usage[key].resetsAt).toLocaleString()
984
+ ] }),
985
+ usage[key].status === "rate-limited" && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { children: t("usageLimited") })
986
+ ] }, key)) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { children: failed ? t("usageUnavailable") : t("usageLoading") })
987
+ ] })
988
+ ] });
989
+ }
990
+
991
+ // src/client/usage.ts
992
+ function registerUsagePill(ctx) {
993
+ ctx.inject(["modelDirectories", "sessions", "remote.session"], (scope) => {
994
+ scope.effect(async () => {
995
+ const unmount = await scope.remote.$mount(usageRemote);
996
+ scope.inject(["remote.opencodeGoUsage"], (ready) => {
997
+ const readUsage = async () => {
998
+ const result = await ready.remote.opencodeGoUsage.read();
999
+ if (!result.ok) throw new Error("OpenCode Go usage unavailable");
1000
+ return result.value;
1001
+ };
1002
+ const translate = ready.locale.bind("settings.opencode-go");
1003
+ ready.slots.inject("conversation.input.right", () => ready.slots.register({
1004
+ name: "conversation.input.right",
1005
+ id: "opencode-go-usage",
1006
+ order: 1e3,
1007
+ inject: (sessionId) => ({
1008
+ directory: ready.modelDirectories.directoryFor(sessionId).store,
1009
+ readUsage,
1010
+ t: (key) => translate(key)
1011
+ })
1012
+ }, UsagePill));
1013
+ });
1014
+ return unmount;
1015
+ });
1016
+ });
1017
+ }
1018
+
824
1019
  // src/client/index.ts
825
1020
  var NS = "settings.opencode-go";
826
1021
  var inject = ["slots", "locale", "remote", "remote.credentials", "remote.llm", "settingsScope"];
827
1022
  function apply(ctx) {
828
1023
  ctx.effect(() => ctx.locale.register(NS, { zh, en }), "llm-opencode-go: copy dictionaries");
1024
+ registerUsagePill(ctx);
829
1025
  const scope = ctx.settingsScope.bind({
830
1026
  namespace: "llm-opencode-go",
831
1027
  decode: (section) => typeof section === "object" && section !== null ? section : void 0
package/lib/index.js CHANGED
@@ -1035,6 +1035,73 @@ var OpencodeGoAdapter = class extends LlmAdapter {
1035
1035
  }
1036
1036
  };
1037
1037
 
1038
+ // src/usage.ts
1039
+ import { TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
1040
+ import { attributionHeaders as attributionHeaders3 } from "@deepseek-ai/dsh-llm";
1041
+
1042
+ // src/usage-contract.ts
1043
+ function parseGoUsage(value) {
1044
+ if (!value || typeof value !== "object") throw new Error("Invalid OpenCode Go usage response");
1045
+ const source = value;
1046
+ const result = {};
1047
+ for (const key of ["rolling", "weekly", "monthly"]) {
1048
+ const row = source[key];
1049
+ if (!row || row.status !== "ok" && row.status !== "rate-limited" || typeof row.percent !== "number" || !Number.isFinite(row.percent) || row.percent < 0 || typeof row.resetsAt !== "string" || !Number.isFinite(Date.parse(row.resetsAt))) {
1050
+ throw new Error("Invalid OpenCode Go usage response");
1051
+ }
1052
+ result[key] = { status: row.status, percent: row.percent, resetsAt: row.resetsAt };
1053
+ }
1054
+ return result;
1055
+ }
1056
+ var usageCodec = {
1057
+ mode: "strict",
1058
+ typeSymbol: "dsh-opencode-go#GoUsage",
1059
+ schema: { parse: parseGoUsage },
1060
+ create: () => ({ parse: parseGoUsage })
1061
+ };
1062
+ var usageRemote = {
1063
+ package: "dsh-opencode-go",
1064
+ descriptors: [{
1065
+ id: "dsh-opencode-go#opencodeGoUsage/read",
1066
+ service: "opencodeGoUsage",
1067
+ namespace: "opencodeGoUsage",
1068
+ method: "read",
1069
+ invocation: { kind: "direct" },
1070
+ parameters: [],
1071
+ result: usageCodec
1072
+ }]
1073
+ };
1074
+
1075
+ // src/usage.ts
1076
+ var GoUsageService = class extends TypertRemoteService {
1077
+ constructor(ctx, options) {
1078
+ super(ctx, "opencodeGoUsage");
1079
+ this.options = options;
1080
+ ctx.inject(["typert"], (scope) => {
1081
+ scope.effect(() => scope.typert.register({
1082
+ package: usageRemote.package,
1083
+ face: "host",
1084
+ schemas: [],
1085
+ model: { services: [], events: [], objects: [] },
1086
+ invocations: usageRemote.descriptors
1087
+ }));
1088
+ });
1089
+ }
1090
+ async read() {
1091
+ const baseURL = assertBaseURL(this.options.baseURL()).replace(/\/$/, "");
1092
+ const key = await this.options.resolveApiKey();
1093
+ if (!key) throw new Error("OpenCode Go API key is not configured");
1094
+ const response = await fetch(`${baseURL}/usage`, {
1095
+ headers: { ...attributionHeaders3(), Authorization: `Bearer ${key}`, Accept: "application/json" },
1096
+ signal: AbortSignal.timeout(1e4),
1097
+ redirect: "error"
1098
+ });
1099
+ if (!response.ok) throw new Error(`OpenCode Go usage unavailable (HTTP ${response.status})`);
1100
+ const body = await response.json();
1101
+ return parseGoUsage(body && typeof body === "object" ? body.usage : void 0);
1102
+ }
1103
+ };
1104
+
1038
1105
  // src/index.ts
1039
1106
  var name = "llm-opencode-go";
1040
1107
  var inject = ["llm"];
@@ -1053,6 +1120,7 @@ function apply(ctx, raw) {
1053
1120
  "MISSING_CREDENTIAL"
1054
1121
  );
1055
1122
  };
1123
+ ctx.plugin(GoUsageService, { baseURL: () => current().baseURL, resolveApiKey });
1056
1124
  const logger = {
1057
1125
  fallback: ({ error }) => {
1058
1126
  ctx.logger.warn(`llm-opencode-go: live model listing unreachable; serving the curated table until the next refresh (${String(error)})`);
@@ -0,0 +1,10 @@
1
+ import type { SnapshotStore } from '@deepseek-ai/dsh-client-store';
2
+ import type { ModelDirectoryState } from '@deepseek-ai/dsh-client-ui-model-selection/client';
3
+ import type { GoUsage } from '../usage-contract.ts';
4
+ export interface UsagePillProps {
5
+ directory: SnapshotStore<ModelDirectoryState>;
6
+ readUsage: () => Promise<GoUsage>;
7
+ t: (key: string) => string;
8
+ }
9
+ /** Only the selected Go provider mounts a poller, so other models send no usage traffic. */
10
+ export declare function UsagePill({ directory, ...props }: UsagePillProps): import("react").JSX.Element | null;
@@ -1,5 +1,15 @@
1
1
  /** Copy dictionaries for the OpenCode Go settings page. English is the key-set source of truth. */
2
2
  export declare const en: {
3
+ usageTitle: string;
4
+ usageHint: string;
5
+ usageWeekShort: string;
6
+ usage_rolling: string;
7
+ usage_weekly: string;
8
+ usage_monthly: string;
9
+ usageResets: string;
10
+ usageLimited: string;
11
+ usageLoading: string;
12
+ usageUnavailable: string;
3
13
  nav: string;
4
14
  title: string;
5
15
  intro: string;
@@ -0,0 +1,2 @@
1
+ import type { Context } from '@deepseek-ai/cordis';
2
+ export declare function registerUsagePill(ctx: Context): void;
@@ -0,0 +1,21 @@
1
+ import type { RemoteResult, TypertRemoteContribution } from '@deepseek-ai/dsh-typert-protocol';
2
+ export interface UsageWindow {
3
+ status: 'ok' | 'rate-limited';
4
+ percent: number;
5
+ resetsAt: string;
6
+ }
7
+ export interface GoUsage {
8
+ rolling: UsageWindow;
9
+ weekly: UsageWindow;
10
+ monthly: UsageWindow;
11
+ }
12
+ /** Reject missing statistics rather than turning unavailable data into zero. */
13
+ export declare function parseGoUsage(value: unknown): GoUsage;
14
+ declare module '@deepseek-ai/dsh-typert-protocol' {
15
+ interface TypertRemoteNamespaceMap {
16
+ opencodeGoUsage: {
17
+ read(): Promise<RemoteResult<GoUsage>>;
18
+ };
19
+ }
20
+ }
21
+ export declare const usageRemote: TypertRemoteContribution;
@@ -0,0 +1,14 @@
1
+ import type { Context } from '@deepseek-ai/cordis';
2
+ import { TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol';
3
+ import { type GoUsage } from './usage-contract.ts';
4
+ interface UsageOptions {
5
+ baseURL: () => string;
6
+ resolveApiKey: () => Promise<string | undefined>;
7
+ }
8
+ /** Account statistics are fetched on the Host; credentials never enter the browser. */
9
+ export declare class GoUsageService extends TypertRemoteService {
10
+ private readonly options;
11
+ constructor(ctx: Context, options: UsageOptions);
12
+ read(): Promise<GoUsage>;
13
+ }
14
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-opencode-go",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "OpenCode Go model provider and settings UI for DeepSeek Harness",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -35,7 +35,8 @@
35
35
  "inject": [
36
36
  "@deepseek-ai/dsh-client-locale",
37
37
  "@deepseek-ai/dsh-client-ui-settings",
38
- "@deepseek-ai/dsh-api-remotes"
38
+ "@deepseek-ai/dsh-api-remotes",
39
+ "@deepseek-ai/dsh-client-ui-model-selection"
39
40
  ],
40
41
  "platform": "web"
41
42
  }
@@ -51,22 +52,25 @@
51
52
  },
52
53
  "peerDependencies": {
53
54
  "@deepseek-ai/cordis": "4.0.2",
55
+ "@deepseek-ai/dsh-api-gateway": "0.1.6-alpha.1",
56
+ "@deepseek-ai/dsh-api-remotes": "0.1.6-alpha.1",
57
+ "@deepseek-ai/dsh-api-settings-controller": "0.1.6-alpha.1",
54
58
  "@deepseek-ai/dsh-attachment": "0.1.6-alpha.1",
59
+ "@deepseek-ai/dsh-client-locale": "0.1.6-alpha.1",
60
+ "@deepseek-ai/dsh-client-store": "0.1.6-alpha.1",
61
+ "@deepseek-ai/dsh-client-ui-model-selection": "0.1.6-alpha.1",
62
+ "@deepseek-ai/dsh-client-ui-renderer": "0.1.6-alpha.1",
63
+ "@deepseek-ai/dsh-client-ui-settings": "0.1.6-alpha.1",
64
+ "@deepseek-ai/dsh-client-ui-slots": "0.1.6-alpha.1",
55
65
  "@deepseek-ai/dsh-credentials": "0.1.6-alpha.1",
56
66
  "@deepseek-ai/dsh-fs": "0.1.6-alpha.1",
57
67
  "@deepseek-ai/dsh-launch-environment": "0.1.6-alpha.1",
58
68
  "@deepseek-ai/dsh-llm": "0.1.6-alpha.1",
59
69
  "@deepseek-ai/dsh-settings": "0.1.6-alpha.1",
60
70
  "@deepseek-ai/dsh-timeout": "0.1.6-alpha.1",
61
- "@deepseek-ai/dsh-api-remotes": "0.1.6-alpha.1",
62
- "@deepseek-ai/dsh-api-gateway": "0.1.6-alpha.1",
63
- "@deepseek-ai/dsh-api-settings-controller": "0.1.6-alpha.1",
64
- "@deepseek-ai/dsh-client-store": "0.1.6-alpha.1",
65
- "@deepseek-ai/dsh-client-ui-slots": "0.1.6-alpha.1",
66
- "@deepseek-ai/dsh-client-ui-settings": "0.1.6-alpha.1",
67
- "@deepseek-ai/dsh-client-ui-renderer": "0.1.6-alpha.1",
68
- "@deepseek-ai/dsh-client-locale": "0.1.6-alpha.1",
69
- "@deepseek-ai/dsh-typert-protocol": "0.1.6-alpha.1"
71
+ "@deepseek-ai/dsh-typert-protocol": "0.1.6-alpha.1",
72
+ "@deepseek-ai/dsh-client-ui-conversation": "0.1.6-alpha.1",
73
+ "@deepseek-ai/dsh-typert-registry": "0.1.6-alpha.1"
70
74
  },
71
75
  "dependencies": {
72
76
  "@deepseek-ai/dsh-brand": "0.1.6-alpha.1",
@@ -84,6 +88,8 @@
84
88
  "@deepseek-ai/dsh-brand": "0.1.6-alpha.1",
85
89
  "@deepseek-ai/dsh-client-locale": "0.1.6-alpha.1",
86
90
  "@deepseek-ai/dsh-client-store": "0.1.6-alpha.1",
91
+ "@deepseek-ai/dsh-client-ui-conversation": "0.1.6-alpha.1",
92
+ "@deepseek-ai/dsh-client-ui-model-selection": "0.1.6-alpha.1",
87
93
  "@deepseek-ai/dsh-client-ui-primitives": "0.1.6-alpha.1",
88
94
  "@deepseek-ai/dsh-client-ui-renderer": "0.1.6-alpha.1",
89
95
  "@deepseek-ai/dsh-client-ui-settings": "0.1.6-alpha.1",
@@ -97,38 +103,39 @@
97
103
  "@deepseek-ai/dsh-settings-file": "0.1.6-alpha.1",
98
104
  "@deepseek-ai/dsh-timeout": "0.1.6-alpha.1",
99
105
  "@deepseek-ai/dsh-typert-protocol": "0.1.6-alpha.1",
106
+ "@deepseek-ai/dsh-typert-registry": "0.1.6-alpha.1",
107
+ "@shikijs/langs": "^4.3.1",
100
108
  "@testing-library/react": "^16.3.2",
109
+ "@types/mdast": "^4.0.4",
101
110
  "@types/node": "^22.20.0",
102
111
  "@types/react": "~18.3.1",
103
- "esbuild": "^0.25.0",
104
- "jsdom": "29.1.1",
105
- "lightningcss": "^1.32.0",
106
- "react": "^18.2.0",
107
- "react-dom": "^18.2.0",
108
- "typescript": "6.0.3",
109
- "vitest": "^4.1.8",
110
112
  "@types/react-dom": "~18.3.0",
113
+ "anser": "^2.3.5",
111
114
  "clsx": "^2.0.0",
112
115
  "diff": "^9.0.0",
113
- "anser": "^2.3.5",
116
+ "esbuild": "^0.25.0",
117
+ "immer": "^10.1.1",
118
+ "jsdom": "29.1.1",
114
119
  "katex": "^0.16.47",
120
+ "lightningcss": "^1.32.0",
121
+ "mdast-util-from-markdown": "^2.0.3",
122
+ "mdast-util-gfm": "^3.1.0",
123
+ "mdast-util-math": "^3.0.0",
115
124
  "micromark-core-commonmark": "^2.0.3",
125
+ "micromark-extension-gfm": "^3.0.0",
126
+ "micromark-extension-math": "^3.1.0",
127
+ "micromark-factory-space": "^2.0.1",
116
128
  "micromark-util-character": "^2.1.1",
117
129
  "micromark-util-classify-character": "^2.0.1",
130
+ "micromark-util-sanitize-uri": "^2.0.1",
118
131
  "micromark-util-symbol": "^2.0.1",
119
132
  "micromark-util-types": "^2.0.2",
133
+ "react": "^18.2.0",
134
+ "react-dom": "^18.2.0",
120
135
  "shiki": "^4.3.1",
121
- "@shikijs/langs": "^4.3.1",
122
- "@types/mdast": "^4.0.4",
123
- "micromark-factory-space": "^2.0.1",
124
- "micromark-extension-math": "^3.1.0",
125
- "mdast-util-from-markdown": "^2.0.3",
126
- "mdast-util-gfm": "^3.1.0",
127
- "mdast-util-math": "^3.0.0",
128
- "micromark-extension-gfm": "^3.0.0",
129
- "micromark-util-sanitize-uri": "^2.0.1",
130
- "zustand": "~4.4.7",
131
- "immer": "^10.1.1"
136
+ "typescript": "6.0.3",
137
+ "vitest": "^4.1.8",
138
+ "zustand": "~4.4.7"
132
139
  },
133
140
  "peerDependenciesMeta": {
134
141
  "@deepseek-ai/dsh-api-remotes": {
@@ -155,7 +162,13 @@
155
162
  "@deepseek-ai/dsh-client-locale": {
156
163
  "optional": true
157
164
  },
158
- "@deepseek-ai/dsh-typert-protocol": {
165
+ "@deepseek-ai/dsh-client-ui-model-selection": {
166
+ "optional": true
167
+ },
168
+ "@deepseek-ai/dsh-client-ui-conversation": {
169
+ "optional": true
170
+ },
171
+ "@deepseek-ai/dsh-typert-registry": {
159
172
  "optional": true
160
173
  }
161
174
  },