Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion provider/openaiprovider/responses.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 != "" || event.Response.Error.Code != "" {
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},
Expand Down
88 changes: 88 additions & 0 deletions provider/openaiprovider/responses_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5546,3 +5546,91 @@ 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")
}
}

// 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)
}
}