Extract token-type and per-model cost calculations from cmd/costs.go into a dedicated pipeline.AggregateCostBreakdown() function. This eliminates duplicate cost calculation logic between CLI and TUI. New types: - TokenTypeCosts: aggregate costs by input/output/cache types - ModelCostBreakdown: per-model cost components Benefits: - Single source of truth for cost calculations - Uses LookupPricingAt() for historical accuracy - Both CLI and TUI now share the same cost logic - Easier to maintain and extend Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
94 lines
2.7 KiB
Go
94 lines
2.7 KiB
Go
package pipeline
|
|
|
|
import (
|
|
"sort"
|
|
"time"
|
|
|
|
"github.com/theirongolddev/cburn/internal/config"
|
|
"github.com/theirongolddev/cburn/internal/model"
|
|
)
|
|
|
|
// TokenTypeCosts holds aggregate costs split by token type.
|
|
type TokenTypeCosts struct {
|
|
InputCost float64
|
|
OutputCost float64
|
|
Cache5mCost float64
|
|
Cache1hCost float64
|
|
CacheReadCost float64
|
|
CacheCost float64
|
|
TotalCost float64
|
|
}
|
|
|
|
// ModelCostBreakdown holds cost components for one model.
|
|
type ModelCostBreakdown struct {
|
|
Model string
|
|
InputCost float64
|
|
OutputCost float64
|
|
Cache5mCost float64
|
|
Cache1hCost float64
|
|
CacheReadCost float64
|
|
CacheCost float64
|
|
TotalCost float64
|
|
}
|
|
|
|
// AggregateCostBreakdown computes token-type and model cost splits.
|
|
// Pricing is resolved at each session timestamp.
|
|
func AggregateCostBreakdown(
|
|
sessions []model.SessionStats,
|
|
since time.Time,
|
|
until time.Time,
|
|
) (TokenTypeCosts, []ModelCostBreakdown) {
|
|
filtered := FilterByTime(sessions, since, until)
|
|
|
|
var totals TokenTypeCosts
|
|
byModel := make(map[string]*ModelCostBreakdown)
|
|
|
|
for _, s := range filtered {
|
|
for modelName, usage := range s.Models {
|
|
pricing, ok := config.LookupPricingAt(modelName, s.StartTime)
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
inputCost := float64(usage.InputTokens) * pricing.InputPerMTok / 1_000_000
|
|
outputCost := float64(usage.OutputTokens) * pricing.OutputPerMTok / 1_000_000
|
|
cache5mCost := float64(usage.CacheCreation5mTokens) * pricing.CacheWrite5mPerMTok / 1_000_000
|
|
cache1hCost := float64(usage.CacheCreation1hTokens) * pricing.CacheWrite1hPerMTok / 1_000_000
|
|
cacheReadCost := float64(usage.CacheReadTokens) * pricing.CacheReadPerMTok / 1_000_000
|
|
|
|
totals.InputCost += inputCost
|
|
totals.OutputCost += outputCost
|
|
totals.Cache5mCost += cache5mCost
|
|
totals.Cache1hCost += cache1hCost
|
|
totals.CacheReadCost += cacheReadCost
|
|
|
|
row, exists := byModel[modelName]
|
|
if !exists {
|
|
row = &ModelCostBreakdown{Model: modelName}
|
|
byModel[modelName] = row
|
|
}
|
|
row.InputCost += inputCost
|
|
row.OutputCost += outputCost
|
|
row.Cache5mCost += cache5mCost
|
|
row.Cache1hCost += cache1hCost
|
|
row.CacheReadCost += cacheReadCost
|
|
}
|
|
}
|
|
|
|
totals.CacheCost = totals.Cache5mCost + totals.Cache1hCost + totals.CacheReadCost
|
|
totals.TotalCost = totals.InputCost + totals.OutputCost + totals.CacheCost
|
|
|
|
modelRows := make([]ModelCostBreakdown, 0, len(byModel))
|
|
for _, row := range byModel {
|
|
row.CacheCost = row.Cache5mCost + row.Cache1hCost + row.CacheReadCost
|
|
row.TotalCost = row.InputCost + row.OutputCost + row.CacheCost
|
|
modelRows = append(modelRows, *row)
|
|
}
|
|
|
|
sort.Slice(modelRows, func(i, j int) bool {
|
|
return modelRows[i].TotalCost > modelRows[j].TotalCost
|
|
})
|
|
|
|
return totals, modelRows
|
|
}
|