diff --git a/backend/cmd/server/wire_gen.go b/backend/cmd/server/wire_gen.go index 5f7dbcdc..513b7996 100644 --- a/backend/cmd/server/wire_gen.go +++ b/backend/cmd/server/wire_gen.go @@ -143,7 +143,7 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) { tlsFingerprintProfileCache := repository.NewTLSFingerprintProfileCache(redisClient) tlsFingerprintProfileService := service.NewTLSFingerprintProfileService(tlsFingerprintProfileRepository, tlsFingerprintProfileCache) accountUsageService := service.NewAccountUsageService(accountRepository, usageLogRepository, claudeUsageFetcher, geminiQuotaService, antigravityQuotaFetcher, usageCache, identityCache, tlsFingerprintProfileService) - antigravityGatewayService := service.NewAntigravityGatewayService(accountRepository, gatewayCache, schedulerSnapshotService, antigravityTokenProvider, rateLimitService, httpUpstream, settingService, internal500CounterCache, accountUsageService) + antigravityGatewayService := service.NewAntigravityGatewayService(accountRepository, gatewayCache, schedulerSnapshotService, antigravityTokenProvider, rateLimitService, httpUpstream, settingService, internal500CounterCache) accountTestService := service.NewAccountTestService(accountRepository, geminiTokenProvider, antigravityGatewayService, httpUpstream, configConfig, tlsFingerprintProfileService) crsSyncService := service.NewCRSSyncService(accountRepository, proxyRepository, oAuthService, openAIOAuthService, geminiOAuthService, configConfig) sessionLimitCache := repository.ProvideSessionLimitCache(redisClient, configConfig) diff --git a/backend/internal/service/account_usage_service.go b/backend/internal/service/account_usage_service.go index 50deba8b..0e5741d8 100644 --- a/backend/internal/service/account_usage_service.go +++ b/backend/internal/service/account_usage_service.go @@ -846,22 +846,6 @@ func (s *AccountUsageService) getAntigravityUsage(ctx context.Context, account * return usage, nil } -// GetAntigravityCredits 返回账号的 AI Credits 信息,复用 getAntigravityUsage 的缓存。 -// 如果缓存存在且 TTL 充足则直接返回;TTL 不足时自动刷新。 -func (s *AccountUsageService) GetAntigravityCredits(ctx context.Context, account *Account) (*UsageInfo, error) { - if account == nil || account.Platform != PlatformAntigravity { - return nil, nil - } - return s.getAntigravityUsage(ctx, account) -} - -// InvalidateAntigravityCreditsCache 清除指定账号的 Antigravity 用量缓存, -// 使下次调用 GetAntigravityCredits 时强制重新拉取。 -// 用于 credits 降级响应重试场景:避免重试命中同一个降级缓存。 -func (s *AccountUsageService) InvalidateAntigravityCreditsCache(accountID int64) { - s.cache.antigravityCache.Delete(accountID) -} - // recalcAntigravityRemainingSeconds 重新计算 Antigravity UsageInfo 中各窗口的 RemainingSeconds // 用于从缓存取出时更新倒计时,避免返回过时的剩余秒数 func recalcAntigravityRemainingSeconds(info *UsageInfo) { diff --git a/backend/internal/service/antigravity_credits_overages.go b/backend/internal/service/antigravity_credits_overages.go index 074b944d..ec365085 100644 --- a/backend/internal/service/antigravity_credits_overages.go +++ b/backend/internal/service/antigravity_credits_overages.go @@ -4,7 +4,6 @@ import ( "context" "encoding/json" "io" - "log/slog" "net/http" "strings" "time" @@ -18,140 +17,8 @@ const ( // 与普通模型限流完全同构:通过 SetModelRateLimit / isRateLimitActiveForKey 读写。 creditsExhaustedKey = "AICredits" creditsExhaustedDuration = 5 * time.Hour - - // credits 降级响应重试参数 - creditsRetryMaxAttempts = 3 - creditsRetryBaseInterval = 500 * time.Millisecond ) -// creditsRetryableErrorCodes 是降级响应中可重试的错误码集合。 -// forbidden 是稳定的封号状态,不属于可恢复的瞬态错误,不重试。 -var creditsRetryableErrorCodes = map[string]bool{ - errorCodeUnauthenticated: true, - errorCodeRateLimited: true, - errorCodeNetworkError: true, -} - -// isAntigravityDegradedResponse 检查 UsageInfo 是否为可重试的降级响应。 -// 仅检测 3 个瞬态错误码(unauthenticated/rate_limited/network_error), -// forbidden 是稳定的封号状态,不属于降级。 -func isAntigravityDegradedResponse(info *UsageInfo) bool { - if info == nil || info.ErrorCode == "" { - return false - } - return creditsRetryableErrorCodes[info.ErrorCode] -} - -// checkAccountCredits 通过共享的 AccountUsageService 缓存检查账号是否有足够的 AI Credits。 -// 缓存 TTL 不足时会自动从 Google loadCodeAssist API 刷新。 -// 检测到降级响应时会清除缓存并重试,最终 fail-open(返回 true)。 -func (s *AntigravityGatewayService) checkAccountCredits( - ctx context.Context, account *Account, -) bool { - if account == nil || account.ID == 0 { - return false - } - if s.accountUsageService == nil { - return true // 无 usage service 时不阻断 - } - - usageInfo, err := s.accountUsageService.GetAntigravityCredits(ctx, account) - if err != nil { - slog.Error("check_credits: get_credits_failed", - "account_id", account.ID, "error", err) - return true // 出错时 fail-open - } - - // 非降级响应:直接检查积分余额 - if !isAntigravityDegradedResponse(usageInfo) { - return s.logCreditsResult(account, usageInfo) - } - - // 降级响应:清除缓存后重试 - return s.retryCreditsOnDegraded(ctx, account, usageInfo) -} - -// retryCreditsOnDegraded 在检测到降级响应后,清除缓存并重试获取 credits。 -// 使用指数退避(500ms → 1s → 2s),最多重试 creditsRetryMaxAttempts 次。 -// 所有重试失败后 fail-open(返回 true),不做熔断。 -func (s *AntigravityGatewayService) retryCreditsOnDegraded( - ctx context.Context, account *Account, lastInfo *UsageInfo, -) bool { - for attempt := 1; attempt <= creditsRetryMaxAttempts; attempt++ { - delay := creditsRetryBaseInterval << (attempt - 1) // 指数退避:500ms, 1s, 2s - slog.Warn("check_credits: degraded response, retrying", - "account_id", account.ID, - "attempt", attempt, - "max_attempts", creditsRetryMaxAttempts, - "error_code", lastInfo.ErrorCode, - "delay", delay, - ) - - select { - case <-ctx.Done(): - slog.Warn("check_credits: context cancelled during retry, fail-open", - "account_id", account.ID, "attempt", attempt) - return true - case <-time.After(delay): - } - - // 清除缓存,强制下次 GetAntigravityCredits 重新拉取 - s.accountUsageService.InvalidateAntigravityCreditsCache(account.ID) - - info, err := s.accountUsageService.GetAntigravityCredits(ctx, account) - if err != nil { - slog.Error("check_credits: retry get_credits_failed", - "account_id", account.ID, "attempt", attempt, "error", err) - continue - } - - // 重试成功(不再是降级响应):检查积分余额 - if !isAntigravityDegradedResponse(info) { - slog.Info("check_credits: retry succeeded", - "account_id", account.ID, "attempt", attempt) - return s.logCreditsResult(account, info) - } - lastInfo = info - } - - // 所有重试失败:fail-open,不做熔断 - slog.Warn("check_credits: all retries exhausted, fail-open", - "account_id", account.ID, - "last_error_code", lastInfo.ErrorCode, - ) - return true -} - -// logCreditsResult 检查积分并记录不足日志,返回是否有积分。 -func (s *AntigravityGatewayService) logCreditsResult(account *Account, info *UsageInfo) bool { - hasCredits := hasEnoughCredits(info) - if !hasCredits { - slog.Warn("check_credits: insufficient credits", - "account_id", account.ID) - } - return hasCredits -} - -// hasEnoughCredits 检查 UsageInfo 中是否有足够的 GOOGLE_ONE_AI 积分。 -// 返回 true 表示积分可用,false 表示积分不足或无积分信息。 -func hasEnoughCredits(info *UsageInfo) bool { - if info == nil || len(info.AICredits) == 0 { - return false - } - - for _, credit := range info.AICredits { - if credit.CreditType == "GOOGLE_ONE_AI" { - minimum := credit.MinimumBalance - if minimum <= 0 { - minimum = 5 - } - return credit.Amount >= minimum - } - } - - return false -} - type antigravity429Category string const ( @@ -224,10 +91,6 @@ func (s *AntigravityGatewayService) clearCreditsExhausted(ctx context.Context, a }); err != nil { logger.LegacyPrintf("service.antigravity_gateway", "clear credits exhausted failed: account=%d err=%v", account.ID, err) } - // 同步更新 Redis 调度快照,避免其他节点/请求延迟感知 - if s.schedulerSnapshot != nil { - _ = s.schedulerSnapshot.UpdateAccountInCache(ctx, account) - } } // classifyAntigravity429 将 Antigravity 的 429 响应归类为配额耗尽、限流或未知。 @@ -278,9 +141,6 @@ func resolveCreditsOveragesModelKey(ctx context.Context, account *Account, upstr } // shouldMarkCreditsExhausted 判断一次 credits 请求失败是否应标记为 credits 耗尽。 -// 注意:不再检查 isURLLevelRateLimit。此函数仅在积分重试失败后调用, -// 如果注入 enabledCreditTypes 后仍返回 "Resource has been exhausted", -// 说明积分也已耗尽,应该标记。clearCreditsExhausted 会在后续成功时自动清除。 func shouldMarkCreditsExhausted(resp *http.Response, respBody []byte, reqErr error) bool { if reqErr != nil || resp == nil { return false @@ -288,6 +148,9 @@ func shouldMarkCreditsExhausted(resp *http.Response, respBody []byte, reqErr err if resp.StatusCode >= 500 || resp.StatusCode == http.StatusRequestTimeout { return false } + // 注意:不再检查 isURLLevelRateLimit。此函数仅在积分重试失败后调用, + // 如果注入 enabledCreditTypes 后仍返回 "Resource has been exhausted", + // 说明积分也已耗尽,应该标记。clearCreditsExhausted 会在后续成功时自动清除。 if info := parseAntigravitySmartRetryInfo(respBody); info != nil { return false } @@ -318,16 +181,6 @@ func (s *AntigravityGatewayService) attemptCreditsOveragesRetry( if creditsBody == nil { return &creditsOveragesRetryResult{handled: false} } - - // Check actual credits balance before attempting retry - if !s.checkAccountCredits(p.ctx, p.account) { - s.setCreditsExhausted(p.ctx, p.account) - modelKey := resolveCreditsOveragesModelKey(p.ctx, p.account, modelName, p.requestedModel) - logger.LegacyPrintf("service.antigravity_gateway", "%s credit_overages_no_credits model=%s account=%d (skipping credits retry)", - p.prefix, modelKey, p.account.ID) - return &creditsOveragesRetryResult{handled: true} - } - modelKey := resolveCreditsOveragesModelKey(p.ctx, p.account, modelName, p.requestedModel) logger.LegacyPrintf("service.antigravity_gateway", "%s status=429 credit_overages_retry model=%s account=%d (injecting enabledCreditTypes)", p.prefix, modelKey, p.account.ID) diff --git a/backend/internal/service/antigravity_credits_overages_test.go b/backend/internal/service/antigravity_credits_overages_test.go index 390982d4..7a5224da 100644 --- a/backend/internal/service/antigravity_credits_overages_test.go +++ b/backend/internal/service/antigravity_credits_overages_test.go @@ -542,105 +542,3 @@ func TestClearCreditsExhausted(t *testing.T) { require.True(t, exists, "普通模型限流应保留") }) } - -// =========================================================================== -// hasEnoughCredits — standalone credits balance check -// =========================================================================== - -func TestHasEnoughCredits(t *testing.T) { - tests := []struct { - name string - info *UsageInfo - want bool - }{ - { - name: "nil UsageInfo", - info: nil, - want: false, - }, - { - name: "empty AICredits list", - info: &UsageInfo{AICredits: []AICredit{}}, - want: false, - }, - { - name: "GOOGLE_ONE_AI with enough credits (amount=18778, minimum=50)", - info: &UsageInfo{ - AICredits: []AICredit{ - {CreditType: "GOOGLE_ONE_AI", Amount: 18778, MinimumBalance: 50}, - }, - }, - want: true, - }, - { - name: "GOOGLE_ONE_AI below minimum (amount=3, minimum=5)", - info: &UsageInfo{ - AICredits: []AICredit{ - {CreditType: "GOOGLE_ONE_AI", Amount: 3, MinimumBalance: 5}, - }, - }, - want: false, - }, - { - name: "GOOGLE_ONE_AI with zero MinimumBalance defaults to 5, amount=6", - info: &UsageInfo{ - AICredits: []AICredit{ - {CreditType: "GOOGLE_ONE_AI", Amount: 6, MinimumBalance: 0}, - }, - }, - want: true, - }, - { - name: "GOOGLE_ONE_AI with zero MinimumBalance defaults to 5, amount=4", - info: &UsageInfo{ - AICredits: []AICredit{ - {CreditType: "GOOGLE_ONE_AI", Amount: 4, MinimumBalance: 0}, - }, - }, - want: false, - }, - { - name: "GOOGLE_ONE_AI exactly at minimum (amount=5, minimum=5)", - info: &UsageInfo{ - AICredits: []AICredit{ - {CreditType: "GOOGLE_ONE_AI", Amount: 5, MinimumBalance: 5}, - }, - }, - want: true, - }, - { - name: "no GOOGLE_ONE_AI credit type", - info: &UsageInfo{ - AICredits: []AICredit{ - {CreditType: "OTHER_CREDIT", Amount: 10000, MinimumBalance: 5}, - }, - }, - want: false, - }, - { - name: "multiple credits, GOOGLE_ONE_AI present with enough", - info: &UsageInfo{ - AICredits: []AICredit{ - {CreditType: "OTHER_CREDIT", Amount: 0, MinimumBalance: 5}, - {CreditType: "GOOGLE_ONE_AI", Amount: 100, MinimumBalance: 10}, - }, - }, - want: true, - }, - { - name: "negative MinimumBalance defaults to 5", - info: &UsageInfo{ - AICredits: []AICredit{ - {CreditType: "GOOGLE_ONE_AI", Amount: 6, MinimumBalance: -1}, - }, - }, - want: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - require.Equal(t, tt.want, hasEnoughCredits(tt.info)) - }) - } -} diff --git a/backend/internal/service/antigravity_gateway_service.go b/backend/internal/service/antigravity_gateway_service.go index fd588bdd..a76e59fb 100644 --- a/backend/internal/service/antigravity_gateway_service.go +++ b/backend/internal/service/antigravity_gateway_service.go @@ -557,13 +557,7 @@ func (s *AntigravityGatewayService) antigravityRetryLoop(p antigravityRetryLoopP if p.requestedModel != "" && p.account.Platform == PlatformAntigravity && p.account.IsOveragesEnabled() && !p.account.isCreditsExhausted() && p.account.isModelRateLimitedWithContext(p.ctx, p.requestedModel) { - // Check actual credits balance before injection - if !s.checkAccountCredits(p.ctx, p.account) { - // No credits available - mark as exhausted and skip injection - s.setCreditsExhausted(p.ctx, p.account) - logger.LegacyPrintf("service.antigravity_gateway", "%s pre_check: no_credits_available account=%d (skipping credits injection)", - p.prefix, p.account.ID) - } else if creditsBody := injectEnabledCreditTypes(p.body); creditsBody != nil { + if creditsBody := injectEnabledCreditTypes(p.body); creditsBody != nil { p.body = creditsBody overagesInjected = true logger.LegacyPrintf("service.antigravity_gateway", "%s pre_check: model_rate_limited_credits_inject model=%s account=%d (injecting enabledCreditTypes)", @@ -876,15 +870,14 @@ func logPrefix(sessionID, accountName string) string { // AntigravityGatewayService 处理 Antigravity 平台的 API 转发 type AntigravityGatewayService struct { - accountRepo AccountRepository - tokenProvider *AntigravityTokenProvider - rateLimitService *RateLimitService - httpUpstream HTTPUpstream - settingService *SettingService - cache GatewayCache // 用于模型级限流时清除粘性会话绑定 - schedulerSnapshot *SchedulerSnapshotService - internal500Cache Internal500CounterCache // INTERNAL 500 渐进惩罚计数器 - accountUsageService *AccountUsageService // 共享 usage 缓存,用于积分余额检查 + accountRepo AccountRepository + tokenProvider *AntigravityTokenProvider + rateLimitService *RateLimitService + httpUpstream HTTPUpstream + settingService *SettingService + cache GatewayCache // 用于模型级限流时清除粘性会话绑定 + schedulerSnapshot *SchedulerSnapshotService + internal500Cache Internal500CounterCache // INTERNAL 500 渐进惩罚计数器 } func NewAntigravityGatewayService( @@ -896,18 +889,16 @@ func NewAntigravityGatewayService( httpUpstream HTTPUpstream, settingService *SettingService, internal500Cache Internal500CounterCache, - accountUsageService *AccountUsageService, ) *AntigravityGatewayService { return &AntigravityGatewayService{ - accountRepo: accountRepo, - tokenProvider: tokenProvider, - rateLimitService: rateLimitService, - httpUpstream: httpUpstream, - settingService: settingService, - cache: cache, - schedulerSnapshot: schedulerSnapshot, - internal500Cache: internal500Cache, - accountUsageService: accountUsageService, + accountRepo: accountRepo, + tokenProvider: tokenProvider, + rateLimitService: rateLimitService, + httpUpstream: httpUpstream, + settingService: settingService, + cache: cache, + schedulerSnapshot: schedulerSnapshot, + internal500Cache: internal500Cache, } }