dsh-recent-usage-metrics 0.3.0 → 0.3.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 CHANGED
@@ -1,3 +1,13 @@
1
+ ## 0.3.1
2
+
3
+ - 修复:切换模型后的第一步,抽屉速度列的悬浮提示显示了上一个模型。
4
+ `request/header` 写在它自己的 `step/start` 之后约 4 毫秒,只按「早于该步开始的最近一条」
5
+ 匹配会漏掉它,退回上一次请求的模型。现在先在该步到下一起点之间找,找不到才回退。
6
+ 在 3119 步的真实日志上,旧规则命中 99.8%,新规则 100%。
7
+
8
+ - 说明:`trae` 等 provider 不单独上报 `cacheReadTokens`,而是并入 `inputTokens`。
9
+ 这类行没有缓存命中率可算,输出 Token 列的提示会显示破折号——这是数据源缺失,不是漏算。
10
+
1
11
  # 更新记录
2
12
 
3
13
  ## 0.3.0
package/client/client.js CHANGED
@@ -1172,14 +1172,25 @@ window.__ModuleLoader__.load({
1172
1172
  /**
1173
1173
  * Which model answered each step, keyed `turn:step`.
1174
1174
  *
1175
- * The fold does not put a provider/model on a step — `stepMetrics` carries only derived
1176
- * figures, and an assistant chat node carries only its usage and timings. The model is
1177
- * recorded once per routed request as a `request/header` event row on the turn, so a step
1178
- * is attributed to the newest header that precedes it.
1179
- *
1180
- * Verified against the log: on a real session the newest preceding header matched the
1181
- * `assistant/message.source` of every one of 40 sampled steps. Headers are per-routed-
1182
- * request, so several consecutive steps share one, and a model switch starts a new one.
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.
1178
+ *
1179
+ * Two things make a naive "newest header before the step started" rule wrong:
1180
+ *
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.
1183
1194
  */
1184
1195
  function modelIndexFromStats(statsSnapshot) {
1185
1196
  const turns = statsSnapshot?.turns
@@ -1191,22 +1202,41 @@ window.__ModuleLoader__.load({
1191
1202
  if (Array.isArray(turn.events)) {
1192
1203
  for (const event of turn.events) {
1193
1204
  if (event?.kind !== 'request-header') continue
1205
+ const at = numberOrNull(event.time)
1206
+ if (at === null) continue
1194
1207
  if (typeof event.model !== 'string' && typeof event.provider !== 'string') continue
1195
- headers.push({ time: numberOrNull(event.time) ?? 0, provider: event.provider ?? null, model: event.model ?? null })
1208
+ headers.push({ time: at, provider: event.provider ?? null, model: event.model ?? null })
1196
1209
  }
1197
1210
  headers.sort((left, right) => left.time - right.time)
1198
1211
  }
1199
1212
  if (headers.length === 0) continue
1213
+
1214
+ const starts = []
1200
1215
  for (const step of turn.steps) {
1201
1216
  const at = numberOrNull(step.startTime)
1202
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
1203
1226
  let chosen = null
1204
1227
  for (const header of headers) {
1205
- if (header.time <= at) chosen = header
1206
- else break
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
+ }
1207
1237
  }
1208
1238
  if (chosen === null) continue
1209
- index.set(`${turn.turn}:${step.step}`, chosen)
1239
+ index.set(`${turn.turn}:${starts[i].step}`, chosen)
1210
1240
  }
1211
1241
  }
1212
1242
  return index
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-recent-usage-metrics",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
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",