diff --git a/backend/internal/pkg/usagestats/usage_log_types.go b/backend/internal/pkg/usagestats/usage_log_types.go index 5d1f7911..fe5f98d6 100644 --- a/backend/internal/pkg/usagestats/usage_log_types.go +++ b/backend/internal/pkg/usagestats/usage_log_types.go @@ -56,8 +56,9 @@ type DashboardStats struct { TotalCacheCreationTokens int64 `json:"total_cache_creation_tokens"` TotalCacheReadTokens int64 `json:"total_cache_read_tokens"` TotalTokens int64 `json:"total_tokens"` - TotalCost float64 `json:"total_cost"` // 累计标准计费 - TotalActualCost float64 `json:"total_actual_cost"` // 累计实际扣除 + TotalCost float64 `json:"total_cost"` // 累计标准计费 + TotalActualCost float64 `json:"total_actual_cost"` // 累计实际扣除 + TotalAccountCost float64 `json:"total_account_cost"` // 累计账号成本 // 今日 Token 使用统计 TodayRequests int64 `json:"today_requests"` @@ -66,8 +67,9 @@ type DashboardStats struct { TodayCacheCreationTokens int64 `json:"today_cache_creation_tokens"` TodayCacheReadTokens int64 `json:"today_cache_read_tokens"` TodayTokens int64 `json:"today_tokens"` - TodayCost float64 `json:"today_cost"` // 今日标准计费 - TodayActualCost float64 `json:"today_actual_cost"` // 今日实际扣除 + TodayCost float64 `json:"today_cost"` // 今日标准计费 + TodayActualCost float64 `json:"today_actual_cost"` // 今日实际扣除 + TodayAccountCost float64 `json:"today_account_cost"` // 今日账号成本 // 系统运行统计 AverageDurationMs float64 `json:"average_duration_ms"` // 平均响应时间 @@ -99,8 +101,9 @@ type ModelStat struct { CacheCreationTokens int64 `json:"cache_creation_tokens"` CacheReadTokens int64 `json:"cache_read_tokens"` TotalTokens int64 `json:"total_tokens"` - Cost float64 `json:"cost"` // 标准计费 - ActualCost float64 `json:"actual_cost"` // 实际扣除 + Cost float64 `json:"cost"` // 标准计费 + ActualCost float64 `json:"actual_cost"` // 实际扣除 + AccountCost float64 `json:"account_cost"` // 账号成本 } // EndpointStat represents usage statistics for a single request endpoint. @@ -125,8 +128,9 @@ type GroupStat struct { GroupName string `json:"group_name"` Requests int64 `json:"requests"` TotalTokens int64 `json:"total_tokens"` - Cost float64 `json:"cost"` // 标准计费 - ActualCost float64 `json:"actual_cost"` // 实际扣除 + Cost float64 `json:"cost"` // 标准计费 + ActualCost float64 `json:"actual_cost"` // 实际扣除 + AccountCost float64 `json:"account_cost"` // 账号成本 } // UserUsageTrendPoint represents user usage trend data point @@ -164,8 +168,9 @@ type UserBreakdownItem struct { Email string `json:"email"` Requests int64 `json:"requests"` TotalTokens int64 `json:"total_tokens"` - Cost float64 `json:"cost"` // 标准计费 - ActualCost float64 `json:"actual_cost"` // 实际扣除 + Cost float64 `json:"cost"` // 标准计费 + ActualCost float64 `json:"actual_cost"` // 实际扣除 + AccountCost float64 `json:"account_cost"` // 账号成本 } // UserBreakdownDimension specifies the dimension to filter for user breakdown. diff --git a/backend/internal/repository/dashboard_aggregation_repo.go b/backend/internal/repository/dashboard_aggregation_repo.go index e82a73a3..5e09e75d 100644 --- a/backend/internal/repository/dashboard_aggregation_repo.go +++ b/backend/internal/repository/dashboard_aggregation_repo.go @@ -331,6 +331,7 @@ func (r *dashboardAggregationRepository) upsertHourlyAggregates(ctx context.Cont COALESCE(SUM(cache_read_tokens), 0) AS cache_read_tokens, COALESCE(SUM(total_cost), 0) AS total_cost, COALESCE(SUM(actual_cost), 0) AS actual_cost, + COALESCE(SUM(COALESCE(account_stats_cost, total_cost) * COALESCE(account_rate_multiplier, 1)), 0) AS account_cost, COALESCE(SUM(COALESCE(duration_ms, 0)), 0) AS total_duration_ms FROM usage_logs WHERE created_at >= $1 AND created_at < $2 @@ -351,6 +352,7 @@ func (r *dashboardAggregationRepository) upsertHourlyAggregates(ctx context.Cont cache_read_tokens, total_cost, actual_cost, + account_cost, total_duration_ms, active_users, computed_at @@ -364,6 +366,7 @@ func (r *dashboardAggregationRepository) upsertHourlyAggregates(ctx context.Cont hourly.cache_read_tokens, hourly.total_cost, hourly.actual_cost, + hourly.account_cost, hourly.total_duration_ms, COALESCE(user_counts.active_users, 0) AS active_users, NOW() @@ -378,6 +381,7 @@ func (r *dashboardAggregationRepository) upsertHourlyAggregates(ctx context.Cont cache_read_tokens = EXCLUDED.cache_read_tokens, total_cost = EXCLUDED.total_cost, actual_cost = EXCLUDED.actual_cost, + account_cost = EXCLUDED.account_cost, total_duration_ms = EXCLUDED.total_duration_ms, active_users = EXCLUDED.active_users, computed_at = EXCLUDED.computed_at @@ -399,6 +403,7 @@ func (r *dashboardAggregationRepository) upsertDailyAggregates(ctx context.Conte COALESCE(SUM(cache_read_tokens), 0) AS cache_read_tokens, COALESCE(SUM(total_cost), 0) AS total_cost, COALESCE(SUM(actual_cost), 0) AS actual_cost, + COALESCE(SUM(account_cost), 0) AS account_cost, COALESCE(SUM(total_duration_ms), 0) AS total_duration_ms FROM usage_dashboard_hourly WHERE bucket_start >= $1 AND bucket_start < $2 @@ -419,6 +424,7 @@ func (r *dashboardAggregationRepository) upsertDailyAggregates(ctx context.Conte cache_read_tokens, total_cost, actual_cost, + account_cost, total_duration_ms, active_users, computed_at @@ -432,6 +438,7 @@ func (r *dashboardAggregationRepository) upsertDailyAggregates(ctx context.Conte daily.cache_read_tokens, daily.total_cost, daily.actual_cost, + daily.account_cost, daily.total_duration_ms, COALESCE(user_counts.active_users, 0) AS active_users, NOW() @@ -446,6 +453,7 @@ func (r *dashboardAggregationRepository) upsertDailyAggregates(ctx context.Conte cache_read_tokens = EXCLUDED.cache_read_tokens, total_cost = EXCLUDED.total_cost, actual_cost = EXCLUDED.actual_cost, + account_cost = EXCLUDED.account_cost, total_duration_ms = EXCLUDED.total_duration_ms, active_users = EXCLUDED.active_users, computed_at = EXCLUDED.computed_at diff --git a/backend/internal/repository/usage_log_repo.go b/backend/internal/repository/usage_log_repo.go index f942a8e1..f2fb87da 100644 --- a/backend/internal/repository/usage_log_repo.go +++ b/backend/internal/repository/usage_log_repo.go @@ -1528,6 +1528,7 @@ func (r *usageLogRepository) fillDashboardUsageStatsAggregated(ctx context.Conte COALESCE(SUM(cache_read_tokens), 0) as total_cache_read_tokens, COALESCE(SUM(total_cost), 0) as total_cost, COALESCE(SUM(actual_cost), 0) as total_actual_cost, + COALESCE(SUM(account_cost), 0) as total_account_cost, COALESCE(SUM(total_duration_ms), 0) as total_duration_ms FROM usage_dashboard_daily ` @@ -1544,6 +1545,7 @@ func (r *usageLogRepository) fillDashboardUsageStatsAggregated(ctx context.Conte &stats.TotalCacheReadTokens, &stats.TotalCost, &stats.TotalActualCost, + &stats.TotalAccountCost, &totalDurationMs, ); err != nil { return err @@ -1562,6 +1564,7 @@ func (r *usageLogRepository) fillDashboardUsageStatsAggregated(ctx context.Conte cache_read_tokens as today_cache_read_tokens, total_cost as today_cost, actual_cost as today_actual_cost, + account_cost as today_account_cost, active_users as active_users FROM usage_dashboard_daily WHERE bucket_date = $1::date @@ -1578,6 +1581,7 @@ func (r *usageLogRepository) fillDashboardUsageStatsAggregated(ctx context.Conte &stats.TodayCacheReadTokens, &stats.TodayCost, &stats.TodayActualCost, + &stats.TodayAccountCost, &stats.ActiveUsers, ); err != nil { if err != sql.ErrNoRows { @@ -1613,6 +1617,7 @@ func (r *usageLogRepository) fillDashboardUsageStatsFromUsageLogs(ctx context.Co cache_read_tokens, total_cost, actual_cost, + COALESCE(account_stats_cost, total_cost) * COALESCE(account_rate_multiplier, 1) AS account_cost, COALESCE(duration_ms, 0) AS duration_ms FROM usage_logs WHERE created_at >= LEAST($1::timestamptz, $3::timestamptz) @@ -1626,6 +1631,7 @@ func (r *usageLogRepository) fillDashboardUsageStatsFromUsageLogs(ctx context.Co COALESCE(SUM(cache_read_tokens) FILTER (WHERE created_at >= $1::timestamptz AND created_at < $2::timestamptz), 0) AS total_cache_read_tokens, COALESCE(SUM(total_cost) FILTER (WHERE created_at >= $1::timestamptz AND created_at < $2::timestamptz), 0) AS total_cost, COALESCE(SUM(actual_cost) FILTER (WHERE created_at >= $1::timestamptz AND created_at < $2::timestamptz), 0) AS total_actual_cost, + COALESCE(SUM(account_cost) FILTER (WHERE created_at >= $1::timestamptz AND created_at < $2::timestamptz), 0) AS total_account_cost, COALESCE(SUM(duration_ms) FILTER (WHERE created_at >= $1::timestamptz AND created_at < $2::timestamptz), 0) AS total_duration_ms, COUNT(*) FILTER (WHERE created_at >= $3::timestamptz AND created_at < $4::timestamptz) AS today_requests, COALESCE(SUM(input_tokens) FILTER (WHERE created_at >= $3::timestamptz AND created_at < $4::timestamptz), 0) AS today_input_tokens, @@ -1633,7 +1639,8 @@ func (r *usageLogRepository) fillDashboardUsageStatsFromUsageLogs(ctx context.Co COALESCE(SUM(cache_creation_tokens) FILTER (WHERE created_at >= $3::timestamptz AND created_at < $4::timestamptz), 0) AS today_cache_creation_tokens, COALESCE(SUM(cache_read_tokens) FILTER (WHERE created_at >= $3::timestamptz AND created_at < $4::timestamptz), 0) AS today_cache_read_tokens, COALESCE(SUM(total_cost) FILTER (WHERE created_at >= $3::timestamptz AND created_at < $4::timestamptz), 0) AS today_cost, - COALESCE(SUM(actual_cost) FILTER (WHERE created_at >= $3::timestamptz AND created_at < $4::timestamptz), 0) AS today_actual_cost + COALESCE(SUM(actual_cost) FILTER (WHERE created_at >= $3::timestamptz AND created_at < $4::timestamptz), 0) AS today_actual_cost, + COALESCE(SUM(account_cost) FILTER (WHERE created_at >= $3::timestamptz AND created_at < $4::timestamptz), 0) AS today_account_cost FROM scoped ` var totalDurationMs int64 @@ -1649,6 +1656,7 @@ func (r *usageLogRepository) fillDashboardUsageStatsFromUsageLogs(ctx context.Co &stats.TotalCacheReadTokens, &stats.TotalCost, &stats.TotalActualCost, + &stats.TotalAccountCost, &totalDurationMs, &stats.TodayRequests, &stats.TodayInputTokens, @@ -1657,6 +1665,7 @@ func (r *usageLogRepository) fillDashboardUsageStatsFromUsageLogs(ctx context.Co &stats.TodayCacheReadTokens, &stats.TodayCost, &stats.TodayActualCost, + &stats.TodayAccountCost, ); err != nil { return err } @@ -2595,7 +2604,8 @@ func (r *usageLogRepository) GetUserModelStats(ctx context.Context, userID int64 COALESCE(SUM(cache_read_tokens), 0) as cache_read_tokens, COALESCE(SUM(input_tokens + output_tokens + cache_creation_tokens + cache_read_tokens), 0) as total_tokens, COALESCE(SUM(total_cost), 0) as cost, - COALESCE(SUM(actual_cost), 0) as actual_cost + COALESCE(SUM(actual_cost), 0) as actual_cost, + COALESCE(SUM(COALESCE(account_stats_cost, total_cost) * COALESCE(account_rate_multiplier, 1)), 0) as account_cost FROM usage_logs WHERE user_id = $1 AND created_at >= $2 AND created_at < $3 GROUP BY model @@ -3002,6 +3012,7 @@ func (r *usageLogRepository) getModelStatsWithFiltersBySource(ctx context.Contex if accountID > 0 && userID == 0 && apiKeyID == 0 { actualCostExpr = "COALESCE(SUM(COALESCE(account_stats_cost, total_cost) * COALESCE(account_rate_multiplier, 1)), 0) as actual_cost" } + accountCostExpr := "COALESCE(SUM(COALESCE(account_stats_cost, total_cost) * COALESCE(account_rate_multiplier, 1)), 0) as account_cost" modelExpr := resolveModelDimensionExpression(source) query := fmt.Sprintf(` @@ -3014,10 +3025,11 @@ func (r *usageLogRepository) getModelStatsWithFiltersBySource(ctx context.Contex COALESCE(SUM(cache_read_tokens), 0) as cache_read_tokens, COALESCE(SUM(input_tokens + output_tokens + cache_creation_tokens + cache_read_tokens), 0) as total_tokens, COALESCE(SUM(total_cost), 0) as cost, + %s, %s FROM usage_logs WHERE created_at >= $1 AND created_at < $2 - `, modelExpr, actualCostExpr) + `, modelExpr, actualCostExpr, accountCostExpr) args := []any{startTime, endTime} if userID > 0 { @@ -3072,7 +3084,8 @@ func (r *usageLogRepository) GetGroupStatsWithFilters(ctx context.Context, start COUNT(*) as requests, COALESCE(SUM(ul.input_tokens + ul.output_tokens + ul.cache_creation_tokens + ul.cache_read_tokens), 0) as total_tokens, COALESCE(SUM(ul.total_cost), 0) as cost, - COALESCE(SUM(ul.actual_cost), 0) as actual_cost + COALESCE(SUM(ul.actual_cost), 0) as actual_cost, + COALESCE(SUM(COALESCE(ul.account_stats_cost, ul.total_cost) * COALESCE(ul.account_rate_multiplier, 1)), 0) as account_cost FROM usage_logs ul LEFT JOIN groups g ON g.id = ul.group_id WHERE ul.created_at >= $1 AND ul.created_at < $2 @@ -3123,6 +3136,7 @@ func (r *usageLogRepository) GetGroupStatsWithFilters(ctx context.Context, start &row.TotalTokens, &row.Cost, &row.ActualCost, + &row.AccountCost, ); err != nil { return nil, err } @@ -3143,7 +3157,8 @@ func (r *usageLogRepository) GetUserBreakdownStats(ctx context.Context, startTim COUNT(*) as requests, COALESCE(SUM(ul.input_tokens + ul.output_tokens + ul.cache_creation_tokens + ul.cache_read_tokens), 0) as total_tokens, COALESCE(SUM(ul.total_cost), 0) as cost, - COALESCE(SUM(ul.actual_cost), 0) as actual_cost + COALESCE(SUM(ul.actual_cost), 0) as actual_cost, + COALESCE(SUM(COALESCE(ul.account_stats_cost, ul.total_cost) * COALESCE(ul.account_rate_multiplier, 1)), 0) as account_cost FROM usage_logs ul LEFT JOIN users u ON u.id = ul.user_id WHERE ul.created_at >= $1 AND ul.created_at < $2 @@ -3214,6 +3229,7 @@ func (r *usageLogRepository) GetUserBreakdownStats(ctx context.Context, startTim &row.TotalTokens, &row.Cost, &row.ActualCost, + &row.AccountCost, ); err != nil { return nil, err } @@ -3392,9 +3408,7 @@ func (r *usageLogRepository) GetStatsWithFilters(ctx context.Context, filters Us ); err != nil { return nil, err } - if filters.AccountID > 0 { - stats.TotalAccountCost = &totalAccountCost - } + stats.TotalAccountCost = &totalAccountCost stats.TotalTokens = stats.TotalInputTokens + stats.TotalOutputTokens + stats.TotalCacheTokens start := time.Unix(0, 0).UTC() @@ -4272,6 +4286,7 @@ func scanModelStatsRows(rows *sql.Rows) ([]ModelStat, error) { &row.TotalTokens, &row.Cost, &row.ActualCost, + &row.AccountCost, ); err != nil { return nil, err } diff --git a/backend/internal/repository/usage_log_repo_integration_test.go b/backend/internal/repository/usage_log_repo_integration_test.go index 0383f3bc..ed3050d8 100644 --- a/backend/internal/repository/usage_log_repo_integration_test.go +++ b/backend/internal/repository/usage_log_repo_integration_test.go @@ -753,8 +753,11 @@ func (s *UsageLogRepoSuite) TestDashboardStats_TodayTotalsAndPerformance() { s.Require().Equal(baseStats.TotalTokens+int64(51), stats.TotalTokens, "TotalTokens mismatch") s.Require().Equal(baseStats.TotalCost+2.3, stats.TotalCost, "TotalCost mismatch") s.Require().Equal(baseStats.TotalActualCost+2.0, stats.TotalActualCost, "TotalActualCost mismatch") + // account_cost falls back to total_cost when account_stats_cost is NULL + s.Require().Equal(baseStats.TotalAccountCost+2.3, stats.TotalAccountCost, "TotalAccountCost mismatch") s.Require().GreaterOrEqual(stats.TodayRequests, int64(1), "expected TodayRequests >= 1") s.Require().GreaterOrEqual(stats.TodayCost, 0.0, "expected TodayCost >= 0") + s.Require().GreaterOrEqual(stats.TodayAccountCost, 0.0, "expected TodayAccountCost >= 0") wantRpm, wantTpm, err := s.repo.getPerformanceStats(s.ctx, 0) s.Require().NoError(err, "getPerformanceStats") @@ -833,6 +836,8 @@ func (s *UsageLogRepoSuite) TestDashboardStatsWithRange_Fallback() { s.Require().Equal(int64(45), stats.TotalTokens) s.Require().Equal(1.5, stats.TotalCost) s.Require().Equal(1.4, stats.TotalActualCost) + // account_cost = COALESCE(account_stats_cost, total_cost) * COALESCE(account_rate_multiplier, 1) = total_cost + s.Require().Equal(1.5, stats.TotalAccountCost) s.Require().InEpsilon(150.0, stats.AverageDurationMs, 0.0001) } diff --git a/backend/internal/repository/usage_log_repo_request_type_test.go b/backend/internal/repository/usage_log_repo_request_type_test.go index acdd6e62..a5ff4bc1 100644 --- a/backend/internal/repository/usage_log_repo_request_type_test.go +++ b/backend/internal/repository/usage_log_repo_request_type_test.go @@ -301,7 +301,7 @@ func TestUsageLogRepositoryGetModelStatsWithFiltersRequestTypePriority(t *testin mock.ExpectQuery("AND \\(request_type = \\$3 OR \\(request_type = 0 AND openai_ws_mode = TRUE\\)\\)"). WithArgs(start, end, requestType). - WillReturnRows(sqlmock.NewRows([]string{"model", "requests", "input_tokens", "output_tokens", "cache_creation_tokens", "cache_read_tokens", "total_tokens", "cost", "actual_cost"})) + WillReturnRows(sqlmock.NewRows([]string{"model", "requests", "input_tokens", "output_tokens", "cache_creation_tokens", "cache_read_tokens", "total_tokens", "cost", "actual_cost", "account_cost"})) stats, err := repo.GetModelStatsWithFilters(context.Background(), start, end, 0, 0, 0, 0, &requestType, &stream, nil) require.NoError(t, err) @@ -346,6 +346,93 @@ func TestUsageLogRepositoryGetStatsWithFiltersRequestTypePriority(t *testing.T) require.NoError(t, err) require.Equal(t, int64(1), stats.TotalRequests) require.Equal(t, int64(9), stats.TotalTokens) + require.NotNil(t, stats.TotalAccountCost, "TotalAccountCost should always be returned") + require.Equal(t, 1.2, *stats.TotalAccountCost) + require.NoError(t, mock.ExpectationsWereMet()) +} + +func TestUsageLogRepositoryGetModelStatsAccountCostColumn(t *testing.T) { + db, mock := newSQLMock(t) + repo := &usageLogRepository{sql: db} + + start := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC) + end := start.Add(24 * time.Hour) + + mock.ExpectQuery("FROM usage_logs"). + WithArgs(start, end). + WillReturnRows(sqlmock.NewRows([]string{ + "model", "requests", "input_tokens", "output_tokens", + "cache_creation_tokens", "cache_read_tokens", "total_tokens", + "cost", "actual_cost", "account_cost", + }). + AddRow("claude-opus-4-6", int64(10), int64(100), int64(200), int64(5), int64(3), int64(308), 2.5, 2.0, 1.8). + AddRow("claude-sonnet-4-6", int64(5), int64(50), int64(100), int64(0), int64(0), int64(150), 1.0, 0.8, 0.7)) + + results, err := repo.GetModelStatsWithFilters(context.Background(), start, end, 0, 0, 0, 0, nil, nil, nil) + require.NoError(t, err) + require.Len(t, results, 2) + require.Equal(t, "claude-opus-4-6", results[0].Model) + require.Equal(t, 2.5, results[0].Cost) + require.Equal(t, 2.0, results[0].ActualCost) + require.Equal(t, 1.8, results[0].AccountCost) + require.Equal(t, "claude-sonnet-4-6", results[1].Model) + require.Equal(t, 0.7, results[1].AccountCost) + require.NoError(t, mock.ExpectationsWereMet()) +} + +func TestUsageLogRepositoryGetGroupStatsAccountCostColumn(t *testing.T) { + db, mock := newSQLMock(t) + repo := &usageLogRepository{sql: db} + + start := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC) + end := start.Add(24 * time.Hour) + + mock.ExpectQuery("FROM usage_logs"). + WithArgs(start, end). + WillReturnRows(sqlmock.NewRows([]string{ + "group_id", "group_name", "requests", "total_tokens", + "cost", "actual_cost", "account_cost", + }). + AddRow(int64(1), "azure-cc", int64(100), int64(5000), 10.0, 8.5, 7.2). + AddRow(int64(2), "max", int64(50), int64(2000), 5.0, 4.0, 3.5)) + + results, err := repo.GetGroupStatsWithFilters(context.Background(), start, end, 0, 0, 0, 0, nil, nil, nil) + require.NoError(t, err) + require.Len(t, results, 2) + require.Equal(t, int64(1), results[0].GroupID) + require.Equal(t, "azure-cc", results[0].GroupName) + require.Equal(t, 10.0, results[0].Cost) + require.Equal(t, 8.5, results[0].ActualCost) + require.Equal(t, 7.2, results[0].AccountCost) + require.Equal(t, int64(2), results[1].GroupID) + require.Equal(t, 3.5, results[1].AccountCost) + require.NoError(t, mock.ExpectationsWereMet()) +} + +func TestUsageLogRepositoryGetStatsWithFiltersAlwaysReturnsAccountCost(t *testing.T) { + db, mock := newSQLMock(t) + repo := &usageLogRepository{sql: db} + + // No AccountID filter set - TotalAccountCost should still be returned + filters := usagestats.UsageLogFilters{} + + mock.ExpectQuery("FROM usage_logs"). + WillReturnRows(sqlmock.NewRows([]string{ + "total_requests", "total_input_tokens", "total_output_tokens", + "total_cache_tokens", "total_cost", "total_actual_cost", + "total_account_cost", "avg_duration_ms", + }).AddRow(int64(50), int64(1000), int64(2000), int64(100), 15.0, 12.5, 11.0, 100.0)) + mock.ExpectQuery("SELECT COALESCE\\(NULLIF\\(TRIM\\(inbound_endpoint\\)"). + WillReturnRows(sqlmock.NewRows([]string{"endpoint", "requests", "total_tokens", "cost", "actual_cost"})) + mock.ExpectQuery("SELECT COALESCE\\(NULLIF\\(TRIM\\(upstream_endpoint\\)"). + WillReturnRows(sqlmock.NewRows([]string{"endpoint", "requests", "total_tokens", "cost", "actual_cost"})) + mock.ExpectQuery("SELECT CONCAT\\("). + WillReturnRows(sqlmock.NewRows([]string{"endpoint", "requests", "total_tokens", "cost", "actual_cost"})) + + stats, err := repo.GetStatsWithFilters(context.Background(), filters) + require.NoError(t, err) + require.NotNil(t, stats.TotalAccountCost, "TotalAccountCost must always be returned, even without AccountID filter") + require.Equal(t, 11.0, *stats.TotalAccountCost) require.NoError(t, mock.ExpectationsWereMet()) } diff --git a/backend/migrations/107_add_account_cost_to_dashboard_tables.sql b/backend/migrations/107_add_account_cost_to_dashboard_tables.sql new file mode 100644 index 00000000..9f815a3f --- /dev/null +++ b/backend/migrations/107_add_account_cost_to_dashboard_tables.sql @@ -0,0 +1,5 @@ +-- Add account_cost column to dashboard aggregation tables for admin dashboard display. +-- account_cost = SUM(COALESCE(account_stats_cost, total_cost) * COALESCE(account_rate_multiplier, 1)) + +ALTER TABLE usage_dashboard_hourly ADD COLUMN IF NOT EXISTS account_cost DECIMAL(20, 10) NOT NULL DEFAULT 0; +ALTER TABLE usage_dashboard_daily ADD COLUMN IF NOT EXISTS account_cost DECIMAL(20, 10) NOT NULL DEFAULT 0; diff --git a/frontend/src/api/admin/usage.ts b/frontend/src/api/admin/usage.ts index 37df7553..7ad00742 100644 --- a/frontend/src/api/admin/usage.ts +++ b/frontend/src/api/admin/usage.ts @@ -17,7 +17,7 @@ export interface AdminUsageStatsResponse { total_tokens: number total_cost: number total_actual_cost: number - total_account_cost?: number + total_account_cost: number average_duration_ms: number endpoints?: EndpointStat[] upstream_endpoints?: EndpointStat[] diff --git a/frontend/src/components/admin/usage/UsageStatsCards.vue b/frontend/src/components/admin/usage/UsageStatsCards.vue index cd962a09..905ece61 100644 --- a/frontend/src/components/admin/usage/UsageStatsCards.vue +++ b/frontend/src/components/admin/usage/UsageStatsCards.vue @@ -28,17 +28,12 @@
{{ t('usage.totalCost') }}
- ${{ ((stats?.total_account_cost ?? stats?.total_actual_cost) || 0).toFixed(4) }} + ${{ (stats?.total_actual_cost || 0).toFixed(4) }}
-- {{ t('usage.userBilled') }}: - ${{ (stats?.total_actual_cost || 0).toFixed(4) }} - · {{ t('usage.standardCost') }}: - ${{ (stats?.total_cost || 0).toFixed(4) }} -
-- {{ t('usage.standardCost') }}: - ${{ (stats?.total_cost || 0).toFixed(4) }} +
+ {{ t('usage.accountCost') }} ${{ (stats?.total_account_cost || 0).toFixed(4) }} + · + {{ t('usage.standardCost') }} ${{ (stats?.total_cost || 0).toFixed(4) }}
${{ formatCost(stats.today_actual_cost) }} + / + ${{ formatCost(stats.today_account_cost) }} + / - / ${{ formatCost(stats.today_cost) }}${{ formatCost(stats.today_cost) }}
@@ -142,15 +148,21 @@${{ formatCost(stats.total_actual_cost) }} + / + ${{ formatCost(stats.total_account_cost) }} + / - / ${{ formatCost(stats.total_cost) }}${{ formatCost(stats.total_cost) }}