Commit 6ade6d30 authored by erio's avatar erio
Browse files

feat(usage): add account cost display to admin dashboard and usage pages

- Add account_cost column to dashboard aggregation tables (migration 107)
- DashboardStats: add TotalAccountCost/TodayAccountCost fields
- ModelStat/GroupStat: add AccountCost field with SQL aggregation
- GetStatsWithFilters: always return TotalAccountCost (remove accountID filter)
- Dashboard Token cards: show user(green)/cost(orange)/standard(gray)
- Usage stats card: show account cost and standard below main value
- Model/Group distribution tables: add orange cost column
parent 7c671b53
...@@ -56,8 +56,9 @@ type DashboardStats struct { ...@@ -56,8 +56,9 @@ type DashboardStats struct {
TotalCacheCreationTokens int64 `json:"total_cache_creation_tokens"` TotalCacheCreationTokens int64 `json:"total_cache_creation_tokens"`
TotalCacheReadTokens int64 `json:"total_cache_read_tokens"` TotalCacheReadTokens int64 `json:"total_cache_read_tokens"`
TotalTokens int64 `json:"total_tokens"` TotalTokens int64 `json:"total_tokens"`
TotalCost float64 `json:"total_cost"` // 累计标准计费 TotalCost float64 `json:"total_cost"` // 累计标准计费
TotalActualCost float64 `json:"total_actual_cost"` // 累计实际扣除 TotalActualCost float64 `json:"total_actual_cost"` // 累计实际扣除
TotalAccountCost float64 `json:"total_account_cost"` // 累计账号成本
// 今日 Token 使用统计 // 今日 Token 使用统计
TodayRequests int64 `json:"today_requests"` TodayRequests int64 `json:"today_requests"`
...@@ -66,8 +67,9 @@ type DashboardStats struct { ...@@ -66,8 +67,9 @@ type DashboardStats struct {
TodayCacheCreationTokens int64 `json:"today_cache_creation_tokens"` TodayCacheCreationTokens int64 `json:"today_cache_creation_tokens"`
TodayCacheReadTokens int64 `json:"today_cache_read_tokens"` TodayCacheReadTokens int64 `json:"today_cache_read_tokens"`
TodayTokens int64 `json:"today_tokens"` TodayTokens int64 `json:"today_tokens"`
TodayCost float64 `json:"today_cost"` // 今日标准计费 TodayCost float64 `json:"today_cost"` // 今日标准计费
TodayActualCost float64 `json:"today_actual_cost"` // 今日实际扣除 TodayActualCost float64 `json:"today_actual_cost"` // 今日实际扣除
TodayAccountCost float64 `json:"today_account_cost"` // 今日账号成本
// 系统运行统计 // 系统运行统计
AverageDurationMs float64 `json:"average_duration_ms"` // 平均响应时间 AverageDurationMs float64 `json:"average_duration_ms"` // 平均响应时间
...@@ -99,8 +101,9 @@ type ModelStat struct { ...@@ -99,8 +101,9 @@ type ModelStat struct {
CacheCreationTokens int64 `json:"cache_creation_tokens"` CacheCreationTokens int64 `json:"cache_creation_tokens"`
CacheReadTokens int64 `json:"cache_read_tokens"` CacheReadTokens int64 `json:"cache_read_tokens"`
TotalTokens int64 `json:"total_tokens"` TotalTokens int64 `json:"total_tokens"`
Cost float64 `json:"cost"` // 标准计费 Cost float64 `json:"cost"` // 标准计费
ActualCost float64 `json:"actual_cost"` // 实际扣除 ActualCost float64 `json:"actual_cost"` // 实际扣除
AccountCost float64 `json:"account_cost"` // 账号成本
} }
// EndpointStat represents usage statistics for a single request endpoint. // EndpointStat represents usage statistics for a single request endpoint.
...@@ -125,8 +128,9 @@ type GroupStat struct { ...@@ -125,8 +128,9 @@ type GroupStat struct {
GroupName string `json:"group_name"` GroupName string `json:"group_name"`
Requests int64 `json:"requests"` Requests int64 `json:"requests"`
TotalTokens int64 `json:"total_tokens"` TotalTokens int64 `json:"total_tokens"`
Cost float64 `json:"cost"` // 标准计费 Cost float64 `json:"cost"` // 标准计费
ActualCost float64 `json:"actual_cost"` // 实际扣除 ActualCost float64 `json:"actual_cost"` // 实际扣除
AccountCost float64 `json:"account_cost"` // 账号成本
} }
// UserUsageTrendPoint represents user usage trend data point // UserUsageTrendPoint represents user usage trend data point
......
...@@ -331,6 +331,7 @@ func (r *dashboardAggregationRepository) upsertHourlyAggregates(ctx context.Cont ...@@ -331,6 +331,7 @@ func (r *dashboardAggregationRepository) upsertHourlyAggregates(ctx context.Cont
COALESCE(SUM(cache_read_tokens), 0) AS cache_read_tokens, COALESCE(SUM(cache_read_tokens), 0) AS cache_read_tokens,
COALESCE(SUM(total_cost), 0) AS total_cost, COALESCE(SUM(total_cost), 0) AS total_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,
COALESCE(SUM(COALESCE(duration_ms, 0)), 0) AS total_duration_ms COALESCE(SUM(COALESCE(duration_ms, 0)), 0) AS total_duration_ms
FROM usage_logs FROM usage_logs
WHERE created_at >= $1 AND created_at < $2 WHERE created_at >= $1 AND created_at < $2
...@@ -351,6 +352,7 @@ func (r *dashboardAggregationRepository) upsertHourlyAggregates(ctx context.Cont ...@@ -351,6 +352,7 @@ func (r *dashboardAggregationRepository) upsertHourlyAggregates(ctx context.Cont
cache_read_tokens, cache_read_tokens,
total_cost, total_cost,
actual_cost, actual_cost,
account_cost,
total_duration_ms, total_duration_ms,
active_users, active_users,
computed_at computed_at
...@@ -364,6 +366,7 @@ func (r *dashboardAggregationRepository) upsertHourlyAggregates(ctx context.Cont ...@@ -364,6 +366,7 @@ func (r *dashboardAggregationRepository) upsertHourlyAggregates(ctx context.Cont
hourly.cache_read_tokens, hourly.cache_read_tokens,
hourly.total_cost, hourly.total_cost,
hourly.actual_cost, hourly.actual_cost,
hourly.account_cost,
hourly.total_duration_ms, hourly.total_duration_ms,
COALESCE(user_counts.active_users, 0) AS active_users, COALESCE(user_counts.active_users, 0) AS active_users,
NOW() NOW()
...@@ -378,6 +381,7 @@ func (r *dashboardAggregationRepository) upsertHourlyAggregates(ctx context.Cont ...@@ -378,6 +381,7 @@ func (r *dashboardAggregationRepository) upsertHourlyAggregates(ctx context.Cont
cache_read_tokens = EXCLUDED.cache_read_tokens, cache_read_tokens = EXCLUDED.cache_read_tokens,
total_cost = EXCLUDED.total_cost, total_cost = EXCLUDED.total_cost,
actual_cost = EXCLUDED.actual_cost, actual_cost = EXCLUDED.actual_cost,
account_cost = EXCLUDED.account_cost,
total_duration_ms = EXCLUDED.total_duration_ms, total_duration_ms = EXCLUDED.total_duration_ms,
active_users = EXCLUDED.active_users, active_users = EXCLUDED.active_users,
computed_at = EXCLUDED.computed_at computed_at = EXCLUDED.computed_at
...@@ -399,6 +403,7 @@ func (r *dashboardAggregationRepository) upsertDailyAggregates(ctx context.Conte ...@@ -399,6 +403,7 @@ func (r *dashboardAggregationRepository) upsertDailyAggregates(ctx context.Conte
COALESCE(SUM(cache_read_tokens), 0) AS cache_read_tokens, COALESCE(SUM(cache_read_tokens), 0) AS cache_read_tokens,
COALESCE(SUM(total_cost), 0) AS total_cost, COALESCE(SUM(total_cost), 0) AS total_cost,
COALESCE(SUM(actual_cost), 0) AS actual_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 COALESCE(SUM(total_duration_ms), 0) AS total_duration_ms
FROM usage_dashboard_hourly FROM usage_dashboard_hourly
WHERE bucket_start >= $1 AND bucket_start < $2 WHERE bucket_start >= $1 AND bucket_start < $2
...@@ -419,6 +424,7 @@ func (r *dashboardAggregationRepository) upsertDailyAggregates(ctx context.Conte ...@@ -419,6 +424,7 @@ func (r *dashboardAggregationRepository) upsertDailyAggregates(ctx context.Conte
cache_read_tokens, cache_read_tokens,
total_cost, total_cost,
actual_cost, actual_cost,
account_cost,
total_duration_ms, total_duration_ms,
active_users, active_users,
computed_at computed_at
...@@ -432,6 +438,7 @@ func (r *dashboardAggregationRepository) upsertDailyAggregates(ctx context.Conte ...@@ -432,6 +438,7 @@ func (r *dashboardAggregationRepository) upsertDailyAggregates(ctx context.Conte
daily.cache_read_tokens, daily.cache_read_tokens,
daily.total_cost, daily.total_cost,
daily.actual_cost, daily.actual_cost,
daily.account_cost,
daily.total_duration_ms, daily.total_duration_ms,
COALESCE(user_counts.active_users, 0) AS active_users, COALESCE(user_counts.active_users, 0) AS active_users,
NOW() NOW()
...@@ -446,6 +453,7 @@ func (r *dashboardAggregationRepository) upsertDailyAggregates(ctx context.Conte ...@@ -446,6 +453,7 @@ func (r *dashboardAggregationRepository) upsertDailyAggregates(ctx context.Conte
cache_read_tokens = EXCLUDED.cache_read_tokens, cache_read_tokens = EXCLUDED.cache_read_tokens,
total_cost = EXCLUDED.total_cost, total_cost = EXCLUDED.total_cost,
actual_cost = EXCLUDED.actual_cost, actual_cost = EXCLUDED.actual_cost,
account_cost = EXCLUDED.account_cost,
total_duration_ms = EXCLUDED.total_duration_ms, total_duration_ms = EXCLUDED.total_duration_ms,
active_users = EXCLUDED.active_users, active_users = EXCLUDED.active_users,
computed_at = EXCLUDED.computed_at computed_at = EXCLUDED.computed_at
......
...@@ -1528,6 +1528,7 @@ func (r *usageLogRepository) fillDashboardUsageStatsAggregated(ctx context.Conte ...@@ -1528,6 +1528,7 @@ func (r *usageLogRepository) fillDashboardUsageStatsAggregated(ctx context.Conte
COALESCE(SUM(cache_read_tokens), 0) as total_cache_read_tokens, COALESCE(SUM(cache_read_tokens), 0) as total_cache_read_tokens,
COALESCE(SUM(total_cost), 0) as total_cost, COALESCE(SUM(total_cost), 0) as total_cost,
COALESCE(SUM(actual_cost), 0) as total_actual_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 COALESCE(SUM(total_duration_ms), 0) as total_duration_ms
FROM usage_dashboard_daily FROM usage_dashboard_daily
` `
...@@ -1544,6 +1545,7 @@ func (r *usageLogRepository) fillDashboardUsageStatsAggregated(ctx context.Conte ...@@ -1544,6 +1545,7 @@ func (r *usageLogRepository) fillDashboardUsageStatsAggregated(ctx context.Conte
&stats.TotalCacheReadTokens, &stats.TotalCacheReadTokens,
&stats.TotalCost, &stats.TotalCost,
&stats.TotalActualCost, &stats.TotalActualCost,
&stats.TotalAccountCost,
&totalDurationMs, &totalDurationMs,
); err != nil { ); err != nil {
return err return err
...@@ -1562,6 +1564,7 @@ func (r *usageLogRepository) fillDashboardUsageStatsAggregated(ctx context.Conte ...@@ -1562,6 +1564,7 @@ func (r *usageLogRepository) fillDashboardUsageStatsAggregated(ctx context.Conte
cache_read_tokens as today_cache_read_tokens, cache_read_tokens as today_cache_read_tokens,
total_cost as today_cost, total_cost as today_cost,
actual_cost as today_actual_cost, actual_cost as today_actual_cost,
account_cost as today_account_cost,
active_users as active_users active_users as active_users
FROM usage_dashboard_daily FROM usage_dashboard_daily
WHERE bucket_date = $1::date WHERE bucket_date = $1::date
...@@ -1578,6 +1581,7 @@ func (r *usageLogRepository) fillDashboardUsageStatsAggregated(ctx context.Conte ...@@ -1578,6 +1581,7 @@ func (r *usageLogRepository) fillDashboardUsageStatsAggregated(ctx context.Conte
&stats.TodayCacheReadTokens, &stats.TodayCacheReadTokens,
&stats.TodayCost, &stats.TodayCost,
&stats.TodayActualCost, &stats.TodayActualCost,
&stats.TodayAccountCost,
&stats.ActiveUsers, &stats.ActiveUsers,
); err != nil { ); err != nil {
if err != sql.ErrNoRows { if err != sql.ErrNoRows {
...@@ -1613,6 +1617,7 @@ func (r *usageLogRepository) fillDashboardUsageStatsFromUsageLogs(ctx context.Co ...@@ -1613,6 +1617,7 @@ func (r *usageLogRepository) fillDashboardUsageStatsFromUsageLogs(ctx context.Co
cache_read_tokens, cache_read_tokens,
total_cost, total_cost,
actual_cost, actual_cost,
COALESCE(account_stats_cost, total_cost) * COALESCE(account_rate_multiplier, 1) AS account_cost,
COALESCE(duration_ms, 0) AS duration_ms COALESCE(duration_ms, 0) AS duration_ms
FROM usage_logs FROM usage_logs
WHERE created_at >= LEAST($1::timestamptz, $3::timestamptz) WHERE created_at >= LEAST($1::timestamptz, $3::timestamptz)
...@@ -1626,6 +1631,7 @@ func (r *usageLogRepository) fillDashboardUsageStatsFromUsageLogs(ctx context.Co ...@@ -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(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(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(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, 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, 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, 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 ...@@ -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_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(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(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 FROM scoped
` `
var totalDurationMs int64 var totalDurationMs int64
...@@ -1649,6 +1656,7 @@ func (r *usageLogRepository) fillDashboardUsageStatsFromUsageLogs(ctx context.Co ...@@ -1649,6 +1656,7 @@ func (r *usageLogRepository) fillDashboardUsageStatsFromUsageLogs(ctx context.Co
&stats.TotalCacheReadTokens, &stats.TotalCacheReadTokens,
&stats.TotalCost, &stats.TotalCost,
&stats.TotalActualCost, &stats.TotalActualCost,
&stats.TotalAccountCost,
&totalDurationMs, &totalDurationMs,
&stats.TodayRequests, &stats.TodayRequests,
&stats.TodayInputTokens, &stats.TodayInputTokens,
...@@ -1657,6 +1665,7 @@ func (r *usageLogRepository) fillDashboardUsageStatsFromUsageLogs(ctx context.Co ...@@ -1657,6 +1665,7 @@ func (r *usageLogRepository) fillDashboardUsageStatsFromUsageLogs(ctx context.Co
&stats.TodayCacheReadTokens, &stats.TodayCacheReadTokens,
&stats.TodayCost, &stats.TodayCost,
&stats.TodayActualCost, &stats.TodayActualCost,
&stats.TodayAccountCost,
); err != nil { ); err != nil {
return err return err
} }
...@@ -2595,7 +2604,8 @@ func (r *usageLogRepository) GetUserModelStats(ctx context.Context, userID int64 ...@@ -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(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(input_tokens + output_tokens + cache_creation_tokens + cache_read_tokens), 0) as total_tokens,
COALESCE(SUM(total_cost), 0) as cost, 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 FROM usage_logs
WHERE user_id = $1 AND created_at >= $2 AND created_at < $3 WHERE user_id = $1 AND created_at >= $2 AND created_at < $3
GROUP BY model GROUP BY model
...@@ -3002,6 +3012,7 @@ func (r *usageLogRepository) getModelStatsWithFiltersBySource(ctx context.Contex ...@@ -3002,6 +3012,7 @@ func (r *usageLogRepository) getModelStatsWithFiltersBySource(ctx context.Contex
if accountID > 0 && userID == 0 && apiKeyID == 0 { 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" 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) modelExpr := resolveModelDimensionExpression(source)
query := fmt.Sprintf(` query := fmt.Sprintf(`
...@@ -3014,10 +3025,11 @@ func (r *usageLogRepository) getModelStatsWithFiltersBySource(ctx context.Contex ...@@ -3014,10 +3025,11 @@ func (r *usageLogRepository) getModelStatsWithFiltersBySource(ctx context.Contex
COALESCE(SUM(cache_read_tokens), 0) as cache_read_tokens, 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(input_tokens + output_tokens + cache_creation_tokens + cache_read_tokens), 0) as total_tokens,
COALESCE(SUM(total_cost), 0) as cost, COALESCE(SUM(total_cost), 0) as cost,
%s,
%s %s
FROM usage_logs FROM usage_logs
WHERE created_at >= $1 AND created_at < $2 WHERE created_at >= $1 AND created_at < $2
`, modelExpr, actualCostExpr) `, modelExpr, actualCostExpr, accountCostExpr)
args := []any{startTime, endTime} args := []any{startTime, endTime}
if userID > 0 { if userID > 0 {
...@@ -3072,7 +3084,8 @@ func (r *usageLogRepository) GetGroupStatsWithFilters(ctx context.Context, start ...@@ -3072,7 +3084,8 @@ func (r *usageLogRepository) GetGroupStatsWithFilters(ctx context.Context, start
COUNT(*) as requests, 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.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.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 FROM usage_logs ul
LEFT JOIN groups g ON g.id = ul.group_id LEFT JOIN groups g ON g.id = ul.group_id
WHERE ul.created_at >= $1 AND ul.created_at < $2 WHERE ul.created_at >= $1 AND ul.created_at < $2
...@@ -3123,6 +3136,7 @@ func (r *usageLogRepository) GetGroupStatsWithFilters(ctx context.Context, start ...@@ -3123,6 +3136,7 @@ func (r *usageLogRepository) GetGroupStatsWithFilters(ctx context.Context, start
&row.TotalTokens, &row.TotalTokens,
&row.Cost, &row.Cost,
&row.ActualCost, &row.ActualCost,
&row.AccountCost,
); err != nil { ); err != nil {
return nil, err return nil, err
} }
...@@ -3392,9 +3406,7 @@ func (r *usageLogRepository) GetStatsWithFilters(ctx context.Context, filters Us ...@@ -3392,9 +3406,7 @@ func (r *usageLogRepository) GetStatsWithFilters(ctx context.Context, filters Us
); err != nil { ); err != nil {
return nil, err return nil, err
} }
if filters.AccountID > 0 { stats.TotalAccountCost = &totalAccountCost
stats.TotalAccountCost = &totalAccountCost
}
stats.TotalTokens = stats.TotalInputTokens + stats.TotalOutputTokens + stats.TotalCacheTokens stats.TotalTokens = stats.TotalInputTokens + stats.TotalOutputTokens + stats.TotalCacheTokens
start := time.Unix(0, 0).UTC() start := time.Unix(0, 0).UTC()
...@@ -4272,6 +4284,7 @@ func scanModelStatsRows(rows *sql.Rows) ([]ModelStat, error) { ...@@ -4272,6 +4284,7 @@ func scanModelStatsRows(rows *sql.Rows) ([]ModelStat, error) {
&row.TotalTokens, &row.TotalTokens,
&row.Cost, &row.Cost,
&row.ActualCost, &row.ActualCost,
&row.AccountCost,
); err != nil { ); err != nil {
return nil, err return nil, err
} }
......
-- 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;
...@@ -17,7 +17,7 @@ export interface AdminUsageStatsResponse { ...@@ -17,7 +17,7 @@ export interface AdminUsageStatsResponse {
total_tokens: number total_tokens: number
total_cost: number total_cost: number
total_actual_cost: number total_actual_cost: number
total_account_cost?: number total_account_cost: number
average_duration_ms: number average_duration_ms: number
endpoints?: EndpointStat[] endpoints?: EndpointStat[]
upstream_endpoints?: EndpointStat[] upstream_endpoints?: EndpointStat[]
......
...@@ -28,17 +28,14 @@ ...@@ -28,17 +28,14 @@
<div class="min-w-0 flex-1"> <div class="min-w-0 flex-1">
<p class="text-xs font-medium text-gray-500">{{ t('usage.totalCost') }}</p> <p class="text-xs font-medium text-gray-500">{{ t('usage.totalCost') }}</p>
<p class="text-xl font-bold text-green-600"> <p class="text-xl font-bold text-green-600">
${{ ((stats?.total_account_cost ?? stats?.total_actual_cost) || 0).toFixed(4) }} ${{ (stats?.total_actual_cost || 0).toFixed(4) }}
</p> </p>
<p class="text-xs text-gray-400" v-if="stats?.total_account_cost != null"> <p class="text-xs text-gray-400">
{{ t('usage.userBilled') }}: <span class="text-orange-500">${{ (stats?.total_account_cost || 0).toFixed(4) }}</span>
<span class="text-gray-300">${{ (stats?.total_actual_cost || 0).toFixed(4) }}</span> <span> {{ t('usage.accountCost') }}</span>
· {{ t('usage.standardCost') }}: <span> · </span>
<span class="text-gray-300">${{ (stats?.total_cost || 0).toFixed(4) }}</span> <span>${{ (stats?.total_cost || 0).toFixed(4) }}</span>
</p> <span> {{ t('usage.standardCost') }}</span>
<p class="text-xs text-gray-400" v-else>
{{ t('usage.standardCost') }}:
<span class="line-through">${{ (stats?.total_cost || 0).toFixed(4) }}</span>
</p> </p>
</div> </div>
</div> </div>
......
...@@ -45,6 +45,7 @@ ...@@ -45,6 +45,7 @@
<th class="pb-2 text-right">{{ t('admin.dashboard.requests') }}</th> <th class="pb-2 text-right">{{ t('admin.dashboard.requests') }}</th>
<th class="pb-2 text-right">{{ t('admin.dashboard.tokens') }}</th> <th class="pb-2 text-right">{{ t('admin.dashboard.tokens') }}</th>
<th class="pb-2 text-right">{{ t('admin.dashboard.actual') }}</th> <th class="pb-2 text-right">{{ t('admin.dashboard.actual') }}</th>
<th class="pb-2 text-right">{{ t('admin.dashboard.accountCost') }}</th>
<th class="pb-2 text-right">{{ t('admin.dashboard.standard') }}</th> <th class="pb-2 text-right">{{ t('admin.dashboard.standard') }}</th>
</tr> </tr>
</thead> </thead>
...@@ -75,13 +76,16 @@ ...@@ -75,13 +76,16 @@
<td class="py-1.5 text-right text-green-600 dark:text-green-400"> <td class="py-1.5 text-right text-green-600 dark:text-green-400">
${{ formatCost(group.actual_cost) }} ${{ formatCost(group.actual_cost) }}
</td> </td>
<td class="py-1.5 text-right text-orange-500 dark:text-orange-400">
${{ formatCost(group.account_cost) }}
</td>
<td class="py-1.5 text-right text-gray-400 dark:text-gray-500"> <td class="py-1.5 text-right text-gray-400 dark:text-gray-500">
${{ formatCost(group.cost) }} ${{ formatCost(group.cost) }}
</td> </td>
</tr> </tr>
<!-- User breakdown sub-rows --> <!-- User breakdown sub-rows -->
<tr v-if="expandedKey === `group-${group.group_id}`"> <tr v-if="expandedKey === `group-${group.group_id}`">
<td colspan="5" class="p-0"> <td colspan="6" class="p-0">
<UserBreakdownSubTable <UserBreakdownSubTable
:items="breakdownItems" :items="breakdownItems"
:loading="breakdownLoading" :loading="breakdownLoading"
......
...@@ -114,6 +114,7 @@ ...@@ -114,6 +114,7 @@
<th class="pb-2 text-right">{{ t('admin.dashboard.requests') }}</th> <th class="pb-2 text-right">{{ t('admin.dashboard.requests') }}</th>
<th class="pb-2 text-right">{{ t('admin.dashboard.tokens') }}</th> <th class="pb-2 text-right">{{ t('admin.dashboard.tokens') }}</th>
<th class="pb-2 text-right">{{ t('admin.dashboard.actual') }}</th> <th class="pb-2 text-right">{{ t('admin.dashboard.actual') }}</th>
<th class="pb-2 text-right">{{ t('admin.dashboard.accountCost') }}</th>
<th class="pb-2 text-right">{{ t('admin.dashboard.standard') }}</th> <th class="pb-2 text-right">{{ t('admin.dashboard.standard') }}</th>
</tr> </tr>
</thead> </thead>
...@@ -142,12 +143,15 @@ ...@@ -142,12 +143,15 @@
<td class="py-1.5 text-right text-green-600 dark:text-green-400"> <td class="py-1.5 text-right text-green-600 dark:text-green-400">
${{ formatCost(model.actual_cost) }} ${{ formatCost(model.actual_cost) }}
</td> </td>
<td class="py-1.5 text-right text-orange-500 dark:text-orange-400">
${{ formatCost(model.account_cost) }}
</td>
<td class="py-1.5 text-right text-gray-400 dark:text-gray-500"> <td class="py-1.5 text-right text-gray-400 dark:text-gray-500">
${{ formatCost(model.cost) }} ${{ formatCost(model.cost) }}
</td> </td>
</tr> </tr>
<tr v-if="expandedKey === `model-${model.model}`"> <tr v-if="expandedKey === `model-${model.model}`">
<td colspan="5" class="p-0"> <td colspan="6" class="p-0">
<UserBreakdownSubTable <UserBreakdownSubTable
:items="breakdownItems" :items="breakdownItems"
:loading="breakdownLoading" :loading="breakdownLoading"
......
...@@ -732,6 +732,7 @@ export default { ...@@ -732,6 +732,7 @@ export default {
totalCost: 'Total Cost', totalCost: 'Total Cost',
standardCost: 'Standard', standardCost: 'Standard',
actualCost: 'Actual', actualCost: 'Actual',
accountCost: 'Cost',
userBilled: 'User billed', userBilled: 'User billed',
accountBilled: 'Account billed', accountBilled: 'Account billed',
accountMultiplier: 'Account rate', accountMultiplier: 'Account rate',
...@@ -1039,6 +1040,7 @@ export default { ...@@ -1039,6 +1040,7 @@ export default {
tokens: 'Tokens', tokens: 'Tokens',
actual: 'Actual', actual: 'Actual',
standard: 'Standard', standard: 'Standard',
accountCost: 'Cost',
noDataAvailable: 'No data available', noDataAvailable: 'No data available',
recentUsage: 'Recent Usage', recentUsage: 'Recent Usage',
viewModelDistribution: 'Model Distribution', viewModelDistribution: 'Model Distribution',
......
...@@ -736,6 +736,7 @@ export default { ...@@ -736,6 +736,7 @@ export default {
totalCost: '总消费', totalCost: '总消费',
standardCost: '标准', standardCost: '标准',
actualCost: '实际', actualCost: '实际',
accountCost: '成本',
userBilled: '用户扣费', userBilled: '用户扣费',
accountBilled: '账号计费', accountBilled: '账号计费',
accountMultiplier: '账号倍率', accountMultiplier: '账号倍率',
...@@ -1026,6 +1027,7 @@ export default { ...@@ -1026,6 +1027,7 @@ export default {
totalCost: '总消费', totalCost: '总消费',
actual: '实际', actual: '实际',
standard: '标准', standard: '标准',
accountCost: '成本',
todayTokens: '今日 Token', todayTokens: '今日 Token',
totalTokens: '总 Token', totalTokens: '总 Token',
input: '输入', input: '输入',
......
...@@ -1158,6 +1158,7 @@ export interface DashboardStats { ...@@ -1158,6 +1158,7 @@ export interface DashboardStats {
total_tokens: number total_tokens: number
total_cost: number // 累计标准计费 total_cost: number // 累计标准计费
total_actual_cost: number // 累计实际扣除 total_actual_cost: number // 累计实际扣除
total_account_cost: number // 累计账号成本
// 今日 Token 使用统计 // 今日 Token 使用统计
today_requests: number today_requests: number
...@@ -1168,6 +1169,7 @@ export interface DashboardStats { ...@@ -1168,6 +1169,7 @@ export interface DashboardStats {
today_tokens: number today_tokens: number
today_cost: number // 今日标准计费 today_cost: number // 今日标准计费
today_actual_cost: number // 今日实际扣除 today_actual_cost: number // 今日实际扣除
today_account_cost: number // 今日账号成本
// 系统运行统计 // 系统运行统计
average_duration_ms: number // 平均响应时间 average_duration_ms: number // 平均响应时间
...@@ -1215,6 +1217,7 @@ export interface ModelStat { ...@@ -1215,6 +1217,7 @@ export interface ModelStat {
total_tokens: number total_tokens: number
cost: number // 标准计费 cost: number // 标准计费
actual_cost: number // 实际扣除 actual_cost: number // 实际扣除
account_cost: number // 账号成本
} }
export interface EndpointStat { export interface EndpointStat {
...@@ -1232,6 +1235,7 @@ export interface GroupStat { ...@@ -1232,6 +1235,7 @@ export interface GroupStat {
total_tokens: number total_tokens: number
cost: number // 标准计费 cost: number // 标准计费
actual_cost: number // 实际扣除 actual_cost: number // 实际扣除
account_cost: number // 账号成本
} }
export interface UserBreakdownItem { export interface UserBreakdownItem {
......
...@@ -112,15 +112,21 @@ ...@@ -112,15 +112,21 @@
</p> </p>
<p class="text-xs"> <p class="text-xs">
<span <span
class="text-amber-600 dark:text-amber-400" class="text-green-600 dark:text-green-400"
:title="t('admin.dashboard.actual')" :title="t('admin.dashboard.actual')"
>${{ formatCost(stats.today_actual_cost) }}</span >${{ formatCost(stats.today_actual_cost) }}</span
> >
<span class="text-gray-400 dark:text-gray-500"> / </span>
<span
class="text-orange-500 dark:text-orange-400"
:title="t('admin.dashboard.accountCost')"
>${{ formatCost(stats.today_account_cost) }}</span
>
<span class="text-gray-400 dark:text-gray-500"> / </span>
<span <span
class="text-gray-400 dark:text-gray-500" class="text-gray-400 dark:text-gray-500"
:title="t('admin.dashboard.standard')" :title="t('admin.dashboard.standard')"
> >${{ formatCost(stats.today_cost) }}</span
/ ${{ formatCost(stats.today_cost) }}</span
> >
</p> </p>
</div> </div>
...@@ -142,15 +148,21 @@ ...@@ -142,15 +148,21 @@
</p> </p>
<p class="text-xs"> <p class="text-xs">
<span <span
class="text-indigo-600 dark:text-indigo-400" class="text-green-600 dark:text-green-400"
:title="t('admin.dashboard.actual')" :title="t('admin.dashboard.actual')"
>${{ formatCost(stats.total_actual_cost) }}</span >${{ formatCost(stats.total_actual_cost) }}</span
> >
<span class="text-gray-400 dark:text-gray-500"> / </span>
<span
class="text-orange-500 dark:text-orange-400"
:title="t('admin.dashboard.accountCost')"
>${{ formatCost(stats.total_account_cost) }}</span
>
<span class="text-gray-400 dark:text-gray-500"> / </span>
<span <span
class="text-gray-400 dark:text-gray-500" class="text-gray-400 dark:text-gray-500"
:title="t('admin.dashboard.standard')" :title="t('admin.dashboard.standard')"
> >${{ formatCost(stats.total_cost) }}</span
/ ${{ formatCost(stats.total_cost) }}</span
> >
</p> </p>
</div> </div>
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment