dsh-tokenlens 1.7.7 → 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 +23 -0
- package/PROTOCOL.md +15 -11
- package/client.js +30 -2
- package/index.js +1 -1
- package/lib/usage-stats.mjs +78 -16
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,29 @@
|
|
|
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
|
+
|
|
21
|
+
## 1.7.8 — 2026-11-10
|
|
22
|
+
|
|
23
|
+
(当日已发布版本:v1.7.7 / v1.7.8)
|
|
24
|
+
|
|
25
|
+
- **fix(ui): 让 TPS 的小时级下钻可被发现**——v1.7.7 加了「点曲线上的某一天 → 该日 24 小时」的交互,但日视图提示只写了计算口径,没有任何地方告诉用户可以点。一个没人发现的交互等于不存在,这也是它「看起来没必要」的真正原因。现在提示末尾明确写出「点击曲线上的某一天可查看该日的小时级速度」
|
|
26
|
+
- 依据实测(最近 14 天会话日志):每天「有样本的小时数」中位数 **9 / 24**,只有 1 天 ≤4 小时,所以小时视图不是稀疏噪声,每个小时背后通常有上百个 step 支撑,聚合后具备统计意义
|
|
27
|
+
- 小时级数据的诊断价值有实例:`2026-10-23` 日均 30.5 tok/s 看似正常,但 14 点那一小时只有 0.8 tok/s(2 个 step 几乎卡死)——日均视图无法暴露这类日内限速
|
|
28
|
+
- 测试:377(新增断言:日视图提示必须写明可点击下钻)
|
|
29
|
+
|
|
7
30
|
## 1.7.7 — 2026-11-10
|
|
8
31
|
|
|
9
32
|
- **feat(ui): TPS 卡片锁定「当前供应商」**——不再跟随上方「当前供应商 / 全部 Harness」切换。这张卡片回答的是「我现在用的模型有多快」,所以始终使用所选供应商自己的日平均序列;没有供应商行时显示空态说明
|
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.
|
|
387
|
-
//
|
|
388
|
-
// `assistant/message`),
|
|
389
|
-
//
|
|
390
|
-
//
|
|
391
|
-
//
|
|
392
|
-
//
|
|
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":
|
|
396
|
-
"tps":
|
|
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":
|
|
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
|
@@ -229,7 +229,9 @@ window.__ModuleLoader__.load({
|
|
|
229
229
|
statsDshTotalLabel: 'DSH 合计',
|
|
230
230
|
statsLast30Days: '最近30天',
|
|
231
231
|
statsSpeedTitle: '输出速度(TPS)',
|
|
232
|
-
statsSpeedHint:
|
|
232
|
+
statsSpeedHint:
|
|
233
|
+
'输出 Token ÷ 纯解码时长(首个 token 到达 → 消息结束),与 DSH 会话统计同口径;首 token 等待单独列出。点击曲线上的某一天可查看该日的小时级速度',
|
|
234
|
+
statsSpeedTtft: '首 token 均值 {seconds}s',
|
|
233
235
|
statsSpeedTotal: '均值 {tps} tok/s',
|
|
234
236
|
statsSpeedPeak: '峰值 {tps} tok/s',
|
|
235
237
|
statsSpeedEmpty: '暂无可计算的速度样本(需要 step 计时数据)',
|
|
@@ -512,7 +514,8 @@ window.__ModuleLoader__.load({
|
|
|
512
514
|
statsLast30Days: 'Last 30 days',
|
|
513
515
|
statsSpeedTitle: 'Output speed (TPS)',
|
|
514
516
|
statsSpeedHint:
|
|
515
|
-
'Output tokens ÷
|
|
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',
|
|
516
519
|
statsSpeedTotal: 'avg {tps} tok/s',
|
|
517
520
|
statsSpeedPeak: 'peak {tps} tok/s',
|
|
518
521
|
statsSpeedEmpty: 'No speed samples yet (needs step timing data)',
|
|
@@ -1637,6 +1640,7 @@ window.__ModuleLoader__.load({
|
|
|
1637
1640
|
seconds: numberOrNull(cell.seconds) ?? 0,
|
|
1638
1641
|
tps,
|
|
1639
1642
|
steps: numberOrNull(cell.steps) ?? 0,
|
|
1643
|
+
ttftSeconds: numberOrNull(cell.ttftSeconds) ?? 0,
|
|
1640
1644
|
}
|
|
1641
1645
|
})
|
|
1642
1646
|
: null
|
|
@@ -1646,6 +1650,8 @@ window.__ModuleLoader__.load({
|
|
|
1646
1650
|
seconds: numberOrNull(entry.seconds) ?? 0,
|
|
1647
1651
|
tps: numberOrNull(entry.tps),
|
|
1648
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,
|
|
1649
1655
|
hours: hours === null ? null : hours.slice(0, 24),
|
|
1650
1656
|
}
|
|
1651
1657
|
})
|
|
@@ -1659,6 +1665,7 @@ window.__ModuleLoader__.load({
|
|
|
1659
1665
|
seconds: numberOrNull(totalRaw.seconds) ?? 0,
|
|
1660
1666
|
tps: numberOrNull(totalRaw.tps),
|
|
1661
1667
|
steps: numberOrNull(totalRaw.steps) ?? 0,
|
|
1668
|
+
ttftSeconds: numberOrNull(totalRaw.ttftSeconds) ?? 0,
|
|
1662
1669
|
},
|
|
1663
1670
|
}
|
|
1664
1671
|
}
|
|
@@ -3678,6 +3685,8 @@ window.__ModuleLoader__.load({
|
|
|
3678
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}
|
|
3679
3686
|
.dsh-tokenlens-repo:hover,.dsh-tokenlens-repo:focus-visible{color:var(--dsw-alias-label-primary,currentColor);opacity:1;text-decoration:underline}
|
|
3680
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}
|
|
3681
3690
|
/* Update status on the plugin-version line (dialog body): wraps on narrow screens. */
|
|
3682
3691
|
.dsh-tokenlens-version-update{white-space:normal;overflow-wrap:anywhere}
|
|
3683
3692
|
.dsh-tokenlens-version .dsh-tokenlens-update-link{color:inherit;font-size:inherit;vertical-align:baseline}
|
|
@@ -6238,6 +6247,21 @@ window.__ModuleLoader__.load({
|
|
|
6238
6247
|
'span',
|
|
6239
6248
|
{ className: 'dsh-tokenlens-hint', 'data-speed-total': String(series.total.tps) },
|
|
6240
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,
|
|
6241
6265
|
)
|
|
6242
6266
|
: null
|
|
6243
6267
|
: h(
|
|
@@ -6436,6 +6460,10 @@ window.__ModuleLoader__.load({
|
|
|
6436
6460
|
tokens: formatTokensPrecise(hovered.outputTokens),
|
|
6437
6461
|
seconds: hovered.seconds.toFixed(1),
|
|
6438
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,
|
|
6439
6467
|
)
|
|
6440
6468
|
const axis = h(
|
|
6441
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.
|
|
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).
|
package/lib/usage-stats.mjs
CHANGED
|
@@ -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 =
|
|
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
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
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] ??=
|
|
388
|
+
const slot = (daySpeed.providers[attributedProvider] ??= emptySpeedSlot())
|
|
344
389
|
slot.output += usage.output
|
|
345
|
-
slot.seconds +=
|
|
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] ??=
|
|
396
|
+
const hourSlot = (hourSpeed.providers[attributedProvider] ??= emptySpeedSlot())
|
|
351
397
|
hourSlot.output += usage.output
|
|
352
|
-
hourSlot.seconds +=
|
|
398
|
+
hourSlot.seconds += decodeSeconds
|
|
399
|
+
hourSlot.ttftSeconds += ttftSeconds
|
|
353
400
|
hourSlot.steps += 1
|
|
354
|
-
stepStarts.delete(
|
|
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] ??=
|
|
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] ??=
|
|
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] ??=
|
|
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] ??=
|
|
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 }, () => (
|
|
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.
|
|
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",
|