| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495 |
- package controller
- import (
- "encoding/json"
- "errors"
- "fmt"
- "net/http"
- "slices"
- "strconv"
- "time"
- "github.com/bytedance/sonic"
- "github.com/gin-gonic/gin"
- "github.com/labring/aiproxy/core/common/reqlimit"
- "github.com/labring/aiproxy/core/controller/utils"
- "github.com/labring/aiproxy/core/middleware"
- "github.com/labring/aiproxy/core/model"
- "github.com/labring/aiproxy/core/relay/mode"
- "gorm.io/gorm"
- )
- func getDashboardTime(
- t, timespan string,
- startTime, endTime time.Time,
- timezoneLocation *time.Location,
- ) (time.Time, time.Time, model.TimeSpanType) {
- end := time.Now()
- if !endTime.IsZero() {
- end = endTime
- }
- if timezoneLocation == nil {
- timezoneLocation = time.Local
- }
- var (
- start time.Time
- timeSpan model.TimeSpanType
- )
- switch t {
- case "month":
- start = end.AddDate(0, 0, -30)
- start = time.Date(start.Year(), start.Month(), start.Day(), 0, 0, 0, 0, timezoneLocation)
- timeSpan = model.TimeSpanDay
- case "two_week":
- start = end.AddDate(0, 0, -15)
- start = time.Date(start.Year(), start.Month(), start.Day(), 0, 0, 0, 0, timezoneLocation)
- timeSpan = model.TimeSpanDay
- case "week":
- start = end.AddDate(0, 0, -7)
- start = time.Date(start.Year(), start.Month(), start.Day(), 0, 0, 0, 0, timezoneLocation)
- timeSpan = model.TimeSpanDay
- case "day":
- start = end.AddDate(0, 0, -1)
- timeSpan = model.TimeSpanHour
- default:
- start = end.AddDate(0, 0, -7)
- timeSpan = model.TimeSpanHour
- }
- if !startTime.IsZero() {
- start = startTime
- }
- switch model.TimeSpanType(timespan) {
- case model.TimeSpanDay, model.TimeSpanHour, model.TimeSpanMonth:
- timeSpan = model.TimeSpanType(timespan)
- }
- return start, end, timeSpan
- }
- func fillGaps(
- data []model.ChartData,
- start, end time.Time,
- t model.TimeSpanType,
- ) []model.ChartData {
- if len(data) == 0 || t == model.TimeSpanMonth {
- return data
- }
- var timeSpan time.Duration
- switch t {
- case model.TimeSpanDay:
- timeSpan = time.Hour * 24
- case model.TimeSpanHour:
- timeSpan = time.Hour
- case model.TimeSpanMinute:
- timeSpan = time.Minute
- default:
- return data
- }
- // Handle first point
- firstPoint := time.Unix(data[0].Timestamp, 0)
- firstAlignedTime := firstPoint
- for !firstAlignedTime.Add(-timeSpan).Before(start) {
- firstAlignedTime = firstAlignedTime.Add(-timeSpan)
- }
- var firstIsZero bool
- if !firstAlignedTime.Equal(firstPoint) {
- data = append([]model.ChartData{
- {
- Timestamp: firstAlignedTime.Unix(),
- },
- }, data...)
- firstIsZero = true
- }
- // Handle last point
- lastPoint := time.Unix(data[len(data)-1].Timestamp, 0)
- lastAlignedTime := lastPoint
- for !lastAlignedTime.Add(timeSpan).After(end) {
- lastAlignedTime = lastAlignedTime.Add(timeSpan)
- }
- var lastIsZero bool
- if !lastAlignedTime.Equal(lastPoint) {
- data = append(data, model.ChartData{
- Timestamp: lastAlignedTime.Unix(),
- })
- lastIsZero = true
- }
- result := make([]model.ChartData, 0, len(data))
- result = append(result, data[0])
- for i := 1; i < len(data); i++ {
- curr := data[i]
- prev := data[i-1]
- hourDiff := (curr.Timestamp - prev.Timestamp) / int64(timeSpan.Seconds())
- // If gap is 1 hour or less, continue
- if hourDiff <= 1 {
- result = append(result, curr)
- continue
- }
- // If gap is more than 3 hours, only add boundary points
- if hourDiff > 3 {
- // Add point for hour after prev
- if i != 1 || (i == 1 && !firstIsZero) {
- result = append(result, model.ChartData{
- Timestamp: prev.Timestamp + int64(timeSpan.Seconds()),
- })
- }
- // Add point for hour before curr
- if i != len(data)-1 || (i == len(data)-1 && !lastIsZero) {
- result = append(result, model.ChartData{
- Timestamp: curr.Timestamp - int64(timeSpan.Seconds()),
- })
- }
- result = append(result, curr)
- continue
- }
- // Fill gaps of 2-3 hours with zero points
- for j := prev.Timestamp + int64(timeSpan.Seconds()); j < curr.Timestamp; j += int64(timeSpan.Seconds()) {
- result = append(result, model.ChartData{
- Timestamp: j,
- })
- }
- result = append(result, curr)
- }
- return result
- }
- // GetDashboard godoc
- //
- // @Summary Get dashboard data
- // @Description Returns the general dashboard data including usage statistics and metrics
- // @Tags dashboard
- // @Produce json
- // @Security ApiKeyAuth
- // @Param channel query int false "Channel ID"
- // @Param model query string false "Model name"
- // @Param start_timestamp query int64 false "Start second timestamp"
- // @Param end_timestamp query int64 false "End second timestamp"
- // @Param timezone query string false "Timezone, default is Local"
- // @Param timespan query string false "Time span type (minute, hour, day, month)"
- // @Success 200 {object} middleware.APIResponse{data=model.DashboardResponse}
- // @Router /api/dashboard/ [get]
- func GetDashboard(c *gin.Context) {
- startTime, endTime := utils.ParseTimeRange(c, -1)
- timezoneLocation, _ := time.LoadLocation(c.DefaultQuery("timezone", "Local"))
- timespan := c.Query("timespan")
- start, end, timeSpan := getDashboardTime(
- c.Query("type"),
- timespan,
- startTime,
- endTime,
- timezoneLocation,
- )
- modelName := c.Query("model")
- channelStr := c.Query("channel")
- channelID, _ := strconv.Atoi(channelStr)
- dashboards, err := model.GetDashboardData(
- start,
- end,
- modelName,
- channelID,
- timeSpan,
- timezoneLocation,
- )
- if err != nil {
- middleware.ErrorResponse(c, http.StatusInternalServerError, err.Error())
- return
- }
- dashboards.ChartData = fillGaps(dashboards.ChartData, start, end, timeSpan)
- if channelID == 0 {
- channelStr = "*"
- }
- rpm, _ := reqlimit.GetChannelModelRequest(c.Request.Context(), channelStr, modelName)
- dashboards.RPM = rpm
- tpm, _ := reqlimit.GetChannelModelTokensRequest(c.Request.Context(), channelStr, modelName)
- dashboards.TPM = tpm
- middleware.SuccessResponse(c, dashboards)
- }
- // GetGroupDashboard godoc
- //
- // @Summary Get dashboard data for a specific group
- // @Description Returns dashboard data and metrics specific to the given group
- // @Tags dashboard
- // @Produce json
- // @Security ApiKeyAuth
- // @Param group path string true "Group"
- // @Param token_name query string false "Token name"
- // @Param model query string false "Model or *"
- // @Param start_timestamp query int64 false "Start second timestamp"
- // @Param end_timestamp query int64 false "End second timestamp"
- // @Param timezone query string false "Timezone, default is Local"
- // @Param timespan query string false "Time span type (minute, hour, day, month)"
- // @Success 200 {object} middleware.APIResponse{data=model.GroupDashboardResponse}
- // @Router /api/dashboard/{group} [get]
- func GetGroupDashboard(c *gin.Context) {
- group := c.Param("group")
- if group == "" {
- middleware.ErrorResponse(c, http.StatusBadRequest, "invalid group parameter")
- return
- }
- startTime, endTime := utils.ParseTimeRange(c, -1)
- timezoneLocation, _ := time.LoadLocation(c.DefaultQuery("timezone", "Local"))
- timespan := c.Query("timespan")
- start, end, timeSpan := getDashboardTime(
- c.Query("type"),
- timespan,
- startTime,
- endTime,
- timezoneLocation,
- )
- tokenName := c.Query("token_name")
- modelName := c.Query("model")
- dashboards, err := model.GetGroupDashboardData(
- group,
- start,
- end,
- tokenName,
- modelName,
- timeSpan,
- timezoneLocation,
- )
- if err != nil {
- middleware.ErrorResponse(c, http.StatusInternalServerError, "failed to get statistics")
- return
- }
- dashboards.ChartData = fillGaps(dashboards.ChartData, start, end, timeSpan)
- rpm, _ := reqlimit.GetGroupModelTokennameRequest(
- c.Request.Context(),
- group,
- modelName,
- tokenName,
- )
- dashboards.RPM = rpm
- tpm, _ := reqlimit.GetGroupModelTokennameTokensRequest(
- c.Request.Context(),
- group,
- modelName,
- tokenName,
- )
- dashboards.TPM = tpm
- middleware.SuccessResponse(c, dashboards)
- }
- type GroupModel struct {
- CreatedAt int64 `json:"created_at,omitempty"`
- UpdatedAt int64 `json:"updated_at,omitempty"`
- Config map[model.ModelConfigKey]any `json:"config,omitempty"`
- Model string `json:"model"`
- Owner model.ModelOwner `json:"owner"`
- Type mode.Mode `json:"type"`
- RPM int64 `json:"rpm,omitempty"`
- TPM int64 `json:"tpm,omitempty"`
- // map[size]map[quality]price_per_image
- ImageQualityPrices map[string]map[string]float64 `json:"image_quality_prices,omitempty"`
- // map[size]price_per_image
- ImagePrices map[string]float64 `json:"image_prices,omitempty"`
- Price model.Price `json:"price,omitempty"`
- EnabledPlugins []string `json:"enabled_plugins,omitempty"`
- }
- func getEnabledPlugins(plugin map[string]json.RawMessage) []string {
- enabledPlugins := make([]string, 0, len(plugin))
- for pluginName, pluginConfig := range plugin {
- pluginConfigNode, err := sonic.Get(pluginConfig)
- if err != nil {
- continue
- }
- if enable, err := pluginConfigNode.Get("enable").Bool(); err == nil && enable {
- enabledPlugins = append(enabledPlugins, pluginName)
- }
- }
- return enabledPlugins
- }
- func NewGroupModel(mc model.ModelConfig) GroupModel {
- gm := GroupModel{
- Config: mc.Config,
- Model: mc.Model,
- Owner: mc.Owner,
- Type: mc.Type,
- RPM: mc.RPM,
- TPM: mc.TPM,
- ImageQualityPrices: mc.ImageQualityPrices,
- ImagePrices: mc.ImagePrices,
- Price: mc.Price,
- EnabledPlugins: getEnabledPlugins(mc.Plugin),
- }
- if !mc.CreatedAt.IsZero() {
- gm.CreatedAt = mc.CreatedAt.Unix()
- }
- if !mc.UpdatedAt.IsZero() {
- gm.UpdatedAt = mc.UpdatedAt.Unix()
- }
- return gm
- }
- // GetGroupDashboardModels godoc
- //
- // @Summary Get model usage data for a specific group
- // @Description Returns model-specific metrics and usage data for the given group
- // @Tags dashboard
- // @Produce json
- // @Security ApiKeyAuth
- // @Param group path string true "Group"
- // @Success 200 {object} middleware.APIResponse{data=[]GroupModel}
- // @Router /api/dashboard/{group}/models [get]
- func GetGroupDashboardModels(c *gin.Context) {
- group := c.Param("group")
- if group == "" {
- middleware.ErrorResponse(c, http.StatusBadRequest, "invalid group parameter")
- return
- }
- groupCache, err := model.CacheGetGroup(group)
- if err != nil {
- if errors.Is(err, gorm.ErrRecordNotFound) {
- middleware.SuccessResponse(
- c,
- model.LoadModelCaches().EnabledModelConfigsBySet[model.ChannelDefaultSet],
- )
- } else {
- middleware.ErrorResponse(c, http.StatusInternalServerError, fmt.Sprintf("failed to get group: %v", err))
- }
- return
- }
- availableSet := groupCache.GetAvailableSets()
- enabledModelConfigs := model.LoadModelCaches().EnabledModelConfigsBySet
- newEnabledModelConfigs := make([]GroupModel, 0)
- for _, set := range availableSet {
- for _, mc := range enabledModelConfigs[set] {
- if slices.ContainsFunc(newEnabledModelConfigs, func(m GroupModel) bool {
- return m.Model == mc.Model
- }) {
- continue
- }
- newEnabledModelConfigs = append(
- newEnabledModelConfigs,
- NewGroupModel(
- middleware.GetGroupAdjustedModelConfig(*groupCache, mc),
- ),
- )
- }
- }
- middleware.SuccessResponse(c, newEnabledModelConfigs)
- }
- // GetTimeSeriesModelData godoc
- //
- // @Summary Get model usage data for a specific channel
- // @Description Returns model-specific metrics and usage data for the given channel
- // @Tags dashboard
- // @Produce json
- // @Security ApiKeyAuth
- // @Param channel query int false "Channel ID"
- // @Param model query string false "Model name"
- // @Param start_timestamp query int64 false "Start timestamp"
- // @Param end_timestamp query int64 false "End timestamp"
- // @Param timezone query string false "Timezone, default is Local"
- // @Param timespan query string false "Time span type (minute, hour, day, month)"
- // @Success 200 {object} middleware.APIResponse{data=[]model.TimeSummaryDataV2}
- // @Router /api/dashboardv2/ [get]
- func GetTimeSeriesModelData(c *gin.Context) {
- channelID, _ := strconv.Atoi(c.Query("channel"))
- modelName := c.Query("model")
- startTime, endTime := utils.ParseTimeRange(c, -1)
- timezoneLocation, _ := time.LoadLocation(c.DefaultQuery("timezone", "Local"))
- models, err := model.GetTimeSeriesModelData(
- channelID,
- modelName,
- startTime,
- endTime,
- model.TimeSpanType(c.Query("timespan")),
- timezoneLocation,
- )
- if err != nil {
- middleware.ErrorResponse(c, http.StatusInternalServerError, err.Error())
- return
- }
- middleware.SuccessResponse(c, models)
- }
- // GetGroupTimeSeriesModelData godoc
- //
- // @Summary Get model usage data for a specific group
- // @Description Returns model-specific metrics and usage data for the given group
- // @Tags dashboard
- // @Produce json
- // @Security ApiKeyAuth
- // @Param group path string true "Group"
- // @Param token_name query string false "Token name"
- // @Param model query string false "Model name"
- // @Param start_timestamp query int64 false "Start timestamp"
- // @Param end_timestamp query int64 false "End timestamp"
- // @Param timezone query string false "Timezone, default is Local"
- // @Param timespan query string false "Time span type (minute, hour, day, month)"
- // @Success 200 {object} middleware.APIResponse{data=[]model.TimeSummaryDataV2}
- // @Router /api/dashboardv2/{group} [get]
- func GetGroupTimeSeriesModelData(c *gin.Context) {
- group := c.Param("group")
- if group == "" {
- middleware.ErrorResponse(c, http.StatusBadRequest, "invalid group parameter")
- return
- }
- tokenName := c.Query("token_name")
- modelName := c.Query("model")
- startTime, endTime := utils.ParseTimeRange(c, -1)
- timezoneLocation, _ := time.LoadLocation(c.DefaultQuery("timezone", "Local"))
- models, err := model.GetGroupTimeSeriesModelData(
- group,
- tokenName,
- modelName,
- startTime,
- endTime,
- model.TimeSpanType(c.Query("timespan")),
- timezoneLocation,
- )
- if err != nil {
- middleware.ErrorResponse(c, http.StatusInternalServerError, err.Error())
- return
- }
- middleware.SuccessResponse(c, models)
- }
|