daili-usage-keeper / internal /api /usage_overview.go
pjpjq's picture
fix: build usage keeper from source
b034029 verified
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
}