dsh-tokenlens 1.7.5 → 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 +23 -0
- package/PROTOCOL.md +42 -6
- package/README.en.md +1 -1
- package/README.md +1 -1
- package/client.js +375 -197
- package/index.js +36 -7
- package/lib/usage-stats.mjs +82 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,29 @@
|
|
|
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
|
+
|
|
18
|
+
## 1.7.6 — 2026-11-06
|
|
19
|
+
|
|
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)
|
|
21
|
+
|
|
22
|
+
- **feat(ui): 版本号左侧新增仓库链接**——弹窗底部一行现在是 `[GitHub] 插件 v1.7.6 · 已是最新版本`,链接指向 `github.com/senyingxidi/dsh-tokenlens`,`target=_blank` + `rel=noopener`,带 `title` 说明;图标沿用同一套 stroke 图标(`aria-hidden`,对读屏透明)
|
|
23
|
+
- **fix(ui): TPS 悬停索引不再跨视图残留**——`speedHover` 原先只存在默认状态里,切换「当前供应商 / 全部 Harness」、切换「数据 / 图表」、切换供应商时都不会重置,于是同一个索引会指向新序列里的另一天(或越界)。现在它与 `chartHover` 一起被清理
|
|
24
|
+
- **fix(ui): 单点 TPS 曲线的命中区溢出**——只有一天样本时命中列从负 x 开始,左半区域无法触发悬停;现在把它夹进 viewBox
|
|
25
|
+
- **fix(stats): 启动期统计结果可能带着未锚定的窗口被缓存**——重启后「套餐起点」要等第一次额度读取才知道,而在此之前算出的统计结果会按 TTL 一直复用。现在 Host 在锚点集合变化后强制重算一次
|
|
26
|
+
- **perf: 模型切换不再无谓重取**——只有额度真正随模型变化的供应商(当前仅 `commandcode`)才会在切换模型时丢弃快照;在 DeepSeek / OpenCode 等模型无关的供应商上切模型不再触发上游请求。客户端也只在 `commandcode` 下补发一次快照
|
|
27
|
+
- 文档:`PROTOCOL.md` 补上 `credits.poolUsd / allowanceUsd / allowanceModel / allowanceBasis`、`speed` 序列,以及 v1.7.2 的窗口锚定与 v1.7.3 的分母结论
|
|
28
|
+
- 测试:370 → 373(仓库链接的位置/属性/图标、悬停状态清理、按供应商差异化的缓存失效)
|
|
29
|
+
|
|
7
30
|
## 1.7.5 — 2026-11-06
|
|
8
31
|
|
|
9
32
|
(当日已发布版本: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)
|
package/PROTOCOL.md
CHANGED
|
@@ -84,7 +84,15 @@ from `dsh-app://app/` and forwards non-asset paths to the Host).
|
|
|
84
84
|
"monthly": { … } | null
|
|
85
85
|
} | null, // null for balance-only providers
|
|
86
86
|
"balance": { "amount": 2.26, "currency": "CNY", "granted": 0, "toppedUp": 2.26 } | null,
|
|
87
|
-
"credits": {
|
|
87
|
+
"credits": {
|
|
88
|
+
"remaining": 4.5, "granted": 10, "purchased": 0,
|
|
89
|
+
// v1.7.2: the SELECTED model's advertised allowance. Advertising only: the
|
|
90
|
+
// monthly percentage is always measured against the credit pool (v1.7.3),
|
|
91
|
+
// because that is what the provider's own console does.
|
|
92
|
+
"poolUsd": 10, "allowanceUsd": 14,
|
|
93
|
+
"allowanceModel": "deepseek/deepseek-v4.1-flash" | null,
|
|
94
|
+
"allowanceBasis": "model" | "pool" | "unknown"
|
|
95
|
+
} | null,
|
|
88
96
|
"account": { "name": "…", "userName": "…" } | null, // non-secret display info
|
|
89
97
|
"links": { "console": "https://opencode.ai/console" } | null, // provider-owned web page, follows the selection
|
|
90
98
|
"fetchedAt": "2026-10-25T09:03:00.000Z",
|
|
@@ -129,9 +137,13 @@ plugin never sizes a month from a plan catalog.
|
|
|
129
137
|
|
|
130
138
|
**The denominator is deliberately model-independent.** GOAT advertises "up to $100" and lists
|
|
131
139
|
per-model allowances ($100 on DeepSeek V4.1 Flash, $60 on several others) because the same
|
|
132
|
-
credits stretch further on a discounted model; the pool itself is one number.
|
|
133
|
-
|
|
134
|
-
`
|
|
140
|
+
credits stretch further on a discounted model; the pool itself is one number. Measured against
|
|
141
|
+
the provider's own console: with $2.94 spent it prints 每月限额 **4%**, i.e. `2.94 / 70 = 4.2%`;
|
|
142
|
+
`2.94 / 100` would print 2.9%. So the percentage is pool-based, and the per-model allowance is
|
|
143
|
+
reported as `credits.allowanceUsd` for context only (v1.7.3). The model *is* forwarded with the
|
|
144
|
+
request (`session.modelSelection.next.model`) so that the allowance can be resolved, and the Host
|
|
145
|
+
drops the cached snapshot on a model switch **only** for providers whose allowance actually
|
|
146
|
+
follows the model (`MODEL_SENSITIVE_ALLOWANCE`, currently `commandcode`).
|
|
135
147
|
|
|
136
148
|
`windowLimits.limited` is a **capability flag** ("this plan paces you with rolling windows"),
|
|
137
149
|
not a state: the live payload carries `limited: true` together with `fiveHour.exceeded: false`.
|
|
@@ -311,7 +323,12 @@ provider effective-date fallback (`effectiveDay`, `effectiveDays`,
|
|
|
311
323
|
folder basename, used by the client to match a sidebar hover card; the full
|
|
312
324
|
`cwd` is Host-only and never sent on the wire), the rolling 5-hour local window
|
|
313
325
|
(`fiveHours`, DSH-only because external harnesses are day-granular), and the
|
|
314
|
-
bounded `daily[]` series used by the chart view
|
|
326
|
+
bounded `daily[]` series used by the chart view, the `speed` series behind the
|
|
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
|
|
329
|
+
period start when the Host learned it, and `sinceEffective` is summed from the
|
|
330
|
+
per-day buckets inside `[window start, today]` rather than borrowed from the
|
|
331
|
+
30-day or all-time bucket — v1.7.2). Absolute local paths are
|
|
315
332
|
stripped from the wire; free-form error messages have path substrings replaced
|
|
316
333
|
with `[path]`.
|
|
317
334
|
|
|
@@ -357,7 +374,7 @@ with `[path]`.
|
|
|
357
374
|
"last30Days": { "total": …, "messages": … },
|
|
358
375
|
"allTime": { "total": …, "messages": … } }
|
|
359
376
|
],
|
|
360
|
-
"daily": [ // <=
|
|
377
|
+
"daily": [ // <=30 days (v1.7.0), ascending; chart series
|
|
361
378
|
{ "day": "2026-10-25",
|
|
362
379
|
"total": { "total": 123456, "messages": 42 },
|
|
363
380
|
"hours": [0, 0, 0, 1234, 5678, …], // 24 integers: local-hour token totals
|
|
@@ -366,6 +383,25 @@ with `[path]`.
|
|
|
366
383
|
"hours": [0, 0, 0, 500, 734, …] }
|
|
367
384
|
} }
|
|
368
385
|
],
|
|
386
|
+
// v1.7.0: output-speed (TPS) series. `tps` is output tokens over the summed
|
|
387
|
+
// model time of the steps that produced them (`step/start` ->
|
|
388
|
+
// `assistant/message`), so a step's tool calls — which are emitted after the
|
|
389
|
+
// message — do not dilute it. A day with no positive duration is omitted
|
|
390
|
+
// instead of being given an infinite rate. `total.tps` is null when the
|
|
391
|
+
// window holds no samples. Each provider row carries its own `speed` object
|
|
392
|
+
// with the same shape, following the same window as its tokens (v1.7.2).
|
|
393
|
+
"speed": {
|
|
394
|
+
"byDay": [
|
|
395
|
+
{ "day": "2026-11-05", "outputTokens": 516951, "seconds": 8493.1,
|
|
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 }, … ] }
|
|
402
|
+
],
|
|
403
|
+
"total": { "outputTokens": 939643, "seconds": 14123.9, "tps": 66.5, "steps": 1424 }
|
|
404
|
+
},
|
|
369
405
|
"zcode": { // extra-harness detail
|
|
370
406
|
"dbPath": "C:\\Users\\…\\.zcode\\cli\\db\\db.sqlite",
|
|
371
407
|
"available": true, "error": null, "rows": 65, "cached": false,
|
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
|
@@ -165,6 +165,8 @@ window.__ModuleLoader__.load({
|
|
|
165
165
|
}
|
|
166
166
|
/** The npm package page the 「打开 npm」 update links point at. */
|
|
167
167
|
const NPM_PACKAGE_URL = 'https://www.npmjs.com/package/dsh-tokenlens'
|
|
168
|
+
/** The source repository, linked to the left of the version (v1.7.6). */
|
|
169
|
+
const REPO_URL = 'https://github.com/senyingxidi/dsh-tokenlens'
|
|
168
170
|
|
|
169
171
|
const zh = {
|
|
170
172
|
title: '词元透镜(dsh-TokenLens)',
|
|
@@ -231,6 +233,12 @@ window.__ModuleLoader__.load({
|
|
|
231
233
|
statsSpeedTotal: '均值 {tps} tok/s',
|
|
232
234
|
statsSpeedPeak: '峰值 {tps} tok/s',
|
|
233
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: '返回日视图',
|
|
234
242
|
statsSpeedBarTitle: '{date} · {tps} tok/s({tokens} 输出 ÷ {seconds}s)',
|
|
235
243
|
statsSpeedDetail: '{tokens} 输出 ÷ {seconds}s',
|
|
236
244
|
statsLast7Days: '最近7天',
|
|
@@ -272,6 +280,8 @@ window.__ModuleLoader__.load({
|
|
|
272
280
|
capacityTokenNoEstimate: '预计可消耗 —',
|
|
273
281
|
capacityTokenEstimateHint: '本机 Token 记录 ÷ 当前套餐百分比估算,仅供参考',
|
|
274
282
|
pluginVersion: '插件 v{version}',
|
|
283
|
+
repoLink: 'GitHub',
|
|
284
|
+
repoLinkHint: '在 GitHub 上打开 dsh-TokenLens 仓库(源码、更新日志、问题反馈)',
|
|
275
285
|
statsToday: '今日',
|
|
276
286
|
statsAllTime: '总计',
|
|
277
287
|
statsInput: '输入',
|
|
@@ -506,6 +516,13 @@ window.__ModuleLoader__.load({
|
|
|
506
516
|
statsSpeedTotal: 'avg {tps} tok/s',
|
|
507
517
|
statsSpeedPeak: 'peak {tps} tok/s',
|
|
508
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',
|
|
509
526
|
statsSpeedBarTitle: '{date} · {tps} tok/s ({tokens} output ÷ {seconds}s)',
|
|
510
527
|
statsSpeedDetail: '{tokens} output ÷ {seconds}s',
|
|
511
528
|
statsLast7Days: 'Last 7 days',
|
|
@@ -547,6 +564,8 @@ window.__ModuleLoader__.load({
|
|
|
547
564
|
capacityTokenNoEstimate: 'Est. quota —',
|
|
548
565
|
capacityTokenEstimateHint: 'Local token records ÷ current plan percentage; estimate only',
|
|
549
566
|
pluginVersion: 'Plugin v{version}',
|
|
567
|
+
repoLink: 'GitHub',
|
|
568
|
+
repoLinkHint: 'Open the dsh-TokenLens repository on GitHub (source, changelog, issues)',
|
|
550
569
|
statsToday: 'Today',
|
|
551
570
|
statsAllTime: 'All time',
|
|
552
571
|
statsInput: 'Input',
|
|
@@ -774,6 +793,10 @@ window.__ModuleLoader__.load({
|
|
|
774
793
|
windows: ['M2.6 3.4h10.8v9.2H2.6z', 'M8 3.4v9.2', 'M2.6 7.9h10.8'],
|
|
775
794
|
coin: ['M8 2.4a5.6 5.6 0 1 0 0 11.2A5.6 5.6 0 0 0 8 2.4Z', 'M8 5.2v5.6', 'M6.3 6.6h3.4', 'M6.3 9.4h3.4'],
|
|
776
795
|
check: ['M3 8.4 6.2 11.4 13 4.4'],
|
|
796
|
+
// v1.7.6: the repository mark, drawn stroke-only like every other icon.
|
|
797
|
+
github: [
|
|
798
|
+
'M8 2.4a5.6 5.6 0 0 0-1.7 10.9c.3.1.4-.1.4-.3v-1c-1.5.3-1.9-.7-1.9-.7-.2-.6-.6-.7-.6-.7-.5-.4 0-.4 0-.4.5 0 .8.5.8.5.5.9 1.3.6 1.7.5 0-.4.2-.7.3-.8-1.3-.1-2.6-.6-2.6-2.7 0-.6.2-1.1.6-1.5-.1-.1-.2-.7 0-1.4 0 0 .5-.2 1.6.6a5.4 5.4 0 0 1 2.8 0c1.1-.8 1.6-.6 1.6-.6.3.7.1 1.3.1 1.4.3.4.5.9.5 1.5 0 2.1-1.3 2.6-2.6 2.7.2.2.4.5.4 1v1.5c0 .2.1.4.4.3A5.6 5.6 0 0 0 8 2.4Z',
|
|
799
|
+
],
|
|
777
800
|
alert: ['M8 2.5 14 13.2H2z', 'M8 6.2v3.2', 'M8 11.3h.01'],
|
|
778
801
|
spinner: ['M8 2.5a5.5 5.5 0 1 1-5.5 5.5', 'M8 5.5v2.8'],
|
|
779
802
|
ban: ['M3.2 3.2 12.8 12.8', 'M8 2.4a5.6 5.6 0 1 0 0 11.2A5.6 5.6 0 0 0 8 2.4Z'],
|
|
@@ -1601,13 +1624,31 @@ window.__ModuleLoader__.load({
|
|
|
1601
1624
|
const byDay = Array.isArray(raw.byDay)
|
|
1602
1625
|
? raw.byDay
|
|
1603
1626
|
.filter((entry) => entry !== null && typeof entry === 'object' && typeof entry.day === 'string')
|
|
1604
|
-
.map((entry) =>
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
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
|
+
})
|
|
1611
1652
|
.filter((entry) => entry.tps !== null && entry.tps >= 0)
|
|
1612
1653
|
: []
|
|
1613
1654
|
const totalRaw = raw.total !== null && typeof raw.total === 'object' ? raw.total : {}
|
|
@@ -2097,6 +2138,9 @@ window.__ModuleLoader__.load({
|
|
|
2097
2138
|
chartHover: null,
|
|
2098
2139
|
// v1.7.2: the hovered point of the output-speed (TPS) curve.
|
|
2099
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,
|
|
2100
2144
|
// v1.2.0 chart drill-down: the clicked day whose 0..23 hour histogram
|
|
2101
2145
|
// is charted instead of the daily trend, or null (daily trend).
|
|
2102
2146
|
chartDay: null,
|
|
@@ -2206,6 +2250,12 @@ window.__ModuleLoader__.load({
|
|
|
2206
2250
|
detailsWindow: settings.detailsWindow,
|
|
2207
2251
|
chartHover: null,
|
|
2208
2252
|
chartDay: null,
|
|
2253
|
+
// v1.7.6: the TPS hover index is view state like the chart's, so it must
|
|
2254
|
+
// not survive a provider switch (the series behind it changed).
|
|
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,
|
|
2209
2259
|
}
|
|
2210
2260
|
}
|
|
2211
2261
|
|
|
@@ -3312,7 +3362,12 @@ window.__ModuleLoader__.load({
|
|
|
3312
3362
|
if (modelChanged) store.set({ modelId: nextModel })
|
|
3313
3363
|
syncProvider(providerId)
|
|
3314
3364
|
const after = store.get()
|
|
3315
|
-
|
|
3365
|
+
// v1.7.6: only Command Code's allowance follows the model, so only its
|
|
3366
|
+
// snapshots are worth refetching on a switch (see index.js
|
|
3367
|
+
// MODEL_SENSITIVE_ALLOWANCE). Everyone else keeps the cached snapshot.
|
|
3368
|
+
const modelSensitive =
|
|
3369
|
+
after.providerId !== null && canonicalProviderId(after.providerId) === 'commandcode'
|
|
3370
|
+
if (modelChanged && modelSensitive && after.inFlight !== true) {
|
|
3316
3371
|
void requestUsage('snapshot')
|
|
3317
3372
|
}
|
|
3318
3373
|
}
|
|
@@ -3485,6 +3540,10 @@ window.__ModuleLoader__.load({
|
|
|
3485
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}
|
|
3486
3541
|
.dsh-tokenlens-speed-plot{position:relative;flex:1 1 auto;min-width:0}
|
|
3487
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)}
|
|
3488
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}
|
|
3489
3548
|
.dsh-tokenlens-speed-axis>span{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
3490
3549
|
@media (max-width:680px){.dsh-tokenlens-chart-layout-row>*{flex-basis:100%!important}}
|
|
@@ -3614,7 +3673,11 @@ window.__ModuleLoader__.load({
|
|
|
3614
3673
|
.dsh-tokenlens-chart-drill-title{color:var(--dsw-alias-label-secondary);font-size:var(--dsw-alias-font-size-sm,12px);line-height:1.4;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-variant-numeric:tabular-nums}
|
|
3615
3674
|
/* 明细 donut window switch (v1.2.0): one pill row under the details title. */
|
|
3616
3675
|
.dsh-tokenlens-details-window{display:flex;align-items:center;gap:6px;flex-wrap:wrap;min-width:0}
|
|
3617
|
-
.dsh-tokenlens-version{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:1.5;text-align:right;opacity:.75;overflow-wrap:anywhere}
|
|
3676
|
+
.dsh-tokenlens-version{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:1.5;text-align:right;opacity:.75;overflow-wrap:anywhere;display:flex;align-items:center;justify-content:flex-end;gap:6px;flex-wrap:wrap}
|
|
3677
|
+
/* v1.7.6: the repository link sits left of the version text. */
|
|
3678
|
+
.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
|
+
.dsh-tokenlens-repo:hover,.dsh-tokenlens-repo:focus-visible{color:var(--dsw-alias-label-primary,currentColor);opacity:1;text-decoration:underline}
|
|
3680
|
+
.dsh-tokenlens-version-text{flex:0 0 auto}
|
|
3618
3681
|
/* Update status on the plugin-version line (dialog body): wraps on narrow screens. */
|
|
3619
3682
|
.dsh-tokenlens-version-update{white-space:normal;overflow-wrap:anywhere}
|
|
3620
3683
|
.dsh-tokenlens-version .dsh-tokenlens-update-link{color:inherit;font-size:inherit;vertical-align:baseline}
|
|
@@ -4495,7 +4558,7 @@ window.__ModuleLoader__.load({
|
|
|
4495
4558
|
'data-scope': scope,
|
|
4496
4559
|
'data-active': effective === scope ? 'true' : 'false',
|
|
4497
4560
|
disabled: stats === null,
|
|
4498
|
-
onClick: () => store.set({ statsScope: scope, chartHover: null, chartDay: null }),
|
|
4561
|
+
onClick: () => store.set({ statsScope: scope, chartHover: null, chartDay: null, speedHover: null }),
|
|
4499
4562
|
},
|
|
4500
4563
|
label,
|
|
4501
4564
|
)
|
|
@@ -4521,7 +4584,7 @@ window.__ModuleLoader__.load({
|
|
|
4521
4584
|
'data-active': active === view ? 'true' : 'false',
|
|
4522
4585
|
'aria-pressed': active === view ? 'true' : 'false',
|
|
4523
4586
|
onClick: () => {
|
|
4524
|
-
if (active !== view) store.set({ statsView: view, chartHover: null, chartDay: null })
|
|
4587
|
+
if (active !== view) store.set({ statsView: view, chartHover: null, chartDay: null, speedHover: null })
|
|
4525
4588
|
},
|
|
4526
4589
|
},
|
|
4527
4590
|
Icon(iconName, 12),
|
|
@@ -4707,6 +4770,24 @@ window.__ModuleLoader__.load({
|
|
|
4707
4770
|
if (store.get().speedHover !== next) store.set({ speedHover: next })
|
|
4708
4771
|
}
|
|
4709
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
|
+
|
|
4710
4791
|
/** The active today bucket of the current stats scope (v0.27.2). */
|
|
4711
4792
|
function todayBucketOf(ctx) {
|
|
4712
4793
|
return ctx.providerScoped ? ctx.currentRow?.today ?? null : ctx.stats?.today ?? null
|
|
@@ -6114,17 +6195,33 @@ window.__ModuleLoader__.load({
|
|
|
6114
6195
|
}
|
|
6115
6196
|
|
|
6116
6197
|
/**
|
|
6117
|
-
* 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.
|
|
6118
6203
|
*
|
|
6119
6204
|
* `tps` is output tokens over the model's OWN request time (`step/start` to
|
|
6120
|
-
* `assistant/message`), so
|
|
6205
|
+
* `assistant/message`), so a step's tool calls do not dilute it — but
|
|
6121
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.
|
|
6122
6210
|
*/
|
|
6123
6211
|
function buildSpeedSection(state, ctx, t) {
|
|
6124
|
-
|
|
6125
|
-
const
|
|
6126
|
-
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
|
|
6127
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
|
+
|
|
6128
6225
|
const head = h(
|
|
6129
6226
|
'div',
|
|
6130
6227
|
{ key: 'head', className: 'dsh-tokenlens-section-head' },
|
|
@@ -6134,203 +6231,266 @@ window.__ModuleLoader__.load({
|
|
|
6134
6231
|
Icon('chart', 14),
|
|
6135
6232
|
t('statsSpeedTitle'),
|
|
6136
6233
|
),
|
|
6137
|
-
|
|
6138
|
-
|
|
6139
|
-
|
|
6140
|
-
|
|
6141
|
-
|
|
6142
|
-
|
|
6143
|
-
|
|
6144
|
-
)
|
|
6145
|
-
const body =
|
|
6146
|
-
byDay.length === 0
|
|
6147
|
-
? h(
|
|
6148
|
-
'div',
|
|
6149
|
-
{ key: 'empty', className: 'dsh-tokenlens-chart-empty', 'data-speed-empty': 'true' },
|
|
6150
|
-
h('span', { key: 'hint', className: 'dsh-tokenlens-hint' }, t('statsSpeedEmpty')),
|
|
6151
|
-
)
|
|
6152
|
-
: (() => {
|
|
6153
|
-
// v1.7.1: a CURVE, not bars — the trend is a rate, and a line reads
|
|
6154
|
-
// it far better than per-day columns. The SVG scales to the card
|
|
6155
|
-
// width; `preserveAspectRatio="none"` lets it fill without keeping
|
|
6156
|
-
// the viewBox ratio, and the per-day hover targets are transparent
|
|
6157
|
-
// columns so nothing is distorted.
|
|
6158
|
-
const width = 320
|
|
6159
|
-
const height = 90
|
|
6160
|
-
const pad = 4
|
|
6161
|
-
const max = Math.max(...byDay.map((entry) => entry.tps ?? 0), 1)
|
|
6162
|
-
const stepX = byDay.length > 1 ? (width - pad * 2) / (byDay.length - 1) : 0
|
|
6163
|
-
const points = byDay.map((entry, index) => {
|
|
6164
|
-
const x = byDay.length > 1 ? pad + stepX * index : width / 2
|
|
6165
|
-
const y = height - pad - ((entry.tps ?? 0) / max) * (height - pad * 2)
|
|
6166
|
-
return { x, y }
|
|
6167
|
-
})
|
|
6168
|
-
const line = points.map((point) => `${point.x.toFixed(1)},${point.y.toFixed(1)}`).join(' ')
|
|
6169
|
-
const area = `${pad.toFixed(1)},${(height - pad).toFixed(1)} ${line} ${(width - pad).toFixed(1)},${(height - pad).toFixed(1)}`
|
|
6170
|
-
const columnWidth = byDay.length > 1 ? stepX : width - pad * 2
|
|
6171
|
-
const columns = byDay.map((entry, index) =>
|
|
6172
|
-
h(
|
|
6173
|
-
'rect',
|
|
6174
|
-
{
|
|
6175
|
-
key: `hit-${entry.day}-${index}`,
|
|
6176
|
-
x: (points[index].x - columnWidth / 2).toFixed(1),
|
|
6177
|
-
y: 0,
|
|
6178
|
-
width: Math.max(1, columnWidth).toFixed(1),
|
|
6179
|
-
height: String(height),
|
|
6180
|
-
fill: 'transparent',
|
|
6181
|
-
'data-speed-day': entry.day,
|
|
6182
|
-
'data-speed-tps': String(entry.tps ?? 0),
|
|
6183
|
-
},
|
|
6184
|
-
h(
|
|
6185
|
-
'title',
|
|
6186
|
-
{ key: 'tip' },
|
|
6187
|
-
t('statsSpeedBarTitle', {
|
|
6188
|
-
date: entry.day,
|
|
6189
|
-
tps: (entry.tps ?? 0).toFixed(1),
|
|
6190
|
-
tokens: formatTokensPrecise(entry.outputTokens),
|
|
6191
|
-
seconds: entry.seconds.toFixed(1),
|
|
6192
|
-
}),
|
|
6193
|
-
),
|
|
6194
|
-
),
|
|
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) }),
|
|
6195
6241
|
)
|
|
6196
|
-
|
|
6197
|
-
|
|
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',
|
|
6198
6249
|
{
|
|
6199
|
-
key: '
|
|
6200
|
-
|
|
6201
|
-
'
|
|
6202
|
-
|
|
6203
|
-
|
|
6204
|
-
role: 'img',
|
|
6205
|
-
'aria-label': t('statsSpeedTitle'),
|
|
6250
|
+
key: 'back',
|
|
6251
|
+
type: 'button',
|
|
6252
|
+
className: 'dsh-tokenlens-link-button',
|
|
6253
|
+
'data-speed-back': 'true',
|
|
6254
|
+
onClick: () => clearSpeedDay(),
|
|
6206
6255
|
},
|
|
6207
|
-
|
|
6208
|
-
|
|
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}`,
|
|
6209
6340
|
className: 'dsh-tokenlens-speed-area',
|
|
6210
6341
|
points: area,
|
|
6211
6342
|
fill: 'rgba(46,158,91,.18)',
|
|
6212
6343
|
'aria-hidden': 'true',
|
|
6213
6344
|
}),
|
|
6214
|
-
|
|
6215
|
-
|
|
6216
|
-
|
|
6217
|
-
|
|
6218
|
-
|
|
6219
|
-
|
|
6220
|
-
|
|
6221
|
-
|
|
6222
|
-
|
|
6223
|
-
|
|
6224
|
-
|
|
6225
|
-
|
|
6226
|
-
|
|
6227
|
-
|
|
6228
|
-
|
|
6229
|
-
|
|
6230
|
-
|
|
6231
|
-
|
|
6232
|
-
|
|
6233
|
-
|
|
6234
|
-
|
|
6235
|
-
|
|
6236
|
-
|
|
6237
|
-
|
|
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
|
-
transform: 'translateX(-50%)',
|
|
6288
|
-
},
|
|
6289
|
-
},
|
|
6290
|
-
`${hovered.day} · ${(hovered.tps ?? 0).toFixed(1)} tok/s`,
|
|
6291
|
-
` · ${t('statsSpeedDetail', {
|
|
6292
|
-
tokens: formatTokensPrecise(hovered.outputTokens),
|
|
6293
|
-
seconds: hovered.seconds.toFixed(1),
|
|
6294
|
-
})}`,
|
|
6295
|
-
)
|
|
6296
|
-
const axis = h(
|
|
6297
|
-
'div',
|
|
6298
|
-
{ key: 'axis', className: 'dsh-tokenlens-speed-axis', 'aria-hidden': 'true' },
|
|
6299
|
-
h('span', { key: 'first' }, byDay[0].day),
|
|
6300
|
-
byDay.length > 1 ? h('span', { key: 'last' }, byDay[byDay.length - 1].day) : null,
|
|
6301
|
-
)
|
|
6302
|
-
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(
|
|
6303
6418
|
'div',
|
|
6304
|
-
{
|
|
6305
|
-
|
|
6306
|
-
|
|
6307
|
-
'
|
|
6308
|
-
|
|
6309
|
-
|
|
6310
|
-
|
|
6311
|
-
|
|
6312
|
-
|
|
6313
|
-
|
|
6314
|
-
|
|
6315
|
-
|
|
6316
|
-
|
|
6317
|
-
|
|
6318
|
-
|
|
6319
|
-
|
|
6320
|
-
|
|
6321
|
-
|
|
6322
|
-
|
|
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
|
+
})}`,
|
|
6323
6439
|
)
|
|
6324
|
-
|
|
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
|
+
|
|
6325
6482
|
return h(
|
|
6326
6483
|
'div',
|
|
6327
6484
|
{ key: 'speed', className: 'dsh-tokenlens-section', 'data-speed-section': 'true' },
|
|
6328
6485
|
head,
|
|
6329
|
-
h(
|
|
6486
|
+
h(
|
|
6487
|
+
'div',
|
|
6488
|
+
{ key: 'hint', className: 'dsh-tokenlens-subtitle' },
|
|
6489
|
+
dailyMode ? t('statsSpeedHint') : t('statsSpeedHourHint'),
|
|
6490
|
+
),
|
|
6330
6491
|
body,
|
|
6331
6492
|
)
|
|
6332
6493
|
}
|
|
6333
|
-
|
|
6334
6494
|
function buildStatsDetails(state, ctx, t) {
|
|
6335
6495
|
const { stats, hasMonth } = ctx
|
|
6336
6496
|
if (stats === null) return null
|
|
@@ -7804,11 +7964,29 @@ window.__ModuleLoader__.load({
|
|
|
7804
7964
|
const body = [buildManageButton(state, t), statsArea]
|
|
7805
7965
|
const displayVersion = currentPluginVersionOf(state)
|
|
7806
7966
|
if (displayVersion !== '') {
|
|
7967
|
+
// v1.7.6: the repository link sits to the LEFT of the version, so the
|
|
7968
|
+
// version stays the right-most element of the row.
|
|
7969
|
+
const repo = h(
|
|
7970
|
+
'a',
|
|
7971
|
+
{
|
|
7972
|
+
key: 'repo',
|
|
7973
|
+
className: 'dsh-tokenlens-repo',
|
|
7974
|
+
href: REPO_URL,
|
|
7975
|
+
target: '_blank',
|
|
7976
|
+
rel: 'noreferrer noopener',
|
|
7977
|
+
'data-repo-link': 'true',
|
|
7978
|
+
title: t('repoLinkHint'),
|
|
7979
|
+
'aria-label': t('repoLinkHint'),
|
|
7980
|
+
},
|
|
7981
|
+
Icon('github', 13),
|
|
7982
|
+
t('repoLink'),
|
|
7983
|
+
)
|
|
7807
7984
|
body.push(
|
|
7808
7985
|
h(
|
|
7809
7986
|
'div',
|
|
7810
7987
|
{ key: 'version', className: 'dsh-tokenlens-version' },
|
|
7811
|
-
|
|
7988
|
+
repo,
|
|
7989
|
+
h('span', { key: 'text', className: 'dsh-tokenlens-version-text' }, t('pluginVersion', { version: displayVersion })),
|
|
7812
7990
|
// Update status sits right next to the plugin version: 有新版本 vX
|
|
7813
7991
|
// (link to the npm page) or 已是最新版本, only for a successful check.
|
|
7814
7992
|
buildVersionUpdateStatus(state, t),
|
package/index.js
CHANGED
|
@@ -84,7 +84,16 @@ 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
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Providers whose monthly allowance depends on the SELECTED model (v1.7.6).
|
|
91
|
+
*
|
|
92
|
+
* Only these need their cached snapshot dropped when the picker changes: for
|
|
93
|
+
* everyone else a model switch does not alter a single number, so refetching
|
|
94
|
+
* would be pure waste.
|
|
95
|
+
*/
|
|
96
|
+
const MODEL_SENSITIVE_ALLOWANCE = new Set(['commandcode'])
|
|
88
97
|
|
|
89
98
|
/** Documented defaults; every field of the raw row config is optional. */
|
|
90
99
|
const DEFAULTS = {
|
|
@@ -1460,8 +1469,7 @@ export function apply(ctx, rawConfig) {
|
|
|
1460
1469
|
return catalog
|
|
1461
1470
|
}
|
|
1462
1471
|
|
|
1463
|
-
const statsService = config.stats.enabled
|
|
1464
|
-
? createUsageStats({
|
|
1472
|
+
const statsService = config.stats.enabled ? createUsageStats({
|
|
1465
1473
|
dshHome: config.stats.dshHome !== '' ? config.stats.dshHome : dshHomeOf(process.env),
|
|
1466
1474
|
config,
|
|
1467
1475
|
providerCatalog: providerModelCatalog,
|
|
@@ -1614,6 +1622,14 @@ export function apply(ctx, rawConfig) {
|
|
|
1614
1622
|
return scrubMessagePaths(out)
|
|
1615
1623
|
}
|
|
1616
1624
|
|
|
1625
|
+
/**
|
|
1626
|
+
* The subscription-anchor set the last stats result was computed with (v1.7.6).
|
|
1627
|
+
*
|
|
1628
|
+
* Empty until a plan period start is known; a change invalidates the cached
|
|
1629
|
+
* stats result so the 最近X天 window is anchored immediately.
|
|
1630
|
+
*/
|
|
1631
|
+
let statsAnchorSignature = '[]'
|
|
1632
|
+
|
|
1617
1633
|
async function getStats(options = {}) {
|
|
1618
1634
|
if (statsService === null) {
|
|
1619
1635
|
return {
|
|
@@ -1622,11 +1638,18 @@ export function apply(ctx, rawConfig) {
|
|
|
1622
1638
|
}
|
|
1623
1639
|
}
|
|
1624
1640
|
try {
|
|
1641
|
+
const starts = Object.fromEntries(state.subscriptionStarts)
|
|
1642
|
+
// v1.7.6: a cached stats result computed BEFORE a plan period start was
|
|
1643
|
+
// known would keep the un-anchored window until the TTL expired. Recompute
|
|
1644
|
+
// once whenever the anchor set changes, so the window is right immediately.
|
|
1645
|
+
const signature = JSON.stringify(Object.entries(starts).sort())
|
|
1646
|
+
const anchorChanged = signature !== statsAnchorSignature
|
|
1625
1647
|
const stats = await statsService.get({
|
|
1626
|
-
fresh: options.fresh === true,
|
|
1648
|
+
fresh: options.fresh === true || (signature !== '[]' && anchorChanged),
|
|
1627
1649
|
// v1.7.2: anchor each subscription provider's window to its plan start.
|
|
1628
|
-
subscriptionStarts:
|
|
1650
|
+
subscriptionStarts: starts,
|
|
1629
1651
|
})
|
|
1652
|
+
if (signature !== '[]') statsAnchorSignature = signature
|
|
1630
1653
|
// v0.22.1: only subscription-backed providers use the 最近X天 window;
|
|
1631
1654
|
// pay-as-you-go/balance providers keep the fixed 最近30天 column.
|
|
1632
1655
|
const decorated =
|
|
@@ -1835,6 +1858,10 @@ export function apply(ctx, rawConfig) {
|
|
|
1835
1858
|
* V4.1 Flash $100 while its credit pool is $70), so a model switch must also
|
|
1836
1859
|
* drop the cached snapshot — otherwise the chip would keep showing the old
|
|
1837
1860
|
* allowance until the next poll.
|
|
1861
|
+
*
|
|
1862
|
+
* v1.7.6: only for providers whose allowance is actually model-sensitive.
|
|
1863
|
+
* Invalidating on every provider made switching models on DeepSeek/OpenCode
|
|
1864
|
+
* throw away a perfectly good snapshot and refetch it.
|
|
1838
1865
|
*/
|
|
1839
1866
|
function rememberRequestedModel(body, providerId) {
|
|
1840
1867
|
const modelId = typeof body?.modelId === 'string' ? body.modelId.trim() : ''
|
|
@@ -1842,8 +1869,10 @@ export function apply(ctx, rawConfig) {
|
|
|
1842
1869
|
const id = canonicalProviderId(providerId)
|
|
1843
1870
|
if (state.selectedModels.get(id) === modelId) return
|
|
1844
1871
|
state.selectedModels.set(id, modelId)
|
|
1845
|
-
|
|
1846
|
-
|
|
1872
|
+
if (MODEL_SENSITIVE_ALLOWANCE.has(id)) {
|
|
1873
|
+
state.caches.delete(id)
|
|
1874
|
+
state.flights.delete(id)
|
|
1875
|
+
}
|
|
1847
1876
|
noteStatus('client-model', { providerId: id, modelId })
|
|
1848
1877
|
}
|
|
1849
1878
|
|
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",
|