revert: remove antigravity credits precheck logic (not part of channel feature)
Restore account_usage_service.go, antigravity_gateway_service.go, antigravity_credits_overages.go and its test to upstream/main state. These credits balance precheck changes were accidentally included during cherry-pick of channel management commits.
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user