dsh-tokenlens 1.5.3 → 1.6.2

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,27 @@
4
4
 
5
5
  **版本记录约定**:同一发布日只保留一个章节,标题取当日**最后一个**已发布版本号;当日多次发布的改动合并到该章节,并在标题下注明当日实际发布过的版本号(npm 上仍可安装这些独立版本)。若当日已有发布,后续小改动不再单独增加版本号,而是并入下一篇日志。这样日志按天规整、不出现同日多个小版本。
6
6
 
7
+ ## 1.6.2 — 2026-11-04
8
+
9
+ (当日已发布版本:v1.6.2。v1.6.1 的混合模型修复未单独发版,已并入本节)
10
+
11
+ - **fix(open-design): 混合模型按 credits 加权取额度,不再取最高倍率**——原实现取「用过的最高倍率」当口径,实测 0.5× 与 4× 各占一半 credits 时会把月额度报成 $15(正确应为 **$67.50**),严重低估。现改为按实际消耗加权的调和平均:`m_eff = Σcredits / Σ(credits/m)`,`月额度 = $60 / m_eff`。90% credits 走 0.5× 时是 **$97.89**,99% 时 **$117.92**,偶尔用一次 4× 模型不再摧毁额度
12
+ - fix(open-design): 单一模型时加权公式**精确退化**为官方表格值(0.5×→$120、1×→$60、2×→$30、4×→$15),纯模型场景不受影响
13
+ - fix(open-design): 无消费权重(仅知道模型列表)时回退到最高倍率,即保守口径(承诺最少剩余额度)
14
+ - **fix(open-design): 未收录价格的模型不再静默少算金额**——`unpricedModels` 此前算出来却没有用上,含未知模型时金额会**偏低却不提示**。现在置 `partial: true`,弹窗显示「含未收录价格的模型,金额为下限」,绝不把下限当完整值
15
+ - 测试:328 → 332(切换模型时金额与额度反向变化、未收录模型使金额成为下限、加权混合三种比例、单一模型退化、无权重回退最高倍率)
16
+ - 备注:`600,000 credits/月` 是插件由「7 天窗口上限 × 4」推算的值(官方定价表月/周 = $120/$30 = 4),**不是服务端字段**;服务端只返回 5 小时 50,000 与 7 天 150,000 两个窗口
17
+
18
+ ## 1.6.0 — 2026-11-03
19
+
20
+ - **feat(open-design): 「本期 Design Plan 累计」改按官方定价表的模型单价计算**——原来是 `usedCredits ÷ creditsPerUsd`,这个算法隐含「按 1× 倍率消耗」的假设,而官方倍率表跨度 0.25×–4×:对 DeepSeek V4.1 Flash(0.5×)会**少算一半**,对 GPT-6 Luna(4×)会**多算 4 倍**。现在按每个模型的实际 Token × 官方单价(输入/输出/缓存读分别计价)累加,结果可直接与定价表比对
21
+ - **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× 倍率模型计价」
22
+ - feat(open-design): 混合模型取额度口径(v1.6.2 修正为按 credits 加权,不再是最高倍率)
23
+ - feat(wire): `designPlan` 新增 `monthlyCapUsd` / `capPercent` / `multiplier` / `creditsUsd`;`estimatedUsd` 语义改为「官方单价口径的消费额」,旧算法结果保留在 `creditsUsd` 便于对照
24
+ - feat(stats): `models[].last30Days` 增加 `input` / `output` / `cacheRead` / `cacheWrite` 拆分,供按单价计价使用(此前只有 total)
25
+ - fix(open-design): 单价表按官方文档转录 13 个 Design Plan 模型;`hunyuan-h4-preview` / `glm-5.3-flash-x` 不在公开价目接口里,价格缺失时按「未定价」处理并如实上报,绝不臆造
26
+ - 测试:318 → 328(倍率阶梯 0.5×→$120 / 1×→$60 / 2×→$30 / 4×→$15、未知模型不臆造倍率与额度、按 token 种类计价、**用控制台真实账单核对:$0.0048 列表价 × 0.5 = $0.0024 实扣**、未定价模型上报)
27
+
7
28
  ## 1.5.3 — 2026-11-03
8
29
 
9
30
  - **feat(open-design): 初次配置只需一个 Control Key**——工作区 ID 改为**可选**。Control Key 本身就能确定它属于哪个工作区,插件改用 `GET /api/v1/workspaces/current` 自动识别(回退 `GET /api/v1/workspaces`,仅当可见工作区恰好一个时采纳)
package/PROTOCOL.md CHANGED
@@ -161,24 +161,63 @@ by observing the weekly windows it sees:
161
161
  "designPlan": {
162
162
  "periodStart": "2026-11-02T09:53:18.000Z", // billing period (billing/summary)
163
163
  "periodEnd": "2026-12-02T09:53:18.000Z",
164
- "usedCredits": 50025, // Σ final+current weekly windows
165
- "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
166
170
  "windows": 1, // weekly windows observed
167
171
  "complete": true, // earliest window starts at periodStart
168
172
  "since": "2026-11-02T09:53:18.000Z", // first window actually observed
169
173
  "limitPerWindow": 150000, // the account's real weekly cap
170
174
  "periodLimitCredits": 600000, // limitPerWindow * 4
171
- "percent": 8.80 // usedCredits / periodLimitCredits
175
+ "percent": 23.68 // usedCredits / periodLimitCredits
172
176
  }
173
177
  ```
174
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
+
175
214
  `windows.monthly` is then filled from that estimate (v1.5.1), with `resetsAt` taken
176
215
  from `periodEnd`, so the existing monthly row and the chip's `月 N%` piece carry it
177
- without a second UI. The ×4 monthly-to-weekly ratio is the **one documented
178
- derivation**: the published pricing table states the monthly cap as exactly four
179
- weekly windows for every tier (Go $120/$30, Plus $160/$40, Pro $360/$90, Max
180
- $600/$150). A partial estimate still reports a percentage but sets the window
181
- `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.
182
221
 
183
222
  Rules the Host guarantees:
184
223
 
package/README.md CHANGED
@@ -327,7 +327,8 @@ Control Key 是 **Open Design 桌面端/CLI 登录时写入本机的会话凭据
327
327
  - OpenCode 的额度定义见[官方文档](https://opencode.ai/docs/go/#usage-limits)(5h = 月度额度的 20%、周 = 50%、月 = 100%);接口只返回百分比,插件也只画百分比。
328
328
  - Command Code 的 `/alpha/*` 是官方 CLI 自用的私有计费路由;免费 / 无方案账户返回空窗口(显示 `—`),插件不会用方案目录猜额度。
329
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.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 个窗口」);覆盖不完整时百分比仍显示但**不会触发告警配色**,插件没运行期间错过的窗口无法计入。
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`:**318 个自动化用例**,覆盖 Host 数据层、供应商适配器、统计折叠、各 harness 读取、Design Plan 周期账本、窗口锚点切片、工作区自动识别、插件集成与浏览器端渲染 / 交互。
443
+ - `node --test`:**332 个自动化用例**,覆盖 Host 数据层、供应商适配器、统计折叠、各 harness 读取、Design Plan 周期账本与官方单价计价、窗口锚点切片、工作区自动识别、插件集成与浏览器端渲染 / 交互。
443
444
  - 在真实环境中验证过 **5 个 harness** 的数据源;本机统计与独立的 SQLite 查询、独立的日志解析逐项对账(差额为 0),跨 harness 合并与供应商映射算术自洽。
444
445
  - 供应商接口按各官方文档与参考客户端行为验证;未配置凭据时 chip 静默隐藏,接口异常时按可重试错误降级。
445
446
 
package/client.js CHANGED
@@ -341,8 +341,9 @@ 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
+ designPlanPartialPricing: '含未收录价格的模型,金额为下限',
346
347
  designPlanComplete: '已覆盖本账期全部窗口',
347
348
  designPlanPartial: '仅统计已观测的 {count} 个窗口',
348
349
  providerSave: '保存',
@@ -601,10 +602,11 @@ window.__ModuleLoader__.load({
601
602
  providerControlKeySaved: 'Saved; paste again to replace it',
602
603
  configureProvider: 'Configure',
603
604
  errorOpenDesignConfig: 'Open Design quota needs a workspace ID and a Control Key (not an inference key)',
604
- designPlanLabel: 'Period Design Plan (estimate)',
605
+ designPlanLabel: 'Period Design Plan',
605
606
  designPlanHint:
606
- '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.',
607
- designPlanWindowsOf: '≈ {count} weekly allowances',
607
+ '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.',
608
+ designPlanMultiplier: 'priced at the {multiplier}× model rate',
609
+ designPlanPartialPricing: 'includes models with no documented price; the amount is a lower bound',
608
610
  designPlanComplete: 'covers every window of this period',
609
611
  designPlanPartial: 'only {count} observed window(s)',
610
612
  providerConsoleURLPlaceholder: 'Leave empty for the built-in default',
@@ -1090,6 +1092,11 @@ window.__ModuleLoader__.load({
1090
1092
  const windows = Number.isFinite(raw.windows) && raw.windows > 0 ? Math.round(raw.windows) : 0
1091
1093
  const estimatedUsd = numberOrNull(raw.estimatedUsd)
1092
1094
  const limitPerWindow = numberOrNull(raw.limitPerWindow)
1095
+ // v1.6.0 documented-price context: the cap the model mix entitles and the
1096
+ // share of it consumed. Both are optional (an older Host omits them).
1097
+ const monthlyCapUsd = numberOrNull(raw.monthlyCapUsd)
1098
+ const capPercent = numberOrNull(raw.capPercent)
1099
+ const multiplier = numberOrNull(raw.multiplier)
1093
1100
  return {
1094
1101
  periodStart: stringOrEmpty(raw.periodStart),
1095
1102
  periodEnd: stringOrEmpty(raw.periodEnd),
@@ -1099,6 +1106,12 @@ window.__ModuleLoader__.load({
1099
1106
  complete: raw.complete === true,
1100
1107
  since: stringOrEmpty(raw.since),
1101
1108
  limitPerWindow: limitPerWindow !== null && limitPerWindow > 0 ? limitPerWindow : null,
1109
+ monthlyCapUsd: monthlyCapUsd !== null && monthlyCapUsd > 0 ? monthlyCapUsd : null,
1110
+ capPercent,
1111
+ multiplier: multiplier !== null && multiplier > 0 ? multiplier : null,
1112
+ // v1.6.2: some consumed model had no documented price, so the dollar
1113
+ // figure is a lower bound and must not read as complete.
1114
+ partial: raw.partial === true,
1102
1115
  }
1103
1116
  }
1104
1117
 
@@ -4018,25 +4031,38 @@ window.__ModuleLoader__.load({
4018
4031
  }
4019
4032
 
4020
4033
  /**
4021
- * The open-design period estimate row (v1.5.0).
4034
+ * The open-design period estimate row (v1.5.0, priced in v1.6.0).
4022
4035
  *
4023
4036
  * Open Design has no period-usage endpoint, so this is the Host's local
4024
- * accumulation of the weekly windows it observed. It is labelled as an
4025
- * estimate and carries a coverage note: when the first observed window does
4026
- * not start at the billing period start, earlier windows were never seen and
4027
- * the value is a lower bound.
4037
+ * accumulation of the observed windows. The dollar figure is the DOCUMENTED
4038
+ * LIST-PRICE value of the tokens consumed this period, which makes it
4039
+ * comparable with the published caps — that is what "can I use $120?" turns
4040
+ * on. The denominator is the cap those model multipliers entitle the account
4041
+ * to (0.5x models unlock the full $120; 1x unlocks $60).
4028
4042
  */
4029
4043
  function buildDesignPlanRow(designPlan, t) {
4030
4044
  if (designPlan === null) return null
4031
4045
  const parts = []
4032
- if (designPlan.estimatedUsd !== null) {
4046
+ const hasUsd = designPlan.estimatedUsd !== null
4047
+ if (hasUsd) {
4048
+ // "$27.31 / $120" — the numerator is documented list-price usage, the
4049
+ // denominator the documented cap for this model mix.
4050
+ const value =
4051
+ designPlan.monthlyCapUsd !== null
4052
+ ? `${formatMoney(designPlan.estimatedUsd, 'USD')} / ${formatMoney(designPlan.monthlyCapUsd, 'USD')}`
4053
+ : formatMoney(designPlan.estimatedUsd, 'USD')
4033
4054
  parts.push(
4034
- h(
4035
- 'span',
4036
- { key: 'usd', className: 'dsh-tokenlens-percent dsh-tokenlens-capacity-money' },
4037
- formatMoney(designPlan.estimatedUsd, 'USD'),
4038
- ),
4055
+ h('span', { key: 'usd', className: 'dsh-tokenlens-percent dsh-tokenlens-capacity-money' }, value),
4039
4056
  )
4057
+ if (designPlan.capPercent !== null) {
4058
+ parts.push(
4059
+ h(
4060
+ 'span',
4061
+ { key: 'cap', className: 'dsh-tokenlens-percent', 'data-level': levelOf(designPlan.capPercent, 'ok') },
4062
+ formatPercent(designPlan.capPercent, t),
4063
+ ),
4064
+ )
4065
+ }
4040
4066
  } else {
4041
4067
  parts.push(
4042
4068
  h(
@@ -4047,10 +4073,14 @@ window.__ModuleLoader__.load({
4047
4073
  )
4048
4074
  }
4049
4075
  const notes = []
4050
- if (designPlan.limitPerWindow !== null && designPlan.usedCredits > 0) {
4051
- const ratio = designPlan.usedCredits / designPlan.limitPerWindow
4052
- notes.push(t('designPlanWindowsOf', { count: ratio.toFixed(1) }))
4053
- }
4076
+ // Say WHICH multiplier set the cap, since that is why the ceiling is not
4077
+ // always $120 (the pricing table: 0.5x→$120, 1x→$60, 2x→$30, 4x→$15).
4078
+ if (designPlan.multiplier !== null) {
4079
+ notes.push(t('designPlanMultiplier', { multiplier: designPlan.multiplier }))
4080
+ }
4081
+ // v1.6.2: a model without a documented price makes the figure a lower bound.
4082
+ // Say it rather than letting the number read as complete.
4083
+ if (designPlan.partial === true) notes.push(t('designPlanPartialPricing'))
4054
4084
  if (designPlan.complete === true) notes.push(t('designPlanComplete'))
4055
4085
  else if (designPlan.windows > 0) notes.push(t('designPlanPartial', { count: designPlan.windows }))
4056
4086
  return h(
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.3'
87
+ export const PLUGIN_VERSION = '1.6.2'
87
88
 
88
89
  /** Documented defaults; every field of the raw row config is optional. */
89
90
  const DEFAULTS = {
@@ -431,12 +432,68 @@ 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
+ // v1.6.1: weight the cap by actual per-model consumption, not the worst
487
+ // multiplier. The caps are credit-denominated, so a mostly-0.5x account that
488
+ // touched a 4x model once keeps nearly the full $120.
489
+ const caps = capsForModels(Object.keys(models), cost.mixes)
490
+ return {
491
+ listPriceUsd: cost.priced === true ? cost.usd : null,
492
+ monthlyCapUsd: caps.monthlyCapUsd,
493
+ multiplier: caps.multiplier,
494
+ unpricedModels: cost.unpricedModels,
495
+ partial: cost.unpricedModels.length > 0,
496
+ }
440
497
  }
441
498
 
442
499
  /**
@@ -1196,12 +1253,12 @@ export function apply(ctx, rawConfig) {
1196
1253
  return consoleUrl === null ? null : { console: consoleUrl }
1197
1254
  }
1198
1255
 
1199
- function snapshotFromResult(providerId, read, now) {
1256
+ async function snapshotFromResult(providerId, read, now) {
1200
1257
  // v1.5.0: the Design Plan period estimate. The ledger observes every read
1201
1258
  // (refreshes and background polls), then the current period is summarized
1202
1259
  // into the snapshot so the dialog can show it without a second round-trip.
1203
1260
  recordDesignPlan(read.result.designPlan ?? null, now)
1204
- const designPlan = designPlanSummaryFor(providerId, read.result.designPlan)
1261
+ const designPlan = await designPlanSummaryFor(providerId, read.result.designPlan)
1205
1262
  return buildSnapshot({
1206
1263
  provider: providerInfo(providerId),
1207
1264
  plan: read.result.plan ?? null,
@@ -1240,7 +1297,7 @@ export function apply(ctx, rawConfig) {
1240
1297
  state.lastAttempts.set(canonicalId, new Date().toISOString())
1241
1298
  try {
1242
1299
  const read = await readProvider(canonicalId, Date.now())
1243
- const snapshot = snapshotFromResult(canonicalId, read, Date.now())
1300
+ const snapshot = await snapshotFromResult(canonicalId, read, Date.now())
1244
1301
  state.caches.set(canonicalId, { snapshot, fetchedAtMs: read.fetchedAtMs })
1245
1302
  state.lastErrors.delete(canonicalId)
1246
1303
  state.requestCounts.set(canonicalId, (state.requestCounts.get(canonicalId) ?? 0) + 1)
@@ -0,0 +1,217 @@
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 NOT simply the highest
77
+ * multiplier: the caps are denominated in credits (600,000/month at 10,000
78
+ * credits per list-price dollar), and each model burns those credits at its own
79
+ * rate. The account's spendable list-price value is therefore
80
+ *
81
+ * 600,000 credits / m_effective
82
+ *
83
+ * where `m_effective` is the credit-weighted harmonic mean of the multipliers
84
+ * actually used:
85
+ *
86
+ * m_eff = Σ credits_i / Σ (credits_i / m_i), credits_i = listUsd_i × 10000 × m_i
87
+ *
88
+ * Using the plain maximum would understate a mostly-0.5x account that touched a
89
+ * 4x model once: 90% of the credits at 0.5x plus 10% at 4x gives $97.89, not
90
+ * $15. With a single model the formula reduces exactly to that model's
91
+ * documented cap, so the pure cases still match the pricing page.
92
+ *
93
+ * @param modelIds - the model ids in the mix, or
94
+ * @param mixes - a per-model `{ listUsd, multiplier }` split (preferred: it
95
+ * weights by real consumption). `modelIds` is used only when no split is given.
96
+ * @returns `{ monthlyCapUsd, weeklyCapUsd, fiveHourCapUsd, multiplier }` where
97
+ * `multiplier` is the effective multiplier, or all-null caps when no model is
98
+ * known.
99
+ */
100
+ export function capsForModels(modelIds, mixes = null) {
101
+ const rows = Array.isArray(mixes)
102
+ ? mixes
103
+ .map((mix) => ({
104
+ listUsd: typeof mix?.listUsd === 'number' && Number.isFinite(mix.listUsd) && mix.listUsd > 0 ? mix.listUsd : 0,
105
+ multiplier:
106
+ typeof mix?.multiplier === 'number' && Number.isFinite(mix.multiplier) && mix.multiplier > 0
107
+ ? mix.multiplier
108
+ : null,
109
+ }))
110
+ .filter((row) => row.multiplier !== null && row.listUsd > 0)
111
+ : null
112
+ let effective = 0
113
+ if (rows !== null && rows.length > 0) {
114
+ // Credit-weighted harmonic mean (see the doc comment).
115
+ let credits = 0
116
+ let inverse = 0
117
+ for (const row of rows) {
118
+ const rowCredits = row.listUsd * row.multiplier
119
+ credits += rowCredits
120
+ inverse += rowCredits / row.multiplier
121
+ }
122
+ effective = credits > 0 && inverse > 0 ? credits / inverse : 0
123
+ } else {
124
+ // No consumption weighting available: fall back to the highest multiplier,
125
+ // the conservative reading (it promises the LEAST remaining capacity).
126
+ let worst = 0
127
+ for (const id of Array.isArray(modelIds) ? modelIds : []) {
128
+ const model = DESIGN_PLAN_MODELS[normalizeModelId(id)]
129
+ if (model === undefined || !Number.isFinite(model.multiplier) || model.multiplier <= 0) continue
130
+ if (model.multiplier > worst) worst = model.multiplier
131
+ }
132
+ effective = worst
133
+ }
134
+ if (!(effective > 0)) {
135
+ return { monthlyCapUsd: null, weeklyCapUsd: null, fiveHourCapUsd: null, multiplier: null }
136
+ }
137
+ return {
138
+ // 600,000 credits / m at 10,000 credits per list-price dollar == $60 / m.
139
+ monthlyCapUsd: 60 / effective,
140
+ weeklyCapUsd: 15 / effective,
141
+ fiveHourCapUsd: 5 / effective,
142
+ multiplier: effective,
143
+ }
144
+ }
145
+
146
+ /** A model id as the documented table spells it (lower-case, trimmed). */
147
+ export function normalizeModelId(value) {
148
+ return typeof value === 'string' ? value.trim().toLowerCase() : ''
149
+ }
150
+
151
+ /** The documented entry for a model id, or null. */
152
+ export function designPlanModel(modelId) {
153
+ return DESIGN_PLAN_MODELS[normalizeModelId(modelId)] ?? null
154
+ }
155
+
156
+ /** A token count as a finite non-negative number. */
157
+ function tokens(value) {
158
+ return typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : 0
159
+ }
160
+
161
+ /**
162
+ * The documented list-price cost of one bucket of tokens for one model.
163
+ *
164
+ * @returns `{ usd, priced }`; `priced` is false when the model has no documented
165
+ * price (the cost is then 0 for that model, and callers must say so rather
166
+ * than pretend the total is complete).
167
+ */
168
+ export function modelCostUsd(modelId, bucket) {
169
+ const model = designPlanModel(modelId)
170
+ if (model === null) return { usd: 0, priced: false }
171
+ const input = tokens(bucket?.input)
172
+ const output = tokens(bucket?.output)
173
+ const cacheRead = tokens(bucket?.cacheRead)
174
+ const cacheWrite = tokens(bucket?.cacheWrite)
175
+ // cacheWrite has no documented Design Plan price; bill it as fresh input so a
176
+ // value is never silently dropped, and flag it so the caller can explain.
177
+ const priced =
178
+ Number.isFinite(model.input) && Number.isFinite(model.output) && Number.isFinite(model.cacheRead)
179
+ if (!priced) return { usd: 0, priced: true, unavailability: 'the model has no documented Design Plan price' }
180
+ const usd =
181
+ (input / 1e6) * model.input +
182
+ (output / 1e6) * model.output +
183
+ (cacheRead / 1e6) * model.cacheRead +
184
+ (cacheWrite / 1e6) * model.input
185
+ return { usd, priced: true }
186
+ }
187
+
188
+ /**
189
+ * Total documented list-price cost of a per-model token map.
190
+ *
191
+ * @param models - `{ [modelId]: { input, output, cacheRead, cacheWrite } }`
192
+ * @returns `{ usd, priced, unpricedModels }` where `unpricedModels` lists the
193
+ * ids that had tokens but no documented price (their cost is not included).
194
+ */
195
+ export function designPlanCostUsd(models) {
196
+ let usd = 0
197
+ let priced = false
198
+ const unpricedModels = []
199
+ const mixes = []
200
+ for (const [modelId, bucket] of Object.entries(models ?? {})) {
201
+ const total = tokens(bucket?.input) + tokens(bucket?.output) + tokens(bucket?.cacheRead) + tokens(bucket?.cacheWrite)
202
+ if (total === 0) continue
203
+ const cost = modelCostUsd(modelId, bucket)
204
+ if (cost.priced !== true) {
205
+ unpricedModels.push(modelId)
206
+ continue
207
+ }
208
+ usd += cost.usd
209
+ priced = true
210
+ // Per-model list-price spend, so the cap can be weighted by real consumption.
211
+ const model = designPlanModel(modelId)
212
+ if (model !== null && Number.isFinite(model.multiplier) && model.multiplier > 0 && cost.usd > 0) {
213
+ mixes.push({ modelId: normalizeModelId(modelId), listUsd: cost.usd, multiplier: model.multiplier })
214
+ }
215
+ }
216
+ return { usd, priced, unpricedModels, mixes }
217
+ }
@@ -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,35 @@ 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,
295
+ // v1.6.2: true when some consumed model has no documented price, so
296
+ // `estimatedUsd` is a lower bound and the UI must say so.
297
+ partial: input?.partial === true,
273
298
  windows,
274
299
  complete: since === key,
275
300
  since,
@@ -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.3",
3
+ "version": "1.6.2",
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",