Files
sub2api/backend/internal/repository/usage_log_repo.go
IanShaw027 481310dea0 fix(test): 修复CI测试失败
- 修复gofmt格式问题
- 为stubUsageLogRepo添加缺失的GetApiKeyStatsAggregated方法
2025-12-27 16:12:06 +08:00

1304 lines
45 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package repository
import (
"context"
"time"
"github.com/Wei-Shaw/sub2api/internal/service"
"github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
"github.com/Wei-Shaw/sub2api/internal/pkg/timezone"
"github.com/Wei-Shaw/sub2api/internal/pkg/usagestats"
"gorm.io/gorm"
)
type usageLogRepository struct {
db *gorm.DB
}
func NewUsageLogRepository(db *gorm.DB) service.UsageLogRepository {
return &usageLogRepository{db: db}
}
// getPerformanceStats 获取 RPM 和 TPM近5分钟平均值可选按用户过滤
func (r *usageLogRepository) getPerformanceStats(ctx context.Context, userID int64) (rpm, tpm int64) {
fiveMinutesAgo := time.Now().Add(-5 * time.Minute)
var perfStats struct {
RequestCount int64 `gorm:"column:request_count"`
TokenCount int64 `gorm:"column:token_count"`
}
db := r.db.WithContext(ctx).Model(&usageLogModel{}).
Select(`
COUNT(*) as request_count,
COALESCE(SUM(input_tokens + output_tokens), 0) as token_count
`).
Where("created_at >= ?", fiveMinutesAgo)
if userID > 0 {
db = db.Where("user_id = ?", userID)
}
db.Scan(&perfStats)
// 返回5分钟平均值
return perfStats.RequestCount / 5, perfStats.TokenCount / 5
}
func (r *usageLogRepository) Create(ctx context.Context, log *service.UsageLog) error {
m := usageLogModelFromService(log)
err := r.db.WithContext(ctx).Create(m).Error
if err == nil {
applyUsageLogModelToService(log, m)
}
return err
}
func (r *usageLogRepository) GetByID(ctx context.Context, id int64) (*service.UsageLog, error) {
var log usageLogModel
err := r.db.WithContext(ctx).First(&log, id).Error
if err != nil {
return nil, translatePersistenceError(err, service.ErrUsageLogNotFound, nil)
}
return usageLogModelToService(&log), nil
}
func (r *usageLogRepository) ListByUser(ctx context.Context, userID int64, params pagination.PaginationParams) ([]service.UsageLog, *pagination.PaginationResult, error) {
var logs []usageLogModel
var total int64
db := r.db.WithContext(ctx).Model(&usageLogModel{}).Where("user_id = ?", userID)
if err := db.Count(&total).Error; err != nil {
return nil, nil, err
}
if err := db.Offset(params.Offset()).Limit(params.Limit()).Order("id DESC").Find(&logs).Error; err != nil {
return nil, nil, err
}
return usageLogModelsToService(logs), paginationResultFromTotal(total, params), nil
}
func (r *usageLogRepository) ListByApiKey(ctx context.Context, apiKeyID int64, params pagination.PaginationParams) ([]service.UsageLog, *pagination.PaginationResult, error) {
var logs []usageLogModel
var total int64
db := r.db.WithContext(ctx).Model(&usageLogModel{}).Where("api_key_id = ?", apiKeyID)
if err := db.Count(&total).Error; err != nil {
return nil, nil, err
}
if err := db.Offset(params.Offset()).Limit(params.Limit()).Order("id DESC").Find(&logs).Error; err != nil {
return nil, nil, err
}
return usageLogModelsToService(logs), paginationResultFromTotal(total, params), nil
}
// UserStats 用户使用统计
type UserStats struct {
TotalRequests int64 `json:"total_requests"`
TotalTokens int64 `json:"total_tokens"`
TotalCost float64 `json:"total_cost"`
InputTokens int64 `json:"input_tokens"`
OutputTokens int64 `json:"output_tokens"`
CacheReadTokens int64 `json:"cache_read_tokens"`
}
func (r *usageLogRepository) GetUserStats(ctx context.Context, userID int64, startTime, endTime time.Time) (*UserStats, error) {
var stats UserStats
err := r.db.WithContext(ctx).Model(&usageLogModel{}).
Select(`
COUNT(*) as total_requests,
COALESCE(SUM(input_tokens + output_tokens + cache_creation_tokens + cache_read_tokens), 0) as total_tokens,
COALESCE(SUM(actual_cost), 0) as total_cost,
COALESCE(SUM(input_tokens), 0) as input_tokens,
COALESCE(SUM(output_tokens), 0) as output_tokens,
COALESCE(SUM(cache_read_tokens), 0) as cache_read_tokens
`).
Where("user_id = ? AND created_at >= ? AND created_at < ?", userID, startTime, endTime).
Scan(&stats).Error
return &stats, err
}
// DashboardStats 仪表盘统计
type DashboardStats = usagestats.DashboardStats
func (r *usageLogRepository) GetDashboardStats(ctx context.Context) (*DashboardStats, error) {
var stats DashboardStats
today := timezone.Today()
now := time.Now()
// 合并用户统计查询
var userStats struct {
TotalUsers int64 `gorm:"column:total_users"`
TodayNewUsers int64 `gorm:"column:today_new_users"`
ActiveUsers int64 `gorm:"column:active_users"`
}
if err := r.db.WithContext(ctx).Raw(`
SELECT
COUNT(*) as total_users,
COUNT(CASE WHEN created_at >= ? THEN 1 END) as today_new_users,
(SELECT COUNT(DISTINCT user_id) FROM usage_logs WHERE created_at >= ?) as active_users
FROM users
`, today, today).Scan(&userStats).Error; err != nil {
return nil, err
}
stats.TotalUsers = userStats.TotalUsers
stats.TodayNewUsers = userStats.TodayNewUsers
stats.ActiveUsers = userStats.ActiveUsers
// 合并API Key统计查询
var apiKeyStats struct {
TotalApiKeys int64 `gorm:"column:total_api_keys"`
ActiveApiKeys int64 `gorm:"column:active_api_keys"`
}
if err := r.db.WithContext(ctx).Raw(`
SELECT
COUNT(*) as total_api_keys,
COUNT(CASE WHEN status = ? THEN 1 END) as active_api_keys
FROM api_keys
`, service.StatusActive).Scan(&apiKeyStats).Error; err != nil {
return nil, err
}
stats.TotalApiKeys = apiKeyStats.TotalApiKeys
stats.ActiveApiKeys = apiKeyStats.ActiveApiKeys
// 合并账户统计查询
var accountStats struct {
TotalAccounts int64 `gorm:"column:total_accounts"`
NormalAccounts int64 `gorm:"column:normal_accounts"`
ErrorAccounts int64 `gorm:"column:error_accounts"`
RateLimitAccounts int64 `gorm:"column:ratelimit_accounts"`
OverloadAccounts int64 `gorm:"column:overload_accounts"`
}
if err := r.db.WithContext(ctx).Raw(`
SELECT
COUNT(*) as total_accounts,
COUNT(CASE WHEN status = ? AND schedulable = true THEN 1 END) as normal_accounts,
COUNT(CASE WHEN status = ? THEN 1 END) as error_accounts,
COUNT(CASE WHEN rate_limited_at IS NOT NULL AND rate_limit_reset_at > ? THEN 1 END) as ratelimit_accounts,
COUNT(CASE WHEN overload_until IS NOT NULL AND overload_until > ? THEN 1 END) as overload_accounts
FROM accounts
`, service.StatusActive, service.StatusError, now, now).Scan(&accountStats).Error; err != nil {
return nil, err
}
stats.TotalAccounts = accountStats.TotalAccounts
stats.NormalAccounts = accountStats.NormalAccounts
stats.ErrorAccounts = accountStats.ErrorAccounts
stats.RateLimitAccounts = accountStats.RateLimitAccounts
stats.OverloadAccounts = accountStats.OverloadAccounts
// 累计 Token 统计
var totalStats struct {
TotalRequests int64 `gorm:"column:total_requests"`
TotalInputTokens int64 `gorm:"column:total_input_tokens"`
TotalOutputTokens int64 `gorm:"column:total_output_tokens"`
TotalCacheCreationTokens int64 `gorm:"column:total_cache_creation_tokens"`
TotalCacheReadTokens int64 `gorm:"column:total_cache_read_tokens"`
TotalCost float64 `gorm:"column:total_cost"`
TotalActualCost float64 `gorm:"column:total_actual_cost"`
AverageDurationMs float64 `gorm:"column:avg_duration_ms"`
}
r.db.WithContext(ctx).Model(&usageLogModel{}).
Select(`
COUNT(*) as total_requests,
COALESCE(SUM(input_tokens), 0) as total_input_tokens,
COALESCE(SUM(output_tokens), 0) as total_output_tokens,
COALESCE(SUM(cache_creation_tokens), 0) as total_cache_creation_tokens,
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(AVG(duration_ms), 0) as avg_duration_ms
`).
Scan(&totalStats)
stats.TotalRequests = totalStats.TotalRequests
stats.TotalInputTokens = totalStats.TotalInputTokens
stats.TotalOutputTokens = totalStats.TotalOutputTokens
stats.TotalCacheCreationTokens = totalStats.TotalCacheCreationTokens
stats.TotalCacheReadTokens = totalStats.TotalCacheReadTokens
stats.TotalTokens = stats.TotalInputTokens + stats.TotalOutputTokens + stats.TotalCacheCreationTokens + stats.TotalCacheReadTokens
stats.TotalCost = totalStats.TotalCost
stats.TotalActualCost = totalStats.TotalActualCost
stats.AverageDurationMs = totalStats.AverageDurationMs
// 今日 Token 统计
var todayStats struct {
TodayRequests int64 `gorm:"column:today_requests"`
TodayInputTokens int64 `gorm:"column:today_input_tokens"`
TodayOutputTokens int64 `gorm:"column:today_output_tokens"`
TodayCacheCreationTokens int64 `gorm:"column:today_cache_creation_tokens"`
TodayCacheReadTokens int64 `gorm:"column:today_cache_read_tokens"`
TodayCost float64 `gorm:"column:today_cost"`
TodayActualCost float64 `gorm:"column:today_actual_cost"`
}
r.db.WithContext(ctx).Model(&usageLogModel{}).
Select(`
COUNT(*) as today_requests,
COALESCE(SUM(input_tokens), 0) as today_input_tokens,
COALESCE(SUM(output_tokens), 0) as today_output_tokens,
COALESCE(SUM(cache_creation_tokens), 0) as today_cache_creation_tokens,
COALESCE(SUM(cache_read_tokens), 0) as today_cache_read_tokens,
COALESCE(SUM(total_cost), 0) as today_cost,
COALESCE(SUM(actual_cost), 0) as today_actual_cost
`).
Where("created_at >= ?", today).
Scan(&todayStats)
stats.TodayRequests = todayStats.TodayRequests
stats.TodayInputTokens = todayStats.TodayInputTokens
stats.TodayOutputTokens = todayStats.TodayOutputTokens
stats.TodayCacheCreationTokens = todayStats.TodayCacheCreationTokens
stats.TodayCacheReadTokens = todayStats.TodayCacheReadTokens
stats.TodayTokens = stats.TodayInputTokens + stats.TodayOutputTokens + stats.TodayCacheCreationTokens + stats.TodayCacheReadTokens
stats.TodayCost = todayStats.TodayCost
stats.TodayActualCost = todayStats.TodayActualCost
// 性能指标RPM 和 TPM最近1分钟全局
stats.Rpm, stats.Tpm = r.getPerformanceStats(ctx, 0)
return &stats, nil
}
func (r *usageLogRepository) ListByAccount(ctx context.Context, accountID int64, params pagination.PaginationParams) ([]service.UsageLog, *pagination.PaginationResult, error) {
var logs []usageLogModel
var total int64
db := r.db.WithContext(ctx).Model(&usageLogModel{}).Where("account_id = ?", accountID)
if err := db.Count(&total).Error; err != nil {
return nil, nil, err
}
if err := db.Offset(params.Offset()).Limit(params.Limit()).Order("id DESC").Find(&logs).Error; err != nil {
return nil, nil, err
}
return usageLogModelsToService(logs), paginationResultFromTotal(total, params), nil
}
func (r *usageLogRepository) ListByUserAndTimeRange(ctx context.Context, userID int64, startTime, endTime time.Time) ([]service.UsageLog, *pagination.PaginationResult, error) {
var logs []usageLogModel
err := r.db.WithContext(ctx).
Where("user_id = ? AND created_at >= ? AND created_at < ?", userID, startTime, endTime).
Order("id DESC").
Find(&logs).Error
return usageLogModelsToService(logs), nil, err
}
// GetUserStatsAggregated returns aggregated usage statistics for a user using database-level aggregation
func (r *usageLogRepository) GetUserStatsAggregated(ctx context.Context, userID int64, startTime, endTime time.Time) (*usagestats.UsageStats, error) {
var stats struct {
TotalRequests int64 `gorm:"column:total_requests"`
TotalInputTokens int64 `gorm:"column:total_input_tokens"`
TotalOutputTokens int64 `gorm:"column:total_output_tokens"`
TotalCacheTokens int64 `gorm:"column:total_cache_tokens"`
TotalCost float64 `gorm:"column:total_cost"`
TotalActualCost float64 `gorm:"column:total_actual_cost"`
AverageDurationMs float64 `gorm:"column:avg_duration_ms"`
}
err := r.db.WithContext(ctx).Model(&usageLogModel{}).
Select(`
COUNT(*) as total_requests,
COALESCE(SUM(input_tokens), 0) as total_input_tokens,
COALESCE(SUM(output_tokens), 0) as total_output_tokens,
COALESCE(SUM(cache_creation_tokens + cache_read_tokens), 0) as total_cache_tokens,
COALESCE(SUM(total_cost), 0) as total_cost,
COALESCE(SUM(actual_cost), 0) as total_actual_cost,
COALESCE(AVG(COALESCE(duration_ms, 0)), 0) as avg_duration_ms
`).
Where("user_id = ? AND created_at >= ? AND created_at < ?", userID, startTime, endTime).
Scan(&stats).Error
if err != nil {
return nil, err
}
return &usagestats.UsageStats{
TotalRequests: stats.TotalRequests,
TotalInputTokens: stats.TotalInputTokens,
TotalOutputTokens: stats.TotalOutputTokens,
TotalCacheTokens: stats.TotalCacheTokens,
TotalTokens: stats.TotalInputTokens + stats.TotalOutputTokens + stats.TotalCacheTokens,
TotalCost: stats.TotalCost,
TotalActualCost: stats.TotalActualCost,
AverageDurationMs: stats.AverageDurationMs,
}, nil
}
// GetApiKeyStatsAggregated returns aggregated usage statistics for an API key using database-level aggregation
func (r *usageLogRepository) GetApiKeyStatsAggregated(ctx context.Context, apiKeyID int64, startTime, endTime time.Time) (*usagestats.UsageStats, error) {
var stats struct {
TotalRequests int64 `gorm:"column:total_requests"`
TotalInputTokens int64 `gorm:"column:total_input_tokens"`
TotalOutputTokens int64 `gorm:"column:total_output_tokens"`
TotalCacheTokens int64 `gorm:"column:total_cache_tokens"`
TotalCost float64 `gorm:"column:total_cost"`
TotalActualCost float64 `gorm:"column:total_actual_cost"`
AverageDurationMs float64 `gorm:"column:avg_duration_ms"`
}
err := r.db.WithContext(ctx).Model(&usageLogModel{}).
Select(`
COUNT(*) as total_requests,
COALESCE(SUM(input_tokens), 0) as total_input_tokens,
COALESCE(SUM(output_tokens), 0) as total_output_tokens,
COALESCE(SUM(cache_creation_tokens + cache_read_tokens), 0) as total_cache_tokens,
COALESCE(SUM(total_cost), 0) as total_cost,
COALESCE(SUM(actual_cost), 0) as total_actual_cost,
COALESCE(AVG(COALESCE(duration_ms, 0)), 0) as avg_duration_ms
`).
Where("api_key_id = ? AND created_at >= ? AND created_at < ?", apiKeyID, startTime, endTime).
Scan(&stats).Error
if err != nil {
return nil, err
}
return &usagestats.UsageStats{
TotalRequests: stats.TotalRequests,
TotalInputTokens: stats.TotalInputTokens,
TotalOutputTokens: stats.TotalOutputTokens,
TotalCacheTokens: stats.TotalCacheTokens,
TotalTokens: stats.TotalInputTokens + stats.TotalOutputTokens + stats.TotalCacheTokens,
TotalCost: stats.TotalCost,
TotalActualCost: stats.TotalActualCost,
AverageDurationMs: stats.AverageDurationMs,
}, nil
}
func (r *usageLogRepository) ListByApiKeyAndTimeRange(ctx context.Context, apiKeyID int64, startTime, endTime time.Time) ([]service.UsageLog, *pagination.PaginationResult, error) {
var logs []usageLogModel
err := r.db.WithContext(ctx).
Where("api_key_id = ? AND created_at >= ? AND created_at < ?", apiKeyID, startTime, endTime).
Order("id DESC").
Find(&logs).Error
return usageLogModelsToService(logs), nil, err
}
func (r *usageLogRepository) ListByAccountAndTimeRange(ctx context.Context, accountID int64, startTime, endTime time.Time) ([]service.UsageLog, *pagination.PaginationResult, error) {
var logs []usageLogModel
err := r.db.WithContext(ctx).
Where("account_id = ? AND created_at >= ? AND created_at < ?", accountID, startTime, endTime).
Order("id DESC").
Find(&logs).Error
return usageLogModelsToService(logs), nil, err
}
func (r *usageLogRepository) ListByModelAndTimeRange(ctx context.Context, modelName string, startTime, endTime time.Time) ([]service.UsageLog, *pagination.PaginationResult, error) {
var logs []usageLogModel
err := r.db.WithContext(ctx).
Where("model = ? AND created_at >= ? AND created_at < ?", modelName, startTime, endTime).
Order("id DESC").
Find(&logs).Error
return usageLogModelsToService(logs), nil, err
}
func (r *usageLogRepository) Delete(ctx context.Context, id int64) error {
return r.db.WithContext(ctx).Delete(&usageLogModel{}, id).Error
}
// GetAccountTodayStats 获取账号今日统计
func (r *usageLogRepository) GetAccountTodayStats(ctx context.Context, accountID int64) (*usagestats.AccountStats, error) {
today := timezone.Today()
var stats struct {
Requests int64 `gorm:"column:requests"`
Tokens int64 `gorm:"column:tokens"`
Cost float64 `gorm:"column:cost"`
}
err := r.db.WithContext(ctx).Model(&usageLogModel{}).
Select(`
COUNT(*) as requests,
COALESCE(SUM(input_tokens + output_tokens + cache_creation_tokens + cache_read_tokens), 0) as tokens,
COALESCE(SUM(actual_cost), 0) as cost
`).
Where("account_id = ? AND created_at >= ?", accountID, today).
Scan(&stats).Error
if err != nil {
return nil, err
}
return &usagestats.AccountStats{
Requests: stats.Requests,
Tokens: stats.Tokens,
Cost: stats.Cost,
}, nil
}
// GetAccountWindowStats 获取账号时间窗口内的统计
func (r *usageLogRepository) GetAccountWindowStats(ctx context.Context, accountID int64, startTime time.Time) (*usagestats.AccountStats, error) {
var stats struct {
Requests int64 `gorm:"column:requests"`
Tokens int64 `gorm:"column:tokens"`
Cost float64 `gorm:"column:cost"`
}
err := r.db.WithContext(ctx).Model(&usageLogModel{}).
Select(`
COUNT(*) as requests,
COALESCE(SUM(input_tokens + output_tokens + cache_creation_tokens + cache_read_tokens), 0) as tokens,
COALESCE(SUM(actual_cost), 0) as cost
`).
Where("account_id = ? AND created_at >= ?", accountID, startTime).
Scan(&stats).Error
if err != nil {
return nil, err
}
return &usagestats.AccountStats{
Requests: stats.Requests,
Tokens: stats.Tokens,
Cost: stats.Cost,
}, nil
}
// TrendDataPoint represents a single point in trend data
type TrendDataPoint = usagestats.TrendDataPoint
// ModelStat represents usage statistics for a single model
type ModelStat = usagestats.ModelStat
// UserUsageTrendPoint represents user usage trend data point
type UserUsageTrendPoint = usagestats.UserUsageTrendPoint
// ApiKeyUsageTrendPoint represents API key usage trend data point
type ApiKeyUsageTrendPoint = usagestats.ApiKeyUsageTrendPoint
// GetApiKeyUsageTrend returns usage trend data grouped by API key and date
func (r *usageLogRepository) GetApiKeyUsageTrend(ctx context.Context, startTime, endTime time.Time, granularity string, limit int) ([]ApiKeyUsageTrendPoint, error) {
var results []ApiKeyUsageTrendPoint
// Choose date format based on granularity
var dateFormat string
if granularity == "hour" {
dateFormat = "YYYY-MM-DD HH24:00"
} else {
dateFormat = "YYYY-MM-DD"
}
// Use raw SQL for complex subquery
query := `
WITH top_keys AS (
SELECT api_key_id
FROM usage_logs
WHERE created_at >= ? AND created_at < ?
GROUP BY api_key_id
ORDER BY SUM(input_tokens + output_tokens + cache_creation_tokens + cache_read_tokens) DESC
LIMIT ?
)
SELECT
TO_CHAR(u.created_at, '` + dateFormat + `') as date,
u.api_key_id,
COALESCE(k.name, '') as key_name,
COUNT(*) as requests,
COALESCE(SUM(u.input_tokens + u.output_tokens + u.cache_creation_tokens + u.cache_read_tokens), 0) as tokens
FROM usage_logs u
LEFT JOIN api_keys k ON u.api_key_id = k.id
WHERE u.api_key_id IN (SELECT api_key_id FROM top_keys)
AND u.created_at >= ? AND u.created_at < ?
GROUP BY date, u.api_key_id, k.name
ORDER BY date ASC, tokens DESC
`
err := r.db.WithContext(ctx).Raw(query, startTime, endTime, limit, startTime, endTime).Scan(&results).Error
if err != nil {
return nil, err
}
return results, nil
}
// GetUserUsageTrend returns usage trend data grouped by user and date
func (r *usageLogRepository) GetUserUsageTrend(ctx context.Context, startTime, endTime time.Time, granularity string, limit int) ([]UserUsageTrendPoint, error) {
var results []UserUsageTrendPoint
// Choose date format based on granularity
var dateFormat string
if granularity == "hour" {
dateFormat = "YYYY-MM-DD HH24:00"
} else {
dateFormat = "YYYY-MM-DD"
}
// Use raw SQL for complex subquery
query := `
WITH top_users AS (
SELECT user_id
FROM usage_logs
WHERE created_at >= ? AND created_at < ?
GROUP BY user_id
ORDER BY SUM(input_tokens + output_tokens + cache_creation_tokens + cache_read_tokens) DESC
LIMIT ?
)
SELECT
TO_CHAR(u.created_at, '` + dateFormat + `') as date,
u.user_id,
COALESCE(us.email, '') as email,
COUNT(*) as requests,
COALESCE(SUM(u.input_tokens + u.output_tokens + u.cache_creation_tokens + u.cache_read_tokens), 0) as tokens,
COALESCE(SUM(u.total_cost), 0) as cost,
COALESCE(SUM(u.actual_cost), 0) as actual_cost
FROM usage_logs u
LEFT JOIN users us ON u.user_id = us.id
WHERE u.user_id IN (SELECT user_id FROM top_users)
AND u.created_at >= ? AND u.created_at < ?
GROUP BY date, u.user_id, us.email
ORDER BY date ASC, tokens DESC
`
err := r.db.WithContext(ctx).Raw(query, startTime, endTime, limit, startTime, endTime).Scan(&results).Error
if err != nil {
return nil, err
}
return results, nil
}
// UserDashboardStats 用户仪表盘统计
type UserDashboardStats = usagestats.UserDashboardStats
// GetUserDashboardStats 获取用户专属的仪表盘统计
func (r *usageLogRepository) GetUserDashboardStats(ctx context.Context, userID int64) (*UserDashboardStats, error) {
var stats UserDashboardStats
today := timezone.Today()
// API Key 统计
r.db.WithContext(ctx).Model(&apiKeyModel{}).
Where("user_id = ?", userID).
Count(&stats.TotalApiKeys)
r.db.WithContext(ctx).Model(&apiKeyModel{}).
Where("user_id = ? AND status = ?", userID, service.StatusActive).
Count(&stats.ActiveApiKeys)
// 累计 Token 统计
var totalStats struct {
TotalRequests int64 `gorm:"column:total_requests"`
TotalInputTokens int64 `gorm:"column:total_input_tokens"`
TotalOutputTokens int64 `gorm:"column:total_output_tokens"`
TotalCacheCreationTokens int64 `gorm:"column:total_cache_creation_tokens"`
TotalCacheReadTokens int64 `gorm:"column:total_cache_read_tokens"`
TotalCost float64 `gorm:"column:total_cost"`
TotalActualCost float64 `gorm:"column:total_actual_cost"`
AverageDurationMs float64 `gorm:"column:avg_duration_ms"`
}
r.db.WithContext(ctx).Model(&usageLogModel{}).
Select(`
COUNT(*) as total_requests,
COALESCE(SUM(input_tokens), 0) as total_input_tokens,
COALESCE(SUM(output_tokens), 0) as total_output_tokens,
COALESCE(SUM(cache_creation_tokens), 0) as total_cache_creation_tokens,
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(AVG(duration_ms), 0) as avg_duration_ms
`).
Where("user_id = ?", userID).
Scan(&totalStats)
stats.TotalRequests = totalStats.TotalRequests
stats.TotalInputTokens = totalStats.TotalInputTokens
stats.TotalOutputTokens = totalStats.TotalOutputTokens
stats.TotalCacheCreationTokens = totalStats.TotalCacheCreationTokens
stats.TotalCacheReadTokens = totalStats.TotalCacheReadTokens
stats.TotalTokens = stats.TotalInputTokens + stats.TotalOutputTokens + stats.TotalCacheCreationTokens + stats.TotalCacheReadTokens
stats.TotalCost = totalStats.TotalCost
stats.TotalActualCost = totalStats.TotalActualCost
stats.AverageDurationMs = totalStats.AverageDurationMs
// 今日 Token 统计
var todayStats struct {
TodayRequests int64 `gorm:"column:today_requests"`
TodayInputTokens int64 `gorm:"column:today_input_tokens"`
TodayOutputTokens int64 `gorm:"column:today_output_tokens"`
TodayCacheCreationTokens int64 `gorm:"column:today_cache_creation_tokens"`
TodayCacheReadTokens int64 `gorm:"column:today_cache_read_tokens"`
TodayCost float64 `gorm:"column:today_cost"`
TodayActualCost float64 `gorm:"column:today_actual_cost"`
}
r.db.WithContext(ctx).Model(&usageLogModel{}).
Select(`
COUNT(*) as today_requests,
COALESCE(SUM(input_tokens), 0) as today_input_tokens,
COALESCE(SUM(output_tokens), 0) as today_output_tokens,
COALESCE(SUM(cache_creation_tokens), 0) as today_cache_creation_tokens,
COALESCE(SUM(cache_read_tokens), 0) as today_cache_read_tokens,
COALESCE(SUM(total_cost), 0) as today_cost,
COALESCE(SUM(actual_cost), 0) as today_actual_cost
`).
Where("user_id = ? AND created_at >= ?", userID, today).
Scan(&todayStats)
stats.TodayRequests = todayStats.TodayRequests
stats.TodayInputTokens = todayStats.TodayInputTokens
stats.TodayOutputTokens = todayStats.TodayOutputTokens
stats.TodayCacheCreationTokens = todayStats.TodayCacheCreationTokens
stats.TodayCacheReadTokens = todayStats.TodayCacheReadTokens
stats.TodayTokens = stats.TodayInputTokens + stats.TodayOutputTokens + stats.TodayCacheCreationTokens + stats.TodayCacheReadTokens
stats.TodayCost = todayStats.TodayCost
stats.TodayActualCost = todayStats.TodayActualCost
// 性能指标RPM 和 TPM最近1分钟仅统计该用户的请求
stats.Rpm, stats.Tpm = r.getPerformanceStats(ctx, userID)
return &stats, nil
}
// GetUserUsageTrendByUserID 获取指定用户的使用趋势
func (r *usageLogRepository) GetUserUsageTrendByUserID(ctx context.Context, userID int64, startTime, endTime time.Time, granularity string) ([]TrendDataPoint, error) {
var results []TrendDataPoint
var dateFormat string
if granularity == "hour" {
dateFormat = "YYYY-MM-DD HH24:00"
} else {
dateFormat = "YYYY-MM-DD"
}
err := r.db.WithContext(ctx).Model(&usageLogModel{}).
Select(`
TO_CHAR(created_at, ?) as date,
COUNT(*) as requests,
COALESCE(SUM(input_tokens), 0) as input_tokens,
COALESCE(SUM(output_tokens), 0) as output_tokens,
COALESCE(SUM(cache_creation_tokens + cache_read_tokens), 0) as cache_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
`, dateFormat).
Where("user_id = ? AND created_at >= ? AND created_at < ?", userID, startTime, endTime).
Group("date").
Order("date ASC").
Scan(&results).Error
if err != nil {
return nil, err
}
return results, nil
}
// GetUserModelStats 获取指定用户的模型统计
func (r *usageLogRepository) GetUserModelStats(ctx context.Context, userID int64, startTime, endTime time.Time) ([]ModelStat, error) {
var results []ModelStat
err := r.db.WithContext(ctx).Model(&usageLogModel{}).
Select(`
model,
COUNT(*) as requests,
COALESCE(SUM(input_tokens), 0) as input_tokens,
COALESCE(SUM(output_tokens), 0) as output_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
`).
Where("user_id = ? AND created_at >= ? AND created_at < ?", userID, startTime, endTime).
Group("model").
Order("total_tokens DESC").
Scan(&results).Error
if err != nil {
return nil, err
}
return results, nil
}
// UsageLogFilters represents filters for usage log queries
type UsageLogFilters = usagestats.UsageLogFilters
// ListWithFilters lists usage logs with optional filters (for admin)
func (r *usageLogRepository) ListWithFilters(ctx context.Context, params pagination.PaginationParams, filters UsageLogFilters) ([]service.UsageLog, *pagination.PaginationResult, error) {
var logs []usageLogModel
var total int64
db := r.db.WithContext(ctx).Model(&usageLogModel{})
// Apply filters
if filters.UserID > 0 {
db = db.Where("user_id = ?", filters.UserID)
}
if filters.ApiKeyID > 0 {
db = db.Where("api_key_id = ?", filters.ApiKeyID)
}
if filters.AccountID > 0 {
db = db.Where("account_id = ?", filters.AccountID)
}
if filters.GroupID > 0 {
db = db.Where("group_id = ?", filters.GroupID)
}
if filters.Model != "" {
db = db.Where("model = ?", filters.Model)
}
if filters.Stream != nil {
db = db.Where("stream = ?", *filters.Stream)
}
if filters.BillingType != nil {
db = db.Where("billing_type = ?", *filters.BillingType)
}
if filters.StartTime != nil {
db = db.Where("created_at >= ?", *filters.StartTime)
}
if filters.EndTime != nil {
db = db.Where("created_at <= ?", *filters.EndTime)
}
if err := db.Count(&total).Error; err != nil {
return nil, nil, err
}
// Preload user, api_key, account, and group for display
if err := db.Preload("User").Preload("ApiKey").Preload("Account").Preload("Group").
Offset(params.Offset()).Limit(params.Limit()).
Order("id DESC").Find(&logs).Error; err != nil {
return nil, nil, err
}
return usageLogModelsToService(logs), paginationResultFromTotal(total, params), nil
}
// UsageStats represents usage statistics
type UsageStats = usagestats.UsageStats
// BatchUserUsageStats represents usage stats for a single user
type BatchUserUsageStats = usagestats.BatchUserUsageStats
// GetBatchUserUsageStats gets today and total actual_cost for multiple users
func (r *usageLogRepository) GetBatchUserUsageStats(ctx context.Context, userIDs []int64) (map[int64]*BatchUserUsageStats, error) {
if len(userIDs) == 0 {
return make(map[int64]*BatchUserUsageStats), nil
}
today := timezone.Today()
result := make(map[int64]*BatchUserUsageStats)
// Initialize result map
for _, id := range userIDs {
result[id] = &BatchUserUsageStats{UserID: id}
}
// Get total actual_cost per user
var totalStats []struct {
UserID int64 `gorm:"column:user_id"`
TotalCost float64 `gorm:"column:total_cost"`
}
err := r.db.WithContext(ctx).Model(&usageLogModel{}).
Select("user_id, COALESCE(SUM(actual_cost), 0) as total_cost").
Where("user_id IN ?", userIDs).
Group("user_id").
Scan(&totalStats).Error
if err != nil {
return nil, err
}
for _, stat := range totalStats {
if s, ok := result[stat.UserID]; ok {
s.TotalActualCost = stat.TotalCost
}
}
// Get today actual_cost per user
var todayStats []struct {
UserID int64 `gorm:"column:user_id"`
TodayCost float64 `gorm:"column:today_cost"`
}
err = r.db.WithContext(ctx).Model(&usageLogModel{}).
Select("user_id, COALESCE(SUM(actual_cost), 0) as today_cost").
Where("user_id IN ? AND created_at >= ?", userIDs, today).
Group("user_id").
Scan(&todayStats).Error
if err != nil {
return nil, err
}
for _, stat := range todayStats {
if s, ok := result[stat.UserID]; ok {
s.TodayActualCost = stat.TodayCost
}
}
return result, nil
}
// BatchApiKeyUsageStats represents usage stats for a single API key
type BatchApiKeyUsageStats = usagestats.BatchApiKeyUsageStats
// GetBatchApiKeyUsageStats gets today and total actual_cost for multiple API keys
func (r *usageLogRepository) GetBatchApiKeyUsageStats(ctx context.Context, apiKeyIDs []int64) (map[int64]*BatchApiKeyUsageStats, error) {
if len(apiKeyIDs) == 0 {
return make(map[int64]*BatchApiKeyUsageStats), nil
}
today := timezone.Today()
result := make(map[int64]*BatchApiKeyUsageStats)
// Initialize result map
for _, id := range apiKeyIDs {
result[id] = &BatchApiKeyUsageStats{ApiKeyID: id}
}
// Get total actual_cost per api key
var totalStats []struct {
ApiKeyID int64 `gorm:"column:api_key_id"`
TotalCost float64 `gorm:"column:total_cost"`
}
err := r.db.WithContext(ctx).Model(&usageLogModel{}).
Select("api_key_id, COALESCE(SUM(actual_cost), 0) as total_cost").
Where("api_key_id IN ?", apiKeyIDs).
Group("api_key_id").
Scan(&totalStats).Error
if err != nil {
return nil, err
}
for _, stat := range totalStats {
if s, ok := result[stat.ApiKeyID]; ok {
s.TotalActualCost = stat.TotalCost
}
}
// Get today actual_cost per api key
var todayStats []struct {
ApiKeyID int64 `gorm:"column:api_key_id"`
TodayCost float64 `gorm:"column:today_cost"`
}
err = r.db.WithContext(ctx).Model(&usageLogModel{}).
Select("api_key_id, COALESCE(SUM(actual_cost), 0) as today_cost").
Where("api_key_id IN ? AND created_at >= ?", apiKeyIDs, today).
Group("api_key_id").
Scan(&todayStats).Error
if err != nil {
return nil, err
}
for _, stat := range todayStats {
if s, ok := result[stat.ApiKeyID]; ok {
s.TodayActualCost = stat.TodayCost
}
}
return result, nil
}
// GetUsageTrendWithFilters returns usage trend data with optional user/api_key filters
func (r *usageLogRepository) GetUsageTrendWithFilters(ctx context.Context, startTime, endTime time.Time, granularity string, userID, apiKeyID int64) ([]TrendDataPoint, error) {
var results []TrendDataPoint
var dateFormat string
if granularity == "hour" {
dateFormat = "YYYY-MM-DD HH24:00"
} else {
dateFormat = "YYYY-MM-DD"
}
db := r.db.WithContext(ctx).Model(&usageLogModel{}).
Select(`
TO_CHAR(created_at, ?) as date,
COUNT(*) as requests,
COALESCE(SUM(input_tokens), 0) as input_tokens,
COALESCE(SUM(output_tokens), 0) as output_tokens,
COALESCE(SUM(cache_creation_tokens + cache_read_tokens), 0) as cache_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
`, dateFormat).
Where("created_at >= ? AND created_at < ?", startTime, endTime)
if userID > 0 {
db = db.Where("user_id = ?", userID)
}
if apiKeyID > 0 {
db = db.Where("api_key_id = ?", apiKeyID)
}
err := db.Group("date").Order("date ASC").Scan(&results).Error
if err != nil {
return nil, err
}
return results, nil
}
// GetModelStatsWithFilters returns model statistics with optional user/api_key filters
func (r *usageLogRepository) GetModelStatsWithFilters(ctx context.Context, startTime, endTime time.Time, userID, apiKeyID, accountID int64) ([]ModelStat, error) {
var results []ModelStat
db := r.db.WithContext(ctx).Model(&usageLogModel{}).
Select(`
model,
COUNT(*) as requests,
COALESCE(SUM(input_tokens), 0) as input_tokens,
COALESCE(SUM(output_tokens), 0) as output_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
`).
Where("created_at >= ? AND created_at < ?", startTime, endTime)
if userID > 0 {
db = db.Where("user_id = ?", userID)
}
if apiKeyID > 0 {
db = db.Where("api_key_id = ?", apiKeyID)
}
if accountID > 0 {
db = db.Where("account_id = ?", accountID)
}
err := db.Group("model").Order("total_tokens DESC").Scan(&results).Error
if err != nil {
return nil, err
}
return results, nil
}
// GetGlobalStats gets usage statistics for all users within a time range
func (r *usageLogRepository) GetGlobalStats(ctx context.Context, startTime, endTime time.Time) (*UsageStats, error) {
var stats struct {
TotalRequests int64 `gorm:"column:total_requests"`
TotalInputTokens int64 `gorm:"column:total_input_tokens"`
TotalOutputTokens int64 `gorm:"column:total_output_tokens"`
TotalCacheTokens int64 `gorm:"column:total_cache_tokens"`
TotalCost float64 `gorm:"column:total_cost"`
TotalActualCost float64 `gorm:"column:total_actual_cost"`
AverageDurationMs float64 `gorm:"column:avg_duration_ms"`
}
err := r.db.WithContext(ctx).Model(&usageLogModel{}).
Select(`
COUNT(*) as total_requests,
COALESCE(SUM(input_tokens), 0) as total_input_tokens,
COALESCE(SUM(output_tokens), 0) as total_output_tokens,
COALESCE(SUM(cache_creation_tokens + cache_read_tokens), 0) as total_cache_tokens,
COALESCE(SUM(total_cost), 0) as total_cost,
COALESCE(SUM(actual_cost), 0) as total_actual_cost,
COALESCE(AVG(duration_ms), 0) as avg_duration_ms
`).
Where("created_at >= ? AND created_at <= ?", startTime, endTime).
Scan(&stats).Error
if err != nil {
return nil, err
}
return &UsageStats{
TotalRequests: stats.TotalRequests,
TotalInputTokens: stats.TotalInputTokens,
TotalOutputTokens: stats.TotalOutputTokens,
TotalCacheTokens: stats.TotalCacheTokens,
TotalTokens: stats.TotalInputTokens + stats.TotalOutputTokens + stats.TotalCacheTokens,
TotalCost: stats.TotalCost,
TotalActualCost: stats.TotalActualCost,
AverageDurationMs: stats.AverageDurationMs,
}, nil
}
// AccountUsageHistory represents daily usage history for an account
type AccountUsageHistory = usagestats.AccountUsageHistory
// AccountUsageSummary represents summary statistics for an account
type AccountUsageSummary = usagestats.AccountUsageSummary
// AccountUsageStatsResponse represents the full usage statistics response for an account
type AccountUsageStatsResponse = usagestats.AccountUsageStatsResponse
// GetAccountUsageStats returns comprehensive usage statistics for an account over a time range
func (r *usageLogRepository) GetAccountUsageStats(ctx context.Context, accountID int64, startTime, endTime time.Time) (*AccountUsageStatsResponse, error) {
daysCount := int(endTime.Sub(startTime).Hours()/24) + 1
if daysCount <= 0 {
daysCount = 30
}
// Get daily history
var historyResults []struct {
Date string `gorm:"column:date"`
Requests int64 `gorm:"column:requests"`
Tokens int64 `gorm:"column:tokens"`
Cost float64 `gorm:"column:cost"`
ActualCost float64 `gorm:"column:actual_cost"`
}
err := r.db.WithContext(ctx).Model(&usageLogModel{}).
Select(`
TO_CHAR(created_at, 'YYYY-MM-DD') as date,
COUNT(*) as requests,
COALESCE(SUM(input_tokens + output_tokens + cache_creation_tokens + cache_read_tokens), 0) as tokens,
COALESCE(SUM(total_cost), 0) as cost,
COALESCE(SUM(actual_cost), 0) as actual_cost
`).
Where("account_id = ? AND created_at >= ? AND created_at < ?", accountID, startTime, endTime).
Group("date").
Order("date ASC").
Scan(&historyResults).Error
if err != nil {
return nil, err
}
// Build history with labels
history := make([]AccountUsageHistory, 0, len(historyResults))
for _, h := range historyResults {
// Parse date to get label (MM/DD)
t, _ := time.Parse("2006-01-02", h.Date)
label := t.Format("01/02")
history = append(history, AccountUsageHistory{
Date: h.Date,
Label: label,
Requests: h.Requests,
Tokens: h.Tokens,
Cost: h.Cost,
ActualCost: h.ActualCost,
})
}
// Calculate summary
var totalActualCost, totalStandardCost float64
var totalRequests, totalTokens int64
var highestCostDay, highestRequestDay *AccountUsageHistory
for i := range history {
h := &history[i]
totalActualCost += h.ActualCost
totalStandardCost += h.Cost
totalRequests += h.Requests
totalTokens += h.Tokens
if highestCostDay == nil || h.ActualCost > highestCostDay.ActualCost {
highestCostDay = h
}
if highestRequestDay == nil || h.Requests > highestRequestDay.Requests {
highestRequestDay = h
}
}
actualDaysUsed := len(history)
if actualDaysUsed == 0 {
actualDaysUsed = 1
}
// Get average duration
var avgDuration struct {
AvgDurationMs float64 `gorm:"column:avg_duration_ms"`
}
r.db.WithContext(ctx).Model(&usageLogModel{}).
Select("COALESCE(AVG(duration_ms), 0) as avg_duration_ms").
Where("account_id = ? AND created_at >= ? AND created_at < ?", accountID, startTime, endTime).
Scan(&avgDuration)
summary := AccountUsageSummary{
Days: daysCount,
ActualDaysUsed: actualDaysUsed,
TotalCost: totalActualCost,
TotalStandardCost: totalStandardCost,
TotalRequests: totalRequests,
TotalTokens: totalTokens,
AvgDailyCost: totalActualCost / float64(actualDaysUsed),
AvgDailyRequests: float64(totalRequests) / float64(actualDaysUsed),
AvgDailyTokens: float64(totalTokens) / float64(actualDaysUsed),
AvgDurationMs: avgDuration.AvgDurationMs,
}
// Set today's stats
todayStr := timezone.Now().Format("2006-01-02")
for i := range history {
if history[i].Date == todayStr {
summary.Today = &struct {
Date string `json:"date"`
Cost float64 `json:"cost"`
Requests int64 `json:"requests"`
Tokens int64 `json:"tokens"`
}{
Date: history[i].Date,
Cost: history[i].ActualCost,
Requests: history[i].Requests,
Tokens: history[i].Tokens,
}
break
}
}
// Set highest cost day
if highestCostDay != nil {
summary.HighestCostDay = &struct {
Date string `json:"date"`
Label string `json:"label"`
Cost float64 `json:"cost"`
Requests int64 `json:"requests"`
}{
Date: highestCostDay.Date,
Label: highestCostDay.Label,
Cost: highestCostDay.ActualCost,
Requests: highestCostDay.Requests,
}
}
// Set highest request day
if highestRequestDay != nil {
summary.HighestRequestDay = &struct {
Date string `json:"date"`
Label string `json:"label"`
Requests int64 `json:"requests"`
Cost float64 `json:"cost"`
}{
Date: highestRequestDay.Date,
Label: highestRequestDay.Label,
Requests: highestRequestDay.Requests,
Cost: highestRequestDay.ActualCost,
}
}
// Get model statistics using the unified method
models, err := r.GetModelStatsWithFilters(ctx, startTime, endTime, 0, 0, accountID)
if err != nil {
models = []ModelStat{}
}
return &AccountUsageStatsResponse{
History: history,
Summary: summary,
Models: models,
}, nil
}
type usageLogModel struct {
ID int64 `gorm:"primaryKey"`
UserID int64 `gorm:"index;not null"`
ApiKeyID int64 `gorm:"index;not null"`
AccountID int64 `gorm:"index;not null"`
RequestID string `gorm:"size:64"`
Model string `gorm:"size:100;index;not null"`
GroupID *int64 `gorm:"index"`
SubscriptionID *int64 `gorm:"index"`
InputTokens int `gorm:"default:0;not null"`
OutputTokens int `gorm:"default:0;not null"`
CacheCreationTokens int `gorm:"default:0;not null"`
CacheReadTokens int `gorm:"default:0;not null"`
CacheCreation5mTokens int `gorm:"default:0;not null"`
CacheCreation1hTokens int `gorm:"default:0;not null"`
InputCost float64 `gorm:"type:decimal(20,10);default:0;not null"`
OutputCost float64 `gorm:"type:decimal(20,10);default:0;not null"`
CacheCreationCost float64 `gorm:"type:decimal(20,10);default:0;not null"`
CacheReadCost float64 `gorm:"type:decimal(20,10);default:0;not null"`
TotalCost float64 `gorm:"type:decimal(20,10);default:0;not null"`
ActualCost float64 `gorm:"type:decimal(20,10);default:0;not null"`
RateMultiplier float64 `gorm:"type:decimal(10,4);default:1;not null"`
BillingType int8 `gorm:"type:smallint;default:0;not null"`
Stream bool `gorm:"default:false;not null"`
DurationMs *int
FirstTokenMs *int
CreatedAt time.Time `gorm:"index;not null"`
User *userModel `gorm:"foreignKey:UserID"`
ApiKey *apiKeyModel `gorm:"foreignKey:ApiKeyID"`
Account *accountModel `gorm:"foreignKey:AccountID"`
Group *groupModel `gorm:"foreignKey:GroupID"`
Subscription *userSubscriptionModel `gorm:"foreignKey:SubscriptionID"`
}
func (usageLogModel) TableName() string { return "usage_logs" }
func usageLogModelToService(m *usageLogModel) *service.UsageLog {
if m == nil {
return nil
}
return &service.UsageLog{
ID: m.ID,
UserID: m.UserID,
ApiKeyID: m.ApiKeyID,
AccountID: m.AccountID,
RequestID: m.RequestID,
Model: m.Model,
GroupID: m.GroupID,
SubscriptionID: m.SubscriptionID,
InputTokens: m.InputTokens,
OutputTokens: m.OutputTokens,
CacheCreationTokens: m.CacheCreationTokens,
CacheReadTokens: m.CacheReadTokens,
CacheCreation5mTokens: m.CacheCreation5mTokens,
CacheCreation1hTokens: m.CacheCreation1hTokens,
InputCost: m.InputCost,
OutputCost: m.OutputCost,
CacheCreationCost: m.CacheCreationCost,
CacheReadCost: m.CacheReadCost,
TotalCost: m.TotalCost,
ActualCost: m.ActualCost,
RateMultiplier: m.RateMultiplier,
BillingType: m.BillingType,
Stream: m.Stream,
DurationMs: m.DurationMs,
FirstTokenMs: m.FirstTokenMs,
CreatedAt: m.CreatedAt,
User: userModelToService(m.User),
ApiKey: apiKeyModelToService(m.ApiKey),
Account: accountModelToService(m.Account),
Group: groupModelToService(m.Group),
Subscription: userSubscriptionModelToService(m.Subscription),
}
}
func usageLogModelsToService(models []usageLogModel) []service.UsageLog {
out := make([]service.UsageLog, 0, len(models))
for i := range models {
if s := usageLogModelToService(&models[i]); s != nil {
out = append(out, *s)
}
}
return out
}
func usageLogModelFromService(log *service.UsageLog) *usageLogModel {
if log == nil {
return nil
}
return &usageLogModel{
ID: log.ID,
UserID: log.UserID,
ApiKeyID: log.ApiKeyID,
AccountID: log.AccountID,
RequestID: log.RequestID,
Model: log.Model,
GroupID: log.GroupID,
SubscriptionID: log.SubscriptionID,
InputTokens: log.InputTokens,
OutputTokens: log.OutputTokens,
CacheCreationTokens: log.CacheCreationTokens,
CacheReadTokens: log.CacheReadTokens,
CacheCreation5mTokens: log.CacheCreation5mTokens,
CacheCreation1hTokens: log.CacheCreation1hTokens,
InputCost: log.InputCost,
OutputCost: log.OutputCost,
CacheCreationCost: log.CacheCreationCost,
CacheReadCost: log.CacheReadCost,
TotalCost: log.TotalCost,
ActualCost: log.ActualCost,
RateMultiplier: log.RateMultiplier,
BillingType: log.BillingType,
Stream: log.Stream,
DurationMs: log.DurationMs,
FirstTokenMs: log.FirstTokenMs,
CreatedAt: log.CreatedAt,
}
}
func applyUsageLogModelToService(log *service.UsageLog, m *usageLogModel) {
if log == nil || m == nil {
return
}
log.ID = m.ID
log.CreatedAt = m.CreatedAt
}