Repository: danielmiessler/Fabric (2026-04-17T03:18:51Z)

Issue:
[Bug]: codex empty response
### What happened?

`empty response`



### Operating System

Linux - amd64

### OS Version

```shell
Linux 6.19.11-arch1-1 #1 SMP PREEMPT_DYNAMIC Thu, 02 Apr 2026 23:33:01 +0000 x86_64 GNU/Linux
```

### How did you install Fabric?

From Source

### Version

```text
v1.4.446
```

### Relevant log output

```shell
➜ echo "something" | fabric --debug 4 -p summarize

DEBUG: Mapped long flag pattern to yaml tag pattern
DEBUG: Mapped short flag p to yaml tag pattern
DEBUG: Mapped long flag temperature to yaml tag temperature
DEBUG: Mapped short flag t to yaml tag temperature
DEBUG: Mapped long flag topp to yaml tag topp
DEBUG: Mapped short flag T to yaml tag topp
DEBUG: Mapped long flag stream to yaml tag stream
DEBUG: Mapped short flag s to yaml tag stream
DEBUG: Mapped long flag presencepenalty to yaml tag presencepenalty
DEBUG: Mapped short flag P to yaml tag presencepenalty
DEBUG: Mapped long flag raw to yaml tag raw
DEBUG: Mapped short flag r to yaml tag raw
DEBUG: Mapped long flag frequencypenalty to yaml tag frequencypenalty
DEBUG: Mapped short flag F to yaml tag frequencypenalty
DEBUG: Mapped long flag model to yaml tag model
DEBUG: Mapped short flag m to yaml tag model
DEBUG: Mapped long flag vendor to yaml tag vendor
DEBUG: Mapped short flag V to yaml tag vendor
DEBUG: Mapped long flag modelContextLength to yaml tag modelContextLength
DEBUG: Mapped long flag yt-dlp-args to yaml tag ytDlpArgs
DEBUG: Mapped long flag seed to yaml tag seed
DEBUG: Mapped short flag e to yaml tag seed
DEBUG: Mapped long flag suppress-think to yaml tag suppressThink
DEBUG: Mapped long flag think-start-tag to yaml tag thinkStartTag
DEBUG: Mapped long flag think-end-tag to yaml tag thinkEndTag
DEBUG: Mapped long flag disable-responses-api to yaml tag disableResponsesAPI
DEBUG: Mapped long flag transcribe-file to yaml tag transcribeFile
DEBUG: Mapped long flag transcribe-model to yaml tag transcribeModel
DEBUG: Mapped long flag split-media-file to yaml tag splitMediaFile
DEBUG: Mapped long flag voice to yaml tag voice
DEBUG: Mapped long flag notification to yaml tag notification
DEBUG: Mapped long flag notification-command to yaml tag notificationCommand
DEBUG: Mapped long flag thinking to yaml tag thinking
DEBUG: CLI flag used: p (yaml: pattern)
DEBUG: Codex configure: authenticated account=<ID> base_url=https://chatgpt.com/backend-api/codex
DEBUG: Starting template processing with input='something'
DEBUG: Template processing complete
DEBUG: FABRIC->LLM request messages (1)
DEBUG: FABRIC->LLM [0] role=user content="# IDENTITY and PURPOSE\n\nYou are an expert content summarizer. You take content in and output aMarkdown formatted summary using the format below.\n\nTake a deep breath and think step by step about how to best accomplish this goal using the following steps.\n\n# OUTPUT SECTIONS\n\n- Combine all of your understanding of the content into a single, 20-word sentence in a section called ONE SENTENCE SUMMARY:.\n\n- Output the 10 most important points of the content as a list with no more than 16 words per point into a section called MAIN POINTS:.\n\n- Output a list of the 5 best takeaways from the content in a section called TAKEAWAYS:.\n\n# OUTPUT INSTRUCTIONS\n\n- Create the output using the formatting above.\n- You only output human readable Markdown.\n- Output numbered lists, not bullets.\n- Do not output warnings or notes—just the requested sections.\n- Do not repeat items in the output sections.\n- Do not start items with the same opening words.\n\n# INPUT:\n\nINPUT:\nsomething"
DEBUG: LLM->FABRIC response content=""

empty response
```

### Relevant screenshots (optional)

_No response_


Discussion:
Fixed in the next version, @svonjoi 



Patch:
diff --git a/internal/plugins/ai/codex/codex.go b/internal/plugins/ai/codex/codex.go
index b1b531f174..5ba59ff7d8 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.


Test patch:
diff --git a/internal/plugins/ai/codex/codex_test.go b/internal/plugins/ai/codex/codex_test.go
index 5989956121..92430b567b 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" {