dashboard.go 14 KB

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