feat(gateway): 增强 /v1/usage 端点返回完整用量统计
为 CC Switch 集成增强 /v1/usage 网关端点,在保持原有 4 字段 (isValid, planName, remaining, unit) 向后兼容的基础上,新增: - usage 对象:今日/累计的请求数、token 用量、费用,以及 RPM/TPM - subscription 对象(订阅模式):日/周/月用量和限额、过期时间 - balance 字段(余额模式):当前钱包余额 用量数据获取采用 best-effort 策略,失败不影响基础响应。 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -173,7 +173,7 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
|
|||||||
userAttributeService := service.NewUserAttributeService(userAttributeDefinitionRepository, userAttributeValueRepository)
|
userAttributeService := service.NewUserAttributeService(userAttributeDefinitionRepository, userAttributeValueRepository)
|
||||||
userAttributeHandler := admin.NewUserAttributeHandler(userAttributeService)
|
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)
|
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)
|
openAIGatewayHandler := handler.NewOpenAIGatewayHandler(openAIGatewayService, concurrencyService, billingCacheService, configConfig)
|
||||||
handlerSettingHandler := handler.ProvideSettingHandler(settingService, buildInfo)
|
handlerSettingHandler := handler.ProvideSettingHandler(settingService, buildInfo)
|
||||||
totpHandler := handler.NewTotpHandler(totpService)
|
totpHandler := handler.NewTotpHandler(totpService)
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ type GatewayHandler struct {
|
|||||||
antigravityGatewayService *service.AntigravityGatewayService
|
antigravityGatewayService *service.AntigravityGatewayService
|
||||||
userService *service.UserService
|
userService *service.UserService
|
||||||
billingCacheService *service.BillingCacheService
|
billingCacheService *service.BillingCacheService
|
||||||
|
usageService *service.UsageService
|
||||||
concurrencyHelper *ConcurrencyHelper
|
concurrencyHelper *ConcurrencyHelper
|
||||||
maxAccountSwitches int
|
maxAccountSwitches int
|
||||||
maxAccountSwitchesGemini int
|
maxAccountSwitchesGemini int
|
||||||
@@ -43,6 +44,7 @@ func NewGatewayHandler(
|
|||||||
userService *service.UserService,
|
userService *service.UserService,
|
||||||
concurrencyService *service.ConcurrencyService,
|
concurrencyService *service.ConcurrencyService,
|
||||||
billingCacheService *service.BillingCacheService,
|
billingCacheService *service.BillingCacheService,
|
||||||
|
usageService *service.UsageService,
|
||||||
cfg *config.Config,
|
cfg *config.Config,
|
||||||
) *GatewayHandler {
|
) *GatewayHandler {
|
||||||
pingInterval := time.Duration(0)
|
pingInterval := time.Duration(0)
|
||||||
@@ -63,6 +65,7 @@ func NewGatewayHandler(
|
|||||||
antigravityGatewayService: antigravityGatewayService,
|
antigravityGatewayService: antigravityGatewayService,
|
||||||
userService: userService,
|
userService: userService,
|
||||||
billingCacheService: billingCacheService,
|
billingCacheService: billingCacheService,
|
||||||
|
usageService: usageService,
|
||||||
concurrencyHelper: NewConcurrencyHelper(concurrencyService, SSEPingFormatClaude, pingInterval),
|
concurrencyHelper: NewConcurrencyHelper(concurrencyService, SSEPingFormatClaude, pingInterval),
|
||||||
maxAccountSwitches: maxAccountSwitches,
|
maxAccountSwitches: maxAccountSwitches,
|
||||||
maxAccountSwitchesGemini: maxAccountSwitchesGemini,
|
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
|
// GET /v1/usage
|
||||||
func (h *GatewayHandler) Usage(c *gin.Context) {
|
func (h *GatewayHandler) Usage(c *gin.Context) {
|
||||||
apiKey, ok := middleware2.GetAPIKeyFromContext(c)
|
apiKey, ok := middleware2.GetAPIKeyFromContext(c)
|
||||||
@@ -539,7 +542,40 @@ func (h *GatewayHandler) Usage(c *gin.Context) {
|
|||||||
return
|
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() {
|
if apiKey.Group != nil && apiKey.Group.IsSubscriptionType() {
|
||||||
subscription, ok := middleware2.GetSubscriptionFromContext(c)
|
subscription, ok := middleware2.GetSubscriptionFromContext(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -548,28 +584,46 @@ func (h *GatewayHandler) Usage(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
remaining := h.calculateSubscriptionRemaining(apiKey.Group, subscription)
|
remaining := h.calculateSubscriptionRemaining(apiKey.Group, subscription)
|
||||||
c.JSON(http.StatusOK, gin.H{
|
resp := gin.H{
|
||||||
"isValid": true,
|
"isValid": true,
|
||||||
"planName": apiKey.Group.Name,
|
"planName": apiKey.Group.Name,
|
||||||
"remaining": remaining,
|
"remaining": remaining,
|
||||||
"unit": "USD",
|
"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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 余额模式:返回钱包余额
|
// 余额模式:返回钱包余额 + 用量统计
|
||||||
latestUser, err := h.userService.GetByID(c.Request.Context(), subject.UserID)
|
latestUser, err := h.userService.GetByID(c.Request.Context(), subject.UserID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.errorResponse(c, http.StatusInternalServerError, "api_error", "Failed to get user info")
|
h.errorResponse(c, http.StatusInternalServerError, "api_error", "Failed to get user info")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
resp := gin.H{
|
||||||
"isValid": true,
|
"isValid": true,
|
||||||
"planName": "钱包余额",
|
"planName": "钱包余额",
|
||||||
"remaining": latestUser.Balance,
|
"remaining": latestUser.Balance,
|
||||||
"unit": "USD",
|
"unit": "USD",
|
||||||
})
|
"balance": latestUser.Balance,
|
||||||
|
}
|
||||||
|
if usageData != nil {
|
||||||
|
resp["usage"] = usageData
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
// calculateSubscriptionRemaining 计算订阅剩余可用额度
|
// calculateSubscriptionRemaining 计算订阅剩余可用额度
|
||||||
|
|||||||
Reference in New Issue
Block a user