dsh-tokenlens 1.5.3 → 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,16 @@
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
+
7
17
  ## 1.5.3 — 2026-11-03
8
18
 
9
19
  - **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`:**328 个自动化用例**,覆盖 Host 数据层、供应商适配器、统计折叠、各 harness 读取、Design Plan 周期账本与官方单价计价、窗口锚点切片、工作区自动识别、插件集成与浏览器端渲染 / 交互。
443
444
  - 在真实环境中验证过 **5 个 harness** 的数据源;本机统计与独立的 SQLite 查询、独立的日志解析逐项对账(差额为 0),跨 harness 合并与供应商映射算术自洽。
444
445
  - 供应商接口按各官方文档与参考客户端行为验证;未配置凭据时 chip 静默隐藏,接口异常时按可重试错误降级。
445
446
 
package/client.js CHANGED
@@ -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: '保存',
@@ -601,10 +601,10 @@ window.__ModuleLoader__.load({
601
601
  providerControlKeySaved: 'Saved; paste again to replace it',
602
602
  configureProvider: 'Configure',
603
603
  errorOpenDesignConfig: 'Open Design quota needs a workspace ID and a Control Key (not an inference key)',
604
- designPlanLabel: 'Period Design Plan (estimate)',
604
+ designPlanLabel: 'Period Design Plan',
605
605
  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',
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',
608
608
  designPlanComplete: 'covers every window of this period',
609
609
  designPlanPartial: 'only {count} observed window(s)',
610
610
  providerConsoleURLPlaceholder: 'Leave empty for the built-in default',
@@ -1090,6 +1090,11 @@ window.__ModuleLoader__.load({
1090
1090
  const windows = Number.isFinite(raw.windows) && raw.windows > 0 ? Math.round(raw.windows) : 0
1091
1091
  const estimatedUsd = numberOrNull(raw.estimatedUsd)
1092
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)
1093
1098
  return {
1094
1099
  periodStart: stringOrEmpty(raw.periodStart),
1095
1100
  periodEnd: stringOrEmpty(raw.periodEnd),
@@ -1099,6 +1104,9 @@ window.__ModuleLoader__.load({
1099
1104
  complete: raw.complete === true,
1100
1105
  since: stringOrEmpty(raw.since),
1101
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,
1102
1110
  }
1103
1111
  }
1104
1112
 
@@ -4018,25 +4026,38 @@ window.__ModuleLoader__.load({
4018
4026
  }
4019
4027
 
4020
4028
  /**
4021
- * The open-design period estimate row (v1.5.0).
4029
+ * The open-design period estimate row (v1.5.0, priced in v1.6.0).
4022
4030
  *
4023
4031
  * 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.
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).
4028
4037
  */
4029
4038
  function buildDesignPlanRow(designPlan, t) {
4030
4039
  if (designPlan === null) return null
4031
4040
  const parts = []
4032
- 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')
4033
4049
  parts.push(
4034
- h(
4035
- 'span',
4036
- { key: 'usd', className: 'dsh-tokenlens-percent dsh-tokenlens-capacity-money' },
4037
- formatMoney(designPlan.estimatedUsd, 'USD'),
4038
- ),
4050
+ h('span', { key: 'usd', className: 'dsh-tokenlens-percent dsh-tokenlens-capacity-money' }, value),
4039
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
+ }
4040
4061
  } else {
4041
4062
  parts.push(
4042
4063
  h(
@@ -4047,9 +4068,10 @@ window.__ModuleLoader__.load({
4047
4068
  )
4048
4069
  }
4049
4070
  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) }))
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 }))
4053
4075
  }
4054
4076
  if (designPlan.complete === true) notes.push(t('designPlanComplete'))
4055
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.3'
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
  /**
@@ -1196,12 +1249,12 @@ export function apply(ctx, rawConfig) {
1196
1249
  return consoleUrl === null ? null : { console: consoleUrl }
1197
1250
  }
1198
1251
 
1199
- function snapshotFromResult(providerId, read, now) {
1252
+ async function snapshotFromResult(providerId, read, now) {
1200
1253
  // v1.5.0: the Design Plan period estimate. The ledger observes every read
1201
1254
  // (refreshes and background polls), then the current period is summarized
1202
1255
  // into the snapshot so the dialog can show it without a second round-trip.
1203
1256
  recordDesignPlan(read.result.designPlan ?? null, now)
1204
- const designPlan = designPlanSummaryFor(providerId, read.result.designPlan)
1257
+ const designPlan = await designPlanSummaryFor(providerId, read.result.designPlan)
1205
1258
  return buildSnapshot({
1206
1259
  provider: providerInfo(providerId),
1207
1260
  plan: read.result.plan ?? null,
@@ -1240,7 +1293,7 @@ export function apply(ctx, rawConfig) {
1240
1293
  state.lastAttempts.set(canonicalId, new Date().toISOString())
1241
1294
  try {
1242
1295
  const read = await readProvider(canonicalId, Date.now())
1243
- const snapshot = snapshotFromResult(canonicalId, read, Date.now())
1296
+ const snapshot = await snapshotFromResult(canonicalId, read, Date.now())
1244
1297
  state.caches.set(canonicalId, { snapshot, fetchedAtMs: read.fetchedAtMs })
1245
1298
  state.lastErrors.delete(canonicalId)
1246
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,
@@ -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.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",