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