| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599 |
- package service
- import (
- "fmt"
- "math"
- "sort"
- "sync"
- "time"
- "github.com/QuantumNous/new-api/model"
- )
- const (
- rankingCacheTTL = 5 * time.Minute
- rankingLeaderboardLimit = 20
- rankingHistoryLimit = 10
- rankingVendorLimit = 5
- rankingMoverLimit = 6
- rankingOthersLabel = "Others"
- rankingUnknownVendor = "Unknown"
- )
- type RankingsResponse struct {
- Models []RankedModel `json:"models"`
- Vendors []RankedVendor `json:"vendors"`
- TopMovers []RankingMover `json:"top_movers"`
- TopDroppers []RankingMover `json:"top_droppers"`
- ModelsHistory ModelHistorySeries `json:"models_history"`
- VendorShareHistory VendorShareSeries `json:"vendor_share_history"`
- }
- type RankedModel struct {
- Rank int `json:"rank"`
- PreviousRank *int `json:"previous_rank,omitempty"`
- ModelName string `json:"model_name"`
- Vendor string `json:"vendor"`
- VendorIcon string `json:"vendor_icon,omitempty"`
- Category string `json:"category"`
- TotalTokens int64 `json:"total_tokens"`
- Share float64 `json:"share"`
- GrowthPct float64 `json:"growth_pct"`
- }
- type RankedVendor struct {
- Rank int `json:"rank"`
- Vendor string `json:"vendor"`
- VendorIcon string `json:"vendor_icon,omitempty"`
- TotalTokens int64 `json:"total_tokens"`
- Share float64 `json:"share"`
- GrowthPct float64 `json:"growth_pct"`
- ModelsCount int `json:"models_count"`
- TopModel string `json:"top_model"`
- }
- type RankingMover struct {
- ModelName string `json:"model_name"`
- Vendor string `json:"vendor"`
- VendorIcon string `json:"vendor_icon,omitempty"`
- RankDelta int `json:"rank_delta"`
- CurrentRank int `json:"current_rank"`
- GrowthPct float64 `json:"growth_pct"`
- }
- type ModelHistoryPoint struct {
- Ts string `json:"ts"`
- Label string `json:"label"`
- Model string `json:"model"`
- Vendor string `json:"vendor"`
- Tokens int64 `json:"tokens"`
- }
- type ModelHistoryModel struct {
- Name string `json:"name"`
- Vendor string `json:"vendor"`
- Total int64 `json:"total"`
- }
- type ModelHistorySeries struct {
- Points []ModelHistoryPoint `json:"points"`
- Models []ModelHistoryModel `json:"models"`
- Buckets int `json:"buckets"`
- }
- type VendorSharePoint struct {
- Ts string `json:"ts"`
- Label string `json:"label"`
- Vendor string `json:"vendor"`
- Share float64 `json:"share"`
- Tokens int64 `json:"tokens"`
- }
- type VendorShareVendor struct {
- Name string `json:"name"`
- Total int64 `json:"total"`
- Share float64 `json:"share"`
- }
- type VendorShareSeries struct {
- Points []VendorSharePoint `json:"points"`
- Vendors []VendorShareVendor `json:"vendors"`
- Buckets int `json:"buckets"`
- }
- type rankingPeriodConfig struct {
- id string
- duration time.Duration
- bucketSize int64
- labelLayout string
- hasPrevious bool
- }
- type rankingCacheItem struct {
- expiresAt time.Time
- data *RankingsResponse
- }
- type rankingModelMeta struct {
- vendor string
- vendorIcon string
- }
- type vendorAggregate struct {
- name string
- icon string
- totalTokens int64
- previousTokens int64
- models map[string]struct{}
- topModel string
- topModelTokens int64
- }
- var (
- rankingCacheMu sync.Mutex
- rankingCache = map[string]rankingCacheItem{}
- )
- func GetRankingsSnapshot(period string) (*RankingsResponse, error) {
- config, err := rankingConfig(period)
- if err != nil {
- return nil, err
- }
- now := time.Now()
- rankingCacheMu.Lock()
- if item, ok := rankingCache[config.id]; ok && now.Before(item.expiresAt) {
- rankingCacheMu.Unlock()
- return item.data, nil
- }
- rankingCacheMu.Unlock()
- data, err := buildRankingsSnapshot(config, now)
- if err != nil {
- return nil, err
- }
- rankingCacheMu.Lock()
- rankingCache[config.id] = rankingCacheItem{
- expiresAt: now.Add(rankingCacheTTL),
- data: data,
- }
- rankingCacheMu.Unlock()
- return data, nil
- }
- func rankingConfig(period string) (rankingPeriodConfig, error) {
- switch period {
- case "", "week":
- return rankingPeriodConfig{id: "week", duration: 7 * 24 * time.Hour, bucketSize: 24 * 3600, labelLayout: "Jan 2", hasPrevious: true}, nil
- case "today":
- return rankingPeriodConfig{id: "today", duration: 24 * time.Hour, bucketSize: 3600, labelLayout: "15:04", hasPrevious: true}, nil
- case "month":
- return rankingPeriodConfig{id: "month", duration: 30 * 24 * time.Hour, bucketSize: 24 * 3600, labelLayout: "Jan 2", hasPrevious: true}, nil
- case "year":
- return rankingPeriodConfig{id: "year", duration: 365 * 24 * time.Hour, bucketSize: 7 * 24 * 3600, labelLayout: "Jan 2", hasPrevious: true}, nil
- case "all":
- return rankingPeriodConfig{id: "all", bucketSize: 30 * 24 * 3600, labelLayout: "Jan 2006"}, nil
- default:
- return rankingPeriodConfig{}, fmt.Errorf("invalid ranking period: %s", period)
- }
- }
- func buildRankingsSnapshot(config rankingPeriodConfig, now time.Time) (*RankingsResponse, error) {
- startTime, endTime := rankingTimeRange(config, now)
- currentTotals, err := model.GetRankingQuotaTotals(startTime, endTime)
- if err != nil {
- return nil, err
- }
- currentBuckets, err := model.GetRankingQuotaBuckets(startTime, endTime, config.bucketSize)
- if err != nil {
- return nil, err
- }
- var previousTotals []model.RankingQuotaTotal
- if config.hasPrevious {
- previousStart, previousEnd := previousRankingTimeRange(config, startTime)
- previousTotals, err = model.GetRankingQuotaTotals(previousStart, previousEnd)
- if err != nil {
- return nil, err
- }
- }
- meta := buildRankingModelMeta()
- totalTokens := sumRankingTokens(currentTotals)
- previousRankByModel := rankingRankMap(previousTotals)
- previousTokensByModel := rankingTokenMap(previousTotals)
- rankedModels := buildRankedModels(currentTotals, totalTokens, previousRankByModel, previousTokensByModel, meta, config.hasPrevious)
- vendors := buildRankedVendors(currentTotals, previousTotals, totalTokens, meta, config.hasPrevious)
- modelHistory := buildModelHistory(currentBuckets, currentTotals, meta, config)
- vendorHistory := buildVendorShareHistory(currentBuckets, vendors, totalTokens, meta, config)
- movers, droppers := buildRankingMovers(rankedModels)
- return &RankingsResponse{
- Models: limitRankedModels(rankedModels, rankingLeaderboardLimit),
- Vendors: vendors,
- TopMovers: movers,
- TopDroppers: droppers,
- ModelsHistory: modelHistory,
- VendorShareHistory: vendorHistory,
- }, nil
- }
- func rankingTimeRange(config rankingPeriodConfig, now time.Time) (int64, int64) {
- endTime := now.Unix()
- if config.duration <= 0 {
- return 0, endTime
- }
- return now.Add(-config.duration).Unix(), endTime
- }
- func previousRankingTimeRange(config rankingPeriodConfig, currentStart int64) (int64, int64) {
- previousEnd := currentStart - 1
- previousStart := time.Unix(currentStart, 0).Add(-config.duration).Unix()
- return previousStart, previousEnd
- }
- func buildRankingModelMeta() map[string]rankingModelMeta {
- vendorByID := make(map[int]model.PricingVendor)
- for _, vendor := range model.GetVendors() {
- vendorByID[vendor.ID] = vendor
- }
- meta := make(map[string]rankingModelMeta)
- for _, pricing := range model.GetPricing() {
- item := rankingModelMeta{vendor: rankingUnknownVendor}
- if vendor, ok := vendorByID[pricing.VendorID]; ok {
- item.vendor = vendor.Name
- item.vendorIcon = vendor.Icon
- } else if pricing.OwnerBy != "" {
- item.vendor = pricing.OwnerBy
- }
- meta[pricing.ModelName] = item
- }
- return meta
- }
- func modelMeta(modelName string, meta map[string]rankingModelMeta) rankingModelMeta {
- if item, ok := meta[modelName]; ok && item.vendor != "" {
- return item
- }
- return rankingModelMeta{vendor: rankingUnknownVendor}
- }
- func buildRankedModels(totals []model.RankingQuotaTotal, totalTokens int64, previousRanks map[string]int, previousTokens map[string]int64, meta map[string]rankingModelMeta, showGrowth bool) []RankedModel {
- rows := make([]RankedModel, 0, len(totals))
- for idx, item := range totals {
- modelMeta := modelMeta(item.ModelName, meta)
- var previousRank *int
- if rank, ok := previousRanks[item.ModelName]; ok {
- rankCopy := rank
- previousRank = &rankCopy
- }
- growth := 0.0
- if showGrowth {
- growth = rankingGrowthPct(item.TotalTokens, previousTokens[item.ModelName])
- }
- rows = append(rows, RankedModel{
- Rank: idx + 1,
- PreviousRank: previousRank,
- ModelName: item.ModelName,
- Vendor: modelMeta.vendor,
- VendorIcon: modelMeta.vendorIcon,
- Category: "all",
- TotalTokens: item.TotalTokens,
- Share: rankingShare(item.TotalTokens, totalTokens),
- GrowthPct: growth,
- })
- }
- return rows
- }
- func buildRankedVendors(currentTotals []model.RankingQuotaTotal, previousTotals []model.RankingQuotaTotal, totalTokens int64, meta map[string]rankingModelMeta, showGrowth bool) []RankedVendor {
- aggregates := make(map[string]*vendorAggregate)
- for _, item := range currentTotals {
- modelMeta := modelMeta(item.ModelName, meta)
- agg := ensureVendorAggregate(aggregates, modelMeta)
- agg.totalTokens += item.TotalTokens
- agg.models[item.ModelName] = struct{}{}
- if item.TotalTokens > agg.topModelTokens {
- agg.topModel = item.ModelName
- agg.topModelTokens = item.TotalTokens
- }
- }
- for _, item := range previousTotals {
- modelMeta := modelMeta(item.ModelName, meta)
- agg := ensureVendorAggregate(aggregates, modelMeta)
- agg.previousTokens += item.TotalTokens
- }
- rows := make([]RankedVendor, 0, len(aggregates))
- for _, agg := range aggregates {
- if agg.totalTokens <= 0 {
- continue
- }
- growth := 0.0
- if showGrowth {
- growth = rankingGrowthPct(agg.totalTokens, agg.previousTokens)
- }
- rows = append(rows, RankedVendor{
- Vendor: agg.name,
- VendorIcon: agg.icon,
- TotalTokens: agg.totalTokens,
- Share: rankingShare(agg.totalTokens, totalTokens),
- GrowthPct: growth,
- ModelsCount: len(agg.models),
- TopModel: agg.topModel,
- })
- }
- sort.Slice(rows, func(i, j int) bool {
- if rows[i].TotalTokens == rows[j].TotalTokens {
- return rows[i].Vendor < rows[j].Vendor
- }
- return rows[i].TotalTokens > rows[j].TotalTokens
- })
- for idx := range rows {
- rows[idx].Rank = idx + 1
- }
- return rows
- }
- func ensureVendorAggregate(aggregates map[string]*vendorAggregate, meta rankingModelMeta) *vendorAggregate {
- name := meta.vendor
- if name == "" {
- name = rankingUnknownVendor
- }
- agg, ok := aggregates[name]
- if !ok {
- agg = &vendorAggregate{
- name: name,
- icon: meta.vendorIcon,
- models: make(map[string]struct{}),
- }
- aggregates[name] = agg
- }
- if agg.icon == "" && meta.vendorIcon != "" {
- agg.icon = meta.vendorIcon
- }
- return agg
- }
- func buildModelHistory(buckets []model.RankingQuotaBucket, totals []model.RankingQuotaTotal, meta map[string]rankingModelMeta, config rankingPeriodConfig) ModelHistorySeries {
- topModels := make(map[string]struct{})
- models := make([]ModelHistoryModel, 0, minInt(len(totals), rankingHistoryLimit)+1)
- otherTotal := int64(0)
- for idx, item := range totals {
- if idx < rankingHistoryLimit {
- topModels[item.ModelName] = struct{}{}
- modelMeta := modelMeta(item.ModelName, meta)
- models = append(models, ModelHistoryModel{Name: item.ModelName, Vendor: modelMeta.vendor, Total: item.TotalTokens})
- continue
- }
- otherTotal += item.TotalTokens
- }
- if otherTotal > 0 {
- models = append(models, ModelHistoryModel{Name: rankingOthersLabel, Vendor: "Various", Total: otherTotal})
- }
- bucketSet := make(map[int64]struct{})
- tokensByBucketAndModel := make(map[int64]map[string]int64)
- for _, item := range buckets {
- modelName := item.ModelName
- if _, ok := topModels[modelName]; !ok {
- modelName = rankingOthersLabel
- }
- bucketSet[item.Bucket] = struct{}{}
- if _, ok := tokensByBucketAndModel[item.Bucket]; !ok {
- tokensByBucketAndModel[item.Bucket] = make(map[string]int64)
- }
- tokensByBucketAndModel[item.Bucket][modelName] += item.Tokens
- }
- sortedBuckets := sortedRankingBuckets(bucketSet)
- points := make([]ModelHistoryPoint, 0, len(sortedBuckets)*len(models))
- for _, bucket := range sortedBuckets {
- for _, historyModel := range models {
- tokens := tokensByBucketAndModel[bucket][historyModel.Name]
- if tokens <= 0 {
- continue
- }
- points = append(points, ModelHistoryPoint{
- Ts: rankingBucketTs(bucket),
- Label: rankingBucketLabel(bucket, config),
- Model: historyModel.Name,
- Vendor: historyModel.Vendor,
- Tokens: tokens,
- })
- }
- }
- return ModelHistorySeries{
- Points: points,
- Models: models,
- Buckets: len(sortedBuckets),
- }
- }
- func buildVendorShareHistory(buckets []model.RankingQuotaBucket, vendors []RankedVendor, totalTokens int64, meta map[string]rankingModelMeta, config rankingPeriodConfig) VendorShareSeries {
- topVendors := make(map[string]struct{})
- vendorRows := make([]VendorShareVendor, 0, minInt(len(vendors), rankingVendorLimit)+1)
- otherTotal := int64(0)
- for idx, vendor := range vendors {
- if idx < rankingVendorLimit {
- topVendors[vendor.Vendor] = struct{}{}
- vendorRows = append(vendorRows, VendorShareVendor{Name: vendor.Vendor, Total: vendor.TotalTokens, Share: vendor.Share})
- continue
- }
- otherTotal += vendor.TotalTokens
- }
- if otherTotal > 0 {
- vendorRows = append(vendorRows, VendorShareVendor{Name: rankingOthersLabel, Total: otherTotal, Share: rankingShare(otherTotal, totalTokens)})
- }
- bucketSet := make(map[int64]struct{})
- tokensByBucketAndVendor := make(map[int64]map[string]int64)
- totalsByBucket := make(map[int64]int64)
- for _, item := range buckets {
- modelMeta := modelMeta(item.ModelName, meta)
- vendorName := modelMeta.vendor
- if _, ok := topVendors[vendorName]; !ok {
- vendorName = rankingOthersLabel
- }
- bucketSet[item.Bucket] = struct{}{}
- if _, ok := tokensByBucketAndVendor[item.Bucket]; !ok {
- tokensByBucketAndVendor[item.Bucket] = make(map[string]int64)
- }
- tokensByBucketAndVendor[item.Bucket][vendorName] += item.Tokens
- totalsByBucket[item.Bucket] += item.Tokens
- }
- sortedBuckets := sortedRankingBuckets(bucketSet)
- points := make([]VendorSharePoint, 0, len(sortedBuckets)*len(vendorRows))
- for _, bucket := range sortedBuckets {
- for _, vendor := range vendorRows {
- tokens := tokensByBucketAndVendor[bucket][vendor.Name]
- if tokens <= 0 {
- continue
- }
- points = append(points, VendorSharePoint{
- Ts: rankingBucketTs(bucket),
- Label: rankingBucketLabel(bucket, config),
- Vendor: vendor.Name,
- Share: rankingShare(tokens, totalsByBucket[bucket]),
- Tokens: tokens,
- })
- }
- }
- return VendorShareSeries{
- Points: points,
- Vendors: vendorRows,
- Buckets: len(sortedBuckets),
- }
- }
- func buildRankingMovers(models []RankedModel) ([]RankingMover, []RankingMover) {
- movers := make([]RankingMover, 0)
- droppers := make([]RankingMover, 0)
- for _, item := range models {
- if item.PreviousRank == nil {
- continue
- }
- delta := *item.PreviousRank - item.Rank
- if delta == 0 {
- continue
- }
- row := RankingMover{
- ModelName: item.ModelName,
- Vendor: item.Vendor,
- VendorIcon: item.VendorIcon,
- RankDelta: delta,
- CurrentRank: item.Rank,
- GrowthPct: item.GrowthPct,
- }
- if delta > 0 {
- movers = append(movers, row)
- } else {
- droppers = append(droppers, row)
- }
- }
- sort.Slice(movers, func(i, j int) bool {
- if movers[i].RankDelta == movers[j].RankDelta {
- return movers[i].GrowthPct > movers[j].GrowthPct
- }
- return movers[i].RankDelta > movers[j].RankDelta
- })
- sort.Slice(droppers, func(i, j int) bool {
- if droppers[i].RankDelta == droppers[j].RankDelta {
- return droppers[i].GrowthPct < droppers[j].GrowthPct
- }
- return droppers[i].RankDelta < droppers[j].RankDelta
- })
- return limitRankingMovers(movers, rankingMoverLimit), limitRankingMovers(droppers, rankingMoverLimit)
- }
- func sortedRankingBuckets(bucketSet map[int64]struct{}) []int64 {
- buckets := make([]int64, 0, len(bucketSet))
- for bucket := range bucketSet {
- buckets = append(buckets, bucket)
- }
- sort.Slice(buckets, func(i, j int) bool {
- return buckets[i] < buckets[j]
- })
- return buckets
- }
- func rankingBucketTs(bucket int64) string {
- return time.Unix(bucket, 0).UTC().Format(time.RFC3339)
- }
- func rankingBucketLabel(bucket int64, config rankingPeriodConfig) string {
- return time.Unix(bucket, 0).Format(config.labelLayout)
- }
- func rankingRankMap(totals []model.RankingQuotaTotal) map[string]int {
- ranks := make(map[string]int, len(totals))
- for idx, item := range totals {
- ranks[item.ModelName] = idx + 1
- }
- return ranks
- }
- func rankingTokenMap(totals []model.RankingQuotaTotal) map[string]int64 {
- tokens := make(map[string]int64, len(totals))
- for _, item := range totals {
- tokens[item.ModelName] = item.TotalTokens
- }
- return tokens
- }
- func sumRankingTokens(totals []model.RankingQuotaTotal) int64 {
- total := int64(0)
- for _, item := range totals {
- total += item.TotalTokens
- }
- return total
- }
- func rankingShare(value int64, total int64) float64 {
- if total <= 0 || value <= 0 {
- return 0
- }
- return roundRankingFloat(float64(value) / float64(total))
- }
- func rankingGrowthPct(current int64, previous int64) float64 {
- if previous <= 0 {
- if current > 0 {
- return 100
- }
- return 0
- }
- return roundRankingFloat((float64(current-previous) / float64(previous)) * 100)
- }
- func roundRankingFloat(value float64) float64 {
- return math.Round(value*10000) / 10000
- }
- func limitRankedModels(rows []RankedModel, limit int) []RankedModel {
- if limit <= 0 || len(rows) <= limit {
- return rows
- }
- return rows[:limit]
- }
- func limitRankingMovers(rows []RankingMover, limit int) []RankingMover {
- if limit <= 0 || len(rows) <= limit {
- return rows
- }
- return rows[:limit]
- }
- func minInt(a int, b int) int {
- if a < b {
- return a
- }
- return b
- }
|