@linxiraos/pi-stats 1.0.9 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/CHANGELOG.md +2 -278
  2. package/LICENSE +22 -0
  3. package/README.md +4 -2
  4. package/package.json +91 -93
  5. package/src/aggregator.ts +16 -4
  6. package/src/client/app/NavRail.tsx +1 -1
  7. package/src/client/data/formatters.ts +13 -0
  8. package/src/client/data/view-models.ts +3 -0
  9. package/src/client/routes/CostsRoute.tsx +28 -10
  10. package/src/client/routes/ErrorsRoute.tsx +5 -5
  11. package/src/client/routes/ModelsRoute.tsx +3 -2
  12. package/src/client/routes/OverviewRoute.tsx +6 -6
  13. package/src/client/routes/ProjectsRoute.tsx +7 -5
  14. package/src/client/routes/ProvidersRoute.tsx +30 -5
  15. package/src/client/routes/RequestsRoute.tsx +5 -5
  16. package/src/client/routes/ToolsRoute.tsx +23 -11
  17. package/src/client/ui/MetricCluster.tsx +7 -3
  18. package/src/client/ui/RequestDrawer.tsx +3 -3
  19. package/src/db.ts +127 -35
  20. package/src/embedded-client.generated.txt +0 -1
  21. package/src/index.ts +43 -19
  22. package/src/port-conflict.ts +13 -8
  23. package/src/server.ts +26 -14
  24. package/src/shared-types.ts +14 -1
  25. package/src/usage-windows.ts +104 -28
  26. package/dist/client/index.css +0 -1
  27. package/dist/client/index.html +0 -24
  28. package/dist/client/index.js +0 -257
  29. package/dist/client/styles.css +0 -1692
  30. package/dist/types/aggregator.d.ts +0 -98
  31. package/dist/types/client/App.d.ts +0 -1
  32. package/dist/types/client/api.d.ts +0 -22
  33. package/dist/types/client/app/AppLayout.d.ts +0 -16
  34. package/dist/types/client/app/NavRail.d.ts +0 -7
  35. package/dist/types/client/app/RangeControl.d.ts +0 -7
  36. package/dist/types/client/app/SyncButton.d.ts +0 -14
  37. package/dist/types/client/app/ThemeToggle.d.ts +0 -1
  38. package/dist/types/client/app/TopBar.d.ts +0 -15
  39. package/dist/types/client/app/routes.d.ts +0 -12
  40. package/dist/types/client/components/AgentTokenShare.d.ts +0 -5
  41. package/dist/types/client/components/chart-shared.d.ts +0 -173
  42. package/dist/types/client/components/models-table-shared.d.ts +0 -175
  43. package/dist/types/client/components/range-meta.d.ts +0 -21
  44. package/dist/types/client/data/charts.d.ts +0 -1
  45. package/dist/types/client/data/formatters.d.ts +0 -8
  46. package/dist/types/client/data/useHashRoute.d.ts +0 -8
  47. package/dist/types/client/data/useResource.d.ts +0 -13
  48. package/dist/types/client/data/view-models.d.ts +0 -74
  49. package/dist/types/client/index.d.ts +0 -2
  50. package/dist/types/client/routes/BehaviorRoute.d.ts +0 -7
  51. package/dist/types/client/routes/CostsRoute.d.ts +0 -7
  52. package/dist/types/client/routes/ErrorsRoute.d.ts +0 -8
  53. package/dist/types/client/routes/GainRoute.d.ts +0 -7
  54. package/dist/types/client/routes/ModelsRoute.d.ts +0 -7
  55. package/dist/types/client/routes/OverviewRoute.d.ts +0 -8
  56. package/dist/types/client/routes/ProjectsRoute.d.ts +0 -7
  57. package/dist/types/client/routes/ProvidersRoute.d.ts +0 -7
  58. package/dist/types/client/routes/RequestsRoute.d.ts +0 -8
  59. package/dist/types/client/routes/ToolsRoute.d.ts +0 -7
  60. package/dist/types/client/routes/index.d.ts +0 -10
  61. package/dist/types/client/types.d.ts +0 -63
  62. package/dist/types/client/ui/AsyncBoundary.d.ts +0 -12
  63. package/dist/types/client/ui/DataTable.d.ts +0 -17
  64. package/dist/types/client/ui/EmptyState.d.ts +0 -7
  65. package/dist/types/client/ui/ErrorState.d.ts +0 -6
  66. package/dist/types/client/ui/JsonBlock.d.ts +0 -7
  67. package/dist/types/client/ui/MetricCluster.d.ts +0 -5
  68. package/dist/types/client/ui/Panel.d.ts +0 -7
  69. package/dist/types/client/ui/RequestDrawer.d.ts +0 -5
  70. package/dist/types/client/ui/SegmentedControl.d.ts +0 -12
  71. package/dist/types/client/ui/Skeleton.d.ts +0 -8
  72. package/dist/types/client/ui/StatusPill.d.ts +0 -7
  73. package/dist/types/client/ui/index.d.ts +0 -11
  74. package/dist/types/client/useSystemTheme.d.ts +0 -11
  75. package/dist/types/db.d.ts +0 -159
  76. package/dist/types/embedded-client.d.ts +0 -18
  77. package/dist/types/gain-aggregator.d.ts +0 -26
  78. package/dist/types/index.d.ts +0 -7
  79. package/dist/types/parser.d.ts +0 -53
  80. package/dist/types/port-conflict.d.ts +0 -14
  81. package/dist/types/server.d.ts +0 -12
  82. package/dist/types/shared-types.d.ts +0 -408
  83. package/dist/types/sync-worker.d.ts +0 -31
  84. package/dist/types/types.d.ts +0 -164
  85. package/dist/types/usage-windows.d.ts +0 -45
  86. package/dist/types/user-metrics.d.ts +0 -81
package/src/db.ts CHANGED
@@ -1,9 +1,15 @@
1
1
  import { Database } from "bun:sqlite";
2
2
  import * as fs from "node:fs/promises";
3
3
  import type { Usage } from "@linxiraos/pi-ai";
4
- import type { GeneratedProvider } from "@linxiraos/pi-catalog/models";
5
- import { calculateUncachedInputCost, getBundledModel } from "@linxiraos/pi-catalog/models";
4
+ import {
5
+ calculateUncachedInputCost,
6
+ calculateUsageCost,
7
+ type GeneratedProvider,
8
+ getBundledModel,
9
+ } from "@linxiraos/pi-catalog/models";
10
+ import type { ModelCost } from "@linxiraos/pi-catalog/types";
6
11
  import { getConfigRootDir, getStatsDbPath } from "@linxiraos/pi-utils";
12
+
7
13
  import { classifyAgentType } from "./parser";
8
14
  import type {
9
15
  AgentType,
@@ -31,9 +37,8 @@ import type {
31
37
  UserMessageStats,
32
38
  } from "./types";
33
39
 
34
- type ModelCost = { input: number; output: number; cacheRead: number; cacheWrite: number };
35
40
  type UsageCost = Usage["cost"];
36
- type CostTokens = Pick<Usage, "input" | "output" | "cacheRead" | "cacheWrite" | "orchestration">;
41
+ type CostTokens = Pick<Usage, "input" | "output" | "cacheRead" | "cacheWrite" | "orchestration" | "cttl">;
37
42
 
38
43
  const ZERO_USAGE_COST: UsageCost = {
39
44
  input: 0,
@@ -43,6 +48,9 @@ const ZERO_USAGE_COST: UsageCost = {
43
48
  total: 0,
44
49
  };
45
50
 
51
+ const UNPRICED_XAI_OAUTH_SQL =
52
+ "CASE WHEN provider = 'xai-oauth' AND total_tokens > 0 AND cost_total = 0 THEN 1 ELSE 0 END";
53
+
46
54
  interface CostBackfillRow {
47
55
  id: number;
48
56
  provider: string;
@@ -71,6 +79,7 @@ interface AggregatedStatsRow {
71
79
  total_cache_write_tokens: number | null;
72
80
  total_premium_requests: number | null;
73
81
  total_cost: number | null;
82
+ unpriced_requests: number | null;
74
83
  total_cached_prompt_cost: number | null;
75
84
  total_no_cache_input_cost: number | null;
76
85
  avg_duration: number | null;
@@ -89,6 +98,19 @@ interface FolderStatsRow extends AggregatedStatsRow {
89
98
  folder: string;
90
99
  }
91
100
 
101
+ interface CostTimeSeriesRow {
102
+ bucket: number;
103
+ model: string;
104
+ provider: string;
105
+ cost: number | null;
106
+ unpriced_requests: number | null;
107
+ cost_input: number | null;
108
+ cost_output: number | null;
109
+ cost_cache_read: number | null;
110
+ cost_cache_write: number | null;
111
+ requests: number;
112
+ }
113
+
92
114
  let db: Database | null = null;
93
115
 
94
116
  const BACKFILL_COMPLETE = "complete";
@@ -99,6 +121,12 @@ const PRIORITY_PREMIUM_REQUESTS_BACKFILL_KEY = "premium_requests_priority_v1";
99
121
  const AGENT_TYPE_BACKFILL_KEY = "agent_type_v1";
100
122
  const FORK_DEDUPE_KEY = "fork_dedupe_v1";
101
123
  const TOOL_CALLS_BACKFILL_KEY = "tool_calls_v1";
124
+ // Older ingests dropped `Usage.orchestration` (never a stored column) when
125
+ // pricing, so subscription models billed on orchestration tokens — multi-agent
126
+ // Grok most notably — were priced from conversation buckets alone and could not
127
+ // reach the inclusive 200K tier. A one-time full re-parse repairs them through
128
+ // the cost-refreshing UPSERT in `insertMessageStats`.
129
+ const COST_REINGEST_BACKFILL_KEY = "messages_cost_reingest_v1";
102
130
  function shouldResetBackfill(value: string | undefined): boolean {
103
131
  return value !== BACKFILL_COMPLETE && value !== BACKFILL_PENDING;
104
132
  }
@@ -301,6 +329,7 @@ export async function initDb(): Promise<Database> {
301
329
  }
302
330
  backfillUserMessages(db);
303
331
  backfillToolCalls(db);
332
+ backfillReingestCosts(db);
304
333
  repairUserMessageLinks(db);
305
334
  backfillPriorityPremiumRequests(db);
306
335
  backfillAgentType(db);
@@ -325,10 +354,11 @@ function getCatalogCost(provider: string, modelId: string): ModelCost | null {
325
354
  return primaryCost;
326
355
  }
327
356
 
328
- if (provider === "openai-codex") {
329
- const openAICost = getBundledModelCost("openai", modelId);
330
- if (openAICost && hasBillableCost(openAICost)) {
331
- return openAICost;
357
+ const fallbackProvider = provider === "openai-codex" ? "openai" : provider === "xai-oauth" ? "xai" : null;
358
+ if (fallbackProvider) {
359
+ const fallbackCost = getBundledModelCost(fallbackProvider, modelId);
360
+ if (fallbackCost && hasBillableCost(fallbackCost)) {
361
+ return fallbackCost;
332
362
  }
333
363
  }
334
364
 
@@ -339,27 +369,42 @@ function calculateCatalogCost(provider: string, modelId: string, tokens: CostTok
339
369
  const cost = getCatalogCost(provider, modelId);
340
370
  if (!cost) return null;
341
371
 
342
- const input = (cost.input / 1_000_000) * tokens.input;
343
- const output = (cost.output / 1_000_000) * tokens.output;
344
- const cacheRead = (cost.cacheRead / 1_000_000) * tokens.cacheRead;
345
- const cacheWrite = (cost.cacheWrite / 1_000_000) * tokens.cacheWrite;
346
-
347
- return {
348
- input,
349
- output,
350
- cacheRead,
351
- cacheWrite,
352
- total: input + output + cacheRead + cacheWrite,
372
+ const orchestration = tokens.orchestration;
373
+ const usage: Usage = {
374
+ ...tokens,
375
+ totalTokens:
376
+ tokens.input +
377
+ tokens.output +
378
+ tokens.cacheRead +
379
+ tokens.cacheWrite +
380
+ (orchestration?.input ?? 0) +
381
+ (orchestration?.output ?? 0) +
382
+ (orchestration?.cacheRead ?? 0),
383
+ cost: { ...ZERO_USAGE_COST },
353
384
  };
385
+ return calculateUsageCost(cost, usage);
386
+ }
387
+
388
+ function normalizeUsageCost(cost: UsageCost): UsageCost {
389
+ const input = cost.input ?? 0;
390
+ const output = cost.output ?? 0;
391
+ const cacheRead = cost.cacheRead ?? 0;
392
+ const cacheWrite = cost.cacheWrite ?? 0;
393
+ const total = cost.total ?? input + output + cacheRead + cacheWrite;
394
+ return { input, output, cacheRead, cacheWrite, total };
354
395
  }
355
396
 
356
397
  function resolveStoredCost(stats: MessageStats): UsageCost {
357
- // `usage.cost` was optional in older session files. Although current
358
- // MessageStats requires it, parsed JSONL can still carry that legacy shape.
359
- const storedCost: UsageCost | undefined = stats.usage.cost;
360
- if (storedCost && storedCost.total !== 0) {
361
- return storedCost;
362
- }
398
+ // `usage.cost` was optional in older session files, and legacy payloads
399
+ // can carry a partially-populated cost object (e.g. only `total`). The
400
+ // messages table declares every cost_* column as REAL NOT NULL, so any
401
+ // missing field must be normalised here before binding into SQLite.
402
+ const raw: UsageCost | undefined = stats.usage.cost;
403
+ const storedCost = raw ? normalizeUsageCost(raw) : undefined;
404
+
405
+ // A missing total is derived from the stored components. An explicit zero
406
+ // remains the sentinel for catalog-based correction.
407
+ if (storedCost && (raw?.total ?? storedCost.total) !== 0) return storedCost;
363
408
 
364
409
  return calculateCatalogCost(stats.provider, stats.model, stats.usage) ?? storedCost ?? ZERO_USAGE_COST;
365
410
  }
@@ -473,8 +518,9 @@ export function setFileOffset(sessionFile: string, offset: number, lastModified:
473
518
  * aggregate. The `WHERE NOT EXISTS` clause skips inserts whose
474
519
  * `(entry_id, timestamp)` already exists under a different `session_file` —
475
520
  * first-write-wins across the lineage. Same-file re-syncs still hit the
476
- * `ON CONFLICT(session_file, entry_id)` upsert below so historical
477
- * `premium_requests` fix-ups continue to work.
521
+ * `ON CONFLICT(session_file, entry_id)` upsert below, which re-derives the
522
+ * stored cost (orchestration-aware) and keeps `premium_requests` monotonic, so
523
+ * a forced re-parse repairs historical `premium_requests` and cost fix-ups.
478
524
  */
479
525
  export function insertMessageStats(stats: MessageStats[]): number {
480
526
  if (!db || stats.length === 0) return 0;
@@ -492,8 +538,13 @@ export function insertMessageStats(stats: MessageStats[]): number {
492
538
  WHERE entry_id = ? AND timestamp = ? AND session_file <> ?
493
539
  )
494
540
  ON CONFLICT(session_file, entry_id) DO UPDATE SET
495
- premium_requests = excluded.premium_requests
496
- WHERE messages.premium_requests < excluded.premium_requests
541
+ premium_requests = MAX(messages.premium_requests, excluded.premium_requests),
542
+ cost_input = excluded.cost_input,
543
+ cost_output = excluded.cost_output,
544
+ cost_cache_read = excluded.cost_cache_read,
545
+ cost_cache_write = excluded.cost_cache_write,
546
+ cost_total = excluded.cost_total,
547
+ cost_no_cache_input = excluded.cost_no_cache_input
497
548
  `);
498
549
 
499
550
  let inserted = 0;
@@ -557,6 +608,7 @@ function buildAggregatedStats(rows: AggregatedStatsRow[]): AggregatedStats {
557
608
  cacheRate: 0,
558
609
  cacheSavings: 0,
559
610
  totalCost: 0,
611
+ unpricedRequests: 0,
560
612
  totalPremiumRequests: 0,
561
613
  avgDuration: null,
562
614
  avgTtft: null,
@@ -591,6 +643,7 @@ function buildAggregatedStats(rows: AggregatedStatsRow[]): AggregatedStats {
591
643
  : 0,
592
644
  cacheSavings: noCacheInputCost > 0 ? (noCacheInputCost - cachedPromptCost) / noCacheInputCost : 0,
593
645
  totalCost: row.total_cost || 0,
646
+ unpricedRequests: row.unpriced_requests || 0,
594
647
  totalPremiumRequests,
595
648
  avgDuration: row.avg_duration,
596
649
  avgTtft: row.avg_ttft,
@@ -617,6 +670,7 @@ export function getOverallStats(cutoff?: number): AggregatedStats {
617
670
  SUM(cache_write_tokens) as total_cache_write_tokens,
618
671
  SUM(premium_requests) as total_premium_requests,
619
672
  SUM(cost_total) as total_cost,
673
+ SUM(${UNPRICED_XAI_OAUTH_SQL}) as unpriced_requests,
620
674
  SUM(CASE WHEN cost_no_cache_input > 0
621
675
  THEN cost_input + cost_cache_read + cost_cache_write
622
676
  ELSE 0 END) as total_cached_prompt_cost,
@@ -652,6 +706,7 @@ export function getStatsByModel(cutoff?: number): ModelStats[] {
652
706
  SUM(cache_write_tokens) as total_cache_write_tokens,
653
707
  SUM(premium_requests) as total_premium_requests,
654
708
  SUM(cost_total) as total_cost,
709
+ SUM(${UNPRICED_XAI_OAUTH_SQL}) as unpriced_requests,
655
710
  SUM(CASE WHEN cost_no_cache_input > 0
656
711
  THEN cost_input + cost_cache_read + cost_cache_write
657
712
  ELSE 0 END) as total_cached_prompt_cost,
@@ -693,6 +748,7 @@ export function getStatsByFolder(cutoff?: number): FolderStats[] {
693
748
  SUM(cache_write_tokens) as total_cache_write_tokens,
694
749
  SUM(premium_requests) as total_premium_requests,
695
750
  SUM(cost_total) as total_cost,
751
+ SUM(${UNPRICED_XAI_OAUTH_SQL}) as unpriced_requests,
696
752
  SUM(CASE WHEN cost_no_cache_input > 0
697
753
  THEN cost_input + cost_cache_read + cost_cache_write
698
754
  ELSE 0 END) as total_cached_prompt_cost,
@@ -841,6 +897,7 @@ export function getStatsByProvider(cutoff?: number | null): ProviderAggregate[]
841
897
  SUM(cache_write_tokens) as total_cache_write_tokens,
842
898
  SUM(input_tokens + output_tokens + cache_read_tokens + cache_write_tokens) as total_tokens,
843
899
  SUM(cost_total) as total_cost,
900
+ SUM(${UNPRICED_XAI_OAUTH_SQL}) as unpriced_requests,
844
901
  SUM(premium_requests) as total_premium_requests,
845
902
  AVG(CASE WHEN duration > 0 THEN output_tokens * 1000.0 / duration ELSE NULL END) as avg_tokens_per_second
846
903
  FROM messages
@@ -860,6 +917,7 @@ export function getStatsByProvider(cutoff?: number | null): ProviderAggregate[]
860
917
  total_cache_write_tokens: number | null;
861
918
  total_tokens: number | null;
862
919
  total_cost: number | null;
920
+ unpriced_requests: number | null;
863
921
  total_premium_requests: number | null;
864
922
  avg_tokens_per_second: number | null;
865
923
  }>;
@@ -874,6 +932,7 @@ export function getStatsByProvider(cutoff?: number | null): ProviderAggregate[]
874
932
  totalCacheWriteTokens: row.total_cache_write_tokens ?? 0,
875
933
  totalTokens: row.total_tokens ?? 0,
876
934
  totalCost: row.total_cost ?? 0,
935
+ unpricedRequests: row.unpriced_requests ?? 0,
877
936
  totalPremiumRequests: row.total_premium_requests ?? 0,
878
937
  avgTokensPerSecond: row.avg_tokens_per_second,
879
938
  }));
@@ -936,6 +995,7 @@ export function getProviderTimeSeries(
936
995
  provider,
937
996
  SUM(input_tokens + output_tokens + cache_read_tokens + cache_write_tokens) as total_tokens,
938
997
  SUM(cost_total) as cost,
998
+ SUM(${UNPRICED_XAI_OAUTH_SQL}) as unpriced_requests,
939
999
  COUNT(*) as requests
940
1000
  FROM messages
941
1001
  ${hasCutoff ? "WHERE timestamp >= ?" : ""}
@@ -949,6 +1009,7 @@ export function getProviderTimeSeries(
949
1009
  provider: string;
950
1010
  total_tokens: number | null;
951
1011
  cost: number | null;
1012
+ unpriced_requests: number | null;
952
1013
  requests: number;
953
1014
  }>;
954
1015
  return rows.map(row => ({
@@ -956,6 +1017,7 @@ export function getProviderTimeSeries(
956
1017
  provider: row.provider,
957
1018
  totalTokens: row.total_tokens ?? 0,
958
1019
  cost: row.cost ?? 0,
1020
+ unpricedRequests: row.unpriced_requests ?? 0,
959
1021
  requests: row.requests,
960
1022
  }));
961
1023
  }
@@ -1105,6 +1167,7 @@ export function getCostTimeSeries(days = 90, cutoff?: number | null): CostTimeSe
1105
1167
  model,
1106
1168
  provider,
1107
1169
  SUM(cost_total) as cost,
1170
+ SUM(${UNPRICED_XAI_OAUTH_SQL}) as unpriced_requests,
1108
1171
  SUM(cost_input) as cost_input,
1109
1172
  SUM(cost_output) as cost_output,
1110
1173
  SUM(cost_cache_read) as cost_cache_read,
@@ -1116,16 +1179,17 @@ export function getCostTimeSeries(days = 90, cutoff?: number | null): CostTimeSe
1116
1179
  ORDER BY bucket ASC
1117
1180
  `);
1118
1181
 
1119
- const rows = hasCutoff ? (stmt.all(seriesCutoff) as any[]) : (stmt.all() as any[]);
1182
+ const rows = (hasCutoff ? stmt.all(seriesCutoff) : stmt.all()) as CostTimeSeriesRow[];
1120
1183
  return rows.map(row => ({
1121
1184
  timestamp: row.bucket,
1122
1185
  model: row.model,
1123
1186
  provider: row.provider,
1124
- cost: row.cost,
1125
- costInput: row.cost_input,
1126
- costOutput: row.cost_output,
1127
- costCacheRead: row.cost_cache_read,
1128
- costCacheWrite: row.cost_cache_write,
1187
+ cost: row.cost ?? 0,
1188
+ unpricedRequests: row.unpriced_requests ?? 0,
1189
+ costInput: row.cost_input ?? 0,
1190
+ costOutput: row.cost_output ?? 0,
1191
+ costCacheRead: row.cost_cache_read ?? 0,
1192
+ costCacheWrite: row.cost_cache_write ?? 0,
1129
1193
  requests: row.requests,
1130
1194
  }));
1131
1195
  }
@@ -1203,6 +1267,29 @@ function backfillToolCalls(database: Database): void {
1203
1267
  .run(TOOL_CALLS_BACKFILL_KEY, BACKFILL_PENDING);
1204
1268
  }
1205
1269
 
1270
+ /**
1271
+ * One-shot `file_offsets` wipe so the next sync re-parses every session and
1272
+ * re-prices `messages` from source. Pre-fix ingests priced subscription rows
1273
+ * from the four stored token buckets only, dropping `Usage.orchestration`
1274
+ * (never a stored column), so multi-agent Grok and any other orchestration-
1275
+ * billed model were understated and could not reach the inclusive 200K tier.
1276
+ * The re-parse reruns the orchestration-aware pricing in `resolveStoredCost`
1277
+ * and the cost-refreshing UPSERT in {@link insertMessageStats} writes it back.
1278
+ * `messages`/`user_messages`/`tool_calls` re-inserts are idempotent, so the
1279
+ * offset reset is safe. Same sentinel protocol as {@link backfillToolCalls}.
1280
+ */
1281
+ function backfillReingestCosts(database: Database): void {
1282
+ const row = database.prepare("SELECT value FROM meta WHERE key = ?").get(COST_REINGEST_BACKFILL_KEY) as
1283
+ | { value: string }
1284
+ | undefined;
1285
+ if (!shouldResetBackfill(row?.value)) return;
1286
+
1287
+ database.run("DELETE FROM file_offsets");
1288
+ database
1289
+ .prepare("INSERT OR REPLACE INTO meta (key, value) VALUES (?, ?)")
1290
+ .run(COST_REINGEST_BACKFILL_KEY, BACKFILL_PENDING);
1291
+ }
1292
+
1206
1293
  /**
1207
1294
  * Reclassify pre-existing `messages` rows by agent type once, after the
1208
1295
  * `agent_type` column is added to an older database (every prior row defaulted
@@ -1328,6 +1415,7 @@ export function markSessionBackfillsComplete(): void {
1328
1415
  TOOL_CALLS_BACKFILL_KEY,
1329
1416
  USER_MESSAGE_LINKS_REPAIR_KEY,
1330
1417
  PRIORITY_PREMIUM_REQUESTS_BACKFILL_KEY,
1418
+ COST_REINGEST_BACKFILL_KEY,
1331
1419
  ]) {
1332
1420
  markComplete.run(key, BACKFILL_COMPLETE);
1333
1421
  }
@@ -1687,6 +1775,8 @@ const TOOL_AGGREGATE_COLUMNS = `
1687
1775
  SUM(COALESCE(m.total_tokens, 0) * 1.0 / t.calls_in_turn) as total_tokens_share,
1688
1776
  SUM(COALESCE(m.output_tokens, 0) * 1.0 / t.calls_in_turn) as output_tokens_share,
1689
1777
  SUM(COALESCE(m.cost_total, 0) / t.calls_in_turn) as cost_share,
1778
+ SUM(CASE WHEN t.provider = 'xai-oauth' AND COALESCE(m.total_tokens, 0) > 0 AND COALESCE(m.cost_total, 0) = 0
1779
+ THEN 1.0 / t.calls_in_turn ELSE 0 END) as unpriced_requests_share,
1690
1780
  MAX(t.timestamp) as last_used
1691
1781
  `;
1692
1782
 
@@ -1701,6 +1791,7 @@ interface ToolAggregateRow {
1701
1791
  total_tokens_share: number | null;
1702
1792
  output_tokens_share: number | null;
1703
1793
  cost_share: number | null;
1794
+ unpriced_requests_share: number | null;
1704
1795
  last_used: number;
1705
1796
  }
1706
1797
 
@@ -1714,6 +1805,7 @@ function rowToToolUsage(row: ToolAggregateRow): ToolUsageStats {
1714
1805
  totalTokensShare: row.total_tokens_share ?? 0,
1715
1806
  outputTokensShare: row.output_tokens_share ?? 0,
1716
1807
  costShare: row.cost_share ?? 0,
1808
+ unpricedRequestsShare: row.unpriced_requests_share ?? 0,
1717
1809
  lastUsed: row.last_used,
1718
1810
  };
1719
1811
  }