From 339b13e8f09cec8dc9b6dd4ad4e81b847ea7e139 Mon Sep 17 00:00:00 2001 From: viettranx Date: Sat, 18 Apr 2026 14:42:02 +0700 Subject: [PATCH] fix(providers): return error on Codex response.failed instead of silent success (#888) response.failed was lumped with response.completed/incomplete, causing silent empty responses with no retry. Split into its own case that returns a descriptive error from processSSEEvent, propagated up to ChatStream caller for proper error surface and retry logic. Added codexErrorDetail to codexAPIResponse to parse the error payload. --- internal/providers/codex.go | 21 ++++++++++++++++++--- internal/providers/codex_types.go | 18 ++++++++++++------ 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/internal/providers/codex.go b/internal/providers/codex.go index e8803dae..f2d6106d 100644 --- a/internal/providers/codex.go +++ b/internal/providers/codex.go @@ -3,6 +3,7 @@ package providers import ( "context" "encoding/json" + "errors" "fmt" "io" "net/http" @@ -148,7 +149,9 @@ func (p *CodexProvider) ChatStream(ctx context.Context, req ChatRequest, onChunk continue } - p.processSSEEvent(&event, result, toolCalls, streamState, onChunk, stripThinking) + if err := p.processSSEEvent(&event, result, toolCalls, streamState, onChunk, stripThinking); err != nil { + return nil, err + } } if err := sse.Err(); err != nil { @@ -189,7 +192,7 @@ func (p *CodexProvider) ChatStream(ctx context.Context, req ChatRequest, onChunk // processSSEEvent handles a single SSE event during streaming. // stripThinking drops reasoning summaries from user-visible output while // leaving billing counters (Usage.ThinkingTokens) untouched. -func (p *CodexProvider) processSSEEvent(event *codexSSEEvent, result *ChatResponse, toolCalls map[string]*codexToolCallAcc, streamState *codexMessageStreamState, onChunk func(StreamChunk), stripThinking bool) { +func (p *CodexProvider) processSSEEvent(event *codexSSEEvent, result *ChatResponse, toolCalls map[string]*codexToolCallAcc, streamState *codexMessageStreamState, onChunk func(StreamChunk), stripThinking bool) error { switch event.Type { case "response.output_item.added": if event.Item != nil { @@ -249,7 +252,7 @@ func (p *CodexProvider) processSSEEvent(event *codexSSEEvent, result *ChatRespon } } - case "response.completed", "response.incomplete", "response.failed": + case "response.completed", "response.incomplete": if event.Response != nil { if result.Content == "" { streamState.ingestCompletedResponse(event.Response) @@ -271,5 +274,17 @@ func (p *CodexProvider) processSSEEvent(event *codexSSEEvent, result *ChatRespon result.FinishReason = "length" } } + + case "response.failed": + errMsg := "codex: response failed during generation" + if event.Response != nil && event.Response.Error != nil { + if event.Response.Error.Message != "" { + errMsg = fmt.Sprintf("codex: response failed: %s", event.Response.Error.Message) + } else if event.Response.Error.Code != "" { + errMsg = fmt.Sprintf("codex: response failed: %s", event.Response.Error.Code) + } + } + return errors.New(errMsg) } + return nil } diff --git a/internal/providers/codex_types.go b/internal/providers/codex_types.go index 2f030a46..1ca09e24 100644 --- a/internal/providers/codex_types.go +++ b/internal/providers/codex_types.go @@ -3,12 +3,18 @@ package providers // Wire types for the OpenAI Responses API (Codex flow). type codexAPIResponse struct { - ID string `json:"id"` - Object string `json:"object"` - Model string `json:"model"` - Output []codexItem `json:"output"` - Usage *codexUsage `json:"usage,omitempty"` - Status string `json:"status"` + ID string `json:"id"` + Object string `json:"object"` + Model string `json:"model"` + Output []codexItem `json:"output"` + Usage *codexUsage `json:"usage,omitempty"` + Status string `json:"status"` + Error *codexErrorDetail `json:"error,omitempty"` // set when status="failed" +} + +type codexErrorDetail struct { + Code string `json:"code"` + Message string `json:"message"` } type codexItem struct {