Skip to content

Emit streamed image-generation result on output_item.done when no partial was sent - #1060

Open
PratikDhanave (PratikDhanave) wants to merge 1 commit into
microsoft:mainfrom
PratikDhanaveFork:responses-streaming-imagegen-done
Open

PratikDhanave (PratikDhanave) wants to merge 1 commit into
microsoft:mainfrom
PratikDhanaveFork:responses-streaming-imagegen-done

Conversation

@PratikDhanave

Copy link
Copy Markdown
Contributor

Problem

In the streaming Responses handler, the ResponseOutputItemImageGenerationCall case on output_item.done is a no-op:

case responses.ResponseOutputItemImageGenerationCall:
    // Dedicated image-generation events emit the call and partial results.

It delegates image emission entirely to response.image_generation_call.partial_image events. But the Responses API only sends those when partial_images > 0, which this provider never requests (no partial_images anywhere in non-test source). In the default case the finished image arrives only on output_item.done, so a streamed image generation drops the result entirely — while the non-streaming path (imageGenerationContents) still emits it. This is a streaming-vs-non-streaming parity gap with user-visible data loss, introduced when #1038 rewrote this handler.

Fix

Emit the finished image from the done item unless a partial_image event was already seen for that item. responsesStreamState now tracks the image-generation item IDs that produced partials, so:

  • default (no partials): the image is emitted from output_item.done;
  • partials enabled: the done handler stays a no-op for that item, preserving the incremental partial results (with their webp/metadata) and avoiding a duplicate.

Test

TestResponsesStreamingImageGenerationCall_NoPartial_EmitsResultOnDone streams an image generation with no partial_image event and asserts the ImageGenerationToolResultContent with the image is surfaced. Fails before the fix (result dropped), passes after. The existing ..._MapsToToolContents partial-path test remains green (no double-emit).

…tial was sent

The streaming ResponseOutputItemImageGenerationCall done handler was a no-op,
delegating image emission to the partial_image events. But the Responses API
only sends partial_image events when partial_images > 0, which this provider
never requests. In the default case the finished image arrives only on
output_item.done, so a streamed image generation dropped the result entirely -
while the non-streaming path still emits it (a streaming-vs-non-streaming
parity gap with user-visible data loss).

Emit the finished image from the done item unless a partial_image event was
already seen for that item (tracked in responsesStreamState), so the default
no-partial case is covered without double-emitting when partials are enabled.
Copilot AI lite review requested due to automatic review settings September 13, 2026 12:35
@github-actions github-actions Bot added area:provider Changes files in the provider area area:provider/openai Changes files in the provider / openai area size:medium At most 100 changed lines across at most 5 files labels Sep 13, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The done-item path can duplicate tool calls, resumed streams can lose partial state, and the test does not assert exactly one call and result.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

Fixes streamed image-generation results being dropped when no partial image event is emitted.

Changes:

  • Tracks image items that emit partial results.
  • Emits completed images from output_item.done when needed.
  • Adds regression coverage for the no-partial path.
File summaries
File Description
provider/openaiprovider/responses.go Updates streaming image result handling.
provider/openaiprovider/responses_test.go Adds no-partial streaming coverage.
Review details

Suppressed comments (2)

provider/openaiprovider/responses.go:1641

  • When a background stream is resumed, responses.go:150 creates a fresh responsesStreamState, and the GET starts after the continuation token's sequence number. If that token was emitted for a partial_image event, the prior partial is not replayed, so this lookup is false at output_item.done and emits the final image again, defeating the no-duplicate behavior across resumption. Persist the partial item IDs in the continuation state (or otherwise restore them when resuming) before applying this check.
			if !state.imagePartialsSeen[item.ID] {

provider/openaiprovider/responses_test.go:7821

  • This test only retains the last result pointer, so it passes even if the no-partial path emits duplicate call/result content. Count the streamed image-generation call and result contents and assert exactly one of each; that would catch the duplicate introduced by the done handler and protect the partial/no-partial distinction.
		for _, content := range update.Contents {
			if r, ok := content.(*message.ImageGenerationToolResultContent); ok {
				result = r
			}
		}
  • Files reviewed: 2/2 changed files
  • Comments generated: 1
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +1641 to +1642
if !state.imagePartialsSeen[item.ID] {
u.Contents = imageGenerationContents(item)
@github-actions github-actions Bot added kind:code Changes production behavior or code kind:tests Changes tests, fixtures, or test infrastructure pending-auto-risk labels Sep 13, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Scope: internal-only (bug fix in unexported streaming state; no exported Go API changed)

Changed Go contract: None exported. responsesStreamState (unexported struct) gains an internal imagePartialsSeen map[string]bool field so the response.output_item.done handler for ResponseOutputItemImageGenerationCall emits the finished image (via imageGenerationContents(item)) when no response.image_generation_call.partial_image event was seen for that item, instead of being an unconditional no-op. This restores parity between Go's own streaming and non-streaming paths (a regression from #1038); no public types, methods, or options were added, removed, or changed.

Upstream evidence reviewed:

  • Python: python/packages/openai/agent_framework_openai/_chat_client.py — the response.output_item.done match statement (~line 3487) has no image_generation_call branch at all; only response.image_generation_call.partial_image (~line 3360) emits image content during streaming. Python's partial_images tool option (~line 1175) is opt-in and None by default, meaning Python's streaming path drops the finished image entirely when partials aren't requested — the same gap this Go PR fixes, except Python still has it. The non-streaming path (case "image_generation_call": ~line 2905) does emit the image from the response item, matching Go's non-streaming imageGenerationContents.
  • .NET: searched dotnet/src/Microsoft.Agents.AI* for ImageGenerationCall/PartialImage; only server-side hosting converters (Microsoft.Agents.AI.Hosting.OpenAI/Responses/...) reference image-generation items — no client-side Responses streaming handler exists to compare against.

Result: aligned (no cross-repo divergence). This is a Go-internal correctness fix that brings the Go streaming path in line with its own non-streaming behavior; it does not diverge from upstream since Python has an equivalent (unfixed) gap and .NET has no comparable client-side streaming handler. No exported Go API surface changed, so public-api-change is not applicable.

Generated by Go API Consistency Review Agent · copilot · auto · 45.1 AIC · ⌖ 5.28 AIC · ⊞ 9.6K ·

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:provider/openai Changes files in the provider / openai area area:provider Changes files in the provider area kind:code Changes production behavior or code kind:tests Changes tests, fixtures, or test infrastructure size:medium At most 100 changed lines across at most 5 files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants