agentfit 0.1.6 → 0.1.8
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/.github/workflows/release.yml +6 -2
- package/LICENSE +662 -21
- package/README.md +1 -1
- package/components/daily-table.tsx +22 -13
- package/components/data-provider.tsx +4 -0
- package/electron/main.mjs +39 -0
- package/generated/prisma/browser.ts +5 -0
- package/generated/prisma/client.ts +5 -0
- package/generated/prisma/internal/class.ts +14 -4
- package/generated/prisma/internal/prismaNamespace.ts +97 -1
- package/generated/prisma/internal/prismaNamespaceBrowser.ts +21 -0
- package/generated/prisma/models/MessageUsage.ts +1473 -0
- package/generated/prisma/models.ts +1 -0
- package/lib/parse-logs.ts +11 -0
- package/lib/pricing.ts +103 -37
- package/lib/queries-codex.ts +1 -0
- package/lib/queries.ts +124 -36
- package/lib/sync.ts +212 -67
- package/package.json +3 -2
- package/prisma/migrations/20260505162205_add_message_usage/migration.sql +29 -0
- package/prisma/schema.prisma +22 -0
- package/prisma/schema.sql +30 -0
package/lib/sync.ts
CHANGED
|
@@ -2,16 +2,28 @@ import fs from 'fs'
|
|
|
2
2
|
import path from 'path'
|
|
3
3
|
import os from 'os'
|
|
4
4
|
import { prisma } from './db'
|
|
5
|
-
import { loadPricing, calculateCost, type ModelPricing } from './pricing'
|
|
5
|
+
import { loadPricing, calculateCost, type ModelPricing, type Speed } from './pricing'
|
|
6
6
|
|
|
7
7
|
const PROJECTS_DIR = path.join(os.homedir(), '.claude', 'projects')
|
|
8
8
|
const IMAGES_DIR = path.resolve(process.cwd(), 'data', 'images')
|
|
9
9
|
|
|
10
|
+
// Bucket per-message timestamps using the user's *local* timezone so daily
|
|
11
|
+
// totals match ccusage (apps/ccusage/src/_date-utils.ts:43-48). en-CA yields
|
|
12
|
+
// YYYY-MM-DD without zero-padding surprises.
|
|
13
|
+
const DATE_FORMATTER = new Intl.DateTimeFormat('en-CA', {
|
|
14
|
+
year: 'numeric',
|
|
15
|
+
month: '2-digit',
|
|
16
|
+
day: '2-digit',
|
|
17
|
+
})
|
|
18
|
+
|
|
10
19
|
interface LogEntry {
|
|
11
20
|
type?: string
|
|
12
21
|
uuid?: string
|
|
13
22
|
version?: string
|
|
23
|
+
requestId?: string
|
|
24
|
+
sessionId?: string
|
|
14
25
|
message?: {
|
|
26
|
+
id?: string
|
|
15
27
|
role?: string
|
|
16
28
|
content?: unknown[]
|
|
17
29
|
model?: string
|
|
@@ -20,11 +32,27 @@ interface LogEntry {
|
|
|
20
32
|
output_tokens?: number
|
|
21
33
|
cache_creation_input_tokens?: number
|
|
22
34
|
cache_read_input_tokens?: number
|
|
35
|
+
speed?: Speed
|
|
23
36
|
}
|
|
24
37
|
}
|
|
25
38
|
timestamp?: string
|
|
26
39
|
}
|
|
27
40
|
|
|
41
|
+
interface MessageUsageRow {
|
|
42
|
+
sessionId: string
|
|
43
|
+
messageId: string
|
|
44
|
+
requestId: string
|
|
45
|
+
model: string
|
|
46
|
+
speed: Speed
|
|
47
|
+
timestamp: Date
|
|
48
|
+
date: string
|
|
49
|
+
inputTokens: number
|
|
50
|
+
outputTokens: number
|
|
51
|
+
cacheCreationTokens: number
|
|
52
|
+
cacheReadTokens: number
|
|
53
|
+
costUSD: number
|
|
54
|
+
}
|
|
55
|
+
|
|
28
56
|
interface ImageInfo {
|
|
29
57
|
messageId: string
|
|
30
58
|
filename: string
|
|
@@ -38,6 +66,33 @@ function decodeProjectPath(dirName: string): string {
|
|
|
38
66
|
return dirName.replace(/-/g, '/')
|
|
39
67
|
}
|
|
40
68
|
|
|
69
|
+
interface WalkedFile {
|
|
70
|
+
absPath: string
|
|
71
|
+
isSubagent: boolean
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function walkJsonl(dir: string, projectRoot: string): WalkedFile[] {
|
|
75
|
+
const out: WalkedFile[] = []
|
|
76
|
+
let entries: fs.Dirent[]
|
|
77
|
+
try {
|
|
78
|
+
entries = fs.readdirSync(dir, { withFileTypes: true })
|
|
79
|
+
} catch {
|
|
80
|
+
return out
|
|
81
|
+
}
|
|
82
|
+
for (const e of entries) {
|
|
83
|
+
const full = path.join(dir, e.name)
|
|
84
|
+
if (e.isDirectory()) {
|
|
85
|
+
out.push(...walkJsonl(full, projectRoot))
|
|
86
|
+
} else if (e.isFile() && e.name.endsWith('.jsonl')) {
|
|
87
|
+
// A file is "subagent" if it sits below the project root (not directly
|
|
88
|
+
// in it). ccusage uses **/*.jsonl for the same effect.
|
|
89
|
+
const isSubagent = path.dirname(full) !== projectRoot
|
|
90
|
+
out.push({ absPath: full, isSubagent })
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return out
|
|
94
|
+
}
|
|
95
|
+
|
|
41
96
|
function getProjectName(projectPath: string): string {
|
|
42
97
|
// The decoded path may be wrong if the actual folder name contains dashes,
|
|
43
98
|
// since decodeProjectPath replaces ALL dashes with slashes.
|
|
@@ -81,6 +136,7 @@ function parseSessionFile(
|
|
|
81
136
|
const toolCalls: Record<string, number> = {}
|
|
82
137
|
const images: ImageInfo[] = []
|
|
83
138
|
const messageTimestamps: string[] = []
|
|
139
|
+
const messageUsages: MessageUsageRow[] = []
|
|
84
140
|
let apiErrors = 0
|
|
85
141
|
let rateLimitErrors = 0
|
|
86
142
|
let userInterruptions = 0
|
|
@@ -158,12 +214,41 @@ function parseSessionFile(
|
|
|
158
214
|
|
|
159
215
|
if (msg.usage) {
|
|
160
216
|
const u = msg.usage
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
217
|
+
const speed: Speed = u.speed === 'fast' ? 'fast' : 'standard'
|
|
218
|
+
const inT = u.input_tokens || 0
|
|
219
|
+
const outT = u.output_tokens || 0
|
|
220
|
+
const ccT = u.cache_creation_input_tokens || 0
|
|
221
|
+
const crT = u.cache_read_input_tokens || 0
|
|
222
|
+
const msgCost = currentModel ? calculateCost(currentModel, u, allPricing, speed) : 0
|
|
223
|
+
|
|
224
|
+
inputTokens += inT
|
|
225
|
+
outputTokens += outT
|
|
226
|
+
cacheCreationTokens += ccT
|
|
227
|
+
cacheReadTokens += crT
|
|
228
|
+
costUSD += msgCost
|
|
229
|
+
|
|
230
|
+
// Per-message row for daily aggregation. Dedup by (messageId, requestId)
|
|
231
|
+
// happens at insert time via the unique index. Mirrors ccusage's
|
|
232
|
+
// createUniqueHash (apps/ccusage/src/data-loader.ts:530-540).
|
|
233
|
+
if (currentModel && msg.id && entry.requestId && entry.timestamp) {
|
|
234
|
+
const ts = new Date(entry.timestamp)
|
|
235
|
+
messageUsages.push({
|
|
236
|
+
// Subagent files have filename basename "agent-<hash>" but carry
|
|
237
|
+
// the parent session's sessionId on every line — use that so
|
|
238
|
+
// sub-agent token usage rolls up to the parent Session row.
|
|
239
|
+
sessionId: entry.sessionId || sessionId,
|
|
240
|
+
messageId: msg.id,
|
|
241
|
+
requestId: entry.requestId,
|
|
242
|
+
model: currentModel,
|
|
243
|
+
speed,
|
|
244
|
+
timestamp: ts,
|
|
245
|
+
date: DATE_FORMATTER.format(ts),
|
|
246
|
+
inputTokens: inT,
|
|
247
|
+
outputTokens: outT,
|
|
248
|
+
cacheCreationTokens: ccT,
|
|
249
|
+
cacheReadTokens: crT,
|
|
250
|
+
costUSD: msgCost,
|
|
251
|
+
})
|
|
167
252
|
}
|
|
168
253
|
}
|
|
169
254
|
}
|
|
@@ -263,6 +348,7 @@ function parseSessionFile(
|
|
|
263
348
|
systemPromptEdits,
|
|
264
349
|
cliVersion: cliVersion || 'unknown',
|
|
265
350
|
images,
|
|
351
|
+
messageUsages,
|
|
266
352
|
}
|
|
267
353
|
}
|
|
268
354
|
|
|
@@ -293,10 +379,10 @@ export async function syncLogs(): Promise<SyncResult> {
|
|
|
293
379
|
fs.mkdirSync(IMAGES_DIR, { recursive: true })
|
|
294
380
|
}
|
|
295
381
|
|
|
296
|
-
//
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
382
|
+
// We always re-read every JSONL — Claude Code appends to the same file
|
|
383
|
+
// throughout a long session, so a one-shot import would freeze the partial
|
|
384
|
+
// state. Idempotency comes from the (messageId, requestId) unique index on
|
|
385
|
+
// MessageUsage and from upserting Session by sessionId.
|
|
300
386
|
const projectDirs = fs.readdirSync(PROJECTS_DIR).filter((d) => {
|
|
301
387
|
try {
|
|
302
388
|
return fs.statSync(path.join(PROJECTS_DIR, d)).isDirectory()
|
|
@@ -310,75 +396,119 @@ export async function syncLogs(): Promise<SyncResult> {
|
|
|
310
396
|
const projectName = getProjectName(projectPath)
|
|
311
397
|
const dirPath = path.join(PROJECTS_DIR, dir)
|
|
312
398
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
continue
|
|
318
|
-
}
|
|
399
|
+
// Recurse for *.jsonl. Top-level files are standalone conversations
|
|
400
|
+
// (filename = sessionId). Files under <session>/subagents/ carry the
|
|
401
|
+
// parent's sessionId on each line; we only mine them for MessageUsage.
|
|
402
|
+
const jsonlFiles = walkJsonl(dirPath, dirPath)
|
|
319
403
|
|
|
320
|
-
for (const
|
|
404
|
+
for (const { absPath, isSubagent } of jsonlFiles) {
|
|
321
405
|
result.filesProcessed++
|
|
322
|
-
const sessionId = path.basename(
|
|
323
|
-
|
|
324
|
-
if (existingIds.has(sessionId)) {
|
|
325
|
-
result.sessionsSkipped++
|
|
326
|
-
continue
|
|
327
|
-
}
|
|
406
|
+
const sessionId = path.basename(absPath, '.jsonl')
|
|
328
407
|
|
|
329
408
|
try {
|
|
330
|
-
const parsed = parseSessionFile(
|
|
409
|
+
const parsed = parseSessionFile(absPath, sessionId, allPricing)
|
|
331
410
|
if (!parsed) {
|
|
332
411
|
result.sessionsSkipped++
|
|
333
412
|
continue
|
|
334
413
|
}
|
|
335
414
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
415
|
+
// Subagent files contribute MessageUsage only — the conversation
|
|
416
|
+
// itself is owned by the top-level JSONL.
|
|
417
|
+
if (isSubagent) {
|
|
418
|
+
for (const m of parsed.messageUsages) {
|
|
419
|
+
await prisma.$executeRaw`
|
|
420
|
+
INSERT OR IGNORE INTO "MessageUsage" (
|
|
421
|
+
"id", "sessionId", "messageId", "requestId", "model", "speed",
|
|
422
|
+
"timestamp", "date",
|
|
423
|
+
"inputTokens", "outputTokens", "cacheCreationTokens", "cacheReadTokens",
|
|
424
|
+
"costUSD", "createdAt"
|
|
425
|
+
) VALUES (
|
|
426
|
+
${`mu_${m.messageId}_${m.requestId}`}, ${m.sessionId}, ${m.messageId}, ${m.requestId},
|
|
427
|
+
${m.model}, ${m.speed},
|
|
428
|
+
${m.timestamp.toISOString()}, ${m.date},
|
|
429
|
+
${m.inputTokens}, ${m.outputTokens}, ${m.cacheCreationTokens}, ${m.cacheReadTokens},
|
|
430
|
+
${m.costUSD}, ${new Date().toISOString()}
|
|
431
|
+
)
|
|
432
|
+
`
|
|
433
|
+
}
|
|
434
|
+
result.sessionsAdded++
|
|
435
|
+
continue
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const sessionData = {
|
|
439
|
+
project: projectName,
|
|
440
|
+
projectPath,
|
|
441
|
+
startTime: new Date(parsed.startTime),
|
|
442
|
+
endTime: new Date(parsed.endTime),
|
|
443
|
+
durationMinutes: parsed.durationMinutes,
|
|
444
|
+
userMessages: parsed.userMessages,
|
|
445
|
+
assistantMessages: parsed.assistantMessages,
|
|
446
|
+
totalMessages: parsed.totalMessages,
|
|
447
|
+
inputTokens: parsed.inputTokens,
|
|
448
|
+
outputTokens: parsed.outputTokens,
|
|
449
|
+
cacheCreationTokens: parsed.cacheCreationTokens,
|
|
450
|
+
cacheReadTokens: parsed.cacheReadTokens,
|
|
451
|
+
totalTokens: parsed.totalTokens,
|
|
452
|
+
costUSD: parsed.costUSD,
|
|
453
|
+
model: parsed.model,
|
|
454
|
+
toolCallsTotal: parsed.toolCallsTotal,
|
|
455
|
+
toolCallsJson: JSON.stringify(parsed.toolCalls),
|
|
456
|
+
skillCallsJson: JSON.stringify(parsed.skillCalls),
|
|
457
|
+
messageTimestamps: JSON.stringify(parsed.messageTimestamps),
|
|
458
|
+
apiErrors: parsed.apiErrors,
|
|
459
|
+
rateLimitErrors: parsed.rateLimitErrors,
|
|
460
|
+
userInterruptions: parsed.userInterruptions,
|
|
461
|
+
permissionModesJson: JSON.stringify(parsed.permissionModes),
|
|
462
|
+
systemPromptEdits: parsed.systemPromptEdits,
|
|
463
|
+
cliVersion: parsed.cliVersion,
|
|
464
|
+
modelCountsJson: JSON.stringify(parsed.modelCounts),
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
await prisma.session.upsert({
|
|
468
|
+
where: { sessionId },
|
|
469
|
+
create: { sessionId, ...sessionData },
|
|
470
|
+
update: sessionData,
|
|
366
471
|
})
|
|
367
472
|
|
|
473
|
+
// Per-message usage rows. Unique index on (messageId, requestId) makes
|
|
474
|
+
// this idempotent across re-syncs and dedupes forks/resumes that copy
|
|
475
|
+
// prior turns into new JSONLs. Use SQLite's INSERT OR IGNORE because
|
|
476
|
+
// Prisma createMany on libsql doesn't expose skipDuplicates.
|
|
477
|
+
for (const m of parsed.messageUsages) {
|
|
478
|
+
await prisma.$executeRaw`
|
|
479
|
+
INSERT OR IGNORE INTO "MessageUsage" (
|
|
480
|
+
"id", "sessionId", "messageId", "requestId", "model", "speed",
|
|
481
|
+
"timestamp", "date",
|
|
482
|
+
"inputTokens", "outputTokens", "cacheCreationTokens", "cacheReadTokens",
|
|
483
|
+
"costUSD", "createdAt"
|
|
484
|
+
) VALUES (
|
|
485
|
+
${`mu_${m.messageId}_${m.requestId}`}, ${m.sessionId}, ${m.messageId}, ${m.requestId},
|
|
486
|
+
${m.model}, ${m.speed},
|
|
487
|
+
${m.timestamp.toISOString()}, ${m.date},
|
|
488
|
+
${m.inputTokens}, ${m.outputTokens}, ${m.cacheCreationTokens}, ${m.cacheReadTokens},
|
|
489
|
+
${m.costUSD}, ${new Date().toISOString()}
|
|
490
|
+
)
|
|
491
|
+
`
|
|
492
|
+
}
|
|
493
|
+
|
|
368
494
|
// Insert image records
|
|
369
495
|
for (const img of parsed.images) {
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
496
|
+
try {
|
|
497
|
+
await prisma.image.create({
|
|
498
|
+
data: {
|
|
499
|
+
sessionId,
|
|
500
|
+
messageId: img.messageId,
|
|
501
|
+
filename: img.filename,
|
|
502
|
+
mediaType: img.mediaType,
|
|
503
|
+
sizeBytes: img.sizeBytes,
|
|
504
|
+
timestamp: new Date(img.timestamp),
|
|
505
|
+
role: img.role,
|
|
506
|
+
},
|
|
507
|
+
})
|
|
508
|
+
result.imagesExtracted++
|
|
509
|
+
} catch {
|
|
510
|
+
// Unique constraint — already imported on a prior sync.
|
|
511
|
+
}
|
|
382
512
|
}
|
|
383
513
|
|
|
384
514
|
result.sessionsAdded++
|
|
@@ -388,6 +518,21 @@ export async function syncLogs(): Promise<SyncResult> {
|
|
|
388
518
|
}
|
|
389
519
|
}
|
|
390
520
|
|
|
521
|
+
// Roll up sub-agent contributions into the parent Session row so per-session
|
|
522
|
+
// tokens/cost reflect total work (top-level + subagents).
|
|
523
|
+
await prisma.$executeRaw`
|
|
524
|
+
UPDATE "Session" SET
|
|
525
|
+
"inputTokens" = COALESCE((SELECT SUM("inputTokens") FROM "MessageUsage" mu WHERE mu."sessionId" = "Session"."sessionId"), "inputTokens"),
|
|
526
|
+
"outputTokens" = COALESCE((SELECT SUM("outputTokens") FROM "MessageUsage" mu WHERE mu."sessionId" = "Session"."sessionId"), "outputTokens"),
|
|
527
|
+
"cacheCreationTokens" = COALESCE((SELECT SUM("cacheCreationTokens") FROM "MessageUsage" mu WHERE mu."sessionId" = "Session"."sessionId"), "cacheCreationTokens"),
|
|
528
|
+
"cacheReadTokens" = COALESCE((SELECT SUM("cacheReadTokens") FROM "MessageUsage" mu WHERE mu."sessionId" = "Session"."sessionId"), "cacheReadTokens"),
|
|
529
|
+
"costUSD" = COALESCE((SELECT SUM("costUSD") FROM "MessageUsage" mu WHERE mu."sessionId" = "Session"."sessionId"), "costUSD"),
|
|
530
|
+
"totalTokens" = COALESCE((
|
|
531
|
+
SELECT SUM("inputTokens" + "outputTokens" + "cacheCreationTokens" + "cacheReadTokens")
|
|
532
|
+
FROM "MessageUsage" mu WHERE mu."sessionId" = "Session"."sessionId"
|
|
533
|
+
), "totalTokens")
|
|
534
|
+
`
|
|
535
|
+
|
|
391
536
|
// Log the sync
|
|
392
537
|
await prisma.syncLog.create({
|
|
393
538
|
data: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentfit",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"description": "Fitness tracker dashboard for AI coding agents (Claude Code, Codex). Visualize usage, cost, tokens, and productivity from local conversation logs.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"usage-tracker"
|
|
16
16
|
],
|
|
17
17
|
"author": "Harry Wang",
|
|
18
|
-
"license": "
|
|
18
|
+
"license": "AGPL-3.0",
|
|
19
19
|
"repository": {
|
|
20
20
|
"type": "git",
|
|
21
21
|
"url": "https://github.com/harrywang/agentfit.git"
|
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
"clsx": "^2.1.1",
|
|
52
52
|
"dagre": "^0.8.5",
|
|
53
53
|
"date-fns": "^4.1.0",
|
|
54
|
+
"electron-updater": "^6.8.3",
|
|
54
55
|
"embla-carousel-react": "^8.6.0",
|
|
55
56
|
"lucide-react": "^1.7.0",
|
|
56
57
|
"next": "16.1.7",
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
-- CreateTable
|
|
2
|
+
CREATE TABLE "MessageUsage" (
|
|
3
|
+
"id" TEXT NOT NULL PRIMARY KEY,
|
|
4
|
+
"sessionId" TEXT NOT NULL,
|
|
5
|
+
"messageId" TEXT NOT NULL,
|
|
6
|
+
"requestId" TEXT NOT NULL,
|
|
7
|
+
"model" TEXT NOT NULL,
|
|
8
|
+
"speed" TEXT NOT NULL DEFAULT 'standard',
|
|
9
|
+
"timestamp" DATETIME NOT NULL,
|
|
10
|
+
"date" TEXT NOT NULL,
|
|
11
|
+
"inputTokens" INTEGER NOT NULL,
|
|
12
|
+
"outputTokens" INTEGER NOT NULL,
|
|
13
|
+
"cacheCreationTokens" INTEGER NOT NULL,
|
|
14
|
+
"cacheReadTokens" INTEGER NOT NULL,
|
|
15
|
+
"costUSD" REAL NOT NULL,
|
|
16
|
+
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
-- CreateIndex
|
|
20
|
+
CREATE INDEX "MessageUsage_date_idx" ON "MessageUsage"("date");
|
|
21
|
+
|
|
22
|
+
-- CreateIndex
|
|
23
|
+
CREATE INDEX "MessageUsage_sessionId_idx" ON "MessageUsage"("sessionId");
|
|
24
|
+
|
|
25
|
+
-- CreateIndex
|
|
26
|
+
CREATE INDEX "MessageUsage_model_idx" ON "MessageUsage"("model");
|
|
27
|
+
|
|
28
|
+
-- CreateIndex
|
|
29
|
+
CREATE UNIQUE INDEX "MessageUsage_messageId_requestId_key" ON "MessageUsage"("messageId", "requestId");
|
package/prisma/schema.prisma
CHANGED
|
@@ -42,6 +42,28 @@ model Session {
|
|
|
42
42
|
@@index([startTime])
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
model MessageUsage {
|
|
46
|
+
id String @id @default(cuid())
|
|
47
|
+
sessionId String
|
|
48
|
+
messageId String
|
|
49
|
+
requestId String
|
|
50
|
+
model String
|
|
51
|
+
speed String @default("standard") // "standard" | "fast"
|
|
52
|
+
timestamp DateTime
|
|
53
|
+
date String // YYYY-MM-DD bucket of timestamp (local date)
|
|
54
|
+
inputTokens Int
|
|
55
|
+
outputTokens Int
|
|
56
|
+
cacheCreationTokens Int
|
|
57
|
+
cacheReadTokens Int
|
|
58
|
+
costUSD Float
|
|
59
|
+
createdAt DateTime @default(now())
|
|
60
|
+
|
|
61
|
+
@@unique([messageId, requestId])
|
|
62
|
+
@@index([date])
|
|
63
|
+
@@index([sessionId])
|
|
64
|
+
@@index([model])
|
|
65
|
+
}
|
|
66
|
+
|
|
45
67
|
model Image {
|
|
46
68
|
id String @id @default(cuid())
|
|
47
69
|
sessionId String
|
package/prisma/schema.sql
CHANGED
|
@@ -31,6 +31,24 @@ CREATE TABLE IF NOT EXISTS "Session" (
|
|
|
31
31
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
32
32
|
);
|
|
33
33
|
|
|
34
|
+
-- CreateTable
|
|
35
|
+
CREATE TABLE IF NOT EXISTS "MessageUsage" (
|
|
36
|
+
"id" TEXT NOT NULL PRIMARY KEY,
|
|
37
|
+
"sessionId" TEXT NOT NULL,
|
|
38
|
+
"messageId" TEXT NOT NULL,
|
|
39
|
+
"requestId" TEXT NOT NULL,
|
|
40
|
+
"model" TEXT NOT NULL,
|
|
41
|
+
"speed" TEXT NOT NULL DEFAULT 'standard',
|
|
42
|
+
"timestamp" DATETIME NOT NULL,
|
|
43
|
+
"date" TEXT NOT NULL,
|
|
44
|
+
"inputTokens" INTEGER NOT NULL,
|
|
45
|
+
"outputTokens" INTEGER NOT NULL,
|
|
46
|
+
"cacheCreationTokens" INTEGER NOT NULL,
|
|
47
|
+
"cacheReadTokens" INTEGER NOT NULL,
|
|
48
|
+
"costUSD" REAL NOT NULL,
|
|
49
|
+
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
50
|
+
);
|
|
51
|
+
|
|
34
52
|
-- CreateTable
|
|
35
53
|
CREATE TABLE IF NOT EXISTS "Image" (
|
|
36
54
|
"id" TEXT NOT NULL PRIMARY KEY,
|
|
@@ -85,6 +103,18 @@ CREATE INDEX IF NOT EXISTS "Session_project_idx" ON "Session"("project");
|
|
|
85
103
|
-- CreateIndex
|
|
86
104
|
CREATE INDEX IF NOT EXISTS "Session_startTime_idx" ON "Session"("startTime");
|
|
87
105
|
|
|
106
|
+
-- CreateIndex
|
|
107
|
+
CREATE UNIQUE INDEX IF NOT EXISTS "MessageUsage_messageId_requestId_key" ON "MessageUsage"("messageId", "requestId");
|
|
108
|
+
|
|
109
|
+
-- CreateIndex
|
|
110
|
+
CREATE INDEX IF NOT EXISTS "MessageUsage_date_idx" ON "MessageUsage"("date");
|
|
111
|
+
|
|
112
|
+
-- CreateIndex
|
|
113
|
+
CREATE INDEX IF NOT EXISTS "MessageUsage_sessionId_idx" ON "MessageUsage"("sessionId");
|
|
114
|
+
|
|
115
|
+
-- CreateIndex
|
|
116
|
+
CREATE INDEX IF NOT EXISTS "MessageUsage_model_idx" ON "MessageUsage"("model");
|
|
117
|
+
|
|
88
118
|
-- CreateIndex
|
|
89
119
|
CREATE INDEX IF NOT EXISTS "Image_sessionId_idx" ON "Image"("sessionId");
|
|
90
120
|
|