antigravity_credits_overages.go 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  1. package service
  2. import (
  3. "context"
  4. "encoding/json"
  5. "io"
  6. "net/http"
  7. "strings"
  8. "time"
  9. "github.com/Wei-Shaw/sub2api/internal/pkg/antigravity"
  10. "github.com/Wei-Shaw/sub2api/internal/pkg/logger"
  11. )
  12. const (
  13. // creditsExhaustedKey 是 model_rate_limits 中标记积分耗尽的特殊 key。
  14. // 与普通模型限流完全同构:通过 SetModelRateLimit / isRateLimitActiveForKey 读写。
  15. creditsExhaustedKey = "AICredits"
  16. creditsExhaustedDuration = 5 * time.Hour
  17. )
  18. type antigravity429Category string
  19. const (
  20. antigravity429Unknown antigravity429Category = "unknown"
  21. antigravity429RateLimited antigravity429Category = "rate_limited"
  22. antigravity429QuotaExhausted antigravity429Category = "quota_exhausted"
  23. )
  24. var (
  25. antigravityQuotaExhaustedKeywords = []string{
  26. "quota_exhausted",
  27. "quota exhausted",
  28. }
  29. creditsExhaustedKeywords = []string{
  30. "google_one_ai",
  31. "insufficient credit",
  32. "insufficient credits",
  33. "not enough credit",
  34. "not enough credits",
  35. "credit exhausted",
  36. "credits exhausted",
  37. "credit balance",
  38. "minimumcreditamountforusage",
  39. "minimum credit amount for usage",
  40. "minimum credit",
  41. "resource has been exhausted",
  42. }
  43. )
  44. // isCreditsExhausted 检查账号的 AICredits 限流 key 是否生效(积分是否耗尽)。
  45. func (a *Account) isCreditsExhausted() bool {
  46. if a == nil {
  47. return false
  48. }
  49. return a.isRateLimitActiveForKey(creditsExhaustedKey)
  50. }
  51. // setCreditsExhausted 标记账号积分耗尽:写入 model_rate_limits["AICredits"] + 更新缓存。
  52. func (s *AntigravityGatewayService) setCreditsExhausted(ctx context.Context, account *Account) {
  53. if account == nil || account.ID == 0 {
  54. return
  55. }
  56. resetAt := time.Now().Add(creditsExhaustedDuration)
  57. if err := s.accountRepo.SetModelRateLimit(ctx, account.ID, creditsExhaustedKey, resetAt); err != nil {
  58. logger.LegacyPrintf("service.antigravity_gateway", "set credits exhausted failed: account=%d err=%v", account.ID, err)
  59. return
  60. }
  61. s.updateAccountModelRateLimitInCache(ctx, account, creditsExhaustedKey, resetAt)
  62. logger.LegacyPrintf("service.antigravity_gateway", "credits_exhausted_marked account=%d reset_at=%s",
  63. account.ID, resetAt.UTC().Format(time.RFC3339))
  64. }
  65. // clearCreditsExhausted 清除账号的 AICredits 限流 key。
  66. func (s *AntigravityGatewayService) clearCreditsExhausted(ctx context.Context, account *Account) {
  67. if account == nil || account.ID == 0 || account.Extra == nil {
  68. return
  69. }
  70. rawLimits, ok := account.Extra[modelRateLimitsKey].(map[string]any)
  71. if !ok {
  72. return
  73. }
  74. if _, exists := rawLimits[creditsExhaustedKey]; !exists {
  75. return
  76. }
  77. delete(rawLimits, creditsExhaustedKey)
  78. account.Extra[modelRateLimitsKey] = rawLimits
  79. if err := s.accountRepo.UpdateExtra(ctx, account.ID, map[string]any{
  80. modelRateLimitsKey: rawLimits,
  81. }); err != nil {
  82. logger.LegacyPrintf("service.antigravity_gateway", "clear credits exhausted failed: account=%d err=%v", account.ID, err)
  83. }
  84. }
  85. // classifyAntigravity429 将 Antigravity 的 429 响应归类为配额耗尽、限流或未知。
  86. func classifyAntigravity429(body []byte) antigravity429Category {
  87. if len(body) == 0 {
  88. return antigravity429Unknown
  89. }
  90. lowerBody := strings.ToLower(string(body))
  91. for _, keyword := range antigravityQuotaExhaustedKeywords {
  92. if strings.Contains(lowerBody, keyword) {
  93. return antigravity429QuotaExhausted
  94. }
  95. }
  96. if info := parseAntigravitySmartRetryInfo(body); info != nil && !info.IsModelCapacityExhausted {
  97. return antigravity429RateLimited
  98. }
  99. return antigravity429Unknown
  100. }
  101. // injectEnabledCreditTypes 在已序列化的 v1internal JSON body 中注入 AI Credits 类型。
  102. func injectEnabledCreditTypes(body []byte) []byte {
  103. var payload map[string]any
  104. if err := json.Unmarshal(body, &payload); err != nil {
  105. return nil
  106. }
  107. payload["enabledCreditTypes"] = []string{"GOOGLE_ONE_AI"}
  108. result, err := json.Marshal(payload)
  109. if err != nil {
  110. return nil
  111. }
  112. return result
  113. }
  114. // resolveCreditsOveragesModelKey 解析当前请求对应的 overages 状态模型 key。
  115. func resolveCreditsOveragesModelKey(ctx context.Context, account *Account, upstreamModelName, requestedModel string) string {
  116. modelKey := strings.TrimSpace(upstreamModelName)
  117. if modelKey != "" {
  118. return modelKey
  119. }
  120. if account == nil {
  121. return ""
  122. }
  123. modelKey = resolveFinalAntigravityModelKey(ctx, account, requestedModel)
  124. if strings.TrimSpace(modelKey) != "" {
  125. return modelKey
  126. }
  127. return resolveAntigravityModelKey(requestedModel)
  128. }
  129. // shouldMarkCreditsExhausted 判断一次 credits 请求失败是否应标记为 credits 耗尽。
  130. func shouldMarkCreditsExhausted(resp *http.Response, respBody []byte, reqErr error) bool {
  131. if reqErr != nil || resp == nil {
  132. return false
  133. }
  134. if resp.StatusCode >= 500 || resp.StatusCode == http.StatusRequestTimeout {
  135. return false
  136. }
  137. // 注意:不再检查 isURLLevelRateLimit。此函数仅在积分重试失败后调用,
  138. // 如果注入 enabledCreditTypes 后仍返回 "Resource has been exhausted",
  139. // 说明积分也已耗尽,应该标记。clearCreditsExhausted 会在后续成功时自动清除。
  140. if info := parseAntigravitySmartRetryInfo(respBody); info != nil {
  141. return false
  142. }
  143. bodyLower := strings.ToLower(string(respBody))
  144. for _, keyword := range creditsExhaustedKeywords {
  145. if strings.Contains(bodyLower, keyword) {
  146. return true
  147. }
  148. }
  149. return false
  150. }
  151. type creditsOveragesRetryResult struct {
  152. handled bool
  153. resp *http.Response
  154. }
  155. // attemptCreditsOveragesRetry 在确认免费配额耗尽后,尝试注入 AI Credits 继续请求。
  156. func (s *AntigravityGatewayService) attemptCreditsOveragesRetry(
  157. p antigravityRetryLoopParams,
  158. baseURL string,
  159. modelName string,
  160. waitDuration time.Duration,
  161. originalStatusCode int,
  162. respBody []byte,
  163. ) *creditsOveragesRetryResult {
  164. creditsBody := injectEnabledCreditTypes(p.body)
  165. if creditsBody == nil {
  166. return &creditsOveragesRetryResult{handled: false}
  167. }
  168. modelKey := resolveCreditsOveragesModelKey(p.ctx, p.account, modelName, p.requestedModel)
  169. logger.LegacyPrintf("service.antigravity_gateway", "%s status=429 credit_overages_retry model=%s account=%d (injecting enabledCreditTypes)",
  170. p.prefix, modelKey, p.account.ID)
  171. creditsReq, err := antigravity.NewAPIRequestWithURL(p.ctx, baseURL, p.action, p.accessToken, creditsBody)
  172. if err != nil {
  173. logger.LegacyPrintf("service.antigravity_gateway", "%s credit_overages_failed model=%s account=%d build_request_err=%v",
  174. p.prefix, modelKey, p.account.ID, err)
  175. return &creditsOveragesRetryResult{handled: true}
  176. }
  177. creditsResp, err := p.httpUpstream.Do(WithAccountTrafficRequest(creditsReq, p.account), p.proxyURL, p.account.ID, p.account.Concurrency)
  178. if err == nil && creditsResp != nil && creditsResp.StatusCode < 400 {
  179. s.clearCreditsExhausted(p.ctx, p.account)
  180. logger.LegacyPrintf("service.antigravity_gateway", "%s status=%d credit_overages_success model=%s account=%d",
  181. p.prefix, creditsResp.StatusCode, modelKey, p.account.ID)
  182. return &creditsOveragesRetryResult{handled: true, resp: creditsResp}
  183. }
  184. s.handleCreditsRetryFailure(p.ctx, p.prefix, modelKey, p.account, creditsResp, err)
  185. return &creditsOveragesRetryResult{handled: true}
  186. }
  187. func (s *AntigravityGatewayService) handleCreditsRetryFailure(
  188. ctx context.Context,
  189. prefix string,
  190. modelKey string,
  191. account *Account,
  192. creditsResp *http.Response,
  193. reqErr error,
  194. ) {
  195. var creditsRespBody []byte
  196. creditsStatusCode := 0
  197. if creditsResp != nil {
  198. creditsStatusCode = creditsResp.StatusCode
  199. if creditsResp.Body != nil {
  200. creditsRespBody, _ = io.ReadAll(io.LimitReader(creditsResp.Body, 64<<10))
  201. _ = creditsResp.Body.Close()
  202. }
  203. }
  204. if shouldMarkCreditsExhausted(creditsResp, creditsRespBody, reqErr) && account != nil {
  205. s.setCreditsExhausted(ctx, account)
  206. logger.LegacyPrintf("service.antigravity_gateway", "%s credit_overages_failed model=%s account=%d marked_exhausted=true status=%d body=%s",
  207. prefix, modelKey, account.ID, creditsStatusCode, truncateForLog(creditsRespBody, 200))
  208. return
  209. }
  210. if account != nil {
  211. logger.LegacyPrintf("service.antigravity_gateway", "%s credit_overages_failed model=%s account=%d marked_exhausted=false status=%d err=%v body=%s",
  212. prefix, modelKey, account.ID, creditsStatusCode, reqErr, truncateForLog(creditsRespBody, 200))
  213. }
  214. }