From a09ffe6a0f3e45014d34ba0b0e5f3cee485c5972 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 10 Mar 2026 20:58:59 +0800 Subject: [PATCH] ccm/ocm: Add `by_user_and_week` cost summary --- service/ccm/service_usage.go | 37 +++++++++++++++++++++++++++++++++--- service/ocm/service_usage.go | 37 +++++++++++++++++++++++++++++++++--- 2 files changed, 68 insertions(+), 6 deletions(-) diff --git a/service/ccm/service_usage.go b/service/ccm/service_usage.go index 7d776774..36e9ee65 100644 --- a/service/ccm/service_usage.go +++ b/service/ccm/service_usage.go @@ -65,9 +65,10 @@ type CostCombinationJSON struct { } type CostsSummaryJSON struct { - TotalUSD float64 `json:"total_usd"` - ByUser map[string]float64 `json:"by_user"` - ByWeek map[string]float64 `json:"by_week,omitempty"` + TotalUSD float64 `json:"total_usd"` + ByUser map[string]float64 `json:"by_user"` + ByWeek map[string]float64 `json:"by_week,omitempty"` + ByUserAndWeek map[string]map[string]float64 `json:"by_user_and_week,omitempty"` } type AggregatedUsageJSON struct { @@ -492,6 +493,31 @@ func buildByWeekCost(combinations []CostCombination) map[string]float64 { return byWeek } +func buildByUserAndWeekCost(combinations []CostCombination) map[string]map[string]float64 { + byUserAndWeek := make(map[string]map[string]float64) + for _, combination := range combinations { + if combination.WeekStartUnix <= 0 { + continue + } + weekStartAt := time.Unix(combination.WeekStartUnix, 0).UTC() + weekKey := formatWeekStartKey(weekStartAt) + for user, userStats := range combination.ByUser { + userWeeks, exists := byUserAndWeek[user] + if !exists { + userWeeks = make(map[string]float64) + byUserAndWeek[user] = userWeeks + } + userWeeks[weekKey] += calculateCost(userStats, combination.Model, combination.ContextWindow) + } + } + for _, weekCosts := range byUserAndWeek { + for weekKey, cost := range weekCosts { + weekCosts[weekKey] = roundCost(cost) + } + } + return byUserAndWeek +} + func deriveWeekStartUnix(cycleHint *WeeklyCycleHint) int64 { if cycleHint == nil || cycleHint.WindowMinutes <= 0 || cycleHint.ResetAt.IsZero() { return 0 @@ -522,6 +548,11 @@ func (u *AggregatedUsage) ToJSON() *AggregatedUsageJSON { result.Costs.ByWeek = nil } + result.Costs.ByUserAndWeek = buildByUserAndWeekCost(u.Combinations) + if len(result.Costs.ByUserAndWeek) == 0 { + result.Costs.ByUserAndWeek = nil + } + for user, cost := range result.Costs.ByUser { result.Costs.ByUser[user] = roundCost(cost) } diff --git a/service/ocm/service_usage.go b/service/ocm/service_usage.go index a4c1d1c8..95d401a4 100644 --- a/service/ocm/service_usage.go +++ b/service/ocm/service_usage.go @@ -80,9 +80,10 @@ type CostCombinationJSON struct { } type CostsSummaryJSON struct { - TotalUSD float64 `json:"total_usd"` - ByUser map[string]float64 `json:"by_user"` - ByWeek map[string]float64 `json:"by_week,omitempty"` + TotalUSD float64 `json:"total_usd"` + ByUser map[string]float64 `json:"by_user"` + ByWeek map[string]float64 `json:"by_week,omitempty"` + ByUserAndWeek map[string]map[string]float64 `json:"by_user_and_week,omitempty"` } type AggregatedUsageJSON struct { @@ -864,6 +865,31 @@ func buildByWeekCost(combinations []CostCombination) map[string]float64 { return byWeek } +func buildByUserAndWeekCost(combinations []CostCombination) map[string]map[string]float64 { + byUserAndWeek := make(map[string]map[string]float64) + for _, combination := range combinations { + if combination.WeekStartUnix <= 0 { + continue + } + weekStartAt := time.Unix(combination.WeekStartUnix, 0).UTC() + weekKey := formatWeekStartKey(weekStartAt) + for user, userStats := range combination.ByUser { + userWeeks, exists := byUserAndWeek[user] + if !exists { + userWeeks = make(map[string]float64) + byUserAndWeek[user] = userWeeks + } + userWeeks[weekKey] += calculateCost(userStats, combination.Model, combination.ServiceTier) + } + } + for _, weekCosts := range byUserAndWeek { + for weekKey, cost := range weekCosts { + weekCosts[weekKey] = roundCost(cost) + } + } + return byUserAndWeek +} + func deriveWeekStartUnix(cycleHint *WeeklyCycleHint) int64 { if cycleHint == nil || cycleHint.WindowMinutes <= 0 || cycleHint.ResetAt.IsZero() { return 0 @@ -894,6 +920,11 @@ func (u *AggregatedUsage) ToJSON() *AggregatedUsageJSON { result.Costs.ByWeek = nil } + result.Costs.ByUserAndWeek = buildByUserAndWeekCost(u.Combinations) + if len(result.Costs.ByUserAndWeek) == 0 { + result.Costs.ByUserAndWeek = nil + } + for user, cost := range result.Costs.ByUser { result.Costs.ByUser[user] = roundCost(cost) }