Merge pull request #1666 from touwaeriol/feat/account-cost-display
feat(usage): add account cost display to admin dashboard and usage pages
This commit is contained in:
@@ -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 {
|
|||||||
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 {
|
|||||||
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 {
|
|||||||
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
|
||||||
@@ -164,8 +168,9 @@ type UserBreakdownItem struct {
|
|||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
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"` // 账号成本
|
||||||
}
|
}
|
||||||
|
|
||||||
// UserBreakdownDimension specifies the dimension to filter for user breakdown.
|
// UserBreakdownDimension specifies the dimension to filter for user breakdown.
|
||||||
|
|||||||
@@ -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
|
|||||||
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
|
|||||||
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
|
|||||||
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
|
|||||||
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
|
|||||||
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
|
|||||||
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
|
|||||||
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
|
|||||||
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
|
|||||||
&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
|
|||||||
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
|
|||||||
&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
|
|||||||
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
|
|||||||
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
|
|||||||
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
|
|||||||
&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
|
|||||||
&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
|
|||||||
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
|
|||||||
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
|
|||||||
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
|
|||||||
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
|
|||||||
&row.TotalTokens,
|
&row.TotalTokens,
|
||||||
&row.Cost,
|
&row.Cost,
|
||||||
&row.ActualCost,
|
&row.ActualCost,
|
||||||
|
&row.AccountCost,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -3143,7 +3157,8 @@ func (r *usageLogRepository) GetUserBreakdownStats(ctx context.Context, startTim
|
|||||||
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 users u ON u.id = ul.user_id
|
LEFT JOIN users u ON u.id = ul.user_id
|
||||||
WHERE ul.created_at >= $1 AND ul.created_at < $2
|
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.TotalTokens,
|
||||||
&row.Cost,
|
&row.Cost,
|
||||||
&row.ActualCost,
|
&row.ActualCost,
|
||||||
|
&row.AccountCost,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -3392,9 +3408,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 +4286,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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -753,8 +753,11 @@ func (s *UsageLogRepoSuite) TestDashboardStats_TodayTotalsAndPerformance() {
|
|||||||
s.Require().Equal(baseStats.TotalTokens+int64(51), stats.TotalTokens, "TotalTokens mismatch")
|
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.TotalCost+2.3, stats.TotalCost, "TotalCost mismatch")
|
||||||
s.Require().Equal(baseStats.TotalActualCost+2.0, stats.TotalActualCost, "TotalActualCost 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.TodayRequests, int64(1), "expected TodayRequests >= 1")
|
||||||
s.Require().GreaterOrEqual(stats.TodayCost, 0.0, "expected TodayCost >= 0")
|
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)
|
wantRpm, wantTpm, err := s.repo.getPerformanceStats(s.ctx, 0)
|
||||||
s.Require().NoError(err, "getPerformanceStats")
|
s.Require().NoError(err, "getPerformanceStats")
|
||||||
@@ -833,6 +836,8 @@ func (s *UsageLogRepoSuite) TestDashboardStatsWithRange_Fallback() {
|
|||||||
s.Require().Equal(int64(45), stats.TotalTokens)
|
s.Require().Equal(int64(45), stats.TotalTokens)
|
||||||
s.Require().Equal(1.5, stats.TotalCost)
|
s.Require().Equal(1.5, stats.TotalCost)
|
||||||
s.Require().Equal(1.4, stats.TotalActualCost)
|
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)
|
s.Require().InEpsilon(150.0, stats.AverageDurationMs, 0.0001)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -301,7 +301,7 @@ func TestUsageLogRepositoryGetModelStatsWithFiltersRequestTypePriority(t *testin
|
|||||||
|
|
||||||
mock.ExpectQuery("AND \\(request_type = \\$3 OR \\(request_type = 0 AND openai_ws_mode = TRUE\\)\\)").
|
mock.ExpectQuery("AND \\(request_type = \\$3 OR \\(request_type = 0 AND openai_ws_mode = TRUE\\)\\)").
|
||||||
WithArgs(start, end, requestType).
|
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)
|
stats, err := repo.GetModelStatsWithFilters(context.Background(), start, end, 0, 0, 0, 0, &requestType, &stream, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -346,6 +346,93 @@ func TestUsageLogRepositoryGetStatsWithFiltersRequestTypePriority(t *testing.T)
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, int64(1), stats.TotalRequests)
|
require.Equal(t, int64(1), stats.TotalRequests)
|
||||||
require.Equal(t, int64(9), stats.TotalTokens)
|
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())
|
require.NoError(t, mock.ExpectationsWereMet())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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,12 @@
|
|||||||
<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">{{ t('usage.accountCost') }} ${{ (stats?.total_account_cost || 0).toFixed(4) }}</span>
|
||||||
<span class="text-gray-300">${{ (stats?.total_actual_cost || 0).toFixed(4) }}</span>
|
<span> · </span>
|
||||||
· {{ t('usage.standardCost') }}:
|
<span>{{ t('usage.standardCost') }} ${{ (stats?.total_cost || 0).toFixed(4) }}</span>
|
||||||
<span class="text-gray-300">${{ (stats?.total_cost || 0).toFixed(4) }}</span>
|
|
||||||
</p>
|
|
||||||
<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>
|
||||||
|
|||||||
@@ -154,7 +154,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="row.account_rate_multiplier != null" class="mt-0.5 text-[11px] text-gray-400">
|
<div v-if="row.account_rate_multiplier != null" class="mt-0.5 text-[11px] text-orange-500 dark:text-orange-400">
|
||||||
A ${{ accountBilled(row).toFixed(6) }}
|
A ${{ accountBilled(row).toFixed(6) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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 @@
|
|||||||
<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 @@
|
|||||||
<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 @@
|
|||||||
<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"
|
||||||
|
|||||||
@@ -25,6 +25,9 @@
|
|||||||
<td class="py-1 text-right text-green-600 dark:text-green-400">
|
<td class="py-1 text-right text-green-600 dark:text-green-400">
|
||||||
${{ formatCost(user.actual_cost) }}
|
${{ formatCost(user.actual_cost) }}
|
||||||
</td>
|
</td>
|
||||||
|
<td class="py-1 text-right text-orange-500 dark:text-orange-400">
|
||||||
|
${{ formatCost(user.account_cost) }}
|
||||||
|
</td>
|
||||||
<td class="py-1 pr-1 text-right text-gray-400 dark:text-gray-500">
|
<td class="py-1 pr-1 text-right text-gray-400 dark:text-gray-500">
|
||||||
${{ formatCost(user.cost) }}
|
${{ formatCost(user.cost) }}
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -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 {
|
|||||||
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 {
|
|||||||
totalCost: '总消费',
|
totalCost: '总消费',
|
||||||
standardCost: '标准',
|
standardCost: '标准',
|
||||||
actualCost: '实际',
|
actualCost: '实际',
|
||||||
|
accountCost: '成本',
|
||||||
userBilled: '用户扣费',
|
userBilled: '用户扣费',
|
||||||
accountBilled: '账号计费',
|
accountBilled: '账号计费',
|
||||||
accountMultiplier: '账号倍率',
|
accountMultiplier: '账号倍率',
|
||||||
@@ -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 {
|
|||||||
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 {
|
|||||||
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 {
|
|||||||
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 {
|
|||||||
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 {
|
||||||
@@ -1241,6 +1245,7 @@ export interface UserBreakdownItem {
|
|||||||
total_tokens: number
|
total_tokens: number
|
||||||
cost: number
|
cost: number
|
||||||
actual_cost: number
|
actual_cost: number
|
||||||
|
account_cost: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UserUsageTrendPoint {
|
export interface UserUsageTrendPoint {
|
||||||
|
|||||||
@@ -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 @@
|
|||||||
</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>
|
||||||
|
|||||||
Reference in New Issue
Block a user