From f364cf1136415b6643d0ba6a31c4b32631e48666 Mon Sep 17 00:00:00 2001 From: PratikDhanave Date: Sat, 18 Jul 2026 23:01:51 +0530 Subject: [PATCH 1/2] Surface the error from a streamed Responses response.failed event MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A response.failed streaming event carries its error on the response object (Response.Error), but responsesProcessStreamingUpdate had no case for ResponseFailedEvent, so it fell through to the default branch and emitted an empty update — silently dropping the failure and its message. Handle ResponseFailedEvent by emitting an ErrorContent built from Response.Error (message + code), mirroring the existing ResponseErrorEvent handling. Adds a streaming test asserting the error is surfaced; the existing failed-update test (which only checks ResponseID) still passes. --- provider/openaiprovider/responses.go | 19 +++++++++- provider/openaiprovider/responses_test.go | 44 +++++++++++++++++++++++ 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/provider/openaiprovider/responses.go b/provider/openaiprovider/responses.go index 371e0801..c7205d19 100644 --- a/provider/openaiprovider/responses.go +++ b/provider/openaiprovider/responses.go @@ -59,7 +59,8 @@ func NewResponsesAgent(oclient openai.Client, config AgentConfig) *agent.Agent { Middlewares: providerMiddlewares, Format: c.formatOf, Unmarshal: c.unmarshal, - }, config.Config) + }, config.Config, + ) } type responsesClient struct { @@ -1040,6 +1041,22 @@ func responsesProcessStreamingUpdate(update responses.ResponseStreamEventUnion, u.ContinuationToken = contToken } + case responses.ResponseFailedEvent: + // A failed response carries its error on the response object rather than + // as a top-level ResponseErrorEvent; surface it as ErrorContent instead + // of letting it fall through to the default (empty) update. + u = createUpdate(message.RoleAssistant, nil) + u.CreatedAt = time.Unix(int64(event.Response.CreatedAt), 0) + u.ResponseID = event.Response.ID + u.FinishReason = responsesFinishReason(&event.Response) + u.AdditionalProperties = responsesPopulateAdditionalProperties(&event.Response) + if event.Response.Error.Message != "" { + u.Contents = []message.Content{&message.ErrorContent{ + Message: event.Response.Error.Message, + ErrorCode: string(event.Response.Error.Code), + }} + } + case responses.ResponseTextDeltaEvent: u = createUpdate(message.RoleAssistant, []message.Content{ &message.TextContent{Text: event.Delta}, diff --git a/provider/openaiprovider/responses_test.go b/provider/openaiprovider/responses_test.go index 4f060310..ecea0879 100644 --- a/provider/openaiprovider/responses_test.go +++ b/provider/openaiprovider/responses_test.go @@ -5546,3 +5546,47 @@ func responsesBodyEqual(t *testing.T, got string, want string) { t.Errorf("body\ngot %s\nwant %s", gotOut, wantOut) } } + +// TestResponsesStreamingFailedResponseSurfacesError verifies that a streamed +// response.failed event surfaces its error as ErrorContent rather than an +// empty update. +func TestResponsesStreamingFailedResponseSurfacesError(t *testing.T) { + const input = ` + { + "model":"gpt-4o-mini", + "input":[{"type":"message","role":"user","content":[{"type":"input_text","text":"test"}]}], + "stream":true + } + ` + const output = `event: response.created +data: {"type":"response.created","response":{"id":"resp_001","object":"response","created_at":1741892091,"status":"in_progress","model":"gpt-4o-mini","output":[]}} + +event: response.failed +data: {"type":"response.failed","response":{"id":"resp_001","object":"response","created_at":1741892091,"status":"failed","model":"gpt-4o-mini","output":[],"error":{"code":"server_error","message":"Internal error"}}} + +` + server := newTestResponsesServerStreaming(t, input, output) + defer server.Close() + a := newTestResponsesClient(server, "gpt-4o-mini") + + var errContent *message.ErrorContent + for update, err := range a.RunText(t.Context(), "test", agent.Stream(true)) { + if err != nil { + t.Fatalf("error = %v", err) + } + for _, c := range update.Contents { + if ec, ok := c.(*message.ErrorContent); ok { + errContent = ec + } + } + } + if errContent == nil { + t.Fatal("expected an ErrorContent for the failed response, got none") + } + if errContent.Message != "Internal error" { + t.Errorf("error message = %q, want %q", errContent.Message, "Internal error") + } + if errContent.ErrorCode != "server_error" { + t.Errorf("error code = %q, want %q", errContent.ErrorCode, "server_error") + } +} From e8a4aaa2fe622982d4e69ae25fba2d37d56d66ad Mon Sep 17 00:00:00 2001 From: PratikDhanave Date: Sun, 19 Jul 2026 13:56:58 +0530 Subject: [PATCH 2/2] Surface code-only errors from a failed Responses event A response.failed event whose error carries a code but no message was collapsing into an empty update, dropping the failure signal. Emit ErrorContent when either the message or the code is populated. --- provider/openaiprovider/responses.go | 2 +- provider/openaiprovider/responses_test.go | 44 +++++++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/provider/openaiprovider/responses.go b/provider/openaiprovider/responses.go index c7205d19..61848804 100644 --- a/provider/openaiprovider/responses.go +++ b/provider/openaiprovider/responses.go @@ -1050,7 +1050,7 @@ func responsesProcessStreamingUpdate(update responses.ResponseStreamEventUnion, u.ResponseID = event.Response.ID u.FinishReason = responsesFinishReason(&event.Response) u.AdditionalProperties = responsesPopulateAdditionalProperties(&event.Response) - if event.Response.Error.Message != "" { + if event.Response.Error.Message != "" || event.Response.Error.Code != "" { u.Contents = []message.Content{&message.ErrorContent{ Message: event.Response.Error.Message, ErrorCode: string(event.Response.Error.Code), diff --git a/provider/openaiprovider/responses_test.go b/provider/openaiprovider/responses_test.go index ecea0879..305ecae9 100644 --- a/provider/openaiprovider/responses_test.go +++ b/provider/openaiprovider/responses_test.go @@ -5590,3 +5590,47 @@ data: {"type":"response.failed","response":{"id":"resp_001","object":"response", t.Errorf("error code = %q, want %q", errContent.ErrorCode, "server_error") } } + +// TestResponsesStreamingFailedResponseSurfacesCodeOnlyError guards the case where a +// failed response carries an error code but no message. The failure must still be +// surfaced as ErrorContent rather than collapsing into an empty update. +func TestResponsesStreamingFailedResponseSurfacesCodeOnlyError(t *testing.T) { + const input = ` + { + "model":"gpt-4o-mini", + "input":[{"type":"message","role":"user","content":[{"type":"input_text","text":"test"}]}], + "stream":true + } + ` + const output = `event: response.created +data: {"type":"response.created","response":{"id":"resp_001","object":"response","created_at":1741892091,"status":"in_progress","model":"gpt-4o-mini","output":[]}} + +event: response.failed +data: {"type":"response.failed","response":{"id":"resp_001","object":"response","created_at":1741892091,"status":"failed","model":"gpt-4o-mini","output":[],"error":{"code":"server_error"}}} + +` + server := newTestResponsesServerStreaming(t, input, output) + defer server.Close() + a := newTestResponsesClient(server, "gpt-4o-mini") + + var errContent *message.ErrorContent + for update, err := range a.RunText(t.Context(), "test", agent.Stream(true)) { + if err != nil { + t.Fatalf("error = %v", err) + } + for _, c := range update.Contents { + if ec, ok := c.(*message.ErrorContent); ok { + errContent = ec + } + } + } + if errContent == nil { + t.Fatal("expected an ErrorContent for the code-only failed response, got none") + } + if errContent.ErrorCode != "server_error" { + t.Errorf("error code = %q, want %q", errContent.ErrorCode, "server_error") + } + if errContent.Message != "" { + t.Errorf("error message = %q, want empty", errContent.Message) + } +}