@lemantorus/opencode-analytics 1.0.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/server/db.js ADDED
@@ -0,0 +1,541 @@
1
+ const { execSync } = require('child_process');
2
+ const path = require('path');
3
+
4
+ function getDbPath() {
5
+ if (process.env.OPENCODE_DB_PATH) {
6
+ return process.env.OPENCODE_DB_PATH;
7
+ }
8
+
9
+ const home = process.env.HOME || process.env.USERPROFILE;
10
+
11
+ if (!home) {
12
+ throw new Error('Could not determine home directory. Set OPENCODE_DB_PATH environment variable.');
13
+ }
14
+
15
+ const dataDir = path.join(home, '.local', 'share', 'opencode');
16
+ return path.join(dataDir, 'opencode.db');
17
+ }
18
+
19
+ const DB_PATH = getDbPath();
20
+
21
+ const cache = new Map();
22
+ const CACHE_TTL = 30000;
23
+
24
+ function query(sql) {
25
+ const cacheKey = sql;
26
+ const cached = cache.get(cacheKey);
27
+ if (cached && Date.now() - cached.time < CACHE_TTL) {
28
+ return cached.data;
29
+ }
30
+
31
+ const escapedSql = sql.replace(/'/g, "'\"'\"'");
32
+ const result = execSync(
33
+ `sqlite3 "${DB_PATH}" -json '${escapedSql}'`,
34
+ { encoding: 'utf8', maxBuffer: 100 * 1024 * 1024, timeout: 60000 }
35
+ );
36
+
37
+ let data;
38
+ try {
39
+ data = JSON.parse(result || '[]');
40
+ } catch {
41
+ data = [];
42
+ }
43
+
44
+ cache.set(cacheKey, { data, time: Date.now() });
45
+ return data;
46
+ }
47
+
48
+ function normalizeModelName(modelId) {
49
+ if (!modelId) return 'unknown';
50
+ return modelId;
51
+ }
52
+
53
+ function getShortModelName(modelId) {
54
+ if (!modelId) return 'unknown';
55
+ return modelId
56
+ .replace(/^zai-org\//, '')
57
+ .replace(/-maas$/, '')
58
+ .replace(/-free$/, '')
59
+ .replace(/-flashx$/, '-flash');
60
+ }
61
+
62
+ function isFreeModel(modelId) {
63
+ if (!modelId) return false;
64
+ return modelId.includes('-free') ||
65
+ modelId === 'big-pickle' ||
66
+ modelId.includes('nemotron');
67
+ }
68
+
69
+ function getOverview(days = null) {
70
+ const timeFilter = (days !== null && days > 0) ? `AND time_created >= ${Date.now() - (days * 24 * 60 * 60 * 1000)}` : '';
71
+
72
+ const rows = query(`
73
+ SELECT
74
+ COUNT(*) as messageCount,
75
+ SUM(CASE WHEN json_extract(data, '$.tokens.input') IS NOT NULL
76
+ THEN CAST(json_extract(data, '$.tokens.input') AS INTEGER) ELSE 0 END) as totalInput,
77
+ SUM(CASE WHEN json_extract(data, '$.tokens.output') IS NOT NULL
78
+ THEN CAST(json_extract(data, '$.tokens.output') AS INTEGER) ELSE 0 END) as totalOutput,
79
+ SUM(CASE WHEN json_extract(data, '$.tokens.cache.read') IS NOT NULL
80
+ THEN CAST(json_extract(data, '$.tokens.cache.read') AS INTEGER) ELSE 0 END) as totalCacheRead,
81
+ SUM(CASE WHEN json_extract(data, '$.tokens.cache.write') IS NOT NULL
82
+ THEN CAST(json_extract(data, '$.tokens.cache.write') AS INTEGER) ELSE 0 END) as totalCacheWrite,
83
+ MIN(time_created) as firstMessage,
84
+ MAX(time_created) as lastMessage
85
+ FROM message
86
+ WHERE data LIKE '%"tokens":%' ${timeFilter}
87
+ `);
88
+
89
+ const row = rows[0] || {};
90
+ return {
91
+ messageCount: row.messageCount || 0,
92
+ totalInput: row.totalInput || 0,
93
+ totalOutput: row.totalOutput || 0,
94
+ totalCacheRead: row.totalCacheRead || 0,
95
+ totalCacheWrite: row.totalCacheWrite || 0,
96
+ firstMessage: row.firstMessage,
97
+ lastMessage: row.lastMessage
98
+ };
99
+ }
100
+
101
+ function getModelsStats() {
102
+ const rows = query(`
103
+ SELECT
104
+ json_extract(data, '$.modelID') as modelId,
105
+ COUNT(*) as messageCount,
106
+ SUM(CASE WHEN json_extract(data, '$.tokens.input') IS NOT NULL
107
+ THEN CAST(json_extract(data, '$.tokens.input') AS INTEGER) ELSE 0 END) as inputTokens,
108
+ SUM(CASE WHEN json_extract(data, '$.tokens.output') IS NOT NULL
109
+ THEN CAST(json_extract(data, '$.tokens.output') AS INTEGER) ELSE 0 END) as outputTokens,
110
+ SUM(CASE WHEN json_extract(data, '$.tokens.cache.read') IS NOT NULL
111
+ THEN CAST(json_extract(data, '$.tokens.cache.read') AS INTEGER) ELSE 0 END) as cacheRead,
112
+ SUM(CASE WHEN json_extract(data, '$.tokens.cache.write') IS NOT NULL
113
+ THEN CAST(json_extract(data, '$.tokens.cache.write') AS INTEGER) ELSE 0 END) as cacheWrite
114
+ FROM message
115
+ WHERE data LIKE '%"modelID"%' AND data LIKE '%"tokens":%'
116
+ GROUP BY json_extract(data, '$.modelID')
117
+ ORDER BY inputTokens DESC
118
+ `);
119
+
120
+ return rows.map(row => ({
121
+ modelId: row.modelId,
122
+ baseModel: normalizeModelName(row.modelId),
123
+ isFree: isFreeModel(row.modelId),
124
+ messageCount: row.messageCount || 0,
125
+ inputTokens: row.inputTokens || 0,
126
+ outputTokens: row.outputTokens || 0,
127
+ cacheRead: row.cacheRead || 0,
128
+ cacheWrite: row.cacheWrite || 0
129
+ }));
130
+ }
131
+
132
+ function getDailyStats(days = 30) {
133
+ const startTime = Date.now() - (days * 24 * 60 * 60 * 1000);
134
+
135
+ const rows = query(`
136
+ SELECT
137
+ date(time_created / 1000, 'unixepoch') as date,
138
+ json_extract(data, '$.modelID') as modelId,
139
+ COUNT(*) as messageCount,
140
+ SUM(CASE WHEN json_extract(data, '$.tokens.input') IS NOT NULL
141
+ THEN CAST(json_extract(data, '$.tokens.input') AS INTEGER) ELSE 0 END) as inputTokens,
142
+ SUM(CASE WHEN json_extract(data, '$.tokens.output') IS NOT NULL
143
+ THEN CAST(json_extract(data, '$.tokens.output') AS INTEGER) ELSE 0 END) as outputTokens,
144
+ SUM(CASE WHEN json_extract(data, '$.tokens.cache.read') IS NOT NULL
145
+ THEN CAST(json_extract(data, '$.tokens.cache.read') AS INTEGER) ELSE 0 END) as cacheRead,
146
+ SUM(CASE WHEN json_extract(data, '$.tokens.cache.write') IS NOT NULL
147
+ THEN CAST(json_extract(data, '$.tokens.cache.write') AS INTEGER) ELSE 0 END) as cacheWrite
148
+ FROM message
149
+ WHERE data LIKE '%"tokens":%' AND time_created >= ${startTime}
150
+ GROUP BY date, json_extract(data, '$.modelID')
151
+ ORDER BY date
152
+ `);
153
+
154
+ return rows.map(row => ({
155
+ date: row.date,
156
+ modelId: row.modelId,
157
+ baseModel: normalizeModelName(row.modelId),
158
+ isFree: isFreeModel(row.modelId),
159
+ messageCount: row.messageCount || 0,
160
+ inputTokens: row.inputTokens || 0,
161
+ outputTokens: row.outputTokens || 0,
162
+ cacheRead: row.cacheRead || 0,
163
+ cacheWrite: row.cacheWrite || 0
164
+ }));
165
+ }
166
+
167
+ function getWeeklyStats(weeks = 12) {
168
+ const startTime = Date.now() - (weeks * 7 * 24 * 60 * 60 * 1000);
169
+
170
+ const rows = query(`
171
+ SELECT
172
+ strftime('%Y-%W', time_created / 1000, 'unixepoch') as week,
173
+ json_extract(data, '$.modelID') as modelId,
174
+ COUNT(*) as messageCount,
175
+ SUM(CASE WHEN json_extract(data, '$.tokens.input') IS NOT NULL
176
+ THEN CAST(json_extract(data, '$.tokens.input') AS INTEGER) ELSE 0 END) as inputTokens,
177
+ SUM(CASE WHEN json_extract(data, '$.tokens.output') IS NOT NULL
178
+ THEN CAST(json_extract(data, '$.tokens.output') AS INTEGER) ELSE 0 END) as outputTokens,
179
+ SUM(CASE WHEN json_extract(data, '$.tokens.cache.read') IS NOT NULL
180
+ THEN CAST(json_extract(data, '$.tokens.cache.read') AS INTEGER) ELSE 0 END) as cacheRead,
181
+ SUM(CASE WHEN json_extract(data, '$.tokens.cache.write') IS NOT NULL
182
+ THEN CAST(json_extract(data, '$.tokens.cache.write') AS INTEGER) ELSE 0 END) as cacheWrite
183
+ FROM message
184
+ WHERE data LIKE '%"tokens":%' AND time_created >= ${startTime}
185
+ GROUP BY week, json_extract(data, '$.modelID')
186
+ ORDER BY week
187
+ `);
188
+
189
+ return rows.map(row => ({
190
+ week: row.week,
191
+ modelId: row.modelId,
192
+ baseModel: normalizeModelName(row.modelId),
193
+ isFree: isFreeModel(row.modelId),
194
+ messageCount: row.messageCount || 0,
195
+ inputTokens: row.inputTokens || 0,
196
+ outputTokens: row.outputTokens || 0,
197
+ cacheRead: row.cacheRead || 0,
198
+ cacheWrite: row.cacheWrite || 0
199
+ }));
200
+ }
201
+
202
+ function getMonthlyStats(months = 12) {
203
+ const startTime = Date.now() - (months * 30 * 24 * 60 * 60 * 1000);
204
+
205
+ const rows = query(`
206
+ SELECT
207
+ strftime('%Y-%m', time_created / 1000, 'unixepoch') as month,
208
+ json_extract(data, '$.modelID') as modelId,
209
+ COUNT(*) as messageCount,
210
+ SUM(CASE WHEN json_extract(data, '$.tokens.input') IS NOT NULL
211
+ THEN CAST(json_extract(data, '$.tokens.input') AS INTEGER) ELSE 0 END) as inputTokens,
212
+ SUM(CASE WHEN json_extract(data, '$.tokens.output') IS NOT NULL
213
+ THEN CAST(json_extract(data, '$.tokens.output') AS INTEGER) ELSE 0 END) as outputTokens,
214
+ SUM(CASE WHEN json_extract(data, '$.tokens.cache.read') IS NOT NULL
215
+ THEN CAST(json_extract(data, '$.tokens.cache.read') AS INTEGER) ELSE 0 END) as cacheRead,
216
+ SUM(CASE WHEN json_extract(data, '$.tokens.cache.write') IS NOT NULL
217
+ THEN CAST(json_extract(data, '$.tokens.cache.write') AS INTEGER) ELSE 0 END) as cacheWrite
218
+ FROM message
219
+ WHERE data LIKE '%"tokens":%' AND time_created >= ${startTime}
220
+ GROUP BY month, json_extract(data, '$.modelID')
221
+ ORDER BY month
222
+ `);
223
+
224
+ return rows.map(row => ({
225
+ month: row.month,
226
+ modelId: row.modelId,
227
+ baseModel: normalizeModelName(row.modelId),
228
+ isFree: isFreeModel(row.modelId),
229
+ messageCount: row.messageCount || 0,
230
+ inputTokens: row.inputTokens || 0,
231
+ outputTokens: row.outputTokens || 0,
232
+ cacheRead: row.cacheRead || 0,
233
+ cacheWrite: row.cacheWrite || 0
234
+ }));
235
+ }
236
+
237
+ function getHourlyStats() {
238
+ const rows = query(`
239
+ SELECT
240
+ CAST(strftime('%H', time_created / 1000, 'unixepoch') AS INTEGER) as hour,
241
+ COUNT(*) as messageCount,
242
+ SUM(CASE WHEN json_extract(data, '$.tokens.input') IS NOT NULL
243
+ THEN CAST(json_extract(data, '$.tokens.input') AS INTEGER) ELSE 0 END) as inputTokens,
244
+ SUM(CASE WHEN json_extract(data, '$.tokens.output') IS NOT NULL
245
+ THEN CAST(json_extract(data, '$.tokens.output') AS INTEGER) ELSE 0 END) as outputTokens,
246
+ SUM(CASE WHEN json_extract(data, '$.tokens.cache.read') IS NOT NULL
247
+ THEN CAST(json_extract(data, '$.tokens.cache.read') AS INTEGER) ELSE 0 END) as cacheRead,
248
+ SUM(CASE WHEN json_extract(data, '$.tokens.cache.write') IS NOT NULL
249
+ THEN CAST(json_extract(data, '$.tokens.cache.write') AS INTEGER) ELSE 0 END) as cacheWrite
250
+ FROM message
251
+ WHERE data LIKE '%"tokens":%'
252
+ GROUP BY hour
253
+ ORDER BY hour
254
+ `);
255
+
256
+ return rows.map(row => ({
257
+ hour: row.hour,
258
+ messageCount: row.messageCount || 0,
259
+ inputTokens: row.inputTokens || 0,
260
+ outputTokens: row.outputTokens || 0,
261
+ cacheRead: row.cacheRead || 0,
262
+ cacheWrite: row.cacheWrite || 0
263
+ }));
264
+ }
265
+
266
+ function getDailyStatsRange(startTs, endTs) {
267
+ const rows = query(`
268
+ SELECT
269
+ date(time_created / 1000, 'unixepoch') as date,
270
+ json_extract(data, '$.modelID') as modelId,
271
+ COUNT(*) as messageCount,
272
+ SUM(CASE WHEN json_extract(data, '$.tokens.input') IS NOT NULL
273
+ THEN CAST(json_extract(data, '$.tokens.input') AS INTEGER) ELSE 0 END) as inputTokens,
274
+ SUM(CASE WHEN json_extract(data, '$.tokens.output') IS NOT NULL
275
+ THEN CAST(json_extract(data, '$.tokens.output') AS INTEGER) ELSE 0 END) as outputTokens,
276
+ SUM(CASE WHEN json_extract(data, '$.tokens.cache.read') IS NOT NULL
277
+ THEN CAST(json_extract(data, '$.tokens.cache.read') AS INTEGER) ELSE 0 END) as cacheRead,
278
+ SUM(CASE WHEN json_extract(data, '$.tokens.cache.write') IS NOT NULL
279
+ THEN CAST(json_extract(data, '$.tokens.cache.write') AS INTEGER) ELSE 0 END) as cacheWrite
280
+ FROM message
281
+ WHERE data LIKE '%"tokens":%' AND time_created >= ${startTs} AND time_created <= ${endTs}
282
+ GROUP BY date, json_extract(data, '$.modelID')
283
+ ORDER BY date
284
+ `);
285
+
286
+ return rows.map(row => ({
287
+ date: row.date,
288
+ modelId: row.modelId,
289
+ baseModel: normalizeModelName(row.modelId),
290
+ isFree: isFreeModel(row.modelId),
291
+ messageCount: row.messageCount || 0,
292
+ inputTokens: row.inputTokens || 0,
293
+ outputTokens: row.outputTokens || 0,
294
+ cacheRead: row.cacheRead || 0,
295
+ cacheWrite: row.cacheWrite || 0
296
+ }));
297
+ }
298
+
299
+ function getModelsStatsByDays(days) {
300
+ const startTime = Date.now() - (days * 24 * 60 * 60 * 1000);
301
+
302
+ const rows = query(`
303
+ SELECT
304
+ json_extract(data, '$.modelID') as modelId,
305
+ COUNT(*) as messageCount,
306
+ SUM(CASE WHEN json_extract(data, '$.tokens.input') IS NOT NULL
307
+ THEN CAST(json_extract(data, '$.tokens.input') AS INTEGER) ELSE 0 END) as inputTokens,
308
+ SUM(CASE WHEN json_extract(data, '$.tokens.output') IS NOT NULL
309
+ THEN CAST(json_extract(data, '$.tokens.output') AS INTEGER) ELSE 0 END) as outputTokens,
310
+ SUM(CASE WHEN json_extract(data, '$.tokens.cache.read') IS NOT NULL
311
+ THEN CAST(json_extract(data, '$.tokens.cache.read') AS INTEGER) ELSE 0 END) as cacheRead,
312
+ SUM(CASE WHEN json_extract(data, '$.tokens.cache.write') IS NOT NULL
313
+ THEN CAST(json_extract(data, '$.tokens.cache.write') AS INTEGER) ELSE 0 END) as cacheWrite
314
+ FROM message
315
+ WHERE data LIKE '%"modelID"%' AND data LIKE '%"tokens":%' AND time_created >= ${startTime}
316
+ GROUP BY json_extract(data, '$.modelID')
317
+ ORDER BY inputTokens DESC
318
+ `);
319
+
320
+ return rows.map(row => ({
321
+ modelId: row.modelId,
322
+ baseModel: normalizeModelName(row.modelId),
323
+ isFree: isFreeModel(row.modelId),
324
+ messageCount: row.messageCount || 0,
325
+ inputTokens: row.inputTokens || 0,
326
+ outputTokens: row.outputTokens || 0,
327
+ cacheRead: row.cacheRead || 0,
328
+ cacheWrite: row.cacheWrite || 0
329
+ }));
330
+ }
331
+
332
+ function getDailyTPSByModel(days = 30, modelFilter = null) {
333
+ const startTime = Date.now() - (days * 24 * 60 * 60 * 1000);
334
+
335
+ const rows = query(`
336
+ SELECT
337
+ date(time_created / 1000, 'unixepoch') as date,
338
+ json_extract(data, '$.modelID') as modelId,
339
+ json_extract(data, '$.tokens.output') as outputTokens,
340
+ json_extract(data, '$.tokens.input') as inputTokens,
341
+ json_extract(data, '$.time.created') as timeCreated,
342
+ json_extract(data, '$.time.completed') as timeCompleted
343
+ FROM message
344
+ WHERE data LIKE '%"tokens":%'
345
+ AND data LIKE '%"time":%'
346
+ AND time_created >= ${startTime}
347
+ `);
348
+
349
+ const modelDailyStats = {};
350
+
351
+ for (const row of rows) {
352
+ if (!row.modelId) continue;
353
+
354
+ const timeCreated = row.timeCreated || row.time_created;
355
+ const timeCompleted = row.timeCompleted;
356
+ const durationSeconds = (timeCompleted && timeCreated && timeCompleted > timeCreated)
357
+ ? (timeCompleted - timeCreated) / 1000
358
+ : 0;
359
+
360
+ if (durationSeconds <= 0) continue;
361
+
362
+ const tps = (row.outputTokens || 0) / durationSeconds;
363
+ if (!isFinite(tps)) continue;
364
+
365
+ const modelKey = normalizeModelName(row.modelId);
366
+ const dateKey = row.date;
367
+
368
+ if (!modelDailyStats[modelKey]) {
369
+ modelDailyStats[modelKey] = {};
370
+ }
371
+ if (!modelDailyStats[modelKey][dateKey]) {
372
+ modelDailyStats[modelKey][dateKey] = { tpsSum: 0, count: 0, inputTokens: 0, outputTokens: 0 };
373
+ }
374
+
375
+ modelDailyStats[modelKey][dateKey].tpsSum += tps;
376
+ modelDailyStats[modelKey][dateKey].count += 1;
377
+ modelDailyStats[modelKey][dateKey].inputTokens += row.inputTokens || 0;
378
+ modelDailyStats[modelKey][dateKey].outputTokens += row.outputTokens || 0;
379
+ }
380
+
381
+ const result = [];
382
+ const allDates = new Set();
383
+ for (const model of Object.values(modelDailyStats)) {
384
+ for (const date of Object.keys(model)) {
385
+ allDates.add(date);
386
+ }
387
+ }
388
+ const sortedDates = Array.from(allDates).sort();
389
+
390
+ for (const [model, dates] of Object.entries(modelDailyStats)) {
391
+ if (modelFilter && !modelFilter.includes(model)) continue;
392
+
393
+ const data = sortedDates.map(date => {
394
+ const dayStats = dates[date];
395
+ return dayStats && dayStats.count > 0
396
+ ? {
397
+ tps: dayStats.tpsSum / dayStats.count,
398
+ inputTokens: dayStats.inputTokens,
399
+ outputTokens: dayStats.outputTokens
400
+ }
401
+ : null;
402
+ });
403
+
404
+ result.push({
405
+ baseModel: model,
406
+ data: data
407
+ });
408
+ }
409
+
410
+ result.sort((a, b) => {
411
+ const totalA = a.data.reduce((s, v) => s + (v?.tps || 0), 0);
412
+ const totalB = b.data.reduce((s, v) => s + (v?.tps || 0), 0);
413
+ return totalB - totalA;
414
+ });
415
+
416
+ return {
417
+ dates: sortedDates,
418
+ models: result
419
+ };
420
+ }
421
+
422
+ function getModelsTPSStats(days = 30) {
423
+ const startTime = Date.now() - (days * 24 * 60 * 60 * 1000);
424
+
425
+ const rows = query(`
426
+ SELECT
427
+ json_extract(data, '$.modelID') as modelId,
428
+ COUNT(*) as messageCount,
429
+ MIN(time_created) as firstMessage,
430
+ MAX(time_created) as lastMessage,
431
+ SUM(CASE WHEN json_extract(data, '$.tokens.output') IS NOT NULL
432
+ THEN CAST(json_extract(data, '$.tokens.output') AS INTEGER) ELSE 0 END) as outputTokens,
433
+ SUM(CASE WHEN json_extract(data, '$.tokens.input') IS NOT NULL
434
+ THEN CAST(json_extract(data, '$.tokens.input') AS INTEGER) ELSE 0 END) as inputTokens
435
+ FROM message
436
+ WHERE data LIKE '%"modelID"%' AND data LIKE '%"tokens":%' AND time_created >= ${startTime}
437
+ GROUP BY json_extract(data, '$.modelID')
438
+ ORDER BY outputTokens DESC
439
+ `);
440
+
441
+ return rows.map(row => {
442
+ const firstMsg = row.firstMessage || 0;
443
+ const lastMsg = row.lastMessage || 0;
444
+ const durationSeconds = lastMsg > firstMsg ? (lastMsg - firstMsg) / 1000 : 1;
445
+ const outputTPS = (row.outputTokens || 0) / durationSeconds;
446
+ const inputTPS = (row.inputTokens || 0) / durationSeconds;
447
+
448
+ return {
449
+ modelId: row.modelId,
450
+ baseModel: normalizeModelName(row.modelId),
451
+ isFree: isFreeModel(row.modelId),
452
+ messageCount: row.messageCount || 0,
453
+ outputTokens: row.outputTokens || 0,
454
+ inputTokens: row.inputTokens || 0,
455
+ outputTPS: outputTPS,
456
+ inputTPS: inputTPS,
457
+ durationSeconds: durationSeconds
458
+ };
459
+ });
460
+ }
461
+
462
+ function getHourlyTPSStats() {
463
+ const now = Date.now();
464
+ const oneDayAgo = now - (24 * 60 * 60 * 1000);
465
+
466
+ const rows = query(`
467
+ SELECT
468
+ CAST(strftime('%H', time_created / 1000, 'unixepoch') AS INTEGER) as hour,
469
+ COUNT(*) as messageCount,
470
+ SUM(CASE WHEN json_extract(data, '$.tokens.output') IS NOT NULL
471
+ THEN CAST(json_extract(data, '$.tokens.output') AS INTEGER) ELSE 0 END) as outputTokens,
472
+ MIN(time_created) as minTime,
473
+ MAX(time_created) as maxTime
474
+ FROM message
475
+ WHERE data LIKE '%"tokens":%' AND time_created >= ${oneDayAgo}
476
+ GROUP BY hour
477
+ ORDER BY hour
478
+ `);
479
+
480
+ const nowHour = new Date().getHours();
481
+ const result = [];
482
+
483
+ for (let i = 0; i < 24; i++) {
484
+ const found = rows.find(r => r.hour === i);
485
+ const hourDiff = Math.abs(nowHour - i);
486
+ const isToday = hourDiff <= 12;
487
+
488
+ if (found && found.maxTime && found.minTime) {
489
+ const durationSeconds = (found.maxTime - found.minTime) / 1000;
490
+ const outputTPS = durationSeconds > 0 ? found.outputTokens / durationSeconds : 0;
491
+ result.push({
492
+ hour: i,
493
+ messageCount: found.messageCount || 0,
494
+ outputTokens: found.outputTokens || 0,
495
+ outputTPS: outputTPS,
496
+ isToday: isToday
497
+ });
498
+ } else {
499
+ result.push({
500
+ hour: i,
501
+ messageCount: 0,
502
+ outputTokens: 0,
503
+ outputTPS: 0,
504
+ isToday: isToday
505
+ });
506
+ }
507
+ }
508
+
509
+ return result;
510
+ }
511
+
512
+ function getModelsList() {
513
+ const rows = query(`
514
+ SELECT DISTINCT json_extract(data, '$.modelID') as modelId
515
+ FROM message
516
+ WHERE data LIKE '%"modelID"%'
517
+ `);
518
+
519
+ return rows
520
+ .map(row => normalizeModelName(row.modelId))
521
+ .filter((v, i, a) => a.indexOf(v) === i)
522
+ .sort();
523
+ }
524
+
525
+ module.exports = {
526
+ normalizeModelName,
527
+ getShortModelName,
528
+ isFreeModel,
529
+ getOverview,
530
+ getModelsStats,
531
+ getModelsStatsByDays,
532
+ getDailyStats,
533
+ getDailyStatsRange,
534
+ getWeeklyStats,
535
+ getMonthlyStats,
536
+ getHourlyStats,
537
+ getModelsTPSStats,
538
+ getHourlyTPSStats,
539
+ getDailyTPSByModel,
540
+ getModelsList
541
+ };