Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
ffe9a92
Prototype of using ImageGenerationTool
ericstj Aug 5, 2025
e5edc77
Handle DataContent returned from ImageGen
ericstj Aug 8, 2025
2d19cce
React to rename and improve metadata
ericstj Aug 9, 2025
5eef474
Handle image_generation tool content from streaming
ericstj Aug 20, 2025
ff80804
Add handling for combining updates with images
ericstj Aug 25, 2025
1725ce1
Add tests for new ChatResponseUpdateExtensions
ericstj Aug 26, 2025
c44f5fb
Merge branch 'main' of https://github.com/dotnet/extensions into Imag…
ericstj Sep 19, 2025
b4fe94b
Rename ImageGenerationTool to HostedImageGenerationTool
ericstj Sep 20, 2025
06bfa30
Remove ChatResponseUpdateCoalescingOptions
ericstj Sep 20, 2025
ca8b15d
Add ImageGeneratingChatClient
ericstj Sep 23, 2025
62e0ac5
Fix namespace of tool
ericstj Sep 26, 2025
81e6e5a
Replace traces of function calling
ericstj Sep 26, 2025
6559a66
More namepsace fix
ericstj Sep 26, 2025
398bbdb
Enable editing
ericstj Sep 30, 2025
ac2de35
Merge branch 'main' of https://github.com/dotnet/extensions into Imag…
ericstj Sep 30, 2025
1d96532
Update to preview OpenAI with image tool support
ericstj Oct 1, 2025
6a6ffa2
Temporary OpenAI feed
ericstj Oct 3, 2025
94ceab2
Fix tests
ericstj Oct 3, 2025
96e9747
Add integration tests for ImageGeneratingChatClient
ericstj Oct 3, 2025
9ddc91a
Remove ChatRole.Tool -> Assistant workaround
ericstj Oct 4, 2025
3b589ac
Remove use of private reflection for Image results
ericstj Oct 6, 2025
20919ab
Add ChatResponseUpdate.Clone
ericstj Oct 6, 2025
e5f68a6
Move all mutable state into RequestState object
ericstj Oct 7, 2025
9f9a430
Adjust prompt to improve integration test reliability
ericstj Oct 7, 2025
799a72e
Refactor tool initialization
ericstj Oct 7, 2025
6029b01
Add integration tests for streaming
ericstj Oct 7, 2025
173352a
Merge remote-tracking branch 'upstream/main' into ImageGenerationTool
ericstj Oct 27, 2025
69d2d98
React to changes and fix tests
ericstj Oct 27, 2025
86363f8
Address feedback
ericstj Oct 28, 2025
94cffbd
Fix SkipTestException from ConditionalTheory
ericstj Oct 30, 2025
67089ab
Merge branch 'main' of https://github.com/dotnet/extensions into Imag…
ericstj Oct 30, 2025
56cf3b4
Fix formatting
ericstj Oct 30, 2025
ad2b953
Add back image replacement coalescing (removed in merge)
ericstj Oct 30, 2025
e20b768
Fix template tests and use new OpenAI
ericstj Oct 30, 2025
b392df3
Merge branch 'main' of https://github.com/dotnet/extensions into Imag…
ericstj Nov 3, 2025
a38fd6c
Remove use of temporary staging nuget feed
ericstj Nov 3, 2025
cfa3f16
Address feedback
ericstj Nov 4, 2025
dcdebef
Make ImageGeneratingChatClient use ImageGenerationTool*Content
ericstj Nov 4, 2025
a60ddd2
Remove ApplyUpdates and Coalesce ImageResults instead of DataContent.
ericstj Nov 5, 2025
50db985
Workaround OpenAI issue where image data is not read for partial images.
ericstj Nov 5, 2025
10467a5
Improved workaround
ericstj Nov 5, 2025
8cff0ae
Return ImageGenerationToolCallContent from OpenAI
ericstj Nov 5, 2025
7780595
Add OpenAI image tool tests with representation of real traffic
ericstj Nov 6, 2025
9b94d68
Correct the event sequence for streaming single image
ericstj Nov 6, 2025
87dfaa4
Fix some docs and refactor for clarity
ericstj Nov 7, 2025
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
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,47 @@ static async Task<ChatResponse> ToChatResponseAsync(
}
}

/// <summary>
/// Coalesces image result content elements in the provided list of <see cref="AIContent"/> items.
/// Unlike other content coalescing methods, this will coalesce non-sequential items based on their Name property,
/// and it will replace earlier items with later ones when duplicates are found.
/// </summary>
private static void CoalesceImageResultContent(IList<AIContent> contents)
{
Dictionary<string, int>? imageResultIndexById = null;
bool hasRemovals = false;

for (int i = 0; i < contents.Count; i++)
{
if (contents[i] is ImageGenerationToolResultContent imageResult && !string.IsNullOrEmpty(imageResult.ImageId))
{
// Check if there's an existing ImageGenerationToolResultContent with the same ImageId to replace
if (imageResultIndexById is null)
{
imageResultIndexById = new(StringComparer.Ordinal);
}

if (imageResultIndexById.TryGetValue(imageResult.ImageId!, out int existingIndex))
{
// Replace the existing imageResult with the new one
contents[existingIndex] = imageResult;
contents[i] = null!; // Mark the current one for removal, then remove in single o(n) pass
hasRemovals = true;
}
else
{
imageResultIndexById[imageResult.ImageId!] = i;
}
}
}

// Remove all of the null slots left over from the coalescing process.
if (hasRemovals)
{
RemoveNullContents(contents);
}
}

/// <summary>Coalesces sequential <see cref="AIContent"/> content elements.</summary>
internal static void CoalesceContent(IList<AIContent> contents)
{
Expand Down Expand Up @@ -219,6 +260,8 @@ internal static void CoalesceContent(IList<AIContent> contents)
return content;
});

CoalesceImageResultContent(contents);

Coalesce<DataContent>(
contents,
mergeSingle: false,
Expand Down Expand Up @@ -394,29 +437,35 @@ static bool TryAsCoalescable(AIContent content, [NotNullWhen(true)] out TContent
}

// Remove all of the null slots left over from the coalescing process.
if (contents is List<AIContent> contentsList)
{
_ = contentsList.RemoveAll(u => u is null);
}
else
{
int nextSlot = 0;
int contentsCount = contents.Count;
for (int i = 0; i < contentsCount; i++)
{
if (contents[i] is { } content)
{
contents[nextSlot++] = content;
}
}
RemoveNullContents(contents);
}
}

for (int i = contentsCount - 1; i >= nextSlot; i--)
private static void RemoveNullContents<T>(IList<T> contents)
where T : class
{
if (contents is List<AIContent> contentsList)
{
_ = contentsList.RemoveAll(u => u is null);
}
else
{
int nextSlot = 0;
int contentsCount = contents.Count;
for (int i = 0; i < contentsCount; i++)
{
if (contents[i] is { } content)
{
contents.RemoveAt(i);
contents[nextSlot++] = content;
}
}

Debug.Assert(nextSlot == contents.Count, "Expected final count to equal list length.");
for (int i = contentsCount - 1; i >= nextSlot; i--)
{
contents.RemoveAt(i);
}

Debug.Assert(nextSlot == contents.Count, "Expected final count to equal list length.");
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ namespace Microsoft.Extensions.AI;
/// </para>
/// <para>
/// The relationship between <see cref="ChatResponse"/> and <see cref="ChatResponseUpdate"/> is
/// codified in the <see cref="ChatResponseExtensions.ToChatResponseAsync"/> and
/// codified in the <see cref="ChatResponseExtensions.ToChatResponseAsync(IAsyncEnumerable{ChatResponseUpdate}, System.Threading.CancellationToken)"/> and
/// <see cref="ChatResponse.ToChatResponseUpdates"/>, which enable bidirectional conversions
/// between the two. Note, however, that the provided conversions might be lossy, for example, if multiple
/// updates all have different <see cref="RawRepresentation"/> objects whereas there's only one slot for
Expand Down Expand Up @@ -58,6 +58,29 @@ public ChatResponseUpdate(ChatRole? role, IList<AIContent>? contents)
_contents = contents;
}

/// <summary>
/// Creates a new ChatResponseUpdate instance that is a copy of the current object.
/// </summary>
/// <remarks>The cloned object is a shallow copy; reference-type properties will reference the same
/// objects as the original. Use this method to duplicate the response update for further modification without
/// affecting the original instance.</remarks>
/// <returns>A new ChatResponseUpdate object with the same property values as the current instance.</returns>
public ChatResponseUpdate Clone() =>
new()
{
AdditionalProperties = AdditionalProperties,
AuthorName = AuthorName,
Contents = Contents,
CreatedAt = CreatedAt,
ConversationId = ConversationId,
FinishReason = FinishReason,
MessageId = MessageId,
ModelId = ModelId,
RawRepresentation = RawRepresentation,
ResponseId = ResponseId,
Role = Role,
};

/// <summary>Gets or sets the name of the author of the response update.</summary>
public string? AuthorName
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics.CodeAnalysis;

namespace Microsoft.Extensions.AI;

/// <summary>
/// Represents the invocation of an image generation tool call by a hosted service.
/// </summary>
[Experimental("MEAI001")]
public sealed class ImageGenerationToolCallContent : AIContent
{
/// <summary>
/// Initializes a new instance of the <see cref="ImageGenerationToolCallContent"/> class.
/// </summary>
public ImageGenerationToolCallContent()
{
}

/// <summary>
/// Gets or sets the unique identifier of the image generation item.
/// </summary>
public string? ImageId { get; set; }
Copy link
Member

Choose a reason for hiding this comment

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

"ImageId" makes it sound to me like this is the ID of the resulting image. What is this? Is it the e equivalent of call id? Would RequestId be better?

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;

namespace Microsoft.Extensions.AI;

/// <summary>
/// Represents an image generation tool call invocation by a hosted service.
/// </summary>
/// <remarks>
/// This content type represents when a hosted AI service invokes an image generation tool.
/// It is informational only and represents the call itself, not the result.
/// </remarks>
[Experimental("MEAI001")]
public sealed class ImageGenerationToolResultContent : AIContent
{
/// <summary>
/// Initializes a new instance of the <see cref="ImageGenerationToolResultContent"/> class.
/// </summary>
public ImageGenerationToolResultContent()
{
}

/// <summary>
/// Gets or sets the unique identifier of the image generation item.
/// </summary>
public string? ImageId { get; set; }

/// <summary>
/// Gets or sets the generated content items.
/// </summary>
/// <remarks>
/// Content is typically <see cref="DataContent"/> for images streamed from the tool, or <see cref="UriContent"/> for remotely hosted images, but
/// can also be provider-specific content types that represent the generated images.
/// </remarks>
public IList<AIContent>? Outputs { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,11 @@ protected ImageGenerationOptions(ImageGenerationOptions? other)
/// </summary>
public ImageGenerationResponseFormat? ResponseFormat { get; set; }

/// <summary>
/// Gets or sets the number of intermediate streaming images to generate.
Copy link
Member

Choose a reason for hiding this comment

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

This is only relevant for streaming requests, right? Might be worth a comment. Should partial or intermediate be in the name? Otherwise it seems like it could be confused with Count, like Count applies to non-streaming and this is the number to generate for streaming. Also, is this per image? Like, if I set Count to 4 and this to 5, will I get 5 or 20?

/// </summary>
public int? StreamingCount { get; set; }

/// <summary>Gets or sets any additional properties associated with the options.</summary>
public AdditionalPropertiesDictionary? AdditionalProperties { get; set; }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1247,6 +1247,10 @@
"Member": "Microsoft.Extensions.AI.ChatResponseUpdate.ChatResponseUpdate(Microsoft.Extensions.AI.ChatRole? role, System.Collections.Generic.IList<Microsoft.Extensions.AI.AIContent>? contents);",
"Stage": "Stable"
},
{
"Member": "Microsoft.Extensions.AI.ChatResponseUpdate Microsoft.Extensions.AI.ChatResponseUpdate.Clone();",
"Stage": "Stable"
},
{
"Member": "override string Microsoft.Extensions.AI.ChatResponseUpdate.ToString();",
"Stage": "Stable"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics.CodeAnalysis;

namespace Microsoft.Extensions.AI;

/// <summary>Represents a hosted tool that can be specified to an AI service to enable it to perform image generation.</summary>
/// <remarks>
/// This tool does not itself implement image generation. It is a marker that can be used to inform a service
/// that the service is allowed to perform image generation if the service is capable of doing so.
/// </remarks>
[Experimental("MEAI001")]
public class HostedImageGenerationTool : AITool
{
/// <summary>
/// Initializes a new instance of the <see cref="HostedImageGenerationTool"/> class with the specified options.
/// </summary>
public HostedImageGenerationTool()
{
}

/// <summary>
/// Gets or sets the options used to configure image generation.
/// </summary>
public ImageGenerationOptions? Options { get; set; }
}
Loading
Loading