mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-03 04:50:52 +08:00
feat(usage): add in-memory usage statistics tracking and API endpoint
- Introduced in-memory request statistics aggregation in `LoggerPlugin`. - Added new structures for detailed metrics collection (e.g., token breakdown, request success/failure). - Implemented `/usage` management API endpoint for retrieving aggregated statistics. - Updated management handlers to support the new usage statistics functionality. - Enhanced documentation to describe the usage metrics API.
This commit is contained in:
@@ -32,6 +32,61 @@ If a plaintext key is detected in the config at startup, it will be bcrypt‑has
|
|||||||
|
|
||||||
## Endpoints
|
## Endpoints
|
||||||
|
|
||||||
|
### Usage Statistics
|
||||||
|
- GET `/usage` — Retrieve aggregated in-memory request metrics
|
||||||
|
- Response:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"usage": {
|
||||||
|
"total_requests": 24,
|
||||||
|
"success_count": 22,
|
||||||
|
"failure_count": 2,
|
||||||
|
"total_tokens": 13890,
|
||||||
|
"requests_by_day": {
|
||||||
|
"2024-05-20": 12
|
||||||
|
},
|
||||||
|
"requests_by_hour": {
|
||||||
|
"09": 4,
|
||||||
|
"18": 8
|
||||||
|
},
|
||||||
|
"tokens_by_day": {
|
||||||
|
"2024-05-20": 9876
|
||||||
|
},
|
||||||
|
"tokens_by_hour": {
|
||||||
|
"09": 1234,
|
||||||
|
"18": 865
|
||||||
|
},
|
||||||
|
"apis": {
|
||||||
|
"POST /v1/chat/completions": {
|
||||||
|
"total_requests": 12,
|
||||||
|
"total_tokens": 9021,
|
||||||
|
"models": {
|
||||||
|
"gpt-4o-mini": {
|
||||||
|
"total_requests": 8,
|
||||||
|
"total_tokens": 7123,
|
||||||
|
"details": [
|
||||||
|
{
|
||||||
|
"timestamp": "2024-05-20T09:15:04.123456Z",
|
||||||
|
"tokens": {
|
||||||
|
"input_tokens": 523,
|
||||||
|
"output_tokens": 308,
|
||||||
|
"reasoning_tokens": 0,
|
||||||
|
"cached_tokens": 0,
|
||||||
|
"total_tokens": 831
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- Notes:
|
||||||
|
- Statistics are recalculated for every request that reports token usage; data resets when the server restarts.
|
||||||
|
- Hourly counters fold all days into the same hour bucket (`00`–`23`).
|
||||||
|
|
||||||
### Config
|
### Config
|
||||||
- GET `/config` — Get the full config
|
- GET `/config` — Get the full config
|
||||||
- Request:
|
- Request:
|
||||||
|
|||||||
@@ -32,6 +32,61 @@
|
|||||||
|
|
||||||
## 端点说明
|
## 端点说明
|
||||||
|
|
||||||
|
### Usage(请求统计)
|
||||||
|
- GET `/usage` — 获取内存中的请求统计
|
||||||
|
- 响应:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"usage": {
|
||||||
|
"total_requests": 24,
|
||||||
|
"success_count": 22,
|
||||||
|
"failure_count": 2,
|
||||||
|
"total_tokens": 13890,
|
||||||
|
"requests_by_day": {
|
||||||
|
"2024-05-20": 12
|
||||||
|
},
|
||||||
|
"requests_by_hour": {
|
||||||
|
"09": 4,
|
||||||
|
"18": 8
|
||||||
|
},
|
||||||
|
"tokens_by_day": {
|
||||||
|
"2024-05-20": 9876
|
||||||
|
},
|
||||||
|
"tokens_by_hour": {
|
||||||
|
"09": 1234,
|
||||||
|
"18": 865
|
||||||
|
},
|
||||||
|
"apis": {
|
||||||
|
"POST /v1/chat/completions": {
|
||||||
|
"total_requests": 12,
|
||||||
|
"total_tokens": 9021,
|
||||||
|
"models": {
|
||||||
|
"gpt-4o-mini": {
|
||||||
|
"total_requests": 8,
|
||||||
|
"total_tokens": 7123,
|
||||||
|
"details": [
|
||||||
|
{
|
||||||
|
"timestamp": "2024-05-20T09:15:04.123456Z",
|
||||||
|
"tokens": {
|
||||||
|
"input_tokens": 523,
|
||||||
|
"output_tokens": 308,
|
||||||
|
"reasoning_tokens": 0,
|
||||||
|
"cached_tokens": 0,
|
||||||
|
"total_tokens": 831
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- 说明:
|
||||||
|
- 仅统计带有 token 使用信息的请求,服务重启后数据会被清空。
|
||||||
|
- 小时维度会将所有日期折叠到 `00`–`23` 的统一小时桶中。
|
||||||
|
|
||||||
### Config
|
### Config
|
||||||
- GET `/config` — 获取完整的配置
|
- GET `/config` — 获取完整的配置
|
||||||
- 请求:
|
- 请求:
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
||||||
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/usage"
|
||||||
coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
|
coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
)
|
)
|
||||||
@@ -29,11 +30,18 @@ type Handler struct {
|
|||||||
attemptsMu sync.Mutex
|
attemptsMu sync.Mutex
|
||||||
failedAttempts map[string]*attemptInfo // keyed by client IP
|
failedAttempts map[string]*attemptInfo // keyed by client IP
|
||||||
authManager *coreauth.Manager
|
authManager *coreauth.Manager
|
||||||
|
usageStats *usage.RequestStatistics
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewHandler creates a new management handler instance.
|
// NewHandler creates a new management handler instance.
|
||||||
func NewHandler(cfg *config.Config, configFilePath string, manager *coreauth.Manager) *Handler {
|
func NewHandler(cfg *config.Config, configFilePath string, manager *coreauth.Manager) *Handler {
|
||||||
return &Handler{cfg: cfg, configFilePath: configFilePath, failedAttempts: make(map[string]*attemptInfo), authManager: manager}
|
return &Handler{
|
||||||
|
cfg: cfg,
|
||||||
|
configFilePath: configFilePath,
|
||||||
|
failedAttempts: make(map[string]*attemptInfo),
|
||||||
|
authManager: manager,
|
||||||
|
usageStats: usage.GetRequestStatistics(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetConfig updates the in-memory config reference when the server hot-reloads.
|
// SetConfig updates the in-memory config reference when the server hot-reloads.
|
||||||
@@ -42,6 +50,9 @@ func (h *Handler) SetConfig(cfg *config.Config) { h.cfg = cfg }
|
|||||||
// SetAuthManager updates the auth manager reference used by management endpoints.
|
// SetAuthManager updates the auth manager reference used by management endpoints.
|
||||||
func (h *Handler) SetAuthManager(manager *coreauth.Manager) { h.authManager = manager }
|
func (h *Handler) SetAuthManager(manager *coreauth.Manager) { h.authManager = manager }
|
||||||
|
|
||||||
|
// SetUsageStatistics allows replacing the usage statistics reference.
|
||||||
|
func (h *Handler) SetUsageStatistics(stats *usage.RequestStatistics) { h.usageStats = stats }
|
||||||
|
|
||||||
// Middleware enforces access control for management endpoints.
|
// Middleware enforces access control for management endpoints.
|
||||||
// All requests (local and remote) require a valid management key.
|
// All requests (local and remote) require a valid management key.
|
||||||
// Additionally, remote access requires allow-remote-management=true.
|
// Additionally, remote access requires allow-remote-management=true.
|
||||||
|
|||||||
17
internal/api/handlers/management/usage.go
Normal file
17
internal/api/handlers/management/usage.go
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
package management
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/usage"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetUsageStatistics returns the in-memory request statistics snapshot.
|
||||||
|
func (h *Handler) GetUsageStatistics(c *gin.Context) {
|
||||||
|
var snapshot usage.StatisticsSnapshot
|
||||||
|
if h != nil && h.usageStats != nil {
|
||||||
|
snapshot = h.usageStats.Snapshot()
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{"usage": snapshot})
|
||||||
|
}
|
||||||
@@ -271,6 +271,7 @@ func (s *Server) setupRoutes() {
|
|||||||
mgmt := s.engine.Group("/v0/management")
|
mgmt := s.engine.Group("/v0/management")
|
||||||
mgmt.Use(s.mgmt.Middleware())
|
mgmt.Use(s.mgmt.Middleware())
|
||||||
{
|
{
|
||||||
|
mgmt.GET("/usage", s.mgmt.GetUsageStatistics)
|
||||||
mgmt.GET("/config", s.mgmt.GetConfig)
|
mgmt.GET("/config", s.mgmt.GetConfig)
|
||||||
|
|
||||||
mgmt.GET("/debug", s.mgmt.GetDebug)
|
mgmt.GET("/debug", s.mgmt.GetDebug)
|
||||||
|
|||||||
@@ -5,36 +5,316 @@ package usage
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
coreusage "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/usage"
|
coreusage "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/usage"
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
coreusage.RegisterPlugin(NewLoggerPlugin())
|
coreusage.RegisterPlugin(NewLoggerPlugin())
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoggerPlugin outputs every usage record to the application log.
|
// LoggerPlugin collects in-memory request statistics for usage analysis.
|
||||||
// It implements the coreusage.Plugin interface to provide usage tracking
|
// It implements coreusage.Plugin to receive usage records emitted by the runtime.
|
||||||
// and logging capabilities for monitoring API consumption.
|
type LoggerPlugin struct {
|
||||||
type LoggerPlugin struct{}
|
stats *RequestStatistics
|
||||||
|
}
|
||||||
|
|
||||||
// NewLoggerPlugin constructs a new logger plugin instance.
|
// NewLoggerPlugin constructs a new logger plugin instance.
|
||||||
//
|
//
|
||||||
// Returns:
|
// Returns:
|
||||||
// - *LoggerPlugin: A new logger plugin instance
|
// - *LoggerPlugin: A new logger plugin instance wired to the shared statistics store.
|
||||||
func NewLoggerPlugin() *LoggerPlugin { return &LoggerPlugin{} }
|
func NewLoggerPlugin() *LoggerPlugin { return &LoggerPlugin{stats: defaultRequestStatistics} }
|
||||||
|
|
||||||
// HandleUsage implements coreusage.Plugin.
|
// HandleUsage implements coreusage.Plugin.
|
||||||
// It processes usage records by marshaling them to JSON and logging them
|
// It updates the in-memory statistics store whenever a usage record is received.
|
||||||
// at debug level for observability purposes.
|
|
||||||
//
|
//
|
||||||
// Parameters:
|
// Parameters:
|
||||||
// - ctx: The context for the usage record
|
// - ctx: The context for the usage record
|
||||||
// - record: The usage record to process and log
|
// - record: The usage record to aggregate
|
||||||
func (p *LoggerPlugin) HandleUsage(ctx context.Context, record coreusage.Record) {
|
func (p *LoggerPlugin) HandleUsage(ctx context.Context, record coreusage.Record) {
|
||||||
// Output all relevant fields for observability; keep logging lightweight and non-blocking.
|
if p == nil || p.stats == nil {
|
||||||
data, _ := json.Marshal(record)
|
return
|
||||||
log.Debug(string(data))
|
}
|
||||||
|
p.stats.Record(ctx, record)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RequestStatistics maintains aggregated request metrics in memory.
|
||||||
|
type RequestStatistics struct {
|
||||||
|
mu sync.RWMutex
|
||||||
|
|
||||||
|
totalRequests int64
|
||||||
|
successCount int64
|
||||||
|
failureCount int64
|
||||||
|
totalTokens int64
|
||||||
|
|
||||||
|
apis map[string]*apiStats
|
||||||
|
|
||||||
|
requestsByDay map[string]int64
|
||||||
|
requestsByHour map[int]int64
|
||||||
|
tokensByDay map[string]int64
|
||||||
|
tokensByHour map[int]int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// apiStats holds aggregated metrics for a single API key.
|
||||||
|
type apiStats struct {
|
||||||
|
TotalRequests int64
|
||||||
|
TotalTokens int64
|
||||||
|
Models map[string]*modelStats
|
||||||
|
}
|
||||||
|
|
||||||
|
// modelStats holds aggregated metrics for a specific model within an API.
|
||||||
|
type modelStats struct {
|
||||||
|
TotalRequests int64
|
||||||
|
TotalTokens int64
|
||||||
|
Details []RequestDetail
|
||||||
|
}
|
||||||
|
|
||||||
|
// RequestDetail stores the timestamp and token usage for a single request.
|
||||||
|
type RequestDetail struct {
|
||||||
|
Timestamp time.Time `json:"timestamp"`
|
||||||
|
Tokens TokenStats `json:"tokens"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TokenStats captures the token usage breakdown for a request.
|
||||||
|
type TokenStats struct {
|
||||||
|
InputTokens int64 `json:"input_tokens"`
|
||||||
|
OutputTokens int64 `json:"output_tokens"`
|
||||||
|
ReasoningTokens int64 `json:"reasoning_tokens"`
|
||||||
|
CachedTokens int64 `json:"cached_tokens"`
|
||||||
|
TotalTokens int64 `json:"total_tokens"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// StatisticsSnapshot represents an immutable view of the aggregated metrics.
|
||||||
|
type StatisticsSnapshot struct {
|
||||||
|
TotalRequests int64 `json:"total_requests"`
|
||||||
|
SuccessCount int64 `json:"success_count"`
|
||||||
|
FailureCount int64 `json:"failure_count"`
|
||||||
|
TotalTokens int64 `json:"total_tokens"`
|
||||||
|
|
||||||
|
APIs map[string]APISnapshot `json:"apis"`
|
||||||
|
|
||||||
|
RequestsByDay map[string]int64 `json:"requests_by_day"`
|
||||||
|
RequestsByHour map[string]int64 `json:"requests_by_hour"`
|
||||||
|
TokensByDay map[string]int64 `json:"tokens_by_day"`
|
||||||
|
TokensByHour map[string]int64 `json:"tokens_by_hour"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// APISnapshot summarises metrics for a single API key.
|
||||||
|
type APISnapshot struct {
|
||||||
|
TotalRequests int64 `json:"total_requests"`
|
||||||
|
TotalTokens int64 `json:"total_tokens"`
|
||||||
|
Models map[string]ModelSnapshot `json:"models"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ModelSnapshot summarises metrics for a specific model.
|
||||||
|
type ModelSnapshot struct {
|
||||||
|
TotalRequests int64 `json:"total_requests"`
|
||||||
|
TotalTokens int64 `json:"total_tokens"`
|
||||||
|
Details []RequestDetail `json:"details"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var defaultRequestStatistics = NewRequestStatistics()
|
||||||
|
|
||||||
|
// GetRequestStatistics returns the shared statistics store.
|
||||||
|
func GetRequestStatistics() *RequestStatistics { return defaultRequestStatistics }
|
||||||
|
|
||||||
|
// NewRequestStatistics constructs an empty statistics store.
|
||||||
|
func NewRequestStatistics() *RequestStatistics {
|
||||||
|
return &RequestStatistics{
|
||||||
|
apis: make(map[string]*apiStats),
|
||||||
|
requestsByDay: make(map[string]int64),
|
||||||
|
requestsByHour: make(map[int]int64),
|
||||||
|
tokensByDay: make(map[string]int64),
|
||||||
|
tokensByHour: make(map[int]int64),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Record ingests a new usage record and updates the aggregates.
|
||||||
|
func (s *RequestStatistics) Record(ctx context.Context, record coreusage.Record) {
|
||||||
|
if s == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
timestamp := record.RequestedAt
|
||||||
|
if timestamp.IsZero() {
|
||||||
|
timestamp = time.Now()
|
||||||
|
}
|
||||||
|
detail := normaliseDetail(record.Detail)
|
||||||
|
totalTokens := detail.TotalTokens
|
||||||
|
statsKey := record.APIKey
|
||||||
|
if statsKey == "" {
|
||||||
|
statsKey = resolveAPIIdentifier(ctx, record)
|
||||||
|
}
|
||||||
|
success := resolveSuccess(ctx)
|
||||||
|
modelName := record.Model
|
||||||
|
if modelName == "" {
|
||||||
|
modelName = "unknown"
|
||||||
|
}
|
||||||
|
dayKey := timestamp.Format("2006-01-02")
|
||||||
|
hourKey := timestamp.Hour()
|
||||||
|
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
|
s.totalRequests++
|
||||||
|
if success {
|
||||||
|
s.successCount++
|
||||||
|
} else {
|
||||||
|
s.failureCount++
|
||||||
|
}
|
||||||
|
s.totalTokens += totalTokens
|
||||||
|
|
||||||
|
stats, ok := s.apis[statsKey]
|
||||||
|
if !ok {
|
||||||
|
stats = &apiStats{Models: make(map[string]*modelStats)}
|
||||||
|
s.apis[statsKey] = stats
|
||||||
|
}
|
||||||
|
s.updateAPIStats(stats, modelName, RequestDetail{Timestamp: timestamp, Tokens: detail})
|
||||||
|
|
||||||
|
s.requestsByDay[dayKey]++
|
||||||
|
s.requestsByHour[hourKey]++
|
||||||
|
s.tokensByDay[dayKey] += totalTokens
|
||||||
|
s.tokensByHour[hourKey] += totalTokens
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *RequestStatistics) updateAPIStats(stats *apiStats, model string, detail RequestDetail) {
|
||||||
|
stats.TotalRequests++
|
||||||
|
stats.TotalTokens += detail.Tokens.TotalTokens
|
||||||
|
modelStatsValue, ok := stats.Models[model]
|
||||||
|
if !ok {
|
||||||
|
modelStatsValue = &modelStats{}
|
||||||
|
stats.Models[model] = modelStatsValue
|
||||||
|
}
|
||||||
|
modelStatsValue.TotalRequests++
|
||||||
|
modelStatsValue.TotalTokens += detail.Tokens.TotalTokens
|
||||||
|
modelStatsValue.Details = append(modelStatsValue.Details, detail)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Snapshot returns a copy of the aggregated metrics for external consumption.
|
||||||
|
func (s *RequestStatistics) Snapshot() StatisticsSnapshot {
|
||||||
|
result := StatisticsSnapshot{}
|
||||||
|
if s == nil {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
s.mu.RLock()
|
||||||
|
defer s.mu.RUnlock()
|
||||||
|
|
||||||
|
result.TotalRequests = s.totalRequests
|
||||||
|
result.SuccessCount = s.successCount
|
||||||
|
result.FailureCount = s.failureCount
|
||||||
|
result.TotalTokens = s.totalTokens
|
||||||
|
|
||||||
|
result.APIs = make(map[string]APISnapshot, len(s.apis))
|
||||||
|
for apiName, stats := range s.apis {
|
||||||
|
apiSnapshot := APISnapshot{
|
||||||
|
TotalRequests: stats.TotalRequests,
|
||||||
|
TotalTokens: stats.TotalTokens,
|
||||||
|
Models: make(map[string]ModelSnapshot, len(stats.Models)),
|
||||||
|
}
|
||||||
|
for modelName, modelStatsValue := range stats.Models {
|
||||||
|
requestDetails := make([]RequestDetail, len(modelStatsValue.Details))
|
||||||
|
copy(requestDetails, modelStatsValue.Details)
|
||||||
|
apiSnapshot.Models[modelName] = ModelSnapshot{
|
||||||
|
TotalRequests: modelStatsValue.TotalRequests,
|
||||||
|
TotalTokens: modelStatsValue.TotalTokens,
|
||||||
|
Details: requestDetails,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result.APIs[apiName] = apiSnapshot
|
||||||
|
}
|
||||||
|
|
||||||
|
result.RequestsByDay = make(map[string]int64, len(s.requestsByDay))
|
||||||
|
for k, v := range s.requestsByDay {
|
||||||
|
result.RequestsByDay[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
result.RequestsByHour = make(map[string]int64, len(s.requestsByHour))
|
||||||
|
for hour, v := range s.requestsByHour {
|
||||||
|
key := formatHour(hour)
|
||||||
|
result.RequestsByHour[key] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
result.TokensByDay = make(map[string]int64, len(s.tokensByDay))
|
||||||
|
for k, v := range s.tokensByDay {
|
||||||
|
result.TokensByDay[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
result.TokensByHour = make(map[string]int64, len(s.tokensByHour))
|
||||||
|
for hour, v := range s.tokensByHour {
|
||||||
|
key := formatHour(hour)
|
||||||
|
result.TokensByHour[key] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveAPIIdentifier(ctx context.Context, record coreusage.Record) string {
|
||||||
|
if ctx != nil {
|
||||||
|
if ginCtx, ok := ctx.Value("gin").(*gin.Context); ok && ginCtx != nil {
|
||||||
|
path := ginCtx.FullPath()
|
||||||
|
if path == "" && ginCtx.Request != nil {
|
||||||
|
path = ginCtx.Request.URL.Path
|
||||||
|
}
|
||||||
|
method := ""
|
||||||
|
if ginCtx.Request != nil {
|
||||||
|
method = ginCtx.Request.Method
|
||||||
|
}
|
||||||
|
if path != "" {
|
||||||
|
if method != "" {
|
||||||
|
return method + " " + path
|
||||||
|
}
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if record.Provider != "" {
|
||||||
|
return record.Provider
|
||||||
|
}
|
||||||
|
return "unknown"
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveSuccess(ctx context.Context) bool {
|
||||||
|
if ctx == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
ginCtx, ok := ctx.Value("gin").(*gin.Context)
|
||||||
|
if !ok || ginCtx == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
status := ginCtx.Writer.Status()
|
||||||
|
if status == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return status < httpStatusBadRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
const httpStatusBadRequest = 400
|
||||||
|
|
||||||
|
func normaliseDetail(detail coreusage.Detail) TokenStats {
|
||||||
|
tokens := TokenStats{
|
||||||
|
InputTokens: detail.InputTokens,
|
||||||
|
OutputTokens: detail.OutputTokens,
|
||||||
|
ReasoningTokens: detail.ReasoningTokens,
|
||||||
|
CachedTokens: detail.CachedTokens,
|
||||||
|
TotalTokens: detail.TotalTokens,
|
||||||
|
}
|
||||||
|
if tokens.TotalTokens == 0 {
|
||||||
|
tokens.TotalTokens = detail.InputTokens + detail.OutputTokens + detail.ReasoningTokens
|
||||||
|
}
|
||||||
|
if tokens.TotalTokens == 0 {
|
||||||
|
tokens.TotalTokens = detail.InputTokens + detail.OutputTokens + detail.ReasoningTokens + detail.CachedTokens
|
||||||
|
}
|
||||||
|
return tokens
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatHour(hour int) string {
|
||||||
|
if hour < 0 {
|
||||||
|
hour = 0
|
||||||
|
}
|
||||||
|
hour = hour % 24
|
||||||
|
return fmt.Sprintf("%02d", hour)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -476,31 +476,6 @@ func (s *Service) ensureAuthDir() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// syncCoreAuthFromAuths registers or updates core auths and disables missing ones.
|
|
||||||
func (s *Service) syncCoreAuthFromAuths(ctx context.Context, auths []*coreauth.Auth) {
|
|
||||||
if s.coreManager == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
seen := make(map[string]struct{}, len(auths))
|
|
||||||
for _, a := range auths {
|
|
||||||
if a == nil || a.ID == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
seen[a.ID] = struct{}{}
|
|
||||||
s.applyCoreAuthAddOrUpdate(ctx, a)
|
|
||||||
}
|
|
||||||
// Disable removed auths
|
|
||||||
for _, stored := range s.coreManager.List() {
|
|
||||||
if stored == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if _, ok := seen[stored.ID]; ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
s.applyCoreAuthRemoval(ctx, stored.ID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// registerModelsForAuth (re)binds provider models in the global registry using the core auth ID as client identifier.
|
// registerModelsForAuth (re)binds provider models in the global registry using the core auth ID as client identifier.
|
||||||
func (s *Service) registerModelsForAuth(a *coreauth.Auth) {
|
func (s *Service) registerModelsForAuth(a *coreauth.Auth) {
|
||||||
if a == nil || a.ID == "" {
|
if a == nil || a.ID == "" {
|
||||||
|
|||||||
Reference in New Issue
Block a user