rankings.go 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599
  1. package service
  2. import (
  3. "fmt"
  4. "math"
  5. "sort"
  6. "sync"
  7. "time"
  8. "github.com/QuantumNous/new-api/model"
  9. )
  10. const (
  11. rankingCacheTTL = 5 * time.Minute
  12. rankingLeaderboardLimit = 20
  13. rankingHistoryLimit = 10
  14. rankingVendorLimit = 5
  15. rankingMoverLimit = 6
  16. rankingOthersLabel = "Others"
  17. rankingUnknownVendor = "Unknown"
  18. )
  19. type RankingsResponse struct {
  20. Models []RankedModel `json:"models"`
  21. Vendors []RankedVendor `json:"vendors"`
  22. TopMovers []RankingMover `json:"top_movers"`
  23. TopDroppers []RankingMover `json:"top_droppers"`
  24. ModelsHistory ModelHistorySeries `json:"models_history"`
  25. VendorShareHistory VendorShareSeries `json:"vendor_share_history"`
  26. }
  27. type RankedModel struct {
  28. Rank int `json:"rank"`
  29. PreviousRank *int `json:"previous_rank,omitempty"`
  30. ModelName string `json:"model_name"`
  31. Vendor string `json:"vendor"`
  32. VendorIcon string `json:"vendor_icon,omitempty"`
  33. Category string `json:"category"`
  34. TotalTokens int64 `json:"total_tokens"`
  35. Share float64 `json:"share"`
  36. GrowthPct float64 `json:"growth_pct"`
  37. }
  38. type RankedVendor struct {
  39. Rank int `json:"rank"`
  40. Vendor string `json:"vendor"`
  41. VendorIcon string `json:"vendor_icon,omitempty"`
  42. TotalTokens int64 `json:"total_tokens"`
  43. Share float64 `json:"share"`
  44. GrowthPct float64 `json:"growth_pct"`
  45. ModelsCount int `json:"models_count"`
  46. TopModel string `json:"top_model"`
  47. }
  48. type RankingMover struct {
  49. ModelName string `json:"model_name"`
  50. Vendor string `json:"vendor"`
  51. VendorIcon string `json:"vendor_icon,omitempty"`
  52. RankDelta int `json:"rank_delta"`
  53. CurrentRank int `json:"current_rank"`
  54. GrowthPct float64 `json:"growth_pct"`
  55. }
  56. type ModelHistoryPoint struct {
  57. Ts string `json:"ts"`
  58. Label string `json:"label"`
  59. Model string `json:"model"`
  60. Vendor string `json:"vendor"`
  61. Tokens int64 `json:"tokens"`
  62. }
  63. type ModelHistoryModel struct {
  64. Name string `json:"name"`
  65. Vendor string `json:"vendor"`
  66. Total int64 `json:"total"`
  67. }
  68. type ModelHistorySeries struct {
  69. Points []ModelHistoryPoint `json:"points"`
  70. Models []ModelHistoryModel `json:"models"`
  71. Buckets int `json:"buckets"`
  72. }
  73. type VendorSharePoint struct {
  74. Ts string `json:"ts"`
  75. Label string `json:"label"`
  76. Vendor string `json:"vendor"`
  77. Share float64 `json:"share"`
  78. Tokens int64 `json:"tokens"`
  79. }
  80. type VendorShareVendor struct {
  81. Name string `json:"name"`
  82. Total int64 `json:"total"`
  83. Share float64 `json:"share"`
  84. }
  85. type VendorShareSeries struct {
  86. Points []VendorSharePoint `json:"points"`
  87. Vendors []VendorShareVendor `json:"vendors"`
  88. Buckets int `json:"buckets"`
  89. }
  90. type rankingPeriodConfig struct {
  91. id string
  92. duration time.Duration
  93. bucketSize int64
  94. labelLayout string
  95. hasPrevious bool
  96. }
  97. type rankingCacheItem struct {
  98. expiresAt time.Time
  99. data *RankingsResponse
  100. }
  101. type rankingModelMeta struct {
  102. vendor string
  103. vendorIcon string
  104. }
  105. type vendorAggregate struct {
  106. name string
  107. icon string
  108. totalTokens int64
  109. previousTokens int64
  110. models map[string]struct{}
  111. topModel string
  112. topModelTokens int64
  113. }
  114. var (
  115. rankingCacheMu sync.Mutex
  116. rankingCache = map[string]rankingCacheItem{}
  117. )
  118. func GetRankingsSnapshot(period string) (*RankingsResponse, error) {
  119. config, err := rankingConfig(period)
  120. if err != nil {
  121. return nil, err
  122. }
  123. now := time.Now()
  124. rankingCacheMu.Lock()
  125. if item, ok := rankingCache[config.id]; ok && now.Before(item.expiresAt) {
  126. rankingCacheMu.Unlock()
  127. return item.data, nil
  128. }
  129. rankingCacheMu.Unlock()
  130. data, err := buildRankingsSnapshot(config, now)
  131. if err != nil {
  132. return nil, err
  133. }
  134. rankingCacheMu.Lock()
  135. rankingCache[config.id] = rankingCacheItem{
  136. expiresAt: now.Add(rankingCacheTTL),
  137. data: data,
  138. }
  139. rankingCacheMu.Unlock()
  140. return data, nil
  141. }
  142. func rankingConfig(period string) (rankingPeriodConfig, error) {
  143. switch period {
  144. case "", "week":
  145. return rankingPeriodConfig{id: "week", duration: 7 * 24 * time.Hour, bucketSize: 24 * 3600, labelLayout: "Jan 2", hasPrevious: true}, nil
  146. case "today":
  147. return rankingPeriodConfig{id: "today", duration: 24 * time.Hour, bucketSize: 3600, labelLayout: "15:04", hasPrevious: true}, nil
  148. case "month":
  149. return rankingPeriodConfig{id: "month", duration: 30 * 24 * time.Hour, bucketSize: 24 * 3600, labelLayout: "Jan 2", hasPrevious: true}, nil
  150. case "year":
  151. return rankingPeriodConfig{id: "year", duration: 365 * 24 * time.Hour, bucketSize: 7 * 24 * 3600, labelLayout: "Jan 2", hasPrevious: true}, nil
  152. case "all":
  153. return rankingPeriodConfig{id: "all", bucketSize: 30 * 24 * 3600, labelLayout: "Jan 2006"}, nil
  154. default:
  155. return rankingPeriodConfig{}, fmt.Errorf("invalid ranking period: %s", period)
  156. }
  157. }
  158. func buildRankingsSnapshot(config rankingPeriodConfig, now time.Time) (*RankingsResponse, error) {
  159. startTime, endTime := rankingTimeRange(config, now)
  160. currentTotals, err := model.GetRankingQuotaTotals(startTime, endTime)
  161. if err != nil {
  162. return nil, err
  163. }
  164. currentBuckets, err := model.GetRankingQuotaBuckets(startTime, endTime, config.bucketSize)
  165. if err != nil {
  166. return nil, err
  167. }
  168. var previousTotals []model.RankingQuotaTotal
  169. if config.hasPrevious {
  170. previousStart, previousEnd := previousRankingTimeRange(config, startTime)
  171. previousTotals, err = model.GetRankingQuotaTotals(previousStart, previousEnd)
  172. if err != nil {
  173. return nil, err
  174. }
  175. }
  176. meta := buildRankingModelMeta()
  177. totalTokens := sumRankingTokens(currentTotals)
  178. previousRankByModel := rankingRankMap(previousTotals)
  179. previousTokensByModel := rankingTokenMap(previousTotals)
  180. rankedModels := buildRankedModels(currentTotals, totalTokens, previousRankByModel, previousTokensByModel, meta, config.hasPrevious)
  181. vendors := buildRankedVendors(currentTotals, previousTotals, totalTokens, meta, config.hasPrevious)
  182. modelHistory := buildModelHistory(currentBuckets, currentTotals, meta, config)
  183. vendorHistory := buildVendorShareHistory(currentBuckets, vendors, totalTokens, meta, config)
  184. movers, droppers := buildRankingMovers(rankedModels)
  185. return &RankingsResponse{
  186. Models: limitRankedModels(rankedModels, rankingLeaderboardLimit),
  187. Vendors: vendors,
  188. TopMovers: movers,
  189. TopDroppers: droppers,
  190. ModelsHistory: modelHistory,
  191. VendorShareHistory: vendorHistory,
  192. }, nil
  193. }
  194. func rankingTimeRange(config rankingPeriodConfig, now time.Time) (int64, int64) {
  195. endTime := now.Unix()
  196. if config.duration <= 0 {
  197. return 0, endTime
  198. }
  199. return now.Add(-config.duration).Unix(), endTime
  200. }
  201. func previousRankingTimeRange(config rankingPeriodConfig, currentStart int64) (int64, int64) {
  202. previousEnd := currentStart - 1
  203. previousStart := time.Unix(currentStart, 0).Add(-config.duration).Unix()
  204. return previousStart, previousEnd
  205. }
  206. func buildRankingModelMeta() map[string]rankingModelMeta {
  207. vendorByID := make(map[int]model.PricingVendor)
  208. for _, vendor := range model.GetVendors() {
  209. vendorByID[vendor.ID] = vendor
  210. }
  211. meta := make(map[string]rankingModelMeta)
  212. for _, pricing := range model.GetPricing() {
  213. item := rankingModelMeta{vendor: rankingUnknownVendor}
  214. if vendor, ok := vendorByID[pricing.VendorID]; ok {
  215. item.vendor = vendor.Name
  216. item.vendorIcon = vendor.Icon
  217. } else if pricing.OwnerBy != "" {
  218. item.vendor = pricing.OwnerBy
  219. }
  220. meta[pricing.ModelName] = item
  221. }
  222. return meta
  223. }
  224. func modelMeta(modelName string, meta map[string]rankingModelMeta) rankingModelMeta {
  225. if item, ok := meta[modelName]; ok && item.vendor != "" {
  226. return item
  227. }
  228. return rankingModelMeta{vendor: rankingUnknownVendor}
  229. }
  230. func buildRankedModels(totals []model.RankingQuotaTotal, totalTokens int64, previousRanks map[string]int, previousTokens map[string]int64, meta map[string]rankingModelMeta, showGrowth bool) []RankedModel {
  231. rows := make([]RankedModel, 0, len(totals))
  232. for idx, item := range totals {
  233. modelMeta := modelMeta(item.ModelName, meta)
  234. var previousRank *int
  235. if rank, ok := previousRanks[item.ModelName]; ok {
  236. rankCopy := rank
  237. previousRank = &rankCopy
  238. }
  239. growth := 0.0
  240. if showGrowth {
  241. growth = rankingGrowthPct(item.TotalTokens, previousTokens[item.ModelName])
  242. }
  243. rows = append(rows, RankedModel{
  244. Rank: idx + 1,
  245. PreviousRank: previousRank,
  246. ModelName: item.ModelName,
  247. Vendor: modelMeta.vendor,
  248. VendorIcon: modelMeta.vendorIcon,
  249. Category: "all",
  250. TotalTokens: item.TotalTokens,
  251. Share: rankingShare(item.TotalTokens, totalTokens),
  252. GrowthPct: growth,
  253. })
  254. }
  255. return rows
  256. }
  257. func buildRankedVendors(currentTotals []model.RankingQuotaTotal, previousTotals []model.RankingQuotaTotal, totalTokens int64, meta map[string]rankingModelMeta, showGrowth bool) []RankedVendor {
  258. aggregates := make(map[string]*vendorAggregate)
  259. for _, item := range currentTotals {
  260. modelMeta := modelMeta(item.ModelName, meta)
  261. agg := ensureVendorAggregate(aggregates, modelMeta)
  262. agg.totalTokens += item.TotalTokens
  263. agg.models[item.ModelName] = struct{}{}
  264. if item.TotalTokens > agg.topModelTokens {
  265. agg.topModel = item.ModelName
  266. agg.topModelTokens = item.TotalTokens
  267. }
  268. }
  269. for _, item := range previousTotals {
  270. modelMeta := modelMeta(item.ModelName, meta)
  271. agg := ensureVendorAggregate(aggregates, modelMeta)
  272. agg.previousTokens += item.TotalTokens
  273. }
  274. rows := make([]RankedVendor, 0, len(aggregates))
  275. for _, agg := range aggregates {
  276. if agg.totalTokens <= 0 {
  277. continue
  278. }
  279. growth := 0.0
  280. if showGrowth {
  281. growth = rankingGrowthPct(agg.totalTokens, agg.previousTokens)
  282. }
  283. rows = append(rows, RankedVendor{
  284. Vendor: agg.name,
  285. VendorIcon: agg.icon,
  286. TotalTokens: agg.totalTokens,
  287. Share: rankingShare(agg.totalTokens, totalTokens),
  288. GrowthPct: growth,
  289. ModelsCount: len(agg.models),
  290. TopModel: agg.topModel,
  291. })
  292. }
  293. sort.Slice(rows, func(i, j int) bool {
  294. if rows[i].TotalTokens == rows[j].TotalTokens {
  295. return rows[i].Vendor < rows[j].Vendor
  296. }
  297. return rows[i].TotalTokens > rows[j].TotalTokens
  298. })
  299. for idx := range rows {
  300. rows[idx].Rank = idx + 1
  301. }
  302. return rows
  303. }
  304. func ensureVendorAggregate(aggregates map[string]*vendorAggregate, meta rankingModelMeta) *vendorAggregate {
  305. name := meta.vendor
  306. if name == "" {
  307. name = rankingUnknownVendor
  308. }
  309. agg, ok := aggregates[name]
  310. if !ok {
  311. agg = &vendorAggregate{
  312. name: name,
  313. icon: meta.vendorIcon,
  314. models: make(map[string]struct{}),
  315. }
  316. aggregates[name] = agg
  317. }
  318. if agg.icon == "" && meta.vendorIcon != "" {
  319. agg.icon = meta.vendorIcon
  320. }
  321. return agg
  322. }
  323. func buildModelHistory(buckets []model.RankingQuotaBucket, totals []model.RankingQuotaTotal, meta map[string]rankingModelMeta, config rankingPeriodConfig) ModelHistorySeries {
  324. topModels := make(map[string]struct{})
  325. models := make([]ModelHistoryModel, 0, minInt(len(totals), rankingHistoryLimit)+1)
  326. otherTotal := int64(0)
  327. for idx, item := range totals {
  328. if idx < rankingHistoryLimit {
  329. topModels[item.ModelName] = struct{}{}
  330. modelMeta := modelMeta(item.ModelName, meta)
  331. models = append(models, ModelHistoryModel{Name: item.ModelName, Vendor: modelMeta.vendor, Total: item.TotalTokens})
  332. continue
  333. }
  334. otherTotal += item.TotalTokens
  335. }
  336. if otherTotal > 0 {
  337. models = append(models, ModelHistoryModel{Name: rankingOthersLabel, Vendor: "Various", Total: otherTotal})
  338. }
  339. bucketSet := make(map[int64]struct{})
  340. tokensByBucketAndModel := make(map[int64]map[string]int64)
  341. for _, item := range buckets {
  342. modelName := item.ModelName
  343. if _, ok := topModels[modelName]; !ok {
  344. modelName = rankingOthersLabel
  345. }
  346. bucketSet[item.Bucket] = struct{}{}
  347. if _, ok := tokensByBucketAndModel[item.Bucket]; !ok {
  348. tokensByBucketAndModel[item.Bucket] = make(map[string]int64)
  349. }
  350. tokensByBucketAndModel[item.Bucket][modelName] += item.Tokens
  351. }
  352. sortedBuckets := sortedRankingBuckets(bucketSet)
  353. points := make([]ModelHistoryPoint, 0, len(sortedBuckets)*len(models))
  354. for _, bucket := range sortedBuckets {
  355. for _, historyModel := range models {
  356. tokens := tokensByBucketAndModel[bucket][historyModel.Name]
  357. if tokens <= 0 {
  358. continue
  359. }
  360. points = append(points, ModelHistoryPoint{
  361. Ts: rankingBucketTs(bucket),
  362. Label: rankingBucketLabel(bucket, config),
  363. Model: historyModel.Name,
  364. Vendor: historyModel.Vendor,
  365. Tokens: tokens,
  366. })
  367. }
  368. }
  369. return ModelHistorySeries{
  370. Points: points,
  371. Models: models,
  372. Buckets: len(sortedBuckets),
  373. }
  374. }
  375. func buildVendorShareHistory(buckets []model.RankingQuotaBucket, vendors []RankedVendor, totalTokens int64, meta map[string]rankingModelMeta, config rankingPeriodConfig) VendorShareSeries {
  376. topVendors := make(map[string]struct{})
  377. vendorRows := make([]VendorShareVendor, 0, minInt(len(vendors), rankingVendorLimit)+1)
  378. otherTotal := int64(0)
  379. for idx, vendor := range vendors {
  380. if idx < rankingVendorLimit {
  381. topVendors[vendor.Vendor] = struct{}{}
  382. vendorRows = append(vendorRows, VendorShareVendor{Name: vendor.Vendor, Total: vendor.TotalTokens, Share: vendor.Share})
  383. continue
  384. }
  385. otherTotal += vendor.TotalTokens
  386. }
  387. if otherTotal > 0 {
  388. vendorRows = append(vendorRows, VendorShareVendor{Name: rankingOthersLabel, Total: otherTotal, Share: rankingShare(otherTotal, totalTokens)})
  389. }
  390. bucketSet := make(map[int64]struct{})
  391. tokensByBucketAndVendor := make(map[int64]map[string]int64)
  392. totalsByBucket := make(map[int64]int64)
  393. for _, item := range buckets {
  394. modelMeta := modelMeta(item.ModelName, meta)
  395. vendorName := modelMeta.vendor
  396. if _, ok := topVendors[vendorName]; !ok {
  397. vendorName = rankingOthersLabel
  398. }
  399. bucketSet[item.Bucket] = struct{}{}
  400. if _, ok := tokensByBucketAndVendor[item.Bucket]; !ok {
  401. tokensByBucketAndVendor[item.Bucket] = make(map[string]int64)
  402. }
  403. tokensByBucketAndVendor[item.Bucket][vendorName] += item.Tokens
  404. totalsByBucket[item.Bucket] += item.Tokens
  405. }
  406. sortedBuckets := sortedRankingBuckets(bucketSet)
  407. points := make([]VendorSharePoint, 0, len(sortedBuckets)*len(vendorRows))
  408. for _, bucket := range sortedBuckets {
  409. for _, vendor := range vendorRows {
  410. tokens := tokensByBucketAndVendor[bucket][vendor.Name]
  411. if tokens <= 0 {
  412. continue
  413. }
  414. points = append(points, VendorSharePoint{
  415. Ts: rankingBucketTs(bucket),
  416. Label: rankingBucketLabel(bucket, config),
  417. Vendor: vendor.Name,
  418. Share: rankingShare(tokens, totalsByBucket[bucket]),
  419. Tokens: tokens,
  420. })
  421. }
  422. }
  423. return VendorShareSeries{
  424. Points: points,
  425. Vendors: vendorRows,
  426. Buckets: len(sortedBuckets),
  427. }
  428. }
  429. func buildRankingMovers(models []RankedModel) ([]RankingMover, []RankingMover) {
  430. movers := make([]RankingMover, 0)
  431. droppers := make([]RankingMover, 0)
  432. for _, item := range models {
  433. if item.PreviousRank == nil {
  434. continue
  435. }
  436. delta := *item.PreviousRank - item.Rank
  437. if delta == 0 {
  438. continue
  439. }
  440. row := RankingMover{
  441. ModelName: item.ModelName,
  442. Vendor: item.Vendor,
  443. VendorIcon: item.VendorIcon,
  444. RankDelta: delta,
  445. CurrentRank: item.Rank,
  446. GrowthPct: item.GrowthPct,
  447. }
  448. if delta > 0 {
  449. movers = append(movers, row)
  450. } else {
  451. droppers = append(droppers, row)
  452. }
  453. }
  454. sort.Slice(movers, func(i, j int) bool {
  455. if movers[i].RankDelta == movers[j].RankDelta {
  456. return movers[i].GrowthPct > movers[j].GrowthPct
  457. }
  458. return movers[i].RankDelta > movers[j].RankDelta
  459. })
  460. sort.Slice(droppers, func(i, j int) bool {
  461. if droppers[i].RankDelta == droppers[j].RankDelta {
  462. return droppers[i].GrowthPct < droppers[j].GrowthPct
  463. }
  464. return droppers[i].RankDelta < droppers[j].RankDelta
  465. })
  466. return limitRankingMovers(movers, rankingMoverLimit), limitRankingMovers(droppers, rankingMoverLimit)
  467. }
  468. func sortedRankingBuckets(bucketSet map[int64]struct{}) []int64 {
  469. buckets := make([]int64, 0, len(bucketSet))
  470. for bucket := range bucketSet {
  471. buckets = append(buckets, bucket)
  472. }
  473. sort.Slice(buckets, func(i, j int) bool {
  474. return buckets[i] < buckets[j]
  475. })
  476. return buckets
  477. }
  478. func rankingBucketTs(bucket int64) string {
  479. return time.Unix(bucket, 0).UTC().Format(time.RFC3339)
  480. }
  481. func rankingBucketLabel(bucket int64, config rankingPeriodConfig) string {
  482. return time.Unix(bucket, 0).Format(config.labelLayout)
  483. }
  484. func rankingRankMap(totals []model.RankingQuotaTotal) map[string]int {
  485. ranks := make(map[string]int, len(totals))
  486. for idx, item := range totals {
  487. ranks[item.ModelName] = idx + 1
  488. }
  489. return ranks
  490. }
  491. func rankingTokenMap(totals []model.RankingQuotaTotal) map[string]int64 {
  492. tokens := make(map[string]int64, len(totals))
  493. for _, item := range totals {
  494. tokens[item.ModelName] = item.TotalTokens
  495. }
  496. return tokens
  497. }
  498. func sumRankingTokens(totals []model.RankingQuotaTotal) int64 {
  499. total := int64(0)
  500. for _, item := range totals {
  501. total += item.TotalTokens
  502. }
  503. return total
  504. }
  505. func rankingShare(value int64, total int64) float64 {
  506. if total <= 0 || value <= 0 {
  507. return 0
  508. }
  509. return roundRankingFloat(float64(value) / float64(total))
  510. }
  511. func rankingGrowthPct(current int64, previous int64) float64 {
  512. if previous <= 0 {
  513. if current > 0 {
  514. return 100
  515. }
  516. return 0
  517. }
  518. return roundRankingFloat((float64(current-previous) / float64(previous)) * 100)
  519. }
  520. func roundRankingFloat(value float64) float64 {
  521. return math.Round(value*10000) / 10000
  522. }
  523. func limitRankedModels(rows []RankedModel, limit int) []RankedModel {
  524. if limit <= 0 || len(rows) <= limit {
  525. return rows
  526. }
  527. return rows[:limit]
  528. }
  529. func limitRankingMovers(rows []RankingMover, limit int) []RankingMover {
  530. if limit <= 0 || len(rows) <= limit {
  531. return rows
  532. }
  533. return rows[:limit]
  534. }
  535. func minInt(a int, b int) int {
  536. if a < b {
  537. return a
  538. }
  539. return b
  540. }