package api import ( "net/http" "time" "cpa-usage-keeper/internal/cpa" "cpa-usage-keeper/internal/redact" "cpa-usage-keeper/internal/service" "github.com/gin-gonic/gin" ) type usageOverviewResponse struct { Usage usageOverviewPayload `json:"usage"` Summary usageOverviewSummary `json:"summary"` Series usageOverviewSeries `json:"series"` HourlySeries usageOverviewSeries `json:"hourly_series"` DailySeries usageOverviewSeries `json:"daily_series"` ServiceHealth usageOverviewServiceHealth `json:"service_health"` Timezone string `json:"timezone"` RangeStart *time.Time `json:"range_start,omitempty"` RangeEnd *time.Time `json:"range_end,omitempty"` } type usageOverviewPayload struct { TotalRequests int64 `json:"total_requests"` SuccessCount int64 `json:"success_count"` FailureCount int64 `json:"failure_count"` TotalTokens int64 `json:"total_tokens"` APIs map[string]usageOverviewAPISnapshot `json:"apis"` RequestsByDay map[string]int64 `json:"requests_by_day"` RequestsByHour map[string]int64 `json:"requests_by_hour"` TokensByDay map[string]int64 `json:"tokens_by_day"` TokensByHour map[string]int64 `json:"tokens_by_hour"` } type usageOverviewSummary struct { RequestCount int64 `json:"request_count"` TokenCount int64 `json:"token_count"` WindowMinutes int64 `json:"window_minutes"` RPM float64 `json:"rpm"` TPM float64 `json:"tpm"` TotalCost float64 `json:"total_cost"` CostAvailable bool `json:"cost_available"` CachedTokens int64 `json:"cached_tokens"` ReasoningTokens int64 `json:"reasoning_tokens"` } type usageOverviewSeries struct { Requests map[string]int64 `json:"requests"` Tokens map[string]int64 `json:"tokens"` RPM map[string]float64 `json:"rpm"` TPM map[string]float64 `json:"tpm"` Cost map[string]float64 `json:"cost"` InputTokens map[string]int64 `json:"input_tokens"` OutputTokens map[string]int64 `json:"output_tokens"` CachedTokens map[string]int64 `json:"cached_tokens"` ReasoningTokens map[string]int64 `json:"reasoning_tokens"` Models map[string]usageOverviewSeriesLine `json:"models"` } type usageOverviewSeriesLine struct { Requests map[string]int64 `json:"requests"` Tokens map[string]int64 `json:"tokens"` RPM map[string]float64 `json:"rpm"` TPM map[string]float64 `json:"tpm"` Cost map[string]float64 `json:"cost"` InputTokens map[string]int64 `json:"input_tokens"` OutputTokens map[string]int64 `json:"output_tokens"` CachedTokens map[string]int64 `json:"cached_tokens"` ReasoningTokens map[string]int64 `json:"reasoning_tokens"` } type usageOverviewServiceHealth struct { TotalSuccess int64 `json:"total_success"` TotalFailure int64 `json:"total_failure"` SuccessRate float64 `json:"success_rate"` Rows int `json:"rows"` Columns int `json:"columns"` BucketSeconds int64 `json:"bucket_seconds"` WindowStart time.Time `json:"window_start"` WindowEnd time.Time `json:"window_end"` BlockDetails []usageOverviewServiceHealthBlock `json:"block_details"` } type usageOverviewServiceHealthBlock struct { StartTime time.Time `json:"start_time"` EndTime time.Time `json:"end_time"` Success int64 `json:"success"` Failure int64 `json:"failure"` Rate float64 `json:"rate"` } type usageOverviewAPISnapshot struct { DisplayName string `json:"display_name,omitempty"` TotalRequests int64 `json:"total_requests"` SuccessCount int64 `json:"success_count"` FailureCount int64 `json:"failure_count"` TotalTokens int64 `json:"total_tokens"` Models map[string]usageOverviewModelSnapshot `json:"models"` } type usageOverviewModelSnapshot struct { TotalRequests int64 `json:"total_requests"` SuccessCount int64 `json:"success_count"` FailureCount int64 `json:"failure_count"` TotalTokens int64 `json:"total_tokens"` } func registerUsageOverviewRoute(router gin.IRoutes, usageProvider service.UsageProvider) { router.GET("/usage/overview", func(c *gin.Context) { if usageProvider == nil { c.JSON(http.StatusOK, usageOverviewResponse{ Usage: buildUsageOverviewPayload(nil), Summary: usageOverviewSummary{}, Series: emptyUsageOverviewSeries(), HourlySeries: emptyUsageOverviewSeries(), DailySeries: emptyUsageOverviewSeries(), ServiceHealth: usageOverviewServiceHealth{BlockDetails: []usageOverviewServiceHealthBlock{}}, Timezone: time.Local.String(), }) return } filter, err := parseUsageFilterQuery(c.Request, time.Now().UTC()) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } overview, err := usageProvider.GetUsageOverview(c.Request.Context(), filter) if err != nil { writeInternalError(c, "get usage overview failed", err) return } var usage *cpa.StatisticsSnapshot if overview != nil { usage = overview.Usage } redactedUsage := redact.UsageSnapshot(usage) c.JSON(http.StatusOK, usageOverviewResponse{ Usage: buildUsageOverviewPayload(redactedUsage), Summary: buildUsageOverviewSummary(overview), Series: buildUsageOverviewSeries(overview), HourlySeries: buildUsageOverviewHourlySeries(overview), DailySeries: buildUsageOverviewDailySeries(overview), ServiceHealth: buildUsageOverviewServiceHealth(overview), Timezone: time.Local.String(), RangeStart: filter.StartTime, RangeEnd: filter.EndTime, }) }) } func buildUsageOverviewPayload(snapshot *cpa.StatisticsSnapshot) usageOverviewPayload { if snapshot == nil { return usageOverviewPayload{ APIs: map[string]usageOverviewAPISnapshot{}, RequestsByDay: map[string]int64{}, RequestsByHour: map[string]int64{}, TokensByDay: map[string]int64{}, TokensByHour: map[string]int64{}, } } payload := usageOverviewPayload{ TotalRequests: snapshot.TotalRequests, SuccessCount: snapshot.SuccessCount, FailureCount: snapshot.FailureCount, TotalTokens: snapshot.TotalTokens, RequestsByDay: cloneInt64Map(snapshot.RequestsByDay), RequestsByHour: cloneInt64Map(snapshot.RequestsByHour), TokensByDay: cloneInt64Map(snapshot.TokensByDay), TokensByHour: cloneInt64Map(snapshot.TokensByHour), APIs: map[string]usageOverviewAPISnapshot{}, } for apiName, apiSnapshot := range snapshot.APIs { payloadAPI := usageOverviewAPISnapshot{ DisplayName: apiSnapshot.DisplayName, TotalRequests: apiSnapshot.TotalRequests, SuccessCount: apiSnapshot.SuccessCount, FailureCount: apiSnapshot.FailureCount, TotalTokens: apiSnapshot.TotalTokens, Models: map[string]usageOverviewModelSnapshot{}, } for modelName, modelSnapshot := range apiSnapshot.Models { payloadAPI.Models[modelName] = usageOverviewModelSnapshot{ TotalRequests: modelSnapshot.TotalRequests, SuccessCount: modelSnapshot.SuccessCount, FailureCount: modelSnapshot.FailureCount, TotalTokens: modelSnapshot.TotalTokens, } } payload.APIs[apiName] = payloadAPI } return payload } func buildUsageOverviewSummary(overview *service.UsageOverviewSnapshot) usageOverviewSummary { if overview == nil { return usageOverviewSummary{} } return usageOverviewSummary{ RequestCount: overview.Summary.RequestCount, TokenCount: overview.Summary.TokenCount, WindowMinutes: overview.Summary.WindowMinutes, RPM: overview.Summary.RPM, TPM: overview.Summary.TPM, TotalCost: overview.Summary.TotalCost, CostAvailable: overview.Summary.CostAvailable, CachedTokens: overview.Summary.CachedTokens, ReasoningTokens: overview.Summary.ReasoningTokens, } } func emptyUsageOverviewSeries() usageOverviewSeries { return usageOverviewSeries{ Requests: map[string]int64{}, Tokens: map[string]int64{}, RPM: map[string]float64{}, TPM: map[string]float64{}, Cost: map[string]float64{}, InputTokens: map[string]int64{}, OutputTokens: map[string]int64{}, CachedTokens: map[string]int64{}, ReasoningTokens: map[string]int64{}, Models: map[string]usageOverviewSeriesLine{}, } } func mapUsageOverviewSeriesLine(series service.UsageOverviewSeries) usageOverviewSeriesLine { return usageOverviewSeriesLine{ Requests: cloneInt64Map(series.Requests), Tokens: cloneInt64Map(series.Tokens), RPM: cloneFloat64Map(series.RPM), TPM: cloneFloat64Map(series.TPM), Cost: cloneFloat64Map(series.Cost), InputTokens: cloneInt64Map(series.InputTokens), OutputTokens: cloneInt64Map(series.OutputTokens), CachedTokens: cloneInt64Map(series.CachedTokens), ReasoningTokens: cloneInt64Map(series.ReasoningTokens), } } func mapUsageOverviewSeries(series service.UsageOverviewSeries) usageOverviewSeries { models := make(map[string]usageOverviewSeriesLine, len(series.Models)) for model, modelSeries := range series.Models { models[model] = mapUsageOverviewSeriesLine(modelSeries) } return usageOverviewSeries{ Requests: cloneInt64Map(series.Requests), Tokens: cloneInt64Map(series.Tokens), RPM: cloneFloat64Map(series.RPM), TPM: cloneFloat64Map(series.TPM), Cost: cloneFloat64Map(series.Cost), InputTokens: cloneInt64Map(series.InputTokens), OutputTokens: cloneInt64Map(series.OutputTokens), CachedTokens: cloneInt64Map(series.CachedTokens), ReasoningTokens: cloneInt64Map(series.ReasoningTokens), Models: models, } } func buildUsageOverviewSeries(overview *service.UsageOverviewSnapshot) usageOverviewSeries { if overview == nil { return emptyUsageOverviewSeries() } return mapUsageOverviewSeries(overview.Series) } func buildUsageOverviewHourlySeries(overview *service.UsageOverviewSnapshot) usageOverviewSeries { if overview == nil { return emptyUsageOverviewSeries() } return mapUsageOverviewSeries(overview.HourlySeries) } func buildUsageOverviewDailySeries(overview *service.UsageOverviewSnapshot) usageOverviewSeries { if overview == nil { return emptyUsageOverviewSeries() } return mapUsageOverviewSeries(overview.DailySeries) } func buildUsageOverviewServiceHealth(overview *service.UsageOverviewSnapshot) usageOverviewServiceHealth { if overview == nil { return usageOverviewServiceHealth{BlockDetails: []usageOverviewServiceHealthBlock{}} } blocks := make([]usageOverviewServiceHealthBlock, 0, len(overview.Health.BlockDetails)) for _, block := range overview.Health.BlockDetails { blocks = append(blocks, usageOverviewServiceHealthBlock{ StartTime: block.StartTime, EndTime: block.EndTime, Success: block.Success, Failure: block.Failure, Rate: block.Rate, }) } return usageOverviewServiceHealth{ TotalSuccess: overview.Health.TotalSuccess, TotalFailure: overview.Health.TotalFailure, SuccessRate: overview.Health.SuccessRate, Rows: overview.Health.Rows, Columns: overview.Health.Columns, BucketSeconds: overview.Health.BucketSeconds, WindowStart: overview.Health.WindowStart, WindowEnd: overview.Health.WindowEnd, BlockDetails: blocks, } } func cloneInt64Map(source map[string]int64) map[string]int64 { if len(source) == 0 { return map[string]int64{} } cloned := make(map[string]int64, len(source)) for key, value := range source { cloned[key] = value } return cloned } func cloneFloat64Map(source map[string]float64) map[string]float64 { if len(source) == 0 { return map[string]float64{} } cloned := make(map[string]float64, len(source)) for key, value := range source { cloned[key] = value } return cloned }