dashboard.go 14 KB

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