@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/README.md +211 -0
- package/bin/opencode-analytics.js +143 -0
- package/package.json +38 -0
- package/public/app.js +880 -0
- package/public/index.html +176 -0
- package/public/styles.css +826 -0
- package/server/db.js +541 -0
- package/server/index.js +491 -0
- package/server/pricing.json +122 -0
- package/server/user-prices.json +4 -0
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
|
+
};
|