mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-05-21 18:44:57 +00:00
207 lines
5.8 KiB
TypeScript
207 lines
5.8 KiB
TypeScript
/// Rollup of one time window (today / 7 days / 30 days / month / all) used as the canonical
|
|
/// input to the menubar payload. Built inside the CLI and also consumed by the day-aggregator
|
|
/// when hydrating per-day cache entries.
|
|
export type PeriodData = {
|
|
label: string
|
|
cost: number
|
|
calls: number
|
|
sessions: number
|
|
inputTokens: number
|
|
outputTokens: number
|
|
cacheReadTokens: number
|
|
cacheWriteTokens: number
|
|
categories: Array<{
|
|
name: string
|
|
cost: number
|
|
turns: number
|
|
editTurns: number
|
|
oneShotTurns: number
|
|
inputTokens: number
|
|
outputTokens: number
|
|
cacheReadTokens: number
|
|
cacheWriteTokens: number
|
|
}>
|
|
models: Array<{ name: string; cost: number; calls: number }>
|
|
}
|
|
|
|
export type ProviderCost = {
|
|
name: string
|
|
cost: number
|
|
}
|
|
import type { OptimizeResult } from './optimize.js'
|
|
|
|
const TOP_ACTIVITIES_LIMIT = 20
|
|
const TOP_MODELS_LIMIT = 20
|
|
const TOP_FINDINGS_LIMIT = 10
|
|
const HISTORY_DAYS_LIMIT = 365
|
|
const SYNTHETIC_MODEL_NAME = '<synthetic>'
|
|
|
|
export type DailyModelBreakdown = {
|
|
name: string
|
|
cost: number
|
|
calls: number
|
|
inputTokens: number
|
|
outputTokens: number
|
|
}
|
|
|
|
export type DailyHistoryEntry = {
|
|
date: string
|
|
cost: number
|
|
calls: number
|
|
inputTokens: number
|
|
outputTokens: number
|
|
cacheReadTokens: number
|
|
cacheWriteTokens: number
|
|
topModels: DailyModelBreakdown[]
|
|
}
|
|
|
|
export type MenubarPayload = {
|
|
generated: string
|
|
current: {
|
|
label: string
|
|
cost: number
|
|
calls: number
|
|
sessions: number
|
|
oneShotRate: number | null
|
|
inputTokens: number
|
|
outputTokens: number
|
|
cacheReadTokens: number
|
|
cacheWriteTokens: number
|
|
cacheHitPercent: number
|
|
topActivities: Array<{
|
|
name: string
|
|
cost: number
|
|
turns: number
|
|
inputTokens: number
|
|
outputTokens: number
|
|
cacheReadTokens: number
|
|
cacheWriteTokens: number
|
|
oneShotRate: number | null
|
|
}>
|
|
topModels: Array<{
|
|
name: string
|
|
cost: number
|
|
calls: number
|
|
}>
|
|
providers: Record<string, number>
|
|
}
|
|
optimize: {
|
|
findingCount: number
|
|
savingsUSD: number
|
|
topFindings: Array<{
|
|
title: string
|
|
impact: 'high' | 'medium' | 'low'
|
|
savingsUSD: number
|
|
}>
|
|
}
|
|
history: {
|
|
daily: DailyHistoryEntry[]
|
|
}
|
|
}
|
|
|
|
function oneShotRateFor(editTurns: number, oneShotTurns: number): number | null {
|
|
if (editTurns === 0) return null
|
|
return oneShotTurns / editTurns
|
|
}
|
|
|
|
function aggregateOneShotRate(categories: PeriodData['categories']): number | null {
|
|
let edits = 0
|
|
let oneShots = 0
|
|
for (const cat of categories) {
|
|
edits += cat.editTurns
|
|
oneShots += cat.oneShotTurns
|
|
}
|
|
if (edits === 0) return null
|
|
return oneShots / edits
|
|
}
|
|
|
|
function cacheHitPercent(inputTokens: number, cacheReadTokens: number): number {
|
|
const denom = inputTokens + cacheReadTokens
|
|
if (denom === 0) return 0
|
|
return (cacheReadTokens / denom) * 100
|
|
}
|
|
|
|
function buildTopActivities(categories: PeriodData['categories']): MenubarPayload['current']['topActivities'] {
|
|
// The CLI supplies categories sorted by cost. There are fewer than 20 known
|
|
// task categories today, so the macOS token-mode resort still receives every
|
|
// category while keeping this payload compact if the taxonomy grows later.
|
|
return categories.slice(0, TOP_ACTIVITIES_LIMIT).map(cat => ({
|
|
name: cat.name,
|
|
cost: cat.cost,
|
|
turns: cat.turns,
|
|
inputTokens: cat.inputTokens,
|
|
outputTokens: cat.outputTokens,
|
|
cacheReadTokens: cat.cacheReadTokens,
|
|
cacheWriteTokens: cat.cacheWriteTokens,
|
|
oneShotRate: oneShotRateFor(cat.editTurns, cat.oneShotTurns),
|
|
}))
|
|
}
|
|
|
|
function buildTopModels(models: PeriodData['models']): MenubarPayload['current']['topModels'] {
|
|
return models
|
|
.filter(m => m.name !== SYNTHETIC_MODEL_NAME)
|
|
.slice(0, TOP_MODELS_LIMIT)
|
|
.map(m => ({ name: m.name, cost: m.cost, calls: m.calls }))
|
|
}
|
|
|
|
function buildOptimize(optimize: OptimizeResult | null): MenubarPayload['optimize'] {
|
|
if (!optimize || optimize.findings.length === 0) {
|
|
return { findingCount: 0, savingsUSD: 0, topFindings: [] }
|
|
}
|
|
const { findings, costRate } = optimize
|
|
const totalSavingsUSD = findings.reduce((s, f) => s + f.tokensSaved * costRate, 0)
|
|
const topFindings = findings.slice(0, TOP_FINDINGS_LIMIT).map(f => ({
|
|
title: f.title,
|
|
impact: f.impact,
|
|
savingsUSD: f.tokensSaved * costRate,
|
|
}))
|
|
return {
|
|
findingCount: findings.length,
|
|
savingsUSD: totalSavingsUSD,
|
|
topFindings,
|
|
}
|
|
}
|
|
|
|
function buildProviders(providers: ProviderCost[]): Record<string, number> {
|
|
const map: Record<string, number> = {}
|
|
for (const p of providers) {
|
|
if (p.cost < 0) continue
|
|
map[p.name.toLowerCase()] = p.cost
|
|
}
|
|
return map
|
|
}
|
|
|
|
function buildHistory(daily: DailyHistoryEntry[] | undefined): MenubarPayload['history'] {
|
|
if (!daily || daily.length === 0) return { daily: [] }
|
|
const sorted = [...daily].sort((a, b) => a.date.localeCompare(b.date))
|
|
const trimmed = sorted.slice(-HISTORY_DAYS_LIMIT)
|
|
return { daily: trimmed }
|
|
}
|
|
|
|
export function buildMenubarPayload(
|
|
current: PeriodData,
|
|
providers: ProviderCost[],
|
|
optimize: OptimizeResult | null,
|
|
dailyHistory?: DailyHistoryEntry[],
|
|
): MenubarPayload {
|
|
return {
|
|
generated: new Date().toISOString(),
|
|
current: {
|
|
label: current.label,
|
|
cost: current.cost,
|
|
calls: current.calls,
|
|
sessions: current.sessions,
|
|
oneShotRate: aggregateOneShotRate(current.categories),
|
|
inputTokens: current.inputTokens,
|
|
outputTokens: current.outputTokens,
|
|
cacheReadTokens: current.cacheReadTokens,
|
|
cacheWriteTokens: current.cacheWriteTokens,
|
|
cacheHitPercent: cacheHitPercent(current.inputTokens, current.cacheReadTokens),
|
|
topActivities: buildTopActivities(current.categories),
|
|
topModels: buildTopModels(current.models),
|
|
providers: buildProviders(providers),
|
|
},
|
|
optimize: buildOptimize(optimize),
|
|
history: buildHistory(dailyHistory),
|
|
}
|
|
}
|