dsh-model-usage 0.8.1 → 0.8.3

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,239 +1,302 @@
1
- # dsh-model-usage(用量)
2
-
3
- ## 截图 / Screenshots
4
-
5
- ![用量弹窗](docs/screenshots/usage-panel.png)
6
-
7
- 「用量」按钮点开的弹窗:当前套餐的 5 小时 / 7 天窗口、信用余额、本次查询对象,以及读取这些数字的凭据引用名。
8
-
9
- The panel behind the **用量** button: the plan's 5-hour and weekly windows, credit balance, the
10
- query target, and the credential reference the numbers were read with. Screenshots of the other
11
- dialogs (refresh settings, target picker, provider directory) go in the same folder.
12
-
13
- ## English
14
-
15
- A [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) plugin. It adds a **用量 / Usage**
16
- entry to the right end of the session header (`conversation.session.header.utilities`) showing the
17
- provider-account usage behind the model the current conversation runs on — read with that provider's
18
- own credential reference, never with another provider's key.
19
-
20
- - **Follows the conversation**: the model comes from the session's `modelSelection` projection, the
21
- key from that provider's own credential reference, the numbers from that provider's usage API.
22
- - **Manual target**: pick any routable provider model in the panel without changing what the
23
- conversation runs on.
24
- - **Warm in the background**: refreshes on an interval while the panel is closed.
25
- - **Read-only directory**: lists the deployment's provider routes, their usage API kind, credential
26
- reference name, and whether that reference is configured — never a value.
27
-
28
- Adapters: Command Code, DeepSeek (official balance and signed-in account wallet), OpenCode Go,
29
- OpenRouter, Moonshot / Kimi, SiliconFlow, and the cc-switch New API convention for other
30
- OpenAI-compatible routes. Everything else reports "usage query not supported" instead of guessing.
31
-
32
- ### Install
33
-
34
- ```sh
35
- # npm (this package is published as a Harness bundle)
36
- dsh plugin --profile <profile> add dsh-model-usage
37
- ```
38
-
39
- Or, from a harness agent session, ask the plugin manager to install the bundle by name
40
- (`install_bundle` with target `dsh-model-usage`). Either path installs the package into the profile,
41
- mounts its `cordis.patch.yml` row, and enables the bundle (installation enables a new bundle by
42
- default). With the profile's HMR enabled the change applies immediately; otherwise restart the host,
43
- and hard-refresh the page for the browser half (it is served by `dsh-client-modules`).
44
-
45
- Requirements: Harness `>=0.2.1-rc.2` (declared as `engines.dsh`) and its Web surface. No build step,
46
- no runtime dependencies: the host half is Node builtins only, the browser half requires nothing but
47
- the frozen `react` module. Nothing is uploaded anywhere; the host process calls the provider API with
48
- the key it already holds and returns only normalized numbers plus the credential *reference name*.
49
-
50
- License: MIT. This is an independent community plugin, not affiliated with or endorsed by DeepSeek.
51
-
52
- ## 中文
53
-
54
- 在会话页面头部最右侧的 utilities 区(`conversation.session.header.utilities`)加一个「用量」入口;
55
- 点开弹窗即可查看**当前对话所用模型**对应供应商账户的用量,也可以在面板里**手动切换**到别的
56
- 供应商模型查询。
57
-
58
- 插件做四件事:
59
-
60
- - **跟随对话查询**:模型取自会话的 `modelSelection` 投影(与模型选择器同一份事实),
61
- 密钥取自该供应商自己的凭据引用,用量来自该供应商的用量接口。
62
- - **支持手动切换查询对象**:可查任意可路由的供应商模型,且不改变当前对话使用的模型。
63
- - **后台自动刷新**:按间隔在后台保持数字新鲜,弹窗关着也照常刷新。
64
- - **只读说明目录**:列出本部署声明的供应商路由、各自的用量接口类型、密钥引用名与是否已配置(永不返回值)。
65
-
66
- ## 原理
67
-
68
- - **node 半**(`lib/index.js`):通过 `ctx.webServer.register()` 注册 loopback Web 路由:
69
- - `POST /model-usage/balance`:按给定(或当前对话)的模型查询用量;
70
- - `GET /model-usage/providers`:只读列出本部署声明的供应商路由与密钥是否就绪;
71
- - `GET /model-usage/models`:可路由模型目录 + 每个供应商对应的用量接口类型(手动切换用)。
72
- 三条路由都会先向 `connection` 服务要一次浏览器信任裁决(Host/Origin 栅栏),被拒即 401/403;
73
- 密钥只在本机 host 进程内使用,浏览器只拿到归一化数据 + 凭据**引用名**(永不返回值)。
74
- - **浏览器半**(`lib/client.js`):注册 `conversation.session.header.utilities` slot,
75
- 从会话的 `modelSelection` 投影读出当前模型(线格式 `{ lastUsed, next }`,`next` 已经含了
76
- 尚未使用的这次选择),随请求发给 host;在后台按间隔刷新用量(弹窗关闭也生效),
77
- 点开弹窗展示状态与用量。控件(按钮 / 弹窗)是本插件自写的,样式与文案分别来自主题 token
78
- 与 Client locale 服务。
79
-
80
- ### 模型与密钥是怎么确定的
81
-
82
- 1. 浏览器读会话投影 `modelSelection` 的 `next ?? lastUsed`(与模型选择器同一份事实),
83
- POST `{ provider, model, reasoningEffort, sessionId }` 给 host。
84
- payload 里没有 provider/model 时,host 依次回退到「本进程的会话投影」→「部署默认模型」
85
- (`agentDefaultModel.currentSelection()`)。
86
- 2. host 用 `llm.listConfigurableProviders()` 找到该 provider 的配置地址
87
- (`settingsNs` + `settingsPath`),再用 `settings.describe()` 读实时配置,得到
88
- `apiKeyEnv`(凭据引用)、`baseURL`、`api`、自定义 headers。
89
- 3. 用 `ctx.credentials.resolve(apiKeyEnv)` 取到**当前对话正在用的那把 key**:
90
- 该引用没配置时回退到同名环境变量;Command Code 再回退 `~/.commandcode/auth.json`。
91
- 解析是逐次调用进行的,在 设置 → 模型 里新填的 key 下一次查询即可用,不需要重启。
92
- 4. 按 provider / baseURL 选择对应的用量接口(见下表)。
93
-
94
- provider 没声明 `apiKeyEnv` 时按约定推导引用名 `${PROVIDER}_API_KEY`
95
- (如 `commandcode` → `COMMANDCODE_API_KEY`,与 设置 → 模型 里存 key 的名字一致);
96
- `deepseek-official` 固定用 `DEEPSEEK_API_KEY`。
97
-
98
- ### 手动切换查询对象
99
-
100
- 面板顶部有一行「查询对象」,显示这次查询用的是谁:
101
-
102
- - 默认 `自动 · 供应商 / 模型`,跟随当前对话的模型选择(对话里换模型,插件跟着换)。
103
- - 点它打开「选择模型」:可选 `自动(跟随当前对话)`,或**任意可路由的供应商模型**
104
- (来自 `sessionController.modelCatalog()`,每条路由标注它对应的用量接口类型;不支持的会写明
105
- `不支持用量查询`)。
106
- - 选中后只改变**插件查询的对象**,不会切换当前对话使用的模型;密钥也随之变成那个供应商自己的
107
- 凭据引用。选择存在浏览器 localStorage(`dsh-model-usage:model`),弹窗里「改回自动」可恢复。
108
- - 注意:用量是**账户级**的,同一把 key 下的模型共享同一份额度,所以切换模型实际是在切换
109
- 「用哪个供应商的 key 去查哪份额度」;卡片下方「模型 / 密钥」两行会如实显示这次用的是谁。
110
-
111
- ### 用量接口
112
-
113
- | provider / baseURL | 用量来源 |
114
- | --- | --- |
115
- | `commandcode`(或 baseURL 落在 `*.commandcode.ai`) | Command Code 私有 API:`GET /alpha/whoami?limits=1`、`/alpha/billing/credits`、`/alpha/billing/subscriptions`、`/alpha/usage/summary?since=`,即官方 CLI `/usage` 的同一组端点;显示套餐、5 小时 / 7 天窗口用量条、信用余额、请求数 |
116
- | `deepseek-official`、`deepseek`(或 baseURL 落在 `*.deepseek.com`) | `GET https://api.deepseek.com/user/balance`,按币种显示余额 |
117
- | `deepseek-account` | `ctx.deepseekAccount.getBalance()`:读已登录 DeepSeek 账户的钱包余额(不涉及 API Key) |
118
- | `opencode-go`(或 baseURL 落在 `opencode.ai/zen/go`) | `GET {origin}/zen/go/v1/usage`(OpenCode Go / Go Plus 订阅额度:5 小时 / 周 / 月,按模型各自的月额度计)。**该端点未公开文档化**,字段按常见写法归一化,识别不了的会原样显示在「原始返回」里 |
119
- | `openrouter`(或 baseURL 落在 `*.openrouter.ai`) | `GET /api/v1/key`(已用 / 额度 / 剩余);key 没有独立额度时再查 `GET /api/v1/credits` 补充账户总额度与已用 |
120
- | `moonshotai`、`moonshotai-cn`、`kimi-coding`(或 baseURL 落在 `*.moonshot.cn` / `*.moonshot.ai`) | `GET /v1/users/me/balance`(可用余额 / 现金余额 / 代金券) |
121
- | `siliconflow`(或 baseURL 落在 `*.siliconflow.cn` / `*.siliconflow.com`) | `GET /v1/user/info`(总余额 / 充值余额 / 赠送余额) |
122
- | 其它已配置 `baseURL` 的 OpenAI 兼容路由 | 尽力而为:按 cc-switch 的 New API 约定查 `GET {origin}/api/user/self`(`Authorization: Bearer <key>`、`User-Agent: cc-switch/1.0`),按 `quota / 500000` 归一化为 USD |
123
-
124
- **OpenCode Zen(按量付费余额)没有用量接口**:`/zen/v1/usage`、`/me`、`/credits` 全是 404,官方只能在 console 网页看,
125
- 所以 `opencode`(baseURL `https://opencode.ai/zen`)在面板里会显示「不支持用量查询」——同一个域名下的 `/zen/go` 才走 Go 适配器。
126
-
127
- 既没有 `baseURL`、也不是已知供应商的路由会明确报错(“无法查询用量”),不会去猜接口。
128
-
129
- 新增一个适配器大约 20 行:在 `kindOf()` 里按 host / provider 认领,写一个 `queryXxx()` 归一到 `{ rows }` 或 `{ windows, models }`,
130
- 客户端会自动用通用卡片渲染(`rows` 出标签值行,`windows` 出用量条,`raw` 收在「原始返回」按钮里)。
131
-
132
- ### 刷新与状态
133
-
134
- 刷新是后台行为,不是点击行为:这个 cell 随会话视图挂载,所以它在**挂载时、切换模型时**各查询一次,
135
- 之后只要「自动刷新」是开着的就按间隔在后台自己刷新 —— 弹窗关着也照样刷新,打开时直接就是最近一次的结果。
136
-
137
- - **状态行**(弹窗顶部)显示本次刷新的新鲜度:`刷新时间:刚刚` / `刷新时间:12 分钟前`,
138
- 查询进行中显示 `刷新中… · 当前显示 3 分钟前数据`,从未查询过显示 `尚未刷新`;
139
- 数据超过刷新间隔时会在状态行补一句提示。
140
- - **自动刷新开关**:状态行右侧的 `自动刷新:开/关` chip,点一下即切换(设置弹窗里也有「开启/关闭」)。
141
- 关闭后不再有后台定时刷新,打开弹窗且数据过期时才查询。
142
- - **刷新间隔**:`刷新设置` 里可选 1 / 5 / 15 / 30 / 60 分钟,也可以直接填 1–1440 之间的分钟数;
143
- 默认开启自动刷新、间隔 5 分钟。策略存在浏览器 localStorage(`dsh-model-usage:refresh`)。
144
- - **上次刷新超过间隔时**:打开弹窗不会清空内容 —— 先显示上次查询到的用量,并标记 `刷新中…`,
145
- 用量接口返回后再替换内容,状态变为 `刷新时间:刚刚`。
146
- - 数据在间隔内时打开弹窗**不会**发请求(直接用缓存),要强制刷新就点卡片里的「刷新」。
147
- - 刷新失败(例如接口 5xx / 网络不通)时保留上次数据,并在卡片下方追加 `上次刷新失败:…`;
148
- 从未成功过才显示整屏错误 + 「重试」。标题按钮在后台刷新时带一个呼吸小圆点,
149
- 鼠标悬停显示刷新策略。
150
- - host 侧每次用量接口调用有 30 秒超时,浏览器侧另有一道 45 秒的外层兜底,
151
- 避免一次卡住的请求把后续自动刷新一直挡在门外。
152
-
153
- ### 弹窗内容
154
-
155
- - 顶部状态行 + 自动刷新 chip + `刷新设置`。
156
- - 卡片:按上面的接口类型渲染(窗口用量条 / 余额 / 钱包)。
157
- - 卡片下方「模型 · 密钥 · 用量页」:本次查询用的 provider/model(含推理档位)与**凭据引用名 + 来源**
158
- (例如 `COMMANDCODE_API_KEY · 凭据 COMMANDCODE_API_KEY(store)`),确认用的是当前对话那把 key。
159
- - 底部「说明」:列出本部署声明的供应商路由 —— 用量接口类型、密钥引用名、是否已配置、接口地址。
160
- 默认只列出**有用量接口或已配置密钥**的路由(有些部署自带的目录有数十个路由),
161
- 其余收在「显示其余 N 个未配置路由」按钮后面;每个弹窗都自带上限高度 + 内部滚动
162
- (Modal 原语把高度留给调用方,不设上限时内容会顶出屏幕)。
163
-
164
- ### 与当前 Harness 的适配
165
-
166
- - manifest:`dsh.bundle.patch`、`dsh.client`(`platform: web`)、`dsh.manifestVersion: 1`,
167
- 以及 `engines.dsh: ">=0.2.1-rc.2"`(本插件逐项核对过的版本,只挡更旧的宿主)。
168
- - 展示元数据:`locale/zh.json` + `locale/en.json`(`meta.title` / `meta.description`)与
169
- `icon.svg` 都已从 `exports` / `files` 导出,插件列表与 bundle 详情直接读取。
170
- - 浏览器半只 require `react`(loader 的冻结模块表),不 require 任何 Harness Client 包;
171
- 数据面走本插件自己的 `webServer` 路由 + 同源 `fetch`(官方浏览器插件同款做法)。
172
- - host 半只用 Node 原生模块(`node:fs` / `node:os`);只有 `webServer` 是硬依赖,
173
- `connection`(信任栅栏)、`credentials`(取键)、`llm` / `settings` / `sessions` /
174
- `sessionProjections` / `sessionController` / `agentDefaultModel` / `deepseekAccount`
175
- 都在每次使用时通过 `ctx.get()` 读取 —— 少挂服务时插件照常挂载并在面板里说明缺什么,
176
- 后激活的服务下一次查询就能用上。
177
- - 浏览器半只有 `slots` 是硬依赖,`locale` 同样按需读取:没有 locale 服务时面板降级显示中文原文,
178
- 而不是整格消失。
179
-
180
- ## 安装
181
-
182
- 包已发布为 npm bundle,安装入口是"把 bundle 装进 profile":
183
-
184
- ```sh
185
- # CLI(推荐)
186
- dsh plugin --profile <profile> add dsh-model-usage
187
- ```
188
-
189
- 或者在 harness 的 agent 会话里让插件管理器装:`install_bundle`,target 传 `dsh-model-usage`
190
- (本机开发时传本目录的绝对路径)。两条路都会把包装进 profile、把 `dsh.bundle.patch` 指向的
191
- row(id `model-usage`)挂进 patch 层并激活——**不要**手写 profile 的 `cordis.patch.yml`,
192
- 也不要往 `$DSH_HOME/profiles/<profile>/node_modules` 手工复制。
193
-
194
- 安装后:
195
-
196
- - host 半改动/升级包后需要重启宿主(`dsh web` 或桌面版);
197
- - 浏览器半(`lib/client.js`)改动后硬刷新页面即可(开发时 `pnpm run dev:web` 会推送重建)。
198
-
199
- ## 发布(维护者)
200
-
201
- ```sh
202
- npm run verify # check + smoke;prepublishOnly 会自动跑同一组检查
203
- npm publish # 公开包,无需 --access
204
- ```
205
-
206
- `scripts/check.mjs` 会在发布前拦住会导致"装完不可用"的问题:缺 `dsh.bundle.patch`、
207
- `private: true`、exports/files 漏掉 locale 或图标、`lib/client.js` 的 loader id 与包名不一致、
208
- require 了非基线模块、往 `document.head`/`document.body` 写东西、字典缺键或中英不对称、
209
- host 半 import 了 node_modules 依赖、路由没用 `ctx.effect` 注册等。
210
- `scripts/smoke.mjs` 则在纯 Node 里把 host 半挂到桩 ctx 上跑一遍:三条路由的 kind/path、
211
- 无会话服务时的错误信封、以及信任栅栏的 403。两个脚本都不随包发布。
212
-
213
- 版本约定:面向用户的可见行为变化 → minor;修 bug → patch。`engines.dsh` 只挡比"已核对版本"更旧的宿主。
214
-
215
- 账号侧前置条件:如果 npm 账号开了 2FA,用普通 `.npmrc` 里的令牌发布会返回
216
- `E403 ... Two-factor authentication or granular access token with bypass 2fa enabled is required`。
217
- 两条解法:`npm publish --otp=<验证码>`(TOTP 账号),或在 npm 网页上建一个
218
- **Granular Access Token**(Permissions = Read and write,Packages = All packages,
219
- 勾选 allow bypass 2FA),再 `npm config set //registry.npmjs.org/:_authToken <token>`。
220
- 令牌只写进用户级 `~/.npmrc`,不要进仓库。
221
-
222
- ## 说明
223
-
224
- - 浏览器半**只从 loader 的模块表取 `react`**,不 require 任何 Harness Client 包:
225
- 面板里的按钮与弹窗是本插件自己的控件,只用当前主题列出的 `--dsw-alias-*` token 上色
226
- (唯一例外是弹窗遮罩,主题没有对应的 scrim token)。
227
- - 浏览器半的文案全部走 Client locale 服务(`ctx.locale`,命名空间 `dsh-model-usage`,
228
- 中英双语),跟随界面语言切换;host 返回的**接口诊断信息**(供应商报错原文)按原样显示。
229
- - 样式表随 cell 渲染(`<style>` 是 cell 输出的一部分),slot 条目卸载时一起消失,
230
- 不往 `document.head` 之外留东西。
231
- - 三条 host 路由在应答前都会先问 composition 的 connection 服务要一次浏览器信任裁决
232
- (本机 Host/Origin 栅栏),与官方浏览器插件同款;被拒时直接回 401/403。
233
- - 浏览器半用**文档相对路径**(`model-usage/...`)访问这三条路由,因此 GUI 挂在子目录下也能用。
234
- - 用量只能查到**供应商账户级**的数据(余额 / 套餐窗口 / 请求数);同一把 key 下的模型共享这些额度。
235
- - Command Code 模式的数字与官方 CLI 的 `/usage` 一致(同一组私有端点)。
236
- - 密钥只在本机 host 进程内使用,不写入日志、不返回给浏览器。
237
- - 本插件以 [MIT 许可](LICENSE)发布;DeepSeek Harness 本身同为 MIT,二者许可一致、可双向复用代码。
238
- 本项目是独立社区插件,与深度求索公司无隶属或背书关系。
239
- - 浏览器半是**手写的闭包工厂 bundle**,改动后硬刷新浏览器生效;host 半改动需重启 `dsh web`。
1
+ # dsh-model-usage
2
+
3
+ Provider-account usage for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)
4
+ sessions. The plugin adds a **Usage** (`用量`) entry to the session header and reports the quota
5
+ windows, balance and request counters of the provider account behind the model the conversation is
6
+ using.
7
+
8
+ Queries run in the host process with the provider's own credential reference. The browser receives
9
+ normalized values and the credential reference name; credential values never leave the host.
10
+
11
+ ![Usage panel](docs/screenshots/usage-panel.png)
12
+
13
+ *Usage panel for a Command Code subscription (Chinese UI): five-hour and weekly quota windows,
14
+ credit balance, spend, request count, and the model and credential reference used for the query.*
15
+
16
+ *Command Code 订阅的用量面板:5 小时 / 7 天窗口、信用余额、已花费、请求数,以及本次查询使用的
17
+ 模型与凭据引用。*
18
+
19
+ Published on npm as [`dsh-model-usage`](https://www.npmjs.com/package/dsh-model-usage).
20
+
21
+ ## Features
22
+
23
+ - **Session model.** The query target comes from the session's `modelSelection` projection, so the
24
+ panel follows model changes in the conversation.
25
+ - **Manual target.** Any routable provider model can be queried from the panel without changing the
26
+ model the conversation runs on.
27
+ - **Background refresh.** The mounted component refreshes on an interval, including while the panel
28
+ is closed.
29
+ - **Command Code balance history.** Successful balance queries save a baseline and then record
30
+ balance changes every 30 minutes, independently of the display refresh setting. Samples are kept
31
+ locally per account for up to seven days and shown as a spend line chart with last 30 minutes,
32
+ last 12 hours, today and yesterday filters.
33
+ - **Provider directory.** The deployment's provider routes are listed with their usage adapter,
34
+ credential reference name and configuration state. Values are never read out.
35
+
36
+ ## Requirements
37
+
38
+ - DeepSeek Harness `>=0.2.1-rc.2` with the Web surface
39
+ - Node.js `>=18`
40
+ - No runtime dependencies. The browser half requires only the platform `react` module.
41
+
42
+ ## Installation
43
+
44
+ ```sh
45
+ dsh plugin --profile <profile> add dsh-model-usage
46
+ ```
47
+
48
+ From an agent session, the bundle can also be installed by name with `install_bundle` (target
49
+ `dsh-model-usage`, or the repository path for a local checkout). Both paths install the package into
50
+ the profile, mount the bundle patch row `model-usage` and enable the bundle.
51
+
52
+ With profile HMR enabled, host-side changes apply immediately; otherwise restart the host
53
+ (`dsh web` or the desktop application). The browser half is served by `dsh-client-modules` and is
54
+ picked up by reloading the page.
55
+
56
+ ## Usage sources
57
+
58
+ | Provider / base URL | Source | Reported values |
59
+ | --- | --- | --- |
60
+ | `commandcode`, `*.commandcode.ai` | `GET /alpha/whoami?limits=1`, `/alpha/billing/credits`, `/alpha/billing/subscriptions`, `/alpha/usage/summary?since=` | Plan, five-hour and weekly windows, credit balance, spend, request count |
61
+ | `deepseek-official`, `deepseek`, `*.deepseek.com` | `GET https://api.deepseek.com/user/balance` | Balance per currency |
62
+ | `deepseek-account` | `ctx.deepseekAccount.getBalance()` | Wallet balances of the signed-in account, without an API key |
63
+ | `opencode-go`, `opencode.ai/zen/go` | `GET {origin}/zen/go/v1/usage` | Five-hour, weekly and monthly allowances per model |
64
+ | `openrouter`, `*.openrouter.ai` | `GET /api/v1/key`; `GET /api/v1/credits` when the key has no limit of its own | Used, limit, remaining and the account credit pool |
65
+ | `moonshotai`, `moonshotai-cn`, `kimi-coding`, `*.moonshot.cn`, `*.moonshot.ai` | `GET /v1/users/me/balance` | Available, cash and voucher balance |
66
+ | `siliconflow`, `*.siliconflow.cn`, `*.siliconflow.com` | `GET /v1/user/info` | Total, charged and granted balance |
67
+ | Other OpenAI-compatible routes with a `baseURL` | cc-switch New API convention: `GET {origin}/api/user/self` | Quota converted to USD; best effort |
68
+
69
+ The OpenCode Go endpoint is not publicly documented. Fields are normalized by their common names;
70
+ payloads that cannot be recognized remain available under **Raw response**. OpenCode Zen
71
+ (pay-as-you-go) has no usage endpoint (`/zen/v1/usage`, `/me` and `/credits` return 404), so
72
+ `opencode` routes report that usage queries are not supported. Routes without a `baseURL` and
73
+ without a known provider return an explicit error instead of an inferred endpoint.
74
+
75
+ Adding an adapter requires selecting the route in `kindOf()` and returning normalized data from a
76
+ `queryXxx()` function: `{ rows }` for label/value lines or `{ windows, models }` for meters. The
77
+ client renders both shapes with the generic card.
78
+
79
+ ## Query target and credentials
80
+
81
+ 1. The browser reads `next ?? lastUsed` from the session's `modelSelection` projection and sends
82
+ `{ provider, model, reasoningEffort, sessionId }` to `POST /model-usage/balance`.
83
+ 2. If the request carries no target, the host falls back to the session projection of the process
84
+ and then to the deployment default (`agentDefaultModel.currentSelection()`).
85
+ 3. The provider's settings location is resolved through `llm.listConfigurableProviders()` and read
86
+ with `settings.describe()`, which supplies `apiKeyEnv`, `baseURL`, `api` and custom headers.
87
+ 4. `ctx.credentials.resolve(apiKeyEnv)` returns the key in use. An unconfigured reference falls back
88
+ to the environment variable of the same name; Command Code also falls back to
89
+ `~/.commandcode/auth.json`. Keys are resolved per request, so a key added in Settings → Models is
90
+ used by the next query without a restart.
91
+ 5. The usage adapter is selected from the provider or base URL.
92
+
93
+ Providers without `apiKeyEnv` use the convention `${PROVIDER}_API_KEY`; `deepseek-official` uses
94
+ `DEEPSEEK_API_KEY`.
95
+
96
+ The panel header shows the active target. `Auto · <provider> / <model>` follows the conversation.
97
+ Selecting it opens the model picker with `Auto (follow the conversation)` and the routable provider
98
+ models from `sessionController.modelCatalog()`, annotated with their adapter or with
99
+ `usage query not supported`. A manual target changes only the panel's query, not the conversation
100
+ model, and the credential reference follows the selected provider. The choice is stored in
101
+ `dsh-model-usage:model` and can be reset to auto from the panel.
102
+
103
+ Usage is account-level: models behind the same key share the same quota, so selecting a target
104
+ selects the account to query. The panel lists the model and credential reference used.
105
+
106
+ ## Refresh and status
107
+
108
+ The component queries when it mounts and when the session model changes. With auto-refresh enabled
109
+ it continues on an interval whether or not the panel is open.
110
+
111
+ - The status line reports freshness: `Updated just now`, `Updated 12 min ago`,
112
+ `Refreshing… · showing data from 3 min ago`, or `Not refreshed yet`. A hint is appended when the
113
+ data is older than the interval.
114
+ - Auto refresh is toggled from the status line or the refresh settings dialog.
115
+ - The interval accepts 1, 5, 15, 30 or 60 minutes, or any value from 1 to 1440. Defaults are enabled
116
+ and five minutes. The policy is stored in `dsh-model-usage:refresh`.
117
+ - Opening the panel with data inside the interval does not send a request; **Refresh** forces one.
118
+ - A failed query keeps the previous data and appends `Last refresh failed: …`. The full error state
119
+ with **Retry** appears only when no query has succeeded.
120
+ - The host applies a 30-second timeout per usage call; the browser applies a 45-second outer timeout.
121
+
122
+ ## Security
123
+
124
+ - Credential values are read in the host process and are never returned to the browser or written to
125
+ logs. Responses contain normalized values plus the reference name and source.
126
+ - All three routes check the composition's browser trust decision (Host/Origin) through the
127
+ `connection` service and answer 401/403 when it is denied.
128
+ - The plugin uploads nothing. Usage requests go from the host to the provider API.
129
+
130
+ ## Compatibility
131
+
132
+ - The bundle declares `dsh.bundle.patch`, `dsh.client` (`platform: web`), `manifestVersion: 1`,
133
+ `engines.dsh` and `engines.node`.
134
+ - `locale/zh.json` and `locale/en.json` provide the plugin title and description, and `icon.svg` is
135
+ the icon; both are exported and included in the package.
136
+ - The browser half registers `conversation.session.header.utilities`, requires only `react`, and
137
+ addresses the plugin routes with document-relative URLs, so a GUI mounted under a subpath works.
138
+ - The host half uses Node built-ins only. `webServer` is the single hard dependency; `connection`,
139
+ `credentials`, `llm`, `settings`, `sessions`, `sessionProjections`, `sessionController`,
140
+ `agentDefaultModel` and `deepseekAccount` are resolved through `ctx.get()` when used, so the
141
+ plugin mounts where those services are optional or activate later.
142
+ - Without a locale service, both dictionaries fall back to the Chinese source text.
143
+
144
+ ## Development
145
+
146
+ ```sh
147
+ npm run verify # check + smoke, also runs from prepublishOnly
148
+ npm test # alias for the same suite
149
+ ```
150
+
151
+ `scripts/check.mjs` validates the manifest and npm metadata, shipped files, README images, loader
152
+ id, baseline module requests, dictionaries, host imports and route registration.
153
+ `scripts/smoke.mjs` mounts the host half on a stub context and exercises the three routes, the error
154
+ envelopes and the trust fence.
155
+
156
+ Release:
157
+
158
+ 1. Set `version` according to Semantic Versioning (visible behavior: minor; fixes: patch).
159
+ 2. Update `CHANGELOG.md`.
160
+ 3. Run `npm run verify`, then `npm publish`.
161
+
162
+ Accounts with 2FA publish with `npm publish --otp=<code>`, or authenticate with a Granular Access
163
+ Token that allows bypassing 2FA. Keep tokens in the user-level `~/.npmrc`, outside the repository.
164
+
165
+ ## License
166
+
167
+ MIT. DeepSeek Harness is also MIT-licensed. This is an independent community plugin, not affiliated
168
+ with or endorsed by DeepSeek.
169
+
170
+ ## 中文
171
+
172
+ 本插件为 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 会话提供
173
+ 当前模型对应供应商账户的用量。它在会话头部增加「用量」入口,显示套餐窗口、余额与请求数;
174
+ 查询在 host 进程内使用该供应商自己的凭据引用完成,浏览器只收到归一化数值与凭据引用名。
175
+
176
+ ### 功能
177
+
178
+ - **跟随会话模型**:查询对象取自会话的 `modelSelection` 投影,对话中切换模型时面板随之切换。
179
+ - **手动查询对象**:可查询任意可路由的供应商模型,不改变当前对话使用的模型。
180
+ - **后台刷新**:已挂载的组件按间隔刷新,弹窗关闭时同样生效。
181
+ - **供应商目录**:列出本部署声明的供应商路由、用量适配器、凭据引用名与配置状态,不读取凭据值。
182
+
183
+ ### 环境要求
184
+
185
+ - DeepSeek Harness `>=0.2.1-rc.2`,并启用 Web 界面
186
+ - Node.js `>=18`
187
+ - 无运行时依赖;浏览器半只使用平台提供的 `react` 模块
188
+
189
+ ### 安装
190
+
191
+ ```sh
192
+ dsh plugin --profile <profile> add dsh-model-usage
193
+ ```
194
+
195
+ 也可在 agent 会话中通过 `install_bundle` 安装(target 为 `dsh-model-usage`,本地开发时传仓库路径)。
196
+ 两种方式都会把包装入 profile、挂载 bundle patch 的 `model-usage` 行并启用。
197
+
198
+ 启用 profile HMR 时 host 侧改动立即生效;否则重启宿主(`dsh web` 或桌面版)。浏览器半由
199
+ `dsh-client-modules` 提供,刷新页面后生效。
200
+
201
+ ### 用量来源
202
+
203
+ | provider / baseURL | 来源 | 返回值 |
204
+ | --- | --- | --- |
205
+ | `commandcode`、`*.commandcode.ai` | `GET /alpha/whoami?limits=1`、`/alpha/billing/credits`、`/alpha/billing/subscriptions`、`/alpha/usage/summary?since=` | 套餐、5 小时 / 7 天窗口、信用余额、已花费、请求数 |
206
+ | `deepseek-official`、`deepseek`、`*.deepseek.com` | `GET https://api.deepseek.com/user/balance` | 按币种的余额 |
207
+ | `deepseek-account` | `ctx.deepseekAccount.getBalance()` | 已登录账户的钱包余额(不涉及 API Key) |
208
+ | `opencode-go`、`opencode.ai/zen/go` | `GET {origin}/zen/go/v1/usage` | 按模型的 5 小时 / 周 / 月额度 |
209
+ | `openrouter`、`*.openrouter.ai` | `GET /api/v1/key`;key 无独立额度时再查 `GET /api/v1/credits` | 已用、额度、剩余与账户额度池 |
210
+ | `moonshotai`、`moonshotai-cn`、`kimi-coding`、`*.moonshot.cn`、`*.moonshot.ai` | `GET /v1/users/me/balance` | 可用余额、现金余额、代金券 |
211
+ | `siliconflow`、`*.siliconflow.cn`、`*.siliconflow.com` | `GET /v1/user/info` | 总余额、充值余额、赠送余额 |
212
+ | 其它带 `baseURL` 的 OpenAI 兼容路由 | cc-switch New API 约定:`GET {origin}/api/user/self` | 按约定换算为 USD,尽力而为 |
213
+
214
+ OpenCode Go 的端点未公开文档化,字段按常见命名归一化;无法识别的返回保留在「原始返回」中。
215
+ OpenCode Zen(按量付费)没有用量接口(`/zen/v1/usage`、`/me`、`/credits` 均为 404),
216
+ `opencode` 路由会明确提示不支持用量查询。既无 `baseURL`、也非已知供应商的路由返回明确错误,
217
+ 不会推断接口。
218
+
219
+ 新增适配器只需在 `kindOf()` 中认领路由,并由 `queryXxx()` 返回归一化数据:`{ rows }` 渲染为
220
+ 标签值行,`{ windows, models }` 渲染为用量条,客户端统一用通用卡片展示。
221
+
222
+ ### 查询对象与凭据
223
+
224
+ 1. 浏览器读取会话 `modelSelection` 投影的 `next ?? lastUsed`,向 `POST /model-usage/balance`
225
+ 发送 `{ provider, model, reasoningEffort, sessionId }`。
226
+ 2. 请求未携带查询对象时,host 依次回退到本进程的会话投影与部署默认模型
227
+ (`agentDefaultModel.currentSelection()`)。
228
+ 3. host 通过 `llm.listConfigurableProviders()` 取得该 provider 的配置位置,并用
229
+ `settings.describe()` 读取实时配置,得到 `apiKeyEnv`、`baseURL`、`api` 与自定义 headers。
230
+ 4. `ctx.credentials.resolve(apiKeyEnv)` 取得当前使用的 key。引用未配置时回退到同名环境变量;
231
+ Command Code 还会回退到 `~/.commandcode/auth.json`。解析在每次查询时进行,因此
232
+ 「设置 → 模型」中新填的 key 无需重启即可用于下一次查询。
233
+ 5. 按 provider 或 baseURL 选择用量适配器。
234
+
235
+ 未声明 `apiKeyEnv` 的 provider 按 `${PROVIDER}_API_KEY` 约定推导引用名,`deepseek-official`
236
+ 使用 `DEEPSEEK_API_KEY`。
237
+
238
+ 面板顶部显示当前查询对象。`自动 · <provider> / <model>` 跟随对话;点击后可在模型选择器中选择
239
+ `自动(跟随当前对话)`,或选择 `sessionController.modelCatalog()` 中任意可路由模型(标注其适配器
240
+ 或「不支持用量查询」)。手动选择只改变面板的查询对象,不改变对话模型,凭据引用随所选供应商切换;
241
+ 选择保存在 `dsh-model-usage:model`,可在面板中改回自动。
242
+
243
+ 用量是账户级数据:同一把 key 下的模型共享同一份额度,因此切换查询对象即切换要查询的账户。
244
+ 面板底部列出本次查询使用的模型与凭据引用。
245
+
246
+ ### 刷新与状态
247
+
248
+ 组件在挂载时与会话模型变化时查询。开启自动刷新后,无论弹窗是否打开,都会按间隔继续查询。
249
+
250
+ - 状态行显示数据新鲜度:`刷新时间:刚刚`、`刷新时间:12 分钟前`、
251
+ `刷新中… · 当前显示 3 分钟前数据`、`尚未刷新`;数据超过刷新间隔时会追加提示。
252
+ - 自动刷新可在状态行或刷新设置中开关。
253
+ - 刷新间隔支持 1 / 5 / 15 / 30 / 60 分钟,也可填写 1–1440 之间的任意分钟数;默认开启、间隔
254
+ 5 分钟。策略保存在 `dsh-model-usage:refresh`。
255
+ - 数据仍在间隔内时打开面板不会发起请求;「刷新」按钮可强制查询。
256
+ - 查询失败时保留上次数据并追加 `上次刷新失败:…`;仅在从未成功时显示整屏错误与「重试」。
257
+ - Command Code 余额历史独立于面板刷新开关,每 30 分钟采样并按账户保存在浏览器本地,最多保留
258
+ 七天;用量卡片提供「最近半小时 / 最近半天 / 今天 / 昨天」折线图筛选。首次记录作为余额基线,
259
+ 后续每段消耗按前后余额差计算,余额增加时该段显示为零。
260
+ - host 侧单次用量请求超时 30 秒,浏览器侧另有 45 秒外层超时。
261
+
262
+ ### 安全
263
+
264
+ - 凭据值只在 host 进程内读取,不返回浏览器,也不写入日志;响应仅包含归一化数值、引用名与来源。
265
+ - 三条路由在应答前通过 `connection` 服务校验浏览器信任(Host/Origin),未通过时返回 401/403。
266
+ - 插件不上传任何数据,用量请求由 host 直接发往供应商接口。
267
+
268
+ ### 兼容性
269
+
270
+ - bundle 声明 `dsh.bundle.patch`、`dsh.client`(`platform: web`)、`manifestVersion: 1`、
271
+ `engines.dsh` 与 `engines.node`。
272
+ - `locale/zh.json`、`locale/en.json` 提供插件标题与描述,`icon.svg` 为图标;两者都已导出并包含在包内。
273
+ - 浏览器半注册 `conversation.session.header.utilities`,只依赖 `react`,并使用文档相对路径访问
274
+ 插件路由,因此 GUI 挂在子目录下同样可用。
275
+ - host 半只使用 Node 内置模块。`webServer` 是唯一硬依赖;`connection`、`credentials`、`llm`、
276
+ `settings`、`sessions`、`sessionProjections`、`sessionController`、`agentDefaultModel`、
277
+ `deepseekAccount` 均按需通过 `ctx.get()` 获取,服务缺失或稍后激活时插件仍可挂载。
278
+ - 没有 locale 服务时,词典回退为中文原文。
279
+
280
+ ### 开发
281
+
282
+ ```sh
283
+ npm run verify # check + smoke,prepublishOnly 同样执行
284
+ npm test # 同一套检查的别名
285
+ ```
286
+
287
+ `scripts/check.mjs` 校验清单与 npm 元数据、随包文件、README 图片、loader id、基线模块、
288
+ 词典、host 导入与路由注册;`scripts/smoke.mjs` 在桩 ctx 上挂载 host 半,覆盖三条路由、
289
+ 错误响应与信任栅栏。
290
+
291
+ 发布流程:
292
+
293
+ 1. 按语义化版本更新 `version`(可见行为变化为 minor,修复为 patch)。
294
+ 2. 更新 `CHANGELOG.md`。
295
+ 3. 运行 `npm run verify`,然后执行 `npm publish`。
296
+
297
+ 启用 2FA 的账号可用 `npm publish --otp=<验证码>` 发布,或使用允许绕过 2FA 的 Granular Access
298
+ Token 认证。令牌只保存在用户级 `~/.npmrc`,不要提交到仓库。
299
+
300
+ ### 许可
301
+
302
+ MIT。DeepSeek Harness 同样以 MIT 许可发布。本项目为独立社区插件,与深度求索公司无隶属或背书关系。
package/cordis.patch.yml CHANGED
@@ -1,11 +1,11 @@
1
- # dsh-model-usage bundle patch: mounts the package's node half as a host entry.
2
- # The node half hosts the loopback-only /model-usage Web routes that query the
3
- # usage of the model the current conversation runs on (with that provider's own
4
- # credential reference); the browser half (discovered by client-modules from
5
- # this row's dsh.client declaration) renders the 用量 button and panel.
6
- # No Config schema is declared, so an empty config keeps the row easy to extend
7
- # from a user patch layer later.
8
- - insert:
9
- - id: model-usage
10
- name: 'dsh-model-usage'
11
- config: {}
1
+ # dsh-model-usage bundle patch: mounts the package's node half as a host entry.
2
+ # The node half hosts the loopback-only /model-usage Web routes that query the
3
+ # usage of the model the current conversation runs on (with that provider's own
4
+ # credential reference); the browser half (discovered by client-modules from
5
+ # this row's dsh.client declaration) renders the 用量 button and panel.
6
+ # No Config schema is declared, so an empty config keeps the row easy to extend
7
+ # from a user patch layer later.
8
+ - insert:
9
+ - id: model-usage
10
+ name: 'dsh-model-usage'
11
+ config: {}
Binary file
package/icon.svg CHANGED
@@ -1,7 +1,7 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32" role="img" aria-label="Usage">
2
- <rect x="1" y="1" width="30" height="30" rx="8" fill="#1f6feb"/>
3
- <path d="M7.5 21.5a8.5 8.5 0 0 1 17 0" fill="none" stroke="#ffffff" stroke-width="2.4" stroke-linecap="round"/>
4
- <path d="M16 21.5 21.4 16.4" fill="none" stroke="#ffffff" stroke-width="2.4" stroke-linecap="round"/>
5
- <circle cx="16" cy="21.5" r="2" fill="#ffffff"/>
6
- <path d="M9.5 26h13" fill="none" stroke="#ffffff" stroke-width="2.2" stroke-linecap="round" opacity="0.7"/>
7
- </svg>
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32" role="img" aria-label="Usage">
2
+ <rect x="1" y="1" width="30" height="30" rx="8" fill="#1f6feb"/>
3
+ <path d="M7.5 21.5a8.5 8.5 0 0 1 17 0" fill="none" stroke="#ffffff" stroke-width="2.4" stroke-linecap="round"/>
4
+ <path d="M16 21.5 21.4 16.4" fill="none" stroke="#ffffff" stroke-width="2.4" stroke-linecap="round"/>
5
+ <circle cx="16" cy="21.5" r="2" fill="#ffffff"/>
6
+ <path d="M9.5 26h13" fill="none" stroke="#ffffff" stroke-width="2.2" stroke-linecap="round" opacity="0.7"/>
7
+ </svg>