From 78f9d7c1f7a2dffdc950c4112e045ad600fafe4d Mon Sep 17 00:00:00 2001 From: Kayvan Sylvan Date: Thu, 16 Apr 2026 20:16:23 -0700 Subject: [PATCH] fix: fall back to streamed delta text when completed Codex response is empty - Prefer extracted completed text only when content stays non-empty - Fall back to accumulated streamed delta text otherwise - Preserve streamed response text before completed response evaluation - Add regression test for empty completed output text - Simulate SSE delta stream followed by blank completion - Verify Send returns delta text when completion lacks content --- internal/plugins/ai/codex/codex.go | 7 +++- internal/plugins/ai/codex/codex_test.go | 55 +++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/internal/plugins/ai/codex/codex.go b/internal/plugins/ai/codex/codex.go index b1b531f1..5ba59ff7 100644 --- a/internal/plugins/ai/codex/codex.go +++ b/internal/plugins/ai/codex/codex.go @@ -242,11 +242,14 @@ func (c *Client) Send(ctx context.Context, msgs []*chat.ChatCompletionMessage, o if err := c.mapRequestError(stream.Err()); err != nil { return "", err } + streamedText := builder.String() if completedResp != nil { - return c.ExtractText(completedResp), nil + if extractedText := c.ExtractText(completedResp); strings.TrimSpace(extractedText) != "" { + return extractedText, nil + } } - return builder.String(), nil + return streamedText, nil } // SendStream sends a request to Codex and streams the response text updates. diff --git a/internal/plugins/ai/codex/codex_test.go b/internal/plugins/ai/codex/codex_test.go index 59899561..92430b56 100644 --- a/internal/plugins/ai/codex/codex_test.go +++ b/internal/plugins/ai/codex/codex_test.go @@ -429,6 +429,61 @@ func TestSendIncludesSourcesFromAnnotatedResponse(t *testing.T) { } } +func TestSendFallsBackToDeltaWhenCompletedResponseHasNoText(t *testing.T) { + apiServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/responses" { + http.NotFound(w, r) + return + } + + w.Header().Set("Content-Type", "text/event-stream") + flusher, ok := w.(http.Flusher) + if !ok { + t.Fatalf("response writer does not implement http.Flusher") + } + fmt.Fprintf(w, "data: %s\n\n", marshalJSON(t, map[string]any{ + "type": string(constant.ResponseOutputTextDelta("").Default()), + "delta": "hello from delta", + })) + flusher.Flush() + fmt.Fprintf(w, "data: %s\n\n", marshalJSON(t, map[string]any{ + "type": "response.completed", + "response": map[string]any{ + "output": []any{ + map[string]any{ + "type": "message", + "content": []any{ + map[string]any{ + "type": "output_text", + "text": "", + }, + }, + }, + }, + }, + })) + flusher.Flush() + fmt.Fprint(w, "data: [DONE]\n\n") + })) + defer apiServer.Close() + + client := newConfiguredTestClient(t, apiServer.URL, "acct_delta_fallback", testJWT("acct_delta_fallback", time.Now().Add(time.Hour))) + + message, err := client.Send(context.Background(), []*chat.ChatCompletionMessage{ + {Role: chat.ChatMessageRoleUser, Content: "Hello"}, + }, &domain.ChatOptions{ + Model: "gpt-5.4", + Temperature: 0.7, + }) + if err != nil { + t.Fatalf("Send() error = %v", err) + } + + if message != "hello from delta" { + t.Fatalf("Send() = %q, want %q", message, "hello from delta") + } +} + func TestSendStreamReadsCodexSSE(t *testing.T) { apiServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/responses" {