From 219d1cddc1aad46fcc7ebd3f6730bac6f65d5b6b Mon Sep 17 00:00:00 2001 From: David Cantu Date: Mon, 27 Apr 2026 16:30:46 -0500 Subject: [PATCH 1/4] Stabilize CodeInterpreter and WebSearch content types Remove [Experimental] from CodeInterpreterToolCallContent, CodeInterpreterToolResultContent, WebSearchToolCallContent, and WebSearchToolResultContent so IChatClient consumers can use them without MEAI001 suppression. - Rename WebSearchToolResultContent.Results -> Outputs to align with CodeInterpreterToolResultContent.Outputs - Uncomment [JsonDerivedType] on AIContent, ToolCallContent, and ToolResultContent for the four content types - Remove corresponding AddAIContentTypeChain runtime workarounds and [JsonSerializable] entries from AIJsonUtilities.Defaults - Update API baseline (Experimental -> Stable) - Update OpenAI WebSearch result construction to use Outputs - Extend Stabilization.Tests to cover the newly stabilized types Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Contents/AIContent.cs | 15 ++---- .../CodeInterpreterToolCallContent.cs | 3 -- .../CodeInterpreterToolResultContent.cs | 3 -- .../Contents/ToolCallContent.cs | 11 +---- .../Contents/ToolResultContent.cs | 11 +---- .../Contents/WebSearchToolCallContent.cs | 3 -- .../Contents/WebSearchToolResultContent.cs | 9 ++-- .../Microsoft.Extensions.AI.Abstractions.json | 26 +++++----- .../Utilities/AIJsonUtilities.Defaults.cs | 14 ------ .../OpenAIResponsesChatClient.cs | 4 +- .../WebSearchToolResultContentTests.cs | 48 +++++++++---------- .../OpenAIResponseClientIntegrationTests.cs | 6 +-- .../OpenAIResponseClientTests.cs | 16 +++---- ...t.Extensions.AI.Stabilization.Tests.csproj | 8 +++- 14 files changed, 68 insertions(+), 109 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIContent.cs index 3c365544398..9de231cc3f6 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIContent.cs @@ -28,17 +28,10 @@ namespace Microsoft.Extensions.AI; [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 -// experimental types in its source generated files. When [Experimental] is removed from these types, -// 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), typeDiscriminator: "codeInterpreterToolCall")] -// [JsonDerivedType(typeof(CodeInterpreterToolResultContent), typeDiscriminator: "codeInterpreterToolResult")] -// [JsonDerivedType(typeof(WebSearchToolCallContent), typeDiscriminator: "webSearchToolCall")] -// [JsonDerivedType(typeof(WebSearchToolResultContent), typeDiscriminator: "webSearchToolResult")] - +[JsonDerivedType(typeof(CodeInterpreterToolCallContent), typeDiscriminator: "codeInterpreterToolCall")] +[JsonDerivedType(typeof(CodeInterpreterToolResultContent), typeDiscriminator: "codeInterpreterToolResult")] +[JsonDerivedType(typeof(WebSearchToolCallContent), typeDiscriminator: "webSearchToolCall")] +[JsonDerivedType(typeof(WebSearchToolResultContent), typeDiscriminator: "webSearchToolResult")] public class AIContent { /// diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/CodeInterpreterToolCallContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/CodeInterpreterToolCallContent.cs index c155b0228b8..3d441876959 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/CodeInterpreterToolCallContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/CodeInterpreterToolCallContent.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; @@ -14,7 +12,6 @@ namespace Microsoft.Extensions.AI; /// This content type represents when a hosted AI service invokes a code interpreter tool. /// It is informational only and represents the call itself, not the result. /// -[Experimental(DiagnosticIds.Experiments.AICodeInterpreter, UrlFormat = DiagnosticIds.UrlFormat)] public sealed class CodeInterpreterToolCallContent : ToolCallContent { /// diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/CodeInterpreterToolResultContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/CodeInterpreterToolResultContent.cs index 9384c927cb7..1fdb44d33a4 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/CodeInterpreterToolResultContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/CodeInterpreterToolResultContent.cs @@ -2,15 +2,12 @@ // 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 the result of a code interpreter tool invocation by a hosted service. /// -[Experimental(DiagnosticIds.Experiments.AICodeInterpreter, UrlFormat = DiagnosticIds.UrlFormat)] public sealed class CodeInterpreterToolResultContent : ToolResultContent { /// diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ToolCallContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ToolCallContent.cs index 75cefc4ee17..2cbddd1878f 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ToolCallContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ToolCallContent.cs @@ -13,15 +13,8 @@ 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 -// experimental, any JsonSerializerContext that includes ToolCallContent will incur errors about using -// experimental types in its source generated files. When [Experimental] is removed from these types, -// 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(WebSearchToolCallContent), "webSearchToolCall")] +[JsonDerivedType(typeof(CodeInterpreterToolCallContent), "codeInterpreterToolCall")] +[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 0e9644b0f79..c34b06afcc4 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ToolResultContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ToolResultContent.cs @@ -13,15 +13,8 @@ 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 -// experimental, any JsonSerializerContext that includes ToolResultContent will incur errors about using -// experimental types in its source generated files. When [Experimental] is removed from these types, -// 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(WebSearchToolResultContent), "webSearchToolResult")] +[JsonDerivedType(typeof(CodeInterpreterToolResultContent), "codeInterpreterToolResult")] +[JsonDerivedType(typeof(WebSearchToolResultContent), "webSearchToolResult")] public class ToolResultContent : AIContent { /// diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/WebSearchToolCallContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/WebSearchToolCallContent.cs index 3d5488ee0a2..9ea3b304166 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/WebSearchToolCallContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/WebSearchToolCallContent.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; @@ -14,7 +12,6 @@ namespace Microsoft.Extensions.AI; /// This content type represents when a hosted AI service invokes a web search tool. /// It is informational only and represents the call itself, not the result. /// -[Experimental(DiagnosticIds.Experiments.AIWebSearch, UrlFormat = DiagnosticIds.UrlFormat)] public sealed class WebSearchToolCallContent : ToolCallContent { /// diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/WebSearchToolResultContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/WebSearchToolResultContent.cs index 144160b85af..ef610f33b19 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/WebSearchToolResultContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/WebSearchToolResultContent.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; @@ -15,7 +13,6 @@ namespace Microsoft.Extensions.AI; /// The results contain a list of items describing the web pages /// found during the search, typically as instances. /// -[Experimental(DiagnosticIds.Experiments.AIWebSearch, UrlFormat = DiagnosticIds.UrlFormat)] public sealed class WebSearchToolResultContent : ToolResultContent { /// @@ -28,12 +25,12 @@ public WebSearchToolResultContent(string callId) } /// - /// Gets or sets the web search results. + /// Gets or sets the web search outputs. /// /// - /// Each item represents a web page found during the search, typically as a instance. + /// Each output represents a web page result found during the search, typically as a instance. /// If a title is available for a result, it may be stored in the item's /// under the key "title". /// - public IList? Results { get; set; } + public IList? Outputs { get; set; } } 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 5fdfe4c26e8..4d420a8054c 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 @@ -1477,33 +1477,33 @@ }, { "Type": "sealed class Microsoft.Extensions.AI.CodeInterpreterToolCallContent : Microsoft.Extensions.AI.ToolCallContent", - "Stage": "Experimental", + "Stage": "Stable", "Methods": [ { "Member": "Microsoft.Extensions.AI.CodeInterpreterToolCallContent.CodeInterpreterToolCallContent(string callId);", - "Stage": "Experimental" + "Stage": "Stable" } ], "Properties": [ { "Member": "System.Collections.Generic.IList? Microsoft.Extensions.AI.CodeInterpreterToolCallContent.Inputs { get; set; }", - "Stage": "Experimental" + "Stage": "Stable" } ] }, { "Type": "sealed class Microsoft.Extensions.AI.CodeInterpreterToolResultContent : Microsoft.Extensions.AI.ToolResultContent", - "Stage": "Experimental", + "Stage": "Stable", "Methods": [ { "Member": "Microsoft.Extensions.AI.CodeInterpreterToolResultContent.CodeInterpreterToolResultContent(string callId);", - "Stage": "Experimental" + "Stage": "Stable" } ], "Properties": [ { "Member": "System.Collections.Generic.IList? Microsoft.Extensions.AI.CodeInterpreterToolResultContent.Outputs { get; set; }", - "Stage": "Experimental" + "Stage": "Stable" } ] }, @@ -4812,33 +4812,33 @@ }, { "Type": "sealed class Microsoft.Extensions.AI.WebSearchToolCallContent : Microsoft.Extensions.AI.ToolCallContent", - "Stage": "Experimental", + "Stage": "Stable", "Methods": [ { "Member": "Microsoft.Extensions.AI.WebSearchToolCallContent.WebSearchToolCallContent(string callId);", - "Stage": "Experimental" + "Stage": "Stable" } ], "Properties": [ { "Member": "System.Collections.Generic.IList? Microsoft.Extensions.AI.WebSearchToolCallContent.Queries { get; set; }", - "Stage": "Experimental" + "Stage": "Stable" } ] }, { "Type": "sealed class Microsoft.Extensions.AI.WebSearchToolResultContent : Microsoft.Extensions.AI.ToolResultContent", - "Stage": "Experimental", + "Stage": "Stable", "Methods": [ { "Member": "Microsoft.Extensions.AI.WebSearchToolResultContent.WebSearchToolResultContent(string callId);", - "Stage": "Experimental" + "Stage": "Stable" } ], "Properties": [ { - "Member": "System.Collections.Generic.IList? Microsoft.Extensions.AI.WebSearchToolResultContent.Results { get; set; }", - "Stage": "Experimental" + "Member": "System.Collections.Generic.IList? Microsoft.Extensions.AI.WebSearchToolResultContent.Outputs { get; set; }", + "Stage": "Stable" } ] } 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 4d4c284d89a..bbfcc878788 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Defaults.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Defaults.cs @@ -48,14 +48,6 @@ private static JsonSerializerOptions CreateDefaultOptions() Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, }; - // Temporary workaround: these types are [Experimental] and can't be added as [JsonDerivedType] on AIContent yet, - // or else consuming assemblies that used source generation with AIContent would implicitly reference them. - // 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(WebSearchToolCallContent), typeDiscriminatorId: "webSearchToolCall", checkBuiltIn: false); - AddAIContentTypeChain(options, typeof(WebSearchToolResultContent), typeDiscriminatorId: "webSearchToolResult", checkBuiltIn: false); - if (JsonSerializer.IsReflectionEnabledByDefault) { // If reflection-based serialization is enabled by default, use it as a fallback for all other types. @@ -117,12 +109,6 @@ private static JsonSerializerOptions CreateDefaultOptions() [JsonSerializable(typeof(AIContent))] [JsonSerializable(typeof(IEnumerable))] - // Temporary workaround: These should be implicitly added in once they're no longer [Experimental] - // and are included via [JsonDerivedType] on AIContent. - [JsonSerializable(typeof(CodeInterpreterToolCallContent))] - [JsonSerializable(typeof(CodeInterpreterToolResultContent))] - [JsonSerializable(typeof(WebSearchToolCallContent))] - [JsonSerializable(typeof(WebSearchToolResultContent))] [JsonSerializable(typeof(ResponseContinuationToken))] // IEmbeddingGenerator diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs index 63d22d72ced..a3b1c5dd2e9 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs @@ -272,7 +272,7 @@ internal static IEnumerable ToChatMessages(IEnumerable // Also yield the WebSearchToolResultContent. yield return CreateUpdate(new WebSearchToolResultContent(wscri.Id) { - Results = GetWebSearchSources(wscri), + Outputs = GetWebSearchSources(wscri), RawRepresentation = wscri, }); break; diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/WebSearchToolResultContentTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/WebSearchToolResultContentTests.cs index d19d9f446d0..1495893575e 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/WebSearchToolResultContentTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/WebSearchToolResultContentTests.cs @@ -17,7 +17,7 @@ public void Constructor_PropsDefault() Assert.Null(c.RawRepresentation); Assert.Null(c.AdditionalProperties); Assert.Equal("callId", c.CallId); - Assert.Null(c.Results); + Assert.Null(c.Outputs); } [Fact] @@ -27,13 +27,13 @@ public void Properties_Roundtrip() Assert.Equal("ws_call123", c.CallId); - Assert.Null(c.Results); + Assert.Null(c.Outputs); IList results = [ new UriContent(new Uri("https://example.com/1"), "text/html") { AdditionalProperties = new() { ["title"] = "Result 1" } } ]; - c.Results = results; - Assert.Same(results, c.Results); + c.Outputs = results; + Assert.Same(results, c.Outputs); Assert.Null(c.RawRepresentation); object raw = new(); @@ -47,11 +47,11 @@ public void Properties_Roundtrip() } [Fact] - public void Results_SupportsMultipleItems() + public void Outputs_SupportsMultipleItems() { WebSearchToolResultContent c = new("ws_call789") { - Results = + Outputs = [ new UriContent(new Uri("https://example.com/1"), "text/html") { AdditionalProperties = new() { ["title"] = "First" } }, new UriContent(new Uri("https://example.com/2"), "text/html") { AdditionalProperties = new() { ["title"] = "Second" } }, @@ -59,16 +59,16 @@ public void Results_SupportsMultipleItems() ] }; - Assert.NotNull(c.Results); - Assert.Equal(3, c.Results.Count); + Assert.NotNull(c.Outputs); + Assert.Equal(3, c.Outputs.Count); - var first = Assert.IsType(c.Results[0]); + var first = Assert.IsType(c.Outputs[0]); Assert.Equal("First", first.AdditionalProperties?["title"]); - var second = Assert.IsType(c.Results[1]); + var second = Assert.IsType(c.Outputs[1]); Assert.Equal("Second", second.AdditionalProperties?["title"]); - var third = Assert.IsType(c.Results[2]); + var third = Assert.IsType(c.Outputs[2]); Assert.Null(third.AdditionalProperties); } @@ -77,7 +77,7 @@ public void Serialization_Roundtrips() { WebSearchToolResultContent content = new("ws_call123") { - Results = + Outputs = [ new UriContent(new Uri("https://example.com"), "text/html") { AdditionalProperties = new() { ["title"] = "Example Page" } }, new UriContent(new Uri("https://another.com"), "text/html"), @@ -89,14 +89,14 @@ public void Serialization_Roundtrips() Assert.NotNull(deserialized); Assert.Equal("ws_call123", deserialized.CallId); - Assert.NotNull(deserialized.Results); - Assert.Equal(2, deserialized.Results.Count); + Assert.NotNull(deserialized.Outputs); + Assert.Equal(2, deserialized.Outputs.Count); - var first = Assert.IsType(deserialized.Results[0]); + var first = Assert.IsType(deserialized.Outputs[0]); Assert.Equal(new Uri("https://example.com"), first.Uri); Assert.Equal("Example Page", first.AdditionalProperties?["title"]?.ToString()); - var second = Assert.IsType(deserialized.Results[1]); + var second = Assert.IsType(deserialized.Outputs[1]); Assert.Equal(new Uri("https://another.com"), second.Uri); } @@ -105,7 +105,7 @@ public void Serialization_AsAIContent_Roundtrips() { AIContent content = new WebSearchToolResultContent("ws_call456") { - Results = + Outputs = [ new UriContent(new Uri("https://test.com"), "text/html") { AdditionalProperties = new() { ["title"] = "Test" } }, ] @@ -116,10 +116,10 @@ public void Serialization_AsAIContent_Roundtrips() var result = Assert.IsType(deserialized); Assert.Equal("ws_call456", result.CallId); - Assert.NotNull(result.Results); - Assert.Single(result.Results); + Assert.NotNull(result.Outputs); + Assert.Single(result.Outputs); - var first = Assert.IsType(result.Results[0]); + var first = Assert.IsType(result.Outputs[0]); Assert.Equal("Test", first.AdditionalProperties?["title"]?.ToString()); } @@ -130,7 +130,7 @@ public void JsonDeserialization_KnownPayload() { "$type": "webSearchToolResult", "callId": "ws-call1", - "results": [ + "outputs": [ { "$type": "uri", "uri": "https://example.com", @@ -148,9 +148,9 @@ public void JsonDeserialization_KnownPayload() Assert.NotNull(result); var wsResult = Assert.IsType(result); Assert.Equal("ws-call1", wsResult.CallId); - Assert.NotNull(wsResult.Results); - Assert.Single(wsResult.Results); - var uriResult = Assert.IsType(wsResult.Results[0]); + Assert.NotNull(wsResult.Outputs); + Assert.Single(wsResult.Outputs); + var uriResult = Assert.IsType(wsResult.Outputs[0]); Assert.Equal(new Uri("https://example.com"), uriResult.Uri); Assert.Equal("text/html", uriResult.MediaType); Assert.NotNull(wsResult.AdditionalProperties); diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientIntegrationTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientIntegrationTests.cs index a4ae5256c67..ce98ac151b0 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientIntegrationTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientIntegrationTests.cs @@ -107,9 +107,9 @@ public async Task UseWebSearch_AnnotationsReflectResults() Assert.Equal(wsCall.CallId, wsResult.CallId); // Verify that sources are populated when opted in. - Assert.NotNull(wsResult.Results); - Assert.NotEmpty(wsResult.Results); - Assert.All(wsResult.Results, r => + Assert.NotNull(wsResult.Outputs); + Assert.NotEmpty(wsResult.Outputs); + Assert.All(wsResult.Outputs, r => { var uriContent = Assert.IsType(r); Assert.NotNull(uriContent.Uri); diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs index 07b26c4b631..688cfd7a20f 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; @@ -7026,15 +7026,15 @@ public async Task WebSearchTool_NonStreaming() var wsResult = Assert.IsType(message.Contents[1]); Assert.Equal("ws_0ed9cd9f8606486b0069888072b3d08190818b61da9cf032a7", wsResult.CallId); Assert.NotNull(wsResult.RawRepresentation); - Assert.NotNull(wsResult.Results); - Assert.Equal(2, wsResult.Results.Count); + Assert.NotNull(wsResult.Outputs); + Assert.Equal(2, wsResult.Outputs.Count); - var source0 = Assert.IsType(wsResult.Results[0]); + var source0 = Assert.IsType(wsResult.Outputs[0]); Assert.Equal(new Uri("https://devblogs.microsoft.com/dotnet/announcing-dotnet-10"), source0.Uri); var source0Raw = Assert.IsType(source0.RawRepresentation); Assert.Equal("\"Announcing .NET 10 - .NET Blog\"", ReadPatchValue(source0Raw, "$.title"u8)); - var source1 = Assert.IsType(wsResult.Results[1]); + var source1 = Assert.IsType(wsResult.Outputs[1]); Assert.Equal(new Uri("https://dotnet.microsoft.com/en-us/download/dotnet/10.0"), source1.Uri); var source1Raw = Assert.IsType(source1.RawRepresentation); Assert.Equal("\"Download .NET 10\"", ReadPatchValue(source1Raw, "$.title"u8)); @@ -7167,9 +7167,9 @@ public async Task WebSearchTool_Streaming() var wsResult = wsResultUpdate.Contents.OfType().First(); Assert.Equal("ws_02441a08b3f3bf4b00698880914730819eb48b3ae0c359bff3", wsResult.CallId); Assert.NotNull(wsResult.RawRepresentation); - Assert.NotNull(wsResult.Results); - Assert.Single(wsResult.Results); - var streamSource = Assert.IsType(wsResult.Results[0]); + Assert.NotNull(wsResult.Outputs); + Assert.Single(wsResult.Outputs); + var streamSource = Assert.IsType(wsResult.Outputs[0]); Assert.Equal(new Uri("https://devblogs.microsoft.com/dotnet/announcing-dotnet-10"), streamSource.Uri); Assert.IsType(streamSource.RawRepresentation); 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 index 952cd369229..b53c13ddae4 100644 --- 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 @@ -1,7 +1,7 @@ Microsoft.Extensions.AI - Stabilization tests for ImageGeneration content types (no MEAI001 suppression). + Stabilization tests for ImageGeneration, CodeInterpreter, and WebSearch content types (no MEAI001 suppression). @@ -22,6 +22,12 @@ + + + + + + From 23be84b0050e4081211a21c1cc621794bc7f690f Mon Sep 17 00:00:00 2001 From: David Cantu Date: Tue, 28 Apr 2026 11:00:58 -0500 Subject: [PATCH 2/4] Parameterize Serialization_DerivedTypes_Roundtrips over JSON context Convert Serialization_DerivedTypes_Roundtrips tests to [Theory] with a useBuiltInJsonContext bool parameter that selects between AIJsonUtilities.DefaultOptions (the library's built-in JsonContext) and TestJsonSerializerContext.Default.Options (a consumer-style source-gen context). Now that CodeInterpreter and WebSearch content types are stable, both paths can be exercised. Also extend the AIContent, ToolCallContent, and ToolResultContent fixtures with the newly stabilized types. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Contents/AIContentTests.cs | 22 +++++++++++++------ .../Contents/InputRequestContentTests.cs | 12 ++++++---- .../Contents/InputResponseContentTests.cs | 12 ++++++---- .../Contents/ToolCallContentTests.cs | 20 +++++++++-------- .../Contents/ToolResultContentTests.cs | 20 +++++++++-------- 5 files changed, 53 insertions(+), 33 deletions(-) diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/AIContentTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/AIContentTests.cs index cd0a171d798..f37627f5ef9 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/AIContentTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/AIContentTests.cs @@ -55,9 +55,13 @@ public void Serialization_Roundtrips() Assert.Single(deserialized.AdditionalProperties); } - [Fact] - public void Serialization_DerivedTypes_Roundtrips() + [Theory] + [InlineData(false)] + [InlineData(true)] + public void Serialization_DerivedTypes_Roundtrips(bool useBuiltInJsonContext) { + JsonSerializerOptions options = useBuiltInJsonContext ? AIJsonUtilities.DefaultOptions : TestJsonSerializerContext.Default.Options; + ChatMessage message = new(ChatRole.User, [ new TextContent("a"), @@ -77,20 +81,24 @@ public void Serialization_DerivedTypes_Roundtrips() new ToolApprovalRequestContent("request123", new McpServerToolCallContent("call123", "myTool", "myServer")), new ToolApprovalResponseContent("request123", approved: true, new McpServerToolCallContent("call456", "myTool2", "myServer2")), new ImageGenerationToolCallContent("img123"), - new ImageGenerationToolResultContent("img456") { Outputs = [new DataContent(new byte[] { 4, 5, 6 }, "image/png")] } + new ImageGenerationToolResultContent("img456") { Outputs = [new DataContent(new byte[] { 4, 5, 6 }, "image/png")] }, + new CodeInterpreterToolCallContent("ci123"), + new CodeInterpreterToolResultContent("ci456"), + new WebSearchToolCallContent("ws123"), + new WebSearchToolResultContent("ws456"), ]); // Verify each element roundtrips individually foreach (AIContent content in message.Contents) { - var serializedElement = JsonSerializer.Serialize(content, AIJsonUtilities.DefaultOptions); - var deserializedElement = JsonSerializer.Deserialize(serializedElement, AIJsonUtilities.DefaultOptions); + var serializedElement = JsonSerializer.Serialize(content, options); + var deserializedElement = JsonSerializer.Deserialize(serializedElement, options); Assert.NotNull(deserializedElement); Assert.Equal(content.GetType(), deserializedElement.GetType()); } - var serialized = JsonSerializer.Serialize(message, AIJsonUtilities.DefaultOptions); - ChatMessage? deserialized = JsonSerializer.Deserialize(serialized, AIJsonUtilities.DefaultOptions); + var serialized = JsonSerializer.Serialize(message, options); + ChatMessage? deserialized = JsonSerializer.Deserialize(serialized, options); Assert.NotNull(deserialized); Assert.Equal(message.Role, deserialized.Role); diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/InputRequestContentTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/InputRequestContentTests.cs index cf5e5a3ac4d..1798955f98d 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/InputRequestContentTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/InputRequestContentTests.cs @@ -28,9 +28,13 @@ public void Constructor_Roundtrips(string id) Assert.Equal(id, content.RequestId); } - [Fact] - public void Serialization_DerivedTypes_Roundtrips() + [Theory] + [InlineData(false)] + [InlineData(true)] + public void Serialization_DerivedTypes_Roundtrips(bool useBuiltInJsonContext) { + JsonSerializerOptions options = useBuiltInJsonContext ? AIJsonUtilities.DefaultOptions : TestJsonSerializerContext.Default.Options; + InputRequestContent[] contents = [ new ToolApprovalRequestContent("request123", new FunctionCallContent("call123", "functionName", new Dictionary { { "param1", 123 } })), @@ -40,8 +44,8 @@ public void Serialization_DerivedTypes_Roundtrips() // Verify each element roundtrips individually foreach (var content in contents) { - var serialized = JsonSerializer.Serialize(content, AIJsonUtilities.DefaultOptions); - var deserialized = JsonSerializer.Deserialize(serialized, AIJsonUtilities.DefaultOptions); + var serialized = JsonSerializer.Serialize(content, options); + var deserialized = JsonSerializer.Deserialize(serialized, options); Assert.NotNull(deserialized); Assert.Equal(content.GetType(), deserialized.GetType()); } diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/InputResponseContentTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/InputResponseContentTests.cs index 207d8c432d4..71d7f656d9e 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/InputResponseContentTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/InputResponseContentTests.cs @@ -27,9 +27,13 @@ public void Constructor_Roundtrips(string id) Assert.Equal(id, content.RequestId); } - [Fact] - public void Serialization_DerivedTypes_Roundtrips() + [Theory] + [InlineData(false)] + [InlineData(true)] + public void Serialization_DerivedTypes_Roundtrips(bool useBuiltInJsonContext) { + JsonSerializerOptions options = useBuiltInJsonContext ? AIJsonUtilities.DefaultOptions : TestJsonSerializerContext.Default.Options; + InputResponseContent[] contents = [ new ToolApprovalResponseContent("request123", true, new FunctionCallContent("call123", "functionName")), @@ -39,8 +43,8 @@ public void Serialization_DerivedTypes_Roundtrips() // Verify each element roundtrips individually foreach (var content in contents) { - var serialized = JsonSerializer.Serialize(content, AIJsonUtilities.DefaultOptions); - var deserialized = JsonSerializer.Deserialize(serialized, AIJsonUtilities.DefaultOptions); + var serialized = JsonSerializer.Serialize(content, options); + var deserialized = JsonSerializer.Deserialize(serialized, options); Assert.NotNull(deserialized); Assert.Equal(content.GetType(), deserialized.GetType()); } diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/ToolCallContentTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/ToolCallContentTests.cs index a06a3d0c9eb..37f862348b1 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/ToolCallContentTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/ToolCallContentTests.cs @@ -44,31 +44,33 @@ public void Constructor_PropsRoundtrip() Assert.Equal("callId1", c.CallId); } - [Fact] - public void Serialization_DerivedTypes_Roundtrips() + [Theory] + [InlineData(false)] + [InlineData(true)] + public void Serialization_DerivedTypes_Roundtrips(bool useBuiltInJsonContext) { + JsonSerializerOptions options = useBuiltInJsonContext ? AIJsonUtilities.DefaultOptions : TestJsonSerializerContext.Default.Options; + ChatMessage message = new(ChatRole.Assistant, [ new FunctionCallContent("call1", "function1", new Dictionary { { "param1", 123 } }), new McpServerToolCallContent("call2", "myTool", "myServer"), new CodeInterpreterToolCallContent("call3"), new ImageGenerationToolCallContent("call4"), + new WebSearchToolCallContent("call5"), ]); // Verify each element roundtrips individually foreach (var content in message.Contents) { - var serialized = JsonSerializer.Serialize(content, AIJsonUtilities.DefaultOptions); - var deserialized = JsonSerializer.Deserialize(serialized, AIJsonUtilities.DefaultOptions); + var serialized = JsonSerializer.Serialize(content, options); + var deserialized = JsonSerializer.Deserialize(serialized, options); Assert.NotNull(deserialized); Assert.Equal(content.GetType(), deserialized.GetType()); } - // Verify the message roundtrips - can't use Array because that's not included as - // JsonSerializable in AIJsonUtilities and we can't use TestJsonSerializerContext here - // because it doesn't include the experimental types. - var serializedMessage = JsonSerializer.Serialize(message, AIJsonUtilities.DefaultOptions); - ChatMessage? deserialized2 = JsonSerializer.Deserialize(serializedMessage, AIJsonUtilities.DefaultOptions); + var serializedMessage = JsonSerializer.Serialize(message, options); + ChatMessage? deserialized2 = JsonSerializer.Deserialize(serializedMessage, options); Assert.NotNull(deserialized2); Assert.Equal(message.Role, deserialized2.Role); diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/ToolResultContentTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/ToolResultContentTests.cs index 7d0f2282b1a..fa33e619908 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/ToolResultContentTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/ToolResultContentTests.cs @@ -43,31 +43,33 @@ public void Constructor_PropsRoundtrip() Assert.Equal("callId1", c.CallId); } - [Fact] - public void Serialization_DerivedTypes_Roundtrips() + [Theory] + [InlineData(false)] + [InlineData(true)] + public void Serialization_DerivedTypes_Roundtrips(bool useBuiltInJsonContext) { + JsonSerializerOptions options = useBuiltInJsonContext ? AIJsonUtilities.DefaultOptions : TestJsonSerializerContext.Default.Options; + ChatMessage message = new(ChatRole.Tool, [ new FunctionResultContent("call1", "result1"), new McpServerToolResultContent("call2"), new CodeInterpreterToolResultContent("call3"), new ImageGenerationToolResultContent("call4"), + new WebSearchToolResultContent("call5"), ]); // Verify each element roundtrips individually foreach (var content in message.Contents) { - var serialized = JsonSerializer.Serialize(content, AIJsonUtilities.DefaultOptions); - var deserialized = JsonSerializer.Deserialize(serialized, AIJsonUtilities.DefaultOptions); + var serialized = JsonSerializer.Serialize(content, options); + var deserialized = JsonSerializer.Deserialize(serialized, options); Assert.NotNull(deserialized); Assert.Equal(content.GetType(), deserialized.GetType()); } - // Verify the message roundtrips - can't use Array because that's not included as - // JsonSerializable in AIJsonUtilities and we can't use TestJsonSerializerContext here - // because it doesn't include the experimental types. - var serializedMessage = JsonSerializer.Serialize(message, AIJsonUtilities.DefaultOptions); - ChatMessage? deserialized2 = JsonSerializer.Deserialize(serializedMessage, AIJsonUtilities.DefaultOptions); + var serializedMessage = JsonSerializer.Serialize(message, options); + ChatMessage? deserialized2 = JsonSerializer.Deserialize(serializedMessage, options); Assert.NotNull(deserialized2); Assert.Equal(message.Role, deserialized2.Role); From da8cad1a44807aee526b582c88e5724e36451aa1 Mon Sep 17 00:00:00 2001 From: David Cantu Date: Tue, 28 Apr 2026 11:38:36 -0500 Subject: [PATCH 3/4] Suppress ApiCompat CP0002 for WebSearchToolResultContent.Results rename The Results property on WebSearchToolResultContent was renamed to Outputs as part of stabilizing the type. Since the experimental member shipped in 10.4.0, the renamed (removed) member trips package validation. Suppress the diagnostic for all target frameworks via a baseline suppression file. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../CompatibilitySuppressions.xml | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 src/Libraries/Microsoft.Extensions.AI.Abstractions/CompatibilitySuppressions.xml diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/CompatibilitySuppressions.xml b/src/Libraries/Microsoft.Extensions.AI.Abstractions/CompatibilitySuppressions.xml new file mode 100644 index 00000000000..6d31cc6cd72 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/CompatibilitySuppressions.xml @@ -0,0 +1,74 @@ + + + + + CP0002 + M:Microsoft.Extensions.AI.WebSearchToolResultContent.get_Results + lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.WebSearchToolResultContent.set_Results(System.Collections.Generic.IList{Microsoft.Extensions.AI.AIContent}) + lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.WebSearchToolResultContent.get_Results + lib/net462/Microsoft.Extensions.AI.Abstractions.dll + lib/net462/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.WebSearchToolResultContent.set_Results(System.Collections.Generic.IList{Microsoft.Extensions.AI.AIContent}) + lib/net462/Microsoft.Extensions.AI.Abstractions.dll + lib/net462/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.WebSearchToolResultContent.get_Results + lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.WebSearchToolResultContent.set_Results(System.Collections.Generic.IList{Microsoft.Extensions.AI.AIContent}) + lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.WebSearchToolResultContent.get_Results + lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.WebSearchToolResultContent.set_Results(System.Collections.Generic.IList{Microsoft.Extensions.AI.AIContent}) + lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.WebSearchToolResultContent.get_Results + lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll + lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.WebSearchToolResultContent.set_Results(System.Collections.Generic.IList{Microsoft.Extensions.AI.AIContent}) + lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll + lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll + true + + \ No newline at end of file From d2efbfc8a15f360b8839ea9b372a20a2ffb7af1c Mon Sep 17 00:00:00 2001 From: David Cantu Date: Tue, 28 Apr 2026 16:55:11 -0500 Subject: [PATCH 4/4] Remove unused AICodeInterpreter and AIWebSearch DiagnosticIds These constants no longer have any references after stabilizing the CodeInterpreter and WebSearch content types. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Shared/DiagnosticIds/DiagnosticIds.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Shared/DiagnosticIds/DiagnosticIds.cs b/src/Shared/DiagnosticIds/DiagnosticIds.cs index ab5f3eb1ccc..ff13437e69e 100644 --- a/src/Shared/DiagnosticIds/DiagnosticIds.cs +++ b/src/Shared/DiagnosticIds/DiagnosticIds.cs @@ -56,8 +56,6 @@ internal static class Experiments internal const string AIChatReduction = AIExperiments; internal const string AIResponseContinuations = AIExperiments; - internal const string AICodeInterpreter = AIExperiments; - internal const string AIWebSearch = AIExperiments; internal const string AIToolSearch = AIExperiments; internal const string AIRealTime = AIExperiments; internal const string AIFiles = AIExperiments;