dashboard.go 12 KB

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