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.
This commit is contained in:
viettranx committed 2026-04-18 14:42:02 +07:00
1 parent 222ccfbc5b
commit 339b13e8f0
2 files changed
+30 -9

No files matched your search

+18 -3
View File
@@ -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
}
+12 -6
View File
@@ -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 {