@oh-my-pi/omp-stats 18.3.4 → 18.4.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.
- package/CHANGELOG.md +16 -0
- package/README.md +13 -11
- package/build.ts +5 -49
- package/dist/client/index.css +1 -1
- package/dist/client/index.html +2 -2
- package/dist/client/index.js +105 -126
- package/dist/types/aggregator.d.ts +25 -22
- package/dist/types/client/api.d.ts +11 -7
- package/dist/types/client/app/LiveChip.d.ts +2 -0
- package/dist/types/client/app/Shell.d.ts +16 -0
- package/dist/types/client/app/ThemeToggle.d.ts +1 -0
- package/dist/types/client/app/nav.d.ts +18 -0
- package/dist/types/client/charts/BarList.d.ts +17 -0
- package/dist/types/client/charts/Chart.d.ts +49 -0
- package/dist/types/client/charts/Legend.d.ts +16 -0
- package/dist/types/client/charts/ShareBar.d.ts +11 -0
- package/dist/types/client/charts/Sparkline.d.ts +11 -0
- package/dist/types/client/charts/TimeChart.d.ts +9 -0
- package/dist/types/client/charts/index.d.ts +7 -0
- package/dist/types/client/charts/types.d.ts +25 -0
- package/dist/types/client/charts/useWidth.d.ts +3 -0
- package/dist/types/client/data/colors.d.ts +25 -0
- package/dist/types/client/data/formatters.d.ts +9 -1
- package/dist/types/client/data/live.d.ts +29 -0
- package/dist/types/client/data/query.d.ts +35 -0
- package/dist/types/client/data/range.d.ts +31 -0
- package/dist/types/client/data/series.d.ts +28 -0
- package/dist/types/client/data/useHashRoute.d.ts +18 -4
- package/dist/types/client/data/view-models.d.ts +111 -28
- package/dist/types/client/index.d.ts +0 -1
- package/dist/types/client/routes/CostsRoute.d.ts +2 -2
- package/dist/types/client/routes/ErrorsRoute.d.ts +2 -2
- package/dist/types/client/routes/FrustrationRoute.d.ts +7 -0
- package/dist/types/client/routes/GainRoute.d.ts +1 -2
- package/dist/types/client/routes/ModelsRoute.d.ts +2 -2
- package/dist/types/client/routes/OverviewRoute.d.ts +1 -2
- package/dist/types/client/routes/ProjectsRoute.d.ts +2 -2
- package/dist/types/client/routes/ProvidersRoute.d.ts +2 -2
- package/dist/types/client/routes/RequestsRoute.d.ts +1 -2
- package/dist/types/client/routes/ToolsRoute.d.ts +2 -2
- package/dist/types/client/routes/TracesRoute.d.ts +2 -2
- package/dist/types/client/routes/index.d.ts +1 -1
- package/dist/types/client/traces/AggregatesPanel.d.ts +4 -3
- package/dist/types/client/traces/SummaryStrip.d.ts +2 -2
- package/dist/types/client/traces/trace-colors.d.ts +18 -12
- package/dist/types/client/ui/Badge.d.ts +19 -0
- package/dist/types/client/ui/Card.d.ts +25 -0
- package/dist/types/client/ui/Drawer.d.ts +25 -0
- package/dist/types/client/ui/JsonBlock.d.ts +2 -2
- package/dist/types/client/ui/Modal.d.ts +23 -0
- package/dist/types/client/ui/RequestDrawer.d.ts +10 -1
- package/dist/types/client/ui/SearchInput.d.ts +8 -0
- package/dist/types/client/ui/Segmented.d.ts +15 -0
- package/dist/types/client/ui/Stat.d.ts +36 -0
- package/dist/types/client/ui/States.d.ts +41 -0
- package/dist/types/client/ui/Table.d.ts +48 -0
- package/dist/types/client/ui/index.d.ts +9 -10
- package/dist/types/db.d.ts +90 -106
- package/dist/types/frustration.d.ts +70 -0
- package/dist/types/index.d.ts +3 -1
- package/dist/types/live.d.ts +30 -0
- package/dist/types/rollup.d.ts +115 -0
- package/dist/types/server.d.ts +6 -1
- package/dist/types/shared-types.d.ts +116 -56
- package/dist/types/types.d.ts +4 -0
- package/dist/types/user-metrics.d.ts +8 -0
- package/package.json +7 -13
- package/src/aggregator.ts +276 -211
- package/src/client/App.tsx +38 -80
- package/src/client/api.ts +65 -7
- package/src/client/app/LiveChip.tsx +97 -0
- package/src/client/app/Shell.tsx +181 -0
- package/src/client/app/ThemeToggle.tsx +5 -2
- package/src/client/app/nav.ts +78 -0
- package/src/client/charts/BarList.tsx +48 -0
- package/src/client/charts/Chart.tsx +388 -0
- package/src/client/charts/Legend.tsx +61 -0
- package/src/client/charts/ShareBar.tsx +26 -0
- package/src/client/charts/Sparkline.tsx +38 -0
- package/src/client/charts/TimeChart.tsx +22 -0
- package/src/client/charts/index.ts +7 -0
- package/src/client/charts/types.ts +27 -0
- package/src/client/charts/useWidth.ts +19 -0
- package/src/client/data/colors.ts +45 -0
- package/src/client/data/formatters.ts +30 -9
- package/src/client/data/live.tsx +85 -0
- package/src/client/data/query.ts +166 -0
- package/src/client/data/range.ts +82 -0
- package/src/client/data/series.ts +85 -0
- package/src/client/data/useHashRoute.ts +41 -83
- package/src/client/data/view-models.ts +347 -140
- package/src/client/index.tsx +3 -6
- package/src/client/routes/CostsRoute.tsx +394 -222
- package/src/client/routes/ErrorsRoute.tsx +401 -98
- package/src/client/routes/FrustrationRoute.tsx +924 -0
- package/src/client/routes/GainRoute.tsx +209 -206
- package/src/client/routes/ModelsRoute.tsx +540 -431
- package/src/client/routes/OverviewRoute.tsx +283 -311
- package/src/client/routes/ProjectsRoute.tsx +298 -156
- package/src/client/routes/ProvidersRoute.tsx +948 -416
- package/src/client/routes/RequestsRoute.tsx +295 -107
- package/src/client/routes/ToolsRoute.tsx +495 -398
- package/src/client/routes/TracesRoute.tsx +136 -117
- package/src/client/routes/costs.css +24 -0
- package/src/client/routes/errors.css +60 -0
- package/src/client/routes/frustration.css +123 -0
- package/src/client/routes/index.ts +1 -1
- package/src/client/routes/models.css +43 -0
- package/src/client/routes/projects.css +6 -0
- package/src/client/routes/providers.css +28 -0
- package/src/client/routes/tools.css +14 -0
- package/src/client/styles.css +1396 -1299
- package/src/client/traces/AggregatesPanel.tsx +55 -74
- package/src/client/traces/Minimap.tsx +6 -15
- package/src/client/traces/SpanDrawer.tsx +128 -194
- package/src/client/traces/SummaryStrip.tsx +21 -24
- package/src/client/traces/TimelineCanvas.tsx +27 -44
- package/src/client/traces/TraceView.tsx +175 -130
- package/src/client/traces/TranscriptList.tsx +13 -41
- package/src/client/traces/trace-colors.ts +60 -53
- package/src/client/traces/traces.css +279 -0
- package/src/client/ui/Badge.tsx +29 -0
- package/src/client/ui/Card.tsx +61 -0
- package/src/client/ui/Drawer.tsx +93 -0
- package/src/client/ui/JsonBlock.tsx +26 -44
- package/src/client/ui/Modal.tsx +106 -0
- package/src/client/ui/RequestDrawer.tsx +163 -188
- package/src/client/ui/SearchInput.tsx +25 -0
- package/src/client/ui/Segmented.tsx +61 -0
- package/src/client/ui/Stat.tsx +104 -0
- package/src/client/ui/States.tsx +81 -0
- package/src/client/ui/Table.tsx +229 -0
- package/src/client/ui/index.ts +9 -10
- package/src/client/ui/modal.css +54 -0
- package/src/client/ui/request-drawer.css +40 -0
- package/src/db.ts +379 -983
- package/src/frustration.ts +473 -0
- package/src/index.ts +22 -22
- package/src/live.ts +259 -0
- package/src/parser.ts +55 -66
- package/src/rollup.ts +951 -0
- package/src/server.ts +123 -40
- package/src/shared-types.ts +120 -56
- package/src/trace.ts +14 -7
- package/src/types.ts +4 -0
- package/src/user-metrics.ts +13 -0
- package/dist/client/styles.css +0 -1976
- package/dist/types/client/app/AppLayout.d.ts +0 -16
- package/dist/types/client/app/NavRail.d.ts +0 -7
- package/dist/types/client/app/RangeControl.d.ts +0 -7
- package/dist/types/client/app/SyncButton.d.ts +0 -14
- package/dist/types/client/app/TopBar.d.ts +0 -15
- package/dist/types/client/app/routes.d.ts +0 -12
- package/dist/types/client/components/AgentTokenShare.d.ts +0 -5
- package/dist/types/client/components/chart-shared.d.ts +0 -178
- package/dist/types/client/components/models-table-shared.d.ts +0 -175
- package/dist/types/client/components/range-meta.d.ts +0 -21
- package/dist/types/client/data/charts.d.ts +0 -1
- package/dist/types/client/data/useResource.d.ts +0 -13
- package/dist/types/client/routes/BehaviorRoute.d.ts +0 -7
- package/dist/types/client/ui/AsyncBoundary.d.ts +0 -12
- package/dist/types/client/ui/DataTable.d.ts +0 -17
- package/dist/types/client/ui/EmptyState.d.ts +0 -7
- package/dist/types/client/ui/ErrorState.d.ts +0 -6
- package/dist/types/client/ui/MetricCluster.d.ts +0 -5
- package/dist/types/client/ui/Panel.d.ts +0 -7
- package/dist/types/client/ui/SegmentedControl.d.ts +0 -12
- package/dist/types/client/ui/Skeleton.d.ts +0 -8
- package/dist/types/client/ui/StatusPill.d.ts +0 -7
- package/src/client/app/AppLayout.tsx +0 -93
- package/src/client/app/NavRail.tsx +0 -44
- package/src/client/app/RangeControl.tsx +0 -39
- package/src/client/app/SyncButton.tsx +0 -75
- package/src/client/app/TopBar.tsx +0 -73
- package/src/client/app/routes.ts +0 -93
- package/src/client/components/AgentTokenShare.tsx +0 -68
- package/src/client/components/chart-shared.tsx +0 -273
- package/src/client/components/models-table-shared.tsx +0 -255
- package/src/client/components/range-meta.ts +0 -73
- package/src/client/data/charts.ts +0 -14
- package/src/client/data/useResource.ts +0 -154
- package/src/client/routes/BehaviorRoute.tsx +0 -623
- package/src/client/ui/AsyncBoundary.tsx +0 -54
- package/src/client/ui/DataTable.tsx +0 -122
- package/src/client/ui/EmptyState.tsx +0 -16
- package/src/client/ui/ErrorState.tsx +0 -25
- package/src/client/ui/MetricCluster.tsx +0 -92
- package/src/client/ui/Panel.tsx +0 -24
- package/src/client/ui/SegmentedControl.tsx +0 -36
- package/src/client/ui/Skeleton.tsx +0 -17
- package/src/client/ui/StatusPill.tsx +0 -15
- package/tailwind.config.js +0 -40
|
@@ -1,15 +1,14 @@
|
|
|
1
|
-
import { rangeMeta } from "../components/range-meta";
|
|
2
1
|
import type {
|
|
3
2
|
AgentType,
|
|
4
3
|
AgentTypeStats,
|
|
5
|
-
BehaviorOverallStats,
|
|
6
|
-
BehaviorTimeSeriesPoint,
|
|
7
4
|
CostTimeSeriesPoint,
|
|
8
5
|
FolderStats,
|
|
6
|
+
MessageStats,
|
|
9
7
|
ModelPerformancePoint,
|
|
10
|
-
TimeRange,
|
|
11
8
|
ToolUsageStats,
|
|
12
9
|
} from "../types";
|
|
10
|
+
import { modelKey } from "./colors";
|
|
11
|
+
import { isUnpricedMessage } from "./formatters";
|
|
13
12
|
|
|
14
13
|
/** Fixed display order for the agent-token-share breakdown. */
|
|
15
14
|
const AGENT_TYPE_ORDER: AgentType[] = ["main", "subagent", "advisor"];
|
|
@@ -72,12 +71,39 @@ export function buildAgentTokenShare(stats: AgentTypeStats[]): AgentTokenShareVi
|
|
|
72
71
|
return { totalTokens, totalCost, segments };
|
|
73
72
|
}
|
|
74
73
|
|
|
75
|
-
|
|
74
|
+
/** API-equivalent cost split by billing component. */
|
|
75
|
+
export interface CostComponents {
|
|
76
|
+
costInput: number;
|
|
77
|
+
costOutput: number;
|
|
78
|
+
costCacheRead: number;
|
|
79
|
+
costCacheWrite: number;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** One model's (model + provider) totals across the cost series. */
|
|
83
|
+
export interface CostModelRow extends CostComponents {
|
|
84
|
+
/** `model::provider` (see `modelKey`). */
|
|
85
|
+
key: string;
|
|
86
|
+
model: string;
|
|
87
|
+
provider: string;
|
|
88
|
+
cost: number;
|
|
89
|
+
requests: number;
|
|
90
|
+
unpricedRequests: number;
|
|
91
|
+
/** Fraction (0-1) of the total estimate. */
|
|
92
|
+
share: number;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface CostSummaryView extends CostComponents {
|
|
76
96
|
totalCost: number;
|
|
97
|
+
requests: number;
|
|
77
98
|
unpricedRequests: number;
|
|
99
|
+
/** Day buckets that carry any usage. */
|
|
100
|
+
activeDays: number;
|
|
101
|
+
/** Total estimate ÷ active days. */
|
|
78
102
|
avgDailyCost: number;
|
|
79
|
-
|
|
80
|
-
|
|
103
|
+
/** Ranked by estimate, then requests, then key. */
|
|
104
|
+
models: CostModelRow[];
|
|
105
|
+
/** Highest-estimate model; `null` when nothing is priced. */
|
|
106
|
+
topModel: CostModelRow | null;
|
|
81
107
|
}
|
|
82
108
|
|
|
83
109
|
export interface ModelPerformanceDataPoint {
|
|
@@ -87,176 +113,357 @@ export interface ModelPerformanceDataPoint {
|
|
|
87
113
|
requests: number;
|
|
88
114
|
}
|
|
89
115
|
|
|
90
|
-
export interface ModelPerformanceSeries {
|
|
91
|
-
label: string;
|
|
92
|
-
data: ModelPerformanceDataPoint[];
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
export interface BehaviorSummaryView {
|
|
96
|
-
totalMessages: number;
|
|
97
|
-
totalYelling: number;
|
|
98
|
-
totalProfanity: number;
|
|
99
|
-
totalAnguish: number;
|
|
100
|
-
totalFrustration: number;
|
|
101
|
-
highestFrictionModel: {
|
|
102
|
-
model: string;
|
|
103
|
-
provider: string;
|
|
104
|
-
score: number;
|
|
105
|
-
} | null;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
116
|
export interface FolderRowView extends FolderStats {
|
|
109
|
-
|
|
110
|
-
|
|
117
|
+
/** Uncached input + cache reads + cache writes + output. */
|
|
118
|
+
conversationTokens: number;
|
|
119
|
+
/** Share of every folder's requests in range (0-1). */
|
|
120
|
+
requestShare: number;
|
|
121
|
+
/** Share of every folder's API-equivalent cost in range (0-1). */
|
|
122
|
+
costShare: number;
|
|
123
|
+
/** Session directory under a system temp location (`/tmp`, `/var/folders`, …). */
|
|
124
|
+
temporary: boolean;
|
|
111
125
|
}
|
|
112
126
|
|
|
113
|
-
export
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
127
|
+
export interface FolderTableView {
|
|
128
|
+
rows: FolderRowView[];
|
|
129
|
+
totalRequests: number;
|
|
130
|
+
failedRequests: number;
|
|
131
|
+
totalCost: number;
|
|
132
|
+
unpricedRequests: number;
|
|
133
|
+
conversationTokens: number;
|
|
134
|
+
/** Cache reads ÷ (uncached input + cache reads) across every folder. */
|
|
135
|
+
cacheRate: number;
|
|
136
|
+
/** Largest single-folder request count / cost, for meter scales. */
|
|
137
|
+
maxRequests: number;
|
|
138
|
+
maxCost: number;
|
|
139
|
+
temporaryCount: number;
|
|
140
|
+
}
|
|
118
141
|
|
|
119
|
-
|
|
142
|
+
/** Totals, per-model breakdown and component split for the Costs page. */
|
|
143
|
+
export function buildCostSummary(costSeries: readonly CostTimeSeriesPoint[]): CostSummaryView {
|
|
144
|
+
const summary: CostSummaryView = {
|
|
145
|
+
totalCost: 0,
|
|
146
|
+
requests: 0,
|
|
147
|
+
unpricedRequests: 0,
|
|
148
|
+
activeDays: new Set(costSeries.map(p => p.timestamp)).size,
|
|
149
|
+
avgDailyCost: 0,
|
|
150
|
+
costInput: 0,
|
|
151
|
+
costOutput: 0,
|
|
152
|
+
costCacheRead: 0,
|
|
153
|
+
costCacheWrite: 0,
|
|
154
|
+
models: [],
|
|
155
|
+
topModel: null,
|
|
156
|
+
};
|
|
157
|
+
const byKey = new Map<string, CostModelRow>();
|
|
120
158
|
for (const point of costSeries) {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
159
|
+
const key = modelKey(point.model, point.provider);
|
|
160
|
+
let row = byKey.get(key);
|
|
161
|
+
if (!row) {
|
|
162
|
+
row = {
|
|
163
|
+
key,
|
|
164
|
+
model: point.model,
|
|
165
|
+
provider: point.provider,
|
|
166
|
+
cost: 0,
|
|
167
|
+
requests: 0,
|
|
168
|
+
unpricedRequests: 0,
|
|
169
|
+
costInput: 0,
|
|
170
|
+
costOutput: 0,
|
|
171
|
+
costCacheRead: 0,
|
|
172
|
+
costCacheWrite: 0,
|
|
173
|
+
share: 0,
|
|
174
|
+
};
|
|
175
|
+
byKey.set(key, row);
|
|
130
176
|
}
|
|
177
|
+
addCostPoint(row, point);
|
|
178
|
+
addCostPoint(summary, point);
|
|
179
|
+
row.cost += point.cost;
|
|
180
|
+
summary.totalCost += point.cost;
|
|
131
181
|
}
|
|
132
182
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
183
|
+
summary.avgDailyCost = summary.activeDays > 0 ? summary.totalCost / summary.activeDays : 0;
|
|
184
|
+
summary.models = [...byKey.values()].sort(
|
|
185
|
+
(a, b) => b.cost - a.cost || b.requests - a.requests || a.key.localeCompare(b.key),
|
|
186
|
+
);
|
|
187
|
+
for (const row of summary.models) row.share = summary.totalCost > 0 ? row.cost / summary.totalCost : 0;
|
|
188
|
+
const top = summary.models[0];
|
|
189
|
+
summary.topModel = top !== undefined && top.cost > 0 ? top : null;
|
|
190
|
+
return summary;
|
|
140
191
|
}
|
|
141
192
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
):
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
bucketCount > 0
|
|
154
|
-
? (() => {
|
|
155
|
-
const maxTimestamp = points.reduce((max, point) => Math.max(max, point.timestamp), 0);
|
|
156
|
-
const anchor = maxTimestamp > 0 ? maxTimestamp : Math.floor(Date.now() / bucketMs) * bucketMs;
|
|
157
|
-
const start = anchor - (bucketCount - 1) * bucketMs;
|
|
158
|
-
return Array.from({ length: bucketCount }, (_, index) => start + index * bucketMs);
|
|
159
|
-
})()
|
|
160
|
-
: Array.from(new Set(points.map(p => p.timestamp))).sort((a, b) => a - b);
|
|
161
|
-
const bucketIndex = new Map(buckets.map((timestamp, index) => [timestamp, index]));
|
|
162
|
-
const seriesByKey = new Map<string, ModelPerformanceSeries>();
|
|
193
|
+
function addCostPoint(
|
|
194
|
+
target: CostComponents & { requests: number; unpricedRequests: number },
|
|
195
|
+
point: CostTimeSeriesPoint,
|
|
196
|
+
): void {
|
|
197
|
+
target.costInput += point.costInput;
|
|
198
|
+
target.costOutput += point.costOutput;
|
|
199
|
+
target.costCacheRead += point.costCacheRead;
|
|
200
|
+
target.costCacheWrite += point.costCacheWrite;
|
|
201
|
+
target.requests += point.requests;
|
|
202
|
+
target.unpricedRequests += point.unpricedRequests;
|
|
203
|
+
}
|
|
163
204
|
|
|
205
|
+
/**
|
|
206
|
+
* Per-model (`model::provider`) performance points in ascending time order,
|
|
207
|
+
* with TTFT converted to seconds. Only buckets where the model served
|
|
208
|
+
* requests are present, so sparse history (all time) keeps every point.
|
|
209
|
+
*/
|
|
210
|
+
export function buildModelPerformanceLookup(
|
|
211
|
+
points: readonly ModelPerformancePoint[],
|
|
212
|
+
): Map<string, ModelPerformanceDataPoint[]> {
|
|
213
|
+
const byKey = new Map<string, ModelPerformanceDataPoint[]>();
|
|
164
214
|
for (const point of points) {
|
|
165
|
-
const key =
|
|
166
|
-
let series =
|
|
215
|
+
const key = modelKey(point.model, point.provider);
|
|
216
|
+
let series = byKey.get(key);
|
|
167
217
|
if (!series) {
|
|
168
|
-
series =
|
|
169
|
-
|
|
170
|
-
data: buckets.map(timestamp => ({
|
|
171
|
-
timestamp,
|
|
172
|
-
avgTtftSeconds: null,
|
|
173
|
-
avgTokensPerSecond: null,
|
|
174
|
-
requests: 0,
|
|
175
|
-
})),
|
|
176
|
-
};
|
|
177
|
-
seriesByKey.set(key, series);
|
|
218
|
+
series = [];
|
|
219
|
+
byKey.set(key, series);
|
|
178
220
|
}
|
|
179
|
-
|
|
180
|
-
const index = bucketIndex.get(point.timestamp);
|
|
181
|
-
if (index === undefined) continue;
|
|
182
|
-
|
|
183
|
-
series.data[index] = {
|
|
221
|
+
series.push({
|
|
184
222
|
timestamp: point.timestamp,
|
|
185
223
|
avgTtftSeconds: point.avgTtft !== null ? point.avgTtft / 1000 : null,
|
|
186
224
|
avgTokensPerSecond: point.avgTokensPerSecond,
|
|
187
225
|
requests: point.requests,
|
|
188
|
-
};
|
|
226
|
+
});
|
|
189
227
|
}
|
|
190
|
-
|
|
191
|
-
return
|
|
228
|
+
for (const series of byKey.values()) series.sort((a, b) => a.timestamp - b.timestamp);
|
|
229
|
+
return byKey;
|
|
192
230
|
}
|
|
193
231
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
232
|
+
/**
|
|
233
|
+
* Folder names are session directories with path separators flattened to `-`
|
|
234
|
+
* (`/tmp/x` → `/tmp-x/`, `-tmp-x`), so temp locations are recognized by their prefix.
|
|
235
|
+
*/
|
|
236
|
+
const TEMP_FOLDER_RE = /^\/?-?(?:private-)?(?:tmp|var-folders)(?:[-/]|$)/;
|
|
237
|
+
|
|
238
|
+
/** Table rows plus range totals for the Projects page; linear in the (possibly tens of thousands of) folders. */
|
|
239
|
+
export function buildFolderRows(folders: readonly FolderStats[]): FolderTableView {
|
|
240
|
+
let totalRequests = 0;
|
|
241
|
+
let failedRequests = 0;
|
|
242
|
+
let totalCost = 0;
|
|
243
|
+
let unpricedRequests = 0;
|
|
244
|
+
let conversationTokens = 0;
|
|
245
|
+
let input = 0;
|
|
246
|
+
let cacheRead = 0;
|
|
247
|
+
let maxRequests = 0;
|
|
248
|
+
let maxCost = 0;
|
|
249
|
+
for (const f of folders) {
|
|
250
|
+
totalRequests += f.totalRequests;
|
|
251
|
+
failedRequests += f.failedRequests;
|
|
252
|
+
totalCost += f.totalCost;
|
|
253
|
+
unpricedRequests += f.unpricedRequests;
|
|
254
|
+
conversationTokens += f.totalInputTokens + f.totalCacheReadTokens + f.totalCacheWriteTokens + f.totalOutputTokens;
|
|
255
|
+
input += f.totalInputTokens;
|
|
256
|
+
cacheRead += f.totalCacheReadTokens;
|
|
257
|
+
if (f.totalRequests > maxRequests) maxRequests = f.totalRequests;
|
|
258
|
+
if (f.totalCost > maxCost) maxCost = f.totalCost;
|
|
210
259
|
}
|
|
211
260
|
|
|
212
|
-
let
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
261
|
+
let temporaryCount = 0;
|
|
262
|
+
const rows = folders.map(f => {
|
|
263
|
+
const temporary = TEMP_FOLDER_RE.test(f.folder);
|
|
264
|
+
if (temporary) temporaryCount++;
|
|
265
|
+
return {
|
|
266
|
+
...f,
|
|
267
|
+
conversationTokens:
|
|
268
|
+
f.totalInputTokens + f.totalCacheReadTokens + f.totalCacheWriteTokens + f.totalOutputTokens,
|
|
269
|
+
requestShare: totalRequests > 0 ? f.totalRequests / totalRequests : 0,
|
|
270
|
+
costShare: totalCost > 0 ? f.totalCost / totalCost : 0,
|
|
271
|
+
temporary,
|
|
272
|
+
};
|
|
273
|
+
});
|
|
218
274
|
|
|
219
275
|
return {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
276
|
+
rows,
|
|
277
|
+
totalRequests,
|
|
278
|
+
failedRequests,
|
|
279
|
+
totalCost,
|
|
280
|
+
unpricedRequests,
|
|
281
|
+
conversationTokens,
|
|
282
|
+
cacheRate: input + cacheRead > 0 ? cacheRead / (input + cacheRead) : 0,
|
|
283
|
+
maxRequests,
|
|
284
|
+
maxCost,
|
|
285
|
+
temporaryCount,
|
|
226
286
|
};
|
|
227
287
|
}
|
|
228
288
|
|
|
229
|
-
export function buildFolderRows(folders: FolderStats[]): FolderRowView[] {
|
|
230
|
-
const sorted = [...folders].sort((a, b) => {
|
|
231
|
-
if (b.totalCost !== a.totalCost) {
|
|
232
|
-
return b.totalCost - a.totalCost;
|
|
233
|
-
}
|
|
234
|
-
return b.totalRequests - a.totalRequests;
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
const maxCost = sorted.reduce((max, f) => Math.max(max, f.totalCost), 0);
|
|
238
|
-
const maxRequests = sorted.reduce((max, f) => Math.max(max, f.totalRequests), 0);
|
|
239
|
-
|
|
240
|
-
return sorted.map(f => ({
|
|
241
|
-
...f,
|
|
242
|
-
costPercentage: maxCost > 0 ? (f.totalCost / maxCost) * 100 : 0,
|
|
243
|
-
requestsPercentage: maxRequests > 0 ? (f.totalRequests / maxRequests) * 100 : 0,
|
|
244
|
-
}));
|
|
245
|
-
}
|
|
246
|
-
|
|
247
289
|
/** Table row for the Tools route: usage stats plus derived rates/shares. */
|
|
248
290
|
export interface ToolRowView extends ToolUsageStats {
|
|
249
291
|
/** errors / calls (0 for zero calls). */
|
|
250
292
|
errorRate: number;
|
|
251
|
-
/**
|
|
252
|
-
|
|
293
|
+
/** This tool's fraction (0..1) of all calls in range. */
|
|
294
|
+
callFraction: number;
|
|
295
|
+
/** This tool's fraction (0..1) of all attributed tokens in range. */
|
|
296
|
+
tokenFraction: number;
|
|
297
|
+
/** This tool's fraction (0..1) of all attributed cost in range. */
|
|
298
|
+
costFraction: number;
|
|
299
|
+
/** Mean result characters fed back into context per call. */
|
|
300
|
+
avgResultChars: number;
|
|
253
301
|
}
|
|
254
302
|
|
|
255
303
|
export function buildToolRows(tools: ToolUsageStats[]): ToolRowView[] {
|
|
256
|
-
|
|
304
|
+
let calls = 0;
|
|
305
|
+
let tokens = 0;
|
|
306
|
+
let cost = 0;
|
|
307
|
+
for (const t of tools) {
|
|
308
|
+
calls += t.calls;
|
|
309
|
+
tokens += t.totalTokensShare;
|
|
310
|
+
cost += t.costShare;
|
|
311
|
+
}
|
|
257
312
|
return tools.map(t => ({
|
|
258
313
|
...t,
|
|
259
314
|
errorRate: t.calls > 0 ? t.errors / t.calls : 0,
|
|
260
|
-
|
|
315
|
+
callFraction: calls > 0 ? t.calls / calls : 0,
|
|
316
|
+
tokenFraction: tokens > 0 ? t.totalTokensShare / tokens : 0,
|
|
317
|
+
costFraction: cost > 0 ? t.costShare / cost : 0,
|
|
318
|
+
avgResultChars: t.calls > 0 ? t.resultChars / t.calls : 0,
|
|
261
319
|
}));
|
|
262
320
|
}
|
|
321
|
+
|
|
322
|
+
export type RequestStatus = "ok" | "aborted" | "failed";
|
|
323
|
+
|
|
324
|
+
/** Outcome of one request: cancellations abort; provider/transport errors fail. */
|
|
325
|
+
export function requestStatus(row: Pick<MessageStats, "stopReason" | "errorMessage">): RequestStatus {
|
|
326
|
+
if (row.stopReason === "aborted") return "aborted";
|
|
327
|
+
if (row.stopReason === "error" || row.errorMessage) return "failed";
|
|
328
|
+
return "ok";
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
export interface RequestLogSummary {
|
|
332
|
+
requests: number;
|
|
333
|
+
failed: number;
|
|
334
|
+
aborted: number;
|
|
335
|
+
tokens: number;
|
|
336
|
+
cost: number;
|
|
337
|
+
/** Requests with tokens but no price (see `isUnpricedMessage`). */
|
|
338
|
+
unpriced: number;
|
|
339
|
+
medianDuration: number | null;
|
|
340
|
+
p95Duration: number | null;
|
|
341
|
+
medianTtft: number | null;
|
|
342
|
+
oldest: number | null;
|
|
343
|
+
newest: number | null;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/** Nearest-rank quantile of an ascending list; `null` when empty. */
|
|
347
|
+
function quantile(sorted: readonly number[], q: number): number | null {
|
|
348
|
+
if (sorted.length === 0) return null;
|
|
349
|
+
return sorted[Math.min(sorted.length - 1, Math.max(0, Math.ceil(q * sorted.length) - 1))];
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/** Totals and latency quantiles over a list of requests. */
|
|
353
|
+
export function summarizeRequests(rows: readonly MessageStats[]): RequestLogSummary {
|
|
354
|
+
let failed = 0;
|
|
355
|
+
let aborted = 0;
|
|
356
|
+
let tokens = 0;
|
|
357
|
+
let cost = 0;
|
|
358
|
+
let unpriced = 0;
|
|
359
|
+
let oldest: number | null = null;
|
|
360
|
+
let newest: number | null = null;
|
|
361
|
+
const durations: number[] = [];
|
|
362
|
+
const ttfts: number[] = [];
|
|
363
|
+
for (const row of rows) {
|
|
364
|
+
const status = requestStatus(row);
|
|
365
|
+
if (status === "failed") failed++;
|
|
366
|
+
else if (status === "aborted") aborted++;
|
|
367
|
+
tokens += row.usage.totalTokens;
|
|
368
|
+
cost += row.usage.cost.total;
|
|
369
|
+
if (isUnpricedMessage(row)) unpriced++;
|
|
370
|
+
if (row.duration !== null) durations.push(row.duration);
|
|
371
|
+
if (row.ttft !== null) ttfts.push(row.ttft);
|
|
372
|
+
if (oldest === null || row.timestamp < oldest) oldest = row.timestamp;
|
|
373
|
+
if (newest === null || row.timestamp > newest) newest = row.timestamp;
|
|
374
|
+
}
|
|
375
|
+
durations.sort((a, b) => a - b);
|
|
376
|
+
ttfts.sort((a, b) => a - b);
|
|
377
|
+
return {
|
|
378
|
+
requests: rows.length,
|
|
379
|
+
failed,
|
|
380
|
+
aborted,
|
|
381
|
+
tokens,
|
|
382
|
+
cost,
|
|
383
|
+
unpriced,
|
|
384
|
+
medianDuration: quantile(durations, 0.5),
|
|
385
|
+
p95Duration: quantile(durations, 0.95),
|
|
386
|
+
medianTtft: quantile(ttfts, 0.5),
|
|
387
|
+
oldest,
|
|
388
|
+
newest,
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const SIGNATURE_MAX = 180;
|
|
393
|
+
const UUID_RE = /\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/gi;
|
|
394
|
+
const PREFIXED_ID_RE = /\b(?:req|msg|call|toolu|chatcmpl|resp|run|gen)[-_][A-Za-z0-9_-]{6,}/g;
|
|
395
|
+
const HEX_RE = /\b[0-9a-f]{12,}\b/gi;
|
|
396
|
+
/** Free-standing numbers; digits inside identifiers (`gpt-5.2`) are kept. */
|
|
397
|
+
const NUMBER_RE = /(?<![\w.-])\d+(?:\.\d+)?/g;
|
|
398
|
+
const HTTP_STATUS_RE = /^[1-5]\d\d$/;
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Normalized error message used to group failures: whitespace collapsed,
|
|
402
|
+
* request ids / hashes / counters replaced with placeholders (HTTP status
|
|
403
|
+
* codes kept), truncated.
|
|
404
|
+
*/
|
|
405
|
+
export function errorSignature(message: string | null): string {
|
|
406
|
+
if (!message?.trim()) return "Unknown error";
|
|
407
|
+
const normalized = message
|
|
408
|
+
.replace(/\s+/g, " ")
|
|
409
|
+
.trim()
|
|
410
|
+
.replace(UUID_RE, "<id>")
|
|
411
|
+
.replace(PREFIXED_ID_RE, "<id>")
|
|
412
|
+
.replace(HEX_RE, "<hex>")
|
|
413
|
+
.replace(NUMBER_RE, n => (HTTP_STATUS_RE.test(n) ? n : "N"));
|
|
414
|
+
return normalized.length > SIGNATURE_MAX ? `${normalized.slice(0, SIGNATURE_MAX - 1)}…` : normalized;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
export interface ErrorGroupModel {
|
|
418
|
+
model: string;
|
|
419
|
+
provider: string;
|
|
420
|
+
count: number;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
export interface ErrorGroupView {
|
|
424
|
+
signature: string;
|
|
425
|
+
count: number;
|
|
426
|
+
firstSeen: number;
|
|
427
|
+
lastSeen: number;
|
|
428
|
+
/** Most recent occurrence (full message, drawer target). */
|
|
429
|
+
latest: MessageStats;
|
|
430
|
+
/** Affected models, most failures first. */
|
|
431
|
+
models: ErrorGroupModel[];
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/** Group failures by `errorSignature`; most frequent first, then most recent. */
|
|
435
|
+
export function groupErrorsBySignature(rows: readonly MessageStats[]): ErrorGroupView[] {
|
|
436
|
+
const groups = new Map<string, ErrorGroupView & { byModel: Map<string, ErrorGroupModel> }>();
|
|
437
|
+
for (const row of rows) {
|
|
438
|
+
const signature = errorSignature(row.errorMessage);
|
|
439
|
+
let group = groups.get(signature);
|
|
440
|
+
if (!group) {
|
|
441
|
+
group = {
|
|
442
|
+
signature,
|
|
443
|
+
count: 0,
|
|
444
|
+
firstSeen: row.timestamp,
|
|
445
|
+
lastSeen: row.timestamp,
|
|
446
|
+
latest: row,
|
|
447
|
+
models: [],
|
|
448
|
+
byModel: new Map(),
|
|
449
|
+
};
|
|
450
|
+
groups.set(signature, group);
|
|
451
|
+
}
|
|
452
|
+
group.count++;
|
|
453
|
+
if (row.timestamp < group.firstSeen) group.firstSeen = row.timestamp;
|
|
454
|
+
if (row.timestamp > group.lastSeen) {
|
|
455
|
+
group.lastSeen = row.timestamp;
|
|
456
|
+
group.latest = row;
|
|
457
|
+
}
|
|
458
|
+
const key = modelKey(row.model, row.provider);
|
|
459
|
+
const model = group.byModel.get(key);
|
|
460
|
+
if (model) model.count++;
|
|
461
|
+
else group.byModel.set(key, { model: row.model, provider: row.provider, count: 1 });
|
|
462
|
+
}
|
|
463
|
+
return [...groups.values()]
|
|
464
|
+
.map(({ byModel, ...group }) => ({
|
|
465
|
+
...group,
|
|
466
|
+
models: [...byModel.values()].sort((a, b) => b.count - a.count || a.model.localeCompare(b.model)),
|
|
467
|
+
}))
|
|
468
|
+
.sort((a, b) => b.count - a.count || b.lastSeen - a.lastSeen);
|
|
469
|
+
}
|
package/src/client/index.tsx
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
import "./styles.css";
|
|
2
|
-
// Side-effect import: registers all Chart.js scales/elements/plugins exactly
|
|
3
|
-
// once before any chart renders. Without it the first chart throws
|
|
4
|
-
// "category is not a registered scale" and unmounts the whole app.
|
|
5
|
-
import "./data/charts";
|
|
6
2
|
import { createRoot } from "react-dom/client";
|
|
7
3
|
import App from "./App";
|
|
8
4
|
|
|
9
|
-
const root =
|
|
10
|
-
root
|
|
5
|
+
const root = document.getElementById("root");
|
|
6
|
+
if (!root) throw new Error("missing #root");
|
|
7
|
+
createRoot(root).render(<App />);
|