opencode-usage-insights 0.1.0 → 0.1.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/dist/tui.js CHANGED
@@ -54,7 +54,7 @@ function totalize(steps) {
54
54
  }
55
55
  return totals;
56
56
  }
57
- function affinityKey(model) {
57
+ function identityKey(model) {
58
58
  if (!model)
59
59
  return;
60
60
  return `${model.providerID}/${model.id}${model.variant ? `#${model.variant}` : ""}`;
@@ -71,14 +71,16 @@ function stepEnd(message) {
71
71
  return;
72
72
  return time.completed ?? time.streamed ?? time.created;
73
73
  }
74
- function cacheDrop(reference, current) {
74
+ function cacheDrop(reference, current, scope = "provider") {
75
75
  if (!reference)
76
76
  return;
77
77
  const before = reference.model;
78
78
  const after = current.model;
79
79
  if (!before || !after)
80
80
  return;
81
- if (before.providerID !== after.providerID || before.id !== after.id || (before.variant ?? "") !== (after.variant ?? "")) {
81
+ if (scope === "provider" && before.providerID !== after.providerID)
82
+ return;
83
+ if (before.id !== after.id || (before.variant ?? "") !== (after.variant ?? "")) {
82
84
  return;
83
85
  }
84
86
  const drop = cacheRead(reference) - cacheRead(current);
@@ -135,8 +137,8 @@ function computeTurn(messages, session) {
135
137
  }
136
138
  const provider = lastStep.model?.providerID;
137
139
  const previousProvider = previousStep?.model?.providerID;
138
- const key = affinityKey(lastStep.model);
139
- const previousKey = affinityKey(previousStep?.model);
140
+ const key = identityKey(lastStep.model);
141
+ const previousKey = identityKey(previousStep?.model);
140
142
  return {
141
143
  endAt: stepEnd(lastStep),
142
144
  startAt: stepCreated(settled[0] ?? lastStep),
@@ -145,9 +147,6 @@ function computeTurn(messages, session) {
145
147
  providerID: provider,
146
148
  modelID: lastStep.model?.id,
147
149
  variant: lastStep.model?.variant,
148
- router: undefined,
149
- identityChanged: false,
150
- affinityKey: key,
151
150
  providerChanged: previousProvider !== undefined && provider !== undefined && previousProvider !== provider,
152
151
  modelChanged: previousKey !== undefined && key !== undefined && previousKey !== key && previousProvider === provider,
153
152
  totals: totalize(settled),
@@ -156,8 +155,7 @@ function computeTurn(messages, session) {
156
155
  drops,
157
156
  cacheDropTokens: drops.reduce((sum, drop) => sum + drop.tokens, 0),
158
157
  cacheExpired: drops.length > 0,
159
- steps: settled.length,
160
- stepIDs: settled.map((step) => step.id)
158
+ steps: settled.length
161
159
  };
162
160
  }
163
161
  function promptGap(current, previous) {
@@ -195,7 +193,7 @@ var DEFAULT_FIELDS = {
195
193
  turn: true,
196
194
  session: true,
197
195
  reasoning: false,
198
- cost: false,
196
+ cost: true,
199
197
  cacheExpiry: true
200
198
  };
201
199
  function formatClock(ms) {
@@ -254,7 +252,7 @@ function displayTotals(totals) {
254
252
  prompt
255
253
  };
256
254
  }
257
- function totalsText(prefix, totals, labels, fields, stepCount = 0) {
255
+ function totalsText(prefix, totals, labels, fields, stepCount = 0, cost = totals.cost) {
258
256
  const display = displayTotals(totals);
259
257
  const input = display.prompt === display.input ? `${labels.input} ${formatTokens(display.input)}` : `${labels.input} ${formatTokens(display.input)} (${formatTokens(display.prompt)})`;
260
258
  const parts = [
@@ -264,8 +262,8 @@ function totalsText(prefix, totals, labels, fields, stepCount = 0) {
264
262
  ];
265
263
  if (fields.reasoning)
266
264
  parts.push(`${labels.reasoning} ${formatTokens(display.reasoning)}`);
267
- if (fields.cost)
268
- parts.push(`${labels.cost} ${formatCost(display.cost)}`);
265
+ if (fields.cost && cost > 0)
266
+ parts.push(`${labels.cost} ${formatCost(cost)}`);
269
267
  const steps = stepCount > 1 ? `${stepCount} ${stepCount === 1 ? labels.step : labels.steps}` : "";
270
268
  const head = [prefix, steps].filter((piece) => piece !== "").join(" ");
271
269
  const numbers = parts.join(", ");
@@ -277,7 +275,6 @@ function formatLineSegments(stats, options) {
277
275
  const labels = { ...DEFAULT_LABELS, ...options?.labels };
278
276
  const fields = { ...DEFAULT_FIELDS, ...options?.fields };
279
277
  const turnTotals = options?.turnTotals ?? "latest";
280
- const identity = options?.identity ?? "endpoint";
281
278
  const segments = [];
282
279
  if (fields.clock && stats.endAt !== undefined) {
283
280
  segments.push({ text: formatClock(stats.endAt), tone: "muted" });
@@ -285,11 +282,7 @@ function formatLineSegments(stats, options) {
285
282
  if (fields.delta && stats.promptGapMs !== undefined) {
286
283
  segments.push({ text: `${labels.delta} ${formatDuration(stats.promptGapMs)}`, tone: "muted" });
287
284
  }
288
- if (identity === "endpoint" && stats.router !== undefined) {
289
- segments.push({ text: stats.router, tone: stats.identityChanged ? "warning" : "base" });
290
- if (stats.identityChanged)
291
- segments.push({ text: labels.providerChanged, tone: "warning" });
292
- } else if (stats.providerID !== undefined) {
285
+ if (stats.providerID !== undefined) {
293
286
  let text = stats.providerID;
294
287
  if (fields.model && stats.modelID !== undefined)
295
288
  text += `/${stats.modelID}`;
@@ -303,10 +296,13 @@ function formatLineSegments(stats, options) {
303
296
  }
304
297
  if (fields.turn && stats.totals.steps > 0) {
305
298
  const totals = turnTotals === "sum" ? stats.totals : stats.latest;
306
- segments.push({ text: totalsText(labels.turn, totals, labels, fields, turnTotals === "sum" ? stats.totals.steps : stats.steps), tone: "base" });
299
+ segments.push({
300
+ text: totalsText(labels.turn, totals, labels, fields, turnTotals === "sum" ? stats.totals.steps : stats.steps, stats.totals.cost),
301
+ tone: "base"
302
+ });
307
303
  }
308
304
  if (fields.session && stats.session) {
309
- segments.push({ text: totalsText(labels.session, stats.session, labels, fields), tone: "muted" });
305
+ segments.push({ text: totalsText(labels.session, stats.session, labels, fields, 0, stats.session.cost), tone: "muted" });
310
306
  }
311
307
  if (fields.cacheExpiry && stats.cacheExpired) {
312
308
  const count = stats.drops.length > 1 ? `\xD7${stats.drops.length}` : "";
@@ -341,10 +337,6 @@ var TOKEN = /^[A-Za-z0-9][A-Za-z0-9._:/-]*$/;
341
337
  function isToken(value, limit = 64) {
342
338
  return value.length > 0 && value.length <= limit && TOKEN.test(value);
343
339
  }
344
- function normalizeToken(value, limit = 64) {
345
- const trimmed = value.trim().toLowerCase();
346
- return isToken(trimmed, limit) ? trimmed : undefined;
347
- }
348
340
 
349
341
  // src/line.ts
350
342
  function toTranscript(messages) {
@@ -485,9 +477,9 @@ function buildWhere(range, filters, column = STEP_TIME) {
485
477
  clauses.push(`${MODEL_KEY_SQL} = ?`);
486
478
  params.push(filters.modelKey);
487
479
  }
488
- if (filters?.router !== undefined && filters.router !== "") {
489
- clauses.push("router = ?");
490
- params.push(filters.router);
480
+ if (filters?.provider !== undefined && filters.provider !== "") {
481
+ clauses.push("provider_id = ?");
482
+ params.push(filters.provider);
491
483
  }
492
484
  if (filters?.sessionID !== undefined && filters.sessionID !== "") {
493
485
  clauses.push("session_id = ?");
@@ -524,34 +516,32 @@ function overview(db, range, filters) {
524
516
  function models(db, range, filters) {
525
517
  const filter = buildWhere(range, filters);
526
518
  const rows = db.query(`SELECT provider_id, model_id, variant,
527
- GROUP_CONCAT(DISTINCT router) AS router_list,
528
519
  ${USAGE_SELECT},
529
520
  MIN(${STEP_TIME}) AS first_at,
530
521
  MAX(COALESCE(completed_at, recorded_at)) AS last_at
531
522
  FROM steps ${filter.where}
532
523
  GROUP BY provider_id, model_id, variant
533
524
  ORDER BY steps DESC, cached DESC`).all(...filter.params);
534
- const models2 = new Map;
525
+ const models = new Map;
535
526
  for (const row of rows) {
536
527
  const modelID = row.model_id ?? "(unknown)";
537
528
  const variant = row.variant ?? undefined;
538
529
  const key = `${modelID}#${variant ?? ""}`;
539
- let model = models2.get(key);
530
+ let model = models.get(key);
540
531
  if (!model) {
541
532
  model = { modelID, variant, providers: [], totals: emptyUsage() };
542
- models2.set(key, model);
533
+ models.set(key, model);
543
534
  }
544
535
  const usage = usageFrom(row);
545
536
  model.providers.push({
546
537
  ...usage,
547
538
  providerID: row.provider_id ?? "(unknown)",
548
- routers: row.router_list ? row.router_list.split(",").filter(Boolean) : [],
549
539
  firstAt: row.first_at ?? undefined,
550
540
  lastAt: row.last_at ?? undefined
551
541
  });
552
542
  addUsage(model.totals, usage);
553
543
  }
554
- return [...models2.values()].sort((a, b) => billedTokens(b.totals) - billedTokens(a.totals));
544
+ return [...models.values()].sort((a, b) => billedTokens(b.totals) - billedTokens(a.totals));
555
545
  }
556
546
  function emptyUsage() {
557
547
  return { steps: 0, input: 0, cached: 0, cacheWrite: 0, output: 0, reasoning: 0, cost: 0 };
@@ -684,7 +674,7 @@ function turns(db, options) {
684
674
  }
685
675
  const steps = db.query(`SELECT message_id, session_id, provider_id, model_id, variant,
686
676
  input_tokens, cache_read, cache_write, output_tokens, reasoning_tokens, cost,
687
- created_at, streamed_at, recorded_at, completed_at, finish, router
677
+ created_at, streamed_at, recorded_at, completed_at, finish
688
678
  FROM steps ${clauses.length > 0 ? `WHERE ${clauses.join(" AND ")}` : ""}
689
679
  ORDER BY session_id, ${STEP_TIME}`).all(...params);
690
680
  const promptClause = options?.sessionID === undefined ? "" : `WHERE session_id = ?${floor === undefined ? "" : " AND created_at >= ?"}`;
@@ -723,18 +713,9 @@ function turns(db, options) {
723
713
  }
724
714
  reference = message;
725
715
  }
726
- const lastWith = (pick) => {
727
- for (let index = bucket.length - 1;index >= 0; index -= 1) {
728
- const value = pick(bucket[index]);
729
- if (value !== null)
730
- return value;
731
- }
732
- return;
733
- };
734
716
  const providerID = last.provider_id ?? undefined;
735
717
  const modelID = last.model_id ?? undefined;
736
718
  const variant = last.variant ?? undefined;
737
- const router = lastWith((row) => row.router);
738
719
  let produced = 0;
739
720
  let window = 0;
740
721
  for (const row of bucket) {
@@ -752,8 +733,6 @@ function turns(db, options) {
752
733
  providerID,
753
734
  modelID,
754
735
  variant,
755
- router,
756
- routerChanged: previous?.router !== undefined && router !== undefined && previous.router !== router,
757
736
  providerChanged: previous?.providerID !== undefined && providerID !== undefined && previous.providerID !== providerID,
758
737
  modelChanged: previous !== undefined && previous.providerID === providerID && `${previous.modelID ?? ""}#${previous.variant ?? ""}` !== `${modelID ?? ""}#${variant ?? ""}`,
759
738
  steps: bucket.length,
@@ -797,31 +776,14 @@ function promptFloor(db, sessionID, count) {
797
776
  const rows = db.query("SELECT created_at FROM prompts WHERE session_id = ? AND created_at IS NOT NULL ORDER BY created_at DESC LIMIT ?").all(sessionID, count);
798
777
  return rows.length === count ? rows[count - 1].created_at : undefined;
799
778
  }
800
- function routerBefore(db, sessionID, at) {
801
- if (at === undefined)
802
- return;
803
- const row = db.query(`SELECT router FROM steps
804
- WHERE session_id = $sessionID AND router IS NOT NULL AND COALESCE(created_at, recorded_at) < $at
805
- ORDER BY COALESCE(created_at, recorded_at) DESC LIMIT 1`).get({ $sessionID: sessionID, $at: at });
806
- return row?.router ?? undefined;
807
- }
808
- function routerForSteps(db, sessionID, messageIDs) {
809
- if (messageIDs.length === 0)
810
- return;
811
- const placeholders = messageIDs.map(() => "?").join(", ");
812
- const row = db.query(`SELECT router FROM steps
813
- WHERE session_id = ? AND message_id IN (${placeholders}) AND router IS NOT NULL
814
- ORDER BY recorded_at DESC LIMIT 1`).get(sessionID, ...messageIDs);
815
- return row?.router ?? undefined;
816
- }
817
779
  function filterOptions(db) {
818
780
  const pick = (column) => db.query(`SELECT ${column} AS value, COUNT(*) AS steps FROM steps WHERE ${column} IS NOT NULL GROUP BY ${column} ORDER BY steps DESC`).all().map((row) => ({ value: row.value, steps: row.steps }));
819
- return { models: pick("model_id"), endpoints: pick("router") };
781
+ return { models: pick("model_id"), providers: pick("provider_id") };
820
782
  }
821
783
 
822
784
  // src/panel-data.ts
823
785
  var PANEL_NAME = "usage-insights";
824
- var PANEL_VIEWS = ["overview", "models", "endpoints", "affinity", "cache", "periods", "latency", "sessions", "turns", "db"];
786
+ var PANEL_VIEWS = ["overview", "models", "providers", "cache", "periods", "latency", "sessions", "turns", "db"];
825
787
  var RANGE_KEYS = ["session", "today", "7d", "30d", "all"];
826
788
  var GRANULARITIES = ["day", "week", "month"];
827
789
  function parseArgs(input) {
@@ -830,9 +792,8 @@ function parseArgs(input) {
830
792
  const value = token.trim().toLowerCase();
831
793
  if (value === "")
832
794
  continue;
833
- const view = value === "providers" ? "endpoints" : value;
834
- if (PANEL_VIEWS.includes(view))
835
- parsed.view = view;
795
+ if (PANEL_VIEWS.includes(value))
796
+ parsed.view = value;
836
797
  else if (RANGE_KEYS.includes(value))
837
798
  parsed.range = value;
838
799
  }
@@ -866,7 +827,7 @@ var DEFAULT_SETTINGS = {
866
827
  granularity: "day",
867
828
  turnsScope: "session",
868
829
  modelID: "",
869
- routerID: ""
830
+ providerID: ""
870
831
  };
871
832
  function sanitizeSettings(raw) {
872
833
  const settings = { ...DEFAULT_SETTINGS, ...raw ?? {} };
@@ -878,7 +839,7 @@ function sanitizeSettings(raw) {
878
839
  granularity: GRANULARITIES.includes(settings.granularity) ? settings.granularity : DEFAULT_SETTINGS.granularity,
879
840
  turnsScope: settings.turnsScope === "all" ? "all" : "session",
880
841
  modelID: typeof settings.modelID === "string" ? settings.modelID : "",
881
- routerID: typeof settings.routerID === "string" ? settings.routerID : ""
842
+ providerID: typeof settings.providerID === "string" ? settings.providerID : ""
882
843
  };
883
844
  }
884
845
  function formatPercent(value, digits = 1) {
@@ -1030,23 +991,23 @@ function viewBar(view, width = 200) {
1030
991
  if (text.length <= width)
1031
992
  return { text, tone: "muted" };
1032
993
  }
1033
- return { text: `[${String((PANEL_VIEWS.indexOf(view) + 1) % 10)} ${view}] \xB7 0-9 or [ ] to switch`, tone: "muted" };
994
+ return { text: `[${PANEL_VIEWS.indexOf(view) + 1} ${view}] \xB7 1-9 or [ ] to switch`, tone: "muted" };
1034
995
  }
1035
996
  function keysFor(of) {
1036
997
  if (of === "db")
1037
- return "\u2191\u2193 pick a retention \xB7 enter prune (asks first) \xB7 v vacuum \xB7 r refresh \xB7 0-9 view \xB7 ? help";
998
+ return "\u2191\u2193 pick a retention \xB7 enter prune (asks first) \xB7 v vacuum \xB7 r refresh \xB7 1-9 view \xB7 ? help";
1038
999
  if (of === "periods")
1039
- return "\u2191\u2193 move \xB7 enter drill \xB7 b bucket \xB7 t range \xB7 m/e filter \xB7 x clear \xB7 0-9 view \xB7 ? help";
1000
+ return "\u2191\u2193 move \xB7 enter drill \xB7 b bucket \xB7 t range \xB7 m/p filter \xB7 x clear \xB7 1-9 view \xB7 ? help";
1040
1001
  if (of === "turns")
1041
- return "\u2191\u2193 move \xB7 a scope \xB7 t range \xB7 1-9 view \xB7 ? help";
1042
- return "\u2191\u2193 move \xB7 enter drill \xB7 s sort \xB7 t range \xB7 m/e filter \xB7 x clear \xB7 1-9 view \xB7 ? help";
1002
+ return "\u2191\u2193 move \xB7 a scope \xB7 t range \xB7 m/p filter \xB7 x clear \xB7 1-9 view \xB7 ? help";
1003
+ return "\u2191\u2193 move \xB7 enter drill \xB7 s sort \xB7 t range \xB7 m/p filter \xB7 x clear \xB7 1-9 view \xB7 ? help";
1043
1004
  }
1044
1005
  function filterSummary(filters) {
1045
1006
  const parts = [];
1046
1007
  if (filters.modelID !== undefined && filters.modelID !== "")
1047
1008
  parts.push(`model=${filters.modelID}`);
1048
- if (filters.routerID !== undefined && filters.routerID !== "")
1049
- parts.push(`endpoint=${filters.routerID}`);
1009
+ if (filters.providerID !== undefined && filters.providerID !== "")
1010
+ parts.push(`provider=${filters.providerID}`);
1050
1011
  return parts.length > 0 ? parts.join(" \xB7 ") : undefined;
1051
1012
  }
1052
1013
  function granularityLabel(granularity) {
@@ -1056,12 +1017,12 @@ function helpLines(width) {
1056
1017
  const lines = [
1057
1018
  { text: "keys", tone: HEADER_TONE },
1058
1019
  { text: " \u2191 \u2193 j k move the cursor \xB7 enter drills in \xB7 backspace goes back" },
1059
- { text: " 0-9 [ ] choose a view \xB7 cycle views (0 is db)" },
1020
+ { text: " 1-9 [ ] choose a view \xB7 cycle views (9 is db)" },
1060
1021
  { text: " t shift+t previous / next range" },
1061
1022
  { text: " s shift+s cycle sort column \xB7 reverse" },
1062
1023
  { text: " b periods bucket: day \u2192 week \u2192 month" },
1063
1024
  { text: " a turns scope: this session \u2194 all sessions" },
1064
- { text: " m e filter by model / endpoint, who served it (x clears)" },
1025
+ { text: " m p filter by model / provider (x clears)" },
1065
1026
  { text: " r re-read the database \xB7 f full screen \xB7 escape close" },
1066
1027
  { text: "" },
1067
1028
  { text: "numbers \xB7 the same buckets as the live line", tone: HEADER_TONE },
@@ -1073,18 +1034,18 @@ function helpLines(width) {
1073
1034
  { text: " hit p50 median of the per-step hit: spots a split pattern" },
1074
1035
  { text: " out everything generated: visible output + reasoning" },
1075
1036
  { text: " reasoning part of `out`" },
1037
+ { text: " cost money, not tokens: a turn sums every step; $0 is left out" },
1076
1038
  { text: " wall request \u2192 completed step \xB7 stream request \u2192 last chunk" },
1077
1039
  { text: " tps out / streaming seconds, shown as p50" },
1078
- { text: " changed the step's model or endpoint differs from the step" },
1079
- { text: " before it in the same session (affinity view)" },
1040
+ { text: " switches how often a step's model or provider differs from the step" },
1041
+ { text: " before it in the same session (the models view's detail), with" },
1042
+ { text: " the cache a provider switch cost" },
1080
1043
  { text: "" },
1081
- { text: "evidence", tone: HEADER_TONE },
1082
- { text: " endpoint where the request was actually sent: the endpoint that ran" },
1083
- { text: " the inference in practice, and the only identity the panel" },
1084
- { text: " shows. The endpoint is the inference provider." },
1085
- { text: " recorded the OpenCode provider is kept in the database as evidence," },
1086
- { text: " not surfaced: it says where a request came from, not who" },
1087
- { text: " ran it" },
1044
+ { text: "identity", tone: HEADER_TONE },
1045
+ { text: " provider the provider the model ran through, as OpenCode is configured" },
1046
+ { text: " (opencode, opencode-go, deepseek, \u2026). A gateway may re-route" },
1047
+ { text: " a request, but the upstream that ran the inference is not" },
1048
+ { text: " observable, so the provider is the identity every view shows." },
1088
1049
  { text: "" },
1089
1050
  { text: "database", tone: HEADER_TONE },
1090
1051
  { text: " the db view shows where the file lives, what it holds and how much" },
@@ -1158,7 +1119,7 @@ function coveragePairs(coverage) {
1158
1119
  const share = (missing) => coverage.steps === 0 ? "\u2013" : `${(missing / coverage.steps * 100).toFixed(1)}%`;
1159
1120
  return [
1160
1121
  ["models", `${formatCount(coverage.models)} \xB7 unknown ${share(coverage.missingModel)}`],
1161
- ["endpoints", `${formatCount(coverage.endpoints)} \xB7 unknown ${share(coverage.missingEndpoint)}`],
1122
+ ["providers", formatCount(coverage.providers)],
1162
1123
  ["unsettled", `${formatCount(coverage.unsettled)} \xB7 no tps ${share(coverage.missingTps)}`],
1163
1124
  [
1164
1125
  "window",
@@ -1202,16 +1163,16 @@ function modelsLines(rows, width, extras, limit = 40) {
1202
1163
  if (columns.spread > 0) {
1203
1164
  widths.push(columns.spread);
1204
1165
  aligns.push("left");
1205
- header.push("served by");
1166
+ header.push("providers");
1206
1167
  }
1207
1168
  const lines = [{ text: formatTableRow(header, widths, aligns), tone: HEADER_TONE }];
1208
1169
  for (const model of rows.slice(0, limit)) {
1209
1170
  const key = modelKey(model.modelID, model.variant);
1210
1171
  const name = modelCell(model.modelID, model.variant, columns.model);
1211
1172
  const buckets = usageBuckets(model.totals);
1212
- const endpoints = unique(model.providers.flatMap((provider) => provider.routers));
1173
+ const providers = unique(model.providers.map((provider) => provider.providerID));
1213
1174
  const cache = extras?.cache?.get(key);
1214
- const changes = extras?.changes?.get(key);
1175
+ const switches = extras?.switches?.get(key);
1215
1176
  const cells = [name, formatCount(buckets.steps), formatTokens(buckets.prompt), formatTokens(buckets.cached)];
1216
1177
  if (columns.hit > 0)
1217
1178
  cells.push(formatPercent(cache?.hit ?? buckets.hit));
@@ -1219,7 +1180,7 @@ function modelsLines(rows, width, extras, limit = 40) {
1219
1180
  if (columns.cost > 0)
1220
1181
  cells.push(formatCost(buckets.cost));
1221
1182
  if (columns.spread > 0)
1222
- cells.push(endpoints.length > 0 ? endpoints.join(", ") : "\u2013");
1183
+ cells.push(providers.join(", "));
1223
1184
  lines.push({
1224
1185
  text: formatTableRow(cells, widths, aligns),
1225
1186
  tone: "base",
@@ -1228,10 +1189,12 @@ function modelsLines(rows, width, extras, limit = 40) {
1228
1189
  const details = [];
1229
1190
  if (cache !== undefined)
1230
1191
  details.push(`p50 hit ${formatPercent(cache.hitP50)}`);
1231
- if (changes !== undefined && changes > 0)
1232
- details.push(count(changes, "identity change"));
1233
- if (endpoints.length === 0)
1234
- details.push("no routing endpoint observed");
1192
+ if (switches !== undefined && switches.changes > 0) {
1193
+ const scope = switches.providerChanges > 0 ? ` (${formatCount(switches.providerChanges)} provider)` : "";
1194
+ details.push(`${count(switches.changes, "switch", "switches")}${scope}`);
1195
+ if (switches.cacheLost > 0)
1196
+ details.push(`${formatTokens(switches.cacheLost)} lost on a switch`);
1197
+ }
1235
1198
  if (details.length > 0) {
1236
1199
  lines.push({ text: truncate(`${" ".repeat(columns.model + 1)}${details.join(" \xB7 ")}`, width), tone: "muted" });
1237
1200
  }
@@ -1276,9 +1239,9 @@ function renderTable(rows, columns, width, options) {
1276
1239
  lines.push({ text: `\u2026 ${count(rows.length - options.limit, "more row")}`, tone: "muted" });
1277
1240
  return lines;
1278
1241
  }
1279
- function endpointSummaryLines(rows, width, limit = 40) {
1242
+ function providerSummaryLines(rows, width, limit = 40) {
1280
1243
  const lines = renderTable(rows, [
1281
- { header: "endpoint", width: 24, cell: (row) => truncate(row.endpoint ?? "\u2013 (never observed)", 24) },
1244
+ { header: "provider", width: 24, cell: (row) => truncate(row.provider ?? "\u2013 (unknown)", 24) },
1282
1245
  { header: "models", width: 13, align: "right", optional: true, cell: (row) => formatCount(row.models.length) },
1283
1246
  { header: "steps", width: 6, align: "right", cell: (row) => formatCount(row.usage.steps) },
1284
1247
  { header: "share", width: 6, align: "right", optional: true, cell: (row) => formatPercent(row.share, 0) },
@@ -1290,16 +1253,16 @@ function endpointSummaryLines(rows, width, limit = 40) {
1290
1253
  { header: "cost", width: 9, align: "right", optional: true, cell: (row) => formatCost(row.usage.cost) }
1291
1254
  ], width, {
1292
1255
  limit,
1293
- empty: "no routing endpoint was observed in this range",
1294
- row: (row) => row.endpoint === undefined ? undefined : { kind: "endpoint", value: row.endpoint },
1295
- tone: (row) => row.endpoint === undefined ? "muted" : "base"
1256
+ empty: "no usage recorded for this range",
1257
+ row: (row) => row.provider === undefined ? undefined : { kind: "provider", value: row.provider },
1258
+ tone: (row) => row.provider === undefined ? "muted" : "base"
1296
1259
  });
1297
1260
  return lines;
1298
1261
  }
1299
- function endpointsLines(rows, width, limit = 40) {
1262
+ function providerLines(rows, width, limit = 40) {
1300
1263
  return renderTable(rows, [
1301
1264
  { header: "model", width: 26, cell: (row) => modelCell(row.modelID, row.variant, 26) },
1302
- { header: "endpoint", width: 22, cell: (row) => truncate(row.endpoint ?? "\u2013 (never observed)", 22) },
1265
+ { header: "provider", width: 22, cell: (row) => truncate(row.provider ?? "\u2013 (unknown)", 22) },
1303
1266
  { header: "steps", width: 6, align: "right", cell: (row) => formatCount(row.usage.steps) },
1304
1267
  { header: "share", width: 6, align: "right", optional: true, cell: (row) => formatPercent(row.share, 0) },
1305
1268
  { header: "prompt", width: 10, align: "right", cell: (row) => formatTokens(row.cache.prompt) },
@@ -1312,57 +1275,9 @@ function endpointsLines(rows, width, limit = 40) {
1312
1275
  limit,
1313
1276
  empty: "no usage recorded for this range",
1314
1277
  row: (row) => ({ kind: "model", value: row.modelID, ...row.variant !== undefined ? { variant: row.variant } : {} }),
1315
- tone: (row) => row.endpoint === undefined ? "muted" : "base"
1278
+ tone: (row) => row.provider === undefined ? "muted" : "base"
1316
1279
  });
1317
1280
  }
1318
- function affinityLines(report, width, limit = 40) {
1319
- const lines = [];
1320
- const rate = report.totals.pairs === 0 ? undefined : report.totals.changes / report.totals.pairs;
1321
- lines.push({
1322
- text: `${count(report.totals.pairs, "comparable step pair")} \xB7 ${count(report.totals.changes, "identity change")} (${formatPercent(rate)}) \xB7 cache lost on change ${formatTokens(report.totals.cacheLost)}`,
1323
- tone: report.totals.changes > 0 ? "warning" : "muted"
1324
- });
1325
- lines.push({ text: "a change is the step's model or endpoint differing from the step before it", tone: "muted" });
1326
- lines.push({ text: "", tone: "muted" });
1327
- const widths = tableWidths(width, [
1328
- [28, 7, 8, 9, 9, 11, 9],
1329
- [18, 6, 7, 8, 8, 9, 8]
1330
- ]);
1331
- const header = ["model", "steps", "changes", "model", "endpoint", "cache lost", "avg"];
1332
- lines.push({ text: formatTableRow(header, widths, widths.map((_, index) => index === 0 ? "left" : "right")), tone: HEADER_TONE });
1333
- if (report.rows.length === 0) {
1334
- lines.push({ text: "no comparable consecutive steps in this range", tone: "muted" });
1335
- }
1336
- for (const row of report.rows.slice(0, limit)) {
1337
- const perChange = row.cacheLostEvents > 0 ? row.cacheLost / row.cacheLostEvents : undefined;
1338
- lines.push({
1339
- text: formatTableRow([
1340
- modelCell(row.modelID, row.variant, widths[0]),
1341
- formatCount(row.steps),
1342
- formatCount(row.changes),
1343
- formatCount(row.modelChanges),
1344
- formatCount(row.endpointChanges),
1345
- row.cacheLost > 0 ? `\u2212${formatTokens(row.cacheLost)}` : "\u2013",
1346
- perChange === undefined ? "\u2013" : `\u2212${formatTokens(perChange)}`
1347
- ], widths, widths.map((_, index) => index === 0 ? "left" : "right")),
1348
- tone: row.changes > 0 ? "warning" : "base",
1349
- row: { kind: "model", value: row.modelID, ...row.variant !== undefined ? { variant: row.variant } : {} }
1350
- });
1351
- }
1352
- if (report.rows.length > limit)
1353
- lines.push({ text: `\u2026 ${count(report.rows.length - limit, "more model")}`, tone: "muted" });
1354
- if (report.transitions.length > 0) {
1355
- lines.push({ text: "", tone: "muted" });
1356
- lines.push({ text: "top transitions", tone: HEADER_TONE });
1357
- for (const transition of report.transitions.slice(0, 8)) {
1358
- lines.push({
1359
- text: truncate(` ${padCell(transition.dimension, 9)}${transition.from} \u2192 ${transition.to} \xD7${formatCount(transition.count)}`, width),
1360
- tone: "muted"
1361
- });
1362
- }
1363
- }
1364
- return lines;
1365
- }
1366
1281
  function cacheLines(data, width) {
1367
1282
  const lines = [];
1368
1283
  const total = data.drops.reduce((sum, drop) => sum + drop.tokens, 0);
@@ -1403,14 +1318,14 @@ function cacheLines(data, width) {
1403
1318
  tone: bucket.drops > 0 ? "warning" : "base"
1404
1319
  });
1405
1320
  }
1406
- if (data.gap.endpoints.length > 1) {
1321
+ if (data.gap.providers.length > 1) {
1407
1322
  lines.push({ text: "", tone: "muted" });
1408
- lines.push({ text: "reuse kept, by endpoint", tone: HEADER_TONE });
1409
- const endpointWidths = [26, 8, 9, 9];
1410
- lines.push({ text: formatTableRow(["endpoint", "pairs", "kept", "lost"], endpointWidths, ["left", "right", "right", "right"]), tone: "muted" });
1411
- for (const row of data.gap.endpoints.slice(0, 8)) {
1323
+ lines.push({ text: "reuse kept, by provider", tone: HEADER_TONE });
1324
+ const providerWidths = [26, 8, 9, 9];
1325
+ lines.push({ text: formatTableRow(["provider", "pairs", "kept", "lost"], providerWidths, ["left", "right", "right", "right"]), tone: "muted" });
1326
+ for (const row of data.gap.providers.slice(0, 8)) {
1412
1327
  lines.push({
1413
- text: formatTableRow([row.endpoint, formatCount(row.samples), formatPercent(row.retainedRatio, 0), formatTokens(row.lostTokens)], endpointWidths, ["left", "right", "right", "right"]),
1328
+ text: formatTableRow([row.provider, formatCount(row.samples), formatPercent(row.retainedRatio, 0), formatTokens(row.lostTokens)], providerWidths, ["left", "right", "right", "right"]),
1414
1329
  tone: "base"
1415
1330
  });
1416
1331
  }
@@ -1633,9 +1548,6 @@ function turnStats(turn) {
1633
1548
  providerID: turn.providerID,
1634
1549
  modelID: turn.modelID,
1635
1550
  variant: turn.variant,
1636
- router: turn.router,
1637
- identityChanged: turn.routerChanged,
1638
- affinityKey: undefined,
1639
1551
  providerChanged: turn.providerChanged,
1640
1552
  modelChanged: turn.modelChanged,
1641
1553
  totals: turn.totals,
@@ -1644,8 +1556,7 @@ function turnStats(turn) {
1644
1556
  drops: turn.drops.map((drop) => ({ messageID: drop.messageID, tokens: drop.tokens, prevCached: 0, gapMs: drop.gapMs })),
1645
1557
  cacheDropTokens: turn.drops.reduce((sum, drop) => sum + drop.tokens, 0),
1646
1558
  cacheExpired: turn.drops.length > 0,
1647
- steps: turn.steps,
1648
- stepIDs: []
1559
+ steps: turn.steps
1649
1560
  };
1650
1561
  }
1651
1562
  function turnsLines(rows, width, options) {
@@ -1664,7 +1575,7 @@ function turnsLines(rows, width, options) {
1664
1575
  const title = showSession ? `${truncate(options.sessionTitles.get(turn.sessionID) ?? turn.sessionID, 18)} ` : "";
1665
1576
  lines.push({
1666
1577
  text: truncate(`${title}${line}`, width),
1667
- tone: turn.drops.length > 0 || turn.routerChanged ? "warning" : "base",
1578
+ tone: turn.drops.length > 0 || turn.providerChanged ? "warning" : "base",
1668
1579
  row: { kind: "turn", value: turn.sessionID }
1669
1580
  });
1670
1581
  }
@@ -1706,9 +1617,9 @@ function sidebarLines(rows, options) {
1706
1617
  const budget = options?.budget ?? 34;
1707
1618
  const labels = { input: options?.labels?.input ?? "in", output: options?.labels?.output ?? "out" };
1708
1619
  const lines = [];
1709
- for (const turn of rows) {
1620
+ for (const [index, turn] of rows.entries()) {
1710
1621
  const clock = turn.endAt !== undefined ? formatClock(turn.endAt).slice(0, 5) : "--:--";
1711
- const identity = turn.router ?? (turn.providerID !== undefined && turn.modelID !== undefined ? `${turn.providerID}/${turn.modelID}` : "unknown");
1622
+ const provider = turn.providerID ?? "unknown";
1712
1623
  const display = displayTotals(turn.latest);
1713
1624
  const input = formatTokens(display.input);
1714
1625
  const prompt = formatTokens(display.prompt);
@@ -1728,7 +1639,12 @@ function sidebarLines(rows, options) {
1728
1639
  ` ${tps === undefined ? "" : `${tps} \xB7 `}${cost}${suffix}`,
1729
1640
  ` hit ${hit}${suffix}`
1730
1641
  ];
1731
- lines.push({ text: truncate(`${clock} ${identity}`, budget), tone: turn.routerChanged ? "warning" : "base" });
1642
+ lines.push({ text: truncate(`${clock} ${provider}`, budget), tone: turn.providerChanged ? "warning" : "base" });
1643
+ const edge = index === 0 || index === rows.length - 1;
1644
+ if (edge || turn.modelChanged || turn.providerChanged) {
1645
+ const model = turn.modelID === undefined ? "unknown" : `${turn.modelID}${turn.variant === undefined ? "" : ` ${turn.variant}`}`;
1646
+ lines.push({ text: truncate(` ${model}`, budget), tone: "muted" });
1647
+ }
1732
1648
  lines.push({
1733
1649
  text: truncate(volumes.find((text) => text.length <= budget) ?? volumes[volumes.length - 1], budget),
1734
1650
  tone: "muted"
@@ -1777,11 +1693,11 @@ function groupBy(items, key) {
1777
1693
  return groups;
1778
1694
  }
1779
1695
  var STEP_COLUMNS = `message_id AS messageID, session_id AS sessionID, provider_id AS providerID,
1780
- model_id AS modelID, variant, router,
1696
+ model_id AS modelID, variant,
1781
1697
  COALESCE(input_tokens, 0) AS input, COALESCE(cache_read, 0) AS cached, COALESCE(cache_write, 0) AS cacheWrite,
1782
1698
  COALESCE(output_tokens, 0) AS output, COALESCE(reasoning_tokens, 0) AS reasoning, COALESCE(cost, 0) AS cost,
1783
1699
  created_at AS createdAt, streamed_at AS streamedAt, completed_at AS completedAt, ${STEP_AT} AS at`;
1784
- var STEP_OPTIONAL = ["providerID", "modelID", "variant", "router", "createdAt", "streamedAt", "completedAt"];
1700
+ var STEP_OPTIONAL = ["providerID", "modelID", "variant", "createdAt", "streamedAt", "completedAt"];
1785
1701
  var PAIR_COLUMNS = STEP_COLUMNS.replace("COALESCE(output_tokens, 0) AS output, COALESCE(reasoning_tokens, 0) AS reasoning, ", "");
1786
1702
  function stepRows(db, options) {
1787
1703
  const filter = buildWhere(options?.range, options?.filters);
@@ -1855,9 +1771,8 @@ function coverageOf(db, options) {
1855
1771
  const filter = buildWhere(options?.range, options?.filters);
1856
1772
  const row = db.query(`SELECT COUNT(*) AS steps,
1857
1773
  COUNT(DISTINCT CASE WHEN model_id IS NOT NULL THEN model_id || '#' || COALESCE(variant, '') END) AS models,
1858
- COUNT(DISTINCT router) AS endpoints,
1774
+ COUNT(DISTINCT provider_id) AS providers,
1859
1775
  COALESCE(SUM(CASE WHEN model_id IS NULL THEN 1 ELSE 0 END), 0) AS missingModel,
1860
- COALESCE(SUM(CASE WHEN router IS NULL THEN 1 ELSE 0 END), 0) AS missingEndpoint,
1861
1776
  COALESCE(SUM(CASE WHEN completed_at IS NOT NULL OR finish IS NOT NULL THEN 0 ELSE 1 END), 0) AS unsettled,
1862
1777
  COALESCE(SUM(CASE WHEN (output_tokens + reasoning_tokens) > 0 AND created_at IS NOT NULL AND streamed_at IS NOT NULL AND streamed_at > created_at THEN 0 ELSE 1 END), 0) AS missingTps,
1863
1778
  MIN(${STEP_AT}) AS firstAt,
@@ -1866,9 +1781,8 @@ function coverageOf(db, options) {
1866
1781
  return {
1867
1782
  steps: row.steps,
1868
1783
  models: row.models,
1869
- endpoints: row.endpoints,
1784
+ providers: row.providers,
1870
1785
  missingModel: row.missingModel,
1871
- missingEndpoint: row.missingEndpoint,
1872
1786
  unsettled: row.unsettled,
1873
1787
  missingTps: row.missingTps,
1874
1788
  firstAt: row.firstAt ?? undefined,
@@ -1904,7 +1818,7 @@ function latencyOf(rows) {
1904
1818
  return { wall: latencyStats(wall), stream: latencyStats(stream), tps: latencyStats(tps) };
1905
1819
  }
1906
1820
  function latencyGroupsOf(rows, by) {
1907
- const key = (step) => by === "model" ? modelKey(step.modelID ?? "(unknown)", step.variant) : by === "provider" ? step.providerID ?? "(unknown)" : step.router ?? "(unknown)";
1821
+ const key = (step) => by === "model" ? modelKey(step.modelID ?? "(unknown)", step.variant) : step.providerID ?? "(unknown)";
1908
1822
  const groups = [];
1909
1823
  for (const [group, steps] of groupBy(rows, key)) {
1910
1824
  const wall = [];
@@ -1942,9 +1856,9 @@ function cacheStatsOfSteps(steps) {
1942
1856
  input += step.input;
1943
1857
  cached += step.cached;
1944
1858
  cacheWrite += step.cacheWrite;
1945
- const prompt2 = step.input + step.cached + step.cacheWrite;
1946
- if (prompt2 > 0)
1947
- ratios.push(step.cached / prompt2);
1859
+ const prompt = step.input + step.cached + step.cacheWrite;
1860
+ if (prompt > 0)
1861
+ ratios.push(step.cached / prompt);
1948
1862
  }
1949
1863
  const prompt = input + cached + cacheWrite;
1950
1864
  return { steps: steps.length, input, cached, cacheWrite, prompt, hit: prompt > 0 ? cached / prompt : undefined, hitP50: percentile(ratios, 50) };
@@ -1961,17 +1875,8 @@ function cacheStatsByModel(rows) {
1961
1875
  }
1962
1876
  return out.sort((left, right) => right.cache.cached - left.cache.cached || left.modelID.localeCompare(right.modelID));
1963
1877
  }
1964
- function noteTransition(map, dimension, from, to) {
1965
- const key = `${dimension}\x00${from}\x00${to}`;
1966
- const entry = map.get(key);
1967
- if (entry !== undefined)
1968
- entry.count += 1;
1969
- else
1970
- map.set(key, { dimension, from, to, count: 1 });
1971
- }
1972
- function affinityOf(rows) {
1878
+ function switchesOf(rows) {
1973
1879
  const perModel = new Map;
1974
- const transitions = new Map;
1975
1880
  const totals = { pairs: 0, changes: 0, cacheLost: 0, cacheLostEvents: 0 };
1976
1881
  for (const { previous, current } of pairsOf(rows)) {
1977
1882
  if (previous.modelID === undefined || current.modelID === undefined)
@@ -1984,7 +1889,7 @@ function affinityOf(rows) {
1984
1889
  pairs: 0,
1985
1890
  changes: 0,
1986
1891
  modelChanges: 0,
1987
- endpointChanges: 0,
1892
+ providerChanges: 0,
1988
1893
  cacheLost: 0,
1989
1894
  cacheLostEvents: 0
1990
1895
  };
@@ -1992,19 +1897,15 @@ function affinityOf(rows) {
1992
1897
  row.pairs += 1;
1993
1898
  totals.pairs += 1;
1994
1899
  const modelChanged = key !== modelKey(previous.modelID, previous.variant);
1995
- const endpointChanged = previous.router !== undefined && current.router !== undefined && previous.router !== current.router;
1996
- if (modelChanged) {
1900
+ const providerChanged = previous.providerID !== undefined && current.providerID !== undefined && previous.providerID !== current.providerID;
1901
+ if (modelChanged)
1997
1902
  row.modelChanges += 1;
1998
- noteTransition(transitions, "model", modelLabel(previous.modelID, previous.variant), modelLabel(current.modelID, current.variant));
1999
- }
2000
- if (endpointChanged) {
2001
- row.endpointChanges += 1;
2002
- noteTransition(transitions, "endpoint", previous.router, current.router);
2003
- }
2004
- if (modelChanged || endpointChanged) {
1903
+ if (providerChanged)
1904
+ row.providerChanges += 1;
1905
+ if (modelChanged || providerChanged) {
2005
1906
  row.changes += 1;
2006
1907
  totals.changes += 1;
2007
- const lost = cacheDrop(message(previous), message(current));
1908
+ const lost = cacheDrop(message(previous), message(current), "model");
2008
1909
  if (lost !== undefined) {
2009
1910
  row.cacheLost += lost;
2010
1911
  row.cacheLostEvents += 1;
@@ -2016,7 +1917,6 @@ function affinityOf(rows) {
2016
1917
  }
2017
1918
  return {
2018
1919
  rows: [...perModel.values()].sort((left, right) => right.changes - left.changes || right.steps - left.steps || left.modelID.localeCompare(right.modelID)),
2019
- transitions: [...transitions.values()].sort((left, right) => right.count - left.count || left.dimension.localeCompare(right.dimension)),
2020
1920
  totals
2021
1921
  };
2022
1922
  }
@@ -2034,13 +1934,15 @@ function bucketFor(gapMs) {
2034
1934
  }
2035
1935
  function gapReportOf(rows) {
2036
1936
  const buckets = GAP_BUCKETS.map(([label]) => ({ label, samples: 0, retained: 0, drops: 0, lostTokens: 0, retainedRatio: undefined }));
2037
- const endpoints = new Map;
1937
+ const providers = new Map;
2038
1938
  let samples = 0;
2039
1939
  for (const { previous, current } of pairsOf(rows)) {
2040
1940
  if (previous.modelID === undefined || current.modelID === undefined)
2041
1941
  continue;
2042
1942
  if (modelKey(previous.modelID, previous.variant) !== modelKey(current.modelID, current.variant))
2043
1943
  continue;
1944
+ if ((previous.providerID ?? "") !== (current.providerID ?? ""))
1945
+ continue;
2044
1946
  const gap = stepGap(message(previous), message(current));
2045
1947
  if (gap === undefined)
2046
1948
  continue;
@@ -2054,8 +1956,8 @@ function gapReportOf(rows) {
2054
1956
  bucket.drops += 1;
2055
1957
  bucket.lostTokens += lost;
2056
1958
  }
2057
- const endpoint = current.router ?? previous.router ?? "(unknown)";
2058
- const row = endpoints.get(endpoint) ?? { endpoint, samples: 0, retained: 0, drops: 0, lostTokens: 0, retainedRatio: undefined };
1959
+ const provider = current.providerID ?? previous.providerID ?? "(unknown)";
1960
+ const row = providers.get(provider) ?? { provider, samples: 0, retained: 0, drops: 0, lostTokens: 0, retainedRatio: undefined };
2059
1961
  row.samples += 1;
2060
1962
  if (lost === undefined)
2061
1963
  row.retained += 1;
@@ -2063,15 +1965,15 @@ function gapReportOf(rows) {
2063
1965
  row.drops += 1;
2064
1966
  row.lostTokens += lost;
2065
1967
  }
2066
- endpoints.set(endpoint, row);
1968
+ providers.set(provider, row);
2067
1969
  }
2068
1970
  for (const bucket of buckets)
2069
1971
  bucket.retainedRatio = bucket.samples > 0 ? bucket.retained / bucket.samples : undefined;
2070
- for (const row of endpoints.values())
1972
+ for (const row of providers.values())
2071
1973
  row.retainedRatio = row.samples > 0 ? row.retained / row.samples : undefined;
2072
1974
  return {
2073
1975
  buckets,
2074
- endpoints: [...endpoints.values()].sort((left, right) => right.samples - left.samples || left.endpoint.localeCompare(right.endpoint)),
1976
+ providers: [...providers.values()].sort((left, right) => right.samples - left.samples || left.provider.localeCompare(right.provider)),
2075
1977
  samples
2076
1978
  };
2077
1979
  }
@@ -2109,7 +2011,7 @@ function cacheDropsOf(db, rows, options) {
2109
2011
  const ttlHint = options?.ttlHintMs ?? DEFAULT_TTL_HINT_MS;
2110
2012
  const losses = [];
2111
2013
  for (const { previous, current } of pairsOf(rows)) {
2112
- const tokens = cacheDrop(message(previous), message(current));
2014
+ const tokens = cacheDrop(message(previous), message(current), "model");
2113
2015
  if (tokens === undefined)
2114
2016
  continue;
2115
2017
  losses.push({ previous, current, tokens, gapMs: stepGap(message(previous), message(current)) });
@@ -2146,7 +2048,7 @@ function cacheDropsOf(db, rows, options) {
2146
2048
  };
2147
2049
  }).sort((left, right) => left.at - right.at);
2148
2050
  }
2149
- function endpointValues(steps) {
2051
+ function groupValues(steps) {
2150
2052
  const wall = [];
2151
2053
  const tps = [];
2152
2054
  for (const step of steps) {
@@ -2158,12 +2060,12 @@ function endpointValues(steps) {
2158
2060
  }
2159
2061
  return { cache: cacheStatsOfSteps(steps), tps: latencyStats(tps).p50, wall: latencyStats(wall).p50 };
2160
2062
  }
2161
- function endpointOf(steps, models2) {
2162
- const values = endpointValues(steps);
2063
+ function providerOf(steps, models) {
2064
+ const values = groupValues(steps);
2163
2065
  const times = steps.map((step) => step.at);
2164
2066
  return {
2165
- endpoint: steps[0].router,
2166
- models: models2.split("\x00"),
2067
+ provider: steps[0].providerID,
2068
+ models: models.split("\x00"),
2167
2069
  share: 0,
2168
2070
  usage: usageOf(steps),
2169
2071
  cache: values.cache,
@@ -2174,21 +2076,21 @@ function endpointOf(steps, models2) {
2174
2076
  };
2175
2077
  }
2176
2078
  var modelNames = (steps) => [...new Set(steps.map((step) => modelKey(step.modelID ?? "(unknown)", step.variant)))].map((key) => modelLabel(key.slice(0, key.indexOf("#")), key.slice(key.indexOf("#") + 1) || undefined)).join("\x00");
2177
- function endpointSummaryOf(rows) {
2079
+ function providerSummaryOf(rows) {
2178
2080
  const out = [];
2179
- for (const steps of groupBy(rows, (step) => step.router ?? "").values()) {
2180
- const summary = endpointOf(steps, modelNames(steps));
2081
+ for (const steps of groupBy(rows, (step) => step.providerID ?? "").values()) {
2082
+ const summary = providerOf(steps, modelNames(steps));
2181
2083
  out.push({ ...summary, share: rows.length === 0 ? 0 : steps.length / rows.length });
2182
2084
  }
2183
- return out.sort((left, right) => right.usage.steps - left.usage.steps || (left.endpoint ?? "").localeCompare(right.endpoint ?? ""));
2085
+ return out.sort((left, right) => right.usage.steps - left.usage.steps || (left.provider ?? "").localeCompare(right.provider ?? ""));
2184
2086
  }
2185
- function endpointBreakdownOf(rows) {
2087
+ function providerBreakdownOf(rows) {
2186
2088
  const out = [];
2187
2089
  for (const [model, modelRows] of groupBy(rows, (step) => modelKey(step.modelID ?? "(unknown)", step.variant))) {
2188
- for (const steps of groupBy(modelRows, (step) => step.router ?? "").values()) {
2090
+ for (const steps of groupBy(modelRows, (step) => step.providerID ?? "").values()) {
2189
2091
  const [modelID, variant] = [model.slice(0, model.indexOf("#")), model.slice(model.indexOf("#") + 1)];
2190
2092
  out.push({
2191
- ...endpointOf(steps, modelNames(steps)),
2093
+ ...providerOf(steps, modelNames(steps)),
2192
2094
  modelID,
2193
2095
  variant: variant === "" ? undefined : variant,
2194
2096
  share: steps.length / modelRows.length
@@ -2210,8 +2112,8 @@ function periodStart(at, granularity) {
2210
2112
  const date = new Date(at);
2211
2113
  const pad = (value) => String(value).padStart(2, "0");
2212
2114
  if (granularity === "month") {
2213
- const start2 = new Date(date.getFullYear(), date.getMonth(), 1);
2214
- return { key: `${start2.getFullYear()}-${pad(start2.getMonth() + 1)}`, label: `${start2.getFullYear()}-${pad(start2.getMonth() + 1)}`, start: start2.getTime() };
2115
+ const start = new Date(date.getFullYear(), date.getMonth(), 1);
2116
+ return { key: `${start.getFullYear()}-${pad(start.getMonth() + 1)}`, label: `${start.getFullYear()}-${pad(start.getMonth() + 1)}`, start: start.getTime() };
2215
2117
  }
2216
2118
  if (granularity === "week") {
2217
2119
  const { year, week, monday } = isoWeek(date);
@@ -2241,8 +2143,7 @@ function periodsOf(rows, granularity) {
2241
2143
  // src/panel-build.ts
2242
2144
  var SORT_KEYS = {
2243
2145
  models: ["steps", "cached", "hit", "out", "cost"],
2244
- endpoints: ["steps", "cached", "hit", "cost", "share", "tps"],
2245
- affinity: ["changes", "lost", "steps"],
2146
+ providers: ["steps", "cached", "hit", "cost", "share", "tps"],
2246
2147
  latency: ["n", "tps", "wall"],
2247
2148
  sessions: ["last", "steps", "cached", "cost"],
2248
2149
  turns: ["time", "cost", "tokens"]
@@ -2271,8 +2172,6 @@ function detailLabel(row) {
2271
2172
  return `session ${row.value}`;
2272
2173
  case "period":
2273
2174
  return `period ${row.value}`;
2274
- case "endpoint":
2275
- return `endpoint ${row.value}`;
2276
2175
  case "provider":
2277
2176
  return `provider ${row.value}`;
2278
2177
  case "prune":
@@ -2283,8 +2182,8 @@ function buildPanel(request) {
2283
2182
  try {
2284
2183
  return build(request);
2285
2184
  } catch (error) {
2286
- const message2 = error instanceof Error ? error.message : String(error);
2287
- return [{ text: `stats query failed: ${message2}`, tone: "warning" }];
2185
+ const message = error instanceof Error ? error.message : String(error);
2186
+ return [{ text: `stats query failed: ${message}`, tone: "warning" }];
2288
2187
  }
2289
2188
  }
2290
2189
  function build(request) {
@@ -2312,16 +2211,14 @@ function build(request) {
2312
2211
  case "models": {
2313
2212
  const rows = sortModels(models(db, range, filters), request.sort, request.sortDesc);
2314
2213
  const pairs = cachePairs(db, { range, filters });
2315
- const affinity = affinityOf(pairs);
2214
+ const switches = switchesOf(pairs);
2316
2215
  return modelsLines(rows, width, {
2317
2216
  cache: cacheByModel(pairs),
2318
- changes: new Map(affinity.rows.map((row) => [modelKey(row.modelID, row.variant), row.changes]))
2217
+ switches: switchesByModel(switches.rows)
2319
2218
  }, 40);
2320
2219
  }
2321
- case "endpoints":
2322
- return endpointSummaryLines(sortEndpoints(endpointSummaryOf(stepRows(db, { range, filters })), request.sort, request.sortDesc), width, 40);
2323
- case "affinity":
2324
- return affinityLines(sortAffinity(affinityOf(cachePairs(db, { range, filters })), request.sort, request.sortDesc), width, 40);
2220
+ case "providers":
2221
+ return providerSummaryLines(sortProviders(providerSummaryOf(stepRows(db, { range, filters })), request.sort, request.sortDesc), width, 40);
2325
2222
  case "cache": {
2326
2223
  const pairs = cachePairs(db, { range, filters });
2327
2224
  return cacheLines({
@@ -2335,7 +2232,7 @@ function build(request) {
2335
2232
  return periodLines(periodsOf(stepRows(db, { range, filters }), request.granularity), width, request.granularity, 40);
2336
2233
  case "latency": {
2337
2234
  const timings = timingRows(db, { range, filters });
2338
- const by = filters.modelID !== undefined ? "endpoint" : "model";
2235
+ const by = filters.modelID !== undefined ? "provider" : "model";
2339
2236
  return latencyLines(latencyOf(timings), width, sortLatency(latencyGroupsOf(timings, by), request.sort, request.sortDesc), 14);
2340
2237
  }
2341
2238
  case "sessions": {
@@ -2380,6 +2277,7 @@ function buildDetail(request, range, filters) {
2380
2277
  if (row.kind === "model") {
2381
2278
  const scoped = { ...filters, modelKey: modelKey(row.value, row.variant) };
2382
2279
  const stats = cacheStatsOf(db, { range, filters: scoped });
2280
+ const steps = stepRows(db, { range, filters: scoped });
2383
2281
  return [
2384
2282
  head,
2385
2283
  {
@@ -2387,9 +2285,9 @@ function buildDetail(request, range, filters) {
2387
2285
  tone: "muted"
2388
2286
  },
2389
2287
  { text: "", tone: "muted" },
2390
- ...endpointsLines(endpointBreakdownOf(stepRows(db, { range, filters: scoped })), width, 40),
2288
+ ...providerLines(providerBreakdownOf(steps), width, 40),
2391
2289
  { text: "", tone: "muted" },
2392
- ...affinityLines(affinityOf(cachePairs(db, { range, filters: scoped })), width, 10),
2290
+ ...latencyLines(latencyOf(steps), width, latencyGroupsOf(steps, "provider"), 14),
2393
2291
  { text: "", tone: "muted" },
2394
2292
  back
2395
2293
  ];
@@ -2398,8 +2296,8 @@ function buildDetail(request, range, filters) {
2398
2296
  const rows = turns(db, { sessionID: row.value, limit: 80 });
2399
2297
  return [head, { text: "", tone: "muted" }, ...turnsLines(rows, width, request.format), { text: "", tone: "muted" }, back];
2400
2298
  }
2401
- if (row.kind === "endpoint") {
2402
- const scoped = { ...filters, router: row.value };
2299
+ if (row.kind === "provider") {
2300
+ const scoped = { ...filters, provider: row.value };
2403
2301
  const steps = stepRows(db, { range, filters: scoped });
2404
2302
  const stats = cacheStatsOf(db, { range, filters: scoped });
2405
2303
  return [
@@ -2409,7 +2307,7 @@ function buildDetail(request, range, filters) {
2409
2307
  tone: "muted"
2410
2308
  },
2411
2309
  { text: "", tone: "muted" },
2412
- ...endpointsLines(endpointBreakdownOf(steps), width, 40),
2310
+ ...providerLines(providerBreakdownOf(steps), width, 40),
2413
2311
  { text: "", tone: "muted" },
2414
2312
  ...latencyLines(latencyOf(steps), width, latencyGroupsOf(steps, "model"), 14),
2415
2313
  { text: "", tone: "muted" },
@@ -2423,7 +2321,7 @@ function buildDetail(request, range, filters) {
2423
2321
  head,
2424
2322
  { text: `${formatCount(period?.steps ?? 0)} steps`, tone: "muted" },
2425
2323
  { text: "", tone: "muted" },
2426
- ...endpointSummaryLines(endpointSummaryOf(stepRows(db, { range, filters })), width, 40),
2324
+ ...providerSummaryLines(providerSummaryOf(stepRows(db, { range, filters })), width, 40),
2427
2325
  { text: "", tone: "muted" },
2428
2326
  back
2429
2327
  ];
@@ -2446,7 +2344,7 @@ function sortModels(rows, sort, desc) {
2446
2344
  };
2447
2345
  return [...rows].sort((left, right) => sign * (pick(right) - pick(left)));
2448
2346
  }
2449
- function sortEndpoints(rows, sort, desc) {
2347
+ function sortProviders(rows, sort, desc) {
2450
2348
  const sign = desc ? -1 : 1;
2451
2349
  const pick = (row) => {
2452
2350
  if (sort === "cached")
@@ -2463,17 +2361,6 @@ function sortEndpoints(rows, sort, desc) {
2463
2361
  };
2464
2362
  return [...rows].sort((left, right) => sign * (pick(right) - pick(left)));
2465
2363
  }
2466
- function sortAffinity(report, sort, desc) {
2467
- const sign = desc ? -1 : 1;
2468
- const pick = (row) => {
2469
- if (sort === "lost")
2470
- return row.cacheLost;
2471
- if (sort === "steps")
2472
- return row.steps;
2473
- return row.changes;
2474
- };
2475
- return { ...report, rows: [...report.rows].sort((left, right) => sign * (pick(right) - pick(left))) };
2476
- }
2477
2364
  function sortLatency(rows, sort, desc) {
2478
2365
  const sign = desc ? -1 : 1;
2479
2366
  const pick = (row) => {
@@ -2516,6 +2403,9 @@ function cacheByModel(pairs) {
2516
2403
  }
2517
2404
  return map;
2518
2405
  }
2406
+ function switchesByModel(rows) {
2407
+ return new Map(rows.map((row) => [modelKey(row.modelID, row.variant), row]));
2408
+ }
2519
2409
  function topCacheModels(pairs) {
2520
2410
  return cacheStatsByModel(pairs).slice(0, 10).map((row) => ({ usage: { modelID: row.modelID, variant: row.variant }, cache: row.cache }));
2521
2411
  }
@@ -2590,7 +2480,7 @@ function ensureRegularFile(path) {
2590
2480
  }
2591
2481
 
2592
2482
  // src/stats/db.ts
2593
- var SCHEMA_VERSION = 8;
2483
+ var SCHEMA_VERSION = 9;
2594
2484
  var LABEL_LIMIT = 4 * TEXT_LIMIT;
2595
2485
  var SCHEMA = `
2596
2486
  CREATE TABLE IF NOT EXISTS meta (
@@ -2632,13 +2522,11 @@ CREATE TABLE IF NOT EXISTS steps (
2632
2522
  streamed_at INTEGER,
2633
2523
  completed_at INTEGER,
2634
2524
  finish TEXT,
2635
- router TEXT,
2636
2525
  recorded_at INTEGER NOT NULL
2637
2526
  );
2638
2527
  CREATE INDEX IF NOT EXISTS steps_session ON steps(session_id, created_at);
2639
2528
  CREATE INDEX IF NOT EXISTS steps_model ON steps(provider_id, model_id, variant);
2640
2529
  CREATE INDEX IF NOT EXISTS steps_completed ON steps(completed_at);
2641
- CREATE INDEX IF NOT EXISTS steps_router ON steps(router);
2642
2530
  -- Every range filter and every ordering compares the step's own clock, which
2643
2531
  -- falls back to the recording time for a step that was already in flight when
2644
2532
  -- the plugin loaded. The index has to cover that expression, or every ranged
@@ -2669,10 +2557,7 @@ function jsonText(value) {
2669
2557
  return null;
2670
2558
  }
2671
2559
  }
2672
- var MIGRATIONS = [
2673
- ["router", "ALTER TABLE steps ADD COLUMN router TEXT"]
2674
- ];
2675
- var REMOVED_COLUMNS = ["provider_state", "upstream", "upstream_source", "upstream_model", "tps"];
2560
+ var REMOVED_COLUMNS = ["provider_state", "upstream", "upstream_source", "upstream_model", "tps", "router"];
2676
2561
  var REMOVED_EVENT_KINDS = ["execution", "step-failed", "model-selected", "usage-recorded"];
2677
2562
  function dropUnused(db) {
2678
2563
  const objects = db.query("SELECT name, type FROM sqlite_master WHERE type IN ('table', 'index')").all();
@@ -2682,6 +2567,8 @@ function dropUnused(db) {
2682
2567
  db.exec("DROP TABLE http_responses");
2683
2568
  if (indexes.has("steps_upstream"))
2684
2569
  db.exec("DROP INDEX steps_upstream");
2570
+ if (indexes.has("steps_router"))
2571
+ db.exec("DROP INDEX steps_router");
2685
2572
  const columns = new Set(db.query("PRAGMA table_info(steps)").all().map((column) => column.name));
2686
2573
  for (const name of REMOVED_COLUMNS) {
2687
2574
  if (columns.has(name))
@@ -2691,30 +2578,9 @@ function dropUnused(db) {
2691
2578
  const kinds = REMOVED_EVENT_KINDS.map((kind) => `'${kind}'`).join(", ");
2692
2579
  db.exec(`DELETE FROM events WHERE kind IN (${kinds})`);
2693
2580
  }
2694
- db.exec("UPDATE steps SET router = NULL WHERE router IN ('opencode', 'openrouter', 'vercel-ai-gateway')");
2695
- }
2696
- function schemaVersion(db) {
2697
- const row = db.query("SELECT value FROM meta WHERE key = 'schema_version'").get();
2698
- const parsed = row === null ? Number.NaN : Number.parseInt(row.value, 10);
2699
- return Number.isFinite(parsed) ? parsed : 0;
2700
- }
2701
- function normalizeRouters(db) {
2702
- db.exec("UPDATE steps SET router = lower(router) WHERE router IS NOT NULL AND router <> lower(router)");
2703
2581
  }
2704
2582
  function migrate(db) {
2705
- const version = schemaVersion(db);
2706
- const steps = db.query("PRAGMA table_info(steps)").all();
2707
- const existing = new Set(steps.map((column) => column.name));
2708
- for (const [name, statement, backfill] of MIGRATIONS) {
2709
- if (existing.has(name))
2710
- continue;
2711
- db.exec(statement);
2712
- if (backfill !== undefined)
2713
- db.exec(backfill);
2714
- }
2715
2583
  dropUnused(db);
2716
- if (version < 7)
2717
- normalizeRouters(db);
2718
2584
  }
2719
2585
  var POSIX = process.platform !== "win32";
2720
2586
  function hardenDirectory(path) {
@@ -2778,11 +2644,11 @@ function recordStep(db, record) {
2778
2644
  db.query(`INSERT INTO steps (
2779
2645
  message_id, session_id, parent_id, provider_id, model_id, variant,
2780
2646
  input_tokens, output_tokens, reasoning_tokens, cache_read, cache_write, cost,
2781
- created_at, streamed_at, completed_at, finish, router, recorded_at
2647
+ created_at, streamed_at, completed_at, finish, recorded_at
2782
2648
  ) VALUES (
2783
2649
  $messageID, $sessionID, $parentID, $providerID, $modelID, $variant,
2784
2650
  $input, $output, $reasoning, $cacheRead, $cacheWrite, $cost,
2785
- $createdAt, $streamedAt, $completedAt, $finish, $router, $recordedAt
2651
+ $createdAt, $streamedAt, $completedAt, $finish, $recordedAt
2786
2652
  )
2787
2653
  ON CONFLICT(message_id) DO UPDATE SET
2788
2654
  session_id = excluded.session_id,
@@ -2800,12 +2666,11 @@ function recordStep(db, record) {
2800
2666
  streamed_at = COALESCE(excluded.streamed_at, steps.streamed_at),
2801
2667
  completed_at = COALESCE(excluded.completed_at, steps.completed_at),
2802
2668
  finish = COALESCE(excluded.finish, steps.finish),
2803
- router = COALESCE(excluded.router, steps.router),
2804
2669
  recorded_at = excluded.recorded_at`).run({
2805
2670
  $messageID: record.messageID,
2806
2671
  $sessionID: record.sessionID,
2807
2672
  $parentID: record.parentID ?? null,
2808
- $providerID: record.providerID ?? null,
2673
+ $providerID: record.providerID !== undefined && isToken(record.providerID) ? record.providerID : null,
2809
2674
  $modelID: record.modelID ?? null,
2810
2675
  $variant: record.variant ?? null,
2811
2676
  $input: record.input ?? 0,
@@ -2818,7 +2683,6 @@ function recordStep(db, record) {
2818
2683
  $streamedAt: record.streamedAt ?? null,
2819
2684
  $completedAt: record.completedAt ?? null,
2820
2685
  $finish: record.finish === undefined ? null : sanitizeLabel(record.finish, 64),
2821
- $router: record.router === undefined ? null : normalizeToken(record.router) ?? null,
2822
2686
  $recordedAt: record.recordedAt ?? Date.now()
2823
2687
  });
2824
2688
  }
@@ -2858,15 +2722,6 @@ function upsertSession(db, record) {
2858
2722
  $updatedAt: record.updatedAt ?? null
2859
2723
  });
2860
2724
  }
2861
- function setStepRouterForSessionModel(db, sessionID, modelID, info) {
2862
- const router = info.router === undefined ? undefined : normalizeToken(info.router);
2863
- if (router === undefined)
2864
- return 0;
2865
- const result = db.query(`UPDATE steps
2866
- SET router = $router
2867
- WHERE session_id = $sessionID AND router IS NULL AND ($modelID IS NULL OR model_id = $modelID)`).run({ $router: router, $sessionID: sessionID, $modelID: modelID ?? null });
2868
- return result.changes;
2869
- }
2870
2725
 
2871
2726
  // src/stats/maintenance.ts
2872
2727
  var STEP_TIME2 = "COALESCE(created_at, recorded_at)";
@@ -2946,8 +2801,8 @@ function Panel(props) {
2946
2801
  ...current.modelID !== "" ? {
2947
2802
  modelID: current.modelID
2948
2803
  } : {},
2949
- ...current.routerID !== "" ? {
2950
- router: current.routerID
2804
+ ...current.providerID !== "" ? {
2805
+ provider: current.providerID
2951
2806
  } : {}
2952
2807
  };
2953
2808
  };
@@ -3003,8 +2858,8 @@ function Panel(props) {
3003
2858
  });
3004
2859
  const header = createMemo(() => {
3005
2860
  const current = settings();
3006
- const filters2 = filterSummary(current);
3007
- const where = detail() !== undefined ? " \xB7 backspace returns" : filters2 === undefined ? "" : ` \xB7 ${filters2}`;
2861
+ const filters = filterSummary(current);
2862
+ const where = detail() !== undefined ? " \xB7 backspace returns" : filters === undefined ? "" : ` \xB7 ${filters}`;
3008
2863
  return [{
3009
2864
  text: `usage insights \xB7 ${rangeLabel(current.range)}${where}`,
3010
2865
  tone: "accent"
@@ -3165,11 +3020,11 @@ function Panel(props) {
3165
3020
  if (db === undefined)
3166
3021
  return;
3167
3022
  const all = filterOptions(db);
3168
- const list = kind === "model" ? all.models : all.endpoints;
3023
+ const list = kind === "model" ? all.models : all.providers;
3169
3024
  const current = settings();
3170
- const selected = kind === "model" ? current.modelID : current.routerID;
3025
+ const selected = kind === "model" ? current.modelID : current.providerID;
3171
3026
  const value = await props.ctx.ui.dialog.select({
3172
- title: kind === "model" ? "Filter by model" : "Filter by endpoint (who served it)",
3027
+ title: kind === "model" ? "Filter by model" : "Filter by provider",
3173
3028
  current: selected,
3174
3029
  options: [{
3175
3030
  title: `any \xB7 ${list.length} observed`,
@@ -3187,7 +3042,7 @@ function Panel(props) {
3187
3042
  if (kind === "model")
3188
3043
  draft.modelID = value;
3189
3044
  else
3190
- draft.routerID = value;
3045
+ draft.providerID = value;
3191
3046
  });
3192
3047
  setAnchor(undefined);
3193
3048
  setDetail(undefined);
@@ -3225,10 +3080,10 @@ function Panel(props) {
3225
3080
  })), command("usage-insights.scope", "Toggle the turns scope", "a", () => props.update((draft) => {
3226
3081
  draft.turnsScope = sanitizeSettings(draft).turnsScope === "session" ? "all" : "session";
3227
3082
  draft.view = "turns";
3228
- })), command("usage-insights.filter.model", "Filter by model", "m", () => void pickFilter("model")), command("usage-insights.filter.endpoint", "Filter by endpoint", "e", () => void pickFilter("endpoint")), command("usage-insights.filter.clear", "Clear filters", "x", () => {
3083
+ })), command("usage-insights.filter.model", "Filter by model", "m", () => void pickFilter("model")), command("usage-insights.filter.provider", "Filter by provider", "p", () => void pickFilter("provider")), command("usage-insights.filter.clear", "Clear filters", "x", () => {
3229
3084
  props.update((draft) => {
3230
3085
  draft.modelID = "";
3231
- draft.routerID = "";
3086
+ draft.providerID = "";
3232
3087
  });
3233
3088
  setAnchor(undefined);
3234
3089
  }), command("usage-insights.refresh", "Refresh", "r", () => setTick((value) => value + 1)), command("usage-insights.vacuum", "Compact the database", "v", () => void compact()), command("usage-insights.help", "Help", "?", () => setHelp((open) => !open)), command("usage-insights.fullscreen", "Toggle full screen", "f", () => props.input.toggleFullscreen()), command("usage-insights.close", "Close usage panel", "escape", () => props.input.close()), ...PANEL_VIEWS.map((name, index) => command(`usage-insights.view.${name}`, `View ${name}`, String((index + 1) % 10), () => {
@@ -3400,8 +3255,6 @@ function TurnsSidebar(props) {
3400
3255
  }
3401
3256
 
3402
3257
  // src/tui.tsx
3403
- var ROUTER_CACHE_MS = 5000;
3404
- var MAX_ROUTER_CACHE = 512;
3405
3258
  var REFRESH_THROTTLE_MS = 300;
3406
3259
  var __test = {
3407
3260
  line: undefined,
@@ -3423,7 +3276,6 @@ var tui_default = Plugin.define({
3423
3276
  updateSettings(mutation);
3424
3277
  };
3425
3278
  const [refresh, setRefresh] = createSignal2(0);
3426
- const routerCache = new Map;
3427
3279
  let database;
3428
3280
  let databaseFailed = false;
3429
3281
  let refreshTimer;
@@ -3472,35 +3324,6 @@ var tui_default = Plugin.define({
3472
3324
  function sessionStart(sessionID) {
3473
3325
  return ctx.data.session.get(sessionID)?.time.created;
3474
3326
  }
3475
- function lookupRouter(sessionID, stepIDs) {
3476
- if (stepIDs.length === 0)
3477
- return;
3478
- const key = `${sessionID}#${stepIDs[stepIDs.length - 1]}#${stepIDs.length}`;
3479
- const now = Date.now();
3480
- const cached = routerCache.get(key);
3481
- if (cached !== undefined && now - cached.at < ROUTER_CACHE_MS)
3482
- return cached.value;
3483
- let value;
3484
- const handle = db();
3485
- if (handle !== undefined) {
3486
- try {
3487
- value = routerForSteps(handle, sessionID, stepIDs);
3488
- } catch {
3489
- value = undefined;
3490
- }
3491
- }
3492
- routerCache.set(key, {
3493
- value,
3494
- at: now
3495
- });
3496
- while (routerCache.size > MAX_ROUTER_CACHE) {
3497
- const oldest = routerCache.keys().next();
3498
- if (oldest.done === true)
3499
- break;
3500
- routerCache.delete(oldest.value);
3501
- }
3502
- return value;
3503
- }
3504
3327
  function sessionIdle(sessionID) {
3505
3328
  try {
3506
3329
  return ctx.data.session.status(sessionID) === "idle";
@@ -3513,23 +3336,7 @@ var tui_default = Plugin.define({
3513
3336
  return;
3514
3337
  const messages = ctx.data.session.message.list(sessionID);
3515
3338
  const info = ctx.data.session.get(sessionID);
3516
- const stats = computeTurn(toTranscriptTail(messages, 2), sessionTotals(info));
3517
- if (stats !== undefined) {
3518
- const router = lookupRouter(sessionID, stats.stepIDs);
3519
- if (router !== undefined) {
3520
- stats.router = router;
3521
- const handle = db();
3522
- if (handle !== undefined) {
3523
- try {
3524
- const previous = routerBefore(handle, sessionID, stats.startAt);
3525
- stats.identityChanged = previous !== undefined && previous !== stats.router;
3526
- } catch {
3527
- stats.identityChanged = false;
3528
- }
3529
- }
3530
- }
3531
- }
3532
- return stats;
3339
+ return computeTurn(toTranscriptTail(messages, 2), sessionTotals(info));
3533
3340
  }
3534
3341
  function toneColor(tone) {
3535
3342
  if (tone === "warning")
@@ -3542,8 +3349,7 @@ var tui_default = Plugin.define({
3542
3349
  const parts = createMemo3(() => lineParts(lineFor(props.sessionID), {
3543
3350
  labels: options.labels,
3544
3351
  fields: options.fields,
3545
- turnTotals: options.turnTotals,
3546
- identity: options.identity
3352
+ turnTotals: options.turnTotals
3547
3353
  }));
3548
3354
  return _$createComponent3(Show, {
3549
3355
  get when() {
@@ -3596,8 +3402,7 @@ var tui_default = Plugin.define({
3596
3402
  return {
3597
3403
  labels: options.labels,
3598
3404
  fields: options.fields,
3599
- turnTotals: options.turnTotals,
3600
- identity: options.identity
3405
+ turnTotals: options.turnTotals
3601
3406
  };
3602
3407
  }
3603
3408
  }) : null
@@ -3620,8 +3425,7 @@ var tui_default = Plugin.define({
3620
3425
  return {
3621
3426
  labels: options.labels,
3622
3427
  fields: options.fields,
3623
- turnTotals: options.turnTotals,
3624
- identity: options.identity
3428
+ turnTotals: options.turnTotals
3625
3429
  };
3626
3430
  }
3627
3431
  })
@@ -3672,10 +3476,7 @@ var tui_default = Plugin.define({
3672
3476
  });
3673
3477
  }
3674
3478
  const disposeEvents = [
3675
- ctx.data.on("session.step.ended", () => {
3676
- routerCache.clear();
3677
- bumpRefresh();
3678
- }),
3479
+ ctx.data.on("session.step.ended", () => bumpRefresh()),
3679
3480
  ctx.data.on("session.execution.succeeded", () => bumpRefresh()),
3680
3481
  ctx.data.on("session.execution.failed", () => bumpRefresh()),
3681
3482
  ctx.data.on("session.execution.interrupted", () => bumpRefresh()),
@@ -3707,8 +3508,8 @@ var tui_default = Plugin.define({
3707
3508
  }
3708
3509
  });
3709
3510
  export {
3710
- tui_default as default,
3711
- __test
3511
+ __test,
3512
+ tui_default as default
3712
3513
  };
3713
3514
 
3714
- //# debugId=8AFC5B22D32833D364756E2164756E21
3515
+ //# debugId=6DDD20AEAE1B401D64756E2164756E21