dsh-tokenlens 1.7.1 → 1.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,26 @@
4
4
 
5
5
  **版本记录约定**:同一发布日只保留一个章节,标题取当日**最后一个**已发布版本号;当日多次发布的改动合并到该章节,并在标题下注明当日实际发布过的版本号(npm 上仍可安装这些独立版本)。若当日已有发布,后续小改动不再单独增加版本号,而是并入下一篇日志。这样日志按天规整、不出现同日多个小版本。
6
6
 
7
+ ## 1.7.3 — 2026-11-06
8
+
9
+ (当日已发布版本:v1.6.5 / v1.6.6 / v1.6.7 / v1.7.0 / v1.7.1 / v1.7.2 / v1.7.3)
10
+
11
+ - **fix(commandcode): 本月百分比改回按额度池计算**——v1.7.2 按「所选模型的额度」当分母(GOAT + DeepSeek V4.1 Flash = $100),但控制台自己的每月限额是按**额度池**算的。同一笔 $2.94 消费:`2.94 / 70 = 4.2%`(控制台显示 4%),`2.94 / 100 = 2.9%`(v1.7.2 显示 2.9%)——所以那一版与你在控制台看到的数对不上。现在分母回到额度池,实机读数 `$2.96 / $70.00 · 4.23%`,与控制台一致
12
+ - **feat(commandcode): 模型额度改为参考信息**——$100 不是分母,而是「该模型折扣后能覆盖多少消费」。控制台有这张按模型表(`assets/plan-tiers-*.js`,已转录到 `lib/commandcode-plans.mjs`),插件把当前模型的等效额度作为一行带标签的补充信息显示,并说明「控制台按额度池计百分比」,不再悄悄替换分母
13
+ - 同步修正测试:改用实机真值(剩余 `67.087370791` + 本期消费 `2.912629209` = 额度池 `70.000000000`)而不是自造数,并断言**切换模型不会移动仪表指针**
14
+ - 测试:369 → 370
15
+
16
+ ## 1.7.2 — 2026-11-06
17
+
18
+ (当日已发布版本:v1.6.5 / v1.6.6 / v1.6.7 / v1.7.0 / v1.7.1 / v1.7.2)
19
+
20
+ - **fix(stats): 「最近X天」改为按套餐购买日起算**——此前窗口起点是「本机第一次记录到该供应商的那一天」,与你实际买的套餐周期无关。现在 Host 在每次读取额度时记下当前计费周期的起点(Command Code 取 `subscription.data.currentPeriodStart`,Open Design 取 Design Plan 的 `periodStart`),统计数据以它为窗口起点,`subscribed` 也随之正确。实机验证:commandcode `effectiveDay 2026-11-05`、`effectiveDays 2`(购买日为 11-05)
21
+ - **fix(stats): 窗口区间与标签严格一致**——`sinceEffective` 不再借用「最近30天」或「全时段」桶,而是按 `[窗口起点, 今天]` 逐日求和;超过 30 天时收缩**窗口起点**而非仅改天数标签
22
+ - **feat(ui): TPS 曲线加纵坐标与悬停读数**——左侧三档刻度(峰值 / 半值 / 0),鼠标划过时显示十字准线、绿点标记与读数条「日期 · 速率 · (输出 ÷ 耗时)」,对齐 DSH 原生图表的交互;曲线与 token 统计共用同一窗口
23
+ - **fix(ui): 本月额度按所选模型计算**——Command Code 的额度池与模型额度是两个数:GOAT 的池是 $70,但控制台对 **DeepSeek V4.1 Flash 标注 $100**(该模型相对同一池子有折扣)。此前一律用池,导致额度与所选模型无关,与你的预期不符。现在客户端把当前模型随请求上报,Host 用控制台同款按模型额度表定分母,`credits.granted` 与本月百分比随之变化;未上报模型或模型不在表内时回退额度池,绝不臆造
24
+ - 表来源:控制台 bundle `assets/plan-tiers-*.js`,2026-11-06 转录(见 `lib/commandcode-plans.mjs`)。已覆盖 `individual-goat` 与 `individual-go`
25
+ - 测试:364 → 369(按模型额度/回退/裸 id 匹配、周期起点上报、订阅窗口锚定与逐日求和、TPS 纵坐标与悬停接线、请求携带模型 id)
26
+
7
27
  ## 1.7.1 — 2026-11-06
8
28
 
9
29
  (当日已发布版本:v1.6.5 / v1.6.6 / v1.6.7 / v1.7.0 / v1.7.1)
package/README.en.md CHANGED
@@ -144,7 +144,7 @@ Host bridge route: `/api/tokenlens`; agent tool: `tokenlens_usage`.
144
144
 
145
145
  ## Development
146
146
 
147
- Node.js ≥ 24, zero runtime dependencies: `node --test` runs the 364 tests.
147
+ Node.js ≥ 24, zero runtime dependencies: `node --test` runs the 370 tests.
148
148
 
149
149
  Release: `node scripts/bump-version.mjs <version> "<note>"` updates `package.json`, `PLUGIN_VERSION` and `CHANGELOG.md`; see [CONTRIBUTING.md](CONTRIBUTING.md) and [SECURITY.md](SECURITY.md).
150
150
 
package/README.md CHANGED
@@ -445,7 +445,7 @@ Host 半注册 `tokenlens_usage`,参数 `action`(默认 `snapshot`)与可
445
445
 
446
446
  ## 验证
447
447
 
448
- - `node --test`:**364 个自动化用例**,覆盖 Host 数据层、供应商适配器(含小米 MiMo 控制台 Cookie 适配器、Command Code 月度额度反推)、统计折叠、各 harness 读取、Design Plan 周期账本与额度计价、TPS 速度序列、窗口锚点切片、工作区自动识别、插件集成与浏览器端渲染 / 交互。
448
+ - `node --test`:**370 个自动化用例**,覆盖 Host 数据层、供应商适配器(含小米 MiMo 控制台 Cookie 适配器、Command Code 月度额度反推)、统计折叠、各 harness 读取、Design Plan 周期账本与额度计价、TPS 速度序列、窗口锚点切片、工作区自动识别、插件集成与浏览器端渲染 / 交互。
449
449
  - 在真实环境中验证过 **5 个 harness** 的数据源;本机统计与独立的 SQLite 查询、独立的日志解析逐项对账(差额为 0),跨 harness 合并与供应商映射算术自洽。
450
450
  - 供应商接口按各官方文档与参考客户端行为验证;未配置凭据时 chip 静默隐藏,接口异常时按可重试错误降级。
451
451
 
package/client.js CHANGED
@@ -199,6 +199,9 @@ window.__ModuleLoader__.load({
199
199
  balanceBonus: '奖励:{amounts}',
200
200
  creditsRemaining: '剩余 {amount}',
201
201
  creditsGranted: '本月额度 {amount}',
202
+ creditsModelAllowance: '{model} 等效额度 {amount}',
203
+ creditsModelAllowanceModel: '当前模型',
204
+ creditsModelAllowanceHint: '控制台的每月百分比按额度池计算;等效额度是该模型折扣后能覆盖的消费额',
202
205
  creditsPurchased: '加购 {amount}',
203
206
  refresh: '刷新',
204
207
  refreshing: '正在刷新…',
@@ -234,6 +237,7 @@ window.__ModuleLoader__.load({
234
237
  statsSpeedPeak: '峰值 {tps} tok/s',
235
238
  statsSpeedEmpty: '暂无可计算的速度样本(需要 step 计时数据)',
236
239
  statsSpeedBarTitle: '{date} · {tps} tok/s({tokens} 输出 ÷ {seconds}s)',
240
+ statsSpeedDetail: '{tokens} 输出 ÷ {seconds}s',
237
241
  statsLast7Days: '最近7天',
238
242
  stats7DaysShort: '7天',
239
243
  statsDaysShort: '{n}天',
@@ -474,6 +478,10 @@ window.__ModuleLoader__.load({
474
478
  balanceBonus: 'Bonus: {amounts}',
475
479
  creditsRemaining: 'Remaining {amount}',
476
480
  creditsGranted: 'Granted {amount}',
481
+ creditsModelAllowance: '{model} equivalent {amount}',
482
+ creditsModelAllowanceModel: 'current model',
483
+ creditsModelAllowanceHint:
484
+ 'The console computes its monthly percentage from the credit pool; the equivalent allowance is what a discounted model can cover',
477
485
  creditsPurchased: 'Purchased {amount}',
478
486
  refresh: 'Refresh',
479
487
  refreshing: 'Refreshing…',
@@ -510,6 +518,7 @@ window.__ModuleLoader__.load({
510
518
  statsSpeedPeak: 'peak {tps} tok/s',
511
519
  statsSpeedEmpty: 'No speed samples yet (needs step timing data)',
512
520
  statsSpeedBarTitle: '{date} · {tps} tok/s ({tokens} output ÷ {seconds}s)',
521
+ statsSpeedDetail: '{tokens} output ÷ {seconds}s',
513
522
  statsLast7Days: 'Last 7 days',
514
523
  stats7DaysShort: '7d',
515
524
  statsDaysShort: '{n}d',
@@ -1094,6 +1103,13 @@ window.__ModuleLoader__.load({
1094
1103
  remaining: numberOrNull(raw.remaining),
1095
1104
  granted: numberOrNull(raw.granted),
1096
1105
  purchased: numberOrNull(raw.purchased),
1106
+ // v1.7.3: the per-model allowance is ADVERTISING, not the meter's
1107
+ // denominator — the console measures the monthly percentage against the
1108
+ // credit pool. It is kept only so the card can explain the difference.
1109
+ poolUsd: numberOrNull(raw.poolUsd),
1110
+ allowanceUsd: numberOrNull(raw.allowanceUsd),
1111
+ allowanceModel: typeof raw.allowanceModel === 'string' && raw.allowanceModel !== '' ? raw.allowanceModel : null,
1112
+ allowanceBasis: typeof raw.allowanceBasis === 'string' ? raw.allowanceBasis : null,
1097
1113
  }
1098
1114
  if (credits.remaining === null && credits.granted === null && credits.purchased === null) return null
1099
1115
  return credits
@@ -2060,6 +2076,10 @@ window.__ModuleLoader__.load({
2060
2076
  let state = {
2061
2077
  // Provider reported by the session projection; null = "Host default".
2062
2078
  providerId: null,
2079
+ // v1.7.2: model reported by the same projection. Command Code's monthly
2080
+ // allowance depends on it ($100 on GOAT for DeepSeek V4.1 Flash), so the
2081
+ // Host needs it with every usage request.
2082
+ modelId: null,
2063
2083
  // Usage for the current provider.
2064
2084
  phase: 'idle', // idle | loading | ready | error
2065
2085
  snapshot: null,
@@ -2086,6 +2106,8 @@ window.__ModuleLoader__.load({
2086
2106
  // v0.25.0 chart hover: the nearest bar index under the pointer, or null.
2087
2107
  // Reset on mouse leave and whenever the scope/view/provider changes.
2088
2108
  chartHover: null,
2109
+ // v1.7.2: the hovered point of the output-speed (TPS) curve.
2110
+ speedHover: null,
2089
2111
  // v1.2.0 chart drill-down: the clicked day whose 0..23 hour histogram
2090
2112
  // is charted instead of the daily trend, or null (daily trend).
2091
2113
  chartDay: null,
@@ -2218,7 +2240,11 @@ window.__ModuleLoader__.load({
2218
2240
 
2219
2241
  /** Build a bridge payload, omitting the provider when the Host should pick. */
2220
2242
  function usagePayload(action, providerId) {
2221
- return providerId === null || providerId === undefined ? { action } : { action, providerId }
2243
+ const base = providerId === null || providerId === undefined ? { action } : { action, providerId }
2244
+ // v1.7.2: the selected model rides along — Command Code's monthly allowance
2245
+ // depends on it (GOAT gives DeepSeek V4.1 Flash $100, its pool is $70).
2246
+ const modelId = store.get().modelId
2247
+ return typeof modelId === 'string' && modelId !== '' ? { ...base, modelId } : base
2222
2248
  }
2223
2249
 
2224
2250
  /** Build a stats payload (stats are global but the call reports the provider too). */
@@ -3282,6 +3308,26 @@ window.__ModuleLoader__.load({
3282
3308
  void requestUsage('snapshot')
3283
3309
  }
3284
3310
 
3311
+ /**
3312
+ * Sync BOTH halves of the session selection (v1.7.2).
3313
+ *
3314
+ * The provider decides which adapter runs; the model decides Command Code's
3315
+ * per-model monthly allowance. A model switch must therefore refetch even
3316
+ * when the provider did not move, or the chip would keep the old allowance
3317
+ * until the next poll.
3318
+ */
3319
+ function syncSelection(providerId, modelId) {
3320
+ const nextModel = typeof modelId === 'string' && modelId !== '' ? modelId : null
3321
+ const before = store.get()
3322
+ const modelChanged = before.modelId !== nextModel
3323
+ if (modelChanged) store.set({ modelId: nextModel })
3324
+ syncProvider(providerId)
3325
+ const after = store.get()
3326
+ if (modelChanged && after.providerId !== null && after.inFlight !== true) {
3327
+ void requestUsage('snapshot')
3328
+ }
3329
+ }
3330
+
3285
3331
  /** The refresh button: usage first, then a fresh stats scan, independently. */
3286
3332
  async function refreshAll() {
3287
3333
  await requestUsage('refresh')
@@ -3445,6 +3491,11 @@ window.__ModuleLoader__.load({
3445
3491
  /* v1.7.1: the TPS trend is a full-width row, exactly like the details card. */
3446
3492
  .dsh-tokenlens-chart-layout-row>.dsh-tokenlens-section[data-speed-section]{flex:1 1 100%;min-width:0}
3447
3493
  .dsh-tokenlens-speed-svg{display:block;width:100%;height:96px;overflow:visible}
3494
+ /* v1.7.2: Y axis + hover readout for the TPS curve. */
3495
+ .dsh-tokenlens-speed-body{display:flex;gap:6px;align-items:stretch;min-width:0}
3496
+ .dsh-tokenlens-speed-yaxis{display:flex;flex-direction:column;justify-content:space-between;align-items:flex-end;color:var(--dsw-alias-label-tertiary);font-size:10px;line-height:1;font-variant-numeric:tabular-nums;flex:0 0 auto;padding:1px 0}
3497
+ .dsh-tokenlens-speed-plot{position:relative;flex:1 1 auto;min-width:0}
3498
+ .dsh-tokenlens-speed-dot{position:absolute;width:6px;height:6px;margin:-3px 0 0 -3px;border-radius:50%;background:#2e9e5b;box-shadow:0 0 0 2px var(--dsw-alias-interactive-bg-hover,rgba(0,0,0,.35));pointer-events:none}
3448
3499
  .dsh-tokenlens-speed-axis{display:flex;justify-content:space-between;gap:6px;color:var(--dsw-alias-label-tertiary);font-size:10px;line-height:1.2;font-variant-numeric:tabular-nums;min-width:0}
3449
3500
  .dsh-tokenlens-speed-axis>span{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
3450
3501
  @media (max-width:680px){.dsh-tokenlens-chart-layout-row>*{flex-basis:100%!important}}
@@ -3803,12 +3854,33 @@ window.__ModuleLoader__.load({
3803
3854
  }
3804
3855
  }
3805
3856
 
3857
+ /**
3858
+ * The MODEL selected in this session (v1.7.2).
3859
+ *
3860
+ * The projection's selection is `{provider, model, reasoningEffort?}`, so the
3861
+ * same read that yields the provider also yields the model id — which is what
3862
+ * decides Command Code's per-model monthly allowance.
3863
+ */
3864
+ function useSelectedModel(props) {
3865
+ const useProjection = props?.useProjection
3866
+ if (typeof useProjection !== 'function') return null
3867
+ const model = useProjection('modelSelection')
3868
+ try {
3869
+ const selection = model?.next ?? model?.lastUsed
3870
+ const id = selection?.model
3871
+ return typeof id === 'string' && id !== '' ? id : null
3872
+ } catch {
3873
+ return null
3874
+ }
3875
+ }
3876
+
3806
3877
  /** Keep the shared store's provider in sync with the session selection. */
3807
3878
  function useProviderSync(props) {
3808
3879
  const providerId = useSelectedProvider(props)
3880
+ const modelId = useSelectedModel(props)
3809
3881
  React.useEffect(() => {
3810
- syncProvider(providerId)
3811
- }, [providerId])
3882
+ syncSelection(providerId, modelId)
3883
+ }, [providerId, modelId])
3812
3884
  return providerId
3813
3885
  }
3814
3886
 
@@ -4311,6 +4383,31 @@ window.__ModuleLoader__.load({
4311
4383
  h('span', { key: 'purchased' }, t('creditsPurchased', { amount: formatMoney(credits.purchased, 'USD') })),
4312
4384
  )
4313
4385
  }
4386
+ // v1.7.3: the console advertises a larger allowance for discounted models
4387
+ // (DeepSeek V4.1 Flash: $100 on a $70 pool) but still prints the monthly
4388
+ // percentage against the POOL. Say so instead of silently disagreeing.
4389
+ if (
4390
+ credits.allowanceUsd !== null &&
4391
+ credits.granted !== null &&
4392
+ Math.abs(credits.allowanceUsd - credits.granted) > 0.005
4393
+ ) {
4394
+ const modelLabel = String(credits.allowanceModel ?? '').replace(/^[^/]+\//, '')
4395
+ parts.push(
4396
+ h(
4397
+ 'span',
4398
+ {
4399
+ key: 'allowance',
4400
+ className: 'dsh-tokenlens-credits-allowance',
4401
+ 'data-model-allowance': String(credits.allowanceUsd),
4402
+ title: t('creditsModelAllowanceHint'),
4403
+ },
4404
+ t('creditsModelAllowance', {
4405
+ model: modelLabel === '' ? t('creditsModelAllowanceModel') : modelLabel,
4406
+ amount: formatMoney(credits.allowanceUsd, 'USD'),
4407
+ }),
4408
+ ),
4409
+ )
4410
+ }
4314
4411
  if (parts.length === 0) return null
4315
4412
  return h('div', { key: 'credits', className: 'dsh-tokenlens-credits' }, parts)
4316
4413
  }
@@ -4649,6 +4746,12 @@ window.__ModuleLoader__.load({
4649
4746
  if (store.get().chartHover !== next) store.set({ chartHover: next })
4650
4747
  }
4651
4748
 
4749
+ /** The hovered point of the output-speed curve (v1.7.2). */
4750
+ function setSpeedHover(index) {
4751
+ const next = Number.isInteger(index) && index >= 0 ? index : null
4752
+ if (store.get().speedHover !== next) store.set({ speedHover: next })
4753
+ }
4754
+
4652
4755
  /** The active today bucket of the current stats scope (v0.27.2). */
4653
4756
  function todayBucketOf(ctx) {
4654
4757
  return ctx.providerScoped ? ctx.currentRow?.today ?? null : ctx.stats?.today ?? null
@@ -6167,14 +6270,102 @@ window.__ModuleLoader__.load({
6167
6270
  }),
6168
6271
  columns,
6169
6272
  )
6273
+ const hoverIndex =
6274
+ Number.isInteger(state.speedHover) && state.speedHover >= 0 && state.speedHover < byDay.length
6275
+ ? state.speedHover
6276
+ : null
6277
+ const hovered = hoverIndex === null ? null : byDay[hoverIndex]
6278
+ const handleMove = (event) => {
6279
+ const index = chartIndexFromEvent(event, byDay.length)
6280
+ if (index !== null) setSpeedHover(index)
6281
+ }
6282
+ const handleLeave = () => setSpeedHover(null)
6283
+ // v1.7.2: a Y axis, like the native charts — the curve alone gives no
6284
+ // sense of magnitude.
6285
+ const ticks = [max, max / 2, 0]
6286
+ const yAxis = h(
6287
+ 'div',
6288
+ { key: 'y', className: 'dsh-tokenlens-speed-yaxis', 'aria-hidden': 'true' },
6289
+ ticks.map((value, index) =>
6290
+ h('span', { key: `tick-${index}`, className: 'dsh-tokenlens-chart-tick' }, `${Math.round(value)}`),
6291
+ ),
6292
+ )
6293
+ const crosshair =
6294
+ hoverIndex === null
6295
+ ? null
6296
+ : h('div', {
6297
+ key: 'cross',
6298
+ className: 'dsh-tokenlens-chart-crosshair',
6299
+ 'data-speed-crosshair': String(hoverIndex),
6300
+ 'aria-hidden': 'true',
6301
+ style: { left: `${(points[hoverIndex].x / width) * 100}%` },
6302
+ })
6303
+ const marker =
6304
+ hoverIndex === null
6305
+ ? null
6306
+ : h('div', {
6307
+ key: 'dot',
6308
+ className: 'dsh-tokenlens-speed-dot',
6309
+ 'aria-hidden': 'true',
6310
+ style: {
6311
+ left: `${(points[hoverIndex].x / width) * 100}%`,
6312
+ top: `${(points[hoverIndex].y / height) * 100}%`,
6313
+ },
6314
+ })
6315
+ const readout =
6316
+ hovered === null
6317
+ ? null
6318
+ : h(
6319
+ 'div',
6320
+ {
6321
+ key: 'readout',
6322
+ className: 'dsh-tokenlens-chart-readout',
6323
+ 'data-speed-readout': 'true',
6324
+ role: 'status',
6325
+ style:
6326
+ hoverIndex === 0
6327
+ ? { left: '0%' }
6328
+ : hoverIndex === byDay.length - 1
6329
+ ? { right: '0%' }
6330
+ : {
6331
+ left: `${(points[hoverIndex].x / width) * 100}%`,
6332
+ transform: 'translateX(-50%)',
6333
+ },
6334
+ },
6335
+ `${hovered.day} · ${(hovered.tps ?? 0).toFixed(1)} tok/s`,
6336
+ ` · ${t('statsSpeedDetail', {
6337
+ tokens: formatTokensPrecise(hovered.outputTokens),
6338
+ seconds: hovered.seconds.toFixed(1),
6339
+ })}`,
6340
+ )
6170
6341
  const axis = h(
6171
6342
  'div',
6172
6343
  { key: 'axis', className: 'dsh-tokenlens-speed-axis', 'aria-hidden': 'true' },
6173
6344
  h('span', { key: 'first' }, byDay[0].day),
6174
- byDay.length > 1 ? h('span', { key: 'peak' }, t('statsSpeedPeak', { tps: max.toFixed(1) })) : null,
6175
6345
  byDay.length > 1 ? h('span', { key: 'last' }, byDay[byDay.length - 1].day) : null,
6176
6346
  )
6177
- return h('div', { key: 'plot', className: 'dsh-tokenlens-speed-plot' }, svg, axis)
6347
+ return h(
6348
+ 'div',
6349
+ { key: 'plot', className: 'dsh-tokenlens-speed-body' },
6350
+ yAxis,
6351
+ h(
6352
+ 'div',
6353
+ {
6354
+ key: 'plotarea',
6355
+ className: 'dsh-tokenlens-speed-plot',
6356
+ 'data-speed-plot': 'true',
6357
+ onMouseMove: handleMove,
6358
+ onMouseLeave: handleLeave,
6359
+ onPointerMove: handleMove,
6360
+ onPointerLeave: handleLeave,
6361
+ },
6362
+ svg,
6363
+ crosshair,
6364
+ marker,
6365
+ readout,
6366
+ axis,
6367
+ ),
6368
+ )
6178
6369
  })()
6179
6370
  return h(
6180
6371
  'div',
package/index.js CHANGED
@@ -84,7 +84,7 @@ export const inject = []
84
84
  export const PLUGIN_NAME = 'dsh-tokenlens'
85
85
 
86
86
  /** Plugin version; mirrors package.json (kept here so diagnostics need no fs read). */
87
- export const PLUGIN_VERSION = '1.7.1'
87
+ export const PLUGIN_VERSION = '1.7.3'
88
88
 
89
89
  /** Documented defaults; every field of the raw row config is optional. */
90
90
  const DEFAULTS = {
@@ -299,6 +299,12 @@ export function apply(ctx, rawConfig) {
299
299
  keys: new Map(), // providerId 鈫?{ ref, source, detail } (never the secret)
300
300
  requestCounts: new Map(), // providerId 鈫?successful reads
301
301
  subscribers: new Set(),
302
+ // v1.7.2: the model the browser last reported per provider, so per-model
303
+ // monthly allowances resolve without a round-trip of their own.
304
+ selectedModels: new Map(),
305
+ // v1.7.2: providerId → local day ('YYYY-MM-DD') the current plan period
306
+ // started, used to anchor the 「最近X天(套餐购买日起算)」 statistics window.
307
+ subscriptionStarts: new Map(),
302
308
  client: null, // last browser `hello`
303
309
  // Optional startup update check (settings.updateCheck): the last result
304
310
  // `{current, latest, available, checkedAt, error}` or null before the first
@@ -1121,6 +1127,11 @@ export function apply(ctx, rawConfig) {
1121
1127
  const custom = effectiveCustomProvider(id)
1122
1128
  const mapping = dshRouteMapping(String(providerId))
1123
1129
  let provider = custom ?? dshRouteProviderConfig(String(providerId)) ?? effectiveProviderConfig(id)
1130
+ // v1.7.2: the model the browser reported decides per-model allowances.
1131
+ const selectedModel = state.selectedModels?.get(id) ?? ''
1132
+ if (selectedModel !== '' && provider !== undefined) {
1133
+ provider = { ...provider, selectedModel }
1134
+ }
1124
1135
  if (provider === undefined || provider.enabled === false) {
1125
1136
  throw new UsageError('disabled', `provider ${JSON.stringify(providerId)} is disabled`, {
1126
1137
  retryable: false,
@@ -1297,6 +1308,10 @@ export function apply(ctx, rawConfig) {
1297
1308
  // into the snapshot so the dialog can show it without a second round-trip.
1298
1309
  recordDesignPlan(read.result.designPlan ?? null, now)
1299
1310
  const designPlan = await designPlanSummaryFor(providerId, read.result.designPlan)
1311
+ // v1.7.2: remember when the current plan period began, so the local token
1312
+ // statistics can be anchored to the purchase date ("套餐购买日起算") instead of
1313
+ // to the first day this machine happened to record that provider.
1314
+ rememberPeriodStart(providerId, read.result)
1300
1315
  return buildSnapshot({
1301
1316
  provider: providerInfo(providerId),
1302
1317
  plan: read.result.plan ?? null,
@@ -1455,6 +1470,30 @@ export function apply(ctx, rawConfig) {
1455
1470
  : null
1456
1471
 
1457
1472
  /** Token statistics for the dialog: today + all-time totals. */
1473
+ /**
1474
+ * Remember the local day the current plan period started (v1.7.2).
1475
+ *
1476
+ * The token statistics window is 「最近X天(套餐购买日起算)」, so it must start at
1477
+ * the plan's period start — not at the first day this machine recorded the
1478
+ * provider. Both providers that report a plan also report a period bound:
1479
+ * Command Code in `subscription.data.currentPeriodStart`, Open Design in its
1480
+ * Design Plan `periodStart`.
1481
+ */
1482
+ function rememberPeriodStart(providerId, result) {
1483
+ const raw =
1484
+ (typeof result?.period?.start === 'string' ? result.period.start : '') ||
1485
+ (typeof result?.designPlan?.periodStart === 'string' ? result.designPlan.periodStart : '')
1486
+ if (raw === '') return
1487
+ const at = Date.parse(raw)
1488
+ if (!Number.isFinite(at)) return
1489
+ const date = new Date(at)
1490
+ const day = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`
1491
+ const id = canonicalProviderId(String(providerId))
1492
+ if (state.subscriptionStarts.get(id) === day) return
1493
+ state.subscriptionStarts.set(id, day)
1494
+ noteStatus('subscription-period', { providerId: id, periodStartDay: day })
1495
+ }
1496
+
1458
1497
  /** Whether the last snapshot of this provider represents a subscription/plan. */
1459
1498
  function providerHasSubscription(providerId) {
1460
1499
  const cached = state.caches.get(canonicalProviderId(String(providerId)))
@@ -1583,7 +1622,11 @@ export function apply(ctx, rawConfig) {
1583
1622
  }
1584
1623
  }
1585
1624
  try {
1586
- const stats = await statsService.get({ fresh: options.fresh === true })
1625
+ const stats = await statsService.get({
1626
+ fresh: options.fresh === true,
1627
+ // v1.7.2: anchor each subscription provider's window to its plan start.
1628
+ subscriptionStarts: Object.fromEntries(state.subscriptionStarts),
1629
+ })
1587
1630
  // v0.22.1: only subscription-backed providers use the 最近X天 window;
1588
1631
  // pay-as-you-go/balance providers keep the fixed 最近30天 column.
1589
1632
  const decorated =
@@ -1785,6 +1828,25 @@ export function apply(ctx, rawConfig) {
1785
1828
  ensurePoll()
1786
1829
  }
1787
1830
 
1831
+ /**
1832
+ * Remember the model the user picked, per provider (v1.7.2).
1833
+ *
1834
+ * The monthly allowance depends on it (Command Code's GOAT gives DeepSeek
1835
+ * V4.1 Flash $100 while its credit pool is $70), so a model switch must also
1836
+ * drop the cached snapshot — otherwise the chip would keep showing the old
1837
+ * allowance until the next poll.
1838
+ */
1839
+ function rememberRequestedModel(body, providerId) {
1840
+ const modelId = typeof body?.modelId === 'string' ? body.modelId.trim() : ''
1841
+ if (modelId === '') return
1842
+ const id = canonicalProviderId(providerId)
1843
+ if (state.selectedModels.get(id) === modelId) return
1844
+ state.selectedModels.set(id, modelId)
1845
+ state.caches.delete(id)
1846
+ state.flights.delete(id)
1847
+ noteStatus('client-model', { providerId: id, modelId })
1848
+ }
1849
+
1788
1850
  /** One JSON request/response exchange with the browser half. */
1789
1851
  async function handleBridge(request) {
1790
1852
  let body
@@ -1870,10 +1932,12 @@ export function apply(ctx, rawConfig) {
1870
1932
  }
1871
1933
  if (action === 'snapshot' || action === 'status') {
1872
1934
  rememberRequestedProvider(body)
1935
+ rememberRequestedModel(body, requestProvider(body))
1873
1936
  return json(await get(requestProvider(body)))
1874
1937
  }
1875
1938
  if (action === 'refresh') {
1876
1939
  rememberRequestedProvider(body)
1940
+ rememberRequestedModel(body, requestProvider(body))
1877
1941
  return json(await get(requestProvider(body), { force: true }))
1878
1942
  }
1879
1943
  if (action === 'stats') return json(await getStats({ fresh: body?.fresh === true }))
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Command Code per-model monthly allowances (v1.7.2).
3
+ *
4
+ * `mimo`-style plan catalogues are not served by any public API — the numbers
5
+ * live in the provider's own console bundle (`assets/plan-tiers-*.js`), which is
6
+ * where the console gets the allowance it prints per model. The plugin needs the
7
+ * same table to answer "how much does THIS model get?", because a plan's credit
8
+ * pool and a model's allowance are two different numbers:
9
+ *
10
+ * - the POOL is what the API meters (`credits.monthlyCredits`, and
11
+ * `windowLimits.*.used` are charged against it): `individual-goat` = $70;
12
+ * - the ALLOWANCE is what the console advertises for a given model, because a
13
+ * discounted model stretches the same pool further: DeepSeek V4.1 Flash on
14
+ * GOAT = $100, even though the pool is $70.
15
+ *
16
+ * Transcribed 2026-11-06 from the console bundle:
17
+ * ```js
18
+ * {"zai-org/GLM-5.2":70,"tencent/hy3-paid":70,"Qwen/Qwen3.8-27B":70,
19
+ * "MiniMaxAI/MiniMax-M3":47,"deepseek/deepseek-v4-flash":60,
20
+ * "deepseek/deepseek-v4.1-flash":100,"deepseek/deepseek-v4.1-flash-fast":60,
21
+ * "moonshotai/Kimi-K2.7-Code":60,"xiaomi/mimo-v2.6-pro-ultraspeed":10,
22
+ * "claude-sonnet-5-5":10,"Qwen/Qwen3.7-Max":33,"Qwen/Qwen3.7-Plus":33,
23
+ * "Qwen/Qwen3.6-Plus":33,"google/gemini-3.7-flash":40,
24
+ * "google/gemini-3.8-flash":40,"z-ai/glm-5.3-flash":100,
25
+ * "meituan/LongCat-2.0":50,"inclusionai/ling-3.1-flash":100,
26
+ * "inclusionai/ling-3.1-flash-1019":100}
27
+ * ```
28
+ *
29
+ * Only the two plans whose tables were captured carry per-model numbers; every
30
+ * other plan falls back to its pool, and a model missing from a table falls back
31
+ * to that plan's pool too — the plugin never invents an allowance.
32
+ */
33
+
34
+ /** Plan id -> `{ poolUsd, byModel }`, with model ids as the console spells them. */
35
+ export const COMMANDCODE_PLAN_ALLOWANCES = {
36
+ // GOAT: $70 of credits, but discounted models advertise up to $100 of usage.
37
+ 'individual-goat': {
38
+ poolUsd: 70,
39
+ byModel: {
40
+ 'zai-org/GLM-5.2': 70,
41
+ 'tencent/hy3-paid': 70,
42
+ 'Qwen/Qwen3.8-27B': 70,
43
+ 'MiniMaxAI/MiniMax-M3': 47,
44
+ 'deepseek/deepseek-v4-flash': 60,
45
+ 'deepseek/deepseek-v4.1-flash': 100,
46
+ 'deepseek/deepseek-v4.1-flash-fast': 60,
47
+ 'moonshotai/Kimi-K2.7-Code': 60,
48
+ 'xiaomi/mimo-v2.6-pro-ultraspeed': 10,
49
+ 'claude-sonnet-5-5': 10,
50
+ 'Qwen/Qwen3.7-Max': 33,
51
+ 'Qwen/Qwen3.7-Plus': 33,
52
+ 'Qwen/Qwen3.6-Plus': 33,
53
+ 'google/gemini-3.7-flash': 40,
54
+ 'google/gemini-3.8-flash': 40,
55
+ 'z-ai/glm-5.3-flash': 100,
56
+ 'meituan/LongCat-2.0': 50,
57
+ 'inclusionai/ling-3.1-flash': 100,
58
+ 'inclusionai/ling-3.1-flash-1019': 100,
59
+ },
60
+ },
61
+ // Go ($10 of credits).
62
+ 'individual-go': {
63
+ poolUsd: 10,
64
+ byModel: {
65
+ 'zai-org/GLM-5.2': 10,
66
+ 'tencent/hy3-paid': 10,
67
+ 'Qwen/Qwen3.8-27B': 10,
68
+ 'Qwen/Qwen3.7-Max': 10,
69
+ 'Qwen/Qwen3.7-Plus': 10,
70
+ 'Qwen/Qwen3.6-Plus': 10,
71
+ 'meituan/LongCat-2.0': 10,
72
+ 'zai-org/GLM-5.3': 10,
73
+ 'z-ai/glm-5.3-flash': 10,
74
+ 'deepseek/deepseek-v4.1-flash': 10,
75
+ 'moonshotai/Kimi-K2.7-Code': 9,
76
+ 'zai-org/GLM-5.2-Fast': 9,
77
+ 'MiniMaxAI/MiniMax-M3': 8,
78
+ 'moonshotai/Kimi-K3': 8,
79
+ 'deepseek/deepseek-v4-flash-fast': 8,
80
+ 'Qwen/Qwen3.8-Max': 7,
81
+ 'Qwen/Qwen3.8-Max-0902': 7,
82
+ },
83
+ },
84
+ // Plans whose pools are documented but whose per-model tables were not captured.
85
+ 'individual-go-v1': { poolUsd: 10, byModel: {} },
86
+ 'individual-pro': { poolUsd: 30, byModel: {} },
87
+ 'individual-pro-v1': { poolUsd: 80, byModel: {} },
88
+ 'individual-provider': { poolUsd: 15, byModel: {} },
89
+ 'individual-max': { poolUsd: 150, byModel: {} },
90
+ 'individual-ultra': { poolUsd: 300, byModel: {} },
91
+ 'teams-pro': { poolUsd: 40, byModel: {} },
92
+ }
93
+
94
+ /** A model id without its vendor prefix (`z-ai/glm-5.3-flash` -> `glm-5.3-flash`). */
95
+ function bareModel(modelId) {
96
+ return typeof modelId === 'string' ? modelId.trim().toLowerCase().replace(/^[^/]+\//, '') : ''
97
+ }
98
+
99
+ /**
100
+ * The monthly allowance advertised for one model on one plan.
101
+ *
102
+ * Matching is tolerant about the vendor prefix and casing, because the same model
103
+ * reaches the plugin as `deepseek/deepseek-v4.1-flash` from DSH and as
104
+ * `deepseek-v4.1-flash` in some payloads.
105
+ *
106
+ * @returns `{ allowanceUsd, poolUsd, matched, basis }` where `basis` is
107
+ * `'model'` when a per-model number was found and `'pool'` otherwise.
108
+ * `allowanceUsd` is `null` when the plan itself is unknown.
109
+ */
110
+ export function commandcodeAllowance(planId, modelId) {
111
+ const plan = typeof planId === 'string' ? COMMANDCODE_PLAN_ALLOWANCES[planId.trim()] : undefined
112
+ if (plan === undefined) return { allowanceUsd: null, poolUsd: null, matched: '', basis: 'unknown' }
113
+ const wanted = bareModel(modelId)
114
+ if (wanted !== '') {
115
+ // Exact key first, then a bare-id match so a missing vendor prefix still hits.
116
+ for (const [key, usd] of Object.entries(plan.byModel)) {
117
+ if (key.toLowerCase() === wanted) return { allowanceUsd: usd, poolUsd: plan.poolUsd, matched: key, basis: 'model' }
118
+ }
119
+ for (const [key, usd] of Object.entries(plan.byModel)) {
120
+ if (bareModel(key) === wanted) return { allowanceUsd: usd, poolUsd: plan.poolUsd, matched: key, basis: 'model' }
121
+ }
122
+ }
123
+ return { allowanceUsd: plan.poolUsd, poolUsd: plan.poolUsd, matched: '', basis: 'pool' }
124
+ }
package/lib/providers.mjs CHANGED
@@ -18,6 +18,7 @@
18
18
  * fake zero.
19
19
  */
20
20
  import { createHash, createHmac } from 'node:crypto'
21
+ import { commandcodeAllowance } from './commandcode-plans.mjs'
21
22
  import { normalizePercent, parseTimestamp, UsageError } from './go-usage.mjs'
22
23
  import {
23
24
  XIAOMI_CONSOLE_URL,
@@ -486,7 +487,10 @@ export function parseOpencodeGo(payload, now = Date.now()) {
486
487
  * why `usageSummary` is consulted); the windows' `used`/`cap` are USD, `resetAt`
487
488
  * is epoch seconds or milliseconds.
488
489
  */
489
- export function parseCommandCode({ credits, subscription, whoami, usageSummary } = {}, now = Date.now()) {
490
+ export function parseCommandCode(
491
+ { credits, subscription, whoami, usageSummary, selectedModel } = {},
492
+ now = Date.now(),
493
+ ) {
490
494
  const document = credits !== null && typeof credits === 'object' ? credits : {}
491
495
  const credit = document.credits !== null && typeof document.credits === 'object' ? document.credits : {}
492
496
  const limits = document.windowLimits !== null && typeof document.windowLimits === 'object'
@@ -563,13 +567,23 @@ export function parseCommandCode({ credits, subscription, whoami, usageSummary }
563
567
  : [toNumber(limits.fiveHour?.used), toNumber(limits.weekly?.used)].filter((value) => value !== null)
564
568
  const spend = periodSpend === null ? null : Math.max(periodSpend, ...liveWindowSpend)
565
569
  const reportedGrant = toNumber(credit.monthlyCreditsGranted)
566
- const granted =
570
+ const poolGrant =
567
571
  reportedGrant !== null && reportedGrant > 0
568
572
  ? reportedGrant
569
573
  : remaining !== null && spend !== null
570
574
  ? remaining + spend
571
575
  : null
572
- const monthlyUsed = granted !== null && remaining !== null ? Math.max(0, granted - remaining) : spend
576
+ // v1.7.2: the SELECTED MODEL's advertised allowance (GOAT + DeepSeek V4.1 Flash
577
+ // = $100 even though the credit pool is $70 — a discounted model stretches the
578
+ // same pool further).
579
+ // v1.7.3: it is NOT the month's denominator. The provider's own console prints
580
+ // 每月限额 4% for $2.94 spent, and 2.94/70 = 4.2% while 2.94/100 = 2.9% — the
581
+ // console measures the percentage against the CREDIT POOL. Using the allowance
582
+ // made the plugin disagree with the console the user checks, so the pool stays
583
+ // the denominator and the allowance is reported as context only.
584
+ const allowance = commandcodeAllowance(planId, selectedModel)
585
+ const granted = poolGrant
586
+ const monthlyUsed = poolGrant !== null && remaining !== null ? Math.max(0, poolGrant - remaining) : spend
573
587
  const monthlyPercent =
574
588
  granted !== null && granted > 0 && monthlyUsed !== null
575
589
  ? normalizePercent((monthlyUsed / granted) * 100)
@@ -596,10 +610,29 @@ export function parseCommandCode({ credits, subscription, whoami, usageSummary }
596
610
  return {
597
611
  windows,
598
612
  plan,
613
+ // v1.7.2: the billing period the plan runs on. The statistics window is
614
+ // anchored to `start`, so 最近X天 counts from the day the plan was bought
615
+ // rather than from whenever this machine first saw the provider.
616
+ period: {
617
+ start: typeof data?.currentPeriodStart === 'string' ? data.currentPeriodStart : null,
618
+ end: typeof data?.currentPeriodEnd === 'string' ? data.currentPeriodEnd : null,
619
+ },
599
620
  credits:
600
621
  remaining === null && granted === null && purchased === null
601
622
  ? null
602
- : { remaining, granted, purchased },
623
+ : {
624
+ remaining,
625
+ // `granted` is the credit pool the month is measured against — the
626
+ // same denominator the provider's console uses.
627
+ granted,
628
+ purchased,
629
+ poolUsd: poolGrant,
630
+ // Advertising only (v1.7.3): the allowance the console lists for the
631
+ // selected model. Never used to compute a percentage.
632
+ allowanceUsd: allowance.allowanceUsd,
633
+ allowanceModel: allowance.basis === 'model' ? allowance.matched : null,
634
+ allowanceBasis: allowance.basis,
635
+ },
603
636
  account:
604
637
  profile === null
605
638
  ? null
@@ -1395,7 +1428,21 @@ export const ADAPTERS = {
1395
1428
  ? undefined
1396
1429
  : fetchJson(joinUrl(config.baseURL, config.usageSummaryPath)).catch(() => undefined),
1397
1430
  ])
1398
- return { kind: 'windows', ...parseCommandCode({ credits, subscription, whoami, usageSummary }, now) }
1431
+ // v1.7.2: the model the user picked decides the monthly allowance (GOAT's
1432
+ // DeepSeek V4.1 Flash gets $100 while the credit pool stays $70).
1433
+ return {
1434
+ kind: 'windows',
1435
+ ...parseCommandCode(
1436
+ {
1437
+ credits,
1438
+ subscription,
1439
+ whoami,
1440
+ usageSummary,
1441
+ selectedModel: typeof provider?.selectedModel === 'string' ? provider.selectedModel : '',
1442
+ },
1443
+ now,
1444
+ ),
1445
+ }
1399
1446
  },
1400
1447
  },
1401
1448
  deepseek: {
@@ -1147,7 +1147,7 @@ export function createUsageStats(input) {
1147
1147
  }
1148
1148
 
1149
1149
  /** Compose DSH + external harnesses into the wire stats (v3). */
1150
- async function compose(dsh, catalog, now) {
1150
+ async function compose(dsh, catalog, now, subscriptionStarts = {}) {
1151
1151
  const thirtyStart = dsh.thirtyStart
1152
1152
  const today = emptyBucket()
1153
1153
  const last7Days = emptyBucket()
@@ -1372,14 +1372,14 @@ export function createUsageStats(input) {
1372
1372
  * therefore excluded. A day with no positive duration is dropped instead of
1373
1373
  * being given an infinite rate.
1374
1374
  */
1375
- const speedSeries = (providerId) => {
1375
+ const speedSeries = (providerId, fromDay = thirtyStart) => {
1376
1376
  const source = dsh.speed ?? {}
1377
1377
  const byDay = []
1378
1378
  let output = 0
1379
1379
  let seconds = 0
1380
1380
  let steps = 0
1381
1381
  for (const day of Object.keys(source).sort()) {
1382
- if (day < thirtyStart) continue
1382
+ if (day < fromDay) continue
1383
1383
  let dayOutput = 0
1384
1384
  let daySeconds = 0
1385
1385
  let daySteps = 0
@@ -1411,25 +1411,60 @@ export function createUsageStats(input) {
1411
1411
  },
1412
1412
  }
1413
1413
  }
1414
+ /**
1415
+ * Sum one provider's per-day buckets inside `[fromDay, today]` (v1.7.2).
1416
+ *
1417
+ * Used for the 最近X天 window so the label and the number describe the same
1418
+ * range even when that range is neither 30 days nor all time (a plan bought
1419
+ * three days ago). Returns null when no day in the range has a record, so the
1420
+ * caller can fall back instead of showing a fabricated zero.
1421
+ */
1422
+ const sumRange = (providerId, fromDay) => {
1423
+ let found = false
1424
+ const bucket = emptyBucket()
1425
+ for (const entry of daily) {
1426
+ if (entry.day < fromDay) continue
1427
+ const row = entry.providers === undefined ? undefined : entry.providers[providerId]
1428
+ if (row === undefined) continue
1429
+ bucket.total += typeof row.total === 'number' ? row.total : 0
1430
+ bucket.messages += typeof row.messages === 'number' ? row.messages : 0
1431
+ found = true
1432
+ }
1433
+ return found ? bucket : null
1434
+ }
1435
+
1414
1436
  for (const provider of providers) {
1415
- const effectiveDay = firstDays.get(provider.providerId) ?? todayDay
1437
+ // v1.7.2: a plan-backed provider's window starts on the day the plan was
1438
+ // bought (the Host reports it), not on the day this machine first recorded
1439
+ // it. Falling back to the first-record day keeps every other provider on
1440
+ // the old behaviour.
1441
+ const planStart = subscriptionStarts[provider.providerId]
1442
+ const effectiveDay =
1443
+ typeof planStart === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(planStart) && planStart <= todayDay
1444
+ ? planStart
1445
+ : (firstDays.get(provider.providerId) ?? todayDay)
1416
1446
  provider.effectiveDay = effectiveDay
1417
1447
  // v1.7.0: the 最近X天 window is capped at the same 30 days the charts and
1418
1448
  // the 最近30天 bucket use. A provider first seen 90 days ago used to report
1419
1449
  // 最近90天 while every other figure on the page covered 30.
1420
- const fullDays = dayCount(effectiveDay, todayDay)
1450
+ // v1.7.2: the cap applies to the WINDOW START, so the label and the summed
1451
+ // bucket always describe the same range (`effectiveDay` stays informative).
1452
+ const windowStart = effectiveDay < thirtyStart ? thirtyStart : effectiveDay
1453
+ const fullDays = dayCount(windowStart, todayDay)
1421
1454
  const cappedDays = Math.min(DAILY_SERIES_DAYS, fullDays)
1422
1455
  provider.effectiveDays = cappedDays
1423
- // The label and the numbers must describe the SAME span. When the real
1424
- // history is longer than the cap, the since-effective bucket is therefore
1425
- // the 30-day one rather than all-time (which would carry older records
1426
- // under a 最近30天 label).
1427
- provider.sinceEffective =
1428
- cappedDays < fullDays ? cloneBucket(provider.last30Days) : cloneBucket(provider.allTime)
1456
+ // The label and the numbers must describe the SAME span (v1.7.2): sum the
1457
+ // per-day buckets inside [windowStart, today] rather than borrowing the
1458
+ // 30-day or all-time bucket, which described a different range.
1459
+ const windowBucket = sumRange(provider.providerId, windowStart)
1460
+ provider.sinceEffective = windowBucket === null ? cloneBucket(provider.last30Days) : windowBucket
1429
1461
  // v1.7.0: each provider row carries its own output-speed series so the
1430
1462
  // chart can follow the same scope switch as the token sections.
1431
- provider.speed = speedSeries(provider.providerId)
1463
+ // v1.7.2: it uses the SAME window as the tokens above.
1464
+ provider.speed = speedSeries(provider.providerId, windowStart)
1432
1465
  for (const harness of Object.values(provider.harnesses ?? {})) {
1466
+ // The daily series carries provider totals only, so the harness split
1467
+ // keeps the 30-day / all-time fallback rather than a wrong range sum.
1433
1468
  harness.sinceEffective =
1434
1469
  cappedDays < fullDays && harness.last30Days !== undefined
1435
1470
  ? cloneBucket(harness.last30Days)
@@ -1470,7 +1505,7 @@ export function createUsageStats(input) {
1470
1505
  const work = (async () => {
1471
1506
  const dsh = dshEnabled ? aggregate(scan()) : emptyDshAggregate(Date.now())
1472
1507
  const catalog = await resolveProviderCatalog(Date.now())
1473
- const result = await compose(dsh, catalog, Date.now())
1508
+ const result = await compose(dsh, catalog, Date.now(), options.subscriptionStarts ?? {})
1474
1509
  lastResult = result
1475
1510
  lastResultAt = Date.now()
1476
1511
  return result
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-tokenlens",
3
- "version": "1.7.1",
3
+ "version": "1.7.3",
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",