From a071b27b417ab2f20630ffb438b9480c271d5cc4 Mon Sep 17 00:00:00 2001 From: li Date: Fri, 7 Aug 2026 19:44:43 +0800 Subject: [PATCH 1/2] =?UTF-8?q?fix(grok):=20405=20=E7=BA=B3=E5=85=A5=20fai?= =?UTF-8?q?lover=20=E4=B8=8E=E4=B8=B4=E6=97=B6=E8=B8=A2=E5=8F=B7=EF=BC=8C?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20Grok=20=E4=BC=9A=E8=AF=9D=E7=B2=98?= =?UTF-8?q?=E6=80=A7=E9=94=81=E6=AD=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Grok 分组里若有账号的自定义 base_url 只实现 /v1/chat/completions,对 /v1/responses 固定返回 405,会话 sticky 到该账号后就永久锁死: 1. shouldFailoverUpstreamError 不含 405,请求不进换号循环 2. handleGrokAccountUpstreamError 的 switch 不含 405,账号不进临时不可调度 3. 账号仍可调度,sticky 不清理,客户端自动重连继续绑同一个坏号 改动两处: - shouldFailoverUpstreamError 增加 405,让请求可换号,sticky 随之更新 - handleGrokAccountUpstreamError 增加 405 → 30 分钟冷却,与 403 同级 Fixes #4668 --- .../internal/service/openai_gateway_grok.go | 2 ++ .../service/openai_gateway_grok_405_test.go | 29 +++++++++++++++++++ .../service/openai_gateway_grok_test.go | 7 +++++ .../service/openai_gateway_upstream_errors.go | 2 +- 4 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 backend/internal/service/openai_gateway_grok_405_test.go diff --git a/backend/internal/service/openai_gateway_grok.go b/backend/internal/service/openai_gateway_grok.go index 4d367c9aeb..87899c1350 100644 --- a/backend/internal/service/openai_gateway_grok.go +++ b/backend/internal/service/openai_gateway_grok.go @@ -1370,6 +1370,8 @@ func (s *OpenAIGatewayService) handleGrokAccountUpstreamError(ctx context.Contex s.tempUnscheduleGrok(ctx, account, 30*time.Minute, "grok payment required") case http.StatusForbidden: s.tempUnscheduleGrok(ctx, account, 30*time.Minute, "grok access or entitlement denied") + case http.StatusMethodNotAllowed: + s.tempUnscheduleGrok(ctx, account, 30*time.Minute, "grok endpoint not supported (405)") case http.StatusTooManyRequests: // updateGrokUsageSnapshot installs rate-limit state for non-pool accounts. default: diff --git a/backend/internal/service/openai_gateway_grok_405_test.go b/backend/internal/service/openai_gateway_grok_405_test.go new file mode 100644 index 0000000000..10ed8a599e --- /dev/null +++ b/backend/internal/service/openai_gateway_grok_405_test.go @@ -0,0 +1,29 @@ +package service + +import ( + "net/http" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestShouldFailoverUpstreamError_405IsFailoverEligible(t *testing.T) { + svc := &OpenAIGatewayService{} + + assert.True(t, svc.shouldFailoverUpstreamError(http.StatusMethodNotAllowed), + "405 should trigger failover so sticky sessions can escape to healthy accounts") +} + +func TestShouldFailoverUpstreamError_ExistingCodesStillWork(t *testing.T) { + svc := &OpenAIGatewayService{} + + failoverCodes := []int{401, 402, 403, 405, 429, 529, 500, 502, 503, 504} + for _, code := range failoverCodes { + assert.True(t, svc.shouldFailoverUpstreamError(code), "status %d should trigger failover", code) + } + + nonFailoverCodes := []int{200, 201, 400, 404, 408, 422} + for _, code := range nonFailoverCodes { + assert.False(t, svc.shouldFailoverUpstreamError(code), "status %d should NOT trigger failover", code) + } +} diff --git a/backend/internal/service/openai_gateway_grok_test.go b/backend/internal/service/openai_gateway_grok_test.go index 19942eae7d..30c25ba586 100644 --- a/backend/internal/service/openai_gateway_grok_test.go +++ b/backend/internal/service/openai_gateway_grok_test.go @@ -2583,6 +2583,13 @@ func TestHandleGrokAccountUpstreamErrorTempUnschedulesNonRateLimitStates(t *test wantMinCooldown: 2*time.Minute - time.Second, wantMaxCooldown: 2*time.Minute + time.Second, }, + { + name: "method not allowed", + status: http.StatusMethodNotAllowed, + wantReason: "grok endpoint not supported (405)", + wantMinCooldown: 30*time.Minute - time.Second, + wantMaxCooldown: 30*time.Minute + time.Second, + }, } for _, tt := range tests { diff --git a/backend/internal/service/openai_gateway_upstream_errors.go b/backend/internal/service/openai_gateway_upstream_errors.go index f56246e120..e0d3b8cf83 100644 --- a/backend/internal/service/openai_gateway_upstream_errors.go +++ b/backend/internal/service/openai_gateway_upstream_errors.go @@ -211,7 +211,7 @@ func isOpenAIContextWindowError(upstreamMsg string, upstreamBody []byte) bool { func (s *OpenAIGatewayService) shouldFailoverUpstreamError(statusCode int) bool { switch statusCode { - case 401, 402, 403, 429, 529: + case 401, 402, 403, 405, 429, 529: return true default: return statusCode >= 500 From 146b8b668494df0a95fb421cbbfb851a79f3b840 Mon Sep 17 00:00:00 2001 From: li Date: Fri, 7 Aug 2026 20:41:23 +0800 Subject: [PATCH 2/2] =?UTF-8?q?revert(grok):=20=E6=92=A4=E5=9B=9E=20405=20?= =?UTF-8?q?=E8=B4=A6=E5=8F=B7=E7=BA=A7=E8=B8=A2=E5=8F=B7=EF=BC=8C=E5=8F=AA?= =?UTF-8?q?=E4=BF=9D=E7=95=99=20failover=20=E5=88=86=E7=B1=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit tempUnscheduleGrok 是账号级、全端点、全模型的下线,而 handleGrokAccountUpstreamError 有 8 个调用点(raw chat / Grok 媒体 / CC 管线 / WS HTTP 桥接 / responses), /v1/responses 的 405 只证明该端点缺失,账号在其他端点上仍然健康。 仓库里已有相反的既定不变式:shouldApplyOpenAIAlphaSearchAccountErrorSideEffects 把 404/405 显式排除在账号状态写入之外,注释写明「端点不存在只说明该上游不支持 独立搜索,账号本身健康」。 failover 分类器加 405 单独就能解开 sticky 锁死(调度器选号时会即时改绑 sticky), 账号级冷却不是必需的。「405 不踢号但记住端点能力」按端点级负缓存另行处理。 --- backend/internal/service/openai_gateway_grok.go | 2 -- backend/internal/service/openai_gateway_grok_test.go | 7 ------- 2 files changed, 9 deletions(-) diff --git a/backend/internal/service/openai_gateway_grok.go b/backend/internal/service/openai_gateway_grok.go index 87899c1350..4d367c9aeb 100644 --- a/backend/internal/service/openai_gateway_grok.go +++ b/backend/internal/service/openai_gateway_grok.go @@ -1370,8 +1370,6 @@ func (s *OpenAIGatewayService) handleGrokAccountUpstreamError(ctx context.Contex s.tempUnscheduleGrok(ctx, account, 30*time.Minute, "grok payment required") case http.StatusForbidden: s.tempUnscheduleGrok(ctx, account, 30*time.Minute, "grok access or entitlement denied") - case http.StatusMethodNotAllowed: - s.tempUnscheduleGrok(ctx, account, 30*time.Minute, "grok endpoint not supported (405)") case http.StatusTooManyRequests: // updateGrokUsageSnapshot installs rate-limit state for non-pool accounts. default: diff --git a/backend/internal/service/openai_gateway_grok_test.go b/backend/internal/service/openai_gateway_grok_test.go index 30c25ba586..19942eae7d 100644 --- a/backend/internal/service/openai_gateway_grok_test.go +++ b/backend/internal/service/openai_gateway_grok_test.go @@ -2583,13 +2583,6 @@ func TestHandleGrokAccountUpstreamErrorTempUnschedulesNonRateLimitStates(t *test wantMinCooldown: 2*time.Minute - time.Second, wantMaxCooldown: 2*time.Minute + time.Second, }, - { - name: "method not allowed", - status: http.StatusMethodNotAllowed, - wantReason: "grok endpoint not supported (405)", - wantMinCooldown: 30*time.Minute - time.Second, - wantMaxCooldown: 30*time.Minute + time.Second, - }, } for _, tt := range tests {