dsh-recent-usage-metrics 0.3.7 → 0.4.0
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 +13 -0
- package/README.en.md +7 -0
- package/README.md +5 -0
- package/client/client.js +184 -55
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,16 @@
|
|
|
1
|
+
## 0.4.0
|
|
2
|
+
|
|
3
|
+
把「模型切换」与「请求配置变更」分开——它们此前被当成同一件事,而后者远比前者常见。
|
|
4
|
+
|
|
5
|
+
- 判定改为:窗口内有 `request/header` **且本步模型与上一步不同**才算切换模型;上一步
|
|
6
|
+
模型未知时以 header 为准。实测一个真实会话:27 个 header 里只有 7 次是真正的模型切换,
|
|
7
|
+
其余 20 次是工具集变化(启用或禁用插件)。旧规则把这 20 次也标成切换模型,是错的。
|
|
8
|
+
- 新增「配置变更」信号:有 header 但模型未变。它的可见后果就是缓存变冷,所以显示在黄色
|
|
9
|
+
那一行里。
|
|
10
|
+
- 序号列现在会在高亮行上给出说明:切换模型时写「该行切换模型至:X」;缓存过低时写
|
|
11
|
+
「该行请求配置变更(工具集 66→65)」加「缓存命中 0.4%,低于 90%」。普通行的序号没有提示。
|
|
12
|
+
- 两个工具数只有都已知时才写成区间,否则只说配置发生变化,不编造数字。
|
|
13
|
+
|
|
1
14
|
## 0.3.7
|
|
2
15
|
|
|
3
16
|
- `#` 表头的提示拆成三行:最近记录、三个状态图标、两种行颜色。原来挤成一句,
|
package/README.en.md
CHANGED
|
@@ -165,6 +165,13 @@ Every drawer column and every header carries a hint:
|
|
|
165
165
|
When both hold, the text takes the warn colour and the bar keeps the business colour. A
|
|
166
166
|
compaction row never takes the cache highlight.
|
|
167
167
|
|
|
168
|
+
**A model switch and a configuration change are not the same thing.** A `request/header` only says
|
|
169
|
+
the routed configuration changed — enabling or disabling a plugin rewrites the tool list and writes
|
|
170
|
+
a header without touching the model. On a real session, only 7 of 27 headers were genuine model
|
|
171
|
+
switches. Blue therefore appears only when the model itself differs; a configuration change without
|
|
172
|
+
a model change shows up as a cold cache, coloured yellow, with the tool-list movement named in the
|
|
173
|
+
row-number hint.
|
|
174
|
+
|
|
168
175
|
**Where the model name comes from:** a `request/header` inside the step's window means a switch
|
|
169
176
|
and supplies the model; otherwise the previous step's model carries over, and a landed reply
|
|
170
177
|
overrides both via `assistant/message.source`. The live row follows the same rule, so the model
|
package/README.md
CHANGED
|
@@ -148,6 +148,11 @@ DSH 的极客级请求指标。它在 **设置 → 通用 → 性能与用量**
|
|
|
148
148
|
|
|
149
149
|
两者同时成立时文字取警告色、色条保留业务蓝。压缩行不参与缓存高亮。
|
|
150
150
|
|
|
151
|
+
**「切换模型」与「配置变更」不是一回事。** 一个请求会写出 `request/header`,只说明路由配置
|
|
152
|
+
变了——启用或禁用插件会改变工具集,同样会写 header,但模型没换。实测一个真实会话:27 个
|
|
153
|
+
header 里只有 7 次是真正的模型切换。所以蓝色只在模型确实不同时出现;模型没变但配置变了,
|
|
154
|
+
它的后果就是缓存变冷,显示为黄色,并在序号提示里写明工具集的变化。
|
|
155
|
+
|
|
151
156
|
**模型名的来源:** 本步窗口内的 `request/header` 判为切换,否则沿用上一步;回复落地后
|
|
152
157
|
由 `assistant/message.source` 覆盖。实时行同样适用,所以生成期间就能看到模型名。
|
|
153
158
|
|
package/client/client.js
CHANGED
|
@@ -122,6 +122,11 @@ window.__ModuleLoader__.load({
|
|
|
122
122
|
hintIndexRows: '行号:1–10 为已完成的最近记录',
|
|
123
123
|
hintIndexGlyphs: '{bolt} 为模型输出中,{tools} 为工具调用中,{compact} 为压缩上下文中',
|
|
124
124
|
hintColour: '整行黄色表示缓存命中过低,蓝色表示这一步切换了模型',
|
|
125
|
+
/* The row-number hint: only a highlighted row carries one, and it says why in numbers. */
|
|
126
|
+
rowSwitched: '该行切换模型至:{model}',
|
|
127
|
+
rowLowCache: '缓存命中 {rate},低于 {threshold}%',
|
|
128
|
+
rowConfigChanged: '该行请求配置变更(工具集 {from}→{to})',
|
|
129
|
+
rowConfigChangedUnknown: '该行请求配置变更,缓存前缀失效',
|
|
125
130
|
hintTime: '请求发出的时刻(本地时间)',
|
|
126
131
|
hintTtft: '从发出到收到第一个 token 的耗时',
|
|
127
132
|
hintSpeed: '模型生成速度 = 输出 token ÷ 解码时长',
|
|
@@ -186,6 +191,10 @@ window.__ModuleLoader__.load({
|
|
|
186
191
|
hintIndexRows: 'Row number: 1–10 are the most recent finished records',
|
|
187
192
|
hintIndexGlyphs: '{bolt} is a model reply, {tools} is a tool call, {compact} is a context compaction',
|
|
188
193
|
hintColour: 'A yellow row means a low cache hit; a blue row means this step switched models',
|
|
194
|
+
rowSwitched: 'This row switched to {model}',
|
|
195
|
+
rowLowCache: 'Cache hit {rate}, below {threshold}%',
|
|
196
|
+
rowConfigChanged: 'Request configuration changed on this row (tools {from}→{to})',
|
|
197
|
+
rowConfigChangedUnknown: 'Request configuration changed on this row; the cached prefix was lost',
|
|
189
198
|
hintTime: 'When the request was issued (local time)',
|
|
190
199
|
hintTtft: 'Time from issuing the request to receiving its first token',
|
|
191
200
|
hintSpeed: 'Generation speed = output tokens ÷ decode time',
|
|
@@ -1306,27 +1315,36 @@ window.__ModuleLoader__.load({
|
|
|
1306
1315
|
}
|
|
1307
1316
|
|
|
1308
1317
|
/**
|
|
1309
|
-
* Which model a step used,
|
|
1318
|
+
* Which model a step used, whether that request changed models, and whether it changed the
|
|
1319
|
+
* routed configuration at all.
|
|
1310
1320
|
*
|
|
1311
|
-
* A `request/header` is written
|
|
1312
|
-
*
|
|
1321
|
+
* A `request/header` is written when the routed configuration changes, and that includes far
|
|
1322
|
+
* more than the model: enabling or disabling a plugin changes the tool list, which rewrites
|
|
1323
|
+
* the request prefix and invalidates the provider's cache. Measured on a real session, 27
|
|
1324
|
+
* headers covered only 7 genuine model switches, so "a header exists" is NOT "the model
|
|
1325
|
+
* changed" — treating the two as one flagged every plugin toggle as a model switch.
|
|
1313
1326
|
*
|
|
1314
|
-
*
|
|
1315
|
-
* changed the routed configuration, and the header names the model it switched to;
|
|
1316
|
-
* - absent → the step reused whatever was already routed, so it inherits the previous
|
|
1317
|
-
* step's model.
|
|
1327
|
+
* The three signals are therefore separated:
|
|
1318
1328
|
*
|
|
1319
|
-
*
|
|
1320
|
-
*
|
|
1321
|
-
*
|
|
1322
|
-
*
|
|
1323
|
-
*
|
|
1324
|
-
*
|
|
1329
|
+
* - `switched` — a header landed in the step's window AND the model differs from the one
|
|
1330
|
+
* before it. When the previous model is unknown the header is trusted, because there is
|
|
1331
|
+
* nothing to contradict it.
|
|
1332
|
+
* - `configChanged` — a header landed in the window but the model did NOT change, so the
|
|
1333
|
+
* change was elsewhere in the configuration (the tool list). Its visible consequence is a
|
|
1334
|
+
* cold cache, which is why it is reported beside the low-cache highlight.
|
|
1335
|
+
* - `toolCount` / `previousToolCount` — the tool-list sizes the two headers carried, so the
|
|
1336
|
+
* hint can say what actually moved rather than gesturing at "the configuration".
|
|
1325
1337
|
*/
|
|
1326
1338
|
function modelPlanFromStats(statsSnapshot, known) {
|
|
1327
1339
|
const plan = new Map()
|
|
1328
1340
|
const turns = statsSnapshot?.turns
|
|
1329
1341
|
if (!Array.isArray(turns)) return plan
|
|
1342
|
+
/*
|
|
1343
|
+
* The plan walks the session's steps in time order across turns, not turn by turn, because
|
|
1344
|
+
* "the previous model" has to compare against the step before it even when that step lives
|
|
1345
|
+
* in an earlier turn — a turn's first step is exactly where a switch is most likely.
|
|
1346
|
+
*/
|
|
1347
|
+
const sequence = []
|
|
1330
1348
|
for (const turn of turns) {
|
|
1331
1349
|
if (!Array.isArray(turn.steps)) continue
|
|
1332
1350
|
const headers = []
|
|
@@ -1336,62 +1354,98 @@ window.__ModuleLoader__.load({
|
|
|
1336
1354
|
const at = numberOrNull(event.time)
|
|
1337
1355
|
if (at === null) continue
|
|
1338
1356
|
if (typeof event.model !== 'string' && typeof event.provider !== 'string') continue
|
|
1339
|
-
headers.push({
|
|
1357
|
+
headers.push({
|
|
1358
|
+
time: at,
|
|
1359
|
+
provider: event.provider ?? null,
|
|
1360
|
+
model: event.model ?? null,
|
|
1361
|
+
tools: numberOrNull(event.toolCount),
|
|
1362
|
+
})
|
|
1340
1363
|
}
|
|
1341
1364
|
headers.sort((left, right) => left.time - right.time)
|
|
1342
1365
|
}
|
|
1343
|
-
const
|
|
1366
|
+
const steps = []
|
|
1344
1367
|
for (const step of turn.steps) {
|
|
1345
1368
|
const at = numberOrNull(step.startTime)
|
|
1346
1369
|
if (at === null || typeof step.step !== 'number') continue
|
|
1347
|
-
|
|
1370
|
+
steps.push({ at, step: step.step })
|
|
1371
|
+
}
|
|
1372
|
+
steps.sort((left, right) => left.at - right.at)
|
|
1373
|
+
for (const step of steps) sequence.push({ turn: turn.turn, step: step.step, at: step.at, headers })
|
|
1374
|
+
}
|
|
1375
|
+
sequence.sort((left, right) => left.at - right.at)
|
|
1376
|
+
|
|
1377
|
+
/** The model of the step before this one, or null when it is not known. */
|
|
1378
|
+
let previousModel = null
|
|
1379
|
+
let previousTools = null
|
|
1380
|
+
let inForce = null
|
|
1381
|
+
|
|
1382
|
+
for (let index = 0; index < sequence.length; index += 1) {
|
|
1383
|
+
const entry = sequence[index]
|
|
1384
|
+
const key = `${entry.turn}:${entry.step}`
|
|
1385
|
+
const nextAt = index + 1 < sequence.length ? sequence[index + 1].at : Number.POSITIVE_INFINITY
|
|
1386
|
+
let inside = null
|
|
1387
|
+
for (const header of entry.headers) {
|
|
1388
|
+
if (header.time < entry.at) continue
|
|
1389
|
+
if (header.time >= nextAt) break
|
|
1390
|
+
inside = header
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
const settled = known === undefined ? undefined : known.get(key)
|
|
1394
|
+
const settledModel =
|
|
1395
|
+
settled !== undefined && settled.model !== null
|
|
1396
|
+
? { provider: settled.provider, model: settled.model }
|
|
1397
|
+
: null
|
|
1398
|
+
|
|
1399
|
+
if (inside !== null) {
|
|
1400
|
+
const headerModel = { provider: inside.provider, model: inside.model }
|
|
1401
|
+
const changed =
|
|
1402
|
+
previousModel === null ||
|
|
1403
|
+
headerModel.model === null ||
|
|
1404
|
+
previousModel.model !== headerModel.model ||
|
|
1405
|
+
previousModel.provider !== headerModel.provider
|
|
1406
|
+
plan.set(key, {
|
|
1407
|
+
provider: inside.provider,
|
|
1408
|
+
model: inside.model,
|
|
1409
|
+
/*
|
|
1410
|
+
* A header alone proves only that the routed configuration changed. Saying "the model
|
|
1411
|
+
* switched" additionally requires that the model itself differs — and when the
|
|
1412
|
+
* previous model is unknown, the header is the only evidence, so it is trusted.
|
|
1413
|
+
*/
|
|
1414
|
+
switched: changed,
|
|
1415
|
+
configChanged: !changed,
|
|
1416
|
+
toolCount: inside.tools,
|
|
1417
|
+
previousToolCount: previousTools,
|
|
1418
|
+
source: 'header',
|
|
1419
|
+
})
|
|
1420
|
+
inForce = headerModel
|
|
1421
|
+
previousModel = headerModel
|
|
1422
|
+
previousTools = inside.tools
|
|
1423
|
+
continue
|
|
1348
1424
|
}
|
|
1349
|
-
if (starts.length === 0) continue
|
|
1350
|
-
starts.sort((left, right) => left.at - right.at)
|
|
1351
1425
|
|
|
1352
1426
|
/*
|
|
1353
|
-
*
|
|
1354
|
-
*
|
|
1355
|
-
* A step whose header names a switch becomes the new carrier, so the step AFTER it
|
|
1356
|
-
* inherits the switched-to model — not the one before the switch, which `known` alone
|
|
1357
|
-
* would give.
|
|
1427
|
+
* No header: the step reused what was already routed. A finished reply outranks the
|
|
1428
|
+
* carried model, because `assistant/message.source` is the authority for a settled step.
|
|
1358
1429
|
*/
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
// The header was written for THIS request, so the model changed here.
|
|
1373
|
-
inForce = { provider: inside.provider, model: inside.model }
|
|
1374
|
-
plan.set(key, { provider: inside.provider, model: inside.model, switched: true, source: 'header' })
|
|
1375
|
-
continue
|
|
1376
|
-
}
|
|
1377
|
-
/*
|
|
1378
|
-
* No header: inherit. A finished reply outranks the carried model, because
|
|
1379
|
-
* `assistant/message.source` is the authority for a step that has settled.
|
|
1380
|
-
*/
|
|
1381
|
-
const settled = known === undefined ? undefined : known.get(key)
|
|
1382
|
-
if (settled !== undefined && settled.model !== null) {
|
|
1383
|
-
inForce = { provider: settled.provider, model: settled.model }
|
|
1384
|
-
plan.set(key, { provider: settled.provider, model: settled.model, switched: false, source: 'reply' })
|
|
1385
|
-
continue
|
|
1386
|
-
}
|
|
1387
|
-
if (inForce !== null) {
|
|
1388
|
-
plan.set(key, { provider: inForce.provider, model: inForce.model, switched: false, source: 'inherit' })
|
|
1389
|
-
}
|
|
1430
|
+
const resolved = settledModel ?? inForce
|
|
1431
|
+
if (resolved !== null) {
|
|
1432
|
+
plan.set(key, {
|
|
1433
|
+
provider: resolved.provider,
|
|
1434
|
+
model: resolved.model,
|
|
1435
|
+
switched: false,
|
|
1436
|
+
configChanged: false,
|
|
1437
|
+
toolCount: null,
|
|
1438
|
+
previousToolCount: null,
|
|
1439
|
+
source: settledModel !== null ? 'reply' : 'inherit',
|
|
1440
|
+
})
|
|
1441
|
+
inForce = resolved
|
|
1442
|
+
previousModel = resolved
|
|
1390
1443
|
}
|
|
1391
1444
|
}
|
|
1392
1445
|
return plan
|
|
1393
1446
|
}
|
|
1394
1447
|
|
|
1448
|
+
|
|
1395
1449
|
/**
|
|
1396
1450
|
* Whether any step started after this compaction finished.
|
|
1397
1451
|
*
|
|
@@ -1464,6 +1518,14 @@ window.__ModuleLoader__.load({
|
|
|
1464
1518
|
row.model = owner.model
|
|
1465
1519
|
row.provider = owner.provider
|
|
1466
1520
|
row.switched = owner.switched === true
|
|
1521
|
+
/*
|
|
1522
|
+
* A configuration change that was not a model switch still rewrote the request prefix,
|
|
1523
|
+
* so it is worth naming when the cache came back cold: that is the difference between
|
|
1524
|
+
* "the account changed" and "the tool list moved".
|
|
1525
|
+
*/
|
|
1526
|
+
row.configChanged = owner.configChanged === true
|
|
1527
|
+
row.toolCount = owner.toolCount ?? null
|
|
1528
|
+
row.previousToolCount = owner.previousToolCount ?? null
|
|
1467
1529
|
}
|
|
1468
1530
|
rows.push(row)
|
|
1469
1531
|
}
|
|
@@ -1884,6 +1946,47 @@ window.__ModuleLoader__.load({
|
|
|
1884
1946
|
)
|
|
1885
1947
|
}
|
|
1886
1948
|
|
|
1949
|
+
/**
|
|
1950
|
+
* The row-number hint, or an empty list when the row is not highlighted.
|
|
1951
|
+
*
|
|
1952
|
+
* A highlighted row is worth explaining, and the number column is the one place that has room
|
|
1953
|
+
* without competing with a figure. Two independent reasons can apply, so the result is a list
|
|
1954
|
+
* of lines rather than a sentence:
|
|
1955
|
+
*
|
|
1956
|
+
* - the model switched, named outright;
|
|
1957
|
+
* - the cache came back cold, with the rate and the threshold it fell below.
|
|
1958
|
+
*
|
|
1959
|
+
* A cold row that ALSO changed its routed configuration names the tool-list movement, because
|
|
1960
|
+
* that is the usual cause of a lost prefix and the difference between "the account rotated"
|
|
1961
|
+
* and "a plugin was enabled". When the two tool counts are not both known, the line says only
|
|
1962
|
+
* that the configuration changed rather than inventing numbers.
|
|
1963
|
+
*/
|
|
1964
|
+
function rowHintLines(row, t) {
|
|
1965
|
+
const lines = []
|
|
1966
|
+
if (row.compaction === true) return lines
|
|
1967
|
+
if (row.switched === true) {
|
|
1968
|
+
lines.push(t('rowSwitched', { model: modelHintText(row, t) }))
|
|
1969
|
+
}
|
|
1970
|
+
if (isLowCacheHit(row)) {
|
|
1971
|
+
if (row.configChanged === true) {
|
|
1972
|
+
const from = numberOrNull(row.previousToolCount)
|
|
1973
|
+
const to = numberOrNull(row.toolCount)
|
|
1974
|
+
lines.push(
|
|
1975
|
+
from !== null && to !== null
|
|
1976
|
+
? t('rowConfigChanged', { from, to })
|
|
1977
|
+
: t('rowConfigChangedUnknown'),
|
|
1978
|
+
)
|
|
1979
|
+
}
|
|
1980
|
+
lines.push(
|
|
1981
|
+
t('rowLowCache', {
|
|
1982
|
+
rate: `${(row.cacheHit * 100).toFixed(1)}%`,
|
|
1983
|
+
threshold: Math.round(LOW_CACHE_HIT * 100),
|
|
1984
|
+
}),
|
|
1985
|
+
)
|
|
1986
|
+
}
|
|
1987
|
+
return lines
|
|
1988
|
+
}
|
|
1989
|
+
|
|
1887
1990
|
/**
|
|
1888
1991
|
* The speed column's hint: which model served this row.
|
|
1889
1992
|
*
|
|
@@ -3972,7 +4075,33 @@ window.__ModuleLoader__.load({
|
|
|
3972
4075
|
*/
|
|
3973
4076
|
h(
|
|
3974
4077
|
'span',
|
|
3975
|
-
{
|
|
4078
|
+
{
|
|
4079
|
+
className: 'dsh-rum-index',
|
|
4080
|
+
role: 'cell',
|
|
4081
|
+
/*
|
|
4082
|
+
* Only a highlighted row explains itself here; an ordinary row's number
|
|
4083
|
+
* needs no gloss. The handler is attached unconditionally but returns
|
|
4084
|
+
* nothing to show when the list is empty, so hovering a plain row stays
|
|
4085
|
+
* silent.
|
|
4086
|
+
*/
|
|
4087
|
+
onMouseEnter: (event) => {
|
|
4088
|
+
const lines = rowHintLines(entry.row, t)
|
|
4089
|
+
if (lines.length === 0) return
|
|
4090
|
+
hint.show(
|
|
4091
|
+
lines.length === 1
|
|
4092
|
+
? lines[0]
|
|
4093
|
+
: h(
|
|
4094
|
+
'span',
|
|
4095
|
+
{ className: 'dsh-rum-tip-body' },
|
|
4096
|
+
...lines.map((line, index) =>
|
|
4097
|
+
h('span', { className: 'dsh-rum-tip-line' }, line),
|
|
4098
|
+
),
|
|
4099
|
+
),
|
|
4100
|
+
event,
|
|
4101
|
+
)
|
|
4102
|
+
},
|
|
4103
|
+
onMouseLeave: hint.hide,
|
|
4104
|
+
},
|
|
3976
4105
|
entry.row.compaction === true
|
|
3977
4106
|
? h(CompactionIcon, {
|
|
3978
4107
|
className:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-recent-usage-metrics",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
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",
|