Skip to content

Yield final image when streaming with HostedImageGenerationTool - #7774

Open
iamAdarshh wants to merge 2 commits into
dotnet:mainfrom
iamAdarshh:fix/7758-streaming-final-image
Open

iamAdarshh wants to merge 2 commits into
dotnet:mainfrom
iamAdarshh:fix/7758-streaming-final-image

Conversation

@iamAdarshh

@iamAdarshh iamAdarshh commented Sep 23, 2026 •

Copy link
Copy Markdown

Fixes #7758. Also fixes microsoft/agent-framework#8674.

Problem

When streaming a Responses API response that uses HostedImageGenerationTool, the final generated image was never surfaced. In OpenAIResponsesChatClient.GetStreamingResponseAsync, the completed ImageGenerationCallResponseItem (from response.output_item.done) was grouped with MessageResponseItem/ReasoningResponseItem, whose content has already been yielded via deltas, so an empty update was produced and ImageResultBytes was dropped.

Partial images are lower-quality intermediate renders, not deltas of the final image. As a result:

  • With StreamingCount set, consumers only received the partial images, and the last one they saw was a partial render.
  • Without StreamingCount, streaming produced no image at all.

Fix

  • Give ImageGenerationCallResponseItem its own case in the streaming path that yields an ImageGenerationToolResultContent with the final image.
  • Extract CreateImageGenerationResultContent, shared by the streaming and non-streaming (AddImageGenerationContents) paths, so both build the result identically.
  • Like the non-streaming path, a result is produced for every ImageGenerationCallResponseItem, including one without ImageResultBytes.

Coalescing: CoalesceImageResultContent replaces earlier results with later ones for the same CallId, so ToChatResponse() ends up with just the final image.

Roundtripping: the final result's RawRepresentation is the ImageGenerationCallResponseItem, the same as in the non-streaming path, so it is sent back once. Partial results carry a streaming update rather than a ResponseItem and are not sent back, so there is no duplication.

Tests

  • HostedImageGenerationTool_Streaming_YieldsFinalImageAfterPartialImages: StreamingCount = 3 yields 3 partial images followed by the final image, and coalescing keeps only the final image.
  • HostedImageGenerationTool_Streaming_WithoutPartialImages_YieldsFinalImage: without StreamingCount, exactly one image result (the final image) is yielded.
  • HostedImageGenerationTool_StreamingAndNonStreaming_ProduceSameImageContents: streaming and non-streaming produce the same tool call and result contents, both for a completed item and for a failed item with no result.

All of them fail without the fix. I also updated the existing streaming fixtures from image_result_b64 to result, which is the property the OpenAI SDK actually reads for ImageResultBytes; the old name was silently ignored.

Microsoft.Extensions.AI.OpenAI.Tests passes on net8.0, net9.0 and net10.0.

Microsoft Reviewers: Open in CodeFlow

Copilot AI lite review requested due to automatic review settings September 23, 2026 11:24
@iamAdarshh
iamAdarshh requested a review from a team as a code owner September 23, 2026 11:24

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.

Copilot review overview

🟢 Approval recommended

The fix is covered by regression tests and there are no unresolved review comments.

Review effort: Lite
Findings: None

What changed in this PR

Fixes streaming HostedImageGenerationTool responses so the final generated image is surfaced correctly.

Changes:

  • Handles completed image-generation results during streaming.
  • Shares result construction between streaming and non-streaming paths.
  • Adds regression tests and corrects response fixtures.
File Summary
test/​Libraries/​Microsoft.Extensions.AI.OpenAI.Tests/​OpenAIResponseClientTests.cs Adds streaming image-generation regression tests and updates fixtures.
src/​Libraries/​Microsoft.Extensions.AI.OpenAI/​OpenAIResponsesChatClient.cs Emits final streamed images and shares result creation logic.

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

@iamAdarshh

Copy link
Copy Markdown
Author

@dotnet-policy-service agree

When streaming a Responses API response that uses image generation, the
completed ImageGenerationCallResponseItem was treated like items whose
content had already been fully yielded via deltas, so an empty update was
produced and the final image was dropped. Partial images are intermediate
renders, not deltas of the final image, so consumers only ever saw the last
partial image, or no image at all when StreamingCount was not set.

Yield an ImageGenerationToolResultContent for the completed item, built the
same way as in the non-streaming path. Coalescing replaces the partial
results with the final one since they share the same CallId.

Also fix the streaming test fixtures to use the "result" property that the
OpenAI SDK reads for the final image bytes.

Fixes dotnet#7758
@iamAdarshh
iamAdarshh force-pushed the fix/7758-streaming-final-image branch from e9a9a47 to 38cdad5 Compare September 23, 2026 11:55
@dotnet-comment-bot

Copy link
Copy Markdown
Collaborator

‼️ Found issues ‼️

Project Coverage Type Expected Actual
Microsoft.Extensions.Diagnostics.Testing Line 99 98.65 🔻
Microsoft.Extensions.Telemetry Line 93 92.64 🔻
Microsoft.Extensions.AI.OpenAI Line 75 68.34 🔻
Microsoft.Extensions.AI.OpenAI Branch 75 57.64 🔻
Microsoft.Extensions.DataIngestion.MarkItDown Line 75 4.46 🔻
Microsoft.Extensions.DataIngestion.MarkItDown Branch 75 0 🔻
Microsoft.Extensions.Diagnostics.ResourceMonitoring Line 99 96.03 🔻
Microsoft.Extensions.Diagnostics.ResourceMonitoring Branch 99 92.76 🔻
Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes Line 99 97.73 🔻
Microsoft.Extensions.ServiceDiscovery.Dns Line 75 71.61 🔻
Microsoft.Extensions.ServiceDiscovery.Abstractions Line 75 42.11 🔻
Microsoft.Extensions.ServiceDiscovery.Abstractions Branch 75 42.86 🔻
Microsoft.Extensions.ServiceDiscovery Line 75 68.88 🔻
Microsoft.Extensions.ServiceDiscovery Branch 75 71.43 🔻
Microsoft.Extensions.ServiceDiscovery.Yarp Line 75 73.85 🔻
Microsoft.Extensions.ServiceDiscovery.Yarp Branch 75 70 🔻
Microsoft.Extensions.VectorData.Abstractions Line 75 37.39 🔻
Microsoft.Extensions.VectorData.Abstractions Branch 75 22.73 🔻

🎉 Good job! The coverage increased 🎉
Update MinCodeCoverage in the project files.

Project Expected Actual
Microsoft.Extensions.Http.Diagnostics 94 95
Microsoft.Gen.BuildMetadata 97 100
Microsoft.Gen.MetadataExtractor 57 73
Microsoft.Gen.MetricsReports 67 69
Microsoft.Extensions.AI.Abstractions 82 86
Microsoft.Extensions.AI.Evaluation.NLP 0 78
Microsoft.Extensions.Caching.Hybrid 82 84
Microsoft.Extensions.DataIngestion 75 89
Microsoft.Extensions.DataIngestion.Markdig 75 90
Microsoft.Extensions.Http.Resilience 97 100

Full code coverage report: https://dev.azure.com/dnceng-public/public/_build/results?buildId=1608856&view=codecoverage-tab

@github-actions github-actions Bot added the area-ai Microsoft.Extensions.AI libraries label Sep 24, 2026
// but any partial images yielded along the way are lower-quality intermediate renders rather than
// deltas of the final image. Yield the final image here; coalescing will replace the partial
// results with this one, since they share the same CallId.
case ImageGenerationCallResponseItem { ImageResultBytes: not null } imageGenItem:

@jozkee jozkee Oct 7, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Should we remove the ImageResultBytes: not null guard here? The non-streaming path handles every ImageGenerationCallResponseItem unconditionally via AddImageGenerationContents, so the guard introduces a streaming/non-streaming asymmetry when ImageResultBytes is null.

We should also cover that with a test.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Good catch, removed the guard in 21edcd9 so streaming now produces a result for every ImageGenerationCallResponseItem, same as AddImageGenerationContents. I turned HostedImageGenerationTool_StreamingAndNonStreaming_ProduceSameImageContents into a theory that also covers a failed item with no result; that case fails with the guard in place and passes without it.

@jozkee jozkee added waiting-author-feedback 📭 The author of this issue needs to respond in order for us to continue investigating this issue. and removed waiting-author-feedback 📭 The author of this issue needs to respond in order for us to continue investigating this issue. labels Oct 7, 2026
…eaming

Drop the ImageResultBytes guard so the streaming path produces an
ImageGenerationToolResultContent for every ImageGenerationCallResponseItem,
matching the non-streaming path. Extend the streaming/non-streaming parity
test to cover an item without a result.

@jozkee jozkee left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks.

@jozkee
jozkee enabled auto-merge (squash) October 9, 2026 16:52
@dotnet-comment-bot

Copy link
Copy Markdown
Collaborator

‼️ Found issues ‼️

Project Coverage Type Expected Actual
Microsoft.Extensions.Diagnostics.Testing Line 99 98.65 🔻
Microsoft.Extensions.Telemetry Line 93 92.32 🔻
Microsoft.Extensions.AI.OpenAI Line 75 68.34 🔻
Microsoft.Extensions.AI.OpenAI Branch 75 57.74 🔻
Microsoft.Extensions.DataIngestion.MarkItDown Line 75 4.46 🔻
Microsoft.Extensions.DataIngestion.MarkItDown Branch 75 0 🔻
Microsoft.Extensions.Diagnostics.ResourceMonitoring Line 99 96.03 🔻
Microsoft.Extensions.Diagnostics.ResourceMonitoring Branch 99 92.76 🔻
Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes Line 99 97.73 🔻
Microsoft.Extensions.ServiceDiscovery.Dns Line 75 69.93 🔻
Microsoft.Extensions.ServiceDiscovery Line 75 67.21 🔻
Microsoft.Extensions.ServiceDiscovery Branch 75 71.43 🔻
Microsoft.Extensions.ServiceDiscovery.Abstractions Line 75 42.11 🔻
Microsoft.Extensions.ServiceDiscovery.Abstractions Branch 75 42.86 🔻
Microsoft.Extensions.ServiceDiscovery.Yarp Line 75 73.85 🔻
Microsoft.Extensions.ServiceDiscovery.Yarp Branch 75 70 🔻
Microsoft.Extensions.VectorData.Abstractions Line 75 37.39 🔻
Microsoft.Extensions.VectorData.Abstractions Branch 75 22.73 🔻

🎉 Good job! The coverage increased 🎉
Update MinCodeCoverage in the project files.

Project Expected Actual
Microsoft.Extensions.Http.Diagnostics 94 95
Microsoft.Gen.BuildMetadata 97 100
Microsoft.Gen.MetadataExtractor 57 73
Microsoft.Gen.MetricsReports 67 69
Microsoft.Extensions.AI.Abstractions 82 86
Microsoft.Extensions.AI.Evaluation.NLP 0 78
Microsoft.Extensions.Caching.Hybrid 82 85
Microsoft.Extensions.DataIngestion 75 89
Microsoft.Extensions.DataIngestion.Markdig 75 90
Microsoft.Extensions.Http.Resilience 97 100

Full code coverage report: https://dev.azure.com/dnceng-public/public/_build/results?buildId=1630024&view=codecoverage-tab

This branch has not been deployed

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

Labels

area-ai Microsoft.Extensions.AI libraries

Projects

None yet

5 participants