From c441638fc01ca9aeffb60133a2d459d53429ecf5 Mon Sep 17 00:00:00 2001 From: JIA-ss <627723154@qq.com> Date: Mon, 2 Feb 2026 18:30:06 +0800 Subject: [PATCH] =?UTF-8?q?feat(gateway):=20=E5=A2=9E=E5=BC=BA=20/v1/usage?= =?UTF-8?q?=20=E7=AB=AF=E7=82=B9=E8=BF=94=E5=9B=9E=E5=AE=8C=E6=95=B4?= =?UTF-8?q?=E7=94=A8=E9=87=8F=E7=BB=9F=E8=AE=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 为 CC Switch 集成增强 /v1/usage 网关端点,在保持原有 4 字段 (isValid, planName, remaining, unit) 向后兼容的基础上,新增: - usage 对象:今日/累计的请求数、token 用量、费用,以及 RPM/TPM - subscription 对象(订阅模式):日/周/月用量和限额、过期时间 - balance 字段(余额模式):当前钱包余额 用量数据获取采用 best-effort 策略,失败不影响基础响应。 Co-Authored-By: Claude Opus 4.5 --- backend/cmd/server/wire_gen.go | 2 +- backend/internal/handler/gateway_handler.go | 68 ++++++++++++++++++--- 2 files changed, 62 insertions(+), 8 deletions(-) diff --git a/backend/cmd/server/wire_gen.go b/backend/cmd/server/wire_gen.go index 7d465fee..fd4383bf 100644 --- a/backend/cmd/server/wire_gen.go +++ b/backend/cmd/server/wire_gen.go @@ -173,7 +173,7 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) { userAttributeService := service.NewUserAttributeService(userAttributeDefinitionRepository, userAttributeValueRepository) userAttributeHandler := admin.NewUserAttributeHandler(userAttributeService) adminHandlers := handler.ProvideAdminHandlers(dashboardHandler, adminUserHandler, groupHandler, accountHandler, adminAnnouncementHandler, oAuthHandler, openAIOAuthHandler, geminiOAuthHandler, antigravityOAuthHandler, proxyHandler, adminRedeemHandler, promoHandler, settingHandler, opsHandler, systemHandler, adminSubscriptionHandler, adminUsageHandler, userAttributeHandler) - gatewayHandler := handler.NewGatewayHandler(gatewayService, geminiMessagesCompatService, antigravityGatewayService, userService, concurrencyService, billingCacheService, configConfig) + gatewayHandler := handler.NewGatewayHandler(gatewayService, geminiMessagesCompatService, antigravityGatewayService, userService, concurrencyService, billingCacheService, usageService, configConfig) openAIGatewayHandler := handler.NewOpenAIGatewayHandler(openAIGatewayService, concurrencyService, billingCacheService, configConfig) handlerSettingHandler := handler.ProvideSettingHandler(settingService, buildInfo) totpHandler := handler.NewTotpHandler(totpService) diff --git a/backend/internal/handler/gateway_handler.go b/backend/internal/handler/gateway_handler.go index 70ea51bf..842242ca 100644 --- a/backend/internal/handler/gateway_handler.go +++ b/backend/internal/handler/gateway_handler.go @@ -30,6 +30,7 @@ type GatewayHandler struct { antigravityGatewayService *service.AntigravityGatewayService userService *service.UserService billingCacheService *service.BillingCacheService + usageService *service.UsageService concurrencyHelper *ConcurrencyHelper maxAccountSwitches int maxAccountSwitchesGemini int @@ -43,6 +44,7 @@ func NewGatewayHandler( userService *service.UserService, concurrencyService *service.ConcurrencyService, billingCacheService *service.BillingCacheService, + usageService *service.UsageService, cfg *config.Config, ) *GatewayHandler { pingInterval := time.Duration(0) @@ -63,6 +65,7 @@ func NewGatewayHandler( antigravityGatewayService: antigravityGatewayService, userService: userService, billingCacheService: billingCacheService, + usageService: usageService, concurrencyHelper: NewConcurrencyHelper(concurrencyService, SSEPingFormatClaude, pingInterval), maxAccountSwitches: maxAccountSwitches, maxAccountSwitchesGemini: maxAccountSwitchesGemini, @@ -524,7 +527,7 @@ func (h *GatewayHandler) AntigravityModels(c *gin.Context) { }) } -// Usage handles getting account balance for CC Switch integration +// Usage handles getting account balance and usage statistics for CC Switch integration // GET /v1/usage func (h *GatewayHandler) Usage(c *gin.Context) { apiKey, ok := middleware2.GetAPIKeyFromContext(c) @@ -539,7 +542,40 @@ func (h *GatewayHandler) Usage(c *gin.Context) { return } - // 订阅模式:返回订阅限额信息 + // Best-effort: 获取用量统计,失败不影响基础响应 + var usageData gin.H + if h.usageService != nil { + dashStats, err := h.usageService.GetUserDashboardStats(c.Request.Context(), subject.UserID) + if err == nil && dashStats != nil { + usageData = gin.H{ + "today": gin.H{ + "requests": dashStats.TodayRequests, + "input_tokens": dashStats.TodayInputTokens, + "output_tokens": dashStats.TodayOutputTokens, + "cache_creation_tokens": dashStats.TodayCacheCreationTokens, + "cache_read_tokens": dashStats.TodayCacheReadTokens, + "total_tokens": dashStats.TodayTokens, + "cost": dashStats.TodayCost, + "actual_cost": dashStats.TodayActualCost, + }, + "total": gin.H{ + "requests": dashStats.TotalRequests, + "input_tokens": dashStats.TotalInputTokens, + "output_tokens": dashStats.TotalOutputTokens, + "cache_creation_tokens": dashStats.TotalCacheCreationTokens, + "cache_read_tokens": dashStats.TotalCacheReadTokens, + "total_tokens": dashStats.TotalTokens, + "cost": dashStats.TotalCost, + "actual_cost": dashStats.TotalActualCost, + }, + "average_duration_ms": dashStats.AverageDurationMs, + "rpm": dashStats.Rpm, + "tpm": dashStats.Tpm, + } + } + } + + // 订阅模式:返回订阅限额信息 + 用量统计 if apiKey.Group != nil && apiKey.Group.IsSubscriptionType() { subscription, ok := middleware2.GetSubscriptionFromContext(c) if !ok { @@ -548,28 +584,46 @@ func (h *GatewayHandler) Usage(c *gin.Context) { } remaining := h.calculateSubscriptionRemaining(apiKey.Group, subscription) - c.JSON(http.StatusOK, gin.H{ + resp := gin.H{ "isValid": true, "planName": apiKey.Group.Name, "remaining": remaining, "unit": "USD", - }) + "subscription": gin.H{ + "daily_usage_usd": subscription.DailyUsageUSD, + "weekly_usage_usd": subscription.WeeklyUsageUSD, + "monthly_usage_usd": subscription.MonthlyUsageUSD, + "daily_limit_usd": apiKey.Group.DailyLimitUSD, + "weekly_limit_usd": apiKey.Group.WeeklyLimitUSD, + "monthly_limit_usd": apiKey.Group.MonthlyLimitUSD, + "expires_at": subscription.ExpiresAt, + }, + } + if usageData != nil { + resp["usage"] = usageData + } + c.JSON(http.StatusOK, resp) return } - // 余额模式:返回钱包余额 + // 余额模式:返回钱包余额 + 用量统计 latestUser, err := h.userService.GetByID(c.Request.Context(), subject.UserID) if err != nil { h.errorResponse(c, http.StatusInternalServerError, "api_error", "Failed to get user info") return } - c.JSON(http.StatusOK, gin.H{ + resp := gin.H{ "isValid": true, "planName": "钱包余额", "remaining": latestUser.Balance, "unit": "USD", - }) + "balance": latestUser.Balance, + } + if usageData != nil { + resp["usage"] = usageData + } + c.JSON(http.StatusOK, resp) } // calculateSubscriptionRemaining 计算订阅剩余可用额度