dsh-recent-usage-metrics 0.3.2 → 0.3.4
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 +18 -0
- package/README.en.md +16 -1
- package/README.md +13 -1
- package/client/client.js +250 -10
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,21 @@
|
|
|
1
|
+
## 0.3.4
|
|
2
|
+
|
|
3
|
+
- 实时行也能显示模型名了:形态与编号行相同——本步窗口内有 `request/header` 判为切换、
|
|
4
|
+
直接用它的模型;否则沿用上一步的模型。回复落地后由 `assistant/message.source` 覆盖。
|
|
5
|
+
实测 3202 步:22 次切换全部命中,沿用 99.84%,唯一缺口是回合第一步且无 header。
|
|
6
|
+
- 实时行补上速度列的悬浮提示。未知时显示破折号而不是「使用对话模型」——后者读起来像
|
|
7
|
+
一个事实,其实只是插件当时的假设。
|
|
8
|
+
- 新增两种行高亮:模型切换用业务蓝,缓存命中低于 90% 用警告黄。
|
|
9
|
+
阈值来自实测:2921 条回复的中位数是 99.9%,1% 分位是 92.4%,90% 只标记 0.9% 的行。
|
|
10
|
+
两者同时成立时文字取警告色、左侧色条保留业务蓝,两个信号都不丢。
|
|
11
|
+
压缩行不参与缓存高亮——它已在速度列自报家门,且它的缓存数字描述的是重放而非应答。
|
|
12
|
+
- 未上报缓存读取的 provider(如 trae 把缓存并入输入)不会被当作 0% 而误标。
|
|
13
|
+
|
|
14
|
+
## 0.3.3
|
|
15
|
+
|
|
16
|
+
- 输出 Token 列的悬浮提示里,输入 token 改为带千分位的完整数字(`370,683 tok`),
|
|
17
|
+
不再是缩写。单元格本身仍缩写以保持对齐,提示存在的意义正是补回被缩写省掉的精度。
|
|
18
|
+
|
|
1
19
|
## 0.3.2
|
|
2
20
|
|
|
3
21
|
- 修复:速度列的悬浮提示常常拿不到模型名,或显示成别的模型。
|
package/README.en.md
CHANGED
|
@@ -151,9 +151,24 @@ Every drawer column and every header carries a hint:
|
|
|
151
151
|
| --- | --- |
|
|
152
152
|
| Clock column | the full timestamp `11-06 10:47:41.828` |
|
|
153
153
|
| Speed column | the model that served that row |
|
|
154
|
-
| Tokens column | input tokens and cache hit rate |
|
|
154
|
+
| Tokens column | input tokens (with thousands separators, e.g. `370,683 tok`) and cache hit rate |
|
|
155
155
|
| Headers | what the column measures (the `#` header draws the four state glyphs) |
|
|
156
156
|
|
|
157
|
+
**Row highlights:**
|
|
158
|
+
|
|
159
|
+
| Trigger | Colour | Frequency |
|
|
160
|
+
| --- | --- | --- |
|
|
161
|
+
| The request switched models | business blue (text + left bar) | 0.7% |
|
|
162
|
+
| Cache hit below 90% | warn yellow (text + left bar) | 0.9% |
|
|
163
|
+
|
|
164
|
+
When both hold, the text takes the warn colour and the bar keeps the business colour. A
|
|
165
|
+
compaction row never takes the cache highlight.
|
|
166
|
+
|
|
167
|
+
**Where the model name comes from:** a `request/header` inside the step's window means a switch
|
|
168
|
+
and supplies the model; otherwise the previous step's model carries over, and a landed reply
|
|
169
|
+
overrides both via `assistant/message.source`. The live row follows the same rule, so the model
|
|
170
|
+
is visible while the reply is still streaming.
|
|
171
|
+
|
|
157
172
|
The hints are a real overlay rather than `title`: `title` can only show plain text, and the
|
|
158
173
|
`#` header has to draw glyphs.
|
|
159
174
|
|
package/README.md
CHANGED
|
@@ -135,9 +135,21 @@ DSH 的极客级请求指标。它在 **设置 → 通用 → 性能与用量**
|
|
|
135
135
|
| --- | --- |
|
|
136
136
|
| 时间列 | 完整时间戳 `11-06 10:47:41.828` |
|
|
137
137
|
| 速度列 | 该行使用的模型 |
|
|
138
|
-
| 输出 Token 列 | 输入 token
|
|
138
|
+
| 输出 Token 列 | 输入 token(带千分位,如 `370,683 tok`)与缓存命中率 |
|
|
139
139
|
| 表头 | 该列的含义(`#` 一列用图标展示四种状态) |
|
|
140
140
|
|
|
141
|
+
**行高亮:**
|
|
142
|
+
|
|
143
|
+
| 触发 | 颜色 | 实测频率 |
|
|
144
|
+
| --- | --- | --- |
|
|
145
|
+
| 该请求切换了模型 | 业务蓝(文字 + 左侧色条) | 0.7% |
|
|
146
|
+
| 缓存命中低于 90% | 警告黄(文字 + 左侧色条) | 0.9% |
|
|
147
|
+
|
|
148
|
+
两者同时成立时文字取警告色、色条保留业务蓝。压缩行不参与缓存高亮。
|
|
149
|
+
|
|
150
|
+
**模型名的来源:** 本步窗口内的 `request/header` 判为切换,否则沿用上一步;回复落地后
|
|
151
|
+
由 `assistant/message.source` 覆盖。实时行同样适用,所以生成期间就能看到模型名。
|
|
152
|
+
|
|
141
153
|
提示是自建的浮层而不是 `title`:`title` 只能显示纯文本,而 `#` 列的说明要画出图标。
|
|
142
154
|
|
|
143
155
|
## 数字是怎么算出来的
|
package/client/client.js
CHANGED
|
@@ -481,6 +481,30 @@ window.__ModuleLoader__.load({
|
|
|
481
481
|
.dsh-rum-live-row {
|
|
482
482
|
background: color-mix(in srgb, var(--dsw-alias-brand-primary) 10%, transparent);
|
|
483
483
|
}
|
|
484
|
+
/*
|
|
485
|
+
* Two reasons a finished row is worth a second look.
|
|
486
|
+
*
|
|
487
|
+
* Both use the theme's own state tokens, never a literal colour, so they follow dark mode and
|
|
488
|
+
* any future palette change. The text colour carries the meaning; the left bar makes the row
|
|
489
|
+
* findable when the drawer is scanned rather than read.
|
|
490
|
+
*
|
|
491
|
+
* Cache-outranks-switch: a cold prompt is a performance fact the user can act on, while a model
|
|
492
|
+
* change is context. When both are true the warn colour wins and the bar keeps the business
|
|
493
|
+
* colour, so neither signal is lost.
|
|
494
|
+
*/
|
|
495
|
+
.dsh-rum-item[data-switched="true"] {
|
|
496
|
+
color: var(--dsw-alias-state-business-primary, var(--dsw-alias-brand-primary));
|
|
497
|
+
box-shadow: inset 2px 0 0 var(--dsw-alias-state-business-primary, var(--dsw-alias-brand-primary));
|
|
498
|
+
}
|
|
499
|
+
.dsh-rum-item[data-lowcache="true"] {
|
|
500
|
+
color: var(--dsw-alias-state-warn-primary, var(--dsw-alias-state-warn-label));
|
|
501
|
+
box-shadow: inset 2px 0 0 var(--dsw-alias-state-warn-primary, var(--dsw-alias-state-warn-label));
|
|
502
|
+
}
|
|
503
|
+
/* The bar keeps the switch signal when a cold prompt outranks it in the text. */
|
|
504
|
+
.dsh-rum-item[data-switched="true"][data-lowcache="true"] {
|
|
505
|
+
color: var(--dsw-alias-state-warn-primary, var(--dsw-alias-state-warn-label));
|
|
506
|
+
box-shadow: inset 2px 0 0 var(--dsw-alias-state-business-primary, var(--dsw-alias-brand-primary));
|
|
507
|
+
}
|
|
484
508
|
/*
|
|
485
509
|
* The streaming marker: the same bolt the status pill pulses, sized for the index column.
|
|
486
510
|
*
|
|
@@ -1196,11 +1220,98 @@ window.__ModuleLoader__.load({
|
|
|
1196
1220
|
const provider = typeof meta.provider === 'string' && meta.provider !== '' ? meta.provider : null
|
|
1197
1221
|
if (model === null && provider === null) continue
|
|
1198
1222
|
if (typeof node.turn !== 'number' || typeof node.step !== 'number') continue
|
|
1199
|
-
index.set(`${node.turn}:${node.step}`, { provider, model })
|
|
1223
|
+
index.set(`${node.turn}:${node.step}`, { provider, model, switched: false, source: 'trajectory' })
|
|
1200
1224
|
}
|
|
1201
1225
|
return index
|
|
1202
1226
|
}
|
|
1203
1227
|
|
|
1228
|
+
/**
|
|
1229
|
+
* Which model a step used, and whether that request changed models.
|
|
1230
|
+
*
|
|
1231
|
+
* A `request/header` is written only when the routed model or tool set CHANGES, so its
|
|
1232
|
+
* placement is itself the signal:
|
|
1233
|
+
*
|
|
1234
|
+
* - inside a step's own seq window (from its `step/start` to the next one) → this request
|
|
1235
|
+
* changed the routed configuration, and the header names the model it switched to;
|
|
1236
|
+
* - absent → the step reused whatever was already routed, so it inherits the previous
|
|
1237
|
+
* step's model.
|
|
1238
|
+
*
|
|
1239
|
+
* Measured on a real 3202-step session: 22 steps carried a header and all 22 were genuine
|
|
1240
|
+
* switches; 3180 inherited, of which every one with a known predecessor matched the model
|
|
1241
|
+
* the reply later reported. The only gaps are a turn's first step with no header, where
|
|
1242
|
+
* nothing earlier is known and the model stays null.
|
|
1243
|
+
*
|
|
1244
|
+
* A finished reply overrides both, because `assistant/message.source` is the authority.
|
|
1245
|
+
*/
|
|
1246
|
+
function modelPlanFromStats(statsSnapshot, known) {
|
|
1247
|
+
const plan = new Map()
|
|
1248
|
+
const turns = statsSnapshot?.turns
|
|
1249
|
+
if (!Array.isArray(turns)) return plan
|
|
1250
|
+
for (const turn of turns) {
|
|
1251
|
+
if (!Array.isArray(turn.steps)) continue
|
|
1252
|
+
const headers = []
|
|
1253
|
+
if (Array.isArray(turn.events)) {
|
|
1254
|
+
for (const event of turn.events) {
|
|
1255
|
+
if (event?.kind !== 'request-header') continue
|
|
1256
|
+
const at = numberOrNull(event.time)
|
|
1257
|
+
if (at === null) continue
|
|
1258
|
+
if (typeof event.model !== 'string' && typeof event.provider !== 'string') continue
|
|
1259
|
+
headers.push({ time: at, provider: event.provider ?? null, model: event.model ?? null })
|
|
1260
|
+
}
|
|
1261
|
+
headers.sort((left, right) => left.time - right.time)
|
|
1262
|
+
}
|
|
1263
|
+
const starts = []
|
|
1264
|
+
for (const step of turn.steps) {
|
|
1265
|
+
const at = numberOrNull(step.startTime)
|
|
1266
|
+
if (at === null || typeof step.step !== 'number') continue
|
|
1267
|
+
starts.push({ at, step: step.step })
|
|
1268
|
+
}
|
|
1269
|
+
if (starts.length === 0) continue
|
|
1270
|
+
starts.sort((left, right) => left.at - right.at)
|
|
1271
|
+
|
|
1272
|
+
/*
|
|
1273
|
+
* The model currently in force, carried forward while the plan is built.
|
|
1274
|
+
*
|
|
1275
|
+
* A step whose header names a switch becomes the new carrier, so the step AFTER it
|
|
1276
|
+
* inherits the switched-to model — not the one before the switch, which `known` alone
|
|
1277
|
+
* would give.
|
|
1278
|
+
*/
|
|
1279
|
+
let inForce = null
|
|
1280
|
+
|
|
1281
|
+
for (let i = 0; i < starts.length; i += 1) {
|
|
1282
|
+
const key = `${turn.turn}:${starts[i].step}`
|
|
1283
|
+
const from = starts[i].at
|
|
1284
|
+
const until = i + 1 < starts.length ? starts[i + 1].at : Number.POSITIVE_INFINITY
|
|
1285
|
+
let inside = null
|
|
1286
|
+
for (const header of headers) {
|
|
1287
|
+
if (header.time < from) continue
|
|
1288
|
+
if (header.time >= until) break
|
|
1289
|
+
inside = header
|
|
1290
|
+
}
|
|
1291
|
+
if (inside !== null) {
|
|
1292
|
+
// The header was written for THIS request, so the model changed here.
|
|
1293
|
+
inForce = { provider: inside.provider, model: inside.model }
|
|
1294
|
+
plan.set(key, { provider: inside.provider, model: inside.model, switched: true, source: 'header' })
|
|
1295
|
+
continue
|
|
1296
|
+
}
|
|
1297
|
+
/*
|
|
1298
|
+
* No header: inherit. A finished reply outranks the carried model, because
|
|
1299
|
+
* `assistant/message.source` is the authority for a step that has settled.
|
|
1300
|
+
*/
|
|
1301
|
+
const settled = known === undefined ? undefined : known.get(key)
|
|
1302
|
+
if (settled !== undefined && settled.model !== null) {
|
|
1303
|
+
inForce = { provider: settled.provider, model: settled.model }
|
|
1304
|
+
plan.set(key, { provider: settled.provider, model: settled.model, switched: false, source: 'reply' })
|
|
1305
|
+
continue
|
|
1306
|
+
}
|
|
1307
|
+
if (inForce !== null) {
|
|
1308
|
+
plan.set(key, { provider: inForce.provider, model: inForce.model, switched: false, source: 'inherit' })
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
return plan
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1204
1315
|
/**
|
|
1205
1316
|
* Whether any step started after this compaction finished.
|
|
1206
1317
|
*
|
|
@@ -1256,17 +1367,23 @@ window.__ModuleLoader__.load({
|
|
|
1256
1367
|
*/
|
|
1257
1368
|
function historyFromStats(statsSnapshot, models) {
|
|
1258
1369
|
const steps = statsSteps(statsSnapshot)
|
|
1370
|
+
/*
|
|
1371
|
+
* `models` carries the per-step attribution worked out from the trajectory nodes and the
|
|
1372
|
+
* turn's request headers. It answers two questions at once: which model, and whether this
|
|
1373
|
+
* request changed it — the drawer colours a row for the second.
|
|
1374
|
+
*/
|
|
1375
|
+
const plan = modelPlanFromStats(statsSnapshot, models)
|
|
1259
1376
|
const rows = []
|
|
1260
1377
|
for (const entry of steps) {
|
|
1261
1378
|
// A step only has a row once it has started; `step/start` is what supplies this.
|
|
1262
1379
|
if (numberOrNull(entry.metrics.startTime) === null) continue
|
|
1263
1380
|
if (!statsStepHasFigures(entry.metrics)) continue
|
|
1264
1381
|
const row = readingOfStatsStep(entry)
|
|
1265
|
-
|
|
1266
|
-
const owner = models === undefined ? undefined : models.get(row.key)
|
|
1382
|
+
const owner = plan.get(row.key) ?? (models === undefined ? undefined : models.get(row.key))
|
|
1267
1383
|
if (owner !== undefined) {
|
|
1268
1384
|
row.model = owner.model
|
|
1269
1385
|
row.provider = owner.provider
|
|
1386
|
+
row.switched = owner.switched === true
|
|
1270
1387
|
}
|
|
1271
1388
|
rows.push(row)
|
|
1272
1389
|
}
|
|
@@ -1529,6 +1646,55 @@ window.__ModuleLoader__.load({
|
|
|
1529
1646
|
return approximate ? t('approxTokensUnit', { value: rounded }) : t('tokensUnit', { value: rounded })
|
|
1530
1647
|
}
|
|
1531
1648
|
|
|
1649
|
+
/**
|
|
1650
|
+
* Cache-hit rate below which a row is worth flagging.
|
|
1651
|
+
*
|
|
1652
|
+
* Measured over 2921 replies in a real session: the median is 99.9% and the 1st percentile
|
|
1653
|
+
* is 92.4%, because a warm conversation re-reads its prefix almost every time. 90% therefore
|
|
1654
|
+
* marks a genuinely cold request rather than ordinary variation, and flags only 0.9% of rows.
|
|
1655
|
+
* A threshold at 70% would flag 0.6% — nearly the same rows, with less headroom above the
|
|
1656
|
+
* noise.
|
|
1657
|
+
*/
|
|
1658
|
+
const LOW_CACHE_HIT = 0.9
|
|
1659
|
+
|
|
1660
|
+
/**
|
|
1661
|
+
* Whether a row should be drawn as a low-cache-hit row.
|
|
1662
|
+
*
|
|
1663
|
+
* Only a REPORTED rate counts. A provider that folds cache reads into its input count (trae
|
|
1664
|
+
* does) reports none, and treating "not reported" as "0%" would flag every one of its rows.
|
|
1665
|
+
* A compaction is excluded: it already announces itself in the speed column, and its cache
|
|
1666
|
+
* figure describes replaying the conversation rather than serving a request.
|
|
1667
|
+
*/
|
|
1668
|
+
function isLowCacheHit(row) {
|
|
1669
|
+
if (row.compaction === true) return false
|
|
1670
|
+
const rate = numberOrNull(row.cacheHit)
|
|
1671
|
+
if (rate === null) return false
|
|
1672
|
+
return rate < LOW_CACHE_HIT
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
/**
|
|
1676
|
+
* A whole token count with thousands separators, for a hover hint.
|
|
1677
|
+
*
|
|
1678
|
+
* The cell itself has to stay narrow, so it abbreviates (`4.9k tok`). The hint has room, and
|
|
1679
|
+
* a reader comparing an input of 370683 against one of 1914 needs the exact digits — `370k`
|
|
1680
|
+
* and `371k` would hide the difference.
|
|
1681
|
+
*
|
|
1682
|
+
* The separator is a comma in both languages this plugin ships, so it is written out rather
|
|
1683
|
+
* than derived from the host locale: that keeps the hint identical in zh and en, and avoids
|
|
1684
|
+
* depending on a locale tag the slot props do not provide.
|
|
1685
|
+
*/
|
|
1686
|
+
function tokensExact(value, t) {
|
|
1687
|
+
if (value === null || value === undefined || !Number.isFinite(value)) return t('none')
|
|
1688
|
+
const digits = String(Math.round(Math.abs(value)))
|
|
1689
|
+
let grouped = ''
|
|
1690
|
+
for (let i = 0; i < digits.length; i += 1) {
|
|
1691
|
+
const fromEnd = digits.length - i
|
|
1692
|
+
grouped += digits[i]
|
|
1693
|
+
if (fromEnd > 1 && (fromEnd - 1) % 3 === 0) grouped += ','
|
|
1694
|
+
}
|
|
1695
|
+
return t('tokensUnit', { value: value < 0 ? `-${grouped}` : grouped })
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1532
1698
|
function clockText(at) {
|
|
1533
1699
|
const date = new Date(at)
|
|
1534
1700
|
if (Number.isNaN(date.getTime())) return '—'
|
|
@@ -1638,19 +1804,34 @@ window.__ModuleLoader__.load({
|
|
|
1638
1804
|
)
|
|
1639
1805
|
}
|
|
1640
1806
|
|
|
1641
|
-
/**
|
|
1807
|
+
/**
|
|
1808
|
+
* The speed column's hint: which model served this row.
|
|
1809
|
+
*
|
|
1810
|
+
* A request with no attribution shows the dash rather than a guess. "Chat model" would read
|
|
1811
|
+
* as a fact about the request when it is only what the plugin would have assumed, and a
|
|
1812
|
+
* wrong model name is worse than none — that is the whole reason this column stopped
|
|
1813
|
+
* borrowing `request/header` rows.
|
|
1814
|
+
*
|
|
1815
|
+
* A compaction is different: it does not "have no model", it has one that is only reported
|
|
1816
|
+
* once the summary lands, so while it runs the generic wording is honest.
|
|
1817
|
+
*/
|
|
1642
1818
|
function modelHintText(row, t) {
|
|
1643
1819
|
if (row.compaction === true) return compactionModelName(row, t)
|
|
1644
1820
|
const model = typeof row.model === 'string' && row.model !== '' ? row.model : null
|
|
1645
1821
|
const provider = typeof row.provider === 'string' && row.provider !== '' ? row.provider : null
|
|
1646
|
-
if (model === null) return t('
|
|
1822
|
+
if (model === null && provider === null) return t('none')
|
|
1823
|
+
if (model === null) return provider
|
|
1647
1824
|
return provider === null ? model : `${provider} / ${model}`
|
|
1648
1825
|
}
|
|
1649
1826
|
|
|
1650
1827
|
/** The tokens column's hint: the input side, which the cell itself does not show. */
|
|
1651
1828
|
function tokensHintText(row, t) {
|
|
1652
1829
|
const none = t('none')
|
|
1653
|
-
|
|
1830
|
+
/*
|
|
1831
|
+
* The input is spelled out with thousands separators; the cell above it abbreviates. The
|
|
1832
|
+
* hint exists precisely for the cases where the abbreviation loses information.
|
|
1833
|
+
*/
|
|
1834
|
+
const input = row.context === null || row.context === undefined ? none : tokensExact(row.context, t)
|
|
1654
1835
|
const hit =
|
|
1655
1836
|
row.cacheHit === null || row.cacheHit === undefined
|
|
1656
1837
|
? none
|
|
@@ -2131,6 +2312,7 @@ window.__ModuleLoader__.load({
|
|
|
2131
2312
|
sessionTurnOpen,
|
|
2132
2313
|
sessionRunning,
|
|
2133
2314
|
sessionStats,
|
|
2315
|
+
models,
|
|
2134
2316
|
) {
|
|
2135
2317
|
const partial = useChat((snapshot) => snapshot.legacy.partial)
|
|
2136
2318
|
const nodes = useChat((snapshot) => snapshot.legacy.nodes)
|
|
@@ -2314,6 +2496,20 @@ window.__ModuleLoader__.load({
|
|
|
2314
2496
|
* lag the fold.
|
|
2315
2497
|
* 3. Reconstruction from the previous step's last tool result, then the turn's start.
|
|
2316
2498
|
*/
|
|
2499
|
+
/*
|
|
2500
|
+
* The model for the running step, worked out the same way the drawer does it: a header
|
|
2501
|
+
* written inside this step names a switch, otherwise the previous step's model carries
|
|
2502
|
+
* over. A finished reply overrides both, which is why the plan prefers it.
|
|
2503
|
+
*
|
|
2504
|
+
* This is what lets the live row show a model while it is still streaming instead of
|
|
2505
|
+
* waiting for `assistant/message` to land.
|
|
2506
|
+
*/
|
|
2507
|
+
const livePlan =
|
|
2508
|
+
sessionStats === undefined ? undefined : modelPlanFromStats(sessionStats, models)
|
|
2509
|
+
const liveOwner =
|
|
2510
|
+
liveStep === null || livePlan === undefined
|
|
2511
|
+
? undefined
|
|
2512
|
+
: livePlan.get(`${liveStep.turn}:${liveStep.step}`)
|
|
2317
2513
|
/*
|
|
2318
2514
|
* A compaction has no step to measure, so the reading is built from the compaction alone:
|
|
2319
2515
|
* its own clock, its own span and its own model. Returning here keeps every step-shaped
|
|
@@ -2419,6 +2615,9 @@ window.__ModuleLoader__.load({
|
|
|
2419
2615
|
: null,
|
|
2420
2616
|
replyLanded: true,
|
|
2421
2617
|
phase: livePhase,
|
|
2618
|
+
model: liveOwner?.model ?? null,
|
|
2619
|
+
provider: liveOwner?.provider ?? null,
|
|
2620
|
+
switched: liveOwner?.switched === true,
|
|
2422
2621
|
compaction: activeCompaction,
|
|
2423
2622
|
}
|
|
2424
2623
|
}
|
|
@@ -2445,6 +2644,9 @@ window.__ModuleLoader__.load({
|
|
|
2445
2644
|
amortized: tokens > 0 && elapsedMs !== null && elapsedMs > 0 ? tokens / (elapsedMs / 1000) : null,
|
|
2446
2645
|
replyLanded: false,
|
|
2447
2646
|
phase: livePhase,
|
|
2647
|
+
model: liveOwner?.model ?? null,
|
|
2648
|
+
provider: liveOwner?.provider ?? null,
|
|
2649
|
+
switched: liveOwner?.switched === true,
|
|
2448
2650
|
compaction: activeCompaction,
|
|
2449
2651
|
}
|
|
2450
2652
|
}
|
|
@@ -2909,6 +3111,14 @@ window.__ModuleLoader__.load({
|
|
|
2909
3111
|
return last.running !== false
|
|
2910
3112
|
}, [sessionStats])
|
|
2911
3113
|
const chatNodes = useChat((snapshot) => snapshot.legacy.nodes)
|
|
3114
|
+
/*
|
|
3115
|
+
* The per-step model attribution, computed once and shared by the live reading and the
|
|
3116
|
+
* history so the two can never disagree about which model a step used.
|
|
3117
|
+
*/
|
|
3118
|
+
const models = useMemo(
|
|
3119
|
+
() => modelIndexFromTrajectory(trajectory),
|
|
3120
|
+
[trajectory],
|
|
3121
|
+
)
|
|
2912
3122
|
const live = useLiveReading(
|
|
2913
3123
|
useChat,
|
|
2914
3124
|
geek,
|
|
@@ -2917,6 +3127,7 @@ window.__ModuleLoader__.load({
|
|
|
2917
3127
|
sessionTurnOpen,
|
|
2918
3128
|
sessionRunning,
|
|
2919
3129
|
sessionStats,
|
|
3130
|
+
models,
|
|
2920
3131
|
)
|
|
2921
3132
|
|
|
2922
3133
|
const triggerRef = useRef(null)
|
|
@@ -2960,8 +3171,8 @@ window.__ModuleLoader__.load({
|
|
|
2960
3171
|
() =>
|
|
2961
3172
|
sessionStats === undefined
|
|
2962
3173
|
? undefined
|
|
2963
|
-
: historyFromStats(sessionStats,
|
|
2964
|
-
[sessionStats,
|
|
3174
|
+
: historyFromStats(sessionStats, models),
|
|
3175
|
+
[sessionStats, models],
|
|
2965
3176
|
)
|
|
2966
3177
|
/*
|
|
2967
3178
|
* Fallback for a composition without the statistics target: derive rows from the Chat
|
|
@@ -3485,7 +3696,17 @@ window.__ModuleLoader__.load({
|
|
|
3485
3696
|
live !== null
|
|
3486
3697
|
? h(
|
|
3487
3698
|
'div',
|
|
3488
|
-
{
|
|
3699
|
+
{
|
|
3700
|
+
className: 'dsh-rum-row dsh-rum-live-row',
|
|
3701
|
+
role: 'row',
|
|
3702
|
+
/*
|
|
3703
|
+
* The live row carries the same two highlights as a settled one, so a
|
|
3704
|
+
* model switch is visible the moment the request starts rather than
|
|
3705
|
+
* only once its row is numbered.
|
|
3706
|
+
*/
|
|
3707
|
+
'data-switched': live.switched === true ? 'true' : undefined,
|
|
3708
|
+
'data-lowcache': isLowCacheHit(live) ? 'true' : undefined,
|
|
3709
|
+
},
|
|
3489
3710
|
/*
|
|
3490
3711
|
* A pulsing dot, no word.
|
|
3491
3712
|
*
|
|
@@ -3547,7 +3768,18 @@ window.__ModuleLoader__.load({
|
|
|
3547
3768
|
h('span', { className: 'dsh-rum-col', role: 'cell' }, secondsText(live.ttftMs, t)),
|
|
3548
3769
|
h(
|
|
3549
3770
|
'span',
|
|
3550
|
-
{
|
|
3771
|
+
{
|
|
3772
|
+
className: 'dsh-rum-col dsh-rum-primary',
|
|
3773
|
+
role: 'cell',
|
|
3774
|
+
/*
|
|
3775
|
+
* The same hint the numbered rows carry: which model is answering.
|
|
3776
|
+
* A compaction names its own summarizer; a request names the model
|
|
3777
|
+
* the plan worked out, and falls back to the generic wording while
|
|
3778
|
+
* nothing is known yet — a turn's first step with no header.
|
|
3779
|
+
*/
|
|
3780
|
+
onMouseEnter: (event) => hint.show(modelHintText(live, t), event),
|
|
3781
|
+
onMouseLeave: hint.hide,
|
|
3782
|
+
},
|
|
3551
3783
|
live.compaction !== null && live.compaction !== undefined
|
|
3552
3784
|
? t('compactionSpeed')
|
|
3553
3785
|
: speedText(live.speed, live.estimated === true, t),
|
|
@@ -3579,6 +3811,14 @@ window.__ModuleLoader__.load({
|
|
|
3579
3811
|
'dsh-rum-row dsh-rum-item' +
|
|
3580
3812
|
(entry.row.compaction === true ? ' dsh-rum-compaction-row' : ''),
|
|
3581
3813
|
'data-phase': entry.phase,
|
|
3814
|
+
/*
|
|
3815
|
+
* Two independent highlights. A request that changed the routed model
|
|
3816
|
+
* gets the business colour; one whose cache hit fell below the threshold
|
|
3817
|
+
* gets the warn colour. Both are attributes rather than inline colours so
|
|
3818
|
+
* the stylesheet owns the tokens and the dark-mode behaviour.
|
|
3819
|
+
*/
|
|
3820
|
+
'data-switched': entry.row.switched === true ? 'true' : undefined,
|
|
3821
|
+
'data-lowcache': isLowCacheHit(entry.row) ? 'true' : undefined,
|
|
3582
3822
|
role: 'row',
|
|
3583
3823
|
},
|
|
3584
3824
|
/*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-recent-usage-metrics",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.4",
|
|
4
4
|
"description": "Geek level for the Performance & usage setting: a live reading of the newest request (first-token latency, speed, tokens, total, amortised) plus a drawer listing the last ten, all derived from the shipped Statistics view data",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|