From a55cfebd094c6ac6291c24796b342947d9ba3b27 Mon Sep 17 00:00:00 2001 From: ianshaw Date: Sun, 25 Jan 2026 00:50:42 +0800 Subject: [PATCH] =?UTF-8?q?fix(ratelimit):=20=E4=BF=AE=E5=A4=8D=20OpenAI?= =?UTF-8?q?=20usage=5Flimit=5Freached=20=E9=94=99=E8=AF=AF=E7=9A=84?= =?UTF-8?q?=E9=87=8D=E7=BD=AE=E6=97=B6=E9=97=B4=E8=A7=A3=E6=9E=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 问题:OpenAI 的 usage_limit_reached 错误(需 37 小时重置)被错误地设置为 5 分钟 - 原因:handle429 只检查 Anthropic 响应头,没有解析 OpenAI 响应体中的 resets_in_seconds - 修复:新增 parseOpenAIRateLimitResetTime 函数解析 OpenAI 响应体 - 影响:避免调度器不断尝试已达配额上限的账户 --- backend/internal/service/ratelimit_service.go | 87 ++++++++++++++++++- 1 file changed, 84 insertions(+), 3 deletions(-) diff --git a/backend/internal/service/ratelimit_service.go b/backend/internal/service/ratelimit_service.go index fc8c831c9..6b7ebb074 100644 --- a/backend/internal/service/ratelimit_service.go +++ b/backend/internal/service/ratelimit_service.go @@ -343,7 +343,7 @@ func (s *RateLimitService) handleCustomErrorCode(ctx context.Context, account *A // handle429 处理429限流错误 // 解析响应头获取重置时间,标记账号为限流状态 func (s *RateLimitService) handle429(ctx context.Context, account *Account, headers http.Header, responseBody []byte) { - // OpenAI 平台:解析 x-codex-* 响应头 + // 1. OpenAI 平台:优先尝试解析 x-codex-* 响应头(用于 rate_limit_exceeded) if account.Platform == PlatformOpenAI { if resetAt := s.calculateOpenAI429ResetTime(headers); resetAt != nil { if err := s.accountRepo.SetRateLimited(ctx, account.ID, *resetAt); err != nil { @@ -353,12 +353,38 @@ func (s *RateLimitService) handle429(ctx context.Context, account *Account, head slog.Info("openai_account_rate_limited", "account_id", account.ID, "reset_at", *resetAt) return } - // 如果解析失败,继续使用默认逻辑 } - // 解析重置时间戳 + // 2. 尝试从响应头解析重置时间(Anthropic) resetTimestamp := headers.Get("anthropic-ratelimit-unified-reset") + + // 3. 如果响应头没有,尝试从响应体解析(OpenAI usage_limit_reached, Gemini) if resetTimestamp == "" { + switch account.Platform { + case PlatformOpenAI: + // 尝试解析 OpenAI 的 usage_limit_reached 错误 + if resetAt := parseOpenAIRateLimitResetTime(responseBody); resetAt != nil { + resetTime := time.Unix(*resetAt, 0) + if err := s.accountRepo.SetRateLimited(ctx, account.ID, resetTime); err != nil { + slog.Warn("rate_limit_set_failed", "account_id", account.ID, "error", err) + return + } + slog.Info("account_rate_limited", "account_id", account.ID, "platform", account.Platform, "reset_at", resetTime, "reset_in", time.Until(resetTime).Truncate(time.Second)) + return + } + case PlatformGemini, PlatformAntigravity: + // 尝试解析 Gemini 格式(用于其他平台) + if resetAt := ParseGeminiRateLimitResetTime(responseBody); resetAt != nil { + resetTime := time.Unix(*resetAt, 0) + if err := s.accountRepo.SetRateLimited(ctx, account.ID, resetTime); err != nil { + slog.Warn("rate_limit_set_failed", "account_id", account.ID, "error", err) + return + } + slog.Info("account_rate_limited", "account_id", account.ID, "platform", account.Platform, "reset_at", resetTime, "reset_in", time.Until(resetTime).Truncate(time.Second)) + return + } + } + // 没有重置时间,使用默认5分钟 resetAt := time.Now().Add(5 * time.Minute) if s.shouldScopeClaudeSonnetRateLimit(account, responseBody) { @@ -369,6 +395,7 @@ func (s *RateLimitService) handle429(ctx context.Context, account *Account, head } return } + slog.Warn("rate_limit_no_reset_time", "account_id", account.ID, "platform", account.Platform, "using_default", "5m") if err := s.accountRepo.SetRateLimited(ctx, account.ID, resetAt); err != nil { slog.Warn("rate_limit_set_failed", "account_id", account.ID, "error", err) } @@ -480,6 +507,60 @@ func (s *RateLimitService) calculateOpenAI429ResetTime(headers http.Header) *tim return nil } +// parseOpenAIRateLimitResetTime 解析 OpenAI 格式的 429 响应,返回重置时间的 Unix 时间戳 +// OpenAI 的 usage_limit_reached 错误格式: +// +// { +// "error": { +// "message": "The usage limit has been reached", +// "type": "usage_limit_reached", +// "resets_at": 1769404154, +// "resets_in_seconds": 133107 +// } +// } +func parseOpenAIRateLimitResetTime(body []byte) *int64 { + var parsed map[string]any + if err := json.Unmarshal(body, &parsed); err != nil { + return nil + } + + errObj, ok := parsed["error"].(map[string]any) + if !ok { + return nil + } + + // 检查是否为 usage_limit_reached 或 rate_limit_exceeded 类型 + errType, _ := errObj["type"].(string) + if errType != "usage_limit_reached" && errType != "rate_limit_exceeded" { + return nil + } + + // 优先使用 resets_at(Unix 时间戳) + if resetsAt, ok := errObj["resets_at"].(float64); ok { + ts := int64(resetsAt) + return &ts + } + if resetsAt, ok := errObj["resets_at"].(string); ok { + if ts, err := strconv.ParseInt(resetsAt, 10, 64); err == nil { + return &ts + } + } + + // 如果没有 resets_at,尝试使用 resets_in_seconds + if resetsInSeconds, ok := errObj["resets_in_seconds"].(float64); ok { + ts := time.Now().Unix() + int64(resetsInSeconds) + return &ts + } + if resetsInSeconds, ok := errObj["resets_in_seconds"].(string); ok { + if sec, err := strconv.ParseInt(resetsInSeconds, 10, 64); err == nil { + ts := time.Now().Unix() + sec + return &ts + } + } + + return nil +} + // handle529 处理529过载错误 // 根据配置设置过载冷却时间 func (s *RateLimitService) handle529(ctx context.Context, account *Account) {