dashboard.go 12 KB

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