Преглед изворни кода

feat: dashboard v2 provide time series model data (#233)

* feat: dashboard v2 provide time series model data

* chore: swag

* fix: ci lint
zijiren пре 6 месеци
родитељ
комит
554347510f
8 измењених фајлова са 1164 додато и 946 уклоњено
  1. 72 3
      core/controller/dashboard.go
  2. 0 92
      core/controller/log.go
  3. 215 206
      core/docs/docs.go
  4. 215 206
      core/docs/swagger.json
  5. 133 120
      core/docs/swagger.yaml
  6. 0 315
      core/model/log.go
  7. 523 0
      core/model/summary.go
  8. 6 4
      core/router/api.go

+ 72 - 3
core/controller/dashboard.go

@@ -157,7 +157,6 @@ func fillGaps(
 //	@Produce		json
 //	@Security		ApiKeyAuth
 //	@Param			channel			query		int		false	"Channel ID"
-//	@Param			type			query		string	false	"Type of time span (day, week, month, two_week)"
 //	@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"
@@ -215,7 +214,6 @@ func GetDashboard(c *gin.Context) {
 //	@Produce		json
 //	@Security		ApiKeyAuth
 //	@Param			group			path		string	true	"Group"
-//	@Param			type			query		string	false	"Type of time span (day, week, month, two_week)"
 //	@Param			token_name		query		string	false	"Token name"
 //	@Param			model			query		string	false	"Model or *"
 //	@Param			start_timestamp	query		int64	false	"Start second timestamp"
@@ -291,7 +289,7 @@ func GetGroupDashboard(c *gin.Context) {
 //	@Router			/api/dashboard/{group}/models [get]
 func GetGroupDashboardModels(c *gin.Context) {
 	group := c.Param("group")
-	if group == "" || group == "*" {
+	if group == "" {
 		middleware.ErrorResponse(c, http.StatusBadRequest, "invalid group parameter")
 		return
 	}
@@ -378,3 +376,74 @@ func GetGroupModelCostRank(c *gin.Context) {
 	}
 	middleware.SuccessResponse(c, models)
 }
+
+// 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			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 (day, hour)"
+//	@Success		200				{object}	middleware.APIResponse{data=[]model.TimeModelData}
+//	@Router			/api/dashboardv2/ [get]
+func GetTimeSeriesModelData(c *gin.Context) {
+	channelID, _ := strconv.Atoi(c.Query("channel"))
+	startTime, endTime := parseTimeRange(c)
+	timezoneLocation, _ := time.LoadLocation(c.DefaultQuery("timezone", "Local"))
+	models, err := model.GetTimeSeriesModelData(
+		channelID,
+		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			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 (day, hour)"
+//	@Success		200				{object}	middleware.APIResponse{data=[]model.TimeModelData}
+//	@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")
+	startTime, endTime := parseTimeRange(c)
+	timezoneLocation, _ := time.LoadLocation(c.DefaultQuery("timezone", "Local"))
+	models, err := model.GetGroupTimeSeriesModelData(
+		group,
+		tokenName,
+		startTime,
+		endTime,
+		model.TimeSpanType(c.Query("timespan")),
+		timezoneLocation,
+	)
+	if err != nil {
+		middleware.ErrorResponse(c, http.StatusInternalServerError, err.Error())
+		return
+	}
+	middleware.SuccessResponse(c, models)
+}

+ 0 - 92
core/controller/log.go

@@ -298,52 +298,6 @@ func SearchGroupLogs(c *gin.Context) {
 	middleware.SuccessResponse(c, result)
 }
 
-// GetUsedModels godoc
-//
-//	@Summary		Get used models
-//	@Description	Get a list of models that have been used in logs
-//	@Tags			logs
-//	@Produce		json
-//	@Security		ApiKeyAuth
-//	@Param			group	query		string	false	"Group or *"
-//	@Success		200		{object}	middleware.APIResponse{data=[]string}
-//	@Router			/api/logs/used/models [get]
-func GetUsedModels(c *gin.Context) {
-	group := c.Query("group")
-	startTime, endTime := parseTimeRange(c)
-	models, err := model.GetUsedModelsFromLog(group, startTime, endTime)
-	if err != nil {
-		middleware.ErrorResponse(c, http.StatusInternalServerError, err.Error())
-		return
-	}
-	middleware.SuccessResponse(c, models)
-}
-
-// GetGroupUsedModels godoc
-//
-//	@Summary		Get group used models
-//	@Description	Get a list of models that have been used in a specific group's logs
-//	@Tags			log
-//	@Produce		json
-//	@Security		ApiKeyAuth
-//	@Param			group	path		string	true	"Group name"
-//	@Success		200		{object}	middleware.APIResponse{data=[]string}
-//	@Router			/api/log/{group}/used/models [get]
-func GetGroupUsedModels(c *gin.Context) {
-	group := c.Param("group")
-	if group == "" || group == "*" {
-		middleware.ErrorResponse(c, http.StatusBadRequest, "invalid group parameter")
-		return
-	}
-	startTime, endTime := parseTimeRange(c)
-	models, err := model.GetUsedModelsFromLog(group, startTime, endTime)
-	if err != nil {
-		middleware.ErrorResponse(c, http.StatusInternalServerError, err.Error())
-		return
-	}
-	middleware.SuccessResponse(c, models)
-}
-
 // GetLogDetail godoc
 //
 //	@Summary		Get log detail
@@ -390,52 +344,6 @@ func GetGroupLogDetail(c *gin.Context) {
 	middleware.SuccessResponse(c, log)
 }
 
-// GetUsedTokenNames godoc
-//
-//	@Summary		Get used token names
-//	@Description	Get a list of token names that have been used in logs
-//	@Tags			logs
-//	@Produce		json
-//	@Security		ApiKeyAuth
-//	@Param			group	query		string	false	"Group or *"
-//	@Success		200		{object}	middleware.APIResponse{data=[]string}
-//	@Router			/api/logs/used/token_names [get]
-func GetUsedTokenNames(c *gin.Context) {
-	group := c.Query("group")
-	startTime, endTime := parseTimeRange(c)
-	tokenNames, err := model.GetUsedTokenNamesFromLog(group, startTime, endTime)
-	if err != nil {
-		middleware.ErrorResponse(c, http.StatusInternalServerError, err.Error())
-		return
-	}
-	middleware.SuccessResponse(c, tokenNames)
-}
-
-// GetGroupUsedTokenNames godoc
-//
-//	@Summary		Get group used token names
-//	@Description	Get a list of token names that have been used in a specific group's logs
-//	@Tags			log
-//	@Produce		json
-//	@Security		ApiKeyAuth
-//	@Param			group	path		string	true	"Group name"
-//	@Success		200		{object}	middleware.APIResponse{data=[]string}
-//	@Router			/api/log/{group}/used/token_names [get]
-func GetGroupUsedTokenNames(c *gin.Context) {
-	group := c.Param("group")
-	if group == "" || group == "*" {
-		middleware.ErrorResponse(c, http.StatusBadRequest, "invalid group parameter")
-		return
-	}
-	startTime, endTime := parseTimeRange(c)
-	tokenNames, err := model.GetUsedTokenNamesFromLog(group, startTime, endTime)
-	if err != nil {
-		middleware.ErrorResponse(c, http.StatusInternalServerError, err.Error())
-		return
-	}
-	middleware.SuccessResponse(c, tokenNames)
-}
-
 // DeleteHistoryLogs godoc
 //
 //	@Summary		Delete historical logs

+ 215 - 206
core/docs/docs.go

@@ -943,12 +943,6 @@ const docTemplate = `{
                         "name": "channel",
                         "in": "query"
                     },
-                    {
-                        "type": "string",
-                        "description": "Type of time span (day, week, month, two_week)",
-                        "name": "type",
-                        "in": "query"
-                    },
                     {
                         "type": "string",
                         "description": "Model name",
@@ -1025,12 +1019,6 @@ const docTemplate = `{
                         "in": "path",
                         "required": true
                     },
-                    {
-                        "type": "string",
-                        "description": "Type of time span (day, week, month, two_week)",
-                        "name": "type",
-                        "in": "query"
-                    },
                     {
                         "type": "string",
                         "description": "Token name",
@@ -1139,6 +1127,157 @@ const docTemplate = `{
                 }
             }
         },
+        "/api/dashboardv2/": {
+            "get": {
+                "security": [
+                    {
+                        "ApiKeyAuth": []
+                    }
+                ],
+                "description": "Returns model-specific metrics and usage data for the given channel",
+                "produces": [
+                    "application/json"
+                ],
+                "tags": [
+                    "dashboard"
+                ],
+                "summary": "Get model usage data for a specific channel",
+                "parameters": [
+                    {
+                        "type": "integer",
+                        "description": "Channel ID",
+                        "name": "channel",
+                        "in": "query"
+                    },
+                    {
+                        "type": "integer",
+                        "description": "Start timestamp",
+                        "name": "start_timestamp",
+                        "in": "query"
+                    },
+                    {
+                        "type": "integer",
+                        "description": "End timestamp",
+                        "name": "end_timestamp",
+                        "in": "query"
+                    },
+                    {
+                        "type": "string",
+                        "description": "Timezone, default is Local",
+                        "name": "timezone",
+                        "in": "query"
+                    },
+                    {
+                        "type": "string",
+                        "description": "Time span type (day, hour)",
+                        "name": "timespan",
+                        "in": "query"
+                    }
+                ],
+                "responses": {
+                    "200": {
+                        "description": "OK",
+                        "schema": {
+                            "allOf": [
+                                {
+                                    "$ref": "#/definitions/middleware.APIResponse"
+                                },
+                                {
+                                    "type": "object",
+                                    "properties": {
+                                        "data": {
+                                            "type": "array",
+                                            "items": {
+                                                "$ref": "#/definitions/model.TimeModelData"
+                                            }
+                                        }
+                                    }
+                                }
+                            ]
+                        }
+                    }
+                }
+            }
+        },
+        "/api/dashboardv2/{group}": {
+            "get": {
+                "security": [
+                    {
+                        "ApiKeyAuth": []
+                    }
+                ],
+                "description": "Returns model-specific metrics and usage data for the given group",
+                "produces": [
+                    "application/json"
+                ],
+                "tags": [
+                    "dashboard"
+                ],
+                "summary": "Get model usage data for a specific group",
+                "parameters": [
+                    {
+                        "type": "string",
+                        "description": "Group",
+                        "name": "group",
+                        "in": "path",
+                        "required": true
+                    },
+                    {
+                        "type": "string",
+                        "description": "Token name",
+                        "name": "token_name",
+                        "in": "query"
+                    },
+                    {
+                        "type": "integer",
+                        "description": "Start timestamp",
+                        "name": "start_timestamp",
+                        "in": "query"
+                    },
+                    {
+                        "type": "integer",
+                        "description": "End timestamp",
+                        "name": "end_timestamp",
+                        "in": "query"
+                    },
+                    {
+                        "type": "string",
+                        "description": "Timezone, default is Local",
+                        "name": "timezone",
+                        "in": "query"
+                    },
+                    {
+                        "type": "string",
+                        "description": "Time span type (day, hour)",
+                        "name": "timespan",
+                        "in": "query"
+                    }
+                ],
+                "responses": {
+                    "200": {
+                        "description": "OK",
+                        "schema": {
+                            "allOf": [
+                                {
+                                    "$ref": "#/definitions/middleware.APIResponse"
+                                },
+                                {
+                                    "type": "object",
+                                    "properties": {
+                                        "data": {
+                                            "type": "array",
+                                            "items": {
+                                                "$ref": "#/definitions/model.TimeModelData"
+                                            }
+                                        }
+                                    }
+                                }
+                            ]
+                        }
+                    }
+                }
+            }
+        },
         "/api/embedmcp/": {
             "get": {
                 "security": [
@@ -2541,104 +2680,6 @@ const docTemplate = `{
                 }
             }
         },
-        "/api/log/{group}/used/models": {
-            "get": {
-                "security": [
-                    {
-                        "ApiKeyAuth": []
-                    }
-                ],
-                "description": "Get a list of models that have been used in a specific group's logs",
-                "produces": [
-                    "application/json"
-                ],
-                "tags": [
-                    "log"
-                ],
-                "summary": "Get group used models",
-                "parameters": [
-                    {
-                        "type": "string",
-                        "description": "Group name",
-                        "name": "group",
-                        "in": "path",
-                        "required": true
-                    }
-                ],
-                "responses": {
-                    "200": {
-                        "description": "OK",
-                        "schema": {
-                            "allOf": [
-                                {
-                                    "$ref": "#/definitions/middleware.APIResponse"
-                                },
-                                {
-                                    "type": "object",
-                                    "properties": {
-                                        "data": {
-                                            "type": "array",
-                                            "items": {
-                                                "type": "string"
-                                            }
-                                        }
-                                    }
-                                }
-                            ]
-                        }
-                    }
-                }
-            }
-        },
-        "/api/log/{group}/used/token_names": {
-            "get": {
-                "security": [
-                    {
-                        "ApiKeyAuth": []
-                    }
-                ],
-                "description": "Get a list of token names that have been used in a specific group's logs",
-                "produces": [
-                    "application/json"
-                ],
-                "tags": [
-                    "log"
-                ],
-                "summary": "Get group used token names",
-                "parameters": [
-                    {
-                        "type": "string",
-                        "description": "Group name",
-                        "name": "group",
-                        "in": "path",
-                        "required": true
-                    }
-                ],
-                "responses": {
-                    "200": {
-                        "description": "OK",
-                        "schema": {
-                            "allOf": [
-                                {
-                                    "$ref": "#/definitions/middleware.APIResponse"
-                                },
-                                {
-                                    "type": "object",
-                                    "properties": {
-                                        "data": {
-                                            "type": "array",
-                                            "items": {
-                                                "type": "string"
-                                            }
-                                        }
-                                    }
-                                }
-                            ]
-                        }
-                    }
-                }
-            }
-        },
         "/api/logs/": {
             "get": {
                 "security": [
@@ -3136,102 +3177,6 @@ const docTemplate = `{
                 }
             }
         },
-        "/api/logs/used/models": {
-            "get": {
-                "security": [
-                    {
-                        "ApiKeyAuth": []
-                    }
-                ],
-                "description": "Get a list of models that have been used in logs",
-                "produces": [
-                    "application/json"
-                ],
-                "tags": [
-                    "logs"
-                ],
-                "summary": "Get used models",
-                "parameters": [
-                    {
-                        "type": "string",
-                        "description": "Group or *",
-                        "name": "group",
-                        "in": "query"
-                    }
-                ],
-                "responses": {
-                    "200": {
-                        "description": "OK",
-                        "schema": {
-                            "allOf": [
-                                {
-                                    "$ref": "#/definitions/middleware.APIResponse"
-                                },
-                                {
-                                    "type": "object",
-                                    "properties": {
-                                        "data": {
-                                            "type": "array",
-                                            "items": {
-                                                "type": "string"
-                                            }
-                                        }
-                                    }
-                                }
-                            ]
-                        }
-                    }
-                }
-            }
-        },
-        "/api/logs/used/token_names": {
-            "get": {
-                "security": [
-                    {
-                        "ApiKeyAuth": []
-                    }
-                ],
-                "description": "Get a list of token names that have been used in logs",
-                "produces": [
-                    "application/json"
-                ],
-                "tags": [
-                    "logs"
-                ],
-                "summary": "Get used token names",
-                "parameters": [
-                    {
-                        "type": "string",
-                        "description": "Group or *",
-                        "name": "group",
-                        "in": "query"
-                    }
-                ],
-                "responses": {
-                    "200": {
-                        "description": "OK",
-                        "schema": {
-                            "allOf": [
-                                {
-                                    "$ref": "#/definitions/middleware.APIResponse"
-                                },
-                                {
-                                    "type": "object",
-                                    "properties": {
-                                        "data": {
-                                            "type": "array",
-                                            "items": {
-                                                "type": "string"
-                                            }
-                                        }
-                                    }
-                                }
-                            ]
-                        }
-                    }
-                }
-            }
-        },
         "/api/mcp/group/all": {
             "get": {
                 "security": [
@@ -10279,6 +10224,56 @@ const docTemplate = `{
                 }
             }
         },
+        "model.ModelData": {
+            "type": "object",
+            "properties": {
+                "cache_creation_tokens": {
+                    "type": "integer"
+                },
+                "cached_tokens": {
+                    "type": "integer"
+                },
+                "exception_count": {
+                    "type": "integer"
+                },
+                "input_tokens": {
+                    "type": "integer"
+                },
+                "max_rpm": {
+                    "type": "integer"
+                },
+                "max_rps": {
+                    "type": "integer"
+                },
+                "max_tpm": {
+                    "type": "integer"
+                },
+                "max_tps": {
+                    "type": "integer"
+                },
+                "model": {
+                    "type": "string"
+                },
+                "output_tokens": {
+                    "type": "integer"
+                },
+                "request_count": {
+                    "type": "integer"
+                },
+                "timestamp": {
+                    "type": "integer"
+                },
+                "total_tokens": {
+                    "type": "integer"
+                },
+                "used_amount": {
+                    "type": "number"
+                },
+                "web_search_count": {
+                    "type": "integer"
+                }
+            }
+        },
         "model.ModelOwner": {
             "type": "string",
             "enum": [
@@ -10798,6 +10793,20 @@ const docTemplate = `{
                 }
             }
         },
+        "model.TimeModelData": {
+            "type": "object",
+            "properties": {
+                "models": {
+                    "type": "array",
+                    "items": {
+                        "$ref": "#/definitions/model.ModelData"
+                    }
+                },
+                "timestamp": {
+                    "type": "integer"
+                }
+            }
+        },
         "model.Tool": {
             "type": "object",
             "properties": {

+ 215 - 206
core/docs/swagger.json

@@ -934,12 +934,6 @@
                         "name": "channel",
                         "in": "query"
                     },
-                    {
-                        "type": "string",
-                        "description": "Type of time span (day, week, month, two_week)",
-                        "name": "type",
-                        "in": "query"
-                    },
                     {
                         "type": "string",
                         "description": "Model name",
@@ -1016,12 +1010,6 @@
                         "in": "path",
                         "required": true
                     },
-                    {
-                        "type": "string",
-                        "description": "Type of time span (day, week, month, two_week)",
-                        "name": "type",
-                        "in": "query"
-                    },
                     {
                         "type": "string",
                         "description": "Token name",
@@ -1130,6 +1118,157 @@
                 }
             }
         },
+        "/api/dashboardv2/": {
+            "get": {
+                "security": [
+                    {
+                        "ApiKeyAuth": []
+                    }
+                ],
+                "description": "Returns model-specific metrics and usage data for the given channel",
+                "produces": [
+                    "application/json"
+                ],
+                "tags": [
+                    "dashboard"
+                ],
+                "summary": "Get model usage data for a specific channel",
+                "parameters": [
+                    {
+                        "type": "integer",
+                        "description": "Channel ID",
+                        "name": "channel",
+                        "in": "query"
+                    },
+                    {
+                        "type": "integer",
+                        "description": "Start timestamp",
+                        "name": "start_timestamp",
+                        "in": "query"
+                    },
+                    {
+                        "type": "integer",
+                        "description": "End timestamp",
+                        "name": "end_timestamp",
+                        "in": "query"
+                    },
+                    {
+                        "type": "string",
+                        "description": "Timezone, default is Local",
+                        "name": "timezone",
+                        "in": "query"
+                    },
+                    {
+                        "type": "string",
+                        "description": "Time span type (day, hour)",
+                        "name": "timespan",
+                        "in": "query"
+                    }
+                ],
+                "responses": {
+                    "200": {
+                        "description": "OK",
+                        "schema": {
+                            "allOf": [
+                                {
+                                    "$ref": "#/definitions/middleware.APIResponse"
+                                },
+                                {
+                                    "type": "object",
+                                    "properties": {
+                                        "data": {
+                                            "type": "array",
+                                            "items": {
+                                                "$ref": "#/definitions/model.TimeModelData"
+                                            }
+                                        }
+                                    }
+                                }
+                            ]
+                        }
+                    }
+                }
+            }
+        },
+        "/api/dashboardv2/{group}": {
+            "get": {
+                "security": [
+                    {
+                        "ApiKeyAuth": []
+                    }
+                ],
+                "description": "Returns model-specific metrics and usage data for the given group",
+                "produces": [
+                    "application/json"
+                ],
+                "tags": [
+                    "dashboard"
+                ],
+                "summary": "Get model usage data for a specific group",
+                "parameters": [
+                    {
+                        "type": "string",
+                        "description": "Group",
+                        "name": "group",
+                        "in": "path",
+                        "required": true
+                    },
+                    {
+                        "type": "string",
+                        "description": "Token name",
+                        "name": "token_name",
+                        "in": "query"
+                    },
+                    {
+                        "type": "integer",
+                        "description": "Start timestamp",
+                        "name": "start_timestamp",
+                        "in": "query"
+                    },
+                    {
+                        "type": "integer",
+                        "description": "End timestamp",
+                        "name": "end_timestamp",
+                        "in": "query"
+                    },
+                    {
+                        "type": "string",
+                        "description": "Timezone, default is Local",
+                        "name": "timezone",
+                        "in": "query"
+                    },
+                    {
+                        "type": "string",
+                        "description": "Time span type (day, hour)",
+                        "name": "timespan",
+                        "in": "query"
+                    }
+                ],
+                "responses": {
+                    "200": {
+                        "description": "OK",
+                        "schema": {
+                            "allOf": [
+                                {
+                                    "$ref": "#/definitions/middleware.APIResponse"
+                                },
+                                {
+                                    "type": "object",
+                                    "properties": {
+                                        "data": {
+                                            "type": "array",
+                                            "items": {
+                                                "$ref": "#/definitions/model.TimeModelData"
+                                            }
+                                        }
+                                    }
+                                }
+                            ]
+                        }
+                    }
+                }
+            }
+        },
         "/api/embedmcp/": {
             "get": {
                 "security": [
@@ -2532,104 +2671,6 @@
                 }
             }
         },
-        "/api/log/{group}/used/models": {
-            "get": {
-                "security": [
-                    {
-                        "ApiKeyAuth": []
-                    }
-                ],
-                "description": "Get a list of models that have been used in a specific group's logs",
-                "produces": [
-                    "application/json"
-                ],
-                "tags": [
-                    "log"
-                ],
-                "summary": "Get group used models",
-                "parameters": [
-                    {
-                        "type": "string",
-                        "description": "Group name",
-                        "name": "group",
-                        "in": "path",
-                        "required": true
-                    }
-                ],
-                "responses": {
-                    "200": {
-                        "description": "OK",
-                        "schema": {
-                            "allOf": [
-                                {
-                                    "$ref": "#/definitions/middleware.APIResponse"
-                                },
-                                {
-                                    "type": "object",
-                                    "properties": {
-                                        "data": {
-                                            "type": "array",
-                                            "items": {
-                                                "type": "string"
-                                            }
-                                        }
-                                    }
-                                }
-                            ]
-                        }
-                    }
-                }
-            }
-        },
-        "/api/log/{group}/used/token_names": {
-            "get": {
-                "security": [
-                    {
-                        "ApiKeyAuth": []
-                    }
-                ],
-                "description": "Get a list of token names that have been used in a specific group's logs",
-                "produces": [
-                    "application/json"
-                ],
-                "tags": [
-                    "log"
-                ],
-                "summary": "Get group used token names",
-                "parameters": [
-                    {
-                        "type": "string",
-                        "description": "Group name",
-                        "name": "group",
-                        "in": "path",
-                        "required": true
-                    }
-                ],
-                "responses": {
-                    "200": {
-                        "description": "OK",
-                        "schema": {
-                            "allOf": [
-                                {
-                                    "$ref": "#/definitions/middleware.APIResponse"
-                                },
-                                {
-                                    "type": "object",
-                                    "properties": {
-                                        "data": {
-                                            "type": "array",
-                                            "items": {
-                                                "type": "string"
-                                            }
-                                        }
-                                    }
-                                }
-                            ]
-                        }
-                    }
-                }
-            }
-        },
         "/api/logs/": {
             "get": {
                 "security": [
@@ -3127,102 +3168,6 @@
                 }
             }
         },
-        "/api/logs/used/models": {
-            "get": {
-                "security": [
-                    {
-                        "ApiKeyAuth": []
-                    }
-                ],
-                "description": "Get a list of models that have been used in logs",
-                "produces": [
-                    "application/json"
-                ],
-                "tags": [
-                    "logs"
-                ],
-                "summary": "Get used models",
-                "parameters": [
-                    {
-                        "type": "string",
-                        "description": "Group or *",
-                        "name": "group",
-                        "in": "query"
-                    }
-                ],
-                "responses": {
-                    "200": {
-                        "description": "OK",
-                        "schema": {
-                            "allOf": [
-                                {
-                                    "$ref": "#/definitions/middleware.APIResponse"
-                                },
-                                {
-                                    "type": "object",
-                                    "properties": {
-                                        "data": {
-                                            "type": "array",
-                                            "items": {
-                                                "type": "string"
-                                            }
-                                        }
-                                    }
-                                }
-                            ]
-                        }
-                    }
-                }
-            }
-        },
-        "/api/logs/used/token_names": {
-            "get": {
-                "security": [
-                    {
-                        "ApiKeyAuth": []
-                    }
-                ],
-                "description": "Get a list of token names that have been used in logs",
-                "produces": [
-                    "application/json"
-                ],
-                "tags": [
-                    "logs"
-                ],
-                "summary": "Get used token names",
-                "parameters": [
-                    {
-                        "type": "string",
-                        "description": "Group or *",
-                        "name": "group",
-                        "in": "query"
-                    }
-                ],
-                "responses": {
-                    "200": {
-                        "description": "OK",
-                        "schema": {
-                            "allOf": [
-                                {
-                                    "$ref": "#/definitions/middleware.APIResponse"
-                                },
-                                {
-                                    "type": "object",
-                                    "properties": {
-                                        "data": {
-                                            "type": "array",
-                                            "items": {
-                                                "type": "string"
-                                            }
-                                        }
-                                    }
-                                }
-                            ]
-                        }
-                    }
-                }
-            }
-        },
         "/api/mcp/group/all": {
             "get": {
                 "security": [
@@ -10270,6 +10215,56 @@
                 }
             }
         },
+        "model.ModelData": {
+            "type": "object",
+            "properties": {
+                "cache_creation_tokens": {
+                    "type": "integer"
+                },
+                "cached_tokens": {
+                    "type": "integer"
+                },
+                "exception_count": {
+                    "type": "integer"
+                },
+                "input_tokens": {
+                    "type": "integer"
+                },
+                "max_rpm": {
+                    "type": "integer"
+                },
+                "max_rps": {
+                    "type": "integer"
+                },
+                "max_tpm": {
+                    "type": "integer"
+                },
+                "max_tps": {
+                    "type": "integer"
+                },
+                "model": {
+                    "type": "string"
+                },
+                "output_tokens": {
+                    "type": "integer"
+                },
+                "request_count": {
+                    "type": "integer"
+                },
+                "timestamp": {
+                    "type": "integer"
+                },
+                "total_tokens": {
+                    "type": "integer"
+                },
+                "used_amount": {
+                    "type": "number"
+                },
+                "web_search_count": {
+                    "type": "integer"
+                }
+            }
+        },
         "model.ModelOwner": {
             "type": "string",
             "enum": [
@@ -10789,6 +10784,20 @@
                 }
             }
         },
+        "model.TimeModelData": {
+            "type": "object",
+            "properties": {
+                "models": {
+                    "type": "array",
+                    "items": {
+                        "$ref": "#/definitions/model.ModelData"
+                    }
+                },
+                "timestamp": {
+                    "type": "integer"
+                }
+            }
+        },
         "model.Tool": {
             "type": "object",
             "properties": {

+ 133 - 120
core/docs/swagger.yaml

@@ -1436,6 +1436,39 @@ definitions:
       updated_at:
         type: string
     type: object
+  model.ModelData:
+    properties:
+      cache_creation_tokens:
+        type: integer
+      cached_tokens:
+        type: integer
+      exception_count:
+        type: integer
+      input_tokens:
+        type: integer
+      max_rpm:
+        type: integer
+      max_rps:
+        type: integer
+      max_tpm:
+        type: integer
+      max_tps:
+        type: integer
+      model:
+        type: string
+      output_tokens:
+        type: integer
+      request_count:
+        type: integer
+      timestamp:
+        type: integer
+      total_tokens:
+        type: integer
+      used_amount:
+        type: number
+      web_search_count:
+        type: integer
+    type: object
   model.ModelOwner:
     enum:
     - openai
@@ -1803,6 +1836,15 @@ definitions:
     - model
     - voice
     type: object
+  model.TimeModelData:
+    properties:
+      models:
+        items:
+          $ref: '#/definitions/model.ModelData'
+        type: array
+      timestamp:
+        type: integer
+    type: object
   model.Tool:
     properties:
       function:
@@ -2467,10 +2509,6 @@ paths:
         in: query
         name: channel
         type: integer
-      - description: Type of time span (day, week, month, two_week)
-        in: query
-        name: type
-        type: string
       - description: Model name
         in: query
         name: model
@@ -2517,10 +2555,6 @@ paths:
         name: group
         required: true
         type: string
-      - description: Type of time span (day, week, month, two_week)
-        in: query
-        name: type
-        type: string
       - description: Token name
         in: query
         name: token_name
@@ -2590,6 +2624,97 @@ paths:
       summary: Get model usage data for a specific group
       tags:
       - dashboard
+  /api/dashboardv2/:
+    get:
+      description: Returns model-specific metrics and usage data for the given channel
+      parameters:
+      - description: Channel ID
+        in: query
+        name: channel
+        type: integer
+      - description: Start timestamp
+        in: query
+        name: start_timestamp
+        type: integer
+      - description: End timestamp
+        in: query
+        name: end_timestamp
+        type: integer
+      - description: Timezone, default is Local
+        in: query
+        name: timezone
+        type: string
+      - description: Time span type (day, hour)
+        in: query
+        name: timespan
+        type: string
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            allOf:
+            - $ref: '#/definitions/middleware.APIResponse'
+            - properties:
+                data:
+                  items:
+                    $ref: '#/definitions/model.TimeModelData'
+                  type: array
+              type: object
+      security:
+      - ApiKeyAuth: []
+      summary: Get model usage data for a specific channel
+      tags:
+      - dashboard
+  /api/dashboardv2/{group}:
+    get:
+      description: Returns model-specific metrics and usage data for the given group
+      parameters:
+      - description: Group
+        in: path
+        name: group
+        required: true
+        type: string
+      - description: Token name
+        in: query
+        name: token_name
+        type: string
+      - description: Start timestamp
+        in: query
+        name: start_timestamp
+        type: integer
+      - description: End timestamp
+        in: query
+        name: end_timestamp
+        type: integer
+      - description: Timezone, default is Local
+        in: query
+        name: timezone
+        type: string
+      - description: Time span type (day, hour)
+        in: query
+        name: timespan
+        type: string
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            allOf:
+            - $ref: '#/definitions/middleware.APIResponse'
+            - properties:
+                data:
+                  items:
+                    $ref: '#/definitions/model.TimeModelData'
+                  type: array
+              type: object
+      security:
+      - ApiKeyAuth: []
+      summary: Get model usage data for a specific group
+      tags:
+      - dashboard
   /api/embedmcp/:
     get:
       consumes:
@@ -3450,64 +3575,6 @@ paths:
       summary: Search group logs
       tags:
       - log
-  /api/log/{group}/used/models:
-    get:
-      description: Get a list of models that have been used in a specific group's
-        logs
-      parameters:
-      - description: Group name
-        in: path
-        name: group
-        required: true
-        type: string
-      produces:
-      - application/json
-      responses:
-        "200":
-          description: OK
-          schema:
-            allOf:
-            - $ref: '#/definitions/middleware.APIResponse'
-            - properties:
-                data:
-                  items:
-                    type: string
-                  type: array
-              type: object
-      security:
-      - ApiKeyAuth: []
-      summary: Get group used models
-      tags:
-      - log
-  /api/log/{group}/used/token_names:
-    get:
-      description: Get a list of token names that have been used in a specific group's
-        logs
-      parameters:
-      - description: Group name
-        in: path
-        name: group
-        required: true
-        type: string
-      produces:
-      - application/json
-      responses:
-        "200":
-          description: OK
-          schema:
-            allOf:
-            - $ref: '#/definitions/middleware.APIResponse'
-            - properties:
-                data:
-                  items:
-                    type: string
-                  type: array
-              type: object
-      security:
-      - ApiKeyAuth: []
-      summary: Get group used token names
-      tags:
-      - log
   /api/logs/:
     delete:
       description: Deletes logs older than the specified retention period
@@ -3814,60 +3881,6 @@ paths:
       summary: Search logs
       tags:
       - logs
-  /api/logs/used/models:
-    get:
-      description: Get a list of models that have been used in logs
-      parameters:
-      - description: Group or *
-        in: query
-        name: group
-        type: string
-      produces:
-      - application/json
-      responses:
-        "200":
-          description: OK
-          schema:
-            allOf:
-            - $ref: '#/definitions/middleware.APIResponse'
-            - properties:
-                data:
-                  items:
-                    type: string
-                  type: array
-              type: object
-      security:
-      - ApiKeyAuth: []
-      summary: Get used models
-      tags:
-      - logs
-  /api/logs/used/token_names:
-    get:
-      description: Get a list of token names that have been used in logs
-      parameters:
-      - description: Group or *
-        in: query
-        name: group
-        type: string
-      produces:
-      - application/json
-      responses:
-        "200":
-          description: OK
-          schema:
-            allOf:
-            - $ref: '#/definitions/middleware.APIResponse'
-            - properties:
-                data:
-                  items:
-                    type: string
-                  type: array
-              type: object
-      security:
-      - ApiKeyAuth: []
-      summary: Get used token names
-      tags:
-      - logs
   /api/mcp/group/{group}:
     get:
       description: Get a list of Group MCPs with pagination and filtering

+ 0 - 315
core/model/log.go

@@ -1,17 +1,14 @@
 package model
 
 import (
-	"cmp"
 	"errors"
 	"fmt"
-	"slices"
 	"strings"
 	"time"
 
 	"github.com/bytedance/sonic"
 	"github.com/labring/aiproxy/core/common"
 	"github.com/labring/aiproxy/core/common/config"
-	"github.com/shopspring/decimal"
 	"golang.org/x/sync/errgroup"
 	"gorm.io/gorm"
 )
@@ -1132,318 +1129,6 @@ func DeleteGroupLogs(groupID string) (int64, error) {
 	return result.RowsAffected, result.Error
 }
 
-type ChartData struct {
-	Timestamp           int64   `json:"timestamp"`
-	RequestCount        int64   `json:"request_count"`
-	UsedAmount          float64 `json:"used_amount"`
-	InputTokens         int64   `json:"input_tokens,omitempty"`
-	OutputTokens        int64   `json:"output_tokens,omitempty"`
-	CachedTokens        int64   `json:"cached_tokens,omitempty"`
-	CacheCreationTokens int64   `json:"cache_creation_tokens,omitempty"`
-	TotalTokens         int64   `json:"total_tokens,omitempty"`
-	ExceptionCount      int64   `json:"exception_count"`
-	WebSearchCount      int64   `json:"web_search_count,omitempty"`
-
-	MaxRPM int64 `json:"max_rpm,omitempty"`
-	MaxTPM int64 `json:"max_tpm,omitempty"`
-	MaxRPS int64 `json:"max_rps,omitempty"`
-	MaxTPS int64 `json:"max_tps,omitempty"`
-}
-
-type DashboardResponse struct {
-	ChartData      []*ChartData `json:"chart_data"`
-	TotalCount     int64        `json:"total_count"`
-	ExceptionCount int64        `json:"exception_count"`
-
-	RPM int64 `json:"rpm"`
-	TPM int64 `json:"tpm"`
-
-	MaxRPM int64 `json:"max_rpm,omitempty"`
-	MaxTPM int64 `json:"max_tpm,omitempty"`
-	MaxRPS int64 `json:"max_rps,omitempty"`
-	MaxTPS int64 `json:"max_tps,omitempty"`
-
-	UsedAmount          float64 `json:"used_amount"`
-	InputTokens         int64   `json:"input_tokens,omitempty"`
-	OutputTokens        int64   `json:"output_tokens,omitempty"`
-	TotalTokens         int64   `json:"total_tokens,omitempty"`
-	CachedTokens        int64   `json:"cached_tokens,omitempty"`
-	CacheCreationTokens int64   `json:"cache_creation_tokens,omitempty"`
-	WebSearchCount      int64   `json:"web_search_count,omitempty"`
-
-	Channels []int    `json:"channels,omitempty"`
-	Models   []string `json:"models,omitempty"`
-}
-
-type GroupDashboardResponse struct {
-	DashboardResponse
-	TokenNames []string `json:"token_names"`
-}
-
-type TimeSpanType string
-
-const (
-	TimeSpanDay  TimeSpanType = "day"
-	TimeSpanHour TimeSpanType = "hour"
-)
-
-// aggregateHourDataToDay converts hourly chart data into daily aggregated data
-func aggregateHourDataToDay(hourlyData []*ChartData, timezone *time.Location) []*ChartData {
-	dayData := make(map[int64]*ChartData)
-	if timezone == nil {
-		timezone = time.Local
-	}
-
-	for _, data := range hourlyData {
-		// Convert timestamp to time in the specified timezone
-		t := time.Unix(data.Timestamp, 0).In(timezone)
-		// Get the start of the day in the specified timezone
-		startOfDay := time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, timezone)
-		dayTimestamp := startOfDay.Unix()
-
-		if _, exists := dayData[dayTimestamp]; !exists {
-			dayData[dayTimestamp] = &ChartData{
-				Timestamp: dayTimestamp,
-			}
-		}
-
-		day := dayData[dayTimestamp]
-		day.RequestCount += data.RequestCount
-		day.UsedAmount = decimal.
-			NewFromFloat(data.UsedAmount).
-			Add(decimal.NewFromFloat(day.UsedAmount)).
-			InexactFloat64()
-		day.ExceptionCount += data.ExceptionCount
-		day.InputTokens += data.InputTokens
-		day.OutputTokens += data.OutputTokens
-		day.CachedTokens += data.CachedTokens
-		day.CacheCreationTokens += data.CacheCreationTokens
-		day.TotalTokens += data.TotalTokens
-		day.WebSearchCount += data.WebSearchCount
-
-		if data.MaxRPM > day.MaxRPM {
-			day.MaxRPM = data.MaxRPM
-		}
-		if data.MaxTPM > day.MaxTPM {
-			day.MaxTPM = data.MaxTPM
-		}
-		if data.MaxRPS > day.MaxRPS {
-			day.MaxRPS = data.MaxRPS
-		}
-		if data.MaxTPS > day.MaxTPS {
-			day.MaxTPS = data.MaxTPS
-		}
-	}
-
-	result := make([]*ChartData, 0, len(dayData))
-	for _, data := range dayData {
-		result = append(result, data)
-	}
-
-	slices.SortFunc(result, func(a, b *ChartData) int {
-		return cmp.Compare(a.Timestamp, b.Timestamp)
-	})
-
-	return result
-}
-
-func GetUsedChannelsFromLog(group string, start, end time.Time) ([]int, error) {
-	return getLogGroupByValuesFromLog[int]("channel_id", group, start, end)
-}
-
-func GetUsedModelsFromLog(group string, start, end time.Time) ([]string, error) {
-	return getLogGroupByValuesFromLog[string]("model", group, start, end)
-}
-
-func GetUsedTokenNamesFromLog(group string, start, end time.Time) ([]string, error) {
-	return getLogGroupByValuesFromLog[string]("token_name", group, start, end)
-}
-
-func getLogGroupByValuesFromLog[T cmp.Ordered](
-	field, group string,
-	start, end time.Time,
-) ([]T, error) {
-	var values []T
-	query := LogDB.
-		Model(&Log{})
-
-	if group == "" {
-		query = query.Where("group_id = ''")
-	} else if group != "*" {
-		query = query.Where("group_id = ?", group)
-	}
-
-	switch {
-	case !start.IsZero() && !end.IsZero():
-		query = query.Where("created_at BETWEEN ? AND ?", start, end)
-	case !start.IsZero():
-		query = query.Where("created_at >= ?", start)
-	case !end.IsZero():
-		query = query.Where("created_at <= ?", end)
-	}
-
-	err := query.
-		Select(field).
-		Group(field).
-		Pluck(field, &values).Error
-	if err != nil {
-		return nil, err
-	}
-	slices.Sort(values)
-	return values, nil
-}
-
-func sumDashboardResponse(chartData []*ChartData) DashboardResponse {
-	dashboardResponse := DashboardResponse{
-		ChartData: chartData,
-	}
-	usedAmount := decimal.NewFromFloat(0)
-	for _, data := range chartData {
-		dashboardResponse.TotalCount += data.RequestCount
-		dashboardResponse.ExceptionCount += data.ExceptionCount
-
-		usedAmount = usedAmount.Add(decimal.NewFromFloat(data.UsedAmount))
-		dashboardResponse.InputTokens += data.InputTokens
-		dashboardResponse.OutputTokens += data.OutputTokens
-		dashboardResponse.TotalTokens += data.TotalTokens
-		dashboardResponse.CachedTokens += data.CachedTokens
-		dashboardResponse.CacheCreationTokens += data.CacheCreationTokens
-		dashboardResponse.WebSearchCount += data.WebSearchCount
-
-		if data.MaxRPM > dashboardResponse.MaxRPM {
-			dashboardResponse.MaxRPM = data.MaxRPM
-		}
-		if data.MaxTPM > dashboardResponse.MaxTPM {
-			dashboardResponse.MaxTPM = data.MaxTPM
-		}
-		if data.MaxRPS > dashboardResponse.MaxRPS {
-			dashboardResponse.MaxRPS = data.MaxRPS
-		}
-		if data.MaxTPS > dashboardResponse.MaxTPS {
-			dashboardResponse.MaxTPS = data.MaxTPS
-		}
-	}
-	dashboardResponse.UsedAmount = usedAmount.InexactFloat64()
-	return dashboardResponse
-}
-
-func GetDashboardData(
-	start,
-	end time.Time,
-	modelName string,
-	channelID int,
-	timeSpan TimeSpanType,
-	timezone *time.Location,
-) (*DashboardResponse, error) {
-	if end.IsZero() {
-		end = time.Now()
-	} else if end.Before(start) {
-		return nil, errors.New("end time is before start time")
-	}
-
-	var (
-		chartData []*ChartData
-		channels  []int
-		models    []string
-	)
-
-	g := new(errgroup.Group)
-
-	g.Go(func() error {
-		var err error
-		chartData, err = getChartData("*", start, end, "", modelName, channelID, timeSpan, timezone)
-		return err
-	})
-
-	g.Go(func() error {
-		var err error
-		channels, err = GetUsedChannels("*", start, end)
-		return err
-	})
-
-	g.Go(func() error {
-		var err error
-		models, err = GetUsedModels("*", "", start, end)
-		return err
-	})
-
-	if err := g.Wait(); err != nil {
-		return nil, err
-	}
-
-	dashboardResponse := sumDashboardResponse(chartData)
-	dashboardResponse.Channels = channels
-	dashboardResponse.Models = models
-
-	return &dashboardResponse, nil
-}
-
-func GetGroupDashboardData(
-	group string,
-	start, end time.Time,
-	tokenName string,
-	modelName string,
-	timeSpan TimeSpanType,
-	timezone *time.Location,
-) (*GroupDashboardResponse, error) {
-	if group == "" || group == "*" {
-		return nil, errors.New("group is required")
-	}
-
-	if end.IsZero() {
-		end = time.Now()
-	} else if end.Before(start) {
-		return nil, errors.New("end time is before start time")
-	}
-
-	var (
-		chartData  []*ChartData
-		tokenNames []string
-		models     []string
-	)
-
-	g := new(errgroup.Group)
-
-	g.Go(func() error {
-		var err error
-		chartData, err = getChartData(
-			group,
-			start,
-			end,
-			tokenName,
-			modelName,
-			0,
-			timeSpan,
-			timezone,
-		)
-		return err
-	})
-
-	g.Go(func() error {
-		var err error
-		tokenNames, err = GetUsedTokenNames(group, start, end)
-		return err
-	})
-
-	g.Go(func() error {
-		var err error
-		models, err = GetUsedModels(group, tokenName, start, end)
-		return err
-	})
-
-	if err := g.Wait(); err != nil {
-		return nil, err
-	}
-
-	dashboardResponse := sumDashboardResponse(chartData)
-	dashboardResponse.Models = models
-
-	return &GroupDashboardResponse{
-		DashboardResponse: dashboardResponse,
-		TokenNames:        tokenNames,
-	}, nil
-}
-
 func GetIPGroups(threshold int, start, end time.Time) (map[string][]string, error) {
 	if threshold < 1 {
 		threshold = 1

+ 523 - 0
core/model/summary.go

@@ -7,6 +7,8 @@ import (
 	"slices"
 	"time"
 
+	"github.com/shopspring/decimal"
+	"golang.org/x/sync/errgroup"
 	"gorm.io/gorm"
 	"gorm.io/gorm/clause"
 )
@@ -455,3 +457,524 @@ func GetModelCostRank(
 
 	return ranks, nil
 }
+
+type ChartData struct {
+	Timestamp           int64   `json:"timestamp"`
+	RequestCount        int64   `json:"request_count"`
+	UsedAmount          float64 `json:"used_amount"`
+	InputTokens         int64   `json:"input_tokens,omitempty"`
+	OutputTokens        int64   `json:"output_tokens,omitempty"`
+	CachedTokens        int64   `json:"cached_tokens,omitempty"`
+	CacheCreationTokens int64   `json:"cache_creation_tokens,omitempty"`
+	TotalTokens         int64   `json:"total_tokens,omitempty"`
+	ExceptionCount      int64   `json:"exception_count"`
+	WebSearchCount      int64   `json:"web_search_count,omitempty"`
+
+	MaxRPM int64 `json:"max_rpm,omitempty"`
+	MaxTPM int64 `json:"max_tpm,omitempty"`
+	MaxRPS int64 `json:"max_rps,omitempty"`
+	MaxTPS int64 `json:"max_tps,omitempty"`
+}
+
+type DashboardResponse struct {
+	ChartData      []*ChartData `json:"chart_data"`
+	TotalCount     int64        `json:"total_count"`
+	ExceptionCount int64        `json:"exception_count"`
+
+	RPM int64 `json:"rpm"`
+	TPM int64 `json:"tpm"`
+
+	MaxRPM int64 `json:"max_rpm,omitempty"`
+	MaxTPM int64 `json:"max_tpm,omitempty"`
+	MaxRPS int64 `json:"max_rps,omitempty"`
+	MaxTPS int64 `json:"max_tps,omitempty"`
+
+	UsedAmount          float64 `json:"used_amount"`
+	InputTokens         int64   `json:"input_tokens,omitempty"`
+	OutputTokens        int64   `json:"output_tokens,omitempty"`
+	TotalTokens         int64   `json:"total_tokens,omitempty"`
+	CachedTokens        int64   `json:"cached_tokens,omitempty"`
+	CacheCreationTokens int64   `json:"cache_creation_tokens,omitempty"`
+	WebSearchCount      int64   `json:"web_search_count,omitempty"`
+
+	Channels []int    `json:"channels,omitempty"`
+	Models   []string `json:"models,omitempty"`
+}
+
+type GroupDashboardResponse struct {
+	DashboardResponse
+	TokenNames []string `json:"token_names"`
+}
+
+type TimeSpanType string
+
+const (
+	TimeSpanDay  TimeSpanType = "day"
+	TimeSpanHour TimeSpanType = "hour"
+)
+
+// aggregateHourDataToDay converts hourly chart data into daily aggregated data
+func aggregateHourDataToDay(hourlyData []*ChartData, timezone *time.Location) []*ChartData {
+	dayData := make(map[int64]*ChartData)
+	if timezone == nil {
+		timezone = time.Local
+	}
+
+	for _, data := range hourlyData {
+		// Convert timestamp to time in the specified timezone
+		t := time.Unix(data.Timestamp, 0).In(timezone)
+		// Get the start of the day in the specified timezone
+		startOfDay := time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, timezone)
+		dayTimestamp := startOfDay.Unix()
+
+		if _, exists := dayData[dayTimestamp]; !exists {
+			dayData[dayTimestamp] = &ChartData{
+				Timestamp: dayTimestamp,
+			}
+		}
+
+		day := dayData[dayTimestamp]
+		day.RequestCount += data.RequestCount
+		day.UsedAmount = decimal.
+			NewFromFloat(data.UsedAmount).
+			Add(decimal.NewFromFloat(day.UsedAmount)).
+			InexactFloat64()
+		day.ExceptionCount += data.ExceptionCount
+		day.InputTokens += data.InputTokens
+		day.OutputTokens += data.OutputTokens
+		day.CachedTokens += data.CachedTokens
+		day.CacheCreationTokens += data.CacheCreationTokens
+		day.TotalTokens += data.TotalTokens
+		day.WebSearchCount += data.WebSearchCount
+
+		if data.MaxRPM > day.MaxRPM {
+			day.MaxRPM = data.MaxRPM
+		}
+		if data.MaxTPM > day.MaxTPM {
+			day.MaxTPM = data.MaxTPM
+		}
+		if data.MaxRPS > day.MaxRPS {
+			day.MaxRPS = data.MaxRPS
+		}
+		if data.MaxTPS > day.MaxTPS {
+			day.MaxTPS = data.MaxTPS
+		}
+	}
+
+	result := make([]*ChartData, 0, len(dayData))
+	for _, data := range dayData {
+		result = append(result, data)
+	}
+
+	slices.SortFunc(result, func(a, b *ChartData) int {
+		return cmp.Compare(a.Timestamp, b.Timestamp)
+	})
+
+	return result
+}
+
+func sumDashboardResponse(chartData []*ChartData) DashboardResponse {
+	dashboardResponse := DashboardResponse{
+		ChartData: chartData,
+	}
+	usedAmount := decimal.NewFromFloat(0)
+	for _, data := range chartData {
+		dashboardResponse.TotalCount += data.RequestCount
+		dashboardResponse.ExceptionCount += data.ExceptionCount
+
+		usedAmount = usedAmount.Add(decimal.NewFromFloat(data.UsedAmount))
+		dashboardResponse.InputTokens += data.InputTokens
+		dashboardResponse.OutputTokens += data.OutputTokens
+		dashboardResponse.TotalTokens += data.TotalTokens
+		dashboardResponse.CachedTokens += data.CachedTokens
+		dashboardResponse.CacheCreationTokens += data.CacheCreationTokens
+		dashboardResponse.WebSearchCount += data.WebSearchCount
+
+		if data.MaxRPM > dashboardResponse.MaxRPM {
+			dashboardResponse.MaxRPM = data.MaxRPM
+		}
+		if data.MaxTPM > dashboardResponse.MaxTPM {
+			dashboardResponse.MaxTPM = data.MaxTPM
+		}
+		if data.MaxRPS > dashboardResponse.MaxRPS {
+			dashboardResponse.MaxRPS = data.MaxRPS
+		}
+		if data.MaxTPS > dashboardResponse.MaxTPS {
+			dashboardResponse.MaxTPS = data.MaxTPS
+		}
+	}
+	dashboardResponse.UsedAmount = usedAmount.InexactFloat64()
+	return dashboardResponse
+}
+
+func GetDashboardData(
+	start,
+	end time.Time,
+	modelName string,
+	channelID int,
+	timeSpan TimeSpanType,
+	timezone *time.Location,
+) (*DashboardResponse, error) {
+	if end.IsZero() {
+		end = time.Now()
+	} else if end.Before(start) {
+		return nil, errors.New("end time is before start time")
+	}
+
+	var (
+		chartData []*ChartData
+		channels  []int
+		models    []string
+	)
+
+	g := new(errgroup.Group)
+
+	g.Go(func() error {
+		var err error
+		chartData, err = getChartData("*", start, end, "", modelName, channelID, timeSpan, timezone)
+		return err
+	})
+
+	g.Go(func() error {
+		var err error
+		channels, err = GetUsedChannels("*", start, end)
+		return err
+	})
+
+	g.Go(func() error {
+		var err error
+		models, err = GetUsedModels("*", "", start, end)
+		return err
+	})
+
+	if err := g.Wait(); err != nil {
+		return nil, err
+	}
+
+	dashboardResponse := sumDashboardResponse(chartData)
+	dashboardResponse.Channels = channels
+	dashboardResponse.Models = models
+
+	return &dashboardResponse, nil
+}
+
+func GetGroupDashboardData(
+	group string,
+	start, end time.Time,
+	tokenName string,
+	modelName string,
+	timeSpan TimeSpanType,
+	timezone *time.Location,
+) (*GroupDashboardResponse, error) {
+	if group == "" || group == "*" {
+		return nil, errors.New("group is required")
+	}
+
+	if end.IsZero() {
+		end = time.Now()
+	} else if end.Before(start) {
+		return nil, errors.New("end time is before start time")
+	}
+
+	var (
+		chartData  []*ChartData
+		tokenNames []string
+		models     []string
+	)
+
+	g := new(errgroup.Group)
+
+	g.Go(func() error {
+		var err error
+		chartData, err = getChartData(
+			group,
+			start,
+			end,
+			tokenName,
+			modelName,
+			0,
+			timeSpan,
+			timezone,
+		)
+		return err
+	})
+
+	g.Go(func() error {
+		var err error
+		tokenNames, err = GetUsedTokenNames(group, start, end)
+		return err
+	})
+
+	g.Go(func() error {
+		var err error
+		models, err = GetUsedModels(group, tokenName, start, end)
+		return err
+	})
+
+	if err := g.Wait(); err != nil {
+		return nil, err
+	}
+
+	dashboardResponse := sumDashboardResponse(chartData)
+	dashboardResponse.Models = models
+
+	return &GroupDashboardResponse{
+		DashboardResponse: dashboardResponse,
+		TokenNames:        tokenNames,
+	}, nil
+}
+
+//nolint:revive
+type ModelData struct {
+	Timestamp           int64   `json:"timestamp,omitempty"`
+	Model               string  `json:"model"`
+	RequestCount        int64   `json:"request_count"`
+	UsedAmount          float64 `json:"used_amount"`
+	ExceptionCount      int64   `json:"exception_count"`
+	InputTokens         int64   `json:"input_tokens,omitempty"`
+	OutputTokens        int64   `json:"output_tokens,omitempty"`
+	CachedTokens        int64   `json:"cached_tokens,omitempty"`
+	CacheCreationTokens int64   `json:"cache_creation_tokens,omitempty"`
+	TotalTokens         int64   `json:"total_tokens,omitempty"`
+	WebSearchCount      int64   `json:"web_search_count,omitempty"`
+	MaxRPM              int64   `json:"max_rpm,omitempty"`
+	MaxRPS              int64   `json:"max_rps,omitempty"`
+	MaxTPM              int64   `json:"max_tpm,omitempty"`
+	MaxTPS              int64   `json:"max_tps,omitempty"`
+}
+
+type TimeModelData struct {
+	Timestamp int64        `json:"timestamp"`
+	Models    []*ModelData `json:"models"`
+}
+
+func GetTimeSeriesModelData(
+	channelID int,
+	start, end time.Time,
+	timeSpan TimeSpanType,
+	timezone *time.Location,
+) ([]*TimeModelData, error) {
+	if end.IsZero() {
+		end = time.Now()
+	} else if end.Before(start) {
+		return nil, errors.New("end time is before start time")
+	}
+
+	query := LogDB.Model(&Summary{})
+
+	if channelID != 0 {
+		query = query.Where("channel_id = ?", channelID)
+	}
+
+	switch {
+	case !start.IsZero() && !end.IsZero():
+		query = query.Where("hour_timestamp BETWEEN ? AND ?", start.Unix(), end.Unix())
+	case !start.IsZero():
+		query = query.Where("hour_timestamp >= ?", start.Unix())
+	case !end.IsZero():
+		query = query.Where("hour_timestamp <= ?", end.Unix())
+	}
+
+	selectFields := "hour_timestamp as timestamp, model, " +
+		"sum(request_count) as request_count, sum(used_amount) as used_amount, " +
+		"sum(exception_count) as exception_count, sum(input_tokens) as input_tokens, " +
+		"sum(output_tokens) as output_tokens, sum(cached_tokens) as cached_tokens, " +
+		"sum(cache_creation_tokens) as cache_creation_tokens, sum(total_tokens) as total_tokens, " +
+		"sum(web_search_count) as web_search_count"
+
+	if channelID != 0 {
+		selectFields += ", max(max_rpm) as max_rpm, max(max_rps) as max_rps, max(max_tpm) as max_tpm, max(max_tps) as max_tps"
+	} else {
+		selectFields += ", 0 as max_rpm, 0 as max_rps, 0 as max_tpm, 0 as max_tps"
+	}
+
+	var rawData []ModelData
+	err := query.
+		Select(selectFields).
+		Group("timestamp, model").
+		Order("timestamp ASC").
+		Scan(&rawData).Error
+	if err != nil {
+		return nil, err
+	}
+
+	if timeSpan == TimeSpanDay && len(rawData) > 0 {
+		rawData = aggregateHourlyToDaily(rawData, timezone)
+	}
+
+	return convertToTimeModelData(rawData), nil
+}
+
+func GetGroupTimeSeriesModelData(
+	group string,
+	tokenName string,
+	start, end time.Time,
+	timeSpan TimeSpanType,
+	timezone *time.Location,
+) ([]*TimeModelData, error) {
+	if end.IsZero() {
+		end = time.Now()
+	} else if end.Before(start) {
+		return nil, errors.New("end time is before start time")
+	}
+
+	query := LogDB.Model(&GroupSummary{}).
+		Where("group_id = ?", group)
+	if tokenName != "" {
+		query = query.Where("token_name = ?", tokenName)
+	}
+
+	switch {
+	case !start.IsZero() && !end.IsZero():
+		query = query.Where("hour_timestamp BETWEEN ? AND ?", start.Unix(), end.Unix())
+	case !start.IsZero():
+		query = query.Where("hour_timestamp >= ?", start.Unix())
+	case !end.IsZero():
+		query = query.Where("hour_timestamp <= ?", end.Unix())
+	}
+
+	selectFields := "hour_timestamp as timestamp, model, " +
+		"sum(request_count) as request_count, sum(used_amount) as used_amount, " +
+		"sum(exception_count) as exception_count, sum(input_tokens) as input_tokens, " +
+		"sum(output_tokens) as output_tokens, sum(cached_tokens) as cached_tokens, " +
+		"sum(cache_creation_tokens) as cache_creation_tokens, sum(total_tokens) as total_tokens, " +
+		"sum(web_search_count) as web_search_count"
+
+	if tokenName != "" {
+		selectFields += ", max(max_rpm) as max_rpm, max(max_rps) as max_rps, max(max_tpm) as max_tpm, max(max_tps) as max_tps"
+	} else {
+		selectFields += ", 0 as max_rpm, 0 as max_rps, 0 as max_tpm, 0 as max_tps"
+	}
+
+	var rawData []ModelData
+	err := query.
+		Select(selectFields).
+		Group("timestamp, model").
+		Order("timestamp ASC").
+		Scan(&rawData).Error
+	if err != nil {
+		return nil, err
+	}
+
+	if timeSpan == TimeSpanDay && len(rawData) > 0 {
+		rawData = aggregateHourlyToDaily(rawData, timezone)
+	}
+
+	return convertToTimeModelData(rawData), nil
+}
+
+func aggregateHourlyToDaily(hourlyData []ModelData, timezone *time.Location) []ModelData {
+	if timezone == nil {
+		timezone = time.Local
+	}
+
+	type AggKey struct {
+		DayTimestamp int64
+		Model        string
+	}
+	dayData := make(map[AggKey]*ModelData)
+
+	for _, data := range hourlyData {
+		t := time.Unix(data.Timestamp, 0).In(timezone)
+		startOfDay := time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, timezone)
+		dayTimestamp := startOfDay.Unix()
+
+		key := AggKey{
+			DayTimestamp: dayTimestamp,
+			Model:        data.Model,
+		}
+
+		if _, exists := dayData[key]; !exists {
+			dayData[key] = &ModelData{
+				Timestamp: dayTimestamp,
+				Model:     data.Model,
+			}
+		}
+
+		day := dayData[key]
+		day.RequestCount += data.RequestCount
+		day.UsedAmount = decimal.
+			NewFromFloat(data.UsedAmount).
+			Add(decimal.NewFromFloat(day.UsedAmount)).
+			InexactFloat64()
+		day.ExceptionCount += data.ExceptionCount
+		day.InputTokens += data.InputTokens
+		day.OutputTokens += data.OutputTokens
+		day.CachedTokens += data.CachedTokens
+		day.CacheCreationTokens += data.CacheCreationTokens
+		day.TotalTokens += data.TotalTokens
+		day.WebSearchCount += data.WebSearchCount
+
+		if data.MaxRPM > day.MaxRPM {
+			day.MaxRPM = data.MaxRPM
+		}
+		if data.MaxTPM > day.MaxTPM {
+			day.MaxTPM = data.MaxTPM
+		}
+		if data.MaxRPS > day.MaxRPS {
+			day.MaxRPS = data.MaxRPS
+		}
+		if data.MaxTPS > day.MaxTPS {
+			day.MaxTPS = data.MaxTPS
+		}
+	}
+
+	result := make([]ModelData, 0, len(dayData))
+	for _, data := range dayData {
+		result = append(result, *data)
+	}
+
+	return result
+}
+
+func convertToTimeModelData(rawData []ModelData) []*TimeModelData {
+	timeMap := make(map[int64][]*ModelData)
+
+	for _, data := range rawData {
+		modelData := &ModelData{
+			Model:               data.Model,
+			RequestCount:        data.RequestCount,
+			UsedAmount:          data.UsedAmount,
+			ExceptionCount:      data.ExceptionCount,
+			InputTokens:         data.InputTokens,
+			OutputTokens:        data.OutputTokens,
+			CachedTokens:        data.CachedTokens,
+			CacheCreationTokens: data.CacheCreationTokens,
+			TotalTokens:         data.TotalTokens,
+			WebSearchCount:      data.WebSearchCount,
+			MaxRPM:              data.MaxRPM,
+			MaxRPS:              data.MaxRPS,
+			MaxTPM:              data.MaxTPM,
+			MaxTPS:              data.MaxTPS,
+		}
+
+		timeMap[data.Timestamp] = append(timeMap[data.Timestamp], modelData)
+	}
+
+	result := make([]*TimeModelData, 0, len(timeMap))
+	for timestamp, models := range timeMap {
+		slices.SortFunc(models, func(a, b *ModelData) int {
+			if a.UsedAmount != b.UsedAmount {
+				return cmp.Compare(b.UsedAmount, a.UsedAmount)
+			}
+			if a.TotalTokens != b.TotalTokens {
+				return cmp.Compare(b.TotalTokens, a.TotalTokens)
+			}
+			if a.RequestCount != b.RequestCount {
+				return cmp.Compare(b.RequestCount, a.RequestCount)
+			}
+			return cmp.Compare(a.Model, b.Model)
+		})
+
+		result = append(result, &TimeModelData{
+			Timestamp: timestamp,
+			Models:    models,
+		})
+	}
+
+	slices.SortFunc(result, func(a, b *TimeModelData) int {
+		return cmp.Compare(a.Timestamp, b.Timestamp)
+	})
+
+	return result
+}

+ 6 - 4
core/router/api.go

@@ -47,6 +47,12 @@ func SetAPIRouter(router *gin.Engine) {
 			modelCostRankRoute.GET("/:group", controller.GetGroupModelCostRank)
 		}
 
+		dashboardV2Route := apiRouter.Group("/dashboardv2")
+		{
+			dashboardV2Route.GET("/", controller.GetTimeSeriesModelData)
+			dashboardV2Route.GET("/:group", controller.GetGroupTimeSeriesModelData)
+		}
+
 		groupsRoute := apiRouter.Group("/groups")
 		{
 			groupsRoute.GET("/", controller.GetGroups)
@@ -147,16 +153,12 @@ func SetAPIRouter(router *gin.Engine) {
 			logsRoute.GET("/search", controller.SearchLogs)
 			logsRoute.GET("/consume_error", controller.SearchConsumeError)
 			logsRoute.GET("/detail/:log_id", controller.GetLogDetail)
-			logsRoute.GET("/used/models", controller.GetUsedModels)
-			logsRoute.GET("/used/token_names", controller.GetUsedTokenNames)
 		}
 		logRoute := apiRouter.Group("/log")
 		{
 			logRoute.GET("/:group", controller.GetGroupLogs)
 			logRoute.GET("/:group/search", controller.SearchGroupLogs)
 			logRoute.GET("/:group/detail/:log_id", controller.GetGroupLogDetail)
-			logRoute.GET("/:group/used/models", controller.GetGroupUsedModels)
-			logRoute.GET("/:group/used/token_names", controller.GetGroupUsedTokenNames)
 		}
 
 		modelConfigsRoute := apiRouter.Group("/model_configs")