Go 62 lines
package ai
// TokenTotals is the summed token usage across ledger records. The fields
// mirror the unexported per-call accounting (input, cached input, output);
// these are real provider counts, never estimates.
type TokenTotals struct {
Input int
CachedInput int
Output int
}
// Total is the sum of the three token classes.
func (t TokenTotals) Total() int { return t.Input + t.CachedInput + t.Output }
// UsageSummary is the aggregated view of the AI-calls ledger
// (<workspace>/state/ai-calls.json). It is the read-only seam cmd/eeco uses
// to surface cumulative AI usage without reaching the unexported ledger types.
type UsageSummary struct {
TotalCalls int
Ran int
Parked int
Tokens TokenTotals
ByProvider map[string]int // provider name -> call count
FirstTS string // earliest record ts ("" if no dated records)
LastTS string // latest record ts ("" if no dated records)
}
// Summarize aggregates <stateDir>/ai-calls.json into a UsageSummary. A
// missing or corrupt ledger yields the zero summary (loadAICalls already
// degrades both to the empty ledger), so callers never need to special-case
// an absent workspace. ts values are RFC 3339 UTC, so a lexical compare is
// chronological — no time.Parse and no clock seam are needed; the date range
// derives from the data itself.
func Summarize(stateDir string) UsageSummary {
ledger := loadAICalls(stateDir)
sum := UsageSummary{ByProvider: map[string]int{}}
for _, r := range ledger.Records {
sum.TotalCalls++
if r.Ran {
sum.Ran++
}
if r.Parked {
sum.Parked++
}
sum.Tokens.Input += r.Tokens.Input
sum.Tokens.CachedInput += r.Tokens.CachedInput
sum.Tokens.Output += r.Tokens.Output
if r.Provider != "" {
sum.ByProvider[r.Provider]++
}
if r.TS != "" {
if sum.FirstTS == "" || r.TS < sum.FirstTS {
sum.FirstTS = r.TS
}
if r.TS > sum.LastTS {
sum.LastTS = r.TS
}
}
}
return sum
}