package redisqueue import ( "context" "encoding/json" "net/http" "strings" "time" "github.com/gin-gonic/gin" internallogging "github.com/router-for-me/CLIProxyAPI/v6/internal/logging" internalusage "github.com/router-for-me/CLIProxyAPI/v6/internal/usage" coreusage "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/usage" ) func init() { coreusage.RegisterPlugin(&usageQueuePlugin{}) } type usageQueuePlugin struct{} func (p *usageQueuePlugin) HandleUsage(ctx context.Context, record coreusage.Record) { if p == nil { return } if !Enabled() || !internalusage.StatisticsEnabled() { return } timestamp := record.RequestedAt if timestamp.IsZero() { timestamp = time.Now() } modelName := strings.TrimSpace(record.Model) if modelName == "" { modelName = "unknown" } provider := strings.TrimSpace(record.Provider) if provider == "" { provider = "unknown" } authType := strings.TrimSpace(record.AuthType) if authType == "" { authType = "unknown" } apiKey := strings.TrimSpace(record.APIKey) requestID := strings.TrimSpace(internallogging.GetRequestID(ctx)) if requestID == "" { if ginCtx, ok := ctx.Value("gin").(*gin.Context); ok && ginCtx != nil { requestID = strings.TrimSpace(internallogging.GetGinRequestID(ginCtx)) } } tokens := internalusage.TokenStats{ InputTokens: record.Detail.InputTokens, OutputTokens: record.Detail.OutputTokens, ReasoningTokens: record.Detail.ReasoningTokens, CachedTokens: record.Detail.CachedTokens, TotalTokens: record.Detail.TotalTokens, } if tokens.TotalTokens == 0 { tokens.TotalTokens = tokens.InputTokens + tokens.OutputTokens + tokens.ReasoningTokens } if tokens.TotalTokens == 0 { tokens.TotalTokens = tokens.InputTokens + tokens.OutputTokens + tokens.ReasoningTokens + tokens.CachedTokens } failed := record.Failed if !failed { failed = !resolveSuccess(ctx) } detail := internalusage.RequestDetail{ Timestamp: timestamp, LatencyMs: record.Latency.Milliseconds(), Source: record.Source, AuthIndex: record.AuthIndex, Tokens: tokens, Failed: failed, } payload, err := json.Marshal(queuedUsageDetail{ RequestDetail: detail, Provider: provider, Model: modelName, Endpoint: resolveEndpoint(ctx), AuthType: authType, APIKey: apiKey, RequestID: requestID, }) if err != nil { return } Enqueue(payload) } type queuedUsageDetail struct { internalusage.RequestDetail Provider string `json:"provider"` Model string `json:"model"` Endpoint string `json:"endpoint"` AuthType string `json:"auth_type"` APIKey string `json:"api_key"` RequestID string `json:"request_id"` } 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 < http.StatusBadRequest } func resolveEndpoint(ctx context.Context) string { if ctx == nil { return "" } ginCtx, ok := ctx.Value("gin").(*gin.Context) if !ok || ginCtx == nil || ginCtx.Request == nil { return "" } path := strings.TrimSpace(ginCtx.FullPath()) if path == "" && ginCtx.Request.URL != nil { path = strings.TrimSpace(ginCtx.Request.URL.Path) } if path == "" { return "" } method := strings.TrimSpace(ginCtx.Request.Method) if method == "" { return path } return method + " " + path }