dsh-tokenlens 1.6.7 → 1.7.1
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 +22 -0
- package/README.en.md +1 -1
- package/README.md +1 -1
- package/client.js +274 -42
- package/index.js +1 -1
- package/lib/usage-stats.mjs +143 -8
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,28 @@
|
|
|
4
4
|
|
|
5
5
|
**版本记录约定**:同一发布日只保留一个章节,标题取当日**最后一个**已发布版本号;当日多次发布的改动合并到该章节,并在标题下注明当日实际发布过的版本号(npm 上仍可安装这些独立版本)。若当日已有发布,后续小改动不再单独增加版本号,而是并入下一篇日志。这样日志按天规整、不出现同日多个小版本。
|
|
6
6
|
|
|
7
|
+
## 1.7.1 — 2026-11-06
|
|
8
|
+
|
|
9
|
+
(当日已发布版本:v1.6.5 / v1.6.6 / v1.6.7 / v1.7.0 / v1.7.1)
|
|
10
|
+
|
|
11
|
+
- **fix(ui): 未最大化窗口时芯片悬停导致整个对话区闪烁**——`Tooltip` 的气泡锚在芯片右缘,窗口变窄时会溢出 dock,撑出**对话区底部的横向滚动条**;滚动条又挤压布局、导致 hover 被反复触发,于是整片区域持续重绘。芯片改用**原生 `title`**:不产生任何 DOM,不可能影响布局。v1.7.0 只去掉原生 `title` 保留 Tooltip,方向反了——真正的成因是 Tooltip 本身
|
|
12
|
+
- **feat(ui): TPS 趋势图由柱状改为曲线**——速率是连续量,折线比逐日立柱更易读。改为单条 `polyline` 曲线 + 淡色面积填充;每天一个透明命中列承载 `title`,鼠标移上去显示「日期 · 速率(输出 ÷ 耗时)」
|
|
13
|
+
- fix(ui): **TPS 卡片宽度与「明细」卡片一致**(`flex: 1 1 100%`)——此前它没有 flex 规则,宽度收缩到内容宽
|
|
14
|
+
- 测试:363 → 364(芯片不再包 Tooltip、标题为原生字符串、速度卡片满宽、曲线为 polyline+area 且不填充线体、透明命中列携带日期与速率)
|
|
15
|
+
|
|
16
|
+
## 1.7.0 — 2026-11-06
|
|
17
|
+
|
|
18
|
+
(当日已发布版本:v1.6.5 / v1.6.6 / v1.6.7 / v1.7.0)
|
|
19
|
+
|
|
20
|
+
- **fix(ui): 弹窗改为 portal 到 `<body>`,恢复视口居中**——DSH 0.2.2-rc.1 里 `conversation.input.overlay` 槽位的祖先建立了包含块(transform / filter / contain),`position: fixed` 因此相对「对话区」解析,弹窗只能贴在对话区底部。改用 `react-dom` 的 `createPortal` 挂到 `document.body`,彻底脱离该祖先链。`react-dom` 缺失时(含 Node 测试环境)自动回退为原地渲染,绝不抛错
|
|
21
|
+
- **fix(ui): 芯片悬停不再让整个对话区闪烁**(v1.7.1 进一步修正为改用原生 `title`)——v1.7.0 曾移除冗余的原生 `title` 并保留平台 `Tooltip`,但窄窗口下成因其实是 Tooltip 气泡溢出
|
|
22
|
+
- **feat(ui): 新增「输出速度(TPS)」趋势图**,位于明细下方、独占整行——输出 Token ÷ 模型自身耗时(`step/start` → `assistant/message`)。工具调用发生在消息之后,因此等待时间不被计入;输出 Token 含思考 Token,弹窗提示已说明
|
|
23
|
+
- feat(stats): Host 侧新增 `speed` 序列(顶层为全 Harness 合计,每个 provider 行另有自己的序列,跟随作用域切换);按天聚合 `{day, outputTokens, seconds, tps, steps}`,无正耗时的日期直接跳过而不是给出无限速率。缓存 schema 升至 6,首次会重扫
|
|
24
|
+
- **fix(stats): 「最近X天」封顶 30 天**——此前 provider 首次出现超过 30 天时会报「最近90天」,而同期所有其他数字与图表都只覆盖 30 天。现在 `effectiveDays` 封顶 30,且 `sinceEffective` 在超窗时改用 30 天桶,保证**标签与数字描述同一区间**;`daily` 序列尾部由 120 天收敛为 30 天
|
|
25
|
+
- 测试:357 → 363(TPS 配对与未配对步骤跳过、零耗时日期跳过、provider 作用域序列、30 天封顶与标签/数据一致、弹窗 portal 与无 react-dom 回退、TPS 图表与空态)
|
|
26
|
+
|
|
27
|
+
## 1.7.0 之前的同日版本见下方各节
|
|
28
|
+
|
|
7
29
|
## 1.6.7 — 2026-11-05
|
|
8
30
|
|
|
9
31
|
(当日已发布版本:v1.6.5 / v1.6.6 / v1.6.7)
|
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 364 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`:**364 个自动化用例**,覆盖 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
|
@@ -40,6 +40,19 @@ window.__ModuleLoader__.load({
|
|
|
40
40
|
// words in the shell's module table, so they need no `dsh.client.external`.
|
|
41
41
|
const primitives = require('@deepseek-ai/dsh-client-ui-primitives')
|
|
42
42
|
const { Modal, Button, Tooltip, IconRefreshOutlineRegular } = primitives
|
|
43
|
+
// v1.7.0: the dialog is portalled to <body>. Rendering it inside the
|
|
44
|
+
// `conversation.input.overlay` slot stopped reaching the viewport in DSH
|
|
45
|
+
// 0.2.2-rc.1: an ancestor there establishes a containing block (transform /
|
|
46
|
+
// filter / contain), so `position: fixed` resolved against the conversation
|
|
47
|
+
// area and the dialog could only appear at its bottom. `createPortal` escapes
|
|
48
|
+
// that ancestor entirely. Tolerated as optional so a Host without react-dom
|
|
49
|
+
// (and the Node test harness) simply renders inline as before.
|
|
50
|
+
let ReactDOM = null
|
|
51
|
+
try {
|
|
52
|
+
ReactDOM = require('react-dom')
|
|
53
|
+
} catch {
|
|
54
|
+
ReactDOM = null
|
|
55
|
+
}
|
|
43
56
|
|
|
44
57
|
const h = React.createElement
|
|
45
58
|
|
|
@@ -215,6 +228,12 @@ window.__ModuleLoader__.load({
|
|
|
215
228
|
statsProviderFallback: '当前供应商({provider})没有本机记录,显示全部 Harness 合计',
|
|
216
229
|
statsDshTotalLabel: 'DSH 合计',
|
|
217
230
|
statsLast30Days: '最近30天',
|
|
231
|
+
statsSpeedTitle: '输出速度(TPS)',
|
|
232
|
+
statsSpeedHint: '输出 Token ÷ 模型自身耗时(step 开始到消息返回);不含工具调用等待,但输出含思考 Token',
|
|
233
|
+
statsSpeedTotal: '均值 {tps} tok/s',
|
|
234
|
+
statsSpeedPeak: '峰值 {tps} tok/s',
|
|
235
|
+
statsSpeedEmpty: '暂无可计算的速度样本(需要 step 计时数据)',
|
|
236
|
+
statsSpeedBarTitle: '{date} · {tps} tok/s({tokens} 输出 ÷ {seconds}s)',
|
|
218
237
|
statsLast7Days: '最近7天',
|
|
219
238
|
stats7DaysShort: '7天',
|
|
220
239
|
statsDaysShort: '{n}天',
|
|
@@ -484,6 +503,13 @@ window.__ModuleLoader__.load({
|
|
|
484
503
|
statsProviderFallback: 'No local records for the current provider ({provider}) — showing all-harness totals',
|
|
485
504
|
statsDshTotalLabel: 'DSH total',
|
|
486
505
|
statsLast30Days: 'Last 30 days',
|
|
506
|
+
statsSpeedTitle: 'Output speed (TPS)',
|
|
507
|
+
statsSpeedHint:
|
|
508
|
+
'Output tokens ÷ the model’s own request time (step start to message); tool waits excluded, reasoning tokens included',
|
|
509
|
+
statsSpeedTotal: 'avg {tps} tok/s',
|
|
510
|
+
statsSpeedPeak: 'peak {tps} tok/s',
|
|
511
|
+
statsSpeedEmpty: 'No speed samples yet (needs step timing data)',
|
|
512
|
+
statsSpeedBarTitle: '{date} · {tps} tok/s ({tokens} output ÷ {seconds}s)',
|
|
487
513
|
statsLast7Days: 'Last 7 days',
|
|
488
514
|
stats7DaysShort: '7d',
|
|
489
515
|
statsDaysShort: '{n}d',
|
|
@@ -1523,6 +1549,8 @@ window.__ModuleLoader__.load({
|
|
|
1523
1549
|
// v0.22.1: only subscription-backed providers use the 最近X天 window.
|
|
1524
1550
|
subscribed: entry.subscribed === true,
|
|
1525
1551
|
harnesses,
|
|
1552
|
+
// v1.7.0: this provider's own output-speed series.
|
|
1553
|
+
speed: normalizeSpeed(entry.speed),
|
|
1526
1554
|
}
|
|
1527
1555
|
}
|
|
1528
1556
|
|
|
@@ -1557,6 +1585,38 @@ window.__ModuleLoader__.load({
|
|
|
1557
1585
|
}
|
|
1558
1586
|
|
|
1559
1587
|
/** The whole stats payload, or null. Handles v1/v2 (DSH-ish) and v3 (provider-scoped). */
|
|
1588
|
+
/**
|
|
1589
|
+
* One output-speed (TPS) series (v1.7.0): `{byDay, total}` or null.
|
|
1590
|
+
*
|
|
1591
|
+
* `tps` is output tokens over the model's own request time, so a day whose
|
|
1592
|
+
* `seconds` is not positive is dropped rather than shown as an infinite rate.
|
|
1593
|
+
*/
|
|
1594
|
+
function normalizeSpeed(raw) {
|
|
1595
|
+
if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) return null
|
|
1596
|
+
const byDay = Array.isArray(raw.byDay)
|
|
1597
|
+
? raw.byDay
|
|
1598
|
+
.filter((entry) => entry !== null && typeof entry === 'object' && typeof entry.day === 'string')
|
|
1599
|
+
.map((entry) => ({
|
|
1600
|
+
day: entry.day,
|
|
1601
|
+
outputTokens: numberOrNull(entry.outputTokens) ?? 0,
|
|
1602
|
+
seconds: numberOrNull(entry.seconds) ?? 0,
|
|
1603
|
+
tps: numberOrNull(entry.tps),
|
|
1604
|
+
steps: numberOrNull(entry.steps) ?? 0,
|
|
1605
|
+
}))
|
|
1606
|
+
.filter((entry) => entry.tps !== null && entry.tps >= 0)
|
|
1607
|
+
: []
|
|
1608
|
+
const totalRaw = raw.total !== null && typeof raw.total === 'object' ? raw.total : {}
|
|
1609
|
+
return {
|
|
1610
|
+
byDay,
|
|
1611
|
+
total: {
|
|
1612
|
+
outputTokens: numberOrNull(totalRaw.outputTokens) ?? 0,
|
|
1613
|
+
seconds: numberOrNull(totalRaw.seconds) ?? 0,
|
|
1614
|
+
tps: numberOrNull(totalRaw.tps),
|
|
1615
|
+
steps: numberOrNull(totalRaw.steps) ?? 0,
|
|
1616
|
+
},
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1560
1620
|
function normalizeStats(raw) {
|
|
1561
1621
|
if (raw === null || typeof raw !== 'object') return null
|
|
1562
1622
|
const version = Number.isFinite(raw.version) ? raw.version : 1
|
|
@@ -1684,6 +1744,8 @@ window.__ModuleLoader__.load({
|
|
|
1684
1744
|
workspaces,
|
|
1685
1745
|
// v0.24.0 daily series for the chart mode (may be empty on old Hosts).
|
|
1686
1746
|
daily,
|
|
1747
|
+
// v1.7.0: output-speed (TPS) series across every harness.
|
|
1748
|
+
speed: normalizeSpeed(raw.speed),
|
|
1687
1749
|
zcode,
|
|
1688
1750
|
}
|
|
1689
1751
|
}
|
|
@@ -3380,6 +3442,11 @@ window.__ModuleLoader__.load({
|
|
|
3380
3442
|
.dsh-tokenlens-chart-layout-row>.dsh-tokenlens-capacity{flex:0.85 1 260px}
|
|
3381
3443
|
.dsh-tokenlens-chart-layout-row>.dsh-tokenlens-stats{flex:1.25 1 330px;min-width:0}
|
|
3382
3444
|
.dsh-tokenlens-chart-layout-row>.dsh-tokenlens-details{flex:1 1 100%;min-width:0}
|
|
3445
|
+
/* v1.7.1: the TPS trend is a full-width row, exactly like the details card. */
|
|
3446
|
+
.dsh-tokenlens-chart-layout-row>.dsh-tokenlens-section[data-speed-section]{flex:1 1 100%;min-width:0}
|
|
3447
|
+
.dsh-tokenlens-speed-svg{display:block;width:100%;height:96px;overflow:visible}
|
|
3448
|
+
.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}
|
|
3449
|
+
.dsh-tokenlens-speed-axis>span{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
3383
3450
|
@media (max-width:680px){.dsh-tokenlens-chart-layout-row>*{flex-basis:100%!important}}
|
|
3384
3451
|
.dsh-tokenlens-details-charts{min-width:0}
|
|
3385
3452
|
/* Data-mode 明细: three FULL-WIDTH blocks stacked vertically (v0.30.0 layout). */
|
|
@@ -3863,26 +3930,28 @@ window.__ModuleLoader__.load({
|
|
|
3863
3930
|
const snapshot = normalizeSnapshot(state.snapshot)
|
|
3864
3931
|
const brand = providerBrand(state, snapshot)
|
|
3865
3932
|
const tooltip = chipTooltip(snapshot, state.providerId, t)
|
|
3933
|
+
// v1.7.1: a NATIVE title, not the platform Tooltip. On an unmaximized
|
|
3934
|
+
// window the Tooltip's bubble rendered at the chip's right edge, overflowed
|
|
3935
|
+
// the dock and produced a horizontal scrollbar on the conversation area;
|
|
3936
|
+
// that scrollbar shifted the layout, which re-triggered the hover and
|
|
3937
|
+
// flickered the whole area. A native title draws nothing in the DOM, so it
|
|
3938
|
+
// cannot affect layout at all. The text is unchanged.
|
|
3866
3939
|
return h(
|
|
3867
3940
|
'span',
|
|
3868
3941
|
{ className: 'dsh-tokenlens-anchor', 'data-composer-stat': 'tokenlens' },
|
|
3869
3942
|
h(
|
|
3870
|
-
|
|
3871
|
-
{
|
|
3872
|
-
|
|
3873
|
-
'
|
|
3874
|
-
|
|
3875
|
-
|
|
3876
|
-
|
|
3877
|
-
|
|
3878
|
-
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
|
|
3882
|
-
h('span', { className: 'dsh-tokenlens-dot', 'data-level': chipLevel(snapshot, state), 'aria-hidden': true }),
|
|
3883
|
-
h('span', { className: 'dsh-tokenlens-brand' }, brand),
|
|
3884
|
-
chipPieces(snapshot, state.providerId, t),
|
|
3885
|
-
),
|
|
3943
|
+
'button',
|
|
3944
|
+
{
|
|
3945
|
+
type: 'button',
|
|
3946
|
+
className: 'dsh-tokenlens-chip',
|
|
3947
|
+
title: tooltip,
|
|
3948
|
+
'aria-label': t('chipHint'),
|
|
3949
|
+
'aria-haspopup': 'dialog',
|
|
3950
|
+
onClick: () => openDialog(sessionId),
|
|
3951
|
+
},
|
|
3952
|
+
h('span', { className: 'dsh-tokenlens-dot', 'data-level': chipLevel(snapshot, state), 'aria-hidden': true }),
|
|
3953
|
+
h('span', { className: 'dsh-tokenlens-brand' }, brand),
|
|
3954
|
+
chipPieces(snapshot, state.providerId, t),
|
|
3886
3955
|
),
|
|
3887
3956
|
)
|
|
3888
3957
|
}
|
|
@@ -3902,24 +3971,23 @@ window.__ModuleLoader__.load({
|
|
|
3902
3971
|
'span',
|
|
3903
3972
|
{ className: 'dsh-tokenlens-anchor', 'data-composer-stat': 'tokenlens' },
|
|
3904
3973
|
h(
|
|
3905
|
-
|
|
3906
|
-
{
|
|
3907
|
-
|
|
3908
|
-
'
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
3912
|
-
|
|
3913
|
-
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
|
|
3919
|
-
|
|
3920
|
-
|
|
3921
|
-
|
|
3922
|
-
),
|
|
3974
|
+
'button',
|
|
3975
|
+
{
|
|
3976
|
+
type: 'button',
|
|
3977
|
+
className: 'dsh-tokenlens-chip',
|
|
3978
|
+
// v1.7.1: native title, for the same reason as the normal chip — a
|
|
3979
|
+
// Tooltip bubble here could overflow a narrow dock and add a
|
|
3980
|
+
// horizontal scrollbar that made the conversation area flicker.
|
|
3981
|
+
title: label,
|
|
3982
|
+
'aria-label': label,
|
|
3983
|
+
'aria-haspopup': 'dialog',
|
|
3984
|
+
...(needsConfig ? { 'data-needs-config': 'true' } : {}),
|
|
3985
|
+
...(statsOnly ? { 'data-stats-only': 'true' } : {}),
|
|
3986
|
+
onClick: () => openDialog(sessionId),
|
|
3987
|
+
},
|
|
3988
|
+
h('span', { className: 'dsh-tokenlens-dot', 'data-level': statsOnly ? 'normal' : 'warning', 'aria-hidden': true }),
|
|
3989
|
+
h('span', { className: 'dsh-tokenlens-brand' }, brand),
|
|
3990
|
+
h('span', { className: 'dsh-tokenlens-metric' }, label),
|
|
3923
3991
|
),
|
|
3924
3992
|
)
|
|
3925
3993
|
}
|
|
@@ -4976,6 +5044,30 @@ window.__ModuleLoader__.load({
|
|
|
4976
5044
|
store.set({ showDetails: store.get().showDetails !== true })
|
|
4977
5045
|
}
|
|
4978
5046
|
|
|
5047
|
+
/**
|
|
5048
|
+
* Detach `node` from the slot's ancestor chain by rendering it into <body>
|
|
5049
|
+
* (v1.7.0).
|
|
5050
|
+
*
|
|
5051
|
+
* The `conversation.input.overlay` slot lives inside the conversation area,
|
|
5052
|
+
* and in DSH 0.2.2-rc.1 an ancestor there establishes a containing block, so
|
|
5053
|
+
* the overlay's `position: fixed` resolved against that ancestor and the
|
|
5054
|
+
* dialog could only appear at the bottom of the conversation. Portalling
|
|
5055
|
+
* detaches it so it centres on the viewport. Returns the node unchanged when
|
|
5056
|
+
* react-dom or a DOM is unavailable (the Node test harness), so the rendered
|
|
5057
|
+
* tree keeps its shape there.
|
|
5058
|
+
*/
|
|
5059
|
+
function portalize(node) {
|
|
5060
|
+
const canPortal =
|
|
5061
|
+
ReactDOM !== null &&
|
|
5062
|
+
typeof ReactDOM.createPortal === 'function' &&
|
|
5063
|
+
typeof document !== 'undefined' &&
|
|
5064
|
+
document !== null &&
|
|
5065
|
+
document.body !== undefined &&
|
|
5066
|
+
document.body !== null
|
|
5067
|
+
if (!canPortal) return node
|
|
5068
|
+
return ReactDOM.createPortal(node, document.body)
|
|
5069
|
+
}
|
|
5070
|
+
|
|
4979
5071
|
/**
|
|
4980
5072
|
* The dialog shell: a fixed overlay with an explicitly sized panel.
|
|
4981
5073
|
*
|
|
@@ -5007,15 +5099,18 @@ window.__ModuleLoader__.load({
|
|
|
5007
5099
|
/* closing must never throw */
|
|
5008
5100
|
}
|
|
5009
5101
|
}
|
|
5010
|
-
|
|
5011
|
-
|
|
5012
|
-
|
|
5013
|
-
|
|
5014
|
-
|
|
5015
|
-
|
|
5016
|
-
|
|
5102
|
+
// v1.7.0: portal to <body> so the overlay centres on the viewport instead
|
|
5103
|
+
// of resolving `position: fixed` against the conversation area.
|
|
5104
|
+
return portalize(
|
|
5105
|
+
h(
|
|
5106
|
+
'div',
|
|
5107
|
+
{
|
|
5108
|
+
className: 'dsh-tokenlens-overlay',
|
|
5109
|
+
role: 'presentation',
|
|
5110
|
+
onMouseDown: (event) => {
|
|
5111
|
+
if (event.target === event.currentTarget) close()
|
|
5112
|
+
},
|
|
5017
5113
|
},
|
|
5018
|
-
},
|
|
5019
5114
|
h(
|
|
5020
5115
|
'div',
|
|
5021
5116
|
{
|
|
@@ -5042,6 +5137,7 @@ window.__ModuleLoader__.load({
|
|
|
5042
5137
|
h('div', { className: 'dsh-tokenlens-modal-body' }, children),
|
|
5043
5138
|
footer === undefined || footer === null ? null : h('div', { className: 'dsh-tokenlens-modal-foot' }, footer),
|
|
5044
5139
|
),
|
|
5140
|
+
),
|
|
5045
5141
|
)
|
|
5046
5142
|
}
|
|
5047
5143
|
|
|
@@ -5959,6 +6055,136 @@ window.__ModuleLoader__.load({
|
|
|
5959
6055
|
)
|
|
5960
6056
|
}
|
|
5961
6057
|
|
|
6058
|
+
/**
|
|
6059
|
+
* The output-speed (TPS) trend card (v1.7.0), shown under 明细.
|
|
6060
|
+
*
|
|
6061
|
+
* `tps` is output tokens over the model's OWN request time (`step/start` to
|
|
6062
|
+
* `assistant/message`), so the step's tool calls do not dilute it — but
|
|
6063
|
+
* reasoning tokens are included in "output", which the hint says.
|
|
6064
|
+
*/
|
|
6065
|
+
function buildSpeedSection(state, ctx, t) {
|
|
6066
|
+
const stats = ctx.stats
|
|
6067
|
+
const scoped = ctx.providerScoped ? ctx.currentRow?.speed ?? null : stats?.speed ?? null
|
|
6068
|
+
const series = scoped === null || scoped === undefined ? null : scoped
|
|
6069
|
+
const byDay = series === null ? [] : series.byDay
|
|
6070
|
+
const head = h(
|
|
6071
|
+
'div',
|
|
6072
|
+
{ key: 'head', className: 'dsh-tokenlens-section-head' },
|
|
6073
|
+
h(
|
|
6074
|
+
'span',
|
|
6075
|
+
{ className: 'dsh-tokenlens-section-title' },
|
|
6076
|
+
Icon('chart', 14),
|
|
6077
|
+
t('statsSpeedTitle'),
|
|
6078
|
+
),
|
|
6079
|
+
series !== null && series.total.tps !== null
|
|
6080
|
+
? h(
|
|
6081
|
+
'span',
|
|
6082
|
+
{ className: 'dsh-tokenlens-hint', 'data-speed-total': String(series.total.tps) },
|
|
6083
|
+
t('statsSpeedTotal', { tps: series.total.tps.toFixed(1) }),
|
|
6084
|
+
)
|
|
6085
|
+
: null,
|
|
6086
|
+
)
|
|
6087
|
+
const body =
|
|
6088
|
+
byDay.length === 0
|
|
6089
|
+
? h(
|
|
6090
|
+
'div',
|
|
6091
|
+
{ key: 'empty', className: 'dsh-tokenlens-chart-empty', 'data-speed-empty': 'true' },
|
|
6092
|
+
h('span', { key: 'hint', className: 'dsh-tokenlens-hint' }, t('statsSpeedEmpty')),
|
|
6093
|
+
)
|
|
6094
|
+
: (() => {
|
|
6095
|
+
// v1.7.1: a CURVE, not bars — the trend is a rate, and a line reads
|
|
6096
|
+
// it far better than per-day columns. The SVG scales to the card
|
|
6097
|
+
// width; `preserveAspectRatio="none"` lets it fill without keeping
|
|
6098
|
+
// the viewBox ratio, and the per-day hover targets are transparent
|
|
6099
|
+
// columns so nothing is distorted.
|
|
6100
|
+
const width = 320
|
|
6101
|
+
const height = 90
|
|
6102
|
+
const pad = 4
|
|
6103
|
+
const max = Math.max(...byDay.map((entry) => entry.tps ?? 0), 1)
|
|
6104
|
+
const stepX = byDay.length > 1 ? (width - pad * 2) / (byDay.length - 1) : 0
|
|
6105
|
+
const points = byDay.map((entry, index) => {
|
|
6106
|
+
const x = byDay.length > 1 ? pad + stepX * index : width / 2
|
|
6107
|
+
const y = height - pad - ((entry.tps ?? 0) / max) * (height - pad * 2)
|
|
6108
|
+
return { x, y }
|
|
6109
|
+
})
|
|
6110
|
+
const line = points.map((point) => `${point.x.toFixed(1)},${point.y.toFixed(1)}`).join(' ')
|
|
6111
|
+
const area = `${pad.toFixed(1)},${(height - pad).toFixed(1)} ${line} ${(width - pad).toFixed(1)},${(height - pad).toFixed(1)}`
|
|
6112
|
+
const columnWidth = byDay.length > 1 ? stepX : width - pad * 2
|
|
6113
|
+
const columns = byDay.map((entry, index) =>
|
|
6114
|
+
h(
|
|
6115
|
+
'rect',
|
|
6116
|
+
{
|
|
6117
|
+
key: `hit-${entry.day}-${index}`,
|
|
6118
|
+
x: (points[index].x - columnWidth / 2).toFixed(1),
|
|
6119
|
+
y: 0,
|
|
6120
|
+
width: Math.max(1, columnWidth).toFixed(1),
|
|
6121
|
+
height: String(height),
|
|
6122
|
+
fill: 'transparent',
|
|
6123
|
+
'data-speed-day': entry.day,
|
|
6124
|
+
'data-speed-tps': String(entry.tps ?? 0),
|
|
6125
|
+
},
|
|
6126
|
+
h(
|
|
6127
|
+
'title',
|
|
6128
|
+
{ key: 'tip' },
|
|
6129
|
+
t('statsSpeedBarTitle', {
|
|
6130
|
+
date: entry.day,
|
|
6131
|
+
tps: (entry.tps ?? 0).toFixed(1),
|
|
6132
|
+
tokens: formatTokensPrecise(entry.outputTokens),
|
|
6133
|
+
seconds: entry.seconds.toFixed(1),
|
|
6134
|
+
}),
|
|
6135
|
+
),
|
|
6136
|
+
),
|
|
6137
|
+
)
|
|
6138
|
+
const svg = h(
|
|
6139
|
+
'svg',
|
|
6140
|
+
{
|
|
6141
|
+
key: 'svg',
|
|
6142
|
+
className: 'dsh-tokenlens-speed-svg',
|
|
6143
|
+
'data-speed-plot': 'true',
|
|
6144
|
+
viewBox: `0 0 ${width} ${height}`,
|
|
6145
|
+
preserveAspectRatio: 'none',
|
|
6146
|
+
role: 'img',
|
|
6147
|
+
'aria-label': t('statsSpeedTitle'),
|
|
6148
|
+
},
|
|
6149
|
+
h('polygon', {
|
|
6150
|
+
key: 'area',
|
|
6151
|
+
className: 'dsh-tokenlens-speed-area',
|
|
6152
|
+
points: area,
|
|
6153
|
+
fill: 'rgba(46,158,91,.18)',
|
|
6154
|
+
'aria-hidden': 'true',
|
|
6155
|
+
}),
|
|
6156
|
+
h('polyline', {
|
|
6157
|
+
key: 'line',
|
|
6158
|
+
className: 'dsh-tokenlens-speed-line',
|
|
6159
|
+
points: line,
|
|
6160
|
+
fill: 'none',
|
|
6161
|
+
stroke: '#2e9e5b',
|
|
6162
|
+
'stroke-width': '1.6',
|
|
6163
|
+
'stroke-linecap': 'round',
|
|
6164
|
+
'stroke-linejoin': 'round',
|
|
6165
|
+
'vector-effect': 'non-scaling-stroke',
|
|
6166
|
+
'aria-hidden': 'true',
|
|
6167
|
+
}),
|
|
6168
|
+
columns,
|
|
6169
|
+
)
|
|
6170
|
+
const axis = h(
|
|
6171
|
+
'div',
|
|
6172
|
+
{ key: 'axis', className: 'dsh-tokenlens-speed-axis', 'aria-hidden': 'true' },
|
|
6173
|
+
h('span', { key: 'first' }, byDay[0].day),
|
|
6174
|
+
byDay.length > 1 ? h('span', { key: 'peak' }, t('statsSpeedPeak', { tps: max.toFixed(1) })) : null,
|
|
6175
|
+
byDay.length > 1 ? h('span', { key: 'last' }, byDay[byDay.length - 1].day) : null,
|
|
6176
|
+
)
|
|
6177
|
+
return h('div', { key: 'plot', className: 'dsh-tokenlens-speed-plot' }, svg, axis)
|
|
6178
|
+
})()
|
|
6179
|
+
return h(
|
|
6180
|
+
'div',
|
|
6181
|
+
{ key: 'speed', className: 'dsh-tokenlens-section', 'data-speed-section': 'true' },
|
|
6182
|
+
head,
|
|
6183
|
+
h('div', { key: 'hint', className: 'dsh-tokenlens-subtitle' }, t('statsSpeedHint')),
|
|
6184
|
+
body,
|
|
6185
|
+
)
|
|
6186
|
+
}
|
|
6187
|
+
|
|
5962
6188
|
function buildStatsDetails(state, ctx, t) {
|
|
5963
6189
|
const { stats, hasMonth } = ctx
|
|
5964
6190
|
if (stats === null) return null
|
|
@@ -7422,6 +7648,12 @@ window.__ModuleLoader__.load({
|
|
|
7422
7648
|
{ key: 'row-details', className: 'dsh-tokenlens-chart-layout-row' },
|
|
7423
7649
|
buildStatsDetails(state, statsCtx, t),
|
|
7424
7650
|
),
|
|
7651
|
+
// v1.7.0: the output-speed trend sits under 明细, as its own full-width row.
|
|
7652
|
+
h(
|
|
7653
|
+
'div',
|
|
7654
|
+
{ key: 'row-speed', className: 'dsh-tokenlens-chart-layout-row' },
|
|
7655
|
+
buildSpeedSection(state, statsCtx, t),
|
|
7656
|
+
),
|
|
7425
7657
|
)
|
|
7426
7658
|
const body = [buildManageButton(state, t), statsArea]
|
|
7427
7659
|
const displayVersion = currentPluginVersionOf(state)
|
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.
|
|
87
|
+
export const PLUGIN_VERSION = '1.7.1'
|
|
88
88
|
|
|
89
89
|
/** Documented defaults; every field of the raw row config is optional. */
|
|
90
90
|
const DEFAULTS = {
|
package/lib/usage-stats.mjs
CHANGED
|
@@ -61,7 +61,17 @@ 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 = 6
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* How many days the daily series (and therefore the 图表 mode and the
|
|
68
|
+
* 最近X天 provider window) may cover (v1.7.0).
|
|
69
|
+
*
|
|
70
|
+
* The wire used to carry 120 days, so a provider first seen months ago reported
|
|
71
|
+
* 最近90天 while every other figure and the charts described 30. One span now
|
|
72
|
+
* governs all of them.
|
|
73
|
+
*/
|
|
74
|
+
export const DAILY_SERIES_DAYS = 30
|
|
65
75
|
|
|
66
76
|
/** What the numbers are, by provider id (display fallback; config can override). */
|
|
67
77
|
const PROVIDER_LABELS = {
|
|
@@ -235,6 +245,11 @@ export function foldSession(records) {
|
|
|
235
245
|
const models = Object.create(null)
|
|
236
246
|
const hours = Object.create(null)
|
|
237
247
|
const workspaces = Object.create(null)
|
|
248
|
+
// v1.7.0: output-speed (TPS) samples. A step's model time is
|
|
249
|
+
// `assistant/message.time − step/start.time`: the step's tool calls are emitted
|
|
250
|
+
// AFTER the message, so they do not dilute it.
|
|
251
|
+
const speed = Object.create(null)
|
|
252
|
+
const stepStarts = new Map()
|
|
238
253
|
let latestTime = 0
|
|
239
254
|
let sessionCwd = null
|
|
240
255
|
const session = { seeded: false, subagent: false, createdAt: null, id: null, inheritedEvents: 0 }
|
|
@@ -267,6 +282,17 @@ export function foldSession(records) {
|
|
|
267
282
|
continue
|
|
268
283
|
}
|
|
269
284
|
if (boundarySeq !== null && Number.isSafeInteger(record.seq) && record.seq <= boundarySeq) continue
|
|
285
|
+
// v1.7.0: remember when each step's model request started, so the matching
|
|
286
|
+
// `assistant/message` can be turned into a duration.
|
|
287
|
+
if (record.type === 'step/start') {
|
|
288
|
+
const turn = record.data?.turn
|
|
289
|
+
const step = record.data?.step
|
|
290
|
+
const startedAt = typeof record.time === 'number' && Number.isFinite(record.time) ? record.time : null
|
|
291
|
+
if (startedAt !== null && Number.isSafeInteger(turn) && Number.isSafeInteger(step)) {
|
|
292
|
+
stepStarts.set(`${turn}:${step}`, startedAt)
|
|
293
|
+
}
|
|
294
|
+
continue
|
|
295
|
+
}
|
|
270
296
|
const usage = usageOf(record)
|
|
271
297
|
if (usage === null) continue
|
|
272
298
|
const time = typeof record.time === 'number' && Number.isFinite(record.time) ? record.time : null
|
|
@@ -301,6 +327,23 @@ export function foldSession(records) {
|
|
|
301
327
|
const dayProvider = (dayBucket.providers[attributedProvider] ??= emptyBucket())
|
|
302
328
|
addBucket(dayProvider, usage)
|
|
303
329
|
addBucket((providers[attributedProvider] ??= emptyBucket()), usage)
|
|
330
|
+
// v1.7.0: one speed sample per completed step.
|
|
331
|
+
if (record.type === 'assistant/message' && usage.output > 0) {
|
|
332
|
+
const turn = record.data?.turn
|
|
333
|
+
const step = record.data?.step
|
|
334
|
+
const startedAt = Number.isSafeInteger(turn) && Number.isSafeInteger(step)
|
|
335
|
+
? stepStarts.get(`${turn}:${step}`)
|
|
336
|
+
: undefined
|
|
337
|
+
const elapsedMs = startedAt === undefined ? null : time - startedAt
|
|
338
|
+
if (elapsedMs !== null && elapsedMs > 0) {
|
|
339
|
+
const daySpeed = (speed[day] ??= { providers: Object.create(null) })
|
|
340
|
+
const slot = (daySpeed.providers[attributedProvider] ??= { output: 0, seconds: 0, steps: 0 })
|
|
341
|
+
slot.output += usage.output
|
|
342
|
+
slot.seconds += elapsedMs / 1000
|
|
343
|
+
slot.steps += 1
|
|
344
|
+
stepStarts.delete(`${turn}:${step}`)
|
|
345
|
+
}
|
|
346
|
+
}
|
|
304
347
|
const modelKey = `${attributedProvider}/${attributedModel}`
|
|
305
348
|
const modelBucket = (models[modelKey] ??= {
|
|
306
349
|
provider: attributedProvider,
|
|
@@ -318,7 +361,7 @@ export function foldSession(records) {
|
|
|
318
361
|
if (Number(key) < cutoff) delete hours[key]
|
|
319
362
|
}
|
|
320
363
|
}
|
|
321
|
-
return { days, providers, models, hours, workspaces, session }
|
|
364
|
+
return { days, providers, models, hours, workspaces, session, speed }
|
|
322
365
|
}
|
|
323
366
|
|
|
324
367
|
/** Merge one file's folded contribution into an aggregate being built. */
|
|
@@ -329,6 +372,16 @@ function mergeFold(aggregate, fold) {
|
|
|
329
372
|
addBucket((target.providers[provider] ??= emptyBucket()), bucket)
|
|
330
373
|
}
|
|
331
374
|
}
|
|
375
|
+
// v1.7.0: merge the speed samples the same way as the token buckets.
|
|
376
|
+
for (const [day, daySpeed] of Object.entries(fold.speed ?? {})) {
|
|
377
|
+
const target = (aggregate.speed[day] ??= { providers: Object.create(null) })
|
|
378
|
+
for (const [provider, slot] of Object.entries(daySpeed.providers ?? {})) {
|
|
379
|
+
const into = (target.providers[provider] ??= { output: 0, seconds: 0, steps: 0 })
|
|
380
|
+
into.output += slot.output
|
|
381
|
+
into.seconds += slot.seconds
|
|
382
|
+
into.steps += slot.steps
|
|
383
|
+
}
|
|
384
|
+
}
|
|
332
385
|
for (const [provider, bucket] of Object.entries(fold.providers ?? {})) {
|
|
333
386
|
addBucket((aggregate.providers[provider] ??= emptyBucket()), bucket)
|
|
334
387
|
}
|
|
@@ -507,7 +560,18 @@ export function createUsageStats(input) {
|
|
|
507
560
|
const modelsAggregate = Object.create(null)
|
|
508
561
|
const hoursAggregate = Object.create(null)
|
|
509
562
|
const workspacesAggregate = Object.create(null)
|
|
563
|
+
// v1.7.0: `speedAggregate[day].providers[providerId] = {output, seconds, steps}`
|
|
564
|
+
const speedAggregate = Object.create(null)
|
|
510
565
|
for (const entry of Object.values(cache.files)) {
|
|
566
|
+
for (const [day, daySpeed] of Object.entries(entry.speed ?? {})) {
|
|
567
|
+
const target = (speedAggregate[day] ??= { providers: Object.create(null) })
|
|
568
|
+
for (const [provider, slot] of Object.entries(daySpeed.providers ?? {})) {
|
|
569
|
+
const into = (target.providers[provider] ??= { output: 0, seconds: 0, steps: 0 })
|
|
570
|
+
into.output += typeof slot.output === 'number' ? slot.output : 0
|
|
571
|
+
into.seconds += typeof slot.seconds === 'number' ? slot.seconds : 0
|
|
572
|
+
into.steps += typeof slot.steps === 'number' ? slot.steps : 0
|
|
573
|
+
}
|
|
574
|
+
}
|
|
511
575
|
for (const [day, dayBucket] of Object.entries(entry.days ?? {})) {
|
|
512
576
|
const target = (daysAggregate[day] ??= { providers: Object.create(null) })
|
|
513
577
|
for (const [provider, bucket] of Object.entries(dayBucket.providers ?? {})) {
|
|
@@ -725,6 +789,8 @@ export function createUsageStats(input) {
|
|
|
725
789
|
providers,
|
|
726
790
|
models: models.slice(0, 12),
|
|
727
791
|
daily,
|
|
792
|
+
// v1.7.0: raw per-day output-speed samples for the wire builder below.
|
|
793
|
+
speed: speedAggregate,
|
|
728
794
|
scan,
|
|
729
795
|
}
|
|
730
796
|
}
|
|
@@ -789,6 +855,8 @@ export function createUsageStats(input) {
|
|
|
789
855
|
models: fold.models,
|
|
790
856
|
hours: fold.hours,
|
|
791
857
|
workspaces: fold.workspaces,
|
|
858
|
+
// v1.7.0: per-day output-speed samples (CACHE_VERSION 6).
|
|
859
|
+
speed: fold.speed,
|
|
792
860
|
}
|
|
793
861
|
nextFiles[path] = entry
|
|
794
862
|
scanned += 1
|
|
@@ -1259,7 +1327,10 @@ export function createUsageStats(input) {
|
|
|
1259
1327
|
}
|
|
1260
1328
|
const daily = [...dailyMap.entries()]
|
|
1261
1329
|
.sort(([left], [right]) => left.localeCompare(right))
|
|
1262
|
-
.
|
|
1330
|
+
// v1.7.0: 30 days, matching the 最近30天 bucket and the 最近X天 label. The
|
|
1331
|
+
// series used to carry 120 days, so the 图表 mode could plot a wider window
|
|
1332
|
+
// than the 数据 mode described.
|
|
1333
|
+
.slice(-DAILY_SERIES_DAYS)
|
|
1263
1334
|
.map(([day, entry]) => {
|
|
1264
1335
|
const providers = Object.create(null)
|
|
1265
1336
|
for (const [providerId, slot] of entry.providers) {
|
|
@@ -1292,15 +1363,77 @@ export function createUsageStats(input) {
|
|
|
1292
1363
|
return Math.max(1, Math.round((end - start) / dayMs) + 1)
|
|
1293
1364
|
}
|
|
1294
1365
|
const providers = [...providerMap.values()].sort((a, b) => b.allTime.total - a.allTime.total)
|
|
1366
|
+
/**
|
|
1367
|
+
* Build one output-speed series (v1.7.0).
|
|
1368
|
+
*
|
|
1369
|
+
* `tps` is output tokens over the summed model time of the steps that
|
|
1370
|
+
* produced them, so it describes generation speed rather than wall clock:
|
|
1371
|
+
* the tool calls of a step are emitted after its `assistant/message` and are
|
|
1372
|
+
* therefore excluded. A day with no positive duration is dropped instead of
|
|
1373
|
+
* being given an infinite rate.
|
|
1374
|
+
*/
|
|
1375
|
+
const speedSeries = (providerId) => {
|
|
1376
|
+
const source = dsh.speed ?? {}
|
|
1377
|
+
const byDay = []
|
|
1378
|
+
let output = 0
|
|
1379
|
+
let seconds = 0
|
|
1380
|
+
let steps = 0
|
|
1381
|
+
for (const day of Object.keys(source).sort()) {
|
|
1382
|
+
if (day < thirtyStart) continue
|
|
1383
|
+
let dayOutput = 0
|
|
1384
|
+
let daySeconds = 0
|
|
1385
|
+
let daySteps = 0
|
|
1386
|
+
for (const [id, slot] of Object.entries(source[day].providers ?? {})) {
|
|
1387
|
+
if (providerId !== null && id !== providerId) continue
|
|
1388
|
+
dayOutput += slot.output
|
|
1389
|
+
daySeconds += slot.seconds
|
|
1390
|
+
daySteps += slot.steps
|
|
1391
|
+
}
|
|
1392
|
+
if (daySeconds <= 0 || dayOutput <= 0) continue
|
|
1393
|
+
byDay.push({
|
|
1394
|
+
day,
|
|
1395
|
+
outputTokens: dayOutput,
|
|
1396
|
+
seconds: Math.round(daySeconds * 10) / 10,
|
|
1397
|
+
tps: Math.round((dayOutput / daySeconds) * 10) / 10,
|
|
1398
|
+
steps: daySteps,
|
|
1399
|
+
})
|
|
1400
|
+
output += dayOutput
|
|
1401
|
+
seconds += daySeconds
|
|
1402
|
+
steps += daySteps
|
|
1403
|
+
}
|
|
1404
|
+
return {
|
|
1405
|
+
byDay,
|
|
1406
|
+
total: {
|
|
1407
|
+
outputTokens: output,
|
|
1408
|
+
seconds: Math.round(seconds * 10) / 10,
|
|
1409
|
+
tps: seconds > 0 ? Math.round((output / seconds) * 10) / 10 : null,
|
|
1410
|
+
steps,
|
|
1411
|
+
},
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1295
1414
|
for (const provider of providers) {
|
|
1296
1415
|
const effectiveDay = firstDays.get(provider.providerId) ?? todayDay
|
|
1297
1416
|
provider.effectiveDay = effectiveDay
|
|
1298
|
-
|
|
1299
|
-
// A provider
|
|
1300
|
-
//
|
|
1301
|
-
|
|
1417
|
+
// v1.7.0: the 最近X天 window is capped at the same 30 days the charts and
|
|
1418
|
+
// the 最近30天 bucket use. A provider first seen 90 days ago used to report
|
|
1419
|
+
// 最近90天 while every other figure on the page covered 30.
|
|
1420
|
+
const fullDays = dayCount(effectiveDay, todayDay)
|
|
1421
|
+
const cappedDays = Math.min(DAILY_SERIES_DAYS, fullDays)
|
|
1422
|
+
provider.effectiveDays = cappedDays
|
|
1423
|
+
// The label and the numbers must describe the SAME span. When the real
|
|
1424
|
+
// history is longer than the cap, the since-effective bucket is therefore
|
|
1425
|
+
// the 30-day one rather than all-time (which would carry older records
|
|
1426
|
+
// under a 最近30天 label).
|
|
1427
|
+
provider.sinceEffective =
|
|
1428
|
+
cappedDays < fullDays ? cloneBucket(provider.last30Days) : cloneBucket(provider.allTime)
|
|
1429
|
+
// v1.7.0: each provider row carries its own output-speed series so the
|
|
1430
|
+
// chart can follow the same scope switch as the token sections.
|
|
1431
|
+
provider.speed = speedSeries(provider.providerId)
|
|
1302
1432
|
for (const harness of Object.values(provider.harnesses ?? {})) {
|
|
1303
|
-
harness.sinceEffective =
|
|
1433
|
+
harness.sinceEffective =
|
|
1434
|
+
cappedDays < fullDays && harness.last30Days !== undefined
|
|
1435
|
+
? cloneBucket(harness.last30Days)
|
|
1436
|
+
: cloneBucket(harness.allTime)
|
|
1304
1437
|
}
|
|
1305
1438
|
}
|
|
1306
1439
|
return {
|
|
@@ -1315,6 +1448,8 @@ export function createUsageStats(input) {
|
|
|
1315
1448
|
allTime,
|
|
1316
1449
|
workspaces,
|
|
1317
1450
|
providers,
|
|
1451
|
+
// v1.7.0: output-speed (TPS) series across every harness.
|
|
1452
|
+
speed: speedSeries(null),
|
|
1318
1453
|
harnesses,
|
|
1319
1454
|
models: dsh.models,
|
|
1320
1455
|
daily,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-tokenlens",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.1",
|
|
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",
|