@oh-my-pi/omp-stats 15.0.0 → 15.0.2
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/package.json +6 -6
- package/src/client/components/BehaviorChart.tsx +85 -270
- package/src/client/components/BehaviorModelsTable.tsx +151 -274
- package/src/client/components/CostChart.tsx +85 -246
- package/src/client/components/ModelsTable.tsx +130 -246
- package/src/client/components/RequestDetail.tsx +0 -2
- package/src/client/components/chart-shared.tsx +320 -0
- package/src/client/components/models-table-shared.tsx +275 -0
- package/src/client/types.ts +21 -121
- package/src/db.ts +41 -1
- package/src/parser.ts +44 -5
- package/src/shared-types.ts +204 -0
- package/src/types.ts +16 -201
package/src/client/types.ts
CHANGED
|
@@ -1,8 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Client-side type definitions.
|
|
3
|
-
*
|
|
3
|
+
*
|
|
4
|
+
* Shared shapes (aggregations, time-series, dashboard payloads) live in
|
|
5
|
+
* `../shared-types` and are re-exported here. The types declared inline below
|
|
6
|
+
* are deliberately client-only because:
|
|
7
|
+
* - `Usage` is redeclared locally so the client bundle avoids importing
|
|
8
|
+
* `@oh-my-pi/pi-ai` (the server-side AI types package).
|
|
9
|
+
* - `MessageStats.stopReason` is widened from the server's `StopReason`
|
|
10
|
+
* enum to `string`, again to keep the client free of pi-ai types.
|
|
11
|
+
* - `TimeRange`, `OverviewStats`, `ModelDashboardStats`,
|
|
12
|
+
* `CostDashboardStats` are UI-only view shapes the server never produces.
|
|
4
13
|
*/
|
|
5
14
|
|
|
15
|
+
import type {
|
|
16
|
+
AggregatedStats,
|
|
17
|
+
CostTimeSeriesPoint,
|
|
18
|
+
ModelPerformancePoint,
|
|
19
|
+
ModelStats,
|
|
20
|
+
ModelTimeSeriesPoint,
|
|
21
|
+
TimeSeriesPoint,
|
|
22
|
+
} from "../shared-types";
|
|
23
|
+
|
|
24
|
+
export * from "../shared-types";
|
|
25
|
+
|
|
6
26
|
export interface Usage {
|
|
7
27
|
input: number;
|
|
8
28
|
output: number;
|
|
@@ -40,80 +60,7 @@ export interface RequestDetails extends MessageStats {
|
|
|
40
60
|
output: unknown;
|
|
41
61
|
}
|
|
42
62
|
|
|
43
|
-
export interface AggregatedStats {
|
|
44
|
-
totalRequests: number;
|
|
45
|
-
successfulRequests: number;
|
|
46
|
-
failedRequests: number;
|
|
47
|
-
errorRate: number;
|
|
48
|
-
totalInputTokens: number;
|
|
49
|
-
totalOutputTokens: number;
|
|
50
|
-
totalCacheReadTokens: number;
|
|
51
|
-
totalCacheWriteTokens: number;
|
|
52
|
-
cacheRate: number;
|
|
53
|
-
totalCost: number;
|
|
54
|
-
totalPremiumRequests: number;
|
|
55
|
-
avgDuration: number | null;
|
|
56
|
-
avgTtft: number | null;
|
|
57
|
-
avgTokensPerSecond: number | null;
|
|
58
|
-
firstTimestamp: number;
|
|
59
|
-
lastTimestamp: number;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
63
|
export type TimeRange = "1h" | "24h" | "7d" | "30d" | "90d" | "all";
|
|
63
|
-
export interface ModelStats extends AggregatedStats {
|
|
64
|
-
model: string;
|
|
65
|
-
provider: string;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export interface FolderStats extends AggregatedStats {
|
|
69
|
-
folder: string;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export interface TimeSeriesPoint {
|
|
73
|
-
timestamp: number;
|
|
74
|
-
requests: number;
|
|
75
|
-
errors: number;
|
|
76
|
-
tokens: number;
|
|
77
|
-
cost: number;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
export interface ModelTimeSeriesPoint {
|
|
81
|
-
timestamp: number;
|
|
82
|
-
model: string;
|
|
83
|
-
provider: string;
|
|
84
|
-
requests: number;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
export interface ModelPerformancePoint {
|
|
88
|
-
timestamp: number;
|
|
89
|
-
model: string;
|
|
90
|
-
provider: string;
|
|
91
|
-
requests: number;
|
|
92
|
-
avgTtft: number | null;
|
|
93
|
-
avgTokensPerSecond: number | null;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
export interface CostTimeSeriesPoint {
|
|
97
|
-
timestamp: number;
|
|
98
|
-
model: string;
|
|
99
|
-
provider: string;
|
|
100
|
-
cost: number;
|
|
101
|
-
costInput: number;
|
|
102
|
-
costOutput: number;
|
|
103
|
-
costCacheRead: number;
|
|
104
|
-
costCacheWrite: number;
|
|
105
|
-
requests: number;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
export interface DashboardStats {
|
|
109
|
-
overall: AggregatedStats;
|
|
110
|
-
byModel: ModelStats[];
|
|
111
|
-
byFolder: FolderStats[];
|
|
112
|
-
timeSeries: TimeSeriesPoint[];
|
|
113
|
-
modelSeries: ModelTimeSeriesPoint[];
|
|
114
|
-
modelPerformanceSeries: ModelPerformancePoint[];
|
|
115
|
-
costSeries: CostTimeSeriesPoint[];
|
|
116
|
-
}
|
|
117
64
|
|
|
118
65
|
export interface OverviewStats {
|
|
119
66
|
overall: AggregatedStats;
|
|
@@ -129,50 +76,3 @@ export interface ModelDashboardStats {
|
|
|
129
76
|
export interface CostDashboardStats {
|
|
130
77
|
costSeries: CostTimeSeriesPoint[];
|
|
131
78
|
}
|
|
132
|
-
|
|
133
|
-
export interface BehaviorTimeSeriesPoint {
|
|
134
|
-
timestamp: number;
|
|
135
|
-
model: string;
|
|
136
|
-
provider: string;
|
|
137
|
-
messages: number;
|
|
138
|
-
yelling: number;
|
|
139
|
-
profanity: number;
|
|
140
|
-
anguish: number;
|
|
141
|
-
negation: number;
|
|
142
|
-
repetition: number;
|
|
143
|
-
blame: number;
|
|
144
|
-
chars: number;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
export interface BehaviorOverallStats {
|
|
148
|
-
totalMessages: number;
|
|
149
|
-
totalYelling: number;
|
|
150
|
-
totalProfanity: number;
|
|
151
|
-
totalAnguish: number;
|
|
152
|
-
totalNegation: number;
|
|
153
|
-
totalRepetition: number;
|
|
154
|
-
totalBlame: number;
|
|
155
|
-
totalChars: number;
|
|
156
|
-
firstTimestamp: number;
|
|
157
|
-
lastTimestamp: number;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
export interface BehaviorModelStats {
|
|
161
|
-
model: string;
|
|
162
|
-
provider: string;
|
|
163
|
-
totalMessages: number;
|
|
164
|
-
totalYelling: number;
|
|
165
|
-
totalProfanity: number;
|
|
166
|
-
totalAnguish: number;
|
|
167
|
-
totalNegation: number;
|
|
168
|
-
totalRepetition: number;
|
|
169
|
-
totalBlame: number;
|
|
170
|
-
totalChars: number;
|
|
171
|
-
lastTimestamp: number;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
export interface BehaviorDashboardStats {
|
|
175
|
-
overall: BehaviorOverallStats;
|
|
176
|
-
byModel: BehaviorModelStats[];
|
|
177
|
-
behaviorSeries: BehaviorTimeSeriesPoint[];
|
|
178
|
-
}
|
package/src/db.ts
CHANGED
|
@@ -38,6 +38,7 @@ const BACKFILL_COMPLETE = "complete";
|
|
|
38
38
|
const BACKFILL_PENDING = "pending";
|
|
39
39
|
const USER_MESSAGES_BACKFILL_KEY = "user_messages_v5";
|
|
40
40
|
const USER_MESSAGE_LINKS_REPAIR_KEY = "user_message_links_v1";
|
|
41
|
+
const PRIORITY_PREMIUM_REQUESTS_BACKFILL_KEY = "premium_requests_priority_v1";
|
|
41
42
|
function shouldResetBackfill(value: string | undefined): boolean {
|
|
42
43
|
return value !== BACKFILL_COMPLETE && value !== BACKFILL_PENDING;
|
|
43
44
|
}
|
|
@@ -179,6 +180,7 @@ export async function initDb(): Promise<Database> {
|
|
|
179
180
|
}
|
|
180
181
|
backfillUserMessages(db);
|
|
181
182
|
repairUserMessageLinks(db);
|
|
183
|
+
backfillPriorityPremiumRequests(db);
|
|
182
184
|
backfillMissingCatalogCosts(db);
|
|
183
185
|
return db;
|
|
184
186
|
}
|
|
@@ -300,13 +302,21 @@ export function setFileOffset(sessionFile: string, offset: number, lastModified:
|
|
|
300
302
|
export function insertMessageStats(stats: MessageStats[]): number {
|
|
301
303
|
if (!db || stats.length === 0) return 0;
|
|
302
304
|
|
|
305
|
+
// Use UPSERT so a re-sync can fix up `premium_requests` for rows persisted
|
|
306
|
+
// before priority service-tier traffic was counted as premium. The guard
|
|
307
|
+
// `WHERE messages.premium_requests < excluded.premium_requests` keeps every
|
|
308
|
+
// other column immutable and never demotes an existing count (e.g. when a
|
|
309
|
+
// later parse drops back to 0 for the same row).
|
|
303
310
|
const stmt = db.prepare(`
|
|
304
|
-
INSERT
|
|
311
|
+
INSERT INTO messages (
|
|
305
312
|
session_file, entry_id, folder, model, provider, api, timestamp,
|
|
306
313
|
duration, ttft, stop_reason, error_message,
|
|
307
314
|
input_tokens, output_tokens, cache_read_tokens, cache_write_tokens, total_tokens, premium_requests,
|
|
308
315
|
cost_input, cost_output, cost_cache_read, cost_cache_write, cost_total
|
|
309
316
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
317
|
+
ON CONFLICT(session_file, entry_id) DO UPDATE SET
|
|
318
|
+
premium_requests = excluded.premium_requests
|
|
319
|
+
WHERE messages.premium_requests < excluded.premium_requests
|
|
310
320
|
`);
|
|
311
321
|
|
|
312
322
|
let inserted = 0;
|
|
@@ -780,6 +790,36 @@ function repairUserMessageLinks(database: Database): void {
|
|
|
780
790
|
.run(USER_MESSAGE_LINKS_REPAIR_KEY, BACKFILL_PENDING);
|
|
781
791
|
}
|
|
782
792
|
|
|
793
|
+
/**
|
|
794
|
+
* One-shot wipe of `file_offsets` so the next sync re-parses every session
|
|
795
|
+
* and re-derives `premium_requests` from recorded `service_tier_change`
|
|
796
|
+
* entries. Earlier ingestions captured priority OpenAI traffic with
|
|
797
|
+
* `premium_requests = 0` because the AI layer only set the field for GitHub
|
|
798
|
+
* Copilot traffic. The parser now folds priority requests into the same
|
|
799
|
+
* counter; combined with the UPSERT in `insertMessageStats`, a single sync
|
|
800
|
+
* pass brings the messages table up to date without touching any other
|
|
801
|
+
* column. Idempotent: gated by a sentinel row in `meta`.
|
|
802
|
+
*/
|
|
803
|
+
function backfillPriorityPremiumRequests(database: Database): void {
|
|
804
|
+
const row = database.prepare("SELECT value FROM meta WHERE key = ?").get(PRIORITY_PREMIUM_REQUESTS_BACKFILL_KEY) as
|
|
805
|
+
| { value: string }
|
|
806
|
+
| undefined;
|
|
807
|
+
if (!shouldResetBackfill(row?.value)) return;
|
|
808
|
+
|
|
809
|
+
database.exec("DELETE FROM file_offsets");
|
|
810
|
+
database
|
|
811
|
+
.prepare("INSERT OR REPLACE INTO meta (key, value) VALUES (?, ?)")
|
|
812
|
+
.run(PRIORITY_PREMIUM_REQUESTS_BACKFILL_KEY, BACKFILL_PENDING);
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
export function markPriorityPremiumRequestsBackfillComplete(): void {
|
|
816
|
+
if (!db) return;
|
|
817
|
+
db.prepare("INSERT OR REPLACE INTO meta (key, value) VALUES (?, ?)").run(
|
|
818
|
+
PRIORITY_PREMIUM_REQUESTS_BACKFILL_KEY,
|
|
819
|
+
BACKFILL_COMPLETE,
|
|
820
|
+
);
|
|
821
|
+
}
|
|
822
|
+
|
|
783
823
|
export function markUserMessagesBackfillComplete(): void {
|
|
784
824
|
if (!db) return;
|
|
785
825
|
db.prepare("INSERT OR REPLACE INTO meta (key, value) VALUES (?, ?)").run(
|
package/src/parser.ts
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
import * as fs from "node:fs/promises";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import type
|
|
3
|
+
import { type AssistantMessage, getPriorityPremiumRequests, type ServiceTier } from "@oh-my-pi/pi-ai";
|
|
4
4
|
import { getSessionsDir, isEnoent } from "@oh-my-pi/pi-utils";
|
|
5
|
-
import type {
|
|
5
|
+
import type {
|
|
6
|
+
MessageStats,
|
|
7
|
+
SessionEntry,
|
|
8
|
+
SessionMessageEntry,
|
|
9
|
+
SessionServiceTierChangeEntry,
|
|
10
|
+
UserMessageLink,
|
|
11
|
+
UserMessageStats,
|
|
12
|
+
} from "./types";
|
|
6
13
|
import { computeUserMessageMetrics } from "./user-metrics";
|
|
7
14
|
|
|
8
15
|
/**
|
|
@@ -24,6 +31,10 @@ function extractFolderFromPath(sessionPath: string): string {
|
|
|
24
31
|
function isAssistantMessage(entry: SessionEntry): entry is SessionMessageEntry {
|
|
25
32
|
if (entry.type !== "message") return false;
|
|
26
33
|
const msgEntry = entry as SessionMessageEntry;
|
|
34
|
+
// Legacy sessions (pre-id tracking) recorded message entries without an `id`.
|
|
35
|
+
// They're not linkable and would violate the messages.entry_id NOT NULL
|
|
36
|
+
// constraint, so skip them at the parser boundary.
|
|
37
|
+
if (typeof msgEntry.id !== "string" || msgEntry.id.length === 0) return false;
|
|
27
38
|
return msgEntry.message?.role === "assistant";
|
|
28
39
|
}
|
|
29
40
|
|
|
@@ -33,9 +44,17 @@ function isAssistantMessage(entry: SessionEntry): entry is SessionMessageEntry {
|
|
|
33
44
|
function isUserMessage(entry: SessionEntry): entry is SessionMessageEntry {
|
|
34
45
|
if (entry.type !== "message") return false;
|
|
35
46
|
const msgEntry = entry as SessionMessageEntry;
|
|
47
|
+
if (typeof msgEntry.id !== "string" || msgEntry.id.length === 0) return false;
|
|
36
48
|
return msgEntry.message?.role === "user";
|
|
37
49
|
}
|
|
38
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Check if an entry is a service-tier change.
|
|
53
|
+
*/
|
|
54
|
+
function isServiceTierChange(entry: SessionEntry): entry is SessionServiceTierChangeEntry {
|
|
55
|
+
return entry.type === "service_tier_change";
|
|
56
|
+
}
|
|
57
|
+
|
|
39
58
|
/**
|
|
40
59
|
* Extract plain text from a user message content payload.
|
|
41
60
|
*/
|
|
@@ -83,10 +102,25 @@ function extractUserStats(sessionFile: string, folder: string, entry: SessionMes
|
|
|
83
102
|
/**
|
|
84
103
|
* Extract stats from an assistant message entry.
|
|
85
104
|
*/
|
|
86
|
-
function extractStats(
|
|
105
|
+
function extractStats(
|
|
106
|
+
sessionFile: string,
|
|
107
|
+
folder: string,
|
|
108
|
+
entry: SessionMessageEntry,
|
|
109
|
+
currentServiceTier: ServiceTier | undefined,
|
|
110
|
+
): MessageStats | null {
|
|
87
111
|
const msg = entry.message as AssistantMessage;
|
|
88
112
|
if (!msg || msg.role !== "assistant") return null;
|
|
89
113
|
|
|
114
|
+
// Backfill: when the session recorded `priority` as the active service tier
|
|
115
|
+
// at this point but the AI usage payload was captured before priority
|
|
116
|
+
// requests were folded into `premiumRequests`, derive the count here so the
|
|
117
|
+
// "Premium Reqs" stat aggregates priority traffic on re-sync. Trust any
|
|
118
|
+
// non-zero value already in `usage.premiumRequests` (Copilot multipliers or
|
|
119
|
+
// the new AI code path) and only synthesise when the field is missing/zero.
|
|
120
|
+
const recorded = msg.usage.premiumRequests ?? 0;
|
|
121
|
+
const derived = recorded > 0 ? recorded : getPriorityPremiumRequests(currentServiceTier, msg.provider);
|
|
122
|
+
const usage = derived === recorded ? msg.usage : { ...msg.usage, premiumRequests: derived };
|
|
123
|
+
|
|
90
124
|
return {
|
|
91
125
|
sessionFile,
|
|
92
126
|
entryId: entry.id,
|
|
@@ -99,7 +133,7 @@ function extractStats(sessionFile: string, folder: string, entry: SessionMessage
|
|
|
99
133
|
ttft: msg.ttft ?? null,
|
|
100
134
|
stopReason: msg.stopReason,
|
|
101
135
|
errorMessage: msg.errorMessage ?? null,
|
|
102
|
-
usage
|
|
136
|
+
usage,
|
|
103
137
|
};
|
|
104
138
|
}
|
|
105
139
|
|
|
@@ -158,7 +192,12 @@ export async function parseSessionFile(sessionPath: string, fromOffset = 0): Pro
|
|
|
158
192
|
const start = Math.max(0, Math.min(fromOffset, bytes.length));
|
|
159
193
|
const unprocessed = bytes.subarray(start);
|
|
160
194
|
const { entries, read } = parseSessionEntriesLenient(unprocessed);
|
|
195
|
+
let currentServiceTier: ServiceTier | undefined;
|
|
161
196
|
for (const entry of entries) {
|
|
197
|
+
if (isServiceTierChange(entry)) {
|
|
198
|
+
currentServiceTier = entry.serviceTier ?? undefined;
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
162
201
|
if (isUserMessage(entry)) {
|
|
163
202
|
const userMsg = extractUserStats(sessionPath, folder, entry);
|
|
164
203
|
if (userMsg) {
|
|
@@ -168,7 +207,7 @@ export async function parseSessionFile(sessionPath: string, fromOffset = 0): Pro
|
|
|
168
207
|
continue;
|
|
169
208
|
}
|
|
170
209
|
if (isAssistantMessage(entry)) {
|
|
171
|
-
const msgStats = extractStats(sessionPath, folder, entry);
|
|
210
|
+
const msgStats = extractStats(sessionPath, folder, entry, currentServiceTier);
|
|
172
211
|
if (msgStats) stats.push(msgStats);
|
|
173
212
|
// Link assistant's responding model back to the user message it answered.
|
|
174
213
|
const parentId = (entry as SessionMessageEntry).parentId;
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared type definitions consumed by both the server-side stats code and the
|
|
3
|
+
* standalone client bundle. Keep this file free of any imports from server-only
|
|
4
|
+
* packages (e.g. `@oh-my-pi/pi-ai`, `bun:sqlite`) so the client can import it
|
|
5
|
+
* without dragging server dependencies into its bundle.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Aggregated stats for a model or folder.
|
|
10
|
+
*/
|
|
11
|
+
export interface AggregatedStats {
|
|
12
|
+
/** Total number of requests */
|
|
13
|
+
totalRequests: number;
|
|
14
|
+
/** Number of successful requests */
|
|
15
|
+
successfulRequests: number;
|
|
16
|
+
/** Number of failed requests */
|
|
17
|
+
failedRequests: number;
|
|
18
|
+
/** Error rate (0-1) */
|
|
19
|
+
errorRate: number;
|
|
20
|
+
/** Total input tokens */
|
|
21
|
+
totalInputTokens: number;
|
|
22
|
+
/** Total output tokens */
|
|
23
|
+
totalOutputTokens: number;
|
|
24
|
+
/** Total cache read tokens */
|
|
25
|
+
totalCacheReadTokens: number;
|
|
26
|
+
/** Total cache write tokens */
|
|
27
|
+
totalCacheWriteTokens: number;
|
|
28
|
+
/** Cache hit rate (0-1) */
|
|
29
|
+
cacheRate: number;
|
|
30
|
+
/** Total cost */
|
|
31
|
+
totalCost: number;
|
|
32
|
+
/** Total premium requests */
|
|
33
|
+
totalPremiumRequests: number;
|
|
34
|
+
/** Average duration in ms */
|
|
35
|
+
avgDuration: number | null;
|
|
36
|
+
/** Average TTFT in ms */
|
|
37
|
+
avgTtft: number | null;
|
|
38
|
+
/** Average tokens per second (output tokens / duration) */
|
|
39
|
+
avgTokensPerSecond: number | null;
|
|
40
|
+
/** Time range */
|
|
41
|
+
firstTimestamp: number;
|
|
42
|
+
lastTimestamp: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Stats grouped by model.
|
|
47
|
+
*/
|
|
48
|
+
export interface ModelStats extends AggregatedStats {
|
|
49
|
+
model: string;
|
|
50
|
+
provider: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Stats grouped by folder.
|
|
55
|
+
*/
|
|
56
|
+
export interface FolderStats extends AggregatedStats {
|
|
57
|
+
folder: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Time series data point.
|
|
62
|
+
*/
|
|
63
|
+
export interface TimeSeriesPoint {
|
|
64
|
+
/** Bucket timestamp (start of hour/day) */
|
|
65
|
+
timestamp: number;
|
|
66
|
+
/** Request count */
|
|
67
|
+
requests: number;
|
|
68
|
+
/** Error count */
|
|
69
|
+
errors: number;
|
|
70
|
+
/** Total tokens */
|
|
71
|
+
tokens: number;
|
|
72
|
+
/** Total cost */
|
|
73
|
+
cost: number;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Model usage time series data point (daily buckets).
|
|
78
|
+
*/
|
|
79
|
+
export interface ModelTimeSeriesPoint {
|
|
80
|
+
/** Bucket timestamp (start of day) */
|
|
81
|
+
timestamp: number;
|
|
82
|
+
/** Model name */
|
|
83
|
+
model: string;
|
|
84
|
+
/** Provider name */
|
|
85
|
+
provider: string;
|
|
86
|
+
/** Request count */
|
|
87
|
+
requests: number;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Model performance time series data point (daily buckets).
|
|
92
|
+
*/
|
|
93
|
+
export interface ModelPerformancePoint {
|
|
94
|
+
/** Bucket timestamp (start of day) */
|
|
95
|
+
timestamp: number;
|
|
96
|
+
/** Model name */
|
|
97
|
+
model: string;
|
|
98
|
+
/** Provider name */
|
|
99
|
+
provider: string;
|
|
100
|
+
/** Request count */
|
|
101
|
+
requests: number;
|
|
102
|
+
/** Average TTFT in ms */
|
|
103
|
+
avgTtft: number | null;
|
|
104
|
+
/** Average tokens per second */
|
|
105
|
+
avgTokensPerSecond: number | null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Cost time series data point (daily buckets).
|
|
110
|
+
*/
|
|
111
|
+
export interface CostTimeSeriesPoint {
|
|
112
|
+
/** Bucket timestamp (start of day) */
|
|
113
|
+
timestamp: number;
|
|
114
|
+
/** Model name */
|
|
115
|
+
model: string;
|
|
116
|
+
/** Provider name */
|
|
117
|
+
provider: string;
|
|
118
|
+
/** Total cost for this bucket */
|
|
119
|
+
cost: number;
|
|
120
|
+
/** Cost breakdown */
|
|
121
|
+
costInput: number;
|
|
122
|
+
costOutput: number;
|
|
123
|
+
costCacheRead: number;
|
|
124
|
+
costCacheWrite: number;
|
|
125
|
+
/** Request count */
|
|
126
|
+
requests: number;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Overall dashboard stats.
|
|
131
|
+
*/
|
|
132
|
+
export interface DashboardStats {
|
|
133
|
+
overall: AggregatedStats;
|
|
134
|
+
byModel: ModelStats[];
|
|
135
|
+
byFolder: FolderStats[];
|
|
136
|
+
timeSeries: TimeSeriesPoint[];
|
|
137
|
+
modelSeries: ModelTimeSeriesPoint[];
|
|
138
|
+
modelPerformanceSeries: ModelPerformancePoint[];
|
|
139
|
+
costSeries: CostTimeSeriesPoint[];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Behavior time-series point (daily bucket, per responding model).
|
|
144
|
+
*/
|
|
145
|
+
export interface BehaviorTimeSeriesPoint {
|
|
146
|
+
/** Bucket timestamp (start of day) */
|
|
147
|
+
timestamp: number;
|
|
148
|
+
/** Responding model ("unknown" if user msg never got a reply) */
|
|
149
|
+
model: string;
|
|
150
|
+
/** Responding provider */
|
|
151
|
+
provider: string;
|
|
152
|
+
/** Number of user messages in bucket */
|
|
153
|
+
messages: number;
|
|
154
|
+
/** Total yelling sentences in bucket */
|
|
155
|
+
yelling: number;
|
|
156
|
+
/** Total profanity hits in bucket */
|
|
157
|
+
profanity: number;
|
|
158
|
+
/** Total anguish signal in bucket */
|
|
159
|
+
anguish: number;
|
|
160
|
+
/** Total corrective-negation hits in bucket */
|
|
161
|
+
negation: number;
|
|
162
|
+
/** Total user-repeating-themselves hits in bucket */
|
|
163
|
+
repetition: number;
|
|
164
|
+
/** Total second-person blame hits in bucket */
|
|
165
|
+
blame: number;
|
|
166
|
+
/** Total characters in bucket */
|
|
167
|
+
chars: number;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export interface BehaviorOverallStats {
|
|
171
|
+
totalMessages: number;
|
|
172
|
+
totalYelling: number;
|
|
173
|
+
totalProfanity: number;
|
|
174
|
+
totalAnguish: number;
|
|
175
|
+
totalNegation: number;
|
|
176
|
+
totalRepetition: number;
|
|
177
|
+
totalBlame: number;
|
|
178
|
+
totalChars: number;
|
|
179
|
+
firstTimestamp: number;
|
|
180
|
+
lastTimestamp: number;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Per-model behavioral aggregate over the active range.
|
|
185
|
+
*/
|
|
186
|
+
export interface BehaviorModelStats {
|
|
187
|
+
model: string;
|
|
188
|
+
provider: string;
|
|
189
|
+
totalMessages: number;
|
|
190
|
+
totalYelling: number;
|
|
191
|
+
totalProfanity: number;
|
|
192
|
+
totalAnguish: number;
|
|
193
|
+
totalNegation: number;
|
|
194
|
+
totalRepetition: number;
|
|
195
|
+
totalBlame: number;
|
|
196
|
+
totalChars: number;
|
|
197
|
+
lastTimestamp: number;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export interface BehaviorDashboardStats {
|
|
201
|
+
overall: BehaviorOverallStats;
|
|
202
|
+
byModel: BehaviorModelStats[];
|
|
203
|
+
behaviorSeries: BehaviorTimeSeriesPoint[];
|
|
204
|
+
}
|