dsh-recent-usage-metrics 0.3.1 → 0.3.2

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 CHANGED
@@ -1,3 +1,12 @@
1
+ ## 0.3.2
2
+
3
+ - 修复:速度列的悬浮提示常常拿不到模型名,或显示成别的模型。
4
+ 原来从统计折叠的 `request/header` 事件推断,但那种事件只在模型或工具集变化时才写——
5
+ 真实会话里 3139 次请求只对应 24 条,覆盖率约 1%,其余步骤只能借用几小时前的旧值。
6
+ - 改为读轨迹域的 assistant 节点:它把 `assistant/message.source` 原样复制成
7
+ `providerMetadata`,每步都有,是客户端半能拿到的唯一逐步事实。
8
+ - 没有轨迹节点的步骤保持空值,不再借用旧 header 猜测。
9
+
1
10
  ## 0.3.1
2
11
 
3
12
  - 修复:切换模型后的第一步,抽屉速度列的悬浮提示显示了上一个模型。
package/client/client.js CHANGED
@@ -1172,72 +1172,31 @@ window.__ModuleLoader__.load({
1172
1172
  /**
1173
1173
  * Which model answered each step, keyed `turn:step`.
1174
1174
  *
1175
- * The fold puts no provider/model on a step — `stepMetrics` carries only derived figures and
1176
- * has no `startSeq` either — so the model is read from the turn's `request-header` event
1177
- * rows, one per routed request.
1175
+ * Read from the Trajectory domain's assistant nodes, which carry `providerMetadata` copied
1176
+ * straight from `assistant/message.source`. That is the only per-step source of truth the
1177
+ * client half can reach:
1178
1178
  *
1179
- * Two things make a naive "newest header before the step started" rule wrong:
1179
+ * - The statistics fold puts no provider/model on a step, and its `request-header` rows are
1180
+ * written only when the routed model or tool set CHANGES — 24 of them for 3139 requests in
1181
+ * a real session, so they describe about 1% of steps and mislabel the rest.
1182
+ * - The Chat snapshot's assistant nodes drop the source entirely.
1180
1183
  *
1181
- * 1. A header is written a few milliseconds AFTER its own `step/start` (measured: 4 ms), so
1182
- * `header.time <= step.startTime` misses the header that belongs to this request and
1183
- * falls back to the PREVIOUS request's model. That showed the wrong provider on the first
1184
- * step after a model switch.
1185
- * 2. Not every step issues a new header: consecutive steps reuse the routed model, and then
1186
- * the newest earlier header is the correct answer.
1187
- *
1188
- * So the window is tried first — every header from this step up to the next step's start —
1189
- * and the newest earlier header is the fallback. Measured against `assistant/message.source`
1190
- * on a real 3119-step log: the time-only rule matched 99.8%, this one matches 100%.
1191
- *
1192
- * `model/selection` is deliberately not used: it records what the user PICKED and can precede
1193
- * the request that actually carries it, so it would name a model that was not called.
1184
+ * A compaction is not covered here: it has its own `compaction.model`.
1194
1185
  */
1195
- function modelIndexFromStats(statsSnapshot) {
1196
- const turns = statsSnapshot?.turns
1186
+ function modelIndexFromTrajectory(trajectorySnapshot) {
1197
1187
  const index = new Map()
1198
- if (!Array.isArray(turns)) return index
1199
- for (const turn of turns) {
1200
- if (!Array.isArray(turn.steps)) continue
1201
- const headers = []
1202
- if (Array.isArray(turn.events)) {
1203
- for (const event of turn.events) {
1204
- if (event?.kind !== 'request-header') continue
1205
- const at = numberOrNull(event.time)
1206
- if (at === null) continue
1207
- if (typeof event.model !== 'string' && typeof event.provider !== 'string') continue
1208
- headers.push({ time: at, provider: event.provider ?? null, model: event.model ?? null })
1209
- }
1210
- headers.sort((left, right) => left.time - right.time)
1211
- }
1212
- if (headers.length === 0) continue
1213
-
1214
- const starts = []
1215
- for (const step of turn.steps) {
1216
- const at = numberOrNull(step.startTime)
1217
- if (at === null || typeof step.step !== 'number') continue
1218
- starts.push({ at, step: step.step })
1219
- }
1220
- if (starts.length === 0) continue
1221
- starts.sort((left, right) => left.at - right.at)
1222
-
1223
- for (let i = 0; i < starts.length; i += 1) {
1224
- const from = starts[i].at
1225
- const until = i + 1 < starts.length ? starts[i + 1].at : Number.POSITIVE_INFINITY
1226
- let chosen = null
1227
- for (const header of headers) {
1228
- if (header.time < from) continue
1229
- if (header.time >= until) break
1230
- chosen = header
1231
- }
1232
- if (chosen === null) {
1233
- for (const header of headers) {
1234
- if (header.time < from) chosen = header
1235
- else break
1236
- }
1237
- }
1238
- if (chosen === null) continue
1239
- index.set(`${turn.turn}:${starts[i].step}`, chosen)
1240
- }
1188
+ const nodes = trajectorySnapshot?.eventNodes
1189
+ if (!Array.isArray(nodes)) return index
1190
+ for (const node of nodes) {
1191
+ if (node === null || typeof node !== 'object') continue
1192
+ if (node.kind !== 'assistant') continue
1193
+ const meta = node.providerMetadata
1194
+ if (meta === null || typeof meta !== 'object') continue
1195
+ const model = typeof meta.model === 'string' && meta.model !== '' ? meta.model : null
1196
+ const provider = typeof meta.provider === 'string' && meta.provider !== '' ? meta.provider : null
1197
+ if (model === null && provider === null) continue
1198
+ if (typeof node.turn !== 'number' || typeof node.step !== 'number') continue
1199
+ index.set(`${node.turn}:${node.step}`, { provider, model })
1241
1200
  }
1242
1201
  return index
1243
1202
  }
@@ -1295,9 +1254,8 @@ window.__ModuleLoader__.load({
1295
1254
  * *after* selection: capping here at ten and then dropping the live step left the drawer
1296
1255
  * showing nine rows for as long as a request was streaming.
1297
1256
  */
1298
- function historyFromStats(statsSnapshot) {
1257
+ function historyFromStats(statsSnapshot, models) {
1299
1258
  const steps = statsSteps(statsSnapshot)
1300
- const models = modelIndexFromStats(statsSnapshot)
1301
1259
  const rows = []
1302
1260
  for (const entry of steps) {
1303
1261
  // A step only has a row once it has started; `step/start` is what supplies this.
@@ -1305,7 +1263,7 @@ window.__ModuleLoader__.load({
1305
1263
  if (!statsStepHasFigures(entry.metrics)) continue
1306
1264
  const row = readingOfStatsStep(entry)
1307
1265
  // Attribute the request to the model that served it, for the speed column's tooltip.
1308
- const owner = models.get(row.key)
1266
+ const owner = models === undefined ? undefined : models.get(row.key)
1309
1267
  if (owner !== undefined) {
1310
1268
  row.model = owner.model
1311
1269
  row.provider = owner.provider
@@ -2999,8 +2957,11 @@ window.__ModuleLoader__.load({
2999
2957
  * what made the drawer lag the log.
3000
2958
  */
3001
2959
  const statsRows = useMemo(
3002
- () => (sessionStats === undefined ? undefined : historyFromStats(sessionStats)),
3003
- [sessionStats],
2960
+ () =>
2961
+ sessionStats === undefined
2962
+ ? undefined
2963
+ : historyFromStats(sessionStats, modelIndexFromTrajectory(trajectory)),
2964
+ [sessionStats, trajectory],
3004
2965
  )
3005
2966
  /*
3006
2967
  * Fallback for a composition without the statistics target: derive rows from the Chat
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-recent-usage-metrics",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
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",