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
16 changes: 15 additions & 1 deletion provider/openaiprovider/responses.go
Original file line number Diff line number Diff line change
Expand Up @@ -1357,6 +1357,10 @@ type responsesStreamState struct {
messageID string
role message.Role
anyFunctions bool
// imagePartialsSeen records image-generation item IDs that emitted a
// partial_image event, so the output_item.done handler does not emit a
// duplicate final image for them.
imagePartialsSeen map[string]bool
}

// responsesProcessStreamingUpdate processes a streaming update from the Responses API.
Expand Down Expand Up @@ -1532,6 +1536,10 @@ func responsesProcessStreamingUpdate(update responses.ResponseStreamEventUnion,
}

case responses.ResponseImageGenCallPartialImageEvent:
if state.imagePartialsSeen == nil {
state.imagePartialsSeen = map[string]bool{}
}
state.imagePartialsSeen[event.ItemID] = true
result := imageGenerationResult(event.ItemID, event.PartialImageB64, cmp.Or(event.OutputFormat, "png"), event)
result.Outputs[0].Header().AdditionalProperties = map[string]any{
"ItemId": event.ItemID,
Expand Down Expand Up @@ -1626,7 +1634,13 @@ func responsesProcessStreamingUpdate(update responses.ResponseStreamEventUnion,
case responses.ResponseFunctionWebSearch:
u.Contents = webSearchContents(item)
case responses.ResponseOutputItemImageGenerationCall:
// Dedicated image-generation events emit the call and partial results.
// The dedicated partial_image events emit the image incrementally, but
// they are only sent when partial_images > 0. In the default case no
// partial event arrives and the finished image lives on the done item,
// so emit it here unless a partial was already seen for this item.
if !state.imagePartialsSeen[item.ID] {
u.Contents = imageGenerationContents(item)
Comment on lines +1641 to +1642
}
case responses.ResponseReasoningItem:
// Carry the completed reasoning item's encrypted content so it can be
// replayed on the next turn when store=false (reasoning delta events only
Expand Down
49 changes: 49 additions & 0 deletions provider/openaiprovider/responses_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7779,3 +7779,52 @@ func TestResponsesToolResult_StructSerializedAsJSON(t *testing.T) {
t.Errorf("tool result was not JSON-encoded (missing field temp_c):\n%s", captured)
}
}

func TestResponsesStreamingImageGenerationCall_NoPartial_EmitsResultOnDone(t *testing.T) {
// The partial_image events are only sent when partial_images > 0. In the
// default case the finished image arrives only on output_item.done, so the
// result must still be surfaced from there.
const imageBase64 = "iVBORw0KGgo="
const input = `
{
"model":"gpt-4o-mini",
"input":[{"type":"message","role":"user","content":[{"type":"input_text","text":"draw"}]}],
"stream":true
}
`
const output = `event: response.created
data: {"type":"response.created","sequence_number":0,"response":{"id":"resp_001","object":"response","created_at":1741892091,"status":"in_progress","model":"gpt-4o-mini","output":[]}}

event: response.image_generation_call.in_progress
data: {"type":"response.image_generation_call.in_progress","sequence_number":1,"output_index":0,"item_id":"ig_123"}

event: response.output_item.done
data: {"type":"response.output_item.done","sequence_number":2,"output_index":0,"item":{"type":"image_generation_call","id":"ig_123","status":"completed","result":"` + imageBase64 + `"}}

event: response.completed
data: {"type":"response.completed","sequence_number":3,"response":{"id":"resp_001","object":"response","created_at":1741892091,"status":"completed","model":"gpt-4o-mini","output":[]}}

`
server := newTestResponsesServerStreaming(t, input, output)
defer server.Close()

a := newTestResponsesClient(server, "gpt-4o-mini")
var result *message.ImageGenerationToolResultContent
for update, err := range a.RunText(t.Context(), "draw", agent.Stream(true)) {
if err != nil {
t.Fatalf("error = %v", err)
}
for _, content := range update.Contents {
if r, ok := content.(*message.ImageGenerationToolResultContent); ok {
result = r
}
}
}
if result == nil || result.CallID != "ig_123" || len(result.Outputs) != 1 {
t.Fatalf("result = %#v, want one output for CallID ig_123", result)
}
image, ok := result.Outputs[0].(*message.DataContent)
if !ok || image.Data != imageBase64 {
t.Fatalf("output = %#v, want DataContent with the image", result.Outputs[0])
}
}
Loading