feat: centralize cost breakdown calculations in pipeline
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>
This commit is contained in:
93
internal/pipeline/costs.go
Normal file
93
internal/pipeline/costs.go
Normal file
@@ -0,0 +1,93 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user