From 5a6f60a95412d31e6acf3e4d762ed9a8c69a6d26 Mon Sep 17 00:00:00 2001 From: song Date: Sat, 17 Jan 2026 11:11:18 +0800 Subject: [PATCH] =?UTF-8?q?fix(antigravity):=20=E5=8C=BA=E5=88=86=20URL=20?= =?UTF-8?q?=E7=BA=A7=E5=88=AB=E5=92=8C=E8=B4=A6=E6=88=B7=E9=85=8D=E9=A2=9D?= =?UTF-8?q?=E7=BA=A7=E5=88=AB=E7=9A=84=20429=20=E9=99=90=E6=B5=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - "Resource has been exhausted" → URL 级别限流,立即切换 URL - "exhausted your capacity on this model" → 账户配额限流,重试 3 次(指数退避)后标记限流 --- .../service/antigravity_gateway_service.go | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/backend/internal/service/antigravity_gateway_service.go b/backend/internal/service/antigravity_gateway_service.go index 00b89260..fcdf04f1 100644 --- a/backend/internal/service/antigravity_gateway_service.go +++ b/backend/internal/service/antigravity_gateway_service.go @@ -92,17 +92,29 @@ urlFallbackLoop: return nil, fmt.Errorf("upstream request failed after retries: %w", err) } - // 429 限流:优先切换 URL,所有 URL 都 429 时才返回 + // 429 限流处理:区分 URL 级别限流和账户配额限流 if resp.StatusCode == http.StatusTooManyRequests { respBody, _ := io.ReadAll(io.LimitReader(resp.Body, 2<<20)) _ = resp.Body.Close() - if urlIdx < len(availableURLs)-1 { + // "Resource has been exhausted" 是 URL 级别限流,切换 URL + if isURLLevelRateLimit(respBody) && urlIdx < len(availableURLs)-1 { antigravity.DefaultURLAvailability.MarkUnavailable(baseURL) log.Printf("%s URL fallback (429): %s -> %s", p.prefix, baseURL, availableURLs[urlIdx+1]) continue urlFallbackLoop } + // 账户/模型配额限流,重试 3 次(指数退避) + if attempt < antigravityMaxRetries { + log.Printf("%s status=429 retry=%d/%d body=%s", p.prefix, attempt, antigravityMaxRetries, truncateForLog(respBody, 200)) + if !sleepAntigravityBackoffWithContext(p.ctx, attempt) { + log.Printf("%s status=context_canceled_during_backoff", p.prefix) + return nil, p.ctx.Err() + } + continue + } + + // 重试用尽,标记账户限流 p.handleError(p.ctx, p.prefix, p.account, resp.StatusCode, resp.Header, respBody, p.quotaScope) log.Printf("%s status=429 rate_limited base_url=%s body=%s", p.prefix, baseURL, truncateForLog(respBody, 200)) resp = &http.Response{ @@ -155,6 +167,16 @@ func shouldRetryAntigravityError(statusCode int) bool { } } +// isURLLevelRateLimit 判断是否为 URL 级别的限流(应切换 URL 重试) +// "Resource has been exhausted" 是 URL/节点级别限流,切换 URL 可能成功 +// "exhausted your capacity on this model" 是账户/模型配额限流,切换 URL 无效 +func isURLLevelRateLimit(body []byte) bool { + // 快速检查:包含 "Resource has been exhausted" 且不包含 "capacity on this model" + bodyStr := string(body) + return strings.Contains(bodyStr, "Resource has been exhausted") && + !strings.Contains(bodyStr, "capacity on this model") +} + // isAntigravityConnectionError 判断是否为连接错误(网络超时、DNS 失败、连接拒绝) func isAntigravityConnectionError(err error) bool { if err == nil {