ops_repo_openai_token_stats.go 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145
  1. package repository
  2. import (
  3. "context"
  4. "database/sql"
  5. "fmt"
  6. "strings"
  7. "github.com/Wei-Shaw/sub2api/internal/service"
  8. )
  9. func (r *opsRepository) GetOpenAITokenStats(ctx context.Context, filter *service.OpsOpenAITokenStatsFilter) (*service.OpsOpenAITokenStatsResponse, error) {
  10. if r == nil || r.db == nil {
  11. return nil, fmt.Errorf("nil ops repository")
  12. }
  13. if filter == nil {
  14. return nil, fmt.Errorf("nil filter")
  15. }
  16. if filter.StartTime.IsZero() || filter.EndTime.IsZero() {
  17. return nil, fmt.Errorf("start_time/end_time required")
  18. }
  19. // 允许 start_time == end_time(结果为空),与 service 层校验口径保持一致。
  20. if filter.StartTime.After(filter.EndTime) {
  21. return nil, fmt.Errorf("start_time must be <= end_time")
  22. }
  23. dashboardFilter := &service.OpsDashboardFilter{
  24. StartTime: filter.StartTime.UTC(),
  25. EndTime: filter.EndTime.UTC(),
  26. Platform: strings.TrimSpace(strings.ToLower(filter.Platform)),
  27. GroupID: filter.GroupID,
  28. }
  29. join, where, baseArgs, next := buildUsageWhere(dashboardFilter, dashboardFilter.StartTime, dashboardFilter.EndTime, 1)
  30. where += " AND ul.model LIKE 'gpt%'"
  31. baseCTE := `
  32. WITH stats AS (
  33. SELECT
  34. ul.model AS model,
  35. COUNT(*)::bigint AS request_count,
  36. ROUND(
  37. AVG(
  38. CASE
  39. WHEN ul.duration_ms > 0 AND ul.output_tokens > 0
  40. THEN ul.output_tokens * 1000.0 / ul.duration_ms
  41. END
  42. )::numeric,
  43. 2
  44. )::float8 AS avg_tokens_per_sec,
  45. ROUND(AVG(ul.first_token_ms)::numeric, 2)::float8 AS avg_first_token_ms,
  46. COALESCE(SUM(ul.output_tokens), 0)::bigint AS total_output_tokens,
  47. COALESCE(ROUND(AVG(ul.duration_ms)::numeric, 0), 0)::bigint AS avg_duration_ms,
  48. COUNT(CASE WHEN ul.first_token_ms IS NOT NULL THEN 1 END)::bigint AS requests_with_first_token
  49. FROM usage_logs ul
  50. ` + join + `
  51. ` + where + `
  52. GROUP BY ul.model
  53. )
  54. `
  55. countSQL := baseCTE + `SELECT COUNT(*) FROM stats`
  56. var total int64
  57. if err := r.db.QueryRowContext(ctx, countSQL, baseArgs...).Scan(&total); err != nil {
  58. return nil, err
  59. }
  60. querySQL := baseCTE + `
  61. SELECT
  62. model,
  63. request_count,
  64. avg_tokens_per_sec,
  65. avg_first_token_ms,
  66. total_output_tokens,
  67. avg_duration_ms,
  68. requests_with_first_token
  69. FROM stats
  70. ORDER BY request_count DESC, model ASC`
  71. args := make([]any, 0, len(baseArgs)+2)
  72. args = append(args, baseArgs...)
  73. if filter.IsTopNMode() {
  74. querySQL += fmt.Sprintf("\nLIMIT $%d", next)
  75. args = append(args, filter.TopN)
  76. } else {
  77. offset := (filter.Page - 1) * filter.PageSize
  78. querySQL += fmt.Sprintf("\nLIMIT $%d OFFSET $%d", next, next+1)
  79. args = append(args, filter.PageSize, offset)
  80. }
  81. rows, err := r.db.QueryContext(ctx, querySQL, args...)
  82. if err != nil {
  83. return nil, err
  84. }
  85. defer func() { _ = rows.Close() }()
  86. items := make([]*service.OpsOpenAITokenStatsItem, 0, 32)
  87. for rows.Next() {
  88. item := &service.OpsOpenAITokenStatsItem{}
  89. var avgTPS sql.NullFloat64
  90. var avgFirstToken sql.NullFloat64
  91. if err := rows.Scan(
  92. &item.Model,
  93. &item.RequestCount,
  94. &avgTPS,
  95. &avgFirstToken,
  96. &item.TotalOutputTokens,
  97. &item.AvgDurationMs,
  98. &item.RequestsWithFirstToken,
  99. ); err != nil {
  100. return nil, err
  101. }
  102. if avgTPS.Valid {
  103. v := avgTPS.Float64
  104. item.AvgTokensPerSec = &v
  105. }
  106. if avgFirstToken.Valid {
  107. v := avgFirstToken.Float64
  108. item.AvgFirstTokenMs = &v
  109. }
  110. items = append(items, item)
  111. }
  112. if err := rows.Err(); err != nil {
  113. return nil, err
  114. }
  115. resp := &service.OpsOpenAITokenStatsResponse{
  116. TimeRange: strings.TrimSpace(filter.TimeRange),
  117. StartTime: dashboardFilter.StartTime,
  118. EndTime: dashboardFilter.EndTime,
  119. Platform: dashboardFilter.Platform,
  120. GroupID: dashboardFilter.GroupID,
  121. Items: items,
  122. Total: total,
  123. }
  124. if filter.IsTopNMode() {
  125. topN := filter.TopN
  126. resp.TopN = &topN
  127. } else {
  128. resp.Page = filter.Page
  129. resp.PageSize = filter.PageSize
  130. }
  131. return resp, nil
  132. }