dashboard.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335
  1. package controller
  2. import (
  3. "errors"
  4. "fmt"
  5. "net/http"
  6. "slices"
  7. "strconv"
  8. "time"
  9. "github.com/gin-gonic/gin"
  10. "github.com/labring/aiproxy/core/common/rpmlimit"
  11. "github.com/labring/aiproxy/core/middleware"
  12. "github.com/labring/aiproxy/core/model"
  13. "gorm.io/gorm"
  14. )
  15. func getDashboardTime(t string, startTimestamp int64, endTimestamp int64, timezoneLocation *time.Location) (time.Time, time.Time, model.TimeSpanType) {
  16. end := time.Now()
  17. if endTimestamp != 0 {
  18. end = time.Unix(endTimestamp, 0)
  19. }
  20. if timezoneLocation == nil {
  21. timezoneLocation = time.Local
  22. }
  23. var start time.Time
  24. var timeSpan model.TimeSpanType
  25. switch t {
  26. case "month":
  27. start = end.AddDate(0, 0, -30)
  28. start = time.Date(start.Year(), start.Month(), start.Day(), 0, 0, 0, 0, timezoneLocation)
  29. timeSpan = model.TimeSpanDay
  30. case "two_week":
  31. start = end.AddDate(0, 0, -15)
  32. start = time.Date(start.Year(), start.Month(), start.Day(), 0, 0, 0, 0, timezoneLocation)
  33. timeSpan = model.TimeSpanDay
  34. case "week":
  35. start = end.AddDate(0, 0, -7)
  36. start = time.Date(start.Year(), start.Month(), start.Day(), 0, 0, 0, 0, timezoneLocation)
  37. timeSpan = model.TimeSpanDay
  38. case "day":
  39. fallthrough
  40. default:
  41. start = end.AddDate(0, 0, -1)
  42. timeSpan = model.TimeSpanHour
  43. }
  44. if startTimestamp != 0 {
  45. start = time.Unix(startTimestamp, 0)
  46. }
  47. return start, end, timeSpan
  48. }
  49. func fillGaps(data []*model.ChartData, start, end time.Time, t model.TimeSpanType) []*model.ChartData {
  50. if len(data) == 0 {
  51. return data
  52. }
  53. var timeSpan time.Duration
  54. switch t {
  55. case model.TimeSpanDay:
  56. timeSpan = time.Hour * 24
  57. default:
  58. timeSpan = time.Hour
  59. }
  60. // Handle first point
  61. firstPoint := time.Unix(data[0].Timestamp, 0)
  62. firstAlignedTime := firstPoint
  63. for !firstAlignedTime.Add(-timeSpan).Before(start) {
  64. firstAlignedTime = firstAlignedTime.Add(-timeSpan)
  65. }
  66. var firstIsZero bool
  67. if !firstAlignedTime.Equal(firstPoint) {
  68. data = append([]*model.ChartData{
  69. {
  70. Timestamp: firstAlignedTime.Unix(),
  71. },
  72. }, data...)
  73. firstIsZero = true
  74. }
  75. // Handle last point
  76. lastPoint := time.Unix(data[len(data)-1].Timestamp, 0)
  77. lastAlignedTime := lastPoint
  78. for !lastAlignedTime.Add(timeSpan).After(end) {
  79. lastAlignedTime = lastAlignedTime.Add(timeSpan)
  80. }
  81. var lastIsZero bool
  82. if !lastAlignedTime.Equal(lastPoint) {
  83. data = append(data, &model.ChartData{
  84. Timestamp: lastAlignedTime.Unix(),
  85. })
  86. lastIsZero = true
  87. }
  88. result := make([]*model.ChartData, 0, len(data))
  89. result = append(result, data[0])
  90. for i := 1; i < len(data); i++ {
  91. curr := data[i]
  92. prev := data[i-1]
  93. hourDiff := (curr.Timestamp - prev.Timestamp) / int64(timeSpan.Seconds())
  94. // If gap is 1 hour or less, continue
  95. if hourDiff <= 1 {
  96. result = append(result, curr)
  97. continue
  98. }
  99. // If gap is more than 3 hours, only add boundary points
  100. if hourDiff > 3 {
  101. // Add point for hour after prev
  102. if i != 1 || (i == 1 && !firstIsZero) {
  103. result = append(result, &model.ChartData{
  104. Timestamp: prev.Timestamp + int64(timeSpan.Seconds()),
  105. })
  106. }
  107. // Add point for hour before curr
  108. if i != len(data)-1 || (i == len(data)-1 && !lastIsZero) {
  109. result = append(result, &model.ChartData{
  110. Timestamp: curr.Timestamp - int64(timeSpan.Seconds()),
  111. })
  112. }
  113. result = append(result, curr)
  114. continue
  115. }
  116. // Fill gaps of 2-3 hours with zero points
  117. for j := prev.Timestamp + int64(timeSpan.Seconds()); j < curr.Timestamp; j += int64(timeSpan.Seconds()) {
  118. result = append(result, &model.ChartData{
  119. Timestamp: j,
  120. })
  121. }
  122. result = append(result, curr)
  123. }
  124. return result
  125. }
  126. // GetDashboard godoc
  127. //
  128. // @Summary Get dashboard data
  129. // @Description Returns the general dashboard data including usage statistics and metrics
  130. // @Tags dashboard
  131. // @Produce json
  132. // @Security ApiKeyAuth
  133. // @Param group query string false "Group or *"
  134. // @Param channel query int false "Channel ID"
  135. // @Param type query string false "Type of time span (day, week, month, two_week)"
  136. // @Param model query string false "Model name"
  137. // @Param start_timestamp query int64 false "Start second timestamp"
  138. // @Param end_timestamp query int64 false "End second timestamp"
  139. // @Param timezone query string false "Timezone, default is Local"
  140. // @Success 200 {object} middleware.APIResponse{data=model.DashboardResponse}
  141. // @Router /api/dashboard [get]
  142. func GetDashboard(c *gin.Context) {
  143. log := middleware.GetLogger(c)
  144. group := c.Query("group")
  145. startTimestamp, _ := strconv.ParseInt(c.Query("start_timestamp"), 10, 64)
  146. endTimestamp, _ := strconv.ParseInt(c.Query("end_timestamp"), 10, 64)
  147. timezoneLocation, _ := time.LoadLocation(c.DefaultQuery("timezone", "Local"))
  148. start, end, timeSpan := getDashboardTime(c.Query("type"), startTimestamp, endTimestamp, timezoneLocation)
  149. modelName := c.Query("model")
  150. channelID, _ := strconv.Atoi(c.Query("channel"))
  151. needRPM := channelID != 0
  152. dashboards, err := model.GetDashboardData(group, start, end, modelName, channelID, timeSpan, needRPM, timezoneLocation)
  153. if err != nil {
  154. middleware.ErrorResponse(c, http.StatusOK, err.Error())
  155. return
  156. }
  157. dashboards.ChartData = fillGaps(dashboards.ChartData, start, end, timeSpan)
  158. if !needRPM {
  159. rpm, err := rpmlimit.GetRPM(c.Request.Context(), group, modelName)
  160. if err != nil {
  161. log.Errorf("failed to get rpm: %v", err)
  162. } else {
  163. dashboards.RPM = rpm
  164. }
  165. }
  166. middleware.SuccessResponse(c, dashboards)
  167. }
  168. // GetGroupDashboard godoc
  169. //
  170. // @Summary Get dashboard data for a specific group
  171. // @Description Returns dashboard data and metrics specific to the given group
  172. // @Tags dashboard
  173. // @Produce json
  174. // @Security ApiKeyAuth
  175. // @Param group path string true "Group"
  176. // @Param type query string false "Type of time span (day, week, month, two_week)"
  177. // @Param token_name query string false "Token name"
  178. // @Param model query string false "Model or *"
  179. // @Param start_timestamp query int64 false "Start second timestamp"
  180. // @Param end_timestamp query int64 false "End second timestamp"
  181. // @Param timezone query string false "Timezone, default is Local"
  182. // @Success 200 {object} middleware.APIResponse{data=model.GroupDashboardResponse}
  183. // @Router /api/dashboard/{group} [get]
  184. func GetGroupDashboard(c *gin.Context) {
  185. log := middleware.GetLogger(c)
  186. group := c.Param("group")
  187. if group == "" || group == "*" {
  188. middleware.ErrorResponse(c, http.StatusOK, "invalid group parameter")
  189. return
  190. }
  191. startTimestamp, _ := strconv.ParseInt(c.Query("start_timestamp"), 10, 64)
  192. endTimestamp, _ := strconv.ParseInt(c.Query("end_timestamp"), 10, 64)
  193. timezoneLocation, _ := time.LoadLocation(c.DefaultQuery("timezone", "Local"))
  194. start, end, timeSpan := getDashboardTime(c.Query("type"), startTimestamp, endTimestamp, timezoneLocation)
  195. tokenName := c.Query("token_name")
  196. modelName := c.Query("model")
  197. needRPM := tokenName != ""
  198. dashboards, err := model.GetGroupDashboardData(group, start, end, tokenName, modelName, timeSpan, needRPM, timezoneLocation)
  199. if err != nil {
  200. middleware.ErrorResponse(c, http.StatusOK, "failed to get statistics")
  201. return
  202. }
  203. dashboards.ChartData = fillGaps(dashboards.ChartData, start, end, timeSpan)
  204. if !needRPM {
  205. rpm, err := rpmlimit.GetRPM(c.Request.Context(), group, modelName)
  206. if err != nil {
  207. log.Errorf("failed to get rpm: %v", err)
  208. } else {
  209. dashboards.RPM = rpm
  210. }
  211. }
  212. middleware.SuccessResponse(c, dashboards)
  213. }
  214. // GetGroupDashboardModels godoc
  215. //
  216. // @Summary Get model usage data for a specific group
  217. // @Description Returns model-specific metrics and usage data for the given group
  218. // @Tags dashboard
  219. // @Produce json
  220. // @Security ApiKeyAuth
  221. // @Param group path string true "Group"
  222. // @Success 200 {object} middleware.APIResponse{data=[]model.ModelConfig}
  223. // @Router /api/dashboard/{group}/models [get]
  224. func GetGroupDashboardModels(c *gin.Context) {
  225. group := c.Param("group")
  226. if group == "" || group == "*" {
  227. middleware.ErrorResponse(c, http.StatusOK, "invalid group parameter")
  228. return
  229. }
  230. groupCache, err := model.CacheGetGroup(group)
  231. if err != nil {
  232. if errors.Is(err, gorm.ErrRecordNotFound) {
  233. middleware.SuccessResponse(c, model.LoadModelCaches().EnabledModelConfigsBySet[model.ChannelDefaultSet])
  234. } else {
  235. middleware.ErrorResponse(c, http.StatusOK, fmt.Sprintf("failed to get group: %v", err))
  236. }
  237. return
  238. }
  239. availableSet := groupCache.GetAvailableSets()
  240. enabledModelConfigs := model.LoadModelCaches().EnabledModelConfigsBySet
  241. newEnabledModelConfigs := make([]model.ModelConfig, 0)
  242. for _, set := range availableSet {
  243. for _, mc := range enabledModelConfigs[set] {
  244. if slices.ContainsFunc(newEnabledModelConfigs, func(m model.ModelConfig) bool {
  245. return m.Model == mc.Model
  246. }) {
  247. continue
  248. }
  249. newEnabledModelConfigs = append(newEnabledModelConfigs, middleware.GetGroupAdjustedModelConfig(groupCache, *mc))
  250. }
  251. }
  252. middleware.SuccessResponse(c, newEnabledModelConfigs)
  253. }
  254. // GetModelCostRank godoc
  255. //
  256. // @Summary Get model cost ranking data
  257. // @Description Returns ranking data for models based on cost
  258. // @Tags dashboard
  259. // @Produce json
  260. // @Security ApiKeyAuth
  261. // @Param group query string false "Group or *"
  262. // @Param channel query int false "Channel ID"
  263. // @Param start_timestamp query int64 false "Start timestamp"
  264. // @Param end_timestamp query int64 false "End timestamp"
  265. // @Success 200 {object} middleware.APIResponse{data=[]model.ModelCostRank}
  266. // @Router /api/model_cost_rank [get]
  267. func GetModelCostRank(c *gin.Context) {
  268. group := c.Query("group")
  269. channelID, _ := strconv.Atoi(c.Query("channel"))
  270. startTime, endTime := parseTimeRange(c)
  271. models, err := model.GetModelCostRank(group, channelID, startTime, endTime)
  272. if err != nil {
  273. middleware.ErrorResponse(c, http.StatusOK, err.Error())
  274. return
  275. }
  276. middleware.SuccessResponse(c, models)
  277. }
  278. // GetGroupModelCostRank godoc
  279. //
  280. // @Summary Get model cost ranking data for a specific group
  281. // @Description Returns model cost ranking data specific to the given group
  282. // @Tags dashboard
  283. // @Produce json
  284. // @Security ApiKeyAuth
  285. // @Param group path string true "Group"
  286. // @Param start_timestamp query int64 false "Start timestamp"
  287. // @Param end_timestamp query int64 false "End timestamp"
  288. // @Success 200 {object} middleware.APIResponse{data=[]model.ModelCostRank}
  289. // @Router /api/model_cost_rank/{group} [get]
  290. func GetGroupModelCostRank(c *gin.Context) {
  291. group := c.Param("group")
  292. if group == "" || group == "*" {
  293. middleware.ErrorResponse(c, http.StatusOK, "invalid group parameter")
  294. return
  295. }
  296. startTime, endTime := parseTimeRange(c)
  297. models, err := model.GetModelCostRank(group, 0, startTime, endTime)
  298. if err != nil {
  299. middleware.ErrorResponse(c, http.StatusOK, err.Error())
  300. return
  301. }
  302. middleware.SuccessResponse(c, models)
  303. }