dsh-tokenlens 1.5.2 → 1.6.0

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/CHANGELOG.md CHANGED
@@ -4,6 +4,24 @@
4
4
 
5
5
  **版本记录约定**:同一发布日只保留一个章节,标题取当日**最后一个**已发布版本号;当日多次发布的改动合并到该章节,并在标题下注明当日实际发布过的版本号(npm 上仍可安装这些独立版本)。若当日已有发布,后续小改动不再单独增加版本号,而是并入下一篇日志。这样日志按天规整、不出现同日多个小版本。
6
6
 
7
+ ## 1.6.0 — 2026-11-03
8
+
9
+ - **feat(open-design): 「本期 Design Plan 累计」改按官方定价表的模型单价计算**——原来是 `usedCredits ÷ creditsPerUsd`,这个算法隐含「按 1× 倍率消耗」的假设,而官方倍率表跨度 0.25×–4×:对 DeepSeek V4.1 Flash(0.5×)会**少算一半**,对 GPT-6 Luna(4×)会**多算 4 倍**。现在按每个模型的实际 Token × 官方单价(输入/输出/缓存读分别计价)累加,结果可直接与定价表比对
10
+ - **feat(open-design): 给出与倍率匹配的月额度**——定价表里 $120 是**列表价**口径且按倍率消耗:只用 0.5× 模型(DeepSeek V4.1 Flash)可动用**完整 $120**,1× 用 $60,2× 用 $30,4× 用 $15。弹窗改为显示「**已用 / 月额度 + 百分比**」(如 `$27.31 / $120.00 22.8%`),并注明「按 0.5× 倍率模型计价」
11
+ - feat(open-design): 混合模型时按**最高**倍率取额度(不是平均)——4× 模型的每一单位消耗其份额的速度是 4 倍,约束由它决定
12
+ - feat(wire): `designPlan` 新增 `monthlyCapUsd` / `capPercent` / `multiplier` / `creditsUsd`;`estimatedUsd` 语义改为「官方单价口径的消费额」,旧算法结果保留在 `creditsUsd` 便于对照
13
+ - feat(stats): `models[].last30Days` 增加 `input` / `output` / `cacheRead` / `cacheWrite` 拆分,供按单价计价使用(此前只有 total)
14
+ - fix(open-design): 单价表按官方文档转录 13 个 Design Plan 模型;`hunyuan-h4-preview` / `glm-5.3-flash-x` 不在公开价目接口里,价格缺失时按「未定价」处理并如实上报,绝不臆造
15
+ - 测试:318 → 328(倍率阶梯 0.5×→$120 / 1×→$60 / 2×→$30 / 4×→$15、混合取最高倍率、未知模型不臆造倍率与额度、按 token 种类计价、**用控制台真实账单核对:$0.0048 列表价 × 0.5 = $0.0024 实扣**、未定价模型上报)
16
+
17
+ ## 1.5.3 — 2026-11-03
18
+
19
+ - **feat(open-design): 初次配置只需一个 Control Key**——工作区 ID 改为**可选**。Control Key 本身就能确定它属于哪个工作区,插件改用 `GET /api/v1/workspaces/current` 自动识别(回退 `GET /api/v1/workspaces`,仅当可见工作区恰好一个时采纳)
20
+ - feat(open-design): **多工作区不猜**——`/workspaces/current` 没有 active 且有多个可见工作区时返回 `missing-key` 并提示手动填写「工作区 ID」,因为猜错会把别的团队的用量当成自己的
21
+ - feat(open-design): 显式配置的工作区 ID 优先于自动识别(管理页 / `providers.open-design.workspaceId` / `OPENDESIGN_WORKSPACE_ID`),多工作区账号据此指定目标
22
+ - feat(ui): 管理页「工作区 ID」文案改为可选,说明留空即自动识别、仅在多工作区时需要填
23
+ - 测试:317 → 318(仅凭 Key 自动识别工作区并用于后续所有计费读取、多工作区拒绝猜测)
24
+
7
25
  ## 1.5.2 — 2026-11-02
8
26
 
9
27
  (当日已发布版本:v1.3.5 / v1.3.6 / v1.3.7 / v1.3.8 / v1.3.9 / v1.4.0 / v1.5.0 / v1.5.1 / v1.5.2)
package/PROTOCOL.md CHANGED
@@ -113,17 +113,27 @@ A `windows` snapshot may also carry a non-null `balance` (Open Design reports
113
113
  allowance windows and a wallet independently); the Client renders it as one extra
114
114
  row at the end of the capacity card and appends the amount to the chip tooltip.
115
115
 
116
- `open-design` needs two credentials that the API-keys page does not provide: a
117
- **workspace id** (the console URL's `?workspaceId=` value) and a **Control Key**
118
- (the `profiles.prod.controlKey` the desktop app / `vela login` writes to
119
- `~/.amr/config.json`). The inference key from the Cloud API Keys page answers
120
- HTTP 401 on the billing endpoints. Missing either value is a `missing-key` error.
121
- Since v1.3.9 both may be entered on the management page: `workspaceId` is
122
- persisted in `providers.json`, and the Control Key is written through the same
123
- `apiKey` override field the store already supported (write-only — the Host never
124
- returns it, only `hasKey`/`keySource`). The environment/credential sources
125
- (`OPENDESIGN_CONTROL_KEY`, `VELA_CONTROL_KEY`, `OPENDESIGN_WORKSPACE_ID`) still
126
- work and the page override wins.
116
+ `open-design` needs a **Control Key** (the `profiles.prod.controlKey` the desktop
117
+ app / `vela login` writes to `~/.amr/config.json`). The inference key from the
118
+ Cloud API Keys page answers HTTP 401 on the billing endpoints, so it cannot be
119
+ used here. A missing Control Key is a `missing-key` error.
120
+
121
+ **The workspace id is optional (v1.5.3).** A Control Key identifies the workspace
122
+ it belongs to, so the plugin discovers it:
123
+
124
+ 1. `GET /api/v1/workspaces/current` — the key's active workspace
125
+ (`workspaceId`, else `activeWorkspaceId`); this is the normal path.
126
+ 2. `GET /api/v1/workspaces` — fallback: accepted only when exactly one workspace
127
+ is visible. Several visible workspaces with none marked active is a
128
+ `missing-key` error rather than a guess, because picking the wrong one would
129
+ report another team's usage as this account's.
130
+
131
+ A configured id always wins over discovery (management page → 工作区 ID, persisted
132
+ in `providers.json`; or `OPENDESIGN_WORKSPACE_ID`), which is how a multi-workspace
133
+ account selects one. The Control Key is written through the same `apiKey` override
134
+ field the store already supported (write-only — the Host never returns it, only
135
+ `hasKey`/`keySource`). `OPENDESIGN_CONTROL_KEY` / `VELA_CONTROL_KEY` are still
136
+ honoured and the page override wins.
127
137
 
128
138
  Reads, in independent layers (v1.4.0) — each may fail without voiding the others:
129
139
 
@@ -151,24 +161,63 @@ by observing the weekly windows it sees:
151
161
  "designPlan": {
152
162
  "periodStart": "2026-11-02T09:53:18.000Z", // billing period (billing/summary)
153
163
  "periodEnd": "2026-12-02T09:53:18.000Z",
154
- "usedCredits": 50025, // Σ final+current weekly windows
155
- "estimatedUsd": 5.0025, // usedCredits / creditsPerUsd
164
+ "usedCredits": 142090, // Σ final+current weekly windows
165
+ "estimatedUsd": 27.31, // v1.6.0: documented list-price cost
166
+ "creditsUsd": 14.209, // the OLD (1x-assuming) conversion
167
+ "monthlyCapUsd": 120, // documented cap for this model mix
168
+ "multiplier": 0.5, // worst documented multiplier seen
169
+ "capPercent": 22.76, // estimatedUsd / monthlyCapUsd
156
170
  "windows": 1, // weekly windows observed
157
171
  "complete": true, // earliest window starts at periodStart
158
172
  "since": "2026-11-02T09:53:18.000Z", // first window actually observed
159
173
  "limitPerWindow": 150000, // the account's real weekly cap
160
174
  "periodLimitCredits": 600000, // limitPerWindow * 4
161
- "percent": 8.80 // usedCredits / periodLimitCredits
175
+ "percent": 23.68 // usedCredits / periodLimitCredits
162
176
  }
163
177
  ```
164
178
 
179
+ ### Why `estimatedUsd` is not `usedCredits / creditsPerUsd` (v1.6.0)
180
+
181
+ Open Design bills **credits**, but the pricing page states every cap in
182
+ **list-price dollars** and assigns each model a credit-burn **multiplier**:
183
+
184
+ | model | multiplier | monthly cap it unlocks |
185
+ |---|---|---|
186
+ | DeepSeek V4.1 Flash | 0.5× | $120 (the whole Go cap) |
187
+ | MiMo V2.6 Flash / DeepSeek V4 Pro / GLM-5.3 Flash … | 1× | $60 |
188
+ | Claude Haiku 5.5 / GLM-5.3 Flash-X | 2× | $30 |
189
+ | GPT-6 Luna | 4× | $15 |
190
+
191
+ So `usedCredits / creditsPerUsd` silently assumes a 1× burn: it reports **half** the
192
+ true list-price value for a 0.5× model and **4× too much** for a 4× model. The
193
+ account's own windows confirm the unit (`50,000` credits per 5h = the documented
194
+ $10, `150,000` per 7d = $30, i.e. 5,000 credits per list-price dollar), which is
195
+ why `creditsPerUsd` is 10,000 — two credits per list-price dollar at 0.5×.
196
+
197
+ `estimatedUsd` is therefore computed from the tokens actually consumed:
198
+
199
+ ```
200
+ cost = Σ per model ( input×p_in + output×p_out + cacheRead×p_read ) / 1e6
201
+ ```
202
+
203
+ with the prices transcribed from the same page. Verified against one real console
204
+ receipt: input 311 + output 2,610 + cacheRead 258,560 on DeepSeek V4.1 Flash gives
205
+ `$0.0048` list price, ×0.5 multiplier = `$0.0024` charged — exactly what the usage
206
+ table shows. Note the table's "input" column is non-cached + cache, so `input` in
207
+ this formula **excludes** `cacheRead` or the cache bytes would be billed twice.
208
+
209
+ The cap uses the **highest** multiplier in the mix, not an average: a 4× model
210
+ consumes its share four times faster, so it binds. `creditsUsd` keeps the old
211
+ number on the wire for comparison; a client showing the old figure can migrate
212
+ without a wire break.
213
+
165
214
  `windows.monthly` is then filled from that estimate (v1.5.1), with `resetsAt` taken
166
215
  from `periodEnd`, so the existing monthly row and the chip's `月 N%` piece carry it
167
- without a second UI. The ×4 monthly-to-weekly ratio is the **one documented
168
- derivation**: the published pricing table states the monthly cap as exactly four
169
- weekly windows for every tier (Go $120/$30, Plus $160/$40, Pro $360/$90, Max
170
- $600/$150). A partial estimate still reports a percentage but sets the window
171
- `status` to `unknown`, so it can never paint a warning/critical dot.
216
+ without a second UI. The ×4 monthly-to-weekly ratio is documented too: the pricing
217
+ table states the monthly cap as exactly four weekly windows for every tier (Go
218
+ $120/$30, Plus $160/$40, Pro $360/$90, Max $600/$150). A partial estimate still
219
+ reports a percentage but sets the window `status` to `unknown`, so it can never
220
+ paint a warning/critical dot.
172
221
 
173
222
  Rules the Host guarantees:
174
223
 
package/README.md CHANGED
@@ -326,8 +326,9 @@ Control Key 是 **Open Design 桌面端/CLI 登录时写入本机的会话凭据
326
326
 
327
327
  - OpenCode 的额度定义见[官方文档](https://opencode.ai/docs/go/#usage-limits)(5h = 月度额度的 20%、周 = 50%、月 = 100%);接口只返回百分比,插件也只画百分比。
328
328
  - Command Code 的 `/alpha/*` 是官方 CLI 自用的私有计费路由;免费 / 无方案账户返回空窗口(显示 `—`),插件不会用方案目录猜额度。
329
- - Open Design 的窗口按账户实际返回的时长渲染:Go 同时有 5 小时和 7 天窗口,Plus / Pro / Max 只有 7 天窗口;钱包余额是独立于窗口的 USD 额度,用尽后继续扣钱包。查询要求工作区权限,鉴权失败会明确显示错误而不是 0;工作区 ID 与 Control Key 缺任一项时,弹窗会指出缺哪一项并给出「去配置」入口(v1.3.5)。窗口、余额、套餐档位是**三层独立读取**(v1.4.0):任一接口不可用时只少那一块数据,不会整套报错。
330
- - **chip 的「月 N%」与「本期 Design Plan 累计」是插件本机算的估算值**(v1.5.0 / v1.5.1)。Open Design 有月度额度(Go 为每月 $120 = 4 × 每周 $30),但**没有「本账期已用」接口**——控制台的 `/billing/usage`、`/billing/coding-plan-usage` 都靠浏览器 Cookie 鉴权,Control Key 一律 401。于是插件累加它观测到的 **7 天窗口**(只记 7 天,因为 5 小时窗口测量的是同一笔消耗,相加会重复计数),再按官方「月度 = 4 × 周额度」的比例折算成百分比填进 chip 的「月」槽位,并按 `creditsPerUsd` 折算美元。弹窗会标注覆盖情况(「已覆盖本账期全部窗口」或「仅统计已观测的 N 个窗口」);覆盖不完整时百分比仍显示但**不会触发告警配色**,插件没运行期间错过的窗口无法计入。
329
+ - Open Design 的窗口按账户实际返回的时长渲染:Go 同时有 5 小时和 7 天窗口,Plus / Pro / Max 只有 7 天窗口;钱包余额是独立于窗口的 USD 额度,用尽后继续扣钱包。查询要求工作区权限,鉴权失败会明确显示错误而不是 0。**只需填一个 Control Key**:工作区 ID 是可选项(v1.5.3),插件会用 Key 调 `/api/v1/workspaces/current` 自动识别你的工作区;只有账号有多个工作区、需要指定其中一个时才在管理页填「工作区 ID」。真正缺 Key 时弹窗会指出并给出「去配置」入口。窗口、余额、套餐档位是**三层独立读取**(v1.4.0):任一接口不可用时只少那一块数据,不会整套报错。
330
+ - **chip 的「月 N%」与「本期 Design Plan 累计」是插件本机算的估算值**(v1.5.0 / v1.6.0)。Open Design 有月度额度(Go 为每月 $120 = 4 × 每周 $30),但**没有「本账期已用」接口**——控制台的 `/billing/usage`、`/billing/coding-plan-usage` 都靠浏览器 Cookie 鉴权,Control Key 一律 401。于是插件累加它观测到的 **7 天窗口**(只记 7 天,因为 5 小时窗口测量的是同一笔消耗,相加会重复计数)。
331
+ - **金额按官方定价表的模型单价计算**(v1.6.0)。原来用 `usedCredits ÷ creditsPerUsd`,隐含「按 1× 倍率消耗」,而官方倍率跨度 0.25×–4×,对 DeepSeek V4.1 Flash(0.5×)会少算一半、对 GPT-6 Luna(4×)会多算 4 倍。现在按每个模型的实际 Token × 官方单价(输入/输出/缓存读分别计价)累加,弹窗显示「已用 / 月额度 + 百分比」(如 `$27.31 / $120.00 22.8%`)并注明「按 0.5× 倍率模型计价」——**只用 0.5× 模型可用满完整 $120**,1× 只能到 $60,2× 到 $30,4× 到 $15。弹窗会标注覆盖情况(「已覆盖本账期全部窗口」或「仅统计已观测的 N 个窗口」);覆盖不完整时百分比仍显示但**不会触发告警配色**,插件没运行期间错过的窗口无法计入。
331
332
  - 未配置凭据的供应商 chip 静默隐藏;`zai`、`minimax`、`deepinfra` 属 best-effort 适配(未公开接口,失败时按可重试错误优雅降级)。
332
333
  - 各供应商的元数据(`kind` / `label` / `shortLabel` / `baseURL` / `apiKeyRef` / `consoleUrl`)集中在 `lib/providers.mjs` 的 `PROVIDER_DEFAULTS` 与 `ADAPTERS` 中,新增适配器只需在该文件登记并补测试。
333
334
 
@@ -439,7 +440,7 @@ Host 半注册 `tokenlens_usage`,参数 `action`(默认 `snapshot`)与可
439
440
 
440
441
  ## 验证
441
442
 
442
- - `node --test`:**317 个自动化用例**,覆盖 Host 数据层、供应商适配器、统计折叠、各 harness 读取、Design Plan 周期账本、窗口锚点切片、插件集成与浏览器端渲染 / 交互。
443
+ - `node --test`:**328 个自动化用例**,覆盖 Host 数据层、供应商适配器、统计折叠、各 harness 读取、Design Plan 周期账本与官方单价计价、窗口锚点切片、工作区自动识别、插件集成与浏览器端渲染 / 交互。
443
444
  - 在真实环境中验证过 **5 个 harness** 的数据源;本机统计与独立的 SQLite 查询、独立的日志解析逐项对账(差额为 0),跨 harness 合并与供应商映射算术自洽。
444
445
  - 供应商接口按各官方文档与参考客户端行为验证;未配置凭据时 chip 静默隐藏,接口异常时按可重试错误降级。
445
446
 
package/client.js CHANGED
@@ -329,9 +329,9 @@ window.__ModuleLoader__.load({
329
329
  presetCollapse: '收起',
330
330
  providerConsoleURL: '控制台地址(Console URL)',
331
331
  providerConsoleURLPlaceholder: '留空则使用内置默认地址',
332
- providerWorkspaceId: '工作区 ID(Workspace ID)',
333
- providerWorkspaceIdHint: 'Open Design Cloud 工作区标识;额度查询需要',
334
- providerWorkspaceIdPlaceholder: '粘贴工作区 ID,或直接粘贴控制台地址(自动提取)',
332
+ providerWorkspaceId: '工作区 ID(Workspace ID,可选)',
333
+ providerWorkspaceIdHint: '可留空——插件会用 Control Key 自动识别你的工作区;仅在账号有多个工作区、需要指定其中一个时才填',
334
+ providerWorkspaceIdPlaceholder: '留空自动识别;或粘贴工作区 ID / 控制台地址',
335
335
  providerWorkspaceIdTooLong: '工作区 ID 最多 120 个字符',
336
336
  providerControlKey: 'Control Key',
337
337
  providerControlKeyHint: '本机 Open Design 登录后写在 ~/.amr/config.json 的 profiles.prod.controlKey;不是 Cloud API Keys 页的推理 Key',
@@ -341,8 +341,8 @@ window.__ModuleLoader__.load({
341
341
  errorOpenDesignConfig: 'Open Design 额度查询需要工作区 ID 与 Control Key(不是推理 Key)',
342
342
  designPlanLabel: '本期 Design Plan 累计',
343
343
  designPlanHint:
344
- 'Open Design 没有月度用量接口,此值由插件在本机累加观测到的 7 天窗口得出,仅供参考;未观测到的窗口不会计入',
345
- designPlanWindowsOf: '≈ {count} 个周额度',
344
+ '按官方定价表的模型单价换算本机实际消耗的 Token(不是 usedCredits ÷ 汇率)。Open Design 没有月度用量接口,此值由插件在本机累计观测到的 7 天窗口得出,仅供参考',
345
+ designPlanMultiplier: '按 {multiplier}× 倍率模型计价',
346
346
  designPlanComplete: '已覆盖本账期全部窗口',
347
347
  designPlanPartial: '仅统计已观测的 {count} 个窗口',
348
348
  providerSave: '保存',
@@ -590,9 +590,10 @@ window.__ModuleLoader__.load({
590
590
  presetExpand: 'Expand',
591
591
  presetCollapse: 'Collapse',
592
592
  providerConsoleURL: 'Console URL',
593
- providerWorkspaceId: 'Workspace ID',
594
- providerWorkspaceIdHint: 'The Open Design Cloud workspace identifier the quota query is scoped to',
595
- providerWorkspaceIdPlaceholder: 'Paste the workspace ID, or the console URL (it is extracted)',
593
+ providerWorkspaceId: 'Workspace ID (optional)',
594
+ providerWorkspaceIdHint:
595
+ 'Leave empty — the plugin finds your workspace from the Control Key. Fill it only when the account has several workspaces and you need a specific one.',
596
+ providerWorkspaceIdPlaceholder: 'Auto-detected; or paste a workspace ID / console URL',
596
597
  providerWorkspaceIdTooLong: 'The workspace ID may be at most 120 characters',
597
598
  providerControlKey: 'Control Key',
598
599
  providerControlKeyHint: 'Written to ~/.amr/config.json as profiles.prod.controlKey after signing in to Open Design; not the inference key from the Cloud API Keys page',
@@ -600,10 +601,10 @@ window.__ModuleLoader__.load({
600
601
  providerControlKeySaved: 'Saved; paste again to replace it',
601
602
  configureProvider: 'Configure',
602
603
  errorOpenDesignConfig: 'Open Design quota needs a workspace ID and a Control Key (not an inference key)',
603
- designPlanLabel: 'Period Design Plan (estimate)',
604
+ designPlanLabel: 'Period Design Plan',
604
605
  designPlanHint:
605
- 'Open Design serves no period-usage endpoint: this value accumulates the 7-day windows observed on this machine. Windows observed while the plugin was not running are not counted.',
606
- designPlanWindowsOf: '≈ {count} weekly allowances',
606
+ 'Priced from the official per-model rates over the tokens this machine actually consumed (not usedCredits ÷ rate). Open Design serves no period-usage endpoint, so the window total is accumulated locally; treat it as an estimate.',
607
+ designPlanMultiplier: 'priced at the {multiplier}× model rate',
607
608
  designPlanComplete: 'covers every window of this period',
608
609
  designPlanPartial: 'only {count} observed window(s)',
609
610
  providerConsoleURLPlaceholder: 'Leave empty for the built-in default',
@@ -1089,6 +1090,11 @@ window.__ModuleLoader__.load({
1089
1090
  const windows = Number.isFinite(raw.windows) && raw.windows > 0 ? Math.round(raw.windows) : 0
1090
1091
  const estimatedUsd = numberOrNull(raw.estimatedUsd)
1091
1092
  const limitPerWindow = numberOrNull(raw.limitPerWindow)
1093
+ // v1.6.0 documented-price context: the cap the model mix entitles and the
1094
+ // share of it consumed. Both are optional (an older Host omits them).
1095
+ const monthlyCapUsd = numberOrNull(raw.monthlyCapUsd)
1096
+ const capPercent = numberOrNull(raw.capPercent)
1097
+ const multiplier = numberOrNull(raw.multiplier)
1092
1098
  return {
1093
1099
  periodStart: stringOrEmpty(raw.periodStart),
1094
1100
  periodEnd: stringOrEmpty(raw.periodEnd),
@@ -1098,6 +1104,9 @@ window.__ModuleLoader__.load({
1098
1104
  complete: raw.complete === true,
1099
1105
  since: stringOrEmpty(raw.since),
1100
1106
  limitPerWindow: limitPerWindow !== null && limitPerWindow > 0 ? limitPerWindow : null,
1107
+ monthlyCapUsd: monthlyCapUsd !== null && monthlyCapUsd > 0 ? monthlyCapUsd : null,
1108
+ capPercent,
1109
+ multiplier: multiplier !== null && multiplier > 0 ? multiplier : null,
1101
1110
  }
1102
1111
  }
1103
1112
 
@@ -4017,25 +4026,38 @@ window.__ModuleLoader__.load({
4017
4026
  }
4018
4027
 
4019
4028
  /**
4020
- * The open-design period estimate row (v1.5.0).
4029
+ * The open-design period estimate row (v1.5.0, priced in v1.6.0).
4021
4030
  *
4022
4031
  * Open Design has no period-usage endpoint, so this is the Host's local
4023
- * accumulation of the weekly windows it observed. It is labelled as an
4024
- * estimate and carries a coverage note: when the first observed window does
4025
- * not start at the billing period start, earlier windows were never seen and
4026
- * the value is a lower bound.
4032
+ * accumulation of the observed windows. The dollar figure is the DOCUMENTED
4033
+ * LIST-PRICE value of the tokens consumed this period, which makes it
4034
+ * comparable with the published caps — that is what "can I use $120?" turns
4035
+ * on. The denominator is the cap those model multipliers entitle the account
4036
+ * to (0.5x models unlock the full $120; 1x unlocks $60).
4027
4037
  */
4028
4038
  function buildDesignPlanRow(designPlan, t) {
4029
4039
  if (designPlan === null) return null
4030
4040
  const parts = []
4031
- if (designPlan.estimatedUsd !== null) {
4041
+ const hasUsd = designPlan.estimatedUsd !== null
4042
+ if (hasUsd) {
4043
+ // "$27.31 / $120" — the numerator is documented list-price usage, the
4044
+ // denominator the documented cap for this model mix.
4045
+ const value =
4046
+ designPlan.monthlyCapUsd !== null
4047
+ ? `${formatMoney(designPlan.estimatedUsd, 'USD')} / ${formatMoney(designPlan.monthlyCapUsd, 'USD')}`
4048
+ : formatMoney(designPlan.estimatedUsd, 'USD')
4032
4049
  parts.push(
4033
- h(
4034
- 'span',
4035
- { key: 'usd', className: 'dsh-tokenlens-percent dsh-tokenlens-capacity-money' },
4036
- formatMoney(designPlan.estimatedUsd, 'USD'),
4037
- ),
4050
+ h('span', { key: 'usd', className: 'dsh-tokenlens-percent dsh-tokenlens-capacity-money' }, value),
4038
4051
  )
4052
+ if (designPlan.capPercent !== null) {
4053
+ parts.push(
4054
+ h(
4055
+ 'span',
4056
+ { key: 'cap', className: 'dsh-tokenlens-percent', 'data-level': levelOf(designPlan.capPercent, 'ok') },
4057
+ formatPercent(designPlan.capPercent, t),
4058
+ ),
4059
+ )
4060
+ }
4039
4061
  } else {
4040
4062
  parts.push(
4041
4063
  h(
@@ -4046,9 +4068,10 @@ window.__ModuleLoader__.load({
4046
4068
  )
4047
4069
  }
4048
4070
  const notes = []
4049
- if (designPlan.limitPerWindow !== null && designPlan.usedCredits > 0) {
4050
- const ratio = designPlan.usedCredits / designPlan.limitPerWindow
4051
- notes.push(t('designPlanWindowsOf', { count: ratio.toFixed(1) }))
4071
+ // Say WHICH multiplier set the cap, since that is why the ceiling is not
4072
+ // always $120 (the pricing table: 0.5x→$120, 1x→$60, 2x→$30, 4x→$15).
4073
+ if (designPlan.multiplier !== null) {
4074
+ notes.push(t('designPlanMultiplier', { multiplier: designPlan.multiplier }))
4052
4075
  }
4053
4076
  if (designPlan.complete === true) notes.push(t('designPlanComplete'))
4054
4077
  else if (designPlan.windows > 0) notes.push(t('designPlanPartial', { count: designPlan.windows }))
package/index.js CHANGED
@@ -38,6 +38,7 @@ import {
38
38
  saveProviderStore,
39
39
  } from './lib/provider-store.mjs'
40
40
  import { makeCustomAdapter, sanitizeUsageBaseURL, validateCustomProvider } from './lib/custom-provider.mjs'
41
+ import { capsForModels, designPlanCostUsd } from './lib/design-plan-cost.mjs'
41
42
  import {
42
43
  defaultDesignPlanLedgerFile,
43
44
  loadLedger,
@@ -83,7 +84,7 @@ export const inject = []
83
84
  export const PLUGIN_NAME = 'dsh-tokenlens'
84
85
 
85
86
  /** Plugin version; mirrors package.json (kept here so diagnostics need no fs read). */
86
- export const PLUGIN_VERSION = '1.5.2'
87
+ export const PLUGIN_VERSION = '1.6.0'
87
88
 
88
89
  /** Documented defaults; every field of the raw row config is optional. */
89
90
  const DEFAULTS = {
@@ -431,12 +432,64 @@ export function apply(ctx, rawConfig) {
431
432
  }
432
433
  }
433
434
 
434
- /** The period estimate for the open-design snapshot, or null. */
435
- function designPlanSummaryFor(providerId, designPlan) {
435
+ /**
436
+ * The period estimate for the open-design snapshot, or null.
437
+ *
438
+ * v1.6.0: the dollar figure is the documented LIST-PRICE value of the tokens
439
+ * this period consumed, not `usedCredits / creditsPerUsd`. The latter assumes
440
+ * a 1x burn rate while the documented multipliers range 0.25x–4x, which made
441
+ * the number incomparable with the published caps (2x too low for DeepSeek
442
+ * V4.1 Flash, 4x too high for GPT-6 Luna). The token usage comes from the
443
+ * same local session-log stats the rest of the dialog uses.
444
+ */
445
+ async function designPlanSummaryFor(providerId, designPlan) {
436
446
  if (canonicalProviderId(providerId) !== 'open-design') return null
437
447
  const periodStart = designPlan?.periodStart
438
448
  if (typeof periodStart !== 'string' || periodStart === '') return null
439
- return summarizeDesignPlan(designPlanLedger, periodStart)
449
+ const pricing = await designPlanPricing()
450
+ return summarizeDesignPlan(designPlanLedger, periodStart, pricing)
451
+ }
452
+
453
+ /**
454
+ * The documented-price context of the current billing period.
455
+ *
456
+ * Reads the per-model token buckets for `open-design` from the local stats
457
+ * (scoped to this period by the day histograms' own totals) and converts them
458
+ * with the documented list prices, then derives the caps those multipliers
459
+ * entitle the account to. Every failure degrades to `null`s, never to a throw:
460
+ * a pricing problem must not hide the credits/balance the Host already has.
461
+ */
462
+ async function designPlanPricing() {
463
+ if (statsService === null) return null
464
+ let stats = null
465
+ try {
466
+ stats = await statsService.get({ fresh: false })
467
+ } catch {
468
+ return null
469
+ }
470
+ // `last30Days` is the closest wire window to a billing period (this account's
471
+ // is 2026-11-02 → 12-02) and it carries the per-token-kind split the
472
+ // documented prices need.
473
+ const models = Object.create(null)
474
+ for (const entry of Array.isArray(stats?.models) ? stats.models : []) {
475
+ if (canonicalProviderId(entry?.providerId) !== 'open-design') continue
476
+ const modelId = typeof entry.modelId === 'string' ? entry.modelId : ''
477
+ if (modelId === '') continue
478
+ const bucket = models[modelId] ?? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }
479
+ bucket.input += typeof entry.last30Days?.input === 'number' ? entry.last30Days.input : 0
480
+ bucket.output += typeof entry.last30Days?.output === 'number' ? entry.last30Days.output : 0
481
+ bucket.cacheRead += typeof entry.last30Days?.cacheRead === 'number' ? entry.last30Days.cacheRead : 0
482
+ bucket.cacheWrite += typeof entry.last30Days?.cacheWrite === 'number' ? entry.last30Days.cacheWrite : 0
483
+ models[modelId] = bucket
484
+ }
485
+ const cost = designPlanCostUsd(models)
486
+ const caps = capsForModels(Object.keys(models))
487
+ return {
488
+ listPriceUsd: cost.priced === true ? cost.usd : null,
489
+ monthlyCapUsd: caps.monthlyCapUsd,
490
+ multiplier: caps.multiplier,
491
+ unpricedModels: cost.unpricedModels,
492
+ }
440
493
  }
441
494
 
442
495
  /**
@@ -1097,6 +1150,10 @@ export function apply(ctx, rawConfig) {
1097
1150
  provider = { ...provider, accessKeyId: accessValue, secretAccessKey: secretValue }
1098
1151
  keyInfo = { ref: accessRef, source: access.source, detail: access.detail }
1099
1152
  } else if (adapterId === 'open-design') {
1153
+ // v1.5.3: the workspace id is OPTIONAL. A Control Key identifies its own
1154
+ // active workspace, so the adapter discovers it when none is configured;
1155
+ // an explicit value (management page / config / env) always wins for
1156
+ // accounts with several workspaces.
1100
1157
  const workspaceId =
1101
1158
  String(provider.workspaceId ?? '').trim() || String(process.env.OPENDESIGN_WORKSPACE_ID ?? '').trim()
1102
1159
  const refs = []
@@ -1125,14 +1182,6 @@ export function apply(ctx, rawConfig) {
1125
1182
  break
1126
1183
  }
1127
1184
  }
1128
- if (workspaceId === '') {
1129
- throw new UsageError(
1130
- 'missing-key',
1131
- 'OpenDesign quota needs a workspace id: set it on the plugin management page (open-design → 工作区 ID), ' +
1132
- 'in providers.open-design.workspaceId, or in OPENDESIGN_WORKSPACE_ID',
1133
- { retryable: false },
1134
- )
1135
- }
1136
1185
  if (controlKey === '') {
1137
1186
  throw new UsageError(
1138
1187
  'missing-key',
@@ -1140,6 +1189,7 @@ export function apply(ctx, rawConfig) {
1140
1189
  { retryable: false },
1141
1190
  )
1142
1191
  }
1192
+ // An empty workspaceId is fine: the adapter resolves it from the key.
1143
1193
  provider = { ...provider, workspaceId, controlKey }
1144
1194
  keyInfo = { ref: controlRef, source: controlSource, detail: controlDetail }
1145
1195
  } else if (typeof provider.apiKeyRef === 'string' && provider.apiKeyRef.trim() !== '') {
@@ -1199,12 +1249,12 @@ export function apply(ctx, rawConfig) {
1199
1249
  return consoleUrl === null ? null : { console: consoleUrl }
1200
1250
  }
1201
1251
 
1202
- function snapshotFromResult(providerId, read, now) {
1252
+ async function snapshotFromResult(providerId, read, now) {
1203
1253
  // v1.5.0: the Design Plan period estimate. The ledger observes every read
1204
1254
  // (refreshes and background polls), then the current period is summarized
1205
1255
  // into the snapshot so the dialog can show it without a second round-trip.
1206
1256
  recordDesignPlan(read.result.designPlan ?? null, now)
1207
- const designPlan = designPlanSummaryFor(providerId, read.result.designPlan)
1257
+ const designPlan = await designPlanSummaryFor(providerId, read.result.designPlan)
1208
1258
  return buildSnapshot({
1209
1259
  provider: providerInfo(providerId),
1210
1260
  plan: read.result.plan ?? null,
@@ -1243,7 +1293,7 @@ export function apply(ctx, rawConfig) {
1243
1293
  state.lastAttempts.set(canonicalId, new Date().toISOString())
1244
1294
  try {
1245
1295
  const read = await readProvider(canonicalId, Date.now())
1246
- const snapshot = snapshotFromResult(canonicalId, read, Date.now())
1296
+ const snapshot = await snapshotFromResult(canonicalId, read, Date.now())
1247
1297
  state.caches.set(canonicalId, { snapshot, fetchedAtMs: read.fetchedAtMs })
1248
1298
  state.lastErrors.delete(canonicalId)
1249
1299
  state.requestCounts.set(canonicalId, (state.requestCounts.get(canonicalId) ?? 0) + 1)
@@ -0,0 +1,168 @@
1
+ /**
2
+ * Design Plan cost accounting (v1.6.0).
3
+ *
4
+ * WHY
5
+ *
6
+ * Open Design bills the Design Plan in "credits", never in dollars. The
7
+ * published pricing page (`open-design.ai/zh/model-credits/`) states the caps in
8
+ * LIST-PRICE dollars ($120/month, $30/7d, $10/5h for Go) and, crucially, that
9
+ * the credits a model BURNS are `list-price value × that model's multiplier`:
10
+ *
11
+ * DeepSeek V4.1 Flash 0.5x -> the whole $120 monthly cap is available
12
+ * MiMo V2.6 Flash 1x -> half of it ($60)
13
+ * Claude Haiku 5.5 2x -> $30
14
+ * GPT-6 Luna 4x -> $15
15
+ *
16
+ * So a dollar amount computed from credits alone (usedCredits / creditsPerUsd)
17
+ * is NOT comparable with the pricing table: it silently assumes the account
18
+ * burns credits at 1x, understating a 0.5x model by 2x and overstating a 4x
19
+ * model by 4x. The honest figure is the LIST-PRICE value of the tokens actually
20
+ * consumed, which is what this module computes:
21
+ *
22
+ * cost = Σ over models ( input×p_in + output×p_out + cacheRead×p_read ) / 1e6
23
+ *
24
+ * That number is directly comparable with the documented caps, and the cap that
25
+ * applies depends on the model mix — hence `monthlyCapFor` and
26
+ * `effectiveMonthlyCap`.
27
+ *
28
+ * Credits remain the authoritative *consumption* signal (the server owns the
29
+ * counters); prices only turn observed tokens into a dollar figure the user can
30
+ * check against the pricing page.
31
+ */
32
+
33
+ /**
34
+ * Documented per-million-token list prices (USD) and Design Plan multipliers.
35
+ *
36
+ * Transcribed from `open-design.ai/zh/model-credits/` (pricing table + the
37
+ * "Design Plan 模型与额度扣除倍率" table). The public
38
+ * `GET /api/v1/public/model-prices` endpoint serves the same input/output/
39
+ * cacheRead numbers; only `hunyuan-h4-preview` and `glm-5.3-flash-x` are absent
40
+ * from it, so their prices fall back to the endpoint at runtime when available.
41
+ *
42
+ * `multiplier` is what the DOCUMENTED table says; an unknown model gets `null`
43
+ * and is reported as unpriceable rather than assumed to be 1x.
44
+ */
45
+ export const DESIGN_PLAN_MODELS = {
46
+ 'deepseek-v4.1-flash': { multiplier: 0.5, input: 0.3, output: 1.2, cacheRead: 0.006, label: 'DeepSeek V4.1 Flash' },
47
+ 'deepseek-v4-flash': { multiplier: 1, input: 0.44, output: 1.32, cacheRead: 0.014, label: 'DeepSeek V4 Flash' },
48
+ 'deepseek-v4-flash-vision-exp': {
49
+ multiplier: 1,
50
+ input: 0.44,
51
+ output: 1.32,
52
+ cacheRead: 0.014,
53
+ label: 'DeepSeek V4 Flash Vision Exp',
54
+ },
55
+ 'deepseek-v4-pro': { multiplier: 1, input: 1.32, output: 3.96, cacheRead: 0.044, label: 'DeepSeek V4 Pro' },
56
+ 'glm-5.3-flash': { multiplier: 1, input: 0.075, output: 0.25, cacheRead: 0.015, label: 'GLM-5.3 Flash' },
57
+ 'glm-5.3-flash-x': { multiplier: 2, input: 0.37, output: 1.25, cacheRead: 0.075, label: 'GLM-5.3 Flash-X' },
58
+ 'kimi-k2.7-code': { multiplier: 1, input: 0.95, output: 4, cacheRead: 0.19, label: 'Kimi K2.7 Code' },
59
+ 'mimo-v2.6-flash': { multiplier: 1, input: 0.14, output: 0.28, cacheRead: 0.0028, label: 'MiMo V2.6 Flash' },
60
+ 'mimo-v2.6-pro': { multiplier: 1, input: 0.435, output: 0.87, cacheRead: 0.0036, label: 'MiMo V2.6 Pro' },
61
+ 'minimax-m3.1-flash': { multiplier: 1, input: 0.15, output: 0.6, cacheRead: 0.015, label: 'MiniMax M3.1 Flash' },
62
+ 'claude-haiku-5.5': { multiplier: 2, input: 0.1, output: 0.5, cacheRead: 0.01, label: 'Claude Haiku 5.5' },
63
+ 'gpt-6-luna': { multiplier: 4, input: 0.1, output: 0.5, cacheRead: 0.01, label: 'GPT-6 Luna' },
64
+ 'hunyuan-h4-preview': { multiplier: 1, input: null, output: null, cacheRead: null, label: 'Hunyuan H4 Preview' },
65
+ }
66
+
67
+ /** The documented list-price dollar caps of the Go tier (used only as a default). */
68
+ export const DESIGN_PLAN_CAPS_USD = { rolling: 10, weekly: 30, monthly: 120 }
69
+
70
+ /**
71
+ * The monthly list-price cap available to a model mix.
72
+ *
73
+ * The $120 monthly cap is entirely Design Plan value and is consumed at the
74
+ * model's multiplier, so using ONLY a 0.5x model (DeepSeek V4.1 Flash) unlocks
75
+ * the full $120 of list-price usage; a 1x model reaches $60; 2x reaches $30.
76
+ * With a mixed history the binding constraint is the WORST (highest) multiplier
77
+ * the account actually used, because every unit of that model spends its share
78
+ * faster. That is why this returns the max multiplier seen, not an average.
79
+ *
80
+ * @returns `{ monthlyCapUsd, weeklyCapUsd, fiveHourCapUsd, multiplier }` where
81
+ * `multiplier` is the max documented multiplier among `modelIds` (0 when none),
82
+ * or all-null caps when no model is known.
83
+ */
84
+ export function capsForModels(modelIds) {
85
+ let worst = 0
86
+ for (const id of Array.isArray(modelIds) ? modelIds : []) {
87
+ const model = DESIGN_PLAN_MODELS[normalizeModelId(id)]
88
+ if (model === undefined || !Number.isFinite(model.multiplier) || model.multiplier <= 0) continue
89
+ if (model.multiplier > worst) worst = model.multiplier
90
+ }
91
+ if (worst === 0) {
92
+ return { monthlyCapUsd: null, weeklyCapUsd: null, fiveHourCapUsd: null, multiplier: null }
93
+ }
94
+ return {
95
+ // 600,000 credits / mult at 10,000 credits per dollar == $60 / mult.
96
+ monthlyCapUsd: 60 / worst,
97
+ weeklyCapUsd: 15 / worst,
98
+ fiveHourCapUsd: 5 / worst,
99
+ multiplier: worst,
100
+ }
101
+ }
102
+
103
+ /** A model id as the documented table spells it (lower-case, trimmed). */
104
+ export function normalizeModelId(value) {
105
+ return typeof value === 'string' ? value.trim().toLowerCase() : ''
106
+ }
107
+
108
+ /** The documented entry for a model id, or null. */
109
+ export function designPlanModel(modelId) {
110
+ return DESIGN_PLAN_MODELS[normalizeModelId(modelId)] ?? null
111
+ }
112
+
113
+ /** A token count as a finite non-negative number. */
114
+ function tokens(value) {
115
+ return typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : 0
116
+ }
117
+
118
+ /**
119
+ * The documented list-price cost of one bucket of tokens for one model.
120
+ *
121
+ * @returns `{ usd, priced }`; `priced` is false when the model has no documented
122
+ * price (the cost is then 0 for that model, and callers must say so rather
123
+ * than pretend the total is complete).
124
+ */
125
+ export function modelCostUsd(modelId, bucket) {
126
+ const model = designPlanModel(modelId)
127
+ if (model === null) return { usd: 0, priced: false }
128
+ const input = tokens(bucket?.input)
129
+ const output = tokens(bucket?.output)
130
+ const cacheRead = tokens(bucket?.cacheRead)
131
+ const cacheWrite = tokens(bucket?.cacheWrite)
132
+ // cacheWrite has no documented Design Plan price; bill it as fresh input so a
133
+ // value is never silently dropped, and flag it so the caller can explain.
134
+ const priced =
135
+ Number.isFinite(model.input) && Number.isFinite(model.output) && Number.isFinite(model.cacheRead)
136
+ if (!priced) return { usd: 0, priced: true, unavailability: 'the model has no documented Design Plan price' }
137
+ const usd =
138
+ (input / 1e6) * model.input +
139
+ (output / 1e6) * model.output +
140
+ (cacheRead / 1e6) * model.cacheRead +
141
+ (cacheWrite / 1e6) * model.input
142
+ return { usd, priced: true }
143
+ }
144
+
145
+ /**
146
+ * Total documented list-price cost of a per-model token map.
147
+ *
148
+ * @param models - `{ [modelId]: { input, output, cacheRead, cacheWrite } }`
149
+ * @returns `{ usd, priced, unpricedModels }` where `unpricedModels` lists the
150
+ * ids that had tokens but no documented price (their cost is not included).
151
+ */
152
+ export function designPlanCostUsd(models) {
153
+ let usd = 0
154
+ let priced = false
155
+ const unpricedModels = []
156
+ for (const [modelId, bucket] of Object.entries(models ?? {})) {
157
+ const total = tokens(bucket?.input) + tokens(bucket?.output) + tokens(bucket?.cacheRead) + tokens(bucket?.cacheWrite)
158
+ if (total === 0) continue
159
+ const cost = modelCostUsd(modelId, bucket)
160
+ if (cost.priced !== true) {
161
+ unpricedModels.push(modelId)
162
+ continue
163
+ }
164
+ usd += cost.usd
165
+ priced = true
166
+ }
167
+ return { usd, priced, unpricedModels }
168
+ }
@@ -234,17 +234,25 @@ export const WEEKS_PER_PERIOD = 4
234
234
  /**
235
235
  * Summarize one period: the estimated Design Plan usage the Host has observed.
236
236
  *
237
+ * @param ledger - the ledger document.
238
+ * @param periodStart - the billing period key.
239
+ * @param input - optional documented-price context (v1.6.0):
240
+ * `{ listPriceUsd, monthlyCapUsd, multiplier }`. `listPriceUsd` replaces the
241
+ * credits÷rate conversion as `estimatedUsd`; it is the documented list-price
242
+ * value of the tokens actually consumed, which is the figure comparable with
243
+ * the published caps.
237
244
  * @returns `null` when the period has no observations at all, else
238
- * `{ usedCredits, estimatedUsd, windows, complete, since, periodStart,
239
- * periodEnd, limitPerWindow, periodLimitCredits, percent }`.
240
- * `percent` is `usedCredits / periodLimitCredits`, where the period limit is
241
- * `limitPerWindow * WEEKS_PER_PERIOD` (the published monthly cap); it is null
242
- * when the upstream reported no usable weekly limit. `complete` is true only
243
- * when the earliest observed window starts at the period start — otherwise
244
- * earlier windows of this period were never seen and both numbers are lower
245
- * bounds.
245
+ * `{ usedCredits, estimatedUsd, creditsUsd, monthlyCapUsd, multiplier,
246
+ * capPercent, windows, complete, since, periodStart, periodEnd,
247
+ * limitPerWindow, periodLimitCredits, percent }`.
248
+ * `percent` is `usedCredits / periodLimitCredits` (observed credits against
249
+ * the account's own windows); `capPercent` is `estimatedUsd / monthlyCapUsd`
250
+ * (documented dollars against the documented cap). The two differ per model
251
+ * mix and are both reported. `complete` is true only when the earliest
252
+ * observed window starts at the period start — otherwise earlier windows of
253
+ * this period were never seen and the numbers are lower bounds.
246
254
  */
247
- export function summarizeDesignPlan(ledger, periodStart) {
255
+ export function summarizeDesignPlan(ledger, periodStart, input = null) {
248
256
  const key = isoOf(periodStart)
249
257
  if (key === '') return null
250
258
  const period = ledger?.periods?.[key]
@@ -258,18 +266,32 @@ export function summarizeDesignPlan(ledger, periodStart) {
258
266
  windows += 1
259
267
  }
260
268
  const creditsPerUsd = period.creditsPerUsd
261
- const estimatedUsd =
269
+ // v1.6.0: `creditsPerUsd` conversion is kept ONLY as a fallback. It assumes the
270
+ // account burns credits at 1x, but the documented multipliers range 0.25x–4x,
271
+ // so it is wrong for any non-1x model (2x too low for DeepSeek V4.1 Flash).
272
+ // The caller passes the documented list-price cost instead.
273
+ const creditsUsd =
262
274
  Number.isSafeInteger(creditsPerUsd) && creditsPerUsd > 0 ? usedCredits / creditsPerUsd : null
275
+ const estimatedUsd = Number.isFinite(input?.listPriceUsd) ? input.listPriceUsd : creditsUsd
263
276
  const since = entries[0][0]
264
277
  const limitPerWindow = entries[entries.length - 1][1].limitCredits
265
278
  const usableWindowLimit = Number.isSafeInteger(limitPerWindow) && limitPerWindow > 0 ? limitPerWindow : null
266
279
  const periodLimitCredits = usableWindowLimit === null ? null : usableWindowLimit * WEEKS_PER_PERIOD
267
280
  const percent = periodLimitCredits === null ? null : (usedCredits / periodLimitCredits) * 100
281
+ const monthlyCapUsd = Number.isFinite(input?.monthlyCapUsd) ? input.monthlyCapUsd : null
282
+ const capPercent =
283
+ estimatedUsd !== null && monthlyCapUsd !== null && monthlyCapUsd > 0
284
+ ? Math.min(100, Math.max(0, (estimatedUsd / monthlyCapUsd) * 100))
285
+ : null
268
286
  return {
269
287
  periodStart: key,
270
288
  periodEnd: period.periodEnd || '',
271
289
  usedCredits,
272
290
  estimatedUsd,
291
+ creditsUsd,
292
+ monthlyCapUsd,
293
+ multiplier: Number.isFinite(input?.multiplier) ? input.multiplier : null,
294
+ capPercent,
273
295
  windows,
274
296
  complete: since === key,
275
297
  since,
package/lib/providers.mjs CHANGED
@@ -783,6 +783,82 @@ const OPEN_DESIGN_WINDOW_SECONDS = {
783
783
  2592000: 'monthly',
784
784
  }
785
785
 
786
+ /**
787
+ * Find the workspace a Control Key belongs to.
788
+ *
789
+ * `GET /api/v1/workspaces/current` answers with the key's active workspace
790
+ * (`workspaceId` / `activeWorkspaceId`), and `GET /api/v1/workspaces` lists every
791
+ * workspace the key can see. A Control Key is workspace-scoped in practice (this
792
+ * plugin talks to one billing account at a time), so the active workspace is the
793
+ * right default; the list is only a fallback for an account without an "active"
794
+ * one, and a single-entry list is unambiguous.
795
+ *
796
+ * @returns `{ workspaceId, name, source, error, retryable }`; `workspaceId` is ''
797
+ * when the workspace could not be determined (the caller turns that into a
798
+ * `missing-key` error carrying `error`).
799
+ */
800
+ export async function discoverOpenDesignWorkspace(baseURL, controlKey, httpJson) {
801
+ const headers = { accept: 'application/json', authorization: `Bearer ${controlKey}` }
802
+ const failed = (error, retryable = false) => ({ workspaceId: '', name: '', source: '', error, retryable })
803
+ try {
804
+ const current = await httpJson(joinUrl(baseURL, '/api/v1/workspaces/current'), { method: 'GET', headers })
805
+ const parsed = parseOpenDesignWorkspace(current)
806
+ if (parsed.workspaceId !== '') {
807
+ return { workspaceId: parsed.workspaceId, name: parsed.name, source: 'current', error: '', retryable: false }
808
+ }
809
+ } catch (error) {
810
+ // A 404/401 here is not fatal: the list endpoint may still answer.
811
+ if (error instanceof UsageError && error.code === 'network') {
812
+ return failed(String(error.message ?? 'network error'), true)
813
+ }
814
+ }
815
+ try {
816
+ const list = await httpJson(joinUrl(baseURL, '/api/v1/workspaces'), { method: 'GET', headers })
817
+ const items = Array.isArray(list?.items) ? list.items : []
818
+ const usable = items
819
+ .filter((item) => item !== null && typeof item === 'object' && typeof item.workspaceId === 'string')
820
+ .filter((item) => item.workspaceId.trim() !== '')
821
+ if (usable.length === 1) {
822
+ const only = usable[0]
823
+ return {
824
+ workspaceId: only.workspaceId.trim(),
825
+ name: typeof only.workspaceName === 'string' ? only.workspaceName : '',
826
+ source: 'list',
827
+ error: '',
828
+ retryable: false,
829
+ }
830
+ }
831
+ if (usable.length > 1) {
832
+ // Never guess between several workspaces: picking the wrong one would
833
+ // report another team's usage as this account's.
834
+ return failed(`${usable.length} workspaces are visible and none is marked active`, false)
835
+ }
836
+ } catch (error) {
837
+ return failed(String(error?.message ?? 'the workspace list request failed'), true)
838
+ }
839
+ return failed('no workspace is available to this Control Key', false)
840
+ }
841
+
842
+ /**
843
+ * Read a workspace id from a `/workspaces/current` or `/workspaces` document.
844
+ *
845
+ * @returns `{ workspaceId, name }` where `workspaceId` is '' when absent.
846
+ */
847
+ export function parseOpenDesignWorkspace(payload) {
848
+ const document = payload !== null && typeof payload === 'object' && !Array.isArray(payload) ? payload : null
849
+ if (document === null) return { workspaceId: '', name: '' }
850
+ for (const key of ['workspaceId', 'activeWorkspaceId']) {
851
+ const value = document[key]
852
+ if (typeof value === 'string' && value.trim() !== '') {
853
+ return {
854
+ workspaceId: value.trim(),
855
+ name: typeof document.workspaceName === 'string' ? document.workspaceName.trim() : '',
856
+ }
857
+ }
858
+ }
859
+ return { workspaceId: '', name: '' }
860
+ }
861
+
786
862
  /** A non-negative integer credit amount serialized as a decimal string. */
787
863
  function openDesignCredits(value) {
788
864
  return typeof value === 'string' && /^\d{1,40}$/.test(value) ? BigInt(value) : null
@@ -966,20 +1042,31 @@ export const ADAPTERS = {
966
1042
  kind: 'windows',
967
1043
  async read({ providerId = 'open-design', provider, httpJson, now }) {
968
1044
  const config = providerConfig(providerId, provider)
969
- const workspaceId = String(config.workspaceId ?? '').trim()
970
- if (workspaceId === '') {
971
- throw new UsageError(
972
- 'missing-key',
973
- 'OpenDesign quota needs a workspace id (providers.open-design.workspaceId or OPENDESIGN_WORKSPACE_ID)',
974
- { retryable: false },
975
- )
976
- }
977
1045
  const controlKey = typeof provider.controlKey === 'string' ? provider.controlKey : ''
978
1046
  if (controlKey === '') {
979
1047
  throw new UsageError('missing-key', 'OpenDesign quota needs a Control Key (OPENDESIGN_CONTROL_KEY or VELA_CONTROL_KEY)', {
980
1048
  retryable: false,
981
1049
  })
982
1050
  }
1051
+ // v1.5.3: the workspace id is DERIVABLE from a Control Key, so a configured
1052
+ // id is only a preference. `GET /api/v1/workspaces/current` answers with the
1053
+ // key's active workspace, which removes the second setup field entirely.
1054
+ let workspaceId = String(config.workspaceId ?? '').trim()
1055
+ if (workspaceId === '') {
1056
+ const adopted = await discoverOpenDesignWorkspace(config.baseURL, controlKey, httpJson)
1057
+ workspaceId = adopted.workspaceId
1058
+ if (workspaceId === '') {
1059
+ throw new UsageError(
1060
+ 'missing-key',
1061
+ 'OpenDesign could not determine the workspace for this Control Key: ' +
1062
+ (adopted.error === ''
1063
+ ? 'the key is not valid for any workspace'
1064
+ : `the discovery request failed (${adopted.error})`) +
1065
+ '. Set 工作区 ID on the plugin management page only if your account has several workspaces.',
1066
+ { retryable: adopted.retryable },
1067
+ )
1068
+ }
1069
+ }
983
1070
  const workspaceBase = joinUrl(config.baseURL, `/api/v1/workspaces/${encodeURIComponent(workspaceId)}`)
984
1071
  const headers = {
985
1072
  accept: 'application/json',
@@ -665,7 +665,16 @@ export function createUsageStats(input) {
665
665
  modelId: model.model,
666
666
  today: { total: todayModel.total, messages: todayModel.messages },
667
667
  last7Days: { total: model.last7Days.total, messages: model.last7Days.messages },
668
- last30Days: { total: model.last30Days.total, messages: model.last30Days.messages },
668
+ // v1.6.0: the input/output/cache split is included so a caller can price
669
+ // the tokens per model (the documented prices differ per token kind).
670
+ last30Days: {
671
+ total: model.last30Days.total,
672
+ messages: model.last30Days.messages,
673
+ input: model.last30Days.input,
674
+ output: model.last30Days.output,
675
+ cacheRead: model.last30Days.cacheRead,
676
+ cacheWrite: model.last30Days.cacheWrite,
677
+ },
669
678
  allTime: { total: model.total.total, messages: model.total.messages },
670
679
  sinceEffective: { total: model.total.total, messages: model.total.messages },
671
680
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-tokenlens",
3
- "version": "1.5.2",
3
+ "version": "1.6.0",
4
4
  "type": "module",
5
5
  "description": "dsh-TokenLens (词元透镜) — provider-aware plan capacity plus local token statistics (today/7d/30d/all-time, by harness/provider/model/workspace) for DSH.",
6
6
  "license": "MIT",