dsh-tokenlens 1.7.6 → 1.7.8
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 +20 -0
- package/PROTOCOL.md +8 -2
- package/README.en.md +1 -1
- package/README.md +1 -1
- package/client.js +333 -196
- package/index.js +1 -1
- package/lib/usage-stats.mjs +82 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,26 @@
|
|
|
4
4
|
|
|
5
5
|
**版本记录约定**:同一发布日只保留一个章节,标题取当日**最后一个**已发布版本号;当日多次发布的改动合并到该章节,并在标题下注明当日实际发布过的版本号(npm 上仍可安装这些独立版本)。若当日已有发布,后续小改动不再单独增加版本号,而是并入下一篇日志。这样日志按天规整、不出现同日多个小版本。
|
|
6
6
|
|
|
7
|
+
## 1.7.8 — 2026-11-10
|
|
8
|
+
|
|
9
|
+
(当日已发布版本:v1.7.7 / v1.7.8)
|
|
10
|
+
|
|
11
|
+
- **fix(ui): 让 TPS 的小时级下钻可被发现**——v1.7.7 加了「点曲线上的某一天 → 该日 24 小时」的交互,但日视图提示只写了计算口径,没有任何地方告诉用户可以点。一个没人发现的交互等于不存在,这也是它「看起来没必要」的真正原因。现在提示末尾明确写出「点击曲线上的某一天可查看该日的小时级速度」
|
|
12
|
+
- 依据实测(最近 14 天会话日志):每天「有样本的小时数」中位数 **9 / 24**,只有 1 天 ≤4 小时,所以小时视图不是稀疏噪声,每个小时背后通常有上百个 step 支撑,聚合后具备统计意义
|
|
13
|
+
- 小时级数据的诊断价值有实例:`2026-10-23` 日均 30.5 tok/s 看似正常,但 14 点那一小时只有 0.8 tok/s(2 个 step 几乎卡死)——日均视图无法暴露这类日内限速
|
|
14
|
+
- 测试:377(新增断言:日视图提示必须写明可点击下钻)
|
|
15
|
+
|
|
16
|
+
## 1.7.7 — 2026-11-10
|
|
17
|
+
|
|
18
|
+
- **feat(ui): TPS 卡片锁定「当前供应商」**——不再跟随上方「当前供应商 / 全部 Harness」切换。这张卡片回答的是「我现在用的模型有多快」,所以始终使用所选供应商自己的日平均序列;没有供应商行时显示空态说明
|
|
19
|
+
- **feat(ui): 点曲线上的点可下钻当日小时级 TPS**——与 token 柱状图的「点某天 → 24 小时」交互一致:点击后曲线切换为该日 24 个本地小时的速率曲线,标题栏显示「2026-11-05 · 小时级」并提供「返回日视图」;再点同一天也会切回日视图
|
|
20
|
+
- 无样本的小时是 `null` 而不是 0,曲线在该处**断开**而不是跌到零(跌到零是假信息)
|
|
21
|
+
- 小时级读数同样是「输出 Token ÷ 模型自身耗时」,悬停显示 `09:00 · 120.0 tok/s(600 输出 ÷ 5.0s)`
|
|
22
|
+
- 该日没有任何小时级样本时显示说明文字,不画空图
|
|
23
|
+
- **feat(stats): Host 侧新增按小时的 TPS 采样**——`foldSession` 在记录每日速率的同时把样本写入按 epoch 小时分桶的 `speedHours`,聚合层再换算成本地 24 槽 `speedDayHours`,随 `speed.byDay[].hours` 下发(仅 provider 维度,合计序列不带)。缓存 schema 升至 7,首次会重扫
|
|
24
|
+
- 文档:`PROTOCOL.md` 补上 `hours` 结构与「null 表示无样本」的约定
|
|
25
|
+
- 测试:373 → 377(TPS 卡片不受作用域切换影响、点柱下钻与返回、无样本下钻的空态、按小时分桶与 null 槽位)
|
|
26
|
+
|
|
7
27
|
## 1.7.6 — 2026-11-06
|
|
8
28
|
|
|
9
29
|
(当日已发布版本:v1.6.5 / v1.6.6 / v1.6.7 / v1.7.0 / v1.7.1 / v1.7.2 / v1.7.3 / v1.7.4 / v1.7.5 / v1.7.6)
|
package/PROTOCOL.md
CHANGED
|
@@ -324,7 +324,8 @@ folder basename, used by the client to match a sidebar hover card; the full
|
|
|
324
324
|
`cwd` is Host-only and never sent on the wire), the rolling 5-hour local window
|
|
325
325
|
(`fiveHours`, DSH-only because external harnesses are day-granular), and the
|
|
326
326
|
bounded `daily[]` series used by the chart view, the `speed` series behind the
|
|
327
|
-
TPS trend (v1.7.0
|
|
327
|
+
TPS trend (v1.7.0, with 24 local-hour slots per day for the drill-down in
|
|
328
|
+
v1.7.7), and the subscription window (`effectiveDay` is the plan's
|
|
328
329
|
period start when the Host learned it, and `sinceEffective` is summed from the
|
|
329
330
|
per-day buckets inside `[window start, today]` rather than borrowed from the
|
|
330
331
|
30-day or all-time bucket — v1.7.2). Absolute local paths are
|
|
@@ -392,7 +393,12 @@ with `[path]`.
|
|
|
392
393
|
"speed": {
|
|
393
394
|
"byDay": [
|
|
394
395
|
{ "day": "2026-11-05", "outputTokens": 516951, "seconds": 8493.1,
|
|
395
|
-
"tps": 60.9, "steps": 859
|
|
396
|
+
"tps": 60.9, "steps": 859,
|
|
397
|
+
// v1.7.7: 24 local-hour slots for the click-to-drill-down view. A slot
|
|
398
|
+
// with no sample is null rather than a zero, so the curve breaks
|
|
399
|
+
// instead of dipping to zero. Only provider-scoped series carry it.
|
|
400
|
+
"hours": [ null, null, …, { "outputTokens": 600, "seconds": 5,
|
|
401
|
+
"tps": 120, "steps": 1 }, … ] }
|
|
396
402
|
],
|
|
397
403
|
"total": { "outputTokens": 939643, "seconds": 14123.9, "tps": 66.5, "steps": 1424 }
|
|
398
404
|
},
|
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
|
|
147
|
+
Node.js ≥ 24, zero runtime dependencies: `node --test` runs the 377 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`:**
|
|
448
|
+
- `node --test`:**377 个自动化用例**,覆盖 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
|
@@ -229,10 +229,17 @@ window.__ModuleLoader__.load({
|
|
|
229
229
|
statsDshTotalLabel: 'DSH 合计',
|
|
230
230
|
statsLast30Days: '最近30天',
|
|
231
231
|
statsSpeedTitle: '输出速度(TPS)',
|
|
232
|
-
statsSpeedHint:
|
|
232
|
+
statsSpeedHint:
|
|
233
|
+
'输出 Token ÷ 模型自身耗时(step 开始到消息返回);不含工具调用等待,但输出含思考 Token。点击曲线上的某一天可查看该日的小时级速度',
|
|
233
234
|
statsSpeedTotal: '均值 {tps} tok/s',
|
|
234
235
|
statsSpeedPeak: '峰值 {tps} tok/s',
|
|
235
236
|
statsSpeedEmpty: '暂无可计算的速度样本(需要 step 计时数据)',
|
|
237
|
+
statsSpeedHourTitle: '{date} · 小时级',
|
|
238
|
+
statsSpeedHourHint: '同一天按本地小时切分;仍是输出 Token ÷ 模型自身耗时,无样本的小时留空',
|
|
239
|
+
statsSpeedHourEmpty: '该日没有可下钻的小时级样本',
|
|
240
|
+
statsSpeedHourBarTitle: '{hour} · {tps} tok/s({tokens} 输出 ÷ {seconds}s)',
|
|
241
|
+
statsSpeedHourAria: '{date} 的小时级输出速度',
|
|
242
|
+
statsSpeedBack: '返回日视图',
|
|
236
243
|
statsSpeedBarTitle: '{date} · {tps} tok/s({tokens} 输出 ÷ {seconds}s)',
|
|
237
244
|
statsSpeedDetail: '{tokens} 输出 ÷ {seconds}s',
|
|
238
245
|
statsLast7Days: '最近7天',
|
|
@@ -506,10 +513,17 @@ window.__ModuleLoader__.load({
|
|
|
506
513
|
statsLast30Days: 'Last 30 days',
|
|
507
514
|
statsSpeedTitle: 'Output speed (TPS)',
|
|
508
515
|
statsSpeedHint:
|
|
509
|
-
'Output tokens ÷ the model’s own request time (step start to message); tool waits excluded, reasoning tokens included',
|
|
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',
|
|
510
517
|
statsSpeedTotal: 'avg {tps} tok/s',
|
|
511
518
|
statsSpeedPeak: 'peak {tps} tok/s',
|
|
512
519
|
statsSpeedEmpty: 'No speed samples yet (needs step timing data)',
|
|
520
|
+
statsSpeedHourTitle: '{date} · hourly',
|
|
521
|
+
statsSpeedHourHint:
|
|
522
|
+
'The same day split by local hour; still output tokens ÷ model time, hours without samples stay empty',
|
|
523
|
+
statsSpeedHourEmpty: 'No hourly samples to drill into for this day',
|
|
524
|
+
statsSpeedHourBarTitle: '{hour} · {tps} tok/s ({tokens} output ÷ {seconds}s)',
|
|
525
|
+
statsSpeedHourAria: 'Hourly output speed for {date}',
|
|
526
|
+
statsSpeedBack: 'Back to daily',
|
|
513
527
|
statsSpeedBarTitle: '{date} · {tps} tok/s ({tokens} output ÷ {seconds}s)',
|
|
514
528
|
statsSpeedDetail: '{tokens} output ÷ {seconds}s',
|
|
515
529
|
statsLast7Days: 'Last 7 days',
|
|
@@ -1611,13 +1625,31 @@ window.__ModuleLoader__.load({
|
|
|
1611
1625
|
const byDay = Array.isArray(raw.byDay)
|
|
1612
1626
|
? raw.byDay
|
|
1613
1627
|
.filter((entry) => entry !== null && typeof entry === 'object' && typeof entry.day === 'string')
|
|
1614
|
-
.map((entry) =>
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1628
|
+
.map((entry) => {
|
|
1629
|
+
// v1.7.7: the day's 24 local-hour slots (null where there is no
|
|
1630
|
+
// sample), used by the click-to-drill-down hourly view.
|
|
1631
|
+
const hours = Array.isArray(entry.hours)
|
|
1632
|
+
? entry.hours.map((cell) => {
|
|
1633
|
+
if (cell === null || typeof cell !== 'object') return null
|
|
1634
|
+
const tps = numberOrNull(cell.tps)
|
|
1635
|
+
if (tps === null || tps < 0) return null
|
|
1636
|
+
return {
|
|
1637
|
+
outputTokens: numberOrNull(cell.outputTokens) ?? 0,
|
|
1638
|
+
seconds: numberOrNull(cell.seconds) ?? 0,
|
|
1639
|
+
tps,
|
|
1640
|
+
steps: numberOrNull(cell.steps) ?? 0,
|
|
1641
|
+
}
|
|
1642
|
+
})
|
|
1643
|
+
: null
|
|
1644
|
+
return {
|
|
1645
|
+
day: entry.day,
|
|
1646
|
+
outputTokens: numberOrNull(entry.outputTokens) ?? 0,
|
|
1647
|
+
seconds: numberOrNull(entry.seconds) ?? 0,
|
|
1648
|
+
tps: numberOrNull(entry.tps),
|
|
1649
|
+
steps: numberOrNull(entry.steps) ?? 0,
|
|
1650
|
+
hours: hours === null ? null : hours.slice(0, 24),
|
|
1651
|
+
}
|
|
1652
|
+
})
|
|
1621
1653
|
.filter((entry) => entry.tps !== null && entry.tps >= 0)
|
|
1622
1654
|
: []
|
|
1623
1655
|
const totalRaw = raw.total !== null && typeof raw.total === 'object' ? raw.total : {}
|
|
@@ -2107,6 +2139,9 @@ window.__ModuleLoader__.load({
|
|
|
2107
2139
|
chartHover: null,
|
|
2108
2140
|
// v1.7.2: the hovered point of the output-speed (TPS) curve.
|
|
2109
2141
|
speedHover: null,
|
|
2142
|
+
// v1.7.7: the day whose 24 local hours the TPS card charts, or null
|
|
2143
|
+
// (daily trend). Clicking a point on the curve toggles it.
|
|
2144
|
+
speedDay: null,
|
|
2110
2145
|
// v1.2.0 chart drill-down: the clicked day whose 0..23 hour histogram
|
|
2111
2146
|
// is charted instead of the daily trend, or null (daily trend).
|
|
2112
2147
|
chartDay: null,
|
|
@@ -2219,6 +2254,9 @@ window.__ModuleLoader__.load({
|
|
|
2219
2254
|
// v1.7.6: the TPS hover index is view state like the chart's, so it must
|
|
2220
2255
|
// not survive a provider switch (the series behind it changed).
|
|
2221
2256
|
speedHover: null,
|
|
2257
|
+
// v1.7.7: an hourly TPS drill-down belongs to one provider's series, so a
|
|
2258
|
+
// provider switch returns to the daily trend.
|
|
2259
|
+
speedDay: null,
|
|
2222
2260
|
}
|
|
2223
2261
|
}
|
|
2224
2262
|
|
|
@@ -3503,6 +3541,10 @@ window.__ModuleLoader__.load({
|
|
|
3503
3541
|
.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}
|
|
3504
3542
|
.dsh-tokenlens-speed-plot{position:relative;flex:1 1 auto;min-width:0}
|
|
3505
3543
|
.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}
|
|
3544
|
+
/* v1.7.7: the daily curve is clickable (drills into that day's hours). */
|
|
3545
|
+
.dsh-tokenlens-speed-svg[data-speed-mode=day] rect[data-speed-day]{cursor:pointer}
|
|
3546
|
+
.dsh-tokenlens-link-button{border:0;background:none;padding:0 0 0 6px;color:var(--dsw-alias-label-secondary,currentColor);font:inherit;font-size:11px;line-height:inherit;text-decoration:underline;cursor:pointer}
|
|
3547
|
+
.dsh-tokenlens-link-button:hover,.dsh-tokenlens-link-button:focus-visible{color:var(--dsw-alias-label-primary,currentColor)}
|
|
3506
3548
|
.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}
|
|
3507
3549
|
.dsh-tokenlens-speed-axis>span{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
3508
3550
|
@media (max-width:680px){.dsh-tokenlens-chart-layout-row>*{flex-basis:100%!important}}
|
|
@@ -4729,6 +4771,24 @@ window.__ModuleLoader__.load({
|
|
|
4729
4771
|
if (store.get().speedHover !== next) store.set({ speedHover: next })
|
|
4730
4772
|
}
|
|
4731
4773
|
|
|
4774
|
+
/**
|
|
4775
|
+
* Drill the TPS card into one day's 24 local hours (v1.7.7), or back out.
|
|
4776
|
+
*
|
|
4777
|
+
* Toggling mirrors the token chart's `activateChartDay`: clicking the already
|
|
4778
|
+
* active day returns to the daily trend, so no separate "back" button is
|
|
4779
|
+
* needed — but one is still offered in the header for discoverability.
|
|
4780
|
+
*/
|
|
4781
|
+
function activateSpeedDay(day) {
|
|
4782
|
+
if (typeof day !== 'string' || day === '') return
|
|
4783
|
+
const next = store.get().speedDay === day ? null : day
|
|
4784
|
+
store.set({ speedDay: next, speedHover: null })
|
|
4785
|
+
}
|
|
4786
|
+
|
|
4787
|
+
/** Leave the hourly TPS drill-down and chart the daily trend again (v1.7.7). */
|
|
4788
|
+
function clearSpeedDay() {
|
|
4789
|
+
if (store.get().speedDay !== null) store.set({ speedDay: null, speedHover: null })
|
|
4790
|
+
}
|
|
4791
|
+
|
|
4732
4792
|
/** The active today bucket of the current stats scope (v0.27.2). */
|
|
4733
4793
|
function todayBucketOf(ctx) {
|
|
4734
4794
|
return ctx.providerScoped ? ctx.currentRow?.today ?? null : ctx.stats?.today ?? null
|
|
@@ -6136,17 +6196,33 @@ window.__ModuleLoader__.load({
|
|
|
6136
6196
|
}
|
|
6137
6197
|
|
|
6138
6198
|
/**
|
|
6139
|
-
* The output-speed (TPS) trend card (v1.7.0
|
|
6199
|
+
* The output-speed (TPS) trend card (v1.7.0, reworked in v1.7.7).
|
|
6200
|
+
*
|
|
6201
|
+
* Always the CURRENT provider's series (v1.7.7): the user reads this card as
|
|
6202
|
+
* "how fast is the model I am using right now", so it deliberately ignores
|
|
6203
|
+
* the 当前供应商 / 全部 Harness switch above it.
|
|
6140
6204
|
*
|
|
6141
6205
|
* `tps` is output tokens over the model's OWN request time (`step/start` to
|
|
6142
|
-
* `assistant/message`), so
|
|
6206
|
+
* `assistant/message`), so a step's tool calls do not dilute it — but
|
|
6143
6207
|
* reasoning tokens are included in "output", which the hint says.
|
|
6208
|
+
*
|
|
6209
|
+
* Clicking a point drills into that day's 24 local hours (v1.7.7), the same
|
|
6210
|
+
* interaction the token chart uses for its daily bars.
|
|
6144
6211
|
*/
|
|
6145
6212
|
function buildSpeedSection(state, ctx, t) {
|
|
6146
|
-
|
|
6147
|
-
const
|
|
6148
|
-
const series = scoped === null || scoped === undefined ? null : scoped
|
|
6213
|
+
// v1.7.7: always provider-scoped, whatever the scope tabs say.
|
|
6214
|
+
const series = ctx.currentRow?.speed ?? null
|
|
6149
6215
|
const byDay = series === null ? [] : series.byDay
|
|
6216
|
+
const drilledDay = typeof state.speedDay === 'string' ? state.speedDay : null
|
|
6217
|
+
const drilled = drilledDay === null ? null : byDay.find((entry) => entry.day === drilledDay) ?? null
|
|
6218
|
+
// v1.7.7: being drilled into a day is a MODE, independent of whether that
|
|
6219
|
+
// day actually carries hourly samples — otherwise a day without them would
|
|
6220
|
+
// silently fall back to the daily chart instead of saying so.
|
|
6221
|
+
const hourlyMode = drilledDay !== null && drilled !== null
|
|
6222
|
+
const hourly = hourlyMode && Array.isArray(drilled.hours) ? drilled.hours : null
|
|
6223
|
+
const pointsSeries = hourlyMode ? hourly ?? [] : byDay
|
|
6224
|
+
const dailyMode = !hourlyMode
|
|
6225
|
+
|
|
6150
6226
|
const head = h(
|
|
6151
6227
|
'div',
|
|
6152
6228
|
{ key: 'head', className: 'dsh-tokenlens-section-head' },
|
|
@@ -6156,205 +6232,266 @@ window.__ModuleLoader__.load({
|
|
|
6156
6232
|
Icon('chart', 14),
|
|
6157
6233
|
t('statsSpeedTitle'),
|
|
6158
6234
|
),
|
|
6159
|
-
|
|
6160
|
-
|
|
6161
|
-
|
|
6162
|
-
|
|
6163
|
-
|
|
6164
|
-
|
|
6165
|
-
|
|
6166
|
-
)
|
|
6167
|
-
const body =
|
|
6168
|
-
byDay.length === 0
|
|
6169
|
-
? h(
|
|
6170
|
-
'div',
|
|
6171
|
-
{ key: 'empty', className: 'dsh-tokenlens-chart-empty', 'data-speed-empty': 'true' },
|
|
6172
|
-
h('span', { key: 'hint', className: 'dsh-tokenlens-hint' }, t('statsSpeedEmpty')),
|
|
6173
|
-
)
|
|
6174
|
-
: (() => {
|
|
6175
|
-
// v1.7.1: a CURVE, not bars — the trend is a rate, and a line reads
|
|
6176
|
-
// it far better than per-day columns. The SVG scales to the card
|
|
6177
|
-
// width; `preserveAspectRatio="none"` lets it fill without keeping
|
|
6178
|
-
// the viewBox ratio, and the per-day hover targets are transparent
|
|
6179
|
-
// columns so nothing is distorted.
|
|
6180
|
-
const width = 320
|
|
6181
|
-
const height = 90
|
|
6182
|
-
const pad = 4
|
|
6183
|
-
const max = Math.max(...byDay.map((entry) => entry.tps ?? 0), 1)
|
|
6184
|
-
const stepX = byDay.length > 1 ? (width - pad * 2) / (byDay.length - 1) : 0
|
|
6185
|
-
const points = byDay.map((entry, index) => {
|
|
6186
|
-
const x = byDay.length > 1 ? pad + stepX * index : width / 2
|
|
6187
|
-
const y = height - pad - ((entry.tps ?? 0) / max) * (height - pad * 2)
|
|
6188
|
-
return { x, y }
|
|
6189
|
-
})
|
|
6190
|
-
const line = points.map((point) => `${point.x.toFixed(1)},${point.y.toFixed(1)}`).join(' ')
|
|
6191
|
-
const area = `${pad.toFixed(1)},${(height - pad).toFixed(1)} ${line} ${(width - pad).toFixed(1)},${(height - pad).toFixed(1)}`
|
|
6192
|
-
const columnWidth = byDay.length > 1 ? stepX : width - pad * 2
|
|
6193
|
-
const columns = byDay.map((entry, index) =>
|
|
6194
|
-
h(
|
|
6195
|
-
'rect',
|
|
6196
|
-
{
|
|
6197
|
-
key: `hit-${entry.day}-${index}`,
|
|
6198
|
-
// v1.7.6: clamp the hit column into the viewBox — a single-day
|
|
6199
|
-
// series used to start at a negative x and lose half its area.
|
|
6200
|
-
x: Math.max(0, Math.min(width - 1, points[index].x - columnWidth / 2)).toFixed(1),
|
|
6201
|
-
y: 0,
|
|
6202
|
-
width: Math.max(1, Math.min(columnWidth, width)).toFixed(1),
|
|
6203
|
-
height: String(height),
|
|
6204
|
-
fill: 'transparent',
|
|
6205
|
-
'data-speed-day': entry.day,
|
|
6206
|
-
'data-speed-tps': String(entry.tps ?? 0),
|
|
6207
|
-
},
|
|
6208
|
-
h(
|
|
6209
|
-
'title',
|
|
6210
|
-
{ key: 'tip' },
|
|
6211
|
-
t('statsSpeedBarTitle', {
|
|
6212
|
-
date: entry.day,
|
|
6213
|
-
tps: (entry.tps ?? 0).toFixed(1),
|
|
6214
|
-
tokens: formatTokensPrecise(entry.outputTokens),
|
|
6215
|
-
seconds: entry.seconds.toFixed(1),
|
|
6216
|
-
}),
|
|
6217
|
-
),
|
|
6218
|
-
),
|
|
6235
|
+
// While drilled in, the header says which day and offers a way back.
|
|
6236
|
+
hourly === null
|
|
6237
|
+
? series !== null && series.total.tps !== null
|
|
6238
|
+
? h(
|
|
6239
|
+
'span',
|
|
6240
|
+
{ className: 'dsh-tokenlens-hint', 'data-speed-total': String(series.total.tps) },
|
|
6241
|
+
t('statsSpeedTotal', { tps: series.total.tps.toFixed(1) }),
|
|
6219
6242
|
)
|
|
6220
|
-
|
|
6221
|
-
|
|
6243
|
+
: null
|
|
6244
|
+
: h(
|
|
6245
|
+
'span',
|
|
6246
|
+
{ key: 'drill', className: 'dsh-tokenlens-hint', 'data-speed-drill': drilled.day },
|
|
6247
|
+
t('statsSpeedHourTitle', { date: drilled.day }),
|
|
6248
|
+
h(
|
|
6249
|
+
'button',
|
|
6222
6250
|
{
|
|
6223
|
-
key: '
|
|
6224
|
-
|
|
6225
|
-
'
|
|
6226
|
-
|
|
6227
|
-
|
|
6228
|
-
role: 'img',
|
|
6229
|
-
'aria-label': t('statsSpeedTitle'),
|
|
6251
|
+
key: 'back',
|
|
6252
|
+
type: 'button',
|
|
6253
|
+
className: 'dsh-tokenlens-link-button',
|
|
6254
|
+
'data-speed-back': 'true',
|
|
6255
|
+
onClick: () => clearSpeedDay(),
|
|
6230
6256
|
},
|
|
6231
|
-
|
|
6232
|
-
|
|
6257
|
+
t('statsSpeedBack'),
|
|
6258
|
+
),
|
|
6259
|
+
),
|
|
6260
|
+
)
|
|
6261
|
+
|
|
6262
|
+
const plotOf = () => {
|
|
6263
|
+
if (pointsSeries.length === 0) {
|
|
6264
|
+
return h(
|
|
6265
|
+
'div',
|
|
6266
|
+
{ key: 'empty', className: 'dsh-tokenlens-chart-empty', 'data-speed-empty': 'true' },
|
|
6267
|
+
h('span', { key: 'hint', className: 'dsh-tokenlens-hint' }, t('statsSpeedEmpty')),
|
|
6268
|
+
)
|
|
6269
|
+
}
|
|
6270
|
+
// The SVG scales to the card width; `preserveAspectRatio="none"` lets it
|
|
6271
|
+
// fill without keeping the viewBox ratio, and the hover targets are
|
|
6272
|
+
// transparent columns so nothing is distorted.
|
|
6273
|
+
const width = 320
|
|
6274
|
+
const height = 90
|
|
6275
|
+
const pad = 4
|
|
6276
|
+
const real = pointsSeries.filter((entry) => entry !== null)
|
|
6277
|
+
const max = Math.max(...real.map((entry) => entry.tps ?? 0), 1)
|
|
6278
|
+
const count = pointsSeries.length
|
|
6279
|
+
const stepX = count > 1 ? (width - pad * 2) / (count - 1) : 0
|
|
6280
|
+
// A null slot (an hour with no sample) breaks the line instead of being
|
|
6281
|
+
// drawn as a dip to zero, which would be a lie.
|
|
6282
|
+
let segment = []
|
|
6283
|
+
const segments = []
|
|
6284
|
+
pointsSeries.forEach((entry, index) => {
|
|
6285
|
+
if (entry === null) {
|
|
6286
|
+
if (segment.length > 0) segments.push(segment)
|
|
6287
|
+
segment = []
|
|
6288
|
+
return
|
|
6289
|
+
}
|
|
6290
|
+
const x = count > 1 ? pad + stepX * index : width / 2
|
|
6291
|
+
const y = height - pad - ((entry.tps ?? 0) / max) * (height - pad * 2)
|
|
6292
|
+
segment.push({ x, y })
|
|
6293
|
+
})
|
|
6294
|
+
if (segment.length > 0) segments.push(segment)
|
|
6295
|
+
const xOf = (index) => (count > 1 ? pad + stepX * index : width / 2)
|
|
6296
|
+
const yOf = (entry) => height - pad - ((entry.tps ?? 0) / max) * (height - pad * 2)
|
|
6297
|
+
const columnWidth = count > 1 ? stepX : width - pad * 2
|
|
6298
|
+
const columns = pointsSeries.map((entry, index) => {
|
|
6299
|
+
if (entry === null) return null
|
|
6300
|
+
const label = dailyMode
|
|
6301
|
+
? t('statsSpeedBarTitle', {
|
|
6302
|
+
date: entry.day,
|
|
6303
|
+
tps: (entry.tps ?? 0).toFixed(1),
|
|
6304
|
+
tokens: formatTokensPrecise(entry.outputTokens),
|
|
6305
|
+
seconds: entry.seconds.toFixed(1),
|
|
6306
|
+
})
|
|
6307
|
+
: t('statsSpeedHourBarTitle', {
|
|
6308
|
+
hour: `${String(index).padStart(2, '0')}:00`,
|
|
6309
|
+
tps: (entry.tps ?? 0).toFixed(1),
|
|
6310
|
+
tokens: formatTokensPrecise(entry.outputTokens),
|
|
6311
|
+
seconds: entry.seconds.toFixed(1),
|
|
6312
|
+
})
|
|
6313
|
+
return h(
|
|
6314
|
+
'rect',
|
|
6315
|
+
{
|
|
6316
|
+
key: `hit-${index}`,
|
|
6317
|
+
x: Math.max(0, Math.min(width - 1, xOf(index) - columnWidth / 2)).toFixed(1),
|
|
6318
|
+
y: 0,
|
|
6319
|
+
width: Math.max(1, Math.min(columnWidth, width)).toFixed(1),
|
|
6320
|
+
height: String(height),
|
|
6321
|
+
fill: 'transparent',
|
|
6322
|
+
// In daily mode the column is also the drill-down target.
|
|
6323
|
+
...(dailyMode ? { 'data-speed-day': entry.day, onClick: () => activateSpeedDay(entry.day) } : {}),
|
|
6324
|
+
...(dailyMode ? {} : { 'data-speed-hour': String(index) }),
|
|
6325
|
+
'data-speed-tps': String(entry.tps ?? 0),
|
|
6326
|
+
},
|
|
6327
|
+
h('title', { key: 'tip' }, label),
|
|
6328
|
+
)
|
|
6329
|
+
})
|
|
6330
|
+
const lineNodes = segments.map((segment, index) => {
|
|
6331
|
+
const line = segment.map((point) => `${point.x.toFixed(1)},${point.y.toFixed(1)}`).join(' ')
|
|
6332
|
+
const area =
|
|
6333
|
+
segment.length > 1
|
|
6334
|
+
? `${segment[0].x.toFixed(1)},${(height - pad).toFixed(1)} ${line} ${segment[segment.length - 1].x.toFixed(1)},${(height - pad).toFixed(1)}`
|
|
6335
|
+
: null
|
|
6336
|
+
return [
|
|
6337
|
+
area === null
|
|
6338
|
+
? null
|
|
6339
|
+
: h('polygon', {
|
|
6340
|
+
key: `area-${index}`,
|
|
6233
6341
|
className: 'dsh-tokenlens-speed-area',
|
|
6234
6342
|
points: area,
|
|
6235
6343
|
fill: 'rgba(46,158,91,.18)',
|
|
6236
6344
|
'aria-hidden': 'true',
|
|
6237
6345
|
}),
|
|
6238
|
-
|
|
6239
|
-
|
|
6240
|
-
|
|
6241
|
-
|
|
6242
|
-
|
|
6243
|
-
|
|
6244
|
-
|
|
6245
|
-
|
|
6246
|
-
|
|
6247
|
-
|
|
6248
|
-
|
|
6249
|
-
|
|
6250
|
-
|
|
6251
|
-
|
|
6252
|
-
|
|
6253
|
-
|
|
6254
|
-
|
|
6255
|
-
|
|
6256
|
-
|
|
6257
|
-
|
|
6258
|
-
|
|
6259
|
-
|
|
6260
|
-
|
|
6261
|
-
|
|
6262
|
-
|
|
6263
|
-
|
|
6264
|
-
|
|
6265
|
-
|
|
6266
|
-
|
|
6267
|
-
|
|
6268
|
-
|
|
6269
|
-
|
|
6270
|
-
|
|
6271
|
-
|
|
6272
|
-
|
|
6273
|
-
|
|
6274
|
-
|
|
6275
|
-
|
|
6276
|
-
|
|
6277
|
-
|
|
6278
|
-
|
|
6279
|
-
|
|
6280
|
-
|
|
6281
|
-
|
|
6282
|
-
|
|
6283
|
-
|
|
6284
|
-
|
|
6285
|
-
|
|
6286
|
-
|
|
6287
|
-
|
|
6288
|
-
|
|
6289
|
-
|
|
6290
|
-
|
|
6291
|
-
|
|
6292
|
-
|
|
6293
|
-
|
|
6294
|
-
|
|
6295
|
-
|
|
6296
|
-
|
|
6297
|
-
|
|
6298
|
-
|
|
6299
|
-
|
|
6300
|
-
|
|
6301
|
-
|
|
6302
|
-
|
|
6303
|
-
|
|
6304
|
-
|
|
6305
|
-
|
|
6306
|
-
|
|
6307
|
-
|
|
6308
|
-
|
|
6309
|
-
|
|
6310
|
-
|
|
6311
|
-
transform: 'translateX(-50%)',
|
|
6312
|
-
},
|
|
6313
|
-
},
|
|
6314
|
-
`${hovered.day} · ${(hovered.tps ?? 0).toFixed(1)} tok/s`,
|
|
6315
|
-
` · ${t('statsSpeedDetail', {
|
|
6316
|
-
tokens: formatTokensPrecise(hovered.outputTokens),
|
|
6317
|
-
seconds: hovered.seconds.toFixed(1),
|
|
6318
|
-
})}`,
|
|
6319
|
-
)
|
|
6320
|
-
const axis = h(
|
|
6321
|
-
'div',
|
|
6322
|
-
{ key: 'axis', className: 'dsh-tokenlens-speed-axis', 'aria-hidden': 'true' },
|
|
6323
|
-
h('span', { key: 'first' }, byDay[0].day),
|
|
6324
|
-
byDay.length > 1 ? h('span', { key: 'last' }, byDay[byDay.length - 1].day) : null,
|
|
6325
|
-
)
|
|
6326
|
-
return h(
|
|
6346
|
+
h('polyline', {
|
|
6347
|
+
key: `line-${index}`,
|
|
6348
|
+
className: 'dsh-tokenlens-speed-line',
|
|
6349
|
+
points: line,
|
|
6350
|
+
fill: 'none',
|
|
6351
|
+
stroke: '#2e9e5b',
|
|
6352
|
+
'stroke-width': '1.6',
|
|
6353
|
+
'stroke-linecap': 'round',
|
|
6354
|
+
'stroke-linejoin': 'round',
|
|
6355
|
+
'vector-effect': 'non-scaling-stroke',
|
|
6356
|
+
'aria-hidden': 'true',
|
|
6357
|
+
}),
|
|
6358
|
+
].filter(Boolean)
|
|
6359
|
+
})
|
|
6360
|
+
const svg = h(
|
|
6361
|
+
'svg',
|
|
6362
|
+
{
|
|
6363
|
+
key: 'svg',
|
|
6364
|
+
className: 'dsh-tokenlens-speed-svg',
|
|
6365
|
+
'data-speed-mode': dailyMode ? 'day' : 'hour',
|
|
6366
|
+
viewBox: `0 0 ${width} ${height}`,
|
|
6367
|
+
preserveAspectRatio: 'none',
|
|
6368
|
+
role: 'img',
|
|
6369
|
+
'aria-label': dailyMode ? t('statsSpeedTitle') : t('statsSpeedHourAria', { date: drilled.day }),
|
|
6370
|
+
},
|
|
6371
|
+
lineNodes.flat(),
|
|
6372
|
+
columns.filter(Boolean),
|
|
6373
|
+
)
|
|
6374
|
+
const hoverIndex =
|
|
6375
|
+
Number.isInteger(state.speedHover) && state.speedHover >= 0 && state.speedHover < count
|
|
6376
|
+
? state.speedHover
|
|
6377
|
+
: null
|
|
6378
|
+
const hovered = hoverIndex === null ? null : pointsSeries[hoverIndex]
|
|
6379
|
+
const handleMove = (event) => {
|
|
6380
|
+
const index = chartIndexFromEvent(event, count)
|
|
6381
|
+
if (index !== null && pointsSeries[index] !== null) setSpeedHover(index)
|
|
6382
|
+
}
|
|
6383
|
+
const handleLeave = () => setSpeedHover(null)
|
|
6384
|
+
const ticks = [max, max / 2, 0]
|
|
6385
|
+
const yAxis = h(
|
|
6386
|
+
'div',
|
|
6387
|
+
{ key: 'y', className: 'dsh-tokenlens-speed-yaxis', 'aria-hidden': 'true' },
|
|
6388
|
+
ticks.map((value, index) =>
|
|
6389
|
+
h('span', { key: `tick-${index}`, className: 'dsh-tokenlens-chart-tick' }, `${Math.round(value)}`),
|
|
6390
|
+
),
|
|
6391
|
+
)
|
|
6392
|
+
const crosshair =
|
|
6393
|
+
hovered === null || hovered === undefined
|
|
6394
|
+
? null
|
|
6395
|
+
: h('div', {
|
|
6396
|
+
key: 'cross',
|
|
6397
|
+
className: 'dsh-tokenlens-chart-crosshair',
|
|
6398
|
+
'data-speed-crosshair': String(hoverIndex),
|
|
6399
|
+
'aria-hidden': 'true',
|
|
6400
|
+
style: { left: `${(xOf(hoverIndex) / width) * 100}%` },
|
|
6401
|
+
})
|
|
6402
|
+
const marker =
|
|
6403
|
+
hovered === null || hovered === undefined
|
|
6404
|
+
? null
|
|
6405
|
+
: h('div', {
|
|
6406
|
+
key: 'dot',
|
|
6407
|
+
className: 'dsh-tokenlens-speed-dot',
|
|
6408
|
+
'aria-hidden': 'true',
|
|
6409
|
+
style: {
|
|
6410
|
+
left: `${(xOf(hoverIndex) / width) * 100}%`,
|
|
6411
|
+
top: `${(yOf(hovered) / height) * 100}%`,
|
|
6412
|
+
},
|
|
6413
|
+
})
|
|
6414
|
+
const readoutLabel = dailyMode ? hovered?.day : `${String(hoverIndex).padStart(2, '0')}:00`
|
|
6415
|
+
const readout =
|
|
6416
|
+
hovered === null || hovered === undefined
|
|
6417
|
+
? null
|
|
6418
|
+
: h(
|
|
6327
6419
|
'div',
|
|
6328
|
-
{
|
|
6329
|
-
|
|
6330
|
-
|
|
6331
|
-
'
|
|
6332
|
-
|
|
6333
|
-
|
|
6334
|
-
|
|
6335
|
-
|
|
6336
|
-
|
|
6337
|
-
|
|
6338
|
-
|
|
6339
|
-
|
|
6340
|
-
|
|
6341
|
-
|
|
6342
|
-
|
|
6343
|
-
|
|
6344
|
-
|
|
6345
|
-
|
|
6346
|
-
|
|
6420
|
+
{
|
|
6421
|
+
key: 'readout',
|
|
6422
|
+
className: 'dsh-tokenlens-chart-readout',
|
|
6423
|
+
'data-speed-readout': 'true',
|
|
6424
|
+
role: 'status',
|
|
6425
|
+
style:
|
|
6426
|
+
hoverIndex === 0
|
|
6427
|
+
? { left: '0%' }
|
|
6428
|
+
: hoverIndex === count - 1
|
|
6429
|
+
? { right: '0%' }
|
|
6430
|
+
: {
|
|
6431
|
+
left: `${(xOf(hoverIndex) / width) * 100}%`,
|
|
6432
|
+
transform: 'translateX(-50%)',
|
|
6433
|
+
},
|
|
6434
|
+
},
|
|
6435
|
+
`${readoutLabel} · ${(hovered.tps ?? 0).toFixed(1)} tok/s`,
|
|
6436
|
+
` · ${t('statsSpeedDetail', {
|
|
6437
|
+
tokens: formatTokensPrecise(hovered.outputTokens),
|
|
6438
|
+
seconds: hovered.seconds.toFixed(1),
|
|
6439
|
+
})}`,
|
|
6347
6440
|
)
|
|
6348
|
-
|
|
6441
|
+
const axis = h(
|
|
6442
|
+
'div',
|
|
6443
|
+
{ key: 'axis', className: 'dsh-tokenlens-speed-axis', 'aria-hidden': 'true' },
|
|
6444
|
+
h('span', { key: 'first' }, dailyMode ? (pointsSeries[0]?.day ?? '') : '00:00'),
|
|
6445
|
+
h('span', { key: 'last' }, dailyMode ? (pointsSeries[count - 1]?.day ?? '') : '23:00'),
|
|
6446
|
+
)
|
|
6447
|
+
return h(
|
|
6448
|
+
'div',
|
|
6449
|
+
{ key: 'plot', className: 'dsh-tokenlens-speed-body' },
|
|
6450
|
+
yAxis,
|
|
6451
|
+
h(
|
|
6452
|
+
'div',
|
|
6453
|
+
{
|
|
6454
|
+
key: 'plotarea',
|
|
6455
|
+
className: `dsh-tokenlens-speed-plot${dailyMode ? '' : ' dsh-tokenlens-speed-plot-hourly'}`,
|
|
6456
|
+
'data-speed-mode': dailyMode ? 'day' : 'hour',
|
|
6457
|
+
onMouseMove: handleMove,
|
|
6458
|
+
onMouseLeave: handleLeave,
|
|
6459
|
+
onPointerMove: handleMove,
|
|
6460
|
+
onPointerLeave: handleLeave,
|
|
6461
|
+
},
|
|
6462
|
+
svg,
|
|
6463
|
+
crosshair,
|
|
6464
|
+
marker,
|
|
6465
|
+
readout,
|
|
6466
|
+
axis,
|
|
6467
|
+
),
|
|
6468
|
+
)
|
|
6469
|
+
}
|
|
6470
|
+
|
|
6471
|
+
// A drilled-in day with no hourly samples explains itself instead of
|
|
6472
|
+
// silently drawing nothing.
|
|
6473
|
+
const allEmpty =
|
|
6474
|
+
pointsSeries.length === 0 || pointsSeries.every((entry) => entry === null || entry === undefined)
|
|
6475
|
+
const body = allEmpty
|
|
6476
|
+
? h(
|
|
6477
|
+
'div',
|
|
6478
|
+
{ key: 'empty', className: 'dsh-tokenlens-chart-empty', 'data-speed-empty': 'true' },
|
|
6479
|
+
h('span', { key: 'hint', className: 'dsh-tokenlens-hint' }, hourlyMode ? t('statsSpeedHourEmpty') : t('statsSpeedEmpty')),
|
|
6480
|
+
)
|
|
6481
|
+
: plotOf()
|
|
6482
|
+
|
|
6349
6483
|
return h(
|
|
6350
6484
|
'div',
|
|
6351
6485
|
{ key: 'speed', className: 'dsh-tokenlens-section', 'data-speed-section': 'true' },
|
|
6352
6486
|
head,
|
|
6353
|
-
h(
|
|
6487
|
+
h(
|
|
6488
|
+
'div',
|
|
6489
|
+
{ key: 'hint', className: 'dsh-tokenlens-subtitle' },
|
|
6490
|
+
dailyMode ? t('statsSpeedHint') : t('statsSpeedHourHint'),
|
|
6491
|
+
),
|
|
6354
6492
|
body,
|
|
6355
6493
|
)
|
|
6356
6494
|
}
|
|
6357
|
-
|
|
6358
6495
|
function buildStatsDetails(state, ctx, t) {
|
|
6359
6496
|
const { stats, hasMonth } = ctx
|
|
6360
6497
|
if (stats === null) return null
|
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.8'
|
|
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 = 7
|
|
65
65
|
|
|
66
66
|
/**
|
|
67
67
|
* How many days the daily series (and therefore the 图表 mode and the
|
|
@@ -249,6 +249,9 @@ export function foldSession(records) {
|
|
|
249
249
|
// `assistant/message.time − step/start.time`: the step's tool calls are emitted
|
|
250
250
|
// AFTER the message, so they do not dilute it.
|
|
251
251
|
const speed = Object.create(null)
|
|
252
|
+
// v1.7.7: the same samples bucketed per epoch HOUR, so the TPS curve can be
|
|
253
|
+
// drilled into one day's 24 local hours (mirrors `hours` above).
|
|
254
|
+
const speedHours = Object.create(null)
|
|
252
255
|
const stepStarts = new Map()
|
|
253
256
|
let latestTime = 0
|
|
254
257
|
let sessionCwd = null
|
|
@@ -341,6 +344,13 @@ export function foldSession(records) {
|
|
|
341
344
|
slot.output += usage.output
|
|
342
345
|
slot.seconds += elapsedMs / 1000
|
|
343
346
|
slot.steps += 1
|
|
347
|
+
// v1.7.7: the hour bucket reuses `hour`, already computed for the token
|
|
348
|
+
// histogram, so both drill-downs describe exactly the same slot.
|
|
349
|
+
const hourSpeed = (speedHours[hour] ??= { providers: Object.create(null) })
|
|
350
|
+
const hourSlot = (hourSpeed.providers[attributedProvider] ??= { output: 0, seconds: 0, steps: 0 })
|
|
351
|
+
hourSlot.output += usage.output
|
|
352
|
+
hourSlot.seconds += elapsedMs / 1000
|
|
353
|
+
hourSlot.steps += 1
|
|
344
354
|
stepStarts.delete(`${turn}:${step}`)
|
|
345
355
|
}
|
|
346
356
|
}
|
|
@@ -361,7 +371,7 @@ export function foldSession(records) {
|
|
|
361
371
|
if (Number(key) < cutoff) delete hours[key]
|
|
362
372
|
}
|
|
363
373
|
}
|
|
364
|
-
return { days, providers, models, hours, workspaces, session, speed }
|
|
374
|
+
return { days, providers, models, hours, workspaces, session, speed, speedHours }
|
|
365
375
|
}
|
|
366
376
|
|
|
367
377
|
/** Merge one file's folded contribution into an aggregate being built. */
|
|
@@ -382,6 +392,16 @@ function mergeFold(aggregate, fold) {
|
|
|
382
392
|
into.steps += slot.steps
|
|
383
393
|
}
|
|
384
394
|
}
|
|
395
|
+
// v1.7.7: the hourly speed buckets merge with the hourly token buckets.
|
|
396
|
+
for (const [hour, hourSpeed] of Object.entries(fold.speedHours ?? {})) {
|
|
397
|
+
const target = (aggregate.speedHours[hour] ??= { providers: Object.create(null) })
|
|
398
|
+
for (const [provider, slot] of Object.entries(hourSpeed.providers ?? {})) {
|
|
399
|
+
const into = (target.providers[provider] ??= { output: 0, seconds: 0, steps: 0 })
|
|
400
|
+
into.output += slot.output
|
|
401
|
+
into.seconds += slot.seconds
|
|
402
|
+
into.steps += slot.steps
|
|
403
|
+
}
|
|
404
|
+
}
|
|
385
405
|
for (const [provider, bucket] of Object.entries(fold.providers ?? {})) {
|
|
386
406
|
addBucket((aggregate.providers[provider] ??= emptyBucket()), bucket)
|
|
387
407
|
}
|
|
@@ -562,7 +582,18 @@ export function createUsageStats(input) {
|
|
|
562
582
|
const workspacesAggregate = Object.create(null)
|
|
563
583
|
// v1.7.0: `speedAggregate[day].providers[providerId] = {output, seconds, steps}`
|
|
564
584
|
const speedAggregate = Object.create(null)
|
|
585
|
+
// v1.7.7: the same, per epoch hour (drives the TPS hour drill-down).
|
|
586
|
+
const speedHoursAggregate = Object.create(null)
|
|
565
587
|
for (const entry of Object.values(cache.files)) {
|
|
588
|
+
for (const [hour, hourSpeed] of Object.entries(entry.speedHours ?? {})) {
|
|
589
|
+
const target = (speedHoursAggregate[hour] ??= { providers: Object.create(null) })
|
|
590
|
+
for (const [provider, slot] of Object.entries(hourSpeed.providers ?? {})) {
|
|
591
|
+
const into = (target.providers[provider] ??= { output: 0, seconds: 0, steps: 0 })
|
|
592
|
+
into.output += typeof slot.output === 'number' ? slot.output : 0
|
|
593
|
+
into.seconds += typeof slot.seconds === 'number' ? slot.seconds : 0
|
|
594
|
+
into.steps += typeof slot.steps === 'number' ? slot.steps : 0
|
|
595
|
+
}
|
|
596
|
+
}
|
|
566
597
|
for (const [day, daySpeed] of Object.entries(entry.speed ?? {})) {
|
|
567
598
|
const target = (speedAggregate[day] ??= { providers: Object.create(null) })
|
|
568
599
|
for (const [provider, slot] of Object.entries(daySpeed.providers ?? {})) {
|
|
@@ -626,6 +657,26 @@ export function createUsageStats(input) {
|
|
|
626
657
|
}
|
|
627
658
|
}
|
|
628
659
|
|
|
660
|
+
// v1.7.7: local-hour OUTPUT-SPEED series per day, for the TPS drill-down.
|
|
661
|
+
// `speedDayHours[day][providerId]` is 24 `{output, seconds, steps}` slots in
|
|
662
|
+
// local time, built from the same epoch-hour buckets as the token histogram
|
|
663
|
+
// so both drill-downs describe the same slot.
|
|
664
|
+
const daySpeedHours = Object.create(null)
|
|
665
|
+
for (const [epochHour, bucket] of Object.entries(speedHoursAggregate)) {
|
|
666
|
+
const hourStart = Number(epochHour) * 3600_000
|
|
667
|
+
const date = new Date(hourStart)
|
|
668
|
+
if (!Number.isFinite(date.getTime())) continue
|
|
669
|
+
const slot = date.getHours()
|
|
670
|
+
const dayEntry = (daySpeedHours[dayKey(hourStart)] ??= Object.create(null))
|
|
671
|
+
for (const [provider, sample] of Object.entries(bucket.providers ?? {})) {
|
|
672
|
+
if (sample === null || typeof sample !== 'object') continue
|
|
673
|
+
const series = (dayEntry[provider] ??= Array.from({ length: 24 }, () => ({ output: 0, seconds: 0, steps: 0 })))
|
|
674
|
+
series[slot].output += typeof sample.output === 'number' ? sample.output : 0
|
|
675
|
+
series[slot].seconds += typeof sample.seconds === 'number' ? sample.seconds : 0
|
|
676
|
+
series[slot].steps += typeof sample.steps === 'number' ? sample.steps : 0
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
629
680
|
const allTime = emptyBucket()
|
|
630
681
|
for (const bucket of Object.values(providersAggregate)) addBucket(allTime, bucket)
|
|
631
682
|
// Rolling 5-hour window from the hourly DSH buckets.
|
|
@@ -791,6 +842,10 @@ export function createUsageStats(input) {
|
|
|
791
842
|
daily,
|
|
792
843
|
// v1.7.0: raw per-day output-speed samples for the wire builder below.
|
|
793
844
|
speed: speedAggregate,
|
|
845
|
+
// v1.7.7: per-hour samples, converted to 24 local slots by the composer.
|
|
846
|
+
speedHours: speedHoursAggregate,
|
|
847
|
+
// v1.7.7: ready-to-ship 24-slot local-hour speed series per day/provider.
|
|
848
|
+
speedDayHours: daySpeedHours,
|
|
794
849
|
scan,
|
|
795
850
|
}
|
|
796
851
|
}
|
|
@@ -857,6 +912,8 @@ export function createUsageStats(input) {
|
|
|
857
912
|
workspaces: fold.workspaces,
|
|
858
913
|
// v1.7.0: per-day output-speed samples (CACHE_VERSION 6).
|
|
859
914
|
speed: fold.speed,
|
|
915
|
+
// v1.7.7: per-hour samples for the TPS drill-down (CACHE_VERSION 7).
|
|
916
|
+
speedHours: fold.speedHours,
|
|
860
917
|
}
|
|
861
918
|
nextFiles[path] = entry
|
|
862
919
|
scanned += 1
|
|
@@ -902,6 +959,8 @@ export function createUsageStats(input) {
|
|
|
902
959
|
allTime: emptyBucket(),
|
|
903
960
|
providers: [],
|
|
904
961
|
models: [],
|
|
962
|
+
speed: Object.create(null),
|
|
963
|
+
speedHours: Object.create(null),
|
|
905
964
|
scan: {
|
|
906
965
|
sessions: 0,
|
|
907
966
|
scanned: 0,
|
|
@@ -1390,13 +1449,32 @@ export function createUsageStats(input) {
|
|
|
1390
1449
|
daySteps += slot.steps
|
|
1391
1450
|
}
|
|
1392
1451
|
if (daySeconds <= 0 || dayOutput <= 0) continue
|
|
1393
|
-
|
|
1452
|
+
const entry = {
|
|
1394
1453
|
day,
|
|
1395
1454
|
outputTokens: dayOutput,
|
|
1396
1455
|
seconds: Math.round(daySeconds * 10) / 10,
|
|
1397
1456
|
tps: Math.round((dayOutput / daySeconds) * 10) / 10,
|
|
1398
1457
|
steps: daySteps,
|
|
1399
|
-
}
|
|
1458
|
+
}
|
|
1459
|
+
// v1.7.7: the day's 24 local-hour slots, for the click-to-drill-down
|
|
1460
|
+
// chart. Only a provider-scoped series carries them: the combined series
|
|
1461
|
+
// is not what the TPS card renders any more.
|
|
1462
|
+
if (providerId !== null) {
|
|
1463
|
+
const slots = dsh.speedDayHours?.[day]?.[providerId]
|
|
1464
|
+
if (Array.isArray(slots)) {
|
|
1465
|
+
entry.hours = slots.map((cell) => {
|
|
1466
|
+
if (cell === null || typeof cell !== 'object') return null
|
|
1467
|
+
if (!(cell.seconds > 0) || !(cell.output > 0)) return null
|
|
1468
|
+
return {
|
|
1469
|
+
outputTokens: cell.output,
|
|
1470
|
+
seconds: Math.round(cell.seconds * 10) / 10,
|
|
1471
|
+
tps: Math.round((cell.output / cell.seconds) * 10) / 10,
|
|
1472
|
+
steps: cell.steps,
|
|
1473
|
+
}
|
|
1474
|
+
})
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
byDay.push(entry)
|
|
1400
1478
|
output += dayOutput
|
|
1401
1479
|
seconds += daySeconds
|
|
1402
1480
|
steps += daySteps
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-tokenlens",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.8",
|
|
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",
|