diff --git a/backend/internal/handler/openai_gateway_credential_failover_test.go b/backend/internal/handler/openai_gateway_credential_failover_test.go index 2540bf2303..91a3881dfa 100644 --- a/backend/internal/handler/openai_gateway_credential_failover_test.go +++ b/backend/internal/handler/openai_gateway_credential_failover_test.go @@ -133,7 +133,8 @@ func TestResponsesFailoverExhaustedAfterForwardedTerminalMarksOpsWithoutDuplicat recorder := httptest.NewRecorder() c, _ := gin.CreateTestContext(recorder) official := "event: response.failed\ndata: {\"type\":\"response.failed\",\"response\":{\"status\":\"failed\",\"error\":{\"code\":\"server_error\",\"message\":\"official failure\"}}}\n\n" - _, _ = recorder.Write([]byte(official)) + _, err := c.Writer.Write([]byte(official)) + require.NoError(t, err) service.MarkOpsStreamError(c, "server_error", "official failure", http.StatusBadGateway) (&GatewayHandler{}).handleResponsesFailoverExhausted(c, &service.UpstreamFailoverError{ diff --git a/backend/internal/service/openai_gateway_grok.go b/backend/internal/service/openai_gateway_grok.go index cc4709443a..9e2c70579b 100644 --- a/backend/internal/service/openai_gateway_grok.go +++ b/backend/internal/service/openai_gateway_grok.go @@ -984,9 +984,7 @@ func sanitizeGrokResponsesTools(body []byte) ([]byte, error) { return deleteGrokOrphanToolControls(body) } if !tools.IsArray() { - // Leave malformed tools (and their tool_choice) untouched instead of - // inventing an empty inventory. - return body, nil + return deleteGrokOrphanToolControls(body) } rawTools := tools.Array() diff --git a/backend/internal/service/openai_gateway_grok_chat_bridge_test.go b/backend/internal/service/openai_gateway_grok_chat_bridge_test.go index 373907f0d7..2df7bb1f8b 100644 --- a/backend/internal/service/openai_gateway_grok_chat_bridge_test.go +++ b/backend/internal/service/openai_gateway_grok_chat_bridge_test.go @@ -481,7 +481,7 @@ func TestForwardGrokChatViaResponsesTraeCompatibilityFieldsKeepCacheRoute(t *tes require.Equal(t, "priority", gjson.GetBytes(upstream.lastBody, "service_tier").String()) require.False(t, gjson.GetBytes(upstream.lastBody, "stop").Exists()) require.False(t, gjson.GetBytes(upstream.lastBody, "reasoning").Exists()) - require.Contains(t, gjson.GetBytes(upstream.lastBody, "input.1.content.0.text").String(), "I should use lookup") + require.Contains(t, gjson.GetBytes(upstream.lastBody, "input.1.content").String(), "I should use lookup") require.Equal(t, "function_call", gjson.GetBytes(upstream.lastBody, "input.2.type").String()) require.Equal(t, "function_call_output", gjson.GetBytes(upstream.lastBody, "input.3.type").String()) diff --git a/backend/internal/service/openai_gateway_grok_test.go b/backend/internal/service/openai_gateway_grok_test.go index 3c93776d4b..f248cf1951 100644 --- a/backend/internal/service/openai_gateway_grok_test.go +++ b/backend/internal/service/openai_gateway_grok_test.go @@ -418,10 +418,9 @@ func TestSanitizeGrokResponsesToolsKeepsToolChoiceOnlyWithSupportedTools(t *test wantToolChoice: true, }, { - name: "malformed non-array tools remain untouched", - body: `{"input":"hello","tools":{"type":"function","name":"lookup"},"tool_choice":"auto"}`, - wantTools: true, - wantToolChoice: true, + name: "malformed non-array tools drop orphan controls", + body: `{"input":"hello","tools":{"type":"function","name":"lookup"},"tool_choice":"auto"}`, + wantTools: true, }, } diff --git a/backend/internal/service/openai_gateway_passthrough.go b/backend/internal/service/openai_gateway_passthrough.go index 6aad83b268..40931138d4 100644 --- a/backend/internal/service/openai_gateway_passthrough.go +++ b/backend/internal/service/openai_gateway_passthrough.go @@ -1491,7 +1491,15 @@ func openAIStreamErrorEventShouldFailover(payload []byte, message string) bool { case http.StatusUnauthorized, http.StatusTooManyRequests, 529: return true } - return isOpenAITransientProcessingError(http.StatusBadRequest, message, payload) + if isOpenAITransientProcessingError(http.StatusBadRequest, message, payload) { + return true + } + combined := strings.ToLower(strings.TrimSpace(message + " " + + gjson.GetBytes(payload, "error.message").String() + " " + + gjson.GetBytes(payload, "response.error.message").String())) + return strings.Contains(combined, "temporary") || + strings.Contains(combined, "try again") || + strings.Contains(combined, "please retry") } func (s *OpenAIGatewayService) handleOpenAIStreamTerminalAccountSideEffects(