dsh-tokenlens 1.7.8 → 1.7.9

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,20 @@
4
4
 
5
5
  **版本记录约定**:同一发布日只保留一个章节,标题取当日**最后一个**已发布版本号;当日多次发布的改动合并到该章节,并在标题下注明当日实际发布过的版本号(npm 上仍可安装这些独立版本)。若当日已有发布,后续小改动不再单独增加版本号,而是并入下一篇日志。这样日志按天规整、不出现同日多个小版本。
6
6
 
7
+ ## 1.7.9 — 2026-11-10
8
+
9
+ (当日已发布版本:v1.7.7 / v1.7.8 / v1.7.9)
10
+
11
+ - **feat(stats): TPS 分母改为「纯解码时长」**——从 `step/start → assistant/message` 改为 `首个流式 token → assistant/message`,与 DSH 原生会话统计、上下文洞察插件**完全同口径**。此前的分母含首 token 等待(prefill + 排队),导致本插件数字只有对方的一半
12
+ - 实测对照(12 个会话、8,414 个 step):旧口径 72.3 tok/s,新口径 144.1 tok/s,平均首 token 等待 5.36 秒——**差距正好 2 倍**
13
+ - 实机验证:`2026-10-23` 的日均从 `30.5` 变为 `185.2 tok/s`
14
+ - **feat(stats): 首 token 等待不再被吞掉**——同一个样本同时累加 `ttftSeconds`,卡片右上角显示「首 token 均值 X.Xs」,悬停读数里也带上该点的首 token 均值。口径统一了,但延迟这个真实成本仍然可见
15
+ - **feat(stats): 首 token 的取法**——`step/start` 记录里没有这个字段,它藏在 `assistant/message.data.stream` 的 chunk 计时里(`time` 或增量组 `time0`),取其中最早的绝对时刻
16
+ - 覆盖率实测:223 个会话、42,912 个 step 中 **99.99%** 有可用首 token 时间、覆盖 100% token;缺首 token 的 step 会被跳过而不是混入另一个分母(曲线不能有两种口径)
17
+ - 缓存 schema 7 → 8,首次会重扫(223 文件约 15 秒)
18
+ - 文档:`PROTOCOL.md` 的 `speed` 段更新为解码口径并补 `ttftSeconds`
19
+ - 测试:377(口径与首 token 断言改写为解码窗口真值;新增「缺 stream 的 step 不得编造时长」)
20
+
7
21
  ## 1.7.8 — 2026-11-10
8
22
 
9
23
  (当日已发布版本:v1.7.7 / v1.7.8)
package/PROTOCOL.md CHANGED
@@ -383,24 +383,28 @@ with `[path]`.
383
383
  "hours": [0, 0, 0, 500, 734, …] }
384
384
  } }
385
385
  ],
386
- // v1.7.0: output-speed (TPS) series. `tps` is output tokens over the summed
387
- // model time of the steps that produced them (`step/start` ->
388
- // `assistant/message`), so a step's tool calls — which are emitted after the
389
- // message — do not dilute it. A day with no positive duration is omitted
390
- // instead of being given an infinite rate. `total.tps` is null when the
391
- // window holds no samples. Each provider row carries its own `speed` object
392
- // with the same shape, following the same window as its tokens (v1.7.2).
386
+ // v1.7.0: output-speed (TPS) series. v1.7.9 CHANGED the denominator: `tps`
387
+ // is output tokens over the summed DECODE time of the steps that produced
388
+ // them (`first streamed token` -> `assistant/message`), which is the same
389
+ // pairing the harness's own session statistics and the 上下文洞察 plugin use.
390
+ // The prefill/queue wait it therefore excludes is reported separately as
391
+ // `ttftSeconds` (summed first-token waits for the bucket), so the card can
392
+ // show it instead of hiding it. A day with no positive decode time, or a
393
+ // step whose first-token instant is unknown, is omitted rather than given an
394
+ // invented duration. Each provider row carries its own `speed` object with
395
+ // the same shape, following the same window as its tokens (v1.7.2).
393
396
  "speed": {
394
397
  "byDay": [
395
- { "day": "2026-11-05", "outputTokens": 516951, "seconds": 8493.1,
396
- "tps": 60.9, "steps": 859,
398
+ { "day": "2026-11-05", "outputTokens": 516951, "seconds": 3201.4,
399
+ "tps": 161.5, "steps": 859, "ttftSeconds": 4604.1,
397
400
  // v1.7.7: 24 local-hour slots for the click-to-drill-down view. A slot
398
401
  // with no sample is null rather than a zero, so the curve breaks
399
402
  // instead of dipping to zero. Only provider-scoped series carry it.
400
403
  "hours": [ null, null, …, { "outputTokens": 600, "seconds": 5,
401
- "tps": 120, "steps": 1 }, … ] }
404
+ "tps": 120, "steps": 1, "ttftSeconds": 4.5 }, … ] }
402
405
  ],
403
- "total": { "outputTokens": 939643, "seconds": 14123.9, "tps": 66.5, "steps": 1424 }
406
+ "total": { "outputTokens": 939643, "seconds": 5819.8, "tps": 161.5,
407
+ "steps": 1424, "ttftSeconds": 7676.3 }
404
408
  },
405
409
  "zcode": { // extra-harness detail
406
410
  "dbPath": "C:\\Users\\…\\.zcode\\cli\\db\\db.sqlite",
package/client.js CHANGED
@@ -230,7 +230,8 @@ window.__ModuleLoader__.load({
230
230
  statsLast30Days: '最近30天',
231
231
  statsSpeedTitle: '输出速度(TPS)',
232
232
  statsSpeedHint:
233
- '输出 Token ÷ 模型自身耗时(step 开始到消息返回);不含工具调用等待,但输出含思考 Token。点击曲线上的某一天可查看该日的小时级速度',
233
+ '输出 Token ÷ 纯解码时长(首个 token 到达 → 消息结束),与 DSH 会话统计同口径;首 token 等待单独列出。点击曲线上的某一天可查看该日的小时级速度',
234
+ statsSpeedTtft: '首 token 均值 {seconds}s',
234
235
  statsSpeedTotal: '均值 {tps} tok/s',
235
236
  statsSpeedPeak: '峰值 {tps} tok/s',
236
237
  statsSpeedEmpty: '暂无可计算的速度样本(需要 step 计时数据)',
@@ -513,7 +514,8 @@ window.__ModuleLoader__.load({
513
514
  statsLast30Days: 'Last 30 days',
514
515
  statsSpeedTitle: 'Output speed (TPS)',
515
516
  statsSpeedHint:
516
- 'Output tokens ÷ the model’s own request time (step start to message); tool waits excluded, reasoning tokens included. Click a day on the curve for that day’s hourly speed',
517
+ 'Output tokens ÷ pure decode time (first token → message end), the same pairing as the harness session stats; the first-token wait is listed separately. Click a day on the curve for that day’s hourly speed',
518
+ statsSpeedTtft: 'first token avg {seconds}s',
517
519
  statsSpeedTotal: 'avg {tps} tok/s',
518
520
  statsSpeedPeak: 'peak {tps} tok/s',
519
521
  statsSpeedEmpty: 'No speed samples yet (needs step timing data)',
@@ -1638,6 +1640,7 @@ window.__ModuleLoader__.load({
1638
1640
  seconds: numberOrNull(cell.seconds) ?? 0,
1639
1641
  tps,
1640
1642
  steps: numberOrNull(cell.steps) ?? 0,
1643
+ ttftSeconds: numberOrNull(cell.ttftSeconds) ?? 0,
1641
1644
  }
1642
1645
  })
1643
1646
  : null
@@ -1647,6 +1650,8 @@ window.__ModuleLoader__.load({
1647
1650
  seconds: numberOrNull(entry.seconds) ?? 0,
1648
1651
  tps: numberOrNull(entry.tps),
1649
1652
  steps: numberOrNull(entry.steps) ?? 0,
1653
+ // v1.7.9: the first-token wait the decode-only rate excludes.
1654
+ ttftSeconds: numberOrNull(entry.ttftSeconds) ?? 0,
1650
1655
  hours: hours === null ? null : hours.slice(0, 24),
1651
1656
  }
1652
1657
  })
@@ -1660,6 +1665,7 @@ window.__ModuleLoader__.load({
1660
1665
  seconds: numberOrNull(totalRaw.seconds) ?? 0,
1661
1666
  tps: numberOrNull(totalRaw.tps),
1662
1667
  steps: numberOrNull(totalRaw.steps) ?? 0,
1668
+ ttftSeconds: numberOrNull(totalRaw.ttftSeconds) ?? 0,
1663
1669
  },
1664
1670
  }
1665
1671
  }
@@ -3679,6 +3685,8 @@ window.__ModuleLoader__.load({
3679
3685
  .dsh-tokenlens-repo{display:inline-flex;align-items:center;gap:4px;color:inherit;text-decoration:none;flex:0 0 auto;border-radius:var(--dsw-radius-sm);transition:color .12s ease,opacity .12s ease}
3680
3686
  .dsh-tokenlens-repo:hover,.dsh-tokenlens-repo:focus-visible{color:var(--dsw-alias-label-primary,currentColor);opacity:1;text-decoration:underline}
3681
3687
  .dsh-tokenlens-version-text{flex:0 0 auto}
3688
+ /* v1.7.9: the first-token wait sits beside the rate, visually secondary. */
3689
+ .dsh-tokenlens-speed-ttft{margin-left:8px;opacity:.8;white-space:nowrap}
3682
3690
  /* Update status on the plugin-version line (dialog body): wraps on narrow screens. */
3683
3691
  .dsh-tokenlens-version-update{white-space:normal;overflow-wrap:anywhere}
3684
3692
  .dsh-tokenlens-version .dsh-tokenlens-update-link{color:inherit;font-size:inherit;vertical-align:baseline}
@@ -6239,6 +6247,21 @@ window.__ModuleLoader__.load({
6239
6247
  'span',
6240
6248
  { className: 'dsh-tokenlens-hint', 'data-speed-total': String(series.total.tps) },
6241
6249
  t('statsSpeedTotal', { tps: series.total.tps.toFixed(1) }),
6250
+ // v1.7.9: the wait the decode-only rate leaves out, stated rather
6251
+ // than hidden — it is often several seconds per step.
6252
+ series.total.steps > 0 && series.total.ttftSeconds > 0
6253
+ ? h(
6254
+ 'span',
6255
+ {
6256
+ key: 'ttft',
6257
+ className: 'dsh-tokenlens-speed-ttft',
6258
+ 'data-speed-ttft': String(series.total.ttftSeconds),
6259
+ },
6260
+ t('statsSpeedTtft', {
6261
+ seconds: (series.total.ttftSeconds / series.total.steps).toFixed(1),
6262
+ }),
6263
+ )
6264
+ : null,
6242
6265
  )
6243
6266
  : null
6244
6267
  : h(
@@ -6437,6 +6460,10 @@ window.__ModuleLoader__.load({
6437
6460
  tokens: formatTokensPrecise(hovered.outputTokens),
6438
6461
  seconds: hovered.seconds.toFixed(1),
6439
6462
  })}`,
6463
+ // v1.7.9: the first-token wait, when this bucket has samples.
6464
+ hovered.steps > 0 && hovered.ttftSeconds > 0
6465
+ ? ` · ${t('statsSpeedTtft', { seconds: (hovered.ttftSeconds / hovered.steps).toFixed(1) })}`
6466
+ : null,
6440
6467
  )
6441
6468
  const axis = h(
6442
6469
  '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.8'
87
+ export const PLUGIN_VERSION = '1.7.9'
88
88
 
89
89
  /**
90
90
  * Providers whose monthly allowance depends on the SELECTED model (v1.7.6).
@@ -61,7 +61,7 @@ const DECODES_CONCATENATED_FRAMES = (() => {
61
61
  const SESSION_FILE = 'session.v4.jsonl.zstd'
62
62
 
63
63
  /** Cache schema version; a mismatch discards the cache. */
64
- const CACHE_VERSION = 7
64
+ const CACHE_VERSION = 8
65
65
 
66
66
  /**
67
67
  * How many days the daily series (and therefore the 图表 mode and the
@@ -109,6 +109,38 @@ function usageFromStream(stream) {
109
109
  return null
110
110
  }
111
111
 
112
+ /**
113
+ * The instant the model emitted its FIRST streamed byte (v1.7.9), or null.
114
+ *
115
+ * Decode-only output speed divides by `message.time − firstToken`, which is the
116
+ * same pairing the harness's own session statistics and the 上下文洞察 plugin use;
117
+ * it therefore excludes prefill/queue (the "首 token 等待"). The timestamp is
118
+ * not on `step/start` — it lives in the message's own `data.stream`, either as
119
+ * an absolute `time` on a chunk entry or as `time0` on a delta-encoded group.
120
+ * The earliest one wins, so the wait is never understated.
121
+ */
122
+ function firstTokenTime(stream) {
123
+ if (!Array.isArray(stream) || stream.length === 0) return null
124
+ let earliest = null
125
+ for (const item of stream) {
126
+ if (item === null || typeof item !== 'object') continue
127
+ const at =
128
+ typeof item.time === 'number' && Number.isFinite(item.time)
129
+ ? item.time
130
+ : typeof item.time0 === 'number' && Number.isFinite(item.time0)
131
+ ? item.time0
132
+ : null
133
+ if (at === null) continue
134
+ if (earliest === null || at < earliest) earliest = at
135
+ }
136
+ return earliest
137
+ }
138
+
139
+ /** One output-speed accumulator (v1.7.9): decode seconds + first-token seconds. */
140
+ function emptySpeedSlot() {
141
+ return { output: 0, seconds: 0, ttftSeconds: 0, steps: 0 }
142
+ }
143
+
112
144
  /**
113
145
  * A JSON-able usage record from one event, or null when it is not billable usage.
114
146
  *
@@ -331,27 +363,42 @@ export function foldSession(records) {
331
363
  addBucket(dayProvider, usage)
332
364
  addBucket((providers[attributedProvider] ??= emptyBucket()), usage)
333
365
  // v1.7.0: one speed sample per completed step.
366
+ // v1.7.9: the denominator is the DECODE window (first streamed token ->
367
+ // message end), matching the harness's own session stats and 上下文洞察. The
368
+ // first-token wait is kept separately as `ttftSeconds` so the card can show
369
+ // it instead of silently folding it into the rate.
334
370
  if (record.type === 'assistant/message' && usage.output > 0) {
335
371
  const turn = record.data?.turn
336
372
  const step = record.data?.step
337
- const startedAt = Number.isSafeInteger(turn) && Number.isSafeInteger(step)
338
- ? stepStarts.get(`${turn}:${step}`)
339
- : undefined
340
- const elapsedMs = startedAt === undefined ? null : time - startedAt
341
- if (elapsedMs !== null && elapsedMs > 0) {
373
+ const key = Number.isSafeInteger(turn) && Number.isSafeInteger(step) ? `${turn}:${step}` : null
374
+ const startedAt = key === null ? undefined : stepStarts.get(key)
375
+ const firstTokenAt = firstTokenTime(record.data?.stream)
376
+ // A step whose first-token instant is unknown is SKIPPED rather than mixed
377
+ // in with the old step-based denominator: two denominators on one curve
378
+ // would be incomparable. Coverage is 99.99% of steps in practice.
379
+ if (
380
+ startedAt !== undefined &&
381
+ firstTokenAt !== null &&
382
+ firstTokenAt > startedAt &&
383
+ time > firstTokenAt
384
+ ) {
385
+ const decodeSeconds = (time - firstTokenAt) / 1000
386
+ const ttftSeconds = (firstTokenAt - startedAt) / 1000
342
387
  const daySpeed = (speed[day] ??= { providers: Object.create(null) })
343
- const slot = (daySpeed.providers[attributedProvider] ??= { output: 0, seconds: 0, steps: 0 })
388
+ const slot = (daySpeed.providers[attributedProvider] ??= emptySpeedSlot())
344
389
  slot.output += usage.output
345
- slot.seconds += elapsedMs / 1000
390
+ slot.seconds += decodeSeconds
391
+ slot.ttftSeconds += ttftSeconds
346
392
  slot.steps += 1
347
393
  // v1.7.7: the hour bucket reuses `hour`, already computed for the token
348
394
  // histogram, so both drill-downs describe exactly the same slot.
349
395
  const hourSpeed = (speedHours[hour] ??= { providers: Object.create(null) })
350
- const hourSlot = (hourSpeed.providers[attributedProvider] ??= { output: 0, seconds: 0, steps: 0 })
396
+ const hourSlot = (hourSpeed.providers[attributedProvider] ??= emptySpeedSlot())
351
397
  hourSlot.output += usage.output
352
- hourSlot.seconds += elapsedMs / 1000
398
+ hourSlot.seconds += decodeSeconds
399
+ hourSlot.ttftSeconds += ttftSeconds
353
400
  hourSlot.steps += 1
354
- stepStarts.delete(`${turn}:${step}`)
401
+ stepStarts.delete(key)
355
402
  }
356
403
  }
357
404
  const modelKey = `${attributedProvider}/${attributedModel}`
@@ -386,9 +433,10 @@ function mergeFold(aggregate, fold) {
386
433
  for (const [day, daySpeed] of Object.entries(fold.speed ?? {})) {
387
434
  const target = (aggregate.speed[day] ??= { providers: Object.create(null) })
388
435
  for (const [provider, slot] of Object.entries(daySpeed.providers ?? {})) {
389
- const into = (target.providers[provider] ??= { output: 0, seconds: 0, steps: 0 })
436
+ const into = (target.providers[provider] ??= emptySpeedSlot())
390
437
  into.output += slot.output
391
438
  into.seconds += slot.seconds
439
+ into.ttftSeconds += slot.ttftSeconds ?? 0
392
440
  into.steps += slot.steps
393
441
  }
394
442
  }
@@ -396,9 +444,10 @@ function mergeFold(aggregate, fold) {
396
444
  for (const [hour, hourSpeed] of Object.entries(fold.speedHours ?? {})) {
397
445
  const target = (aggregate.speedHours[hour] ??= { providers: Object.create(null) })
398
446
  for (const [provider, slot] of Object.entries(hourSpeed.providers ?? {})) {
399
- const into = (target.providers[provider] ??= { output: 0, seconds: 0, steps: 0 })
447
+ const into = (target.providers[provider] ??= emptySpeedSlot())
400
448
  into.output += slot.output
401
449
  into.seconds += slot.seconds
450
+ into.ttftSeconds += slot.ttftSeconds ?? 0
402
451
  into.steps += slot.steps
403
452
  }
404
453
  }
@@ -588,18 +637,20 @@ export function createUsageStats(input) {
588
637
  for (const [hour, hourSpeed] of Object.entries(entry.speedHours ?? {})) {
589
638
  const target = (speedHoursAggregate[hour] ??= { providers: Object.create(null) })
590
639
  for (const [provider, slot] of Object.entries(hourSpeed.providers ?? {})) {
591
- const into = (target.providers[provider] ??= { output: 0, seconds: 0, steps: 0 })
640
+ const into = (target.providers[provider] ??= emptySpeedSlot())
592
641
  into.output += typeof slot.output === 'number' ? slot.output : 0
593
642
  into.seconds += typeof slot.seconds === 'number' ? slot.seconds : 0
643
+ into.ttftSeconds += typeof slot.ttftSeconds === 'number' ? slot.ttftSeconds : 0
594
644
  into.steps += typeof slot.steps === 'number' ? slot.steps : 0
595
645
  }
596
646
  }
597
647
  for (const [day, daySpeed] of Object.entries(entry.speed ?? {})) {
598
648
  const target = (speedAggregate[day] ??= { providers: Object.create(null) })
599
649
  for (const [provider, slot] of Object.entries(daySpeed.providers ?? {})) {
600
- const into = (target.providers[provider] ??= { output: 0, seconds: 0, steps: 0 })
650
+ const into = (target.providers[provider] ??= emptySpeedSlot())
601
651
  into.output += typeof slot.output === 'number' ? slot.output : 0
602
652
  into.seconds += typeof slot.seconds === 'number' ? slot.seconds : 0
653
+ into.ttftSeconds += typeof slot.ttftSeconds === 'number' ? slot.ttftSeconds : 0
603
654
  into.steps += typeof slot.steps === 'number' ? slot.steps : 0
604
655
  }
605
656
  }
@@ -670,9 +721,10 @@ export function createUsageStats(input) {
670
721
  const dayEntry = (daySpeedHours[dayKey(hourStart)] ??= Object.create(null))
671
722
  for (const [provider, sample] of Object.entries(bucket.providers ?? {})) {
672
723
  if (sample === null || typeof sample !== 'object') continue
673
- const series = (dayEntry[provider] ??= Array.from({ length: 24 }, () => ({ output: 0, seconds: 0, steps: 0 })))
724
+ const series = (dayEntry[provider] ??= Array.from({ length: 24 }, () => emptySpeedSlot()))
674
725
  series[slot].output += typeof sample.output === 'number' ? sample.output : 0
675
726
  series[slot].seconds += typeof sample.seconds === 'number' ? sample.seconds : 0
727
+ series[slot].ttftSeconds += typeof sample.ttftSeconds === 'number' ? sample.ttftSeconds : 0
676
728
  series[slot].steps += typeof sample.steps === 'number' ? sample.steps : 0
677
729
  }
678
730
  }
@@ -1436,25 +1488,31 @@ export function createUsageStats(input) {
1436
1488
  const byDay = []
1437
1489
  let output = 0
1438
1490
  let seconds = 0
1491
+ let ttftSeconds = 0
1439
1492
  let steps = 0
1440
1493
  for (const day of Object.keys(source).sort()) {
1441
1494
  if (day < fromDay) continue
1442
1495
  let dayOutput = 0
1443
1496
  let daySeconds = 0
1497
+ let dayTtftSeconds = 0
1444
1498
  let daySteps = 0
1445
1499
  for (const [id, slot] of Object.entries(source[day].providers ?? {})) {
1446
1500
  if (providerId !== null && id !== providerId) continue
1447
1501
  dayOutput += slot.output
1448
1502
  daySeconds += slot.seconds
1503
+ dayTtftSeconds += typeof slot.ttftSeconds === 'number' ? slot.ttftSeconds : 0
1449
1504
  daySteps += slot.steps
1450
1505
  }
1451
1506
  if (daySeconds <= 0 || dayOutput <= 0) continue
1452
1507
  const entry = {
1453
1508
  day,
1454
1509
  outputTokens: dayOutput,
1510
+ // v1.7.9: the DECODE window (first streamed token -> message end).
1455
1511
  seconds: Math.round(daySeconds * 10) / 10,
1456
1512
  tps: Math.round((dayOutput / daySeconds) * 10) / 10,
1457
1513
  steps: daySteps,
1514
+ // The prefill/queue wait this rate excludes, so the card can show it.
1515
+ ttftSeconds: Math.round(dayTtftSeconds * 10) / 10,
1458
1516
  }
1459
1517
  // v1.7.7: the day's 24 local-hour slots, for the click-to-drill-down
1460
1518
  // chart. Only a provider-scoped series carries them: the combined series
@@ -1470,6 +1528,8 @@ export function createUsageStats(input) {
1470
1528
  seconds: Math.round(cell.seconds * 10) / 10,
1471
1529
  tps: Math.round((cell.output / cell.seconds) * 10) / 10,
1472
1530
  steps: cell.steps,
1531
+ ttftSeconds:
1532
+ typeof cell.ttftSeconds === 'number' ? Math.round(cell.ttftSeconds * 10) / 10 : 0,
1473
1533
  }
1474
1534
  })
1475
1535
  }
@@ -1477,6 +1537,7 @@ export function createUsageStats(input) {
1477
1537
  byDay.push(entry)
1478
1538
  output += dayOutput
1479
1539
  seconds += daySeconds
1540
+ ttftSeconds += dayTtftSeconds
1480
1541
  steps += daySteps
1481
1542
  }
1482
1543
  return {
@@ -1486,6 +1547,7 @@ export function createUsageStats(input) {
1486
1547
  seconds: Math.round(seconds * 10) / 10,
1487
1548
  tps: seconds > 0 ? Math.round((output / seconds) * 10) / 10 : null,
1488
1549
  steps,
1550
+ ttftSeconds: Math.round(ttftSeconds * 10) / 10,
1489
1551
  },
1490
1552
  }
1491
1553
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-tokenlens",
3
- "version": "1.7.8",
3
+ "version": "1.7.9",
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",