Merge pull request #7775 from YahooYuan666/fix/grok-empty-completed-silent-refusal

fix(grok): fail over silent-refusal empty response.completed on Grok Responses streams
This commit is contained in:
Wesley Liddick
2026-10-08 11:28:22 +08:00
committed by GitHub
2 changed files with 74 additions and 2 deletions
@@ -687,8 +687,12 @@ func (s *OpenAIGatewayService) handleStreamingResponseWithReasoning(ctx context.
// OpenAI Responses streams that terminate with an empty
// response.completed (no output, no usage, no error, nothing sent
// to the client) are silent upstream refusals: fail over instead of
// recording a successful 0/0 usage turn (issue #5009).
if account != nil && account.Platform == PlatformOpenAI &&
// recording a successful 0/0 usage turn (issue #5009). Grok
// subscriptions behind vendor gateways show the same silent-refusal
// shape on /v1/responses (forwardGrokResponses reaches this same
// handler), so cover PlatformGrok too — the passthrough-path guard
// is already unconditional.
if account != nil && (account.Platform == PlatformOpenAI || account.Platform == PlatformGrok) &&
(eventType == "response.completed" || eventType == "response.done") &&
!sawFailedEvent && !responsesSemanticOutputSeen && !clientOutputStarted &&
openAIResponsesCompletedEventIsEmpty(dataBytes, usage) {
@@ -166,3 +166,71 @@ func TestOpenAIResponsesCompletedEventIsEmpty(t *testing.T) {
})
}
}
// TestOpenAIResponsesEmptyCompletedFailsOverGrok verifies that Grok platform
// accounts — which reach the stream relay through forwardGrokResponses instead
// of the passthrough path — get the same #5009 silent-refusal failover for an
// empty response.completed (issue #7774).
func TestOpenAIResponsesEmptyCompletedFailsOverGrok(t *testing.T) {
gin.SetMode(gin.TestMode)
upstream := &httpUpstreamRecorder{resp: &http.Response{
StatusCode: http.StatusOK,
Header: http.Header{"Content-Type": []string{"text/event-stream"}},
Body: io.NopCloser(strings.NewReader(
"data: {\"type\":\"response.created\",\"response\":{\"id\":\"resp_grok\",\"object\":\"response\",\"status\":\"in_progress\"}}\n\n" +
"data: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_grok\",\"object\":\"response\",\"status\":\"completed\",\"output\":[]}}\n\n",
)),
}}
svc := newOpenAIImageGenerationControlTestService(upstream)
c, recorder := newOpenAIImageGenerationControlTestContext(true, "codex_cli_rs/0.144.1")
account := newOpenAIImageGenerationControlTestAccount()
account.Platform = PlatformGrok
body := []byte(`{
"model":"grok-4.7",
"stream":true,
"input":[{"type":"message","role":"user","content":[{"type":"input_text","text":"continue"}]}]
}`)
_, err := svc.Forward(context.Background(), c, account, body)
require.Error(t, err)
var failoverErr *UpstreamFailoverError
require.True(t, errors.As(err, &failoverErr), "empty completed must produce UpstreamFailoverError, got: %v", err)
require.Equal(t, http.StatusBadGateway, failoverErr.StatusCode)
require.Empty(t, recorder.Body.String(), "no empty success stream may reach the client")
}
// TestOpenAIResponsesEmptyCompletedWithOutputSucceedsGrok ensures normal Grok
// streams with real semantic output still succeed.
func TestOpenAIResponsesEmptyCompletedWithOutputSucceedsGrok(t *testing.T) {
gin.SetMode(gin.TestMode)
upstream := &httpUpstreamRecorder{resp: &http.Response{
StatusCode: http.StatusOK,
Header: http.Header{"Content-Type": []string{"text/event-stream"}},
Body: io.NopCloser(strings.NewReader(
"data: {\"type\":\"response.created\",\"response\":{\"id\":\"resp_grok_ok\",\"object\":\"response\",\"status\":\"in_progress\"}}\n\n" +
"data: {\"type\":\"response.output_text.delta\",\"delta\":\"hello grok\"}\n\n" +
"data: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_grok_ok\",\"object\":\"response\",\"status\":\"completed\",\"usage\":{\"input_tokens\":11,\"output_tokens\":4,\"total_tokens\":15}}}\n\n",
)),
}}
svc := newOpenAIImageGenerationControlTestService(upstream)
c, recorder := newOpenAIImageGenerationControlTestContext(true, "codex_cli_rs/0.144.1")
account := newOpenAIImageGenerationControlTestAccount()
account.Platform = PlatformGrok
body := []byte(`{
"model":"grok-4.7",
"stream":true,
"input":[{"type":"message","role":"user","content":[{"type":"input_text","text":"continue"}]}]
}`)
result, err := svc.Forward(context.Background(), c, account, body)
require.NoError(t, err)
require.NotNil(t, result)
require.Contains(t, recorder.Body.String(), "hello grok")
require.NotNil(t, result.Usage)
require.Equal(t, 11, result.Usage.InputTokens)
require.Equal(t, 4, result.Usage.OutputTokens)
}