agentfit 0.1.9 → 0.1.10
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/components/data-provider.tsx +70 -53
- package/lib/format.ts +12 -0
- package/lib/pricing.ts +7 -1
- package/lib/sync.ts +5 -11
- package/package.json +1 -1
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { createContext, useContext, useEffect, useState, useCallback, useMemo, type ReactNode } from 'react'
|
|
4
4
|
import { toast } from 'sonner'
|
|
5
5
|
import type { UsageData, SessionSummary, DailyUsage, ProjectSummary, OverviewStats } from '@/lib/parse-logs'
|
|
6
|
+
import { formatLocalDate } from '@/lib/format'
|
|
6
7
|
|
|
7
8
|
export type AgentType = 'claude' | 'codex' | 'combined'
|
|
8
9
|
export type TimeRange = '7d' | '30d' | '90d' | 'all'
|
|
@@ -58,6 +59,7 @@ function filterData(raw: UsageData | null, range: TimeRange, project: string): U
|
|
|
58
59
|
const cutoff = new Date()
|
|
59
60
|
cutoff.setDate(cutoff.getDate() - days)
|
|
60
61
|
const cutoffISO = range === 'all' ? '' : cutoff.toISOString()
|
|
62
|
+
const cutoffDate = range === 'all' ? '' : formatLocalDate(cutoff)
|
|
61
63
|
|
|
62
64
|
// Filter sessions by time range and project
|
|
63
65
|
const sessions = raw.sessions.filter(s => {
|
|
@@ -66,14 +68,57 @@ function filterData(raw: UsageData | null, range: TimeRange, project: string): U
|
|
|
66
68
|
return true
|
|
67
69
|
})
|
|
68
70
|
|
|
69
|
-
//
|
|
71
|
+
// Daily: preserve the server's per-message-derived rows (with
|
|
72
|
+
// modelBreakdowns intact, including haiku/sub-agent tokens) when the
|
|
73
|
+
// project filter is inactive — only trim by date. Re-aggregating from
|
|
74
|
+
// session rollups loses the per-(date, model) split, mis-buckets
|
|
75
|
+
// cross-midnight tokens, and can drift from ccusage. When a project
|
|
76
|
+
// filter IS active we have no choice but to rebuild from sessions, since
|
|
77
|
+
// per-message rows aren't tagged by project on the client.
|
|
78
|
+
let daily: DailyUsage[]
|
|
79
|
+
if (project === 'all') {
|
|
80
|
+
daily = raw.daily.filter(d => range === 'all' || d.date >= cutoffDate)
|
|
81
|
+
} else {
|
|
82
|
+
const dailyMap = new Map<string, DailyUsage>()
|
|
83
|
+
for (const s of sessions) {
|
|
84
|
+
const date = formatLocalDate(new Date(s.startTime))
|
|
85
|
+
if (!dailyMap.has(date)) {
|
|
86
|
+
dailyMap.set(date, {
|
|
87
|
+
date, sessions: 0, messages: 0,
|
|
88
|
+
userMessages: 0, assistantMessages: 0,
|
|
89
|
+
inputTokens: 0, outputTokens: 0,
|
|
90
|
+
cacheCreationTokens: 0, cacheReadTokens: 0,
|
|
91
|
+
totalTokens: 0, costUSD: 0, toolCalls: 0,
|
|
92
|
+
toolCallsDetail: {}, interruptions: 0, rateLimitErrors: 0,
|
|
93
|
+
modelBreakdowns: [],
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
const day = dailyMap.get(date)!
|
|
97
|
+
day.sessions++
|
|
98
|
+
day.messages += s.totalMessages
|
|
99
|
+
day.userMessages += s.userMessages
|
|
100
|
+
day.assistantMessages += s.assistantMessages
|
|
101
|
+
day.inputTokens += s.inputTokens
|
|
102
|
+
day.outputTokens += s.outputTokens
|
|
103
|
+
day.cacheCreationTokens += s.cacheCreationTokens
|
|
104
|
+
day.cacheReadTokens += s.cacheReadTokens
|
|
105
|
+
day.totalTokens += s.totalTokens
|
|
106
|
+
day.costUSD += s.costUSD
|
|
107
|
+
day.toolCalls += s.toolCallsTotal
|
|
108
|
+
day.interruptions += s.userInterruptions
|
|
109
|
+
day.rateLimitErrors += s.rateLimitErrors
|
|
110
|
+
for (const [tool, count] of Object.entries(s.toolCalls)) {
|
|
111
|
+
day.toolCallsDetail[tool] = (day.toolCallsDetail[tool] || 0) + count
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
daily = Array.from(dailyMap.values()).sort((a, b) => a.date.localeCompare(b.date))
|
|
115
|
+
}
|
|
116
|
+
|
|
70
117
|
const projectMap = new Map<string, ProjectSummary>()
|
|
71
|
-
const dailyMap = new Map<string, DailyUsage>()
|
|
72
118
|
const toolUsage: Record<string, number> = {}
|
|
73
119
|
const models: Record<string, number> = {}
|
|
74
120
|
|
|
75
121
|
for (const s of sessions) {
|
|
76
|
-
// Projects
|
|
77
122
|
if (!projectMap.has(s.project)) {
|
|
78
123
|
projectMap.set(s.project, {
|
|
79
124
|
name: s.project,
|
|
@@ -94,55 +139,27 @@ function filterData(raw: UsageData | null, range: TimeRange, project: string): U
|
|
|
94
139
|
proj.totalDurationMinutes += s.durationMinutes
|
|
95
140
|
for (const [tool, count] of Object.entries(s.toolCalls)) {
|
|
96
141
|
proj.toolCalls[tool] = (proj.toolCalls[tool] || 0) + count
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// Daily
|
|
100
|
-
const date = s.startTime.slice(0, 10)
|
|
101
|
-
if (!dailyMap.has(date)) {
|
|
102
|
-
dailyMap.set(date, {
|
|
103
|
-
date, sessions: 0, messages: 0,
|
|
104
|
-
userMessages: 0, assistantMessages: 0,
|
|
105
|
-
inputTokens: 0, outputTokens: 0,
|
|
106
|
-
cacheCreationTokens: 0, cacheReadTokens: 0,
|
|
107
|
-
totalTokens: 0, costUSD: 0, toolCalls: 0,
|
|
108
|
-
toolCallsDetail: {}, interruptions: 0, rateLimitErrors: 0,
|
|
109
|
-
// Client-side rebuild: per-(date, model) breakdown is not derivable
|
|
110
|
-
// from session-level rows. The unfiltered case (project=all,
|
|
111
|
-
// range=all) returns raw and preserves the server's breakdowns.
|
|
112
|
-
modelBreakdowns: [],
|
|
113
|
-
})
|
|
114
|
-
}
|
|
115
|
-
const day = dailyMap.get(date)!
|
|
116
|
-
day.sessions++
|
|
117
|
-
day.messages += s.totalMessages
|
|
118
|
-
day.userMessages += s.userMessages
|
|
119
|
-
day.assistantMessages += s.assistantMessages
|
|
120
|
-
day.inputTokens += s.inputTokens
|
|
121
|
-
day.outputTokens += s.outputTokens
|
|
122
|
-
day.cacheCreationTokens += s.cacheCreationTokens
|
|
123
|
-
day.cacheReadTokens += s.cacheReadTokens
|
|
124
|
-
day.totalTokens += s.totalTokens
|
|
125
|
-
day.costUSD += s.costUSD
|
|
126
|
-
day.toolCalls += s.toolCallsTotal
|
|
127
|
-
day.interruptions += s.userInterruptions
|
|
128
|
-
day.rateLimitErrors += s.rateLimitErrors
|
|
129
|
-
for (const [tool, count] of Object.entries(s.toolCalls)) {
|
|
130
|
-
day.toolCallsDetail[tool] = (day.toolCallsDetail[tool] || 0) + count
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// Tools
|
|
134
|
-
for (const [tool, count] of Object.entries(s.toolCalls)) {
|
|
135
142
|
toolUsage[tool] = (toolUsage[tool] || 0) + count
|
|
136
143
|
}
|
|
137
|
-
|
|
138
|
-
// Models — aggregate at message level
|
|
139
144
|
for (const [m, count] of Object.entries(s.modelCounts || {})) {
|
|
140
145
|
models[m] = (models[m] || 0) + count
|
|
141
146
|
}
|
|
142
147
|
}
|
|
143
148
|
|
|
144
149
|
const projects = Array.from(projectMap.values()).sort((a, b) => b.totalCost - a.totalCost)
|
|
145
|
-
|
|
150
|
+
|
|
151
|
+
// Overview token/cost totals come from the daily rows so the overview
|
|
152
|
+
// and the daily breakdown agree (mirrors getUsageData on the server).
|
|
153
|
+
const tokenTotals = daily.reduce(
|
|
154
|
+
(acc, d) => ({
|
|
155
|
+
input: acc.input + d.inputTokens,
|
|
156
|
+
output: acc.output + d.outputTokens,
|
|
157
|
+
cc: acc.cc + d.cacheCreationTokens,
|
|
158
|
+
cr: acc.cr + d.cacheReadTokens,
|
|
159
|
+
cost: acc.cost + d.costUSD,
|
|
160
|
+
}),
|
|
161
|
+
{ input: 0, output: 0, cc: 0, cr: 0, cost: 0 }
|
|
162
|
+
)
|
|
146
163
|
|
|
147
164
|
const overview: OverviewStats = {
|
|
148
165
|
totalSessions: sessions.length,
|
|
@@ -150,22 +167,22 @@ function filterData(raw: UsageData | null, range: TimeRange, project: string): U
|
|
|
150
167
|
totalMessages: sessions.reduce((a, s) => a + s.totalMessages, 0),
|
|
151
168
|
totalUserMessages: sessions.reduce((a, s) => a + s.userMessages, 0),
|
|
152
169
|
totalAssistantMessages: sessions.reduce((a, s) => a + s.assistantMessages, 0),
|
|
153
|
-
totalInputTokens:
|
|
154
|
-
totalOutputTokens:
|
|
155
|
-
totalCacheCreationTokens:
|
|
156
|
-
totalCacheReadTokens:
|
|
157
|
-
totalTokens:
|
|
158
|
-
totalCostUSD:
|
|
170
|
+
totalInputTokens: tokenTotals.input,
|
|
171
|
+
totalOutputTokens: tokenTotals.output,
|
|
172
|
+
totalCacheCreationTokens: tokenTotals.cc,
|
|
173
|
+
totalCacheReadTokens: tokenTotals.cr,
|
|
174
|
+
totalTokens: tokenTotals.input + tokenTotals.output + tokenTotals.cc + tokenTotals.cr,
|
|
175
|
+
totalCostUSD: tokenTotals.cost,
|
|
159
176
|
totalSystemPromptEdits: sessions.reduce((a, s) => a + (s.systemPromptEdits ?? 0), 0),
|
|
160
177
|
totalDurationMinutes: sessions.reduce((a, s) => a + s.durationMinutes, 0),
|
|
161
178
|
totalToolCalls: sessions.reduce((a, s) => a + s.toolCallsTotal, 0),
|
|
162
179
|
totalApiErrors: sessions.reduce((a, s) => a + s.apiErrors, 0),
|
|
163
180
|
totalRateLimitDays: (() => {
|
|
164
|
-
const
|
|
181
|
+
const rateDays = new Set<string>()
|
|
165
182
|
for (const s of sessions) {
|
|
166
|
-
if (s.rateLimitErrors > 0)
|
|
183
|
+
if (s.rateLimitErrors > 0) rateDays.add(formatLocalDate(new Date(s.startTime)))
|
|
167
184
|
}
|
|
168
|
-
return
|
|
185
|
+
return rateDays.size
|
|
169
186
|
})(),
|
|
170
187
|
totalUserInterruptions: sessions.reduce((a, s) => a + s.userInterruptions, 0),
|
|
171
188
|
models,
|
package/lib/format.ts
CHANGED
|
@@ -24,3 +24,15 @@ export function formatDuration(minutes: number): string {
|
|
|
24
24
|
export function formatNumber(n: number): string {
|
|
25
25
|
return n.toLocaleString()
|
|
26
26
|
}
|
|
27
|
+
|
|
28
|
+
// Local-timezone YYYY-MM-DD bucket. Matches ccusage's _date-utils.ts so daily
|
|
29
|
+
// totals agree to the cent — UTC slicing shifts cross-midnight tokens.
|
|
30
|
+
const LOCAL_DATE_FORMATTER = new Intl.DateTimeFormat('en-CA', {
|
|
31
|
+
year: 'numeric',
|
|
32
|
+
month: '2-digit',
|
|
33
|
+
day: '2-digit',
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
export function formatLocalDate(d: Date): string {
|
|
37
|
+
return LOCAL_DATE_FORMATTER.format(d)
|
|
38
|
+
}
|
package/lib/pricing.ts
CHANGED
|
@@ -28,7 +28,10 @@ export async function loadPricing(): Promise<Record<string, ModelPricing>> {
|
|
|
28
28
|
if (pricingCache) return pricingCache
|
|
29
29
|
|
|
30
30
|
try {
|
|
31
|
-
|
|
31
|
+
// Bypass Next's data cache; we memoize the parsed result in
|
|
32
|
+
// `pricingCache` ourselves. Double-caching once let an empty body
|
|
33
|
+
// freeze every newly-listed model at cost=0 until process restart.
|
|
34
|
+
const res = await fetch(LITELLM_PRICING_URL, { cache: 'no-store' })
|
|
32
35
|
const data = await res.json()
|
|
33
36
|
const filtered: Record<string, ModelPricing> = {}
|
|
34
37
|
for (const [key, value] of Object.entries(data)) {
|
|
@@ -55,6 +58,9 @@ export async function loadPricing(): Promise<Record<string, ModelPricing>> {
|
|
|
55
58
|
: undefined,
|
|
56
59
|
}
|
|
57
60
|
}
|
|
61
|
+
// Refuse to memoize an empty map — that would be a transient fetch
|
|
62
|
+
// hiccup turning into a process-lifetime zero-cost lock.
|
|
63
|
+
if (Object.keys(filtered).length === 0) return getFallbackPricing()
|
|
58
64
|
pricingCache = filtered
|
|
59
65
|
return filtered
|
|
60
66
|
} catch {
|
package/lib/sync.ts
CHANGED
|
@@ -3,19 +3,11 @@ import path from 'path'
|
|
|
3
3
|
import os from 'os'
|
|
4
4
|
import { prisma } from './db'
|
|
5
5
|
import { loadPricing, calculateCost, type ModelPricing, type Speed } from './pricing'
|
|
6
|
+
import { formatLocalDate } from './format'
|
|
6
7
|
|
|
7
8
|
const PROJECTS_DIR = path.join(os.homedir(), '.claude', 'projects')
|
|
8
9
|
const IMAGES_DIR = path.resolve(process.cwd(), 'data', 'images')
|
|
9
10
|
|
|
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
|
-
|
|
19
11
|
interface LogEntry {
|
|
20
12
|
type?: string
|
|
21
13
|
uuid?: string
|
|
@@ -242,7 +234,7 @@ function parseSessionFile(
|
|
|
242
234
|
model: currentModel,
|
|
243
235
|
speed,
|
|
244
236
|
timestamp: ts,
|
|
245
|
-
date:
|
|
237
|
+
date: formatLocalDate(ts),
|
|
246
238
|
inputTokens: inT,
|
|
247
239
|
outputTokens: outT,
|
|
248
240
|
cacheCreationTokens: ccT,
|
|
@@ -417,7 +409,7 @@ export async function syncLogs(): Promise<SyncResult> {
|
|
|
417
409
|
if (isSubagent) {
|
|
418
410
|
for (const m of parsed.messageUsages) {
|
|
419
411
|
await prisma.$executeRaw`
|
|
420
|
-
INSERT
|
|
412
|
+
INSERT INTO "MessageUsage" (
|
|
421
413
|
"id", "sessionId", "messageId", "requestId", "model", "speed",
|
|
422
414
|
"timestamp", "date",
|
|
423
415
|
"inputTokens", "outputTokens", "cacheCreationTokens", "cacheReadTokens",
|
|
@@ -429,6 +421,8 @@ export async function syncLogs(): Promise<SyncResult> {
|
|
|
429
421
|
${m.inputTokens}, ${m.outputTokens}, ${m.cacheCreationTokens}, ${m.cacheReadTokens},
|
|
430
422
|
${m.costUSD}, ${new Date().toISOString()}
|
|
431
423
|
)
|
|
424
|
+
ON CONFLICT("messageId", "requestId") DO UPDATE SET
|
|
425
|
+
"costUSD" = MAX("MessageUsage"."costUSD", excluded."costUSD")
|
|
432
426
|
`
|
|
433
427
|
}
|
|
434
428
|
result.sessionsAdded++
|
package/package.json
CHANGED