mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-05-21 02:07:12 +00:00
Timestamps in session files are UTC ISO strings. Several code paths extracted the date via .slice(0, 10) which gives the UTC date, while date range filtering uses local-time boundaries. This caused turns between UTC midnight and local midnight to be bucketed under the wrong day -- the menubar showed lower today cost than the TUI because those turns were attributed to tomorrow (UTC) but filtered as today (local). format.ts already had a localDateString fix; this applies the same pattern everywhere via dateKey() in day-aggregator.ts.
143 lines
4.8 KiB
TypeScript
143 lines
4.8 KiB
TypeScript
import type { DailyEntry } from './daily-cache.js'
|
|
import type { PeriodData } from './menubar-json.js'
|
|
import { CATEGORY_LABELS, type ProjectSummary, type TaskCategory } from './types.js'
|
|
|
|
function emptyEntry(date: string): DailyEntry {
|
|
return {
|
|
date,
|
|
cost: 0,
|
|
calls: 0,
|
|
sessions: 0,
|
|
inputTokens: 0,
|
|
outputTokens: 0,
|
|
cacheReadTokens: 0,
|
|
cacheWriteTokens: 0,
|
|
editTurns: 0,
|
|
oneShotTurns: 0,
|
|
models: {},
|
|
categories: {},
|
|
providers: {},
|
|
}
|
|
}
|
|
|
|
export function dateKey(iso: string): string {
|
|
const d = new Date(iso)
|
|
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
|
|
}
|
|
|
|
export function aggregateProjectsIntoDays(projects: ProjectSummary[]): DailyEntry[] {
|
|
const byDate = new Map<string, DailyEntry>()
|
|
const ensure = (date: string): DailyEntry => {
|
|
let d = byDate.get(date)
|
|
if (!d) { d = emptyEntry(date); byDate.set(date, d) }
|
|
return d
|
|
}
|
|
|
|
for (const project of projects) {
|
|
for (const session of project.sessions) {
|
|
const sessionDate = dateKey(session.firstTimestamp)
|
|
ensure(sessionDate).sessions += 1
|
|
|
|
for (const turn of session.turns) {
|
|
if (turn.assistantCalls.length === 0) continue
|
|
const turnDate = dateKey(turn.assistantCalls[0]!.timestamp)
|
|
const turnDay = ensure(turnDate)
|
|
|
|
const editTurns = turn.hasEdits ? 1 : 0
|
|
const oneShotTurns = turn.hasEdits && turn.retries === 0 ? 1 : 0
|
|
const turnCost = turn.assistantCalls.reduce((s, c) => s + c.costUSD, 0)
|
|
|
|
turnDay.editTurns += editTurns
|
|
turnDay.oneShotTurns += oneShotTurns
|
|
|
|
const cat = turnDay.categories[turn.category] ?? { turns: 0, cost: 0, editTurns: 0, oneShotTurns: 0 }
|
|
cat.turns += 1
|
|
cat.cost += turnCost
|
|
cat.editTurns += editTurns
|
|
cat.oneShotTurns += oneShotTurns
|
|
turnDay.categories[turn.category] = cat
|
|
|
|
for (const call of turn.assistantCalls) {
|
|
const callDate = dateKey(call.timestamp)
|
|
const callDay = ensure(callDate)
|
|
|
|
callDay.cost += call.costUSD
|
|
callDay.calls += 1
|
|
callDay.inputTokens += call.usage.inputTokens
|
|
callDay.outputTokens += call.usage.outputTokens
|
|
callDay.cacheReadTokens += call.usage.cacheReadInputTokens
|
|
callDay.cacheWriteTokens += call.usage.cacheCreationInputTokens
|
|
|
|
const model = callDay.models[call.model] ?? {
|
|
calls: 0, cost: 0,
|
|
inputTokens: 0, outputTokens: 0,
|
|
cacheReadTokens: 0, cacheWriteTokens: 0,
|
|
}
|
|
model.calls += 1
|
|
model.cost += call.costUSD
|
|
model.inputTokens += call.usage.inputTokens
|
|
model.outputTokens += call.usage.outputTokens
|
|
model.cacheReadTokens += call.usage.cacheReadInputTokens
|
|
model.cacheWriteTokens += call.usage.cacheCreationInputTokens
|
|
callDay.models[call.model] = model
|
|
|
|
const provider = callDay.providers[call.provider] ?? { calls: 0, cost: 0 }
|
|
provider.calls += 1
|
|
provider.cost += call.costUSD
|
|
callDay.providers[call.provider] = provider
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return [...byDate.values()].sort((a, b) => a.date.localeCompare(b.date))
|
|
}
|
|
|
|
export function buildPeriodDataFromDays(days: DailyEntry[], label: string): PeriodData {
|
|
let cost = 0, calls = 0, sessions = 0
|
|
let inputTokens = 0, outputTokens = 0, cacheReadTokens = 0, cacheWriteTokens = 0
|
|
const catTotals: Record<string, { turns: number; cost: number; editTurns: number; oneShotTurns: number }> = {}
|
|
const modelTotals: Record<string, { calls: number; cost: number }> = {}
|
|
|
|
for (const d of days) {
|
|
cost += d.cost
|
|
calls += d.calls
|
|
sessions += d.sessions
|
|
inputTokens += d.inputTokens
|
|
outputTokens += d.outputTokens
|
|
cacheReadTokens += d.cacheReadTokens
|
|
cacheWriteTokens += d.cacheWriteTokens
|
|
|
|
for (const [name, m] of Object.entries(d.models)) {
|
|
const acc = modelTotals[name] ?? { calls: 0, cost: 0 }
|
|
acc.calls += m.calls
|
|
acc.cost += m.cost
|
|
modelTotals[name] = acc
|
|
}
|
|
for (const [cat, c] of Object.entries(d.categories)) {
|
|
const acc = catTotals[cat] ?? { turns: 0, cost: 0, editTurns: 0, oneShotTurns: 0 }
|
|
acc.turns += c.turns
|
|
acc.cost += c.cost
|
|
acc.editTurns += c.editTurns
|
|
acc.oneShotTurns += c.oneShotTurns
|
|
catTotals[cat] = acc
|
|
}
|
|
}
|
|
|
|
return {
|
|
label,
|
|
cost,
|
|
calls,
|
|
sessions,
|
|
inputTokens,
|
|
outputTokens,
|
|
cacheReadTokens,
|
|
cacheWriteTokens,
|
|
categories: Object.entries(catTotals)
|
|
.sort(([, a], [, b]) => b.cost - a.cost)
|
|
.map(([cat, d]) => ({ name: CATEGORY_LABELS[cat as TaskCategory] ?? cat, ...d })),
|
|
models: Object.entries(modelTotals)
|
|
.sort(([, a], [, b]) => b.cost - a.cost)
|
|
.map(([name, d]) => ({ name, ...d })),
|
|
}
|
|
}
|