diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIContent.cs index 42bb93a7051..3c365544398 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIContent.cs @@ -26,6 +26,8 @@ namespace Microsoft.Extensions.AI; [JsonDerivedType(typeof(ToolApprovalResponseContent), typeDiscriminator: "toolApprovalResponse")] [JsonDerivedType(typeof(McpServerToolCallContent), typeDiscriminator: "mcpServerToolCall")] [JsonDerivedType(typeof(McpServerToolResultContent), typeDiscriminator: "mcpServerToolResult")] +[JsonDerivedType(typeof(ImageGenerationToolCallContent), typeDiscriminator: "imageGenerationToolCall")] +[JsonDerivedType(typeof(ImageGenerationToolResultContent), typeDiscriminator: "imageGenerationToolResult")] // These should be added in once they're no longer [Experimental]. If they're included while still // experimental, any JsonSerializerContext that includes AIContent will incur errors about using @@ -34,8 +36,6 @@ namespace Microsoft.Extensions.AI; // as well as the [JsonSerializable] attributes for them on the JsonContext should be removed. // [JsonDerivedType(typeof(CodeInterpreterToolCallContent), typeDiscriminator: "codeInterpreterToolCall")] // [JsonDerivedType(typeof(CodeInterpreterToolResultContent), typeDiscriminator: "codeInterpreterToolResult")] -// [JsonDerivedType(typeof(ImageGenerationToolCallContent), typeDiscriminator: "imageGenerationToolCall")] -// [JsonDerivedType(typeof(ImageGenerationToolResultContent), typeDiscriminator: "imageGenerationToolResult")] // [JsonDerivedType(typeof(WebSearchToolCallContent), typeDiscriminator: "webSearchToolCall")] // [JsonDerivedType(typeof(WebSearchToolResultContent), typeDiscriminator: "webSearchToolResult")] diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ImageGenerationToolCallContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ImageGenerationToolCallContent.cs index 45522e3d599..5425a5b72ab 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ImageGenerationToolCallContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ImageGenerationToolCallContent.cs @@ -1,15 +1,11 @@ // 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; -using Microsoft.Shared.DiagnosticIds; - namespace Microsoft.Extensions.AI; /// /// Represents the invocation of an image generation tool call by a hosted service. /// -[Experimental(DiagnosticIds.Experiments.AIImageGeneration, UrlFormat = DiagnosticIds.UrlFormat)] public sealed class ImageGenerationToolCallContent : ToolCallContent { /// diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ImageGenerationToolResultContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ImageGenerationToolResultContent.cs index 796583130cf..1162120be57 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ImageGenerationToolResultContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ImageGenerationToolResultContent.cs @@ -2,19 +2,16 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using Microsoft.Shared.DiagnosticIds; namespace Microsoft.Extensions.AI; /// -/// Represents an image generation tool call invocation by a hosted service. +/// Represents the result of an image generation tool invocation by a hosted service. /// /// -/// 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. +/// This content type is used to represent the result of an image generation tool invocation by a hosted service. +/// It is informational only. /// -[Experimental(DiagnosticIds.Experiments.AIImageGeneration, UrlFormat = DiagnosticIds.UrlFormat)] public sealed class ImageGenerationToolResultContent : ToolResultContent { /// diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ToolCallContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ToolCallContent.cs index ecdba681b0d..75cefc4ee17 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ToolCallContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ToolCallContent.cs @@ -12,6 +12,7 @@ namespace Microsoft.Extensions.AI; /// [JsonDerivedType(typeof(FunctionCallContent), "functionCall")] [JsonDerivedType(typeof(McpServerToolCallContent), "mcpServerToolCall")] +[JsonDerivedType(typeof(ImageGenerationToolCallContent), "imageGenerationToolCall")] // Same as in AIContent. // These should be added in once they're no longer [Experimental]. If they're included while still @@ -20,7 +21,6 @@ namespace Microsoft.Extensions.AI; // these lines should be uncommented and the corresponding lines in AIJsonUtilities.CreateDefaultOptions // as well as the [JsonSerializable] attributes for them on the JsonContext should be removed. // [JsonDerivedType(typeof(CodeInterpreterToolCallContent), "codeInterpreterToolCall")] -// [JsonDerivedType(typeof(ImageGenerationToolCallContent), "imageGenerationToolCall")] // [JsonDerivedType(typeof(WebSearchToolCallContent), "webSearchToolCall")] public class ToolCallContent : AIContent { diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ToolResultContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ToolResultContent.cs index 63a99476bb7..0e9644b0f79 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ToolResultContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ToolResultContent.cs @@ -12,6 +12,7 @@ namespace Microsoft.Extensions.AI; /// [JsonDerivedType(typeof(FunctionResultContent), "functionResult")] [JsonDerivedType(typeof(McpServerToolResultContent), "mcpServerToolResult")] +[JsonDerivedType(typeof(ImageGenerationToolResultContent), "imageGenerationToolResult")] // Same as in AIContent. // These should be added in once they're no longer [Experimental]. If they're included while still @@ -20,7 +21,6 @@ namespace Microsoft.Extensions.AI; // these lines should be uncommented and the corresponding lines in AIJsonUtilities.CreateDefaultOptions // as well as the [JsonSerializable] attributes for them on the JsonContext should be removed. // [JsonDerivedType(typeof(CodeInterpreterToolResultContent), "codeInterpreterToolResult")] -// [JsonDerivedType(typeof(ImageGenerationToolResultContent), "imageGenerationToolResult")] // [JsonDerivedType(typeof(WebSearchToolResultContent), "webSearchToolResult")] public class ToolResultContent : AIContent { diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Image/ImageGenerationOptions.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Image/ImageGenerationOptions.cs index 0dd2325808a..80baf52eb14 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Image/ImageGenerationOptions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Image/ImageGenerationOptions.cs @@ -10,7 +10,6 @@ namespace Microsoft.Extensions.AI; /// Represents the options for an image generation request. -[Experimental(DiagnosticIds.Experiments.AIImageGeneration, UrlFormat = DiagnosticIds.UrlFormat)] public class ImageGenerationOptions { /// Initializes a new instance of the class. @@ -101,7 +100,6 @@ protected ImageGenerationOptions(ImageGenerationOptions? other) /// /// Not all implementations support all response formats and this value might be ignored by the implementation if not supported. /// -[Experimental(DiagnosticIds.Experiments.AIImageGeneration, UrlFormat = DiagnosticIds.UrlFormat)] public enum ImageGenerationResponseFormat { /// @@ -117,5 +115,7 @@ public enum ImageGenerationResponseFormat /// /// The generated image is returned as a hosted resource identifier, which can be used to retrieve the image later. /// + [Experimental(DiagnosticIds.Experiments.AIImageGeneration, UrlFormat = DiagnosticIds.UrlFormat)] Hosted, + } diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json index 05a27940682..33ec7d50234 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json @@ -1,5 +1,5 @@ -{ - "Name": "Microsoft.Extensions.AI.Abstractions, Version=10.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35", +{ + "Name": "Microsoft.Extensions.AI.Abstractions, Version=10.6.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35", "Types": [ { "Type": "sealed class Microsoft.Extensions.AI.AdditionalPropertiesDictionary : Microsoft.Extensions.AI.AdditionalPropertiesDictionary", @@ -2521,29 +2521,29 @@ }, { "Type": "class Microsoft.Extensions.AI.HostedImageGenerationTool : Microsoft.Extensions.AI.AITool", - "Stage": "Experimental", + "Stage": "Stable", "Methods": [ { "Member": "Microsoft.Extensions.AI.HostedImageGenerationTool.HostedImageGenerationTool();", - "Stage": "Experimental" + "Stage": "Stable" }, { "Member": "Microsoft.Extensions.AI.HostedImageGenerationTool.HostedImageGenerationTool(System.Collections.Generic.IReadOnlyDictionary? additionalProperties);", - "Stage": "Experimental" + "Stage": "Stable" } ], "Properties": [ { "Member": "override System.Collections.Generic.IReadOnlyDictionary Microsoft.Extensions.AI.HostedImageGenerationTool.AdditionalProperties { get; }", - "Stage": "Experimental" + "Stage": "Stable" }, { "Member": "override string Microsoft.Extensions.AI.HostedImageGenerationTool.Name { get; }", - "Stage": "Experimental" + "Stage": "Stable" }, { "Member": "Microsoft.Extensions.AI.ImageGenerationOptions? Microsoft.Extensions.AI.HostedImageGenerationTool.Options { get; set; }", - "Stage": "Experimental" + "Stage": "Stable" } ] }, @@ -2821,53 +2821,53 @@ }, { "Type": "class Microsoft.Extensions.AI.ImageGenerationOptions", - "Stage": "Experimental", + "Stage": "Stable", "Methods": [ { "Member": "Microsoft.Extensions.AI.ImageGenerationOptions.ImageGenerationOptions();", - "Stage": "Experimental" + "Stage": "Stable" }, { "Member": "Microsoft.Extensions.AI.ImageGenerationOptions.ImageGenerationOptions(Microsoft.Extensions.AI.ImageGenerationOptions? other);", - "Stage": "Experimental" + "Stage": "Stable" }, { "Member": "virtual Microsoft.Extensions.AI.ImageGenerationOptions Microsoft.Extensions.AI.ImageGenerationOptions.Clone();", - "Stage": "Experimental" + "Stage": "Stable" } ], "Properties": [ { "Member": "Microsoft.Extensions.AI.AdditionalPropertiesDictionary? Microsoft.Extensions.AI.ImageGenerationOptions.AdditionalProperties { get; set; }", - "Stage": "Experimental" + "Stage": "Stable" }, { "Member": "int? Microsoft.Extensions.AI.ImageGenerationOptions.Count { get; set; }", - "Stage": "Experimental" + "Stage": "Stable" }, { "Member": "System.Drawing.Size? Microsoft.Extensions.AI.ImageGenerationOptions.ImageSize { get; set; }", - "Stage": "Experimental" + "Stage": "Stable" }, { "Member": "string? Microsoft.Extensions.AI.ImageGenerationOptions.MediaType { get; set; }", - "Stage": "Experimental" + "Stage": "Stable" }, { "Member": "string? Microsoft.Extensions.AI.ImageGenerationOptions.ModelId { get; set; }", - "Stage": "Experimental" + "Stage": "Stable" }, { "Member": "System.Func? Microsoft.Extensions.AI.ImageGenerationOptions.RawRepresentationFactory { get; set; }", - "Stage": "Experimental" + "Stage": "Stable" }, { "Member": "Microsoft.Extensions.AI.ImageGenerationResponseFormat? Microsoft.Extensions.AI.ImageGenerationOptions.ResponseFormat { get; set; }", - "Stage": "Experimental" + "Stage": "Stable" }, { "Member": "int? Microsoft.Extensions.AI.ImageGenerationOptions.StreamingCount { get; set; }", - "Stage": "Experimental" + "Stage": "Stable" } ] }, @@ -2929,17 +2929,17 @@ }, { "Type": "enum Microsoft.Extensions.AI.ImageGenerationResponseFormat", - "Stage": "Experimental", + "Stage": "Stable", "Methods": [ { "Member": "Microsoft.Extensions.AI.ImageGenerationResponseFormat.ImageGenerationResponseFormat();", - "Stage": "Experimental" + "Stage": "Stable" } ], "Fields": [ { "Member": "const Microsoft.Extensions.AI.ImageGenerationResponseFormat Microsoft.Extensions.AI.ImageGenerationResponseFormat.Data", - "Stage": "Experimental", + "Stage": "Stable", "Value": "1" }, { @@ -2949,34 +2949,34 @@ }, { "Member": "const Microsoft.Extensions.AI.ImageGenerationResponseFormat Microsoft.Extensions.AI.ImageGenerationResponseFormat.Uri", - "Stage": "Experimental", + "Stage": "Stable", "Value": "0" } ] }, { "Type": "sealed class Microsoft.Extensions.AI.ImageGenerationToolCallContent : Microsoft.Extensions.AI.ToolCallContent", - "Stage": "Experimental", + "Stage": "Stable", "Methods": [ { "Member": "Microsoft.Extensions.AI.ImageGenerationToolCallContent.ImageGenerationToolCallContent(string callId);", - "Stage": "Experimental" + "Stage": "Stable" } ] }, { "Type": "sealed class Microsoft.Extensions.AI.ImageGenerationToolResultContent : Microsoft.Extensions.AI.ToolResultContent", - "Stage": "Experimental", + "Stage": "Stable", "Methods": [ { "Member": "Microsoft.Extensions.AI.ImageGenerationToolResultContent.ImageGenerationToolResultContent(string callId);", - "Stage": "Experimental" + "Stage": "Stable" } ], "Properties": [ { "Member": "System.Collections.Generic.IList? Microsoft.Extensions.AI.ImageGenerationToolResultContent.Outputs { get; set; }", - "Stage": "Experimental" + "Stage": "Stable" } ] }, @@ -4811,4 +4811,4 @@ ] } ] -} +} \ No newline at end of file diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedImageGenerationTool.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedImageGenerationTool.cs index 8371e28e906..8ecb125571d 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedImageGenerationTool.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedImageGenerationTool.cs @@ -2,8 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using Microsoft.Shared.DiagnosticIds; namespace Microsoft.Extensions.AI; @@ -12,7 +10,6 @@ namespace Microsoft.Extensions.AI; /// 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. /// -[Experimental(DiagnosticIds.Experiments.AIImageGeneration, UrlFormat = DiagnosticIds.UrlFormat)] public class HostedImageGenerationTool : AITool { /// Any additional properties associated with the tool. diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Defaults.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Defaults.cs index 6feffa455c0..4d4c284d89a 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Defaults.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Defaults.cs @@ -53,8 +53,6 @@ private static JsonSerializerOptions CreateDefaultOptions() // Once they're no longer [Experimental] and added as [JsonDerivedType] on AIContent, these lines should be removed. AddAIContentTypeChain(options, typeof(CodeInterpreterToolCallContent), typeDiscriminatorId: "codeInterpreterToolCall", checkBuiltIn: false); AddAIContentTypeChain(options, typeof(CodeInterpreterToolResultContent), typeDiscriminatorId: "codeInterpreterToolResult", checkBuiltIn: false); - AddAIContentTypeChain(options, typeof(ImageGenerationToolCallContent), typeDiscriminatorId: "imageGenerationToolCall", checkBuiltIn: false); - AddAIContentTypeChain(options, typeof(ImageGenerationToolResultContent), typeDiscriminatorId: "imageGenerationToolResult", checkBuiltIn: false); AddAIContentTypeChain(options, typeof(WebSearchToolCallContent), typeDiscriminatorId: "webSearchToolCall", checkBuiltIn: false); AddAIContentTypeChain(options, typeof(WebSearchToolResultContent), typeDiscriminatorId: "webSearchToolResult", checkBuiltIn: false); @@ -123,8 +121,6 @@ private static JsonSerializerOptions CreateDefaultOptions() // and are included via [JsonDerivedType] on AIContent. [JsonSerializable(typeof(CodeInterpreterToolCallContent))] [JsonSerializable(typeof(CodeInterpreterToolResultContent))] - [JsonSerializable(typeof(ImageGenerationToolCallContent))] - [JsonSerializable(typeof(ImageGenerationToolResultContent))] [JsonSerializable(typeof(WebSearchToolCallContent))] [JsonSerializable(typeof(WebSearchToolResultContent))] [JsonSerializable(typeof(ResponseContinuationToken))] diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIImageGenerator.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIImageGenerator.cs index 9a7aebbaac3..97c6e9c1b79 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIImageGenerator.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIImageGenerator.cs @@ -179,8 +179,6 @@ private OpenAI.Images.ImageGenerationOptions ToOpenAIImageGenerationOptions(Imag { ImageGenerationResponseFormat.Uri => GeneratedImageFormat.Uri, ImageGenerationResponseFormat.Data => GeneratedImageFormat.Bytes, - - // ImageGenerationResponseFormat.Hosted not supported by ImageGenerator, however other OpenAI API support file IDs. _ => (GeneratedImageFormat?)null }; @@ -214,8 +212,6 @@ private ImageEditOptions ToOpenAIImageEditOptions(ImageGenerationOptions? option { ImageGenerationResponseFormat.Uri => GeneratedImageFormat.Uri, ImageGenerationResponseFormat.Data => GeneratedImageFormat.Bytes, - - // ImageGenerationResponseFormat.Hosted not supported by ImageGenerator, however other OpenAI API support file IDs. _ => (GeneratedImageFormat?)null }; diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientIntegrationTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientIntegrationTests.cs index c98197e0b65..fd9840c9f73 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientIntegrationTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientIntegrationTests.cs @@ -4,6 +4,7 @@ using System; using System.ClientModel; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Text.Json; using System.Threading.Tasks; @@ -132,6 +133,59 @@ public async Task UseWebSearch_AnnotationsReflectResults() }); } + [ConditionalTheory] + [InlineData(false, "gpt-image-1-mini")] + [InlineData(true, "gpt-image-2")] + public async Task UseImageGeneration_ProducesImageContent(bool streaming, string imageModel) + { + SkipIfNotEnabled(); + + if (TestRunnerConfiguration.Instance["OpenAI:ChatModel"]?.StartsWith("gpt-5.4", StringComparison.OrdinalIgnoreCase) is not true) + { + throw new SkipTestException("Image generation tool requires gpt-5.4 or later."); + } + + var chatOptions = new ChatOptions + { + Tools = + [ + new HostedImageGenerationTool + { + Options = new ImageGenerationOptions { ModelId = imageModel }, + }, + ], + }; + + ChatResponse response = streaming + ? await ChatClient.GetStreamingResponseAsync("Generate an image of a simple blue circle on a white background.", chatOptions).ToChatResponseAsync() + : await ChatClient.GetResponseAsync("Generate an image of a simple blue circle on a white background.", chatOptions); + + Assert.NotNull(response); + + ChatMessage m = Assert.Single(response.Messages); + + // Verify that the image generation tool call and result content are present. + var igCall = m.Contents.OfType().FirstOrDefault(); + Assert.NotNull(igCall); + Assert.NotNull(igCall.CallId); + + var igResult = m.Contents.OfType().FirstOrDefault(); + Assert.NotNull(igResult); + Assert.Equal(igCall.CallId, igResult.CallId); + + // Verify that the result contains image data. + Assert.NotNull(igResult.Outputs); + Assert.NotEmpty(igResult.Outputs); + var imageContent = Assert.Single(igResult.Outputs.OfType()); + Assert.False(imageContent.Data.IsEmpty); + Assert.StartsWith("image/", imageContent.MediaType, StringComparison.Ordinal); + + // Save to temp file for manual inspection. + string extension = imageContent.MediaType == "image/png" ? ".png" : ".webp"; + string tempPath = Path.Combine(Path.GetTempPath(), $"image_gen_test_{imageModel}{extension}"); + File.WriteAllBytes(tempPath, imageContent.Data.ToArray()); + } + [ConditionalFact] public async Task RemoteMCP_ListTools() { diff --git a/test/Libraries/Microsoft.Extensions.AI.Stabilization.Tests/Microsoft.Extensions.AI.Stabilization.Tests.csproj b/test/Libraries/Microsoft.Extensions.AI.Stabilization.Tests/Microsoft.Extensions.AI.Stabilization.Tests.csproj new file mode 100644 index 00000000000..952cd369229 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Stabilization.Tests/Microsoft.Extensions.AI.Stabilization.Tests.csproj @@ -0,0 +1,35 @@ + + + Microsoft.Extensions.AI + Stabilization tests for ImageGeneration content types (no MEAI001 suppression). + + + + + $(NoWarn);CA1063;CA1861;CA2201;VSTHRD003;S104 + true + + + + false + + + + true + + + + + + + + + + + + + + + + +