usage_log_repo_stats.go 37 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099
  1. package repository
  2. import (
  3. "context"
  4. "database/sql"
  5. "fmt"
  6. "os"
  7. "sort"
  8. "strings"
  9. "time"
  10. "github.com/Wei-Shaw/sub2api/internal/pkg/logger"
  11. "github.com/Wei-Shaw/sub2api/internal/pkg/timezone"
  12. "github.com/Wei-Shaw/sub2api/internal/pkg/usagestats"
  13. "github.com/Wei-Shaw/sub2api/internal/service"
  14. "github.com/lib/pq"
  15. )
  16. // GetUserStatsAggregated returns aggregated usage statistics for a user using database-level aggregation
  17. func (r *usageLogRepository) GetUserStatsAggregated(ctx context.Context, userID int64, startTime, endTime time.Time) (*usagestats.UsageStats, error) {
  18. query := `
  19. SELECT
  20. COUNT(*) as total_requests,
  21. COALESCE(SUM(input_tokens), 0) as total_input_tokens,
  22. COALESCE(SUM(output_tokens), 0) as total_output_tokens,
  23. COALESCE(SUM(cache_creation_tokens + cache_read_tokens), 0) as total_cache_tokens,
  24. COALESCE(SUM(cache_creation_tokens), 0) as total_cache_creation_tokens,
  25. COALESCE(SUM(cache_read_tokens), 0) as total_cache_read_tokens,
  26. COALESCE(SUM(total_cost), 0) as total_cost,
  27. COALESCE(SUM(actual_cost), 0) as total_actual_cost,
  28. COALESCE(AVG(COALESCE(duration_ms, 0)), 0) as avg_duration_ms
  29. FROM usage_logs
  30. WHERE user_id = $1 AND created_at >= $2 AND created_at < $3
  31. `
  32. var stats usagestats.UsageStats
  33. if err := scanSingleRow(
  34. ctx,
  35. r.sql,
  36. query,
  37. []any{userID, startTime, endTime},
  38. &stats.TotalRequests,
  39. &stats.TotalInputTokens,
  40. &stats.TotalOutputTokens,
  41. &stats.TotalCacheTokens,
  42. &stats.TotalCacheCreationTokens,
  43. &stats.TotalCacheReadTokens,
  44. &stats.TotalCost,
  45. &stats.TotalActualCost,
  46. &stats.AverageDurationMs,
  47. ); err != nil {
  48. return nil, err
  49. }
  50. stats.TotalTokens = stats.TotalInputTokens + stats.TotalOutputTokens + stats.TotalCacheTokens
  51. return &stats, nil
  52. }
  53. // GetAPIKeyStatsAggregated returns aggregated usage statistics for an API key using database-level aggregation
  54. func (r *usageLogRepository) GetAPIKeyStatsAggregated(ctx context.Context, apiKeyID int64, startTime, endTime time.Time) (*usagestats.UsageStats, error) {
  55. query := `
  56. SELECT
  57. COUNT(*) as total_requests,
  58. COALESCE(SUM(input_tokens), 0) as total_input_tokens,
  59. COALESCE(SUM(output_tokens), 0) as total_output_tokens,
  60. COALESCE(SUM(cache_creation_tokens + cache_read_tokens), 0) as total_cache_tokens,
  61. COALESCE(SUM(cache_creation_tokens), 0) as total_cache_creation_tokens,
  62. COALESCE(SUM(cache_read_tokens), 0) as total_cache_read_tokens,
  63. COALESCE(SUM(total_cost), 0) as total_cost,
  64. COALESCE(SUM(actual_cost), 0) as total_actual_cost,
  65. COALESCE(AVG(COALESCE(duration_ms, 0)), 0) as avg_duration_ms
  66. FROM usage_logs
  67. WHERE api_key_id = $1 AND created_at >= $2 AND created_at < $3
  68. `
  69. var stats usagestats.UsageStats
  70. if err := scanSingleRow(
  71. ctx,
  72. r.sql,
  73. query,
  74. []any{apiKeyID, startTime, endTime},
  75. &stats.TotalRequests,
  76. &stats.TotalInputTokens,
  77. &stats.TotalOutputTokens,
  78. &stats.TotalCacheTokens,
  79. &stats.TotalCacheCreationTokens,
  80. &stats.TotalCacheReadTokens,
  81. &stats.TotalCost,
  82. &stats.TotalActualCost,
  83. &stats.AverageDurationMs,
  84. ); err != nil {
  85. return nil, err
  86. }
  87. stats.TotalTokens = stats.TotalInputTokens + stats.TotalOutputTokens + stats.TotalCacheTokens
  88. return &stats, nil
  89. }
  90. // GetAccountStatsAggregated 使用 SQL 聚合统计账号使用数据
  91. //
  92. // 性能优化说明:
  93. // 原实现先查询所有日志记录,再在应用层循环计算统计值:
  94. // 1. 需要传输大量数据到应用层
  95. // 2. 应用层循环计算增加 CPU 和内存开销
  96. //
  97. // 新实现使用 SQL 聚合函数:
  98. // 1. 在数据库层完成 COUNT/SUM/AVG 计算
  99. // 2. 只返回单行聚合结果,大幅减少数据传输量
  100. // 3. 利用数据库索引优化聚合查询性能
  101. func (r *usageLogRepository) GetAccountStatsAggregated(ctx context.Context, accountID int64, startTime, endTime time.Time) (*usagestats.UsageStats, error) {
  102. query := `
  103. SELECT
  104. COUNT(*) as total_requests,
  105. COALESCE(SUM(input_tokens), 0) as total_input_tokens,
  106. COALESCE(SUM(output_tokens), 0) as total_output_tokens,
  107. COALESCE(SUM(cache_creation_tokens + cache_read_tokens), 0) as total_cache_tokens,
  108. COALESCE(SUM(cache_creation_tokens), 0) as total_cache_creation_tokens,
  109. COALESCE(SUM(cache_read_tokens), 0) as total_cache_read_tokens,
  110. COALESCE(SUM(total_cost), 0) as total_cost,
  111. COALESCE(SUM(actual_cost), 0) as total_actual_cost,
  112. COALESCE(AVG(COALESCE(duration_ms, 0)), 0) as avg_duration_ms
  113. FROM usage_logs
  114. WHERE account_id = $1 AND created_at >= $2 AND created_at < $3
  115. `
  116. var stats usagestats.UsageStats
  117. if err := scanSingleRow(
  118. ctx,
  119. r.sql,
  120. query,
  121. []any{accountID, startTime, endTime},
  122. &stats.TotalRequests,
  123. &stats.TotalInputTokens,
  124. &stats.TotalOutputTokens,
  125. &stats.TotalCacheTokens,
  126. &stats.TotalCacheCreationTokens,
  127. &stats.TotalCacheReadTokens,
  128. &stats.TotalCost,
  129. &stats.TotalActualCost,
  130. &stats.AverageDurationMs,
  131. ); err != nil {
  132. return nil, err
  133. }
  134. stats.TotalTokens = stats.TotalInputTokens + stats.TotalOutputTokens + stats.TotalCacheTokens
  135. return &stats, nil
  136. }
  137. // GetModelStatsAggregated 使用 SQL 聚合统计模型使用数据
  138. // 性能优化:数据库层聚合计算,避免应用层循环统计
  139. func (r *usageLogRepository) GetModelStatsAggregated(ctx context.Context, modelName string, startTime, endTime time.Time) (*usagestats.UsageStats, error) {
  140. query := fmt.Sprintf(`
  141. SELECT
  142. COUNT(*) as total_requests,
  143. COALESCE(SUM(input_tokens), 0) as total_input_tokens,
  144. COALESCE(SUM(output_tokens), 0) as total_output_tokens,
  145. COALESCE(SUM(cache_creation_tokens + cache_read_tokens), 0) as total_cache_tokens,
  146. COALESCE(SUM(cache_creation_tokens), 0) as total_cache_creation_tokens,
  147. COALESCE(SUM(cache_read_tokens), 0) as total_cache_read_tokens,
  148. COALESCE(SUM(total_cost), 0) as total_cost,
  149. COALESCE(SUM(actual_cost), 0) as total_actual_cost,
  150. COALESCE(AVG(COALESCE(duration_ms, 0)), 0) as avg_duration_ms
  151. FROM usage_logs
  152. WHERE %s = $1 AND created_at >= $2 AND created_at < $3
  153. `, rawUsageLogModelColumn)
  154. var stats usagestats.UsageStats
  155. if err := scanSingleRow(
  156. ctx,
  157. r.sql,
  158. query,
  159. []any{modelName, startTime, endTime},
  160. &stats.TotalRequests,
  161. &stats.TotalInputTokens,
  162. &stats.TotalOutputTokens,
  163. &stats.TotalCacheTokens,
  164. &stats.TotalCacheCreationTokens,
  165. &stats.TotalCacheReadTokens,
  166. &stats.TotalCost,
  167. &stats.TotalActualCost,
  168. &stats.AverageDurationMs,
  169. ); err != nil {
  170. return nil, err
  171. }
  172. stats.TotalTokens = stats.TotalInputTokens + stats.TotalOutputTokens + stats.TotalCacheTokens
  173. return &stats, nil
  174. }
  175. // GetDailyStatsAggregated 使用 SQL 聚合统计用户的每日使用数据
  176. // 性能优化:使用 GROUP BY 在数据库层按日期分组聚合,避免应用层循环分组统计
  177. func (r *usageLogRepository) GetDailyStatsAggregated(ctx context.Context, userID int64, startTime, endTime time.Time) (result []map[string]any, err error) {
  178. tzName := resolveUsageStatsTimezone()
  179. query := `
  180. SELECT
  181. -- 使用应用时区分组,避免数据库会话时区导致日边界偏移。
  182. TO_CHAR(created_at AT TIME ZONE $4, 'YYYY-MM-DD') as date,
  183. COUNT(*) as total_requests,
  184. COALESCE(SUM(input_tokens), 0) as total_input_tokens,
  185. COALESCE(SUM(output_tokens), 0) as total_output_tokens,
  186. COALESCE(SUM(cache_creation_tokens + cache_read_tokens), 0) as total_cache_tokens,
  187. COALESCE(SUM(total_cost), 0) as total_cost,
  188. COALESCE(SUM(actual_cost), 0) as total_actual_cost,
  189. COALESCE(AVG(COALESCE(duration_ms, 0)), 0) as avg_duration_ms
  190. FROM usage_logs
  191. WHERE user_id = $1 AND created_at >= $2 AND created_at < $3
  192. GROUP BY 1
  193. ORDER BY 1
  194. `
  195. rows, err := r.sql.QueryContext(ctx, query, userID, startTime, endTime, tzName)
  196. if err != nil {
  197. return nil, err
  198. }
  199. defer func() {
  200. if closeErr := rows.Close(); closeErr != nil && err == nil {
  201. err = closeErr
  202. result = nil
  203. }
  204. }()
  205. result = make([]map[string]any, 0)
  206. for rows.Next() {
  207. var (
  208. date string
  209. totalRequests int64
  210. totalInputTokens int64
  211. totalOutputTokens int64
  212. totalCacheTokens int64
  213. totalCost float64
  214. totalActualCost float64
  215. avgDurationMs float64
  216. )
  217. if err = rows.Scan(
  218. &date,
  219. &totalRequests,
  220. &totalInputTokens,
  221. &totalOutputTokens,
  222. &totalCacheTokens,
  223. &totalCost,
  224. &totalActualCost,
  225. &avgDurationMs,
  226. ); err != nil {
  227. return nil, err
  228. }
  229. result = append(result, map[string]any{
  230. "date": date,
  231. "total_requests": totalRequests,
  232. "total_input_tokens": totalInputTokens,
  233. "total_output_tokens": totalOutputTokens,
  234. "total_cache_tokens": totalCacheTokens,
  235. "total_tokens": totalInputTokens + totalOutputTokens + totalCacheTokens,
  236. "total_cost": totalCost,
  237. "total_actual_cost": totalActualCost,
  238. "average_duration_ms": avgDurationMs,
  239. })
  240. }
  241. if err = rows.Err(); err != nil {
  242. return nil, err
  243. }
  244. return result, nil
  245. }
  246. // resolveUsageStatsTimezone 获取用于 SQL 分组的时区名称。
  247. // 优先使用应用初始化的时区,其次尝试读取 TZ 环境变量,最后回落为 UTC。
  248. func resolveUsageStatsTimezone() string {
  249. tzName := timezone.Name()
  250. if tzName != "" && tzName != "Local" {
  251. return tzName
  252. }
  253. if envTZ := strings.TrimSpace(os.Getenv("TZ")); envTZ != "" {
  254. return envTZ
  255. }
  256. return "UTC"
  257. }
  258. // GetAccountTodayStats 获取账号今日统计
  259. func (r *usageLogRepository) GetAccountTodayStats(ctx context.Context, accountID int64) (*usagestats.AccountStats, error) {
  260. today := timezone.Today()
  261. query := `
  262. SELECT
  263. COUNT(*) as requests,
  264. COALESCE(SUM(input_tokens + output_tokens + cache_creation_tokens + cache_read_tokens), 0) as tokens,
  265. COALESCE(SUM(COALESCE(account_stats_cost, total_cost) * COALESCE(account_rate_multiplier, 1)), 0) as cost,
  266. COALESCE(SUM(total_cost), 0) as standard_cost,
  267. COALESCE(SUM(actual_cost), 0) as user_cost
  268. FROM usage_logs
  269. WHERE account_id = $1 AND created_at >= $2
  270. `
  271. stats := &usagestats.AccountStats{}
  272. if err := scanSingleRow(
  273. ctx,
  274. r.sql,
  275. query,
  276. []any{accountID, today},
  277. &stats.Requests,
  278. &stats.Tokens,
  279. &stats.Cost,
  280. &stats.StandardCost,
  281. &stats.UserCost,
  282. ); err != nil {
  283. return nil, err
  284. }
  285. return stats, nil
  286. }
  287. // GetAccountWindowStats 获取账号时间窗口内的统计
  288. func (r *usageLogRepository) GetAccountWindowStats(ctx context.Context, accountID int64, startTime time.Time) (*usagestats.AccountStats, error) {
  289. query := `
  290. SELECT
  291. COUNT(*) as requests,
  292. COALESCE(SUM(input_tokens + output_tokens + cache_creation_tokens + cache_read_tokens), 0) as tokens,
  293. COALESCE(SUM(COALESCE(account_stats_cost, total_cost) * COALESCE(account_rate_multiplier, 1)), 0) as cost,
  294. COALESCE(SUM(total_cost), 0) as standard_cost,
  295. COALESCE(SUM(actual_cost), 0) as user_cost
  296. FROM usage_logs
  297. WHERE account_id = $1 AND created_at >= $2
  298. `
  299. stats := &usagestats.AccountStats{}
  300. if err := scanSingleRow(
  301. ctx,
  302. r.sql,
  303. query,
  304. []any{accountID, startTime},
  305. &stats.Requests,
  306. &stats.Tokens,
  307. &stats.Cost,
  308. &stats.StandardCost,
  309. &stats.UserCost,
  310. ); err != nil {
  311. return nil, err
  312. }
  313. return stats, nil
  314. }
  315. // GetAccountWindowStatsBatch 批量获取同一窗口起点下多个账号的统计数据。
  316. // 返回 map[accountID]*AccountStats,未命中的账号会返回零值统计,便于上层直接复用。
  317. func (r *usageLogRepository) GetAccountWindowStatsBatch(ctx context.Context, accountIDs []int64, startTime time.Time) (map[int64]*usagestats.AccountStats, error) {
  318. result := make(map[int64]*usagestats.AccountStats, len(accountIDs))
  319. if len(accountIDs) == 0 {
  320. return result, nil
  321. }
  322. query := `
  323. SELECT
  324. account_id,
  325. COUNT(*) as requests,
  326. COALESCE(SUM(input_tokens + output_tokens + cache_creation_tokens + cache_read_tokens), 0) as tokens,
  327. COALESCE(SUM(COALESCE(account_stats_cost, total_cost) * COALESCE(account_rate_multiplier, 1)), 0) as cost,
  328. COALESCE(SUM(total_cost), 0) as standard_cost,
  329. COALESCE(SUM(actual_cost), 0) as user_cost
  330. FROM usage_logs
  331. WHERE account_id = ANY($1) AND created_at >= $2
  332. GROUP BY account_id
  333. `
  334. rows, err := r.sql.QueryContext(ctx, query, pq.Array(accountIDs), startTime)
  335. if err != nil {
  336. return nil, err
  337. }
  338. defer func() { _ = rows.Close() }()
  339. for rows.Next() {
  340. var accountID int64
  341. stats := &usagestats.AccountStats{}
  342. if err := rows.Scan(
  343. &accountID,
  344. &stats.Requests,
  345. &stats.Tokens,
  346. &stats.Cost,
  347. &stats.StandardCost,
  348. &stats.UserCost,
  349. ); err != nil {
  350. return nil, err
  351. }
  352. result[accountID] = stats
  353. }
  354. if err := rows.Err(); err != nil {
  355. return nil, err
  356. }
  357. for _, accountID := range accountIDs {
  358. if _, ok := result[accountID]; !ok {
  359. result[accountID] = &usagestats.AccountStats{}
  360. }
  361. }
  362. return result, nil
  363. }
  364. // GetGeminiUsageTotalsBatch 批量聚合 Gemini 账号在窗口内的 Pro/Flash 请求与用量。
  365. // 模型分类规则与 service.geminiModelClassFromName 一致:model 包含 flash/lite 视为 flash,其余视为 pro。
  366. func (r *usageLogRepository) GetGeminiUsageTotalsBatch(ctx context.Context, accountIDs []int64, startTime, endTime time.Time) (map[int64]service.GeminiUsageTotals, error) {
  367. result := make(map[int64]service.GeminiUsageTotals, len(accountIDs))
  368. if len(accountIDs) == 0 {
  369. return result, nil
  370. }
  371. query := `
  372. SELECT
  373. account_id,
  374. COALESCE(SUM(CASE WHEN LOWER(COALESCE(model, '')) LIKE '%flash%' OR LOWER(COALESCE(model, '')) LIKE '%lite%' THEN 1 ELSE 0 END), 0) AS flash_requests,
  375. COALESCE(SUM(CASE WHEN LOWER(COALESCE(model, '')) LIKE '%flash%' OR LOWER(COALESCE(model, '')) LIKE '%lite%' THEN 0 ELSE 1 END), 0) AS pro_requests,
  376. COALESCE(SUM(CASE WHEN LOWER(COALESCE(model, '')) LIKE '%flash%' OR LOWER(COALESCE(model, '')) LIKE '%lite%' THEN (input_tokens + output_tokens + cache_creation_tokens + cache_read_tokens) ELSE 0 END), 0) AS flash_tokens,
  377. COALESCE(SUM(CASE WHEN LOWER(COALESCE(model, '')) LIKE '%flash%' OR LOWER(COALESCE(model, '')) LIKE '%lite%' THEN 0 ELSE (input_tokens + output_tokens + cache_creation_tokens + cache_read_tokens) END), 0) AS pro_tokens,
  378. COALESCE(SUM(CASE WHEN LOWER(COALESCE(model, '')) LIKE '%flash%' OR LOWER(COALESCE(model, '')) LIKE '%lite%' THEN actual_cost ELSE 0 END), 0) AS flash_cost,
  379. COALESCE(SUM(CASE WHEN LOWER(COALESCE(model, '')) LIKE '%flash%' OR LOWER(COALESCE(model, '')) LIKE '%lite%' THEN 0 ELSE actual_cost END), 0) AS pro_cost
  380. FROM usage_logs
  381. WHERE account_id = ANY($1) AND created_at >= $2 AND created_at < $3
  382. GROUP BY account_id
  383. `
  384. rows, err := r.sql.QueryContext(ctx, query, pq.Array(accountIDs), startTime, endTime)
  385. if err != nil {
  386. return nil, err
  387. }
  388. defer func() { _ = rows.Close() }()
  389. for rows.Next() {
  390. var accountID int64
  391. var totals service.GeminiUsageTotals
  392. if err := rows.Scan(
  393. &accountID,
  394. &totals.FlashRequests,
  395. &totals.ProRequests,
  396. &totals.FlashTokens,
  397. &totals.ProTokens,
  398. &totals.FlashCost,
  399. &totals.ProCost,
  400. ); err != nil {
  401. return nil, err
  402. }
  403. result[accountID] = totals
  404. }
  405. if err := rows.Err(); err != nil {
  406. return nil, err
  407. }
  408. for _, accountID := range accountIDs {
  409. if _, ok := result[accountID]; !ok {
  410. result[accountID] = service.GeminiUsageTotals{}
  411. }
  412. }
  413. return result, nil
  414. }
  415. // UsageStats represents usage statistics
  416. type UsageStats = usagestats.UsageStats
  417. // BatchUserUsageStats represents usage stats for a single user
  418. type BatchUserUsageStats = usagestats.BatchUserUsageStats
  419. // PlatformUsage represents per-platform usage breakdown
  420. type PlatformUsage = usagestats.PlatformUsage
  421. func normalizePositiveInt64IDs(ids []int64) []int64 {
  422. if len(ids) == 0 {
  423. return nil
  424. }
  425. seen := make(map[int64]struct{}, len(ids))
  426. out := make([]int64, 0, len(ids))
  427. for _, id := range ids {
  428. if id <= 0 {
  429. continue
  430. }
  431. if _, ok := seen[id]; ok {
  432. continue
  433. }
  434. seen[id] = struct{}{}
  435. out = append(out, id)
  436. }
  437. return out
  438. }
  439. // GetBatchUserUsageStats gets today and total actual_cost for multiple users within a time range.
  440. // If startTime is zero, defaults to 30 days ago.
  441. func (r *usageLogRepository) GetBatchUserUsageStats(ctx context.Context, userIDs []int64, startTime, endTime time.Time) (map[int64]*BatchUserUsageStats, error) {
  442. result := make(map[int64]*BatchUserUsageStats)
  443. normalizedUserIDs := normalizePositiveInt64IDs(userIDs)
  444. if len(normalizedUserIDs) == 0 {
  445. return result, nil
  446. }
  447. // 默认最近 30 天
  448. if startTime.IsZero() {
  449. startTime = time.Now().AddDate(0, 0, -30)
  450. }
  451. if endTime.IsZero() {
  452. endTime = time.Now()
  453. }
  454. for _, id := range normalizedUserIDs {
  455. result[id] = &BatchUserUsageStats{UserID: id}
  456. }
  457. // GROUP BY (user_id, effective_platform) 一次查询同时得到总值与按平台拆分。
  458. // 应用层把同一 user_id 的多行累加为总值,并把非空 platform 行收集到 ByPlatform。
  459. query := `
  460. SELECT
  461. ul.user_id,
  462. ` + usageLogEffectivePlatformExpr + ` as platform,
  463. COALESCE(SUM(ul.actual_cost) FILTER (WHERE ul.created_at >= $2 AND ul.created_at < $3), 0) as total_cost,
  464. COALESCE(SUM(ul.actual_cost) FILTER (WHERE ul.created_at >= $4), 0) as today_cost
  465. FROM usage_logs ul
  466. LEFT JOIN groups g ON g.id = ul.group_id
  467. LEFT JOIN accounts a ON a.id = ul.account_id
  468. WHERE ul.user_id = ANY($1)
  469. AND ul.created_at >= LEAST($2, $4)
  470. AND ` + usageLogSuccessFilterUL + `
  471. GROUP BY ul.user_id, ` + usageLogEffectivePlatformExpr + `
  472. `
  473. today := timezone.Today()
  474. rows, err := r.sql.QueryContext(ctx, query, pq.Array(normalizedUserIDs), startTime, endTime, today)
  475. if err != nil {
  476. return nil, err
  477. }
  478. for rows.Next() {
  479. var userID int64
  480. var platform sql.NullString
  481. var total float64
  482. var todayTotal float64
  483. if err := rows.Scan(&userID, &platform, &total, &todayTotal); err != nil {
  484. _ = rows.Close()
  485. return nil, err
  486. }
  487. stats, ok := result[userID]
  488. if !ok {
  489. continue
  490. }
  491. stats.TotalActualCost += total
  492. stats.TodayActualCost += todayTotal
  493. if platform.Valid && platform.String != "" {
  494. stats.ByPlatform = append(stats.ByPlatform, PlatformUsage{
  495. Platform: platform.String,
  496. TotalActualCost: total,
  497. TodayActualCost: todayTotal,
  498. })
  499. }
  500. }
  501. if err := rows.Close(); err != nil {
  502. return nil, err
  503. }
  504. if err := rows.Err(); err != nil {
  505. return nil, err
  506. }
  507. return result, nil
  508. }
  509. // BatchAPIKeyUsageStats represents usage stats for a single API key
  510. type BatchAPIKeyUsageStats = usagestats.BatchAPIKeyUsageStats
  511. // GetBatchAPIKeyUsageStats gets today and total actual_cost for multiple API keys within a time range.
  512. // If startTime is zero, defaults to 30 days ago.
  513. func (r *usageLogRepository) GetBatchAPIKeyUsageStats(ctx context.Context, apiKeyIDs []int64, startTime, endTime time.Time) (map[int64]*BatchAPIKeyUsageStats, error) {
  514. result := make(map[int64]*BatchAPIKeyUsageStats)
  515. normalizedAPIKeyIDs := normalizePositiveInt64IDs(apiKeyIDs)
  516. if len(normalizedAPIKeyIDs) == 0 {
  517. return result, nil
  518. }
  519. // 默认最近 30 天
  520. if startTime.IsZero() {
  521. startTime = time.Now().AddDate(0, 0, -30)
  522. }
  523. if endTime.IsZero() {
  524. endTime = time.Now()
  525. }
  526. for _, id := range normalizedAPIKeyIDs {
  527. result[id] = &BatchAPIKeyUsageStats{APIKeyID: id}
  528. }
  529. query := `
  530. SELECT
  531. api_key_id,
  532. COALESCE(SUM(actual_cost) FILTER (WHERE created_at >= $2 AND created_at < $3), 0) as total_cost,
  533. COALESCE(SUM(actual_cost) FILTER (WHERE created_at >= $4), 0) as today_cost
  534. FROM usage_logs
  535. WHERE api_key_id = ANY($1)
  536. AND created_at >= LEAST($2, $4)
  537. GROUP BY api_key_id
  538. `
  539. today := timezone.Today()
  540. rows, err := r.sql.QueryContext(ctx, query, pq.Array(normalizedAPIKeyIDs), startTime, endTime, today)
  541. if err != nil {
  542. return nil, err
  543. }
  544. for rows.Next() {
  545. var apiKeyID int64
  546. var total float64
  547. var todayTotal float64
  548. if err := rows.Scan(&apiKeyID, &total, &todayTotal); err != nil {
  549. _ = rows.Close()
  550. return nil, err
  551. }
  552. if stats, ok := result[apiKeyID]; ok {
  553. stats.TotalActualCost = total
  554. stats.TodayActualCost = todayTotal
  555. }
  556. }
  557. if err := rows.Close(); err != nil {
  558. return nil, err
  559. }
  560. if err := rows.Err(); err != nil {
  561. return nil, err
  562. }
  563. return result, nil
  564. }
  565. // resolveEndpointColumn maps endpoint type to the corresponding DB column name.
  566. func resolveEndpointColumn(endpointType string) string {
  567. switch endpointType {
  568. case "upstream":
  569. return "ul.upstream_endpoint"
  570. case "path":
  571. return "ul.inbound_endpoint || ' -> ' || ul.upstream_endpoint"
  572. default:
  573. return "ul.inbound_endpoint"
  574. }
  575. }
  576. // GetGlobalStats gets usage statistics for all users within a time range
  577. func (r *usageLogRepository) GetGlobalStats(ctx context.Context, startTime, endTime time.Time) (*UsageStats, error) {
  578. query := `
  579. SELECT
  580. COUNT(*) as total_requests,
  581. COALESCE(SUM(input_tokens), 0) as total_input_tokens,
  582. COALESCE(SUM(output_tokens), 0) as total_output_tokens,
  583. COALESCE(SUM(cache_creation_tokens + cache_read_tokens), 0) as total_cache_tokens,
  584. COALESCE(SUM(total_cost), 0) as total_cost,
  585. COALESCE(SUM(actual_cost), 0) as total_actual_cost,
  586. COALESCE(AVG(duration_ms), 0) as avg_duration_ms
  587. FROM usage_logs
  588. WHERE created_at >= $1 AND created_at < $2
  589. `
  590. stats := &UsageStats{}
  591. if err := scanSingleRow(
  592. ctx,
  593. r.sql,
  594. query,
  595. []any{startTime, endTime},
  596. &stats.TotalRequests,
  597. &stats.TotalInputTokens,
  598. &stats.TotalOutputTokens,
  599. &stats.TotalCacheTokens,
  600. &stats.TotalCost,
  601. &stats.TotalActualCost,
  602. &stats.AverageDurationMs,
  603. ); err != nil {
  604. return nil, err
  605. }
  606. stats.TotalTokens = stats.TotalInputTokens + stats.TotalOutputTokens + stats.TotalCacheTokens
  607. return stats, nil
  608. }
  609. // GetStatsWithFilters gets usage statistics with optional filters
  610. func (r *usageLogRepository) GetStatsWithFilters(ctx context.Context, filters UsageLogFilters) (*UsageStats, error) {
  611. conditions := make([]string, 0, 9)
  612. args := make([]any, 0, 9)
  613. if filters.UserID > 0 {
  614. conditions = append(conditions, fmt.Sprintf("user_id = $%d", len(args)+1))
  615. args = append(args, filters.UserID)
  616. }
  617. if filters.APIKeyID > 0 {
  618. conditions = append(conditions, fmt.Sprintf("api_key_id = $%d", len(args)+1))
  619. args = append(args, filters.APIKeyID)
  620. }
  621. if filters.AccountID > 0 {
  622. conditions = append(conditions, fmt.Sprintf("account_id = $%d", len(args)+1))
  623. args = append(args, filters.AccountID)
  624. }
  625. if filters.GroupID > 0 {
  626. conditions = append(conditions, fmt.Sprintf("group_id = $%d", len(args)+1))
  627. args = append(args, filters.GroupID)
  628. }
  629. conditions, args = appendUsageLogModelWhereCondition(conditions, args, filters.Model, filters.ModelFilterSource)
  630. conditions, args = appendRequestTypeOrStreamWhereCondition(conditions, args, filters.RequestType, filters.Stream)
  631. conditions, args = appendNativeCompactionV2WhereCondition(conditions, args, filters.NativeCompactionV2, "")
  632. if filters.BillingType != nil {
  633. conditions = append(conditions, fmt.Sprintf("billing_type = $%d", len(args)+1))
  634. args = append(args, int16(*filters.BillingType))
  635. }
  636. conditions, args = appendUsageLogBillingModeWhereCondition(conditions, args, filters.BillingMode)
  637. if filters.UpstreamModelMismatch != nil {
  638. conditions = append(conditions, upstreamModelMismatchCondition("upstream_model_mismatch", *filters.UpstreamModelMismatch))
  639. }
  640. if filters.StartTime != nil {
  641. conditions = append(conditions, fmt.Sprintf("created_at >= $%d", len(args)+1))
  642. args = append(args, *filters.StartTime)
  643. }
  644. if filters.EndTime != nil {
  645. conditions = append(conditions, fmt.Sprintf("created_at < $%d", len(args)+1))
  646. args = append(args, *filters.EndTime)
  647. }
  648. query := fmt.Sprintf(`
  649. WITH scoped AS (
  650. SELECT
  651. COALESCE(NULLIF(TRIM(inbound_endpoint), ''), 'unknown') AS inbound_endpoint,
  652. COALESCE(NULLIF(TRIM(upstream_endpoint), ''), 'unknown') AS upstream_endpoint,
  653. input_tokens,
  654. output_tokens,
  655. cache_creation_tokens,
  656. cache_read_tokens,
  657. total_cost,
  658. actual_cost,
  659. COALESCE(account_stats_cost, total_cost) * COALESCE(account_rate_multiplier, 1) AS account_cost,
  660. duration_ms
  661. FROM usage_logs
  662. %s
  663. )
  664. SELECT
  665. GROUPING(inbound_endpoint) AS inbound_grouped,
  666. GROUPING(upstream_endpoint) AS upstream_grouped,
  667. inbound_endpoint,
  668. upstream_endpoint,
  669. COUNT(*) AS requests,
  670. COALESCE(SUM(input_tokens), 0) AS input_tokens,
  671. COALESCE(SUM(output_tokens), 0) AS output_tokens,
  672. COALESCE(SUM(cache_creation_tokens), 0) AS cache_creation_tokens,
  673. COALESCE(SUM(cache_read_tokens), 0) AS cache_read_tokens,
  674. COALESCE(SUM(total_cost), 0) AS cost,
  675. COALESCE(SUM(actual_cost), 0) AS actual_cost,
  676. COALESCE(SUM(account_cost), 0) AS account_cost,
  677. COALESCE(AVG(duration_ms), 0) AS avg_duration_ms
  678. FROM scoped
  679. GROUP BY GROUPING SETS (
  680. (),
  681. (inbound_endpoint),
  682. (upstream_endpoint),
  683. (inbound_endpoint, upstream_endpoint)
  684. )
  685. `, buildWhere(conditions))
  686. stats := &UsageStats{}
  687. var totalAccountCost float64
  688. useAccountCostForEndpoint := filters.AccountID > 0 && filters.UserID == 0 && filters.APIKeyID == 0
  689. rows, err := r.sql.QueryContext(ctx, query, args...)
  690. if err != nil {
  691. return nil, err
  692. }
  693. defer func() { _ = rows.Close() }()
  694. for rows.Next() {
  695. var (
  696. inboundGrouped, upstreamGrouped int
  697. inboundEndpoint, upstreamEndpoint sql.NullString
  698. requests, inputTokens, outputTokens, cacheCreationTokens, cacheReads int64
  699. cost, actualCost, accountCost, averageDurationMs float64
  700. )
  701. if err := rows.Scan(
  702. &inboundGrouped,
  703. &upstreamGrouped,
  704. &inboundEndpoint,
  705. &upstreamEndpoint,
  706. &requests,
  707. &inputTokens,
  708. &outputTokens,
  709. &cacheCreationTokens,
  710. &cacheReads,
  711. &cost,
  712. &actualCost,
  713. &accountCost,
  714. &averageDurationMs,
  715. ); err != nil {
  716. return nil, err
  717. }
  718. totalTokens := inputTokens + outputTokens + cacheCreationTokens + cacheReads
  719. endpointActualCost := actualCost
  720. if useAccountCostForEndpoint {
  721. endpointActualCost = accountCost
  722. }
  723. switch {
  724. case inboundGrouped == 1 && upstreamGrouped == 1:
  725. stats.TotalRequests = requests
  726. stats.TotalInputTokens = inputTokens
  727. stats.TotalOutputTokens = outputTokens
  728. stats.TotalCacheCreationTokens = cacheCreationTokens
  729. stats.TotalCacheReadTokens = cacheReads
  730. stats.TotalCacheTokens = cacheCreationTokens + cacheReads
  731. stats.TotalCost = cost
  732. stats.TotalActualCost = actualCost
  733. totalAccountCost = accountCost
  734. stats.AverageDurationMs = averageDurationMs
  735. case inboundGrouped == 0 && upstreamGrouped == 1:
  736. stats.Endpoints = append(stats.Endpoints, EndpointStat{
  737. Endpoint: inboundEndpoint.String, Requests: requests, TotalTokens: totalTokens,
  738. Cost: cost, ActualCost: endpointActualCost,
  739. })
  740. case inboundGrouped == 1 && upstreamGrouped == 0:
  741. stats.UpstreamEndpoints = append(stats.UpstreamEndpoints, EndpointStat{
  742. Endpoint: upstreamEndpoint.String, Requests: requests, TotalTokens: totalTokens,
  743. Cost: cost, ActualCost: endpointActualCost,
  744. })
  745. case inboundGrouped == 0 && upstreamGrouped == 0:
  746. stats.EndpointPaths = append(stats.EndpointPaths, EndpointStat{
  747. Endpoint: inboundEndpoint.String + " -> " + upstreamEndpoint.String,
  748. Requests: requests, TotalTokens: totalTokens, Cost: cost, ActualCost: endpointActualCost,
  749. })
  750. }
  751. }
  752. if err := rows.Err(); err != nil {
  753. return nil, err
  754. }
  755. sortEndpointStats := func(values []EndpointStat) {
  756. sort.Slice(values, func(i, j int) bool {
  757. if values[i].Requests != values[j].Requests {
  758. return values[i].Requests > values[j].Requests
  759. }
  760. return values[i].Endpoint < values[j].Endpoint
  761. })
  762. }
  763. sortEndpointStats(stats.Endpoints)
  764. sortEndpointStats(stats.UpstreamEndpoints)
  765. sortEndpointStats(stats.EndpointPaths)
  766. stats.TotalAccountCost = &totalAccountCost
  767. stats.TotalTokens = stats.TotalInputTokens + stats.TotalOutputTokens + stats.TotalCacheTokens
  768. return stats, nil
  769. }
  770. // AccountUsageHistory represents daily usage history for an account
  771. type AccountUsageHistory = usagestats.AccountUsageHistory
  772. // AccountUsageSummary represents summary statistics for an account
  773. type AccountUsageSummary = usagestats.AccountUsageSummary
  774. // AccountUsageStatsResponse represents the full usage statistics response for an account
  775. type AccountUsageStatsResponse = usagestats.AccountUsageStatsResponse
  776. // EndpointStat represents endpoint usage statistics row.
  777. type EndpointStat = usagestats.EndpointStat
  778. func (r *usageLogRepository) getEndpointStatsByColumnWithFilters(ctx context.Context, endpointColumn string, startTime, endTime time.Time, userID, apiKeyID, accountID, groupID int64, model string, modelSource string, requestType *int16, stream *bool, billingType *int8, billingMode string) (results []EndpointStat, err error) {
  779. actualCostExpr := "COALESCE(SUM(actual_cost), 0) as actual_cost"
  780. if accountID > 0 && userID == 0 && apiKeyID == 0 {
  781. actualCostExpr = "COALESCE(SUM(COALESCE(account_stats_cost, total_cost) * COALESCE(account_rate_multiplier, 1)), 0) as actual_cost"
  782. }
  783. query := fmt.Sprintf(`
  784. SELECT
  785. COALESCE(NULLIF(TRIM(%s), ''), 'unknown') AS endpoint,
  786. COUNT(*) AS requests,
  787. COALESCE(SUM(input_tokens + output_tokens + cache_creation_tokens + cache_read_tokens), 0) AS total_tokens,
  788. COALESCE(SUM(total_cost), 0) as cost,
  789. %s
  790. FROM usage_logs
  791. WHERE created_at >= $1 AND created_at < $2
  792. `, endpointColumn, actualCostExpr)
  793. args := []any{startTime, endTime}
  794. if userID > 0 {
  795. query += fmt.Sprintf(" AND user_id = $%d", len(args)+1)
  796. args = append(args, userID)
  797. }
  798. if apiKeyID > 0 {
  799. query += fmt.Sprintf(" AND api_key_id = $%d", len(args)+1)
  800. args = append(args, apiKeyID)
  801. }
  802. if accountID > 0 {
  803. query += fmt.Sprintf(" AND account_id = $%d", len(args)+1)
  804. args = append(args, accountID)
  805. }
  806. if groupID > 0 {
  807. query += fmt.Sprintf(" AND group_id = $%d", len(args)+1)
  808. args = append(args, groupID)
  809. }
  810. query, args = appendUsageLogModelQueryFilter(query, args, model, modelSource)
  811. query, args = appendRequestTypeOrStreamQueryFilter(query, args, requestType, stream)
  812. if billingType != nil {
  813. query += fmt.Sprintf(" AND billing_type = $%d", len(args)+1)
  814. args = append(args, int16(*billingType))
  815. }
  816. query, args = appendUsageLogBillingModeQueryFilter(query, args, billingMode, "")
  817. query += " GROUP BY endpoint ORDER BY requests DESC"
  818. rows, err := r.sql.QueryContext(ctx, query, args...)
  819. if err != nil {
  820. return nil, err
  821. }
  822. defer func() {
  823. if closeErr := rows.Close(); closeErr != nil && err == nil {
  824. err = closeErr
  825. results = nil
  826. }
  827. }()
  828. results = make([]EndpointStat, 0)
  829. for rows.Next() {
  830. var row EndpointStat
  831. if err := rows.Scan(&row.Endpoint, &row.Requests, &row.TotalTokens, &row.Cost, &row.ActualCost); err != nil {
  832. return nil, err
  833. }
  834. results = append(results, row)
  835. }
  836. if err := rows.Err(); err != nil {
  837. return nil, err
  838. }
  839. return results, nil
  840. }
  841. // GetEndpointStatsWithFilters returns inbound endpoint statistics with optional filters.
  842. func (r *usageLogRepository) GetEndpointStatsWithFilters(ctx context.Context, startTime, endTime time.Time, userID, apiKeyID, accountID, groupID int64, model string, requestType *int16, stream *bool, billingType *int8) ([]EndpointStat, error) {
  843. return r.getEndpointStatsByColumnWithFilters(ctx, "inbound_endpoint", startTime, endTime, userID, apiKeyID, accountID, groupID, model, "", requestType, stream, billingType, "")
  844. }
  845. // GetUpstreamEndpointStatsWithFilters returns upstream endpoint statistics with optional filters.
  846. func (r *usageLogRepository) GetUpstreamEndpointStatsWithFilters(ctx context.Context, startTime, endTime time.Time, userID, apiKeyID, accountID, groupID int64, model string, requestType *int16, stream *bool, billingType *int8) ([]EndpointStat, error) {
  847. return r.getEndpointStatsByColumnWithFilters(ctx, "upstream_endpoint", startTime, endTime, userID, apiKeyID, accountID, groupID, model, "", requestType, stream, billingType, "")
  848. }
  849. // GetAccountUsageStats returns comprehensive usage statistics for an account over a time range
  850. func (r *usageLogRepository) GetAccountUsageStats(ctx context.Context, accountID int64, startTime, endTime time.Time) (resp *AccountUsageStatsResponse, err error) {
  851. daysCount := int(endTime.Sub(startTime).Hours()/24) + 1
  852. if daysCount <= 0 {
  853. daysCount = 30
  854. }
  855. query := `
  856. SELECT
  857. TO_CHAR(created_at, 'YYYY-MM-DD') as date,
  858. COUNT(*) as requests,
  859. COALESCE(SUM(input_tokens + output_tokens + cache_creation_tokens + cache_read_tokens), 0) as tokens,
  860. COALESCE(SUM(total_cost), 0) as cost,
  861. COALESCE(SUM(COALESCE(account_stats_cost, total_cost) * COALESCE(account_rate_multiplier, 1)), 0) as actual_cost,
  862. COALESCE(SUM(actual_cost), 0) as user_cost
  863. FROM usage_logs
  864. WHERE account_id = $1 AND created_at >= $2 AND created_at < $3
  865. GROUP BY date
  866. ORDER BY date ASC
  867. `
  868. rows, err := r.sql.QueryContext(ctx, query, accountID, startTime, endTime)
  869. if err != nil {
  870. return nil, err
  871. }
  872. defer func() {
  873. // 保持主错误优先;仅在无错误时回传 Close 失败。
  874. // 同时清空返回值,避免误用不完整结果。
  875. if closeErr := rows.Close(); closeErr != nil && err == nil {
  876. err = closeErr
  877. resp = nil
  878. }
  879. }()
  880. history := make([]AccountUsageHistory, 0)
  881. for rows.Next() {
  882. var date string
  883. var requests int64
  884. var tokens int64
  885. var cost float64
  886. var actualCost float64
  887. var userCost float64
  888. if err = rows.Scan(&date, &requests, &tokens, &cost, &actualCost, &userCost); err != nil {
  889. return nil, err
  890. }
  891. t, _ := time.Parse("2006-01-02", date)
  892. history = append(history, AccountUsageHistory{
  893. Date: date,
  894. Label: t.Format("01/02"),
  895. Requests: requests,
  896. Tokens: tokens,
  897. Cost: cost,
  898. ActualCost: actualCost,
  899. UserCost: userCost,
  900. })
  901. }
  902. if err = rows.Err(); err != nil {
  903. return nil, err
  904. }
  905. var totalAccountCost, totalUserCost, totalStandardCost float64
  906. var totalRequests, totalTokens int64
  907. var highestCostDay, highestRequestDay *AccountUsageHistory
  908. for i := range history {
  909. h := &history[i]
  910. totalAccountCost += h.ActualCost
  911. totalUserCost += h.UserCost
  912. totalStandardCost += h.Cost
  913. totalRequests += h.Requests
  914. totalTokens += h.Tokens
  915. if highestCostDay == nil || h.ActualCost > highestCostDay.ActualCost {
  916. highestCostDay = h
  917. }
  918. if highestRequestDay == nil || h.Requests > highestRequestDay.Requests {
  919. highestRequestDay = h
  920. }
  921. }
  922. actualDaysUsed := len(history)
  923. if actualDaysUsed == 0 {
  924. actualDaysUsed = 1
  925. }
  926. avgQuery := "SELECT COALESCE(AVG(duration_ms), 0) as avg_duration_ms FROM usage_logs WHERE account_id = $1 AND created_at >= $2 AND created_at < $3"
  927. var avgDuration float64
  928. if err := scanSingleRow(ctx, r.sql, avgQuery, []any{accountID, startTime, endTime}, &avgDuration); err != nil {
  929. return nil, err
  930. }
  931. summary := AccountUsageSummary{
  932. Days: daysCount,
  933. ActualDaysUsed: actualDaysUsed,
  934. TotalCost: totalAccountCost,
  935. TotalUserCost: totalUserCost,
  936. TotalStandardCost: totalStandardCost,
  937. TotalRequests: totalRequests,
  938. TotalTokens: totalTokens,
  939. AvgDailyCost: totalAccountCost / float64(actualDaysUsed),
  940. AvgDailyUserCost: totalUserCost / float64(actualDaysUsed),
  941. AvgDailyRequests: float64(totalRequests) / float64(actualDaysUsed),
  942. AvgDailyTokens: float64(totalTokens) / float64(actualDaysUsed),
  943. AvgDurationMs: avgDuration,
  944. }
  945. todayStr := timezone.Now().Format("2006-01-02")
  946. for i := range history {
  947. if history[i].Date == todayStr {
  948. summary.Today = &struct {
  949. Date string `json:"date"`
  950. Cost float64 `json:"cost"`
  951. UserCost float64 `json:"user_cost"`
  952. Requests int64 `json:"requests"`
  953. Tokens int64 `json:"tokens"`
  954. }{
  955. Date: history[i].Date,
  956. Cost: history[i].ActualCost,
  957. UserCost: history[i].UserCost,
  958. Requests: history[i].Requests,
  959. Tokens: history[i].Tokens,
  960. }
  961. break
  962. }
  963. }
  964. if highestCostDay != nil {
  965. summary.HighestCostDay = &struct {
  966. Date string `json:"date"`
  967. Label string `json:"label"`
  968. Cost float64 `json:"cost"`
  969. UserCost float64 `json:"user_cost"`
  970. Requests int64 `json:"requests"`
  971. }{
  972. Date: highestCostDay.Date,
  973. Label: highestCostDay.Label,
  974. Cost: highestCostDay.ActualCost,
  975. UserCost: highestCostDay.UserCost,
  976. Requests: highestCostDay.Requests,
  977. }
  978. }
  979. if highestRequestDay != nil {
  980. summary.HighestRequestDay = &struct {
  981. Date string `json:"date"`
  982. Label string `json:"label"`
  983. Requests int64 `json:"requests"`
  984. Cost float64 `json:"cost"`
  985. UserCost float64 `json:"user_cost"`
  986. }{
  987. Date: highestRequestDay.Date,
  988. Label: highestRequestDay.Label,
  989. Requests: highestRequestDay.Requests,
  990. Cost: highestRequestDay.ActualCost,
  991. UserCost: highestRequestDay.UserCost,
  992. }
  993. }
  994. models, err := r.GetModelStatsWithFilters(ctx, startTime, endTime, 0, 0, accountID, 0, nil, nil, nil)
  995. if err != nil {
  996. models = []ModelStat{}
  997. }
  998. endpoints, endpointErr := r.GetEndpointStatsWithFilters(ctx, startTime, endTime, 0, 0, accountID, 0, "", nil, nil, nil)
  999. if endpointErr != nil {
  1000. logger.LegacyPrintf("repository.usage_log", "GetEndpointStatsWithFilters failed in GetAccountUsageStats: %v", endpointErr)
  1001. endpoints = []EndpointStat{}
  1002. }
  1003. upstreamEndpoints, upstreamEndpointErr := r.GetUpstreamEndpointStatsWithFilters(ctx, startTime, endTime, 0, 0, accountID, 0, "", nil, nil, nil)
  1004. if upstreamEndpointErr != nil {
  1005. logger.LegacyPrintf("repository.usage_log", "GetUpstreamEndpointStatsWithFilters failed in GetAccountUsageStats: %v", upstreamEndpointErr)
  1006. upstreamEndpoints = []EndpointStat{}
  1007. }
  1008. resp = &AccountUsageStatsResponse{
  1009. History: history,
  1010. Summary: summary,
  1011. Models: models,
  1012. Endpoints: endpoints,
  1013. UpstreamEndpoints: upstreamEndpoints,
  1014. }
  1015. return resp, nil
  1016. }