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/README.md +58 -54
- package/dist/core.d.ts +19 -31
- package/dist/index.d.ts +5 -3
- package/dist/index.js +36 -249
- package/dist/index.js.map +9 -10
- package/dist/line.d.ts +1 -2
- package/dist/panel-build.d.ts +3 -1
- package/dist/panel-data.d.ts +23 -19
- package/dist/recorder.d.ts +4 -17
- package/dist/sidebar.d.ts +3 -3
- package/dist/stats/aggregates.d.ts +38 -37
- package/dist/stats/db.d.ts +8 -17
- package/dist/stats/queries.d.ts +10 -30
- package/dist/text.d.ts +5 -8
- package/dist/tui.d.ts +7 -12
- package/dist/tui.js +168 -367
- package/dist/tui.js.map +13 -13
- package/package.json +2 -2
- package/dist/endpoint.d.ts +0 -46
package/dist/tui.js
CHANGED
|
@@ -54,7 +54,7 @@ function totalize(steps) {
|
|
|
54
54
|
}
|
|
55
55
|
return totals;
|
|
56
56
|
}
|
|
57
|
-
function
|
|
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 (
|
|
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 =
|
|
139
|
-
const previousKey =
|
|
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:
|
|
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(
|
|
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 (
|
|
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({
|
|
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?.
|
|
489
|
-
clauses.push("
|
|
490
|
-
params.push(filters.
|
|
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
|
|
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 =
|
|
530
|
+
let model = models.get(key);
|
|
540
531
|
if (!model) {
|
|
541
532
|
model = { modelID, variant, providers: [], totals: emptyUsage() };
|
|
542
|
-
|
|
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 [...
|
|
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
|
|
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"),
|
|
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", "
|
|
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
|
-
|
|
834
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: `[${
|
|
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
|
|
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/
|
|
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/
|
|
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.
|
|
1049
|
-
parts.push(`
|
|
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: "
|
|
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
|
|
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: "
|
|
1079
|
-
{ text: " before it in the same session (
|
|
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: "
|
|
1082
|
-
{ text: "
|
|
1083
|
-
{ text: "
|
|
1084
|
-
{ text: "
|
|
1085
|
-
{ text: "
|
|
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
|
-
["
|
|
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("
|
|
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
|
|
1173
|
+
const providers = unique(model.providers.map((provider) => provider.providerID));
|
|
1213
1174
|
const cache = extras?.cache?.get(key);
|
|
1214
|
-
const
|
|
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(
|
|
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 (
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
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
|
|
1242
|
+
function providerSummaryLines(rows, width, limit = 40) {
|
|
1280
1243
|
const lines = renderTable(rows, [
|
|
1281
|
-
{ header: "
|
|
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
|
|
1294
|
-
row: (row) => row.
|
|
1295
|
-
tone: (row) => row.
|
|
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
|
|
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: "
|
|
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.
|
|
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.
|
|
1321
|
+
if (data.gap.providers.length > 1) {
|
|
1407
1322
|
lines.push({ text: "", tone: "muted" });
|
|
1408
|
-
lines.push({ text: "reuse kept, by
|
|
1409
|
-
const
|
|
1410
|
-
lines.push({ text: formatTableRow(["
|
|
1411
|
-
for (const row of data.gap.
|
|
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.
|
|
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.
|
|
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
|
|
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} ${
|
|
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,
|
|
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", "
|
|
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
|
|
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
|
-
|
|
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) :
|
|
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
|
|
1946
|
-
if (
|
|
1947
|
-
ratios.push(step.cached /
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1999
|
-
|
|
2000
|
-
if (
|
|
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
|
|
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
|
|
2058
|
-
const row =
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
2162
|
-
const values =
|
|
2063
|
+
function providerOf(steps, models) {
|
|
2064
|
+
const values = groupValues(steps);
|
|
2163
2065
|
const times = steps.map((step) => step.at);
|
|
2164
2066
|
return {
|
|
2165
|
-
|
|
2166
|
-
models:
|
|
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
|
|
2079
|
+
function providerSummaryOf(rows) {
|
|
2178
2080
|
const out = [];
|
|
2179
|
-
for (const steps of groupBy(rows, (step) => step.
|
|
2180
|
-
const summary =
|
|
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.
|
|
2085
|
+
return out.sort((left, right) => right.usage.steps - left.usage.steps || (left.provider ?? "").localeCompare(right.provider ?? ""));
|
|
2184
2086
|
}
|
|
2185
|
-
function
|
|
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.
|
|
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
|
-
...
|
|
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
|
|
2214
|
-
return { key: `${
|
|
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
|
-
|
|
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
|
|
2287
|
-
return [{ text: `stats query failed: ${
|
|
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
|
|
2214
|
+
const switches = switchesOf(pairs);
|
|
2316
2215
|
return modelsLines(rows, width, {
|
|
2317
2216
|
cache: cacheByModel(pairs),
|
|
2318
|
-
|
|
2217
|
+
switches: switchesByModel(switches.rows)
|
|
2319
2218
|
}, 40);
|
|
2320
2219
|
}
|
|
2321
|
-
case "
|
|
2322
|
-
return
|
|
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 ? "
|
|
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
|
-
...
|
|
2288
|
+
...providerLines(providerBreakdownOf(steps), width, 40),
|
|
2391
2289
|
{ text: "", tone: "muted" },
|
|
2392
|
-
...
|
|
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 === "
|
|
2402
|
-
const scoped = { ...filters,
|
|
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
|
-
...
|
|
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
|
-
...
|
|
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
|
|
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 =
|
|
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
|
|
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,
|
|
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, $
|
|
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
|
|
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.
|
|
2950
|
-
|
|
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
|
|
3007
|
-
const where = detail() !== undefined ? " \xB7 backspace returns" :
|
|
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.
|
|
3023
|
+
const list = kind === "model" ? all.models : all.providers;
|
|
3169
3024
|
const current = settings();
|
|
3170
|
-
const selected = kind === "model" ? current.modelID : current.
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
3711
|
-
|
|
3511
|
+
__test,
|
|
3512
|
+
tui_default as default
|
|
3712
3513
|
};
|
|
3713
3514
|
|
|
3714
|
-
//# debugId=
|
|
3515
|
+
//# debugId=6DDD20AEAE1B401D64756E2164756E21
|