dsh-tokenlens 1.7.6 → 1.7.7
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 +11 -0
- package/PROTOCOL.md +8 -2
- package/README.en.md +1 -1
- package/README.md +1 -1
- package/client.js +330 -194
- package/index.js +1 -1
- package/lib/usage-stats.mjs +82 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,17 @@
|
|
|
4
4
|
|
|
5
5
|
**版本记录约定**:同一发布日只保留一个章节,标题取当日**最后一个**已发布版本号;当日多次发布的改动合并到该章节,并在标题下注明当日实际发布过的版本号(npm 上仍可安装这些独立版本)。若当日已有发布,后续小改动不再单独增加版本号,而是并入下一篇日志。这样日志按天规整、不出现同日多个小版本。
|
|
6
6
|
|
|
7
|
+
## 1.7.7 — 2026-11-10
|
|
8
|
+
|
|
9
|
+
- **feat(ui): TPS 卡片锁定「当前供应商」**——不再跟随上方「当前供应商 / 全部 Harness」切换。这张卡片回答的是「我现在用的模型有多快」,所以始终使用所选供应商自己的日平均序列;没有供应商行时显示空态说明
|
|
10
|
+
- **feat(ui): 点曲线上的点可下钻当日小时级 TPS**——与 token 柱状图的「点某天 → 24 小时」交互一致:点击后曲线切换为该日 24 个本地小时的速率曲线,标题栏显示「2026-11-05 · 小时级」并提供「返回日视图」;再点同一天也会切回日视图
|
|
11
|
+
- 无样本的小时是 `null` 而不是 0,曲线在该处**断开**而不是跌到零(跌到零是假信息)
|
|
12
|
+
- 小时级读数同样是「输出 Token ÷ 模型自身耗时」,悬停显示 `09:00 · 120.0 tok/s(600 输出 ÷ 5.0s)`
|
|
13
|
+
- 该日没有任何小时级样本时显示说明文字,不画空图
|
|
14
|
+
- **feat(stats): Host 侧新增按小时的 TPS 采样**——`foldSession` 在记录每日速率的同时把样本写入按 epoch 小时分桶的 `speedHours`,聚合层再换算成本地 24 槽 `speedDayHours`,随 `speed.byDay[].hours` 下发(仅 provider 维度,合计序列不带)。缓存 schema 升至 7,首次会重扫
|
|
15
|
+
- 文档:`PROTOCOL.md` 补上 `hours` 结构与「null 表示无样本」的约定
|
|
16
|
+
- 测试:373 → 377(TPS 卡片不受作用域切换影响、点柱下钻与返回、无样本下钻的空态、按小时分桶与 null 槽位)
|
|
17
|
+
|
|
7
18
|
## 1.7.6 — 2026-11-06
|
|
8
19
|
|
|
9
20
|
(当日已发布版本: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
|
@@ -233,6 +233,12 @@ window.__ModuleLoader__.load({
|
|
|
233
233
|
statsSpeedTotal: '均值 {tps} tok/s',
|
|
234
234
|
statsSpeedPeak: '峰值 {tps} tok/s',
|
|
235
235
|
statsSpeedEmpty: '暂无可计算的速度样本(需要 step 计时数据)',
|
|
236
|
+
statsSpeedHourTitle: '{date} · 小时级',
|
|
237
|
+
statsSpeedHourHint: '同一天按本地小时切分;仍是输出 Token ÷ 模型自身耗时,无样本的小时留空',
|
|
238
|
+
statsSpeedHourEmpty: '该日没有可下钻的小时级样本',
|
|
239
|
+
statsSpeedHourBarTitle: '{hour} · {tps} tok/s({tokens} 输出 ÷ {seconds}s)',
|
|
240
|
+
statsSpeedHourAria: '{date} 的小时级输出速度',
|
|
241
|
+
statsSpeedBack: '返回日视图',
|
|
236
242
|
statsSpeedBarTitle: '{date} · {tps} tok/s({tokens} 输出 ÷ {seconds}s)',
|
|
237
243
|
statsSpeedDetail: '{tokens} 输出 ÷ {seconds}s',
|
|
238
244
|
statsLast7Days: '最近7天',
|
|
@@ -510,6 +516,13 @@ window.__ModuleLoader__.load({
|
|
|
510
516
|
statsSpeedTotal: 'avg {tps} tok/s',
|
|
511
517
|
statsSpeedPeak: 'peak {tps} tok/s',
|
|
512
518
|
statsSpeedEmpty: 'No speed samples yet (needs step timing data)',
|
|
519
|
+
statsSpeedHourTitle: '{date} · hourly',
|
|
520
|
+
statsSpeedHourHint:
|
|
521
|
+
'The same day split by local hour; still output tokens ÷ model time, hours without samples stay empty',
|
|
522
|
+
statsSpeedHourEmpty: 'No hourly samples to drill into for this day',
|
|
523
|
+
statsSpeedHourBarTitle: '{hour} · {tps} tok/s ({tokens} output ÷ {seconds}s)',
|
|
524
|
+
statsSpeedHourAria: 'Hourly output speed for {date}',
|
|
525
|
+
statsSpeedBack: 'Back to daily',
|
|
513
526
|
statsSpeedBarTitle: '{date} · {tps} tok/s ({tokens} output ÷ {seconds}s)',
|
|
514
527
|
statsSpeedDetail: '{tokens} output ÷ {seconds}s',
|
|
515
528
|
statsLast7Days: 'Last 7 days',
|
|
@@ -1611,13 +1624,31 @@ window.__ModuleLoader__.load({
|
|
|
1611
1624
|
const byDay = Array.isArray(raw.byDay)
|
|
1612
1625
|
? raw.byDay
|
|
1613
1626
|
.filter((entry) => entry !== null && typeof entry === 'object' && typeof entry.day === 'string')
|
|
1614
|
-
.map((entry) =>
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1627
|
+
.map((entry) => {
|
|
1628
|
+
// v1.7.7: the day's 24 local-hour slots (null where there is no
|
|
1629
|
+
// sample), used by the click-to-drill-down hourly view.
|
|
1630
|
+
const hours = Array.isArray(entry.hours)
|
|
1631
|
+
? entry.hours.map((cell) => {
|
|
1632
|
+
if (cell === null || typeof cell !== 'object') return null
|
|
1633
|
+
const tps = numberOrNull(cell.tps)
|
|
1634
|
+
if (tps === null || tps < 0) return null
|
|
1635
|
+
return {
|
|
1636
|
+
outputTokens: numberOrNull(cell.outputTokens) ?? 0,
|
|
1637
|
+
seconds: numberOrNull(cell.seconds) ?? 0,
|
|
1638
|
+
tps,
|
|
1639
|
+
steps: numberOrNull(cell.steps) ?? 0,
|
|
1640
|
+
}
|
|
1641
|
+
})
|
|
1642
|
+
: null
|
|
1643
|
+
return {
|
|
1644
|
+
day: entry.day,
|
|
1645
|
+
outputTokens: numberOrNull(entry.outputTokens) ?? 0,
|
|
1646
|
+
seconds: numberOrNull(entry.seconds) ?? 0,
|
|
1647
|
+
tps: numberOrNull(entry.tps),
|
|
1648
|
+
steps: numberOrNull(entry.steps) ?? 0,
|
|
1649
|
+
hours: hours === null ? null : hours.slice(0, 24),
|
|
1650
|
+
}
|
|
1651
|
+
})
|
|
1621
1652
|
.filter((entry) => entry.tps !== null && entry.tps >= 0)
|
|
1622
1653
|
: []
|
|
1623
1654
|
const totalRaw = raw.total !== null && typeof raw.total === 'object' ? raw.total : {}
|
|
@@ -2107,6 +2138,9 @@ window.__ModuleLoader__.load({
|
|
|
2107
2138
|
chartHover: null,
|
|
2108
2139
|
// v1.7.2: the hovered point of the output-speed (TPS) curve.
|
|
2109
2140
|
speedHover: null,
|
|
2141
|
+
// v1.7.7: the day whose 24 local hours the TPS card charts, or null
|
|
2142
|
+
// (daily trend). Clicking a point on the curve toggles it.
|
|
2143
|
+
speedDay: null,
|
|
2110
2144
|
// v1.2.0 chart drill-down: the clicked day whose 0..23 hour histogram
|
|
2111
2145
|
// is charted instead of the daily trend, or null (daily trend).
|
|
2112
2146
|
chartDay: null,
|
|
@@ -2219,6 +2253,9 @@ window.__ModuleLoader__.load({
|
|
|
2219
2253
|
// v1.7.6: the TPS hover index is view state like the chart's, so it must
|
|
2220
2254
|
// not survive a provider switch (the series behind it changed).
|
|
2221
2255
|
speedHover: null,
|
|
2256
|
+
// v1.7.7: an hourly TPS drill-down belongs to one provider's series, so a
|
|
2257
|
+
// provider switch returns to the daily trend.
|
|
2258
|
+
speedDay: null,
|
|
2222
2259
|
}
|
|
2223
2260
|
}
|
|
2224
2261
|
|
|
@@ -3503,6 +3540,10 @@ window.__ModuleLoader__.load({
|
|
|
3503
3540
|
.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
3541
|
.dsh-tokenlens-speed-plot{position:relative;flex:1 1 auto;min-width:0}
|
|
3505
3542
|
.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}
|
|
3543
|
+
/* v1.7.7: the daily curve is clickable (drills into that day's hours). */
|
|
3544
|
+
.dsh-tokenlens-speed-svg[data-speed-mode=day] rect[data-speed-day]{cursor:pointer}
|
|
3545
|
+
.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}
|
|
3546
|
+
.dsh-tokenlens-link-button:hover,.dsh-tokenlens-link-button:focus-visible{color:var(--dsw-alias-label-primary,currentColor)}
|
|
3506
3547
|
.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
3548
|
.dsh-tokenlens-speed-axis>span{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
3508
3549
|
@media (max-width:680px){.dsh-tokenlens-chart-layout-row>*{flex-basis:100%!important}}
|
|
@@ -4729,6 +4770,24 @@ window.__ModuleLoader__.load({
|
|
|
4729
4770
|
if (store.get().speedHover !== next) store.set({ speedHover: next })
|
|
4730
4771
|
}
|
|
4731
4772
|
|
|
4773
|
+
/**
|
|
4774
|
+
* Drill the TPS card into one day's 24 local hours (v1.7.7), or back out.
|
|
4775
|
+
*
|
|
4776
|
+
* Toggling mirrors the token chart's `activateChartDay`: clicking the already
|
|
4777
|
+
* active day returns to the daily trend, so no separate "back" button is
|
|
4778
|
+
* needed — but one is still offered in the header for discoverability.
|
|
4779
|
+
*/
|
|
4780
|
+
function activateSpeedDay(day) {
|
|
4781
|
+
if (typeof day !== 'string' || day === '') return
|
|
4782
|
+
const next = store.get().speedDay === day ? null : day
|
|
4783
|
+
store.set({ speedDay: next, speedHover: null })
|
|
4784
|
+
}
|
|
4785
|
+
|
|
4786
|
+
/** Leave the hourly TPS drill-down and chart the daily trend again (v1.7.7). */
|
|
4787
|
+
function clearSpeedDay() {
|
|
4788
|
+
if (store.get().speedDay !== null) store.set({ speedDay: null, speedHover: null })
|
|
4789
|
+
}
|
|
4790
|
+
|
|
4732
4791
|
/** The active today bucket of the current stats scope (v0.27.2). */
|
|
4733
4792
|
function todayBucketOf(ctx) {
|
|
4734
4793
|
return ctx.providerScoped ? ctx.currentRow?.today ?? null : ctx.stats?.today ?? null
|
|
@@ -6136,17 +6195,33 @@ window.__ModuleLoader__.load({
|
|
|
6136
6195
|
}
|
|
6137
6196
|
|
|
6138
6197
|
/**
|
|
6139
|
-
* The output-speed (TPS) trend card (v1.7.0
|
|
6198
|
+
* The output-speed (TPS) trend card (v1.7.0, reworked in v1.7.7).
|
|
6199
|
+
*
|
|
6200
|
+
* Always the CURRENT provider's series (v1.7.7): the user reads this card as
|
|
6201
|
+
* "how fast is the model I am using right now", so it deliberately ignores
|
|
6202
|
+
* the 当前供应商 / 全部 Harness switch above it.
|
|
6140
6203
|
*
|
|
6141
6204
|
* `tps` is output tokens over the model's OWN request time (`step/start` to
|
|
6142
|
-
* `assistant/message`), so
|
|
6205
|
+
* `assistant/message`), so a step's tool calls do not dilute it — but
|
|
6143
6206
|
* reasoning tokens are included in "output", which the hint says.
|
|
6207
|
+
*
|
|
6208
|
+
* Clicking a point drills into that day's 24 local hours (v1.7.7), the same
|
|
6209
|
+
* interaction the token chart uses for its daily bars.
|
|
6144
6210
|
*/
|
|
6145
6211
|
function buildSpeedSection(state, ctx, t) {
|
|
6146
|
-
|
|
6147
|
-
const
|
|
6148
|
-
const series = scoped === null || scoped === undefined ? null : scoped
|
|
6212
|
+
// v1.7.7: always provider-scoped, whatever the scope tabs say.
|
|
6213
|
+
const series = ctx.currentRow?.speed ?? null
|
|
6149
6214
|
const byDay = series === null ? [] : series.byDay
|
|
6215
|
+
const drilledDay = typeof state.speedDay === 'string' ? state.speedDay : null
|
|
6216
|
+
const drilled = drilledDay === null ? null : byDay.find((entry) => entry.day === drilledDay) ?? null
|
|
6217
|
+
// v1.7.7: being drilled into a day is a MODE, independent of whether that
|
|
6218
|
+
// day actually carries hourly samples — otherwise a day without them would
|
|
6219
|
+
// silently fall back to the daily chart instead of saying so.
|
|
6220
|
+
const hourlyMode = drilledDay !== null && drilled !== null
|
|
6221
|
+
const hourly = hourlyMode && Array.isArray(drilled.hours) ? drilled.hours : null
|
|
6222
|
+
const pointsSeries = hourlyMode ? hourly ?? [] : byDay
|
|
6223
|
+
const dailyMode = !hourlyMode
|
|
6224
|
+
|
|
6150
6225
|
const head = h(
|
|
6151
6226
|
'div',
|
|
6152
6227
|
{ key: 'head', className: 'dsh-tokenlens-section-head' },
|
|
@@ -6156,205 +6231,266 @@ window.__ModuleLoader__.load({
|
|
|
6156
6231
|
Icon('chart', 14),
|
|
6157
6232
|
t('statsSpeedTitle'),
|
|
6158
6233
|
),
|
|
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
|
-
),
|
|
6234
|
+
// While drilled in, the header says which day and offers a way back.
|
|
6235
|
+
hourly === null
|
|
6236
|
+
? series !== null && series.total.tps !== null
|
|
6237
|
+
? h(
|
|
6238
|
+
'span',
|
|
6239
|
+
{ className: 'dsh-tokenlens-hint', 'data-speed-total': String(series.total.tps) },
|
|
6240
|
+
t('statsSpeedTotal', { tps: series.total.tps.toFixed(1) }),
|
|
6219
6241
|
)
|
|
6220
|
-
|
|
6221
|
-
|
|
6242
|
+
: null
|
|
6243
|
+
: h(
|
|
6244
|
+
'span',
|
|
6245
|
+
{ key: 'drill', className: 'dsh-tokenlens-hint', 'data-speed-drill': drilled.day },
|
|
6246
|
+
t('statsSpeedHourTitle', { date: drilled.day }),
|
|
6247
|
+
h(
|
|
6248
|
+
'button',
|
|
6222
6249
|
{
|
|
6223
|
-
key: '
|
|
6224
|
-
|
|
6225
|
-
'
|
|
6226
|
-
|
|
6227
|
-
|
|
6228
|
-
role: 'img',
|
|
6229
|
-
'aria-label': t('statsSpeedTitle'),
|
|
6250
|
+
key: 'back',
|
|
6251
|
+
type: 'button',
|
|
6252
|
+
className: 'dsh-tokenlens-link-button',
|
|
6253
|
+
'data-speed-back': 'true',
|
|
6254
|
+
onClick: () => clearSpeedDay(),
|
|
6230
6255
|
},
|
|
6231
|
-
|
|
6232
|
-
|
|
6256
|
+
t('statsSpeedBack'),
|
|
6257
|
+
),
|
|
6258
|
+
),
|
|
6259
|
+
)
|
|
6260
|
+
|
|
6261
|
+
const plotOf = () => {
|
|
6262
|
+
if (pointsSeries.length === 0) {
|
|
6263
|
+
return h(
|
|
6264
|
+
'div',
|
|
6265
|
+
{ key: 'empty', className: 'dsh-tokenlens-chart-empty', 'data-speed-empty': 'true' },
|
|
6266
|
+
h('span', { key: 'hint', className: 'dsh-tokenlens-hint' }, t('statsSpeedEmpty')),
|
|
6267
|
+
)
|
|
6268
|
+
}
|
|
6269
|
+
// The SVG scales to the card width; `preserveAspectRatio="none"` lets it
|
|
6270
|
+
// fill without keeping the viewBox ratio, and the hover targets are
|
|
6271
|
+
// transparent columns so nothing is distorted.
|
|
6272
|
+
const width = 320
|
|
6273
|
+
const height = 90
|
|
6274
|
+
const pad = 4
|
|
6275
|
+
const real = pointsSeries.filter((entry) => entry !== null)
|
|
6276
|
+
const max = Math.max(...real.map((entry) => entry.tps ?? 0), 1)
|
|
6277
|
+
const count = pointsSeries.length
|
|
6278
|
+
const stepX = count > 1 ? (width - pad * 2) / (count - 1) : 0
|
|
6279
|
+
// A null slot (an hour with no sample) breaks the line instead of being
|
|
6280
|
+
// drawn as a dip to zero, which would be a lie.
|
|
6281
|
+
let segment = []
|
|
6282
|
+
const segments = []
|
|
6283
|
+
pointsSeries.forEach((entry, index) => {
|
|
6284
|
+
if (entry === null) {
|
|
6285
|
+
if (segment.length > 0) segments.push(segment)
|
|
6286
|
+
segment = []
|
|
6287
|
+
return
|
|
6288
|
+
}
|
|
6289
|
+
const x = count > 1 ? pad + stepX * index : width / 2
|
|
6290
|
+
const y = height - pad - ((entry.tps ?? 0) / max) * (height - pad * 2)
|
|
6291
|
+
segment.push({ x, y })
|
|
6292
|
+
})
|
|
6293
|
+
if (segment.length > 0) segments.push(segment)
|
|
6294
|
+
const xOf = (index) => (count > 1 ? pad + stepX * index : width / 2)
|
|
6295
|
+
const yOf = (entry) => height - pad - ((entry.tps ?? 0) / max) * (height - pad * 2)
|
|
6296
|
+
const columnWidth = count > 1 ? stepX : width - pad * 2
|
|
6297
|
+
const columns = pointsSeries.map((entry, index) => {
|
|
6298
|
+
if (entry === null) return null
|
|
6299
|
+
const label = dailyMode
|
|
6300
|
+
? t('statsSpeedBarTitle', {
|
|
6301
|
+
date: entry.day,
|
|
6302
|
+
tps: (entry.tps ?? 0).toFixed(1),
|
|
6303
|
+
tokens: formatTokensPrecise(entry.outputTokens),
|
|
6304
|
+
seconds: entry.seconds.toFixed(1),
|
|
6305
|
+
})
|
|
6306
|
+
: t('statsSpeedHourBarTitle', {
|
|
6307
|
+
hour: `${String(index).padStart(2, '0')}:00`,
|
|
6308
|
+
tps: (entry.tps ?? 0).toFixed(1),
|
|
6309
|
+
tokens: formatTokensPrecise(entry.outputTokens),
|
|
6310
|
+
seconds: entry.seconds.toFixed(1),
|
|
6311
|
+
})
|
|
6312
|
+
return h(
|
|
6313
|
+
'rect',
|
|
6314
|
+
{
|
|
6315
|
+
key: `hit-${index}`,
|
|
6316
|
+
x: Math.max(0, Math.min(width - 1, xOf(index) - columnWidth / 2)).toFixed(1),
|
|
6317
|
+
y: 0,
|
|
6318
|
+
width: Math.max(1, Math.min(columnWidth, width)).toFixed(1),
|
|
6319
|
+
height: String(height),
|
|
6320
|
+
fill: 'transparent',
|
|
6321
|
+
// In daily mode the column is also the drill-down target.
|
|
6322
|
+
...(dailyMode ? { 'data-speed-day': entry.day, onClick: () => activateSpeedDay(entry.day) } : {}),
|
|
6323
|
+
...(dailyMode ? {} : { 'data-speed-hour': String(index) }),
|
|
6324
|
+
'data-speed-tps': String(entry.tps ?? 0),
|
|
6325
|
+
},
|
|
6326
|
+
h('title', { key: 'tip' }, label),
|
|
6327
|
+
)
|
|
6328
|
+
})
|
|
6329
|
+
const lineNodes = segments.map((segment, index) => {
|
|
6330
|
+
const line = segment.map((point) => `${point.x.toFixed(1)},${point.y.toFixed(1)}`).join(' ')
|
|
6331
|
+
const area =
|
|
6332
|
+
segment.length > 1
|
|
6333
|
+
? `${segment[0].x.toFixed(1)},${(height - pad).toFixed(1)} ${line} ${segment[segment.length - 1].x.toFixed(1)},${(height - pad).toFixed(1)}`
|
|
6334
|
+
: null
|
|
6335
|
+
return [
|
|
6336
|
+
area === null
|
|
6337
|
+
? null
|
|
6338
|
+
: h('polygon', {
|
|
6339
|
+
key: `area-${index}`,
|
|
6233
6340
|
className: 'dsh-tokenlens-speed-area',
|
|
6234
6341
|
points: area,
|
|
6235
6342
|
fill: 'rgba(46,158,91,.18)',
|
|
6236
6343
|
'aria-hidden': 'true',
|
|
6237
6344
|
}),
|
|
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(
|
|
6345
|
+
h('polyline', {
|
|
6346
|
+
key: `line-${index}`,
|
|
6347
|
+
className: 'dsh-tokenlens-speed-line',
|
|
6348
|
+
points: line,
|
|
6349
|
+
fill: 'none',
|
|
6350
|
+
stroke: '#2e9e5b',
|
|
6351
|
+
'stroke-width': '1.6',
|
|
6352
|
+
'stroke-linecap': 'round',
|
|
6353
|
+
'stroke-linejoin': 'round',
|
|
6354
|
+
'vector-effect': 'non-scaling-stroke',
|
|
6355
|
+
'aria-hidden': 'true',
|
|
6356
|
+
}),
|
|
6357
|
+
].filter(Boolean)
|
|
6358
|
+
})
|
|
6359
|
+
const svg = h(
|
|
6360
|
+
'svg',
|
|
6361
|
+
{
|
|
6362
|
+
key: 'svg',
|
|
6363
|
+
className: 'dsh-tokenlens-speed-svg',
|
|
6364
|
+
'data-speed-mode': dailyMode ? 'day' : 'hour',
|
|
6365
|
+
viewBox: `0 0 ${width} ${height}`,
|
|
6366
|
+
preserveAspectRatio: 'none',
|
|
6367
|
+
role: 'img',
|
|
6368
|
+
'aria-label': dailyMode ? t('statsSpeedTitle') : t('statsSpeedHourAria', { date: drilled.day }),
|
|
6369
|
+
},
|
|
6370
|
+
lineNodes.flat(),
|
|
6371
|
+
columns.filter(Boolean),
|
|
6372
|
+
)
|
|
6373
|
+
const hoverIndex =
|
|
6374
|
+
Number.isInteger(state.speedHover) && state.speedHover >= 0 && state.speedHover < count
|
|
6375
|
+
? state.speedHover
|
|
6376
|
+
: null
|
|
6377
|
+
const hovered = hoverIndex === null ? null : pointsSeries[hoverIndex]
|
|
6378
|
+
const handleMove = (event) => {
|
|
6379
|
+
const index = chartIndexFromEvent(event, count)
|
|
6380
|
+
if (index !== null && pointsSeries[index] !== null) setSpeedHover(index)
|
|
6381
|
+
}
|
|
6382
|
+
const handleLeave = () => setSpeedHover(null)
|
|
6383
|
+
const ticks = [max, max / 2, 0]
|
|
6384
|
+
const yAxis = h(
|
|
6385
|
+
'div',
|
|
6386
|
+
{ key: 'y', className: 'dsh-tokenlens-speed-yaxis', 'aria-hidden': 'true' },
|
|
6387
|
+
ticks.map((value, index) =>
|
|
6388
|
+
h('span', { key: `tick-${index}`, className: 'dsh-tokenlens-chart-tick' }, `${Math.round(value)}`),
|
|
6389
|
+
),
|
|
6390
|
+
)
|
|
6391
|
+
const crosshair =
|
|
6392
|
+
hovered === null || hovered === undefined
|
|
6393
|
+
? null
|
|
6394
|
+
: h('div', {
|
|
6395
|
+
key: 'cross',
|
|
6396
|
+
className: 'dsh-tokenlens-chart-crosshair',
|
|
6397
|
+
'data-speed-crosshair': String(hoverIndex),
|
|
6398
|
+
'aria-hidden': 'true',
|
|
6399
|
+
style: { left: `${(xOf(hoverIndex) / width) * 100}%` },
|
|
6400
|
+
})
|
|
6401
|
+
const marker =
|
|
6402
|
+
hovered === null || hovered === undefined
|
|
6403
|
+
? null
|
|
6404
|
+
: h('div', {
|
|
6405
|
+
key: 'dot',
|
|
6406
|
+
className: 'dsh-tokenlens-speed-dot',
|
|
6407
|
+
'aria-hidden': 'true',
|
|
6408
|
+
style: {
|
|
6409
|
+
left: `${(xOf(hoverIndex) / width) * 100}%`,
|
|
6410
|
+
top: `${(yOf(hovered) / height) * 100}%`,
|
|
6411
|
+
},
|
|
6412
|
+
})
|
|
6413
|
+
const readoutLabel = dailyMode ? hovered?.day : `${String(hoverIndex).padStart(2, '0')}:00`
|
|
6414
|
+
const readout =
|
|
6415
|
+
hovered === null || hovered === undefined
|
|
6416
|
+
? null
|
|
6417
|
+
: h(
|
|
6327
6418
|
'div',
|
|
6328
|
-
{
|
|
6329
|
-
|
|
6330
|
-
|
|
6331
|
-
'
|
|
6332
|
-
|
|
6333
|
-
|
|
6334
|
-
|
|
6335
|
-
|
|
6336
|
-
|
|
6337
|
-
|
|
6338
|
-
|
|
6339
|
-
|
|
6340
|
-
|
|
6341
|
-
|
|
6342
|
-
|
|
6343
|
-
|
|
6344
|
-
|
|
6345
|
-
|
|
6346
|
-
|
|
6419
|
+
{
|
|
6420
|
+
key: 'readout',
|
|
6421
|
+
className: 'dsh-tokenlens-chart-readout',
|
|
6422
|
+
'data-speed-readout': 'true',
|
|
6423
|
+
role: 'status',
|
|
6424
|
+
style:
|
|
6425
|
+
hoverIndex === 0
|
|
6426
|
+
? { left: '0%' }
|
|
6427
|
+
: hoverIndex === count - 1
|
|
6428
|
+
? { right: '0%' }
|
|
6429
|
+
: {
|
|
6430
|
+
left: `${(xOf(hoverIndex) / width) * 100}%`,
|
|
6431
|
+
transform: 'translateX(-50%)',
|
|
6432
|
+
},
|
|
6433
|
+
},
|
|
6434
|
+
`${readoutLabel} · ${(hovered.tps ?? 0).toFixed(1)} tok/s`,
|
|
6435
|
+
` · ${t('statsSpeedDetail', {
|
|
6436
|
+
tokens: formatTokensPrecise(hovered.outputTokens),
|
|
6437
|
+
seconds: hovered.seconds.toFixed(1),
|
|
6438
|
+
})}`,
|
|
6347
6439
|
)
|
|
6348
|
-
|
|
6440
|
+
const axis = h(
|
|
6441
|
+
'div',
|
|
6442
|
+
{ key: 'axis', className: 'dsh-tokenlens-speed-axis', 'aria-hidden': 'true' },
|
|
6443
|
+
h('span', { key: 'first' }, dailyMode ? (pointsSeries[0]?.day ?? '') : '00:00'),
|
|
6444
|
+
h('span', { key: 'last' }, dailyMode ? (pointsSeries[count - 1]?.day ?? '') : '23:00'),
|
|
6445
|
+
)
|
|
6446
|
+
return h(
|
|
6447
|
+
'div',
|
|
6448
|
+
{ key: 'plot', className: 'dsh-tokenlens-speed-body' },
|
|
6449
|
+
yAxis,
|
|
6450
|
+
h(
|
|
6451
|
+
'div',
|
|
6452
|
+
{
|
|
6453
|
+
key: 'plotarea',
|
|
6454
|
+
className: `dsh-tokenlens-speed-plot${dailyMode ? '' : ' dsh-tokenlens-speed-plot-hourly'}`,
|
|
6455
|
+
'data-speed-mode': dailyMode ? 'day' : 'hour',
|
|
6456
|
+
onMouseMove: handleMove,
|
|
6457
|
+
onMouseLeave: handleLeave,
|
|
6458
|
+
onPointerMove: handleMove,
|
|
6459
|
+
onPointerLeave: handleLeave,
|
|
6460
|
+
},
|
|
6461
|
+
svg,
|
|
6462
|
+
crosshair,
|
|
6463
|
+
marker,
|
|
6464
|
+
readout,
|
|
6465
|
+
axis,
|
|
6466
|
+
),
|
|
6467
|
+
)
|
|
6468
|
+
}
|
|
6469
|
+
|
|
6470
|
+
// A drilled-in day with no hourly samples explains itself instead of
|
|
6471
|
+
// silently drawing nothing.
|
|
6472
|
+
const allEmpty =
|
|
6473
|
+
pointsSeries.length === 0 || pointsSeries.every((entry) => entry === null || entry === undefined)
|
|
6474
|
+
const body = allEmpty
|
|
6475
|
+
? h(
|
|
6476
|
+
'div',
|
|
6477
|
+
{ key: 'empty', className: 'dsh-tokenlens-chart-empty', 'data-speed-empty': 'true' },
|
|
6478
|
+
h('span', { key: 'hint', className: 'dsh-tokenlens-hint' }, hourlyMode ? t('statsSpeedHourEmpty') : t('statsSpeedEmpty')),
|
|
6479
|
+
)
|
|
6480
|
+
: plotOf()
|
|
6481
|
+
|
|
6349
6482
|
return h(
|
|
6350
6483
|
'div',
|
|
6351
6484
|
{ key: 'speed', className: 'dsh-tokenlens-section', 'data-speed-section': 'true' },
|
|
6352
6485
|
head,
|
|
6353
|
-
h(
|
|
6486
|
+
h(
|
|
6487
|
+
'div',
|
|
6488
|
+
{ key: 'hint', className: 'dsh-tokenlens-subtitle' },
|
|
6489
|
+
dailyMode ? t('statsSpeedHint') : t('statsSpeedHourHint'),
|
|
6490
|
+
),
|
|
6354
6491
|
body,
|
|
6355
6492
|
)
|
|
6356
6493
|
}
|
|
6357
|
-
|
|
6358
6494
|
function buildStatsDetails(state, ctx, t) {
|
|
6359
6495
|
const { stats, hasMonth } = ctx
|
|
6360
6496
|
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.7'
|
|
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.7",
|
|
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",
|