From 5be997456bbb125159c9995919b227ea5415777c Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> Date: Fri, 24 Oct 2025 15:01:23 +0100 Subject: [PATCH 01/11] Add utility for ResponseTool --- ...icrosoftExtensionsAIResponsesExtensions.cs | 7 ++ .../OpenAIResponsesChatClient.cs | 99 +++++++++++++++++++ 2 files changed, 106 insertions(+) diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIResponsesExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIResponsesExtensions.cs index 0d119a742e2..7ce897f7543 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIResponsesExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIResponsesExtensions.cs @@ -21,6 +21,13 @@ public static class MicrosoftExtensionsAIResponsesExtensions public static FunctionTool AsOpenAIResponseTool(this AIFunctionDeclaration function) => OpenAIResponsesChatClient.ToResponseTool(Throw.IfNull(function)); + /// Creates an OpenAI from an . + /// The tool to convert. + /// An OpenAI representing or if there is no mapping. + /// is . + public static ResponseTool? AsOpenAIResponseTool(this AITool tool) + => OpenAIResponsesChatClient.ToResponseTool(Throw.IfNull(tool)); + /// /// Creates an OpenAI from a . /// diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs index 039d04c1f8e..02b780b99e3 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs @@ -431,6 +431,105 @@ void IDisposable.Dispose() // Nothing to dispose. Implementation required for the IChatClient interface. } + internal static ResponseTool? ToResponseTool(AITool tool, ChatOptions? options = null) + { + switch (tool) + { + case ResponseToolAITool rtat: + return rtat.Tool; + + case AIFunctionDeclaration aiFunction: + bool? strict = + OpenAIClientExtensions.HasStrict(aiFunction.AdditionalProperties) ?? + OpenAIClientExtensions.HasStrict(options?.AdditionalProperties); + + return ResponseTool.CreateFunctionTool( + aiFunction.Name, + OpenAIClientExtensions.ToOpenAIFunctionParameters(aiFunction, strict), + strict, + aiFunction.Description); + + case HostedWebSearchTool webSearchTool: + WebSearchToolLocation? location = null; + if (webSearchTool.AdditionalProperties.TryGetValue(nameof(WebSearchToolLocation), out object? objLocation)) + { + location = objLocation as WebSearchToolLocation; + } + + WebSearchToolContextSize? size = null; + if (webSearchTool.AdditionalProperties.TryGetValue(nameof(WebSearchToolContextSize), out object? objSize) && + objSize is WebSearchToolContextSize) + { + size = (WebSearchToolContextSize)objSize; + } + + return ResponseTool.CreateWebSearchTool(location, size); + + case HostedFileSearchTool fileSearchTool: + return ResponseTool.CreateFileSearchTool( + fileSearchTool.Inputs?.OfType().Select(c => c.VectorStoreId) ?? [], + fileSearchTool.MaximumResultCount); + + case HostedCodeInterpreterTool codeTool: + return ResponseTool.CreateCodeInterpreterTool( + new CodeInterpreterToolContainer(codeTool.Inputs?.OfType().Select(f => f.FileId).ToList() is { Count: > 0 } ids ? + CodeInterpreterToolContainerConfiguration.CreateAutomaticContainerConfiguration(ids) : + new())); + + case HostedMcpServerTool mcpTool: + McpTool responsesMcpTool = Uri.TryCreate(mcpTool.ServerAddress, UriKind.Absolute, out Uri? url) ? + ResponseTool.CreateMcpTool( + mcpTool.ServerName, + url, + mcpTool.AuthorizationToken, + mcpTool.ServerDescription) : + ResponseTool.CreateMcpTool( + mcpTool.ServerName, + new McpToolConnectorId(mcpTool.ServerAddress), + mcpTool.AuthorizationToken, + mcpTool.ServerDescription); + + if (mcpTool.AllowedTools is not null) + { + responsesMcpTool.AllowedTools = new(); + AddAllMcpFilters(mcpTool.AllowedTools, responsesMcpTool.AllowedTools); + } + + switch (mcpTool.ApprovalMode) + { + case HostedMcpServerToolAlwaysRequireApprovalMode: + responsesMcpTool.ToolCallApprovalPolicy = new McpToolCallApprovalPolicy(GlobalMcpToolCallApprovalPolicy.AlwaysRequireApproval); + break; + + case HostedMcpServerToolNeverRequireApprovalMode: + responsesMcpTool.ToolCallApprovalPolicy = new McpToolCallApprovalPolicy(GlobalMcpToolCallApprovalPolicy.NeverRequireApproval); + break; + + case HostedMcpServerToolRequireSpecificApprovalMode specificMode: + responsesMcpTool.ToolCallApprovalPolicy = new McpToolCallApprovalPolicy(new CustomMcpToolCallApprovalPolicy()); + + if (specificMode.AlwaysRequireApprovalToolNames is { Count: > 0 } alwaysRequireToolNames) + { + responsesMcpTool.ToolCallApprovalPolicy.CustomPolicy.ToolsAlwaysRequiringApproval = new(); + AddAllMcpFilters(alwaysRequireToolNames, responsesMcpTool.ToolCallApprovalPolicy.CustomPolicy.ToolsAlwaysRequiringApproval); + } + + if (specificMode.NeverRequireApprovalToolNames is { Count: > 0 } neverRequireToolNames) + { + responsesMcpTool.ToolCallApprovalPolicy.CustomPolicy.ToolsNeverRequiringApproval = new(); + AddAllMcpFilters(neverRequireToolNames, responsesMcpTool.ToolCallApprovalPolicy.CustomPolicy.ToolsNeverRequiringApproval); + } + + break; + } + + return responsesMcpTool; + + default: + return null; + } + } + internal static FunctionTool ToResponseTool(AIFunctionDeclaration aiFunction, ChatOptions? options = null) { bool? strict = From a23c230308a8c8ca40c1aeb0767ea0ccef62f9dc Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> Date: Fri, 24 Oct 2025 15:26:15 +0100 Subject: [PATCH 02/11] Adding UT for Utility --- .../OpenAIConversionTests.cs | 168 ++++++++++++++++++ 1 file changed, 168 insertions(+) diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs index 844fb5618ed..2ffc775a11a 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs @@ -112,6 +112,168 @@ public void AsOpenAIResponseTool_ProducesValidInstance() Assert.NotNull(tool); } + [Fact] + public void AsOpenAIResponseTool_WithAIFunctionDeclaration_ProducesValidFunctionTool() + { + var tool = _testFunction.AsOpenAIResponseTool(); + + Assert.NotNull(tool); + Assert.IsType(tool); + } + + [Fact] + public void AsOpenAIResponseTool_WithHostedWebSearchTool_ProducesValidWebSearchTool() + { + var webSearchTool = new HostedWebSearchTool(); + + var result = webSearchTool.AsOpenAIResponseTool(); + + Assert.NotNull(result); + Assert.IsType(result); + } + + [Fact] + public void AsOpenAIResponseTool_WithHostedFileSearchTool_ProducesValidFileSearchTool() + { + var fileSearchTool = new HostedFileSearchTool(); + + var result = fileSearchTool.AsOpenAIResponseTool(); + + Assert.NotNull(result); + Assert.IsType(result); + } + + [Fact] + public void AsOpenAIResponseTool_WithHostedFileSearchToolWithVectorStores_ProducesValidFileSearchTool() + { + var vectorStoreContent = new HostedVectorStoreContent("vector-store-123"); + var fileSearchTool = new HostedFileSearchTool + { + Inputs = [vectorStoreContent] + }; + + var result = fileSearchTool.AsOpenAIResponseTool(); + + Assert.NotNull(result); + var tool = Assert.IsType(result); + Assert.Single(tool.VectorStoreIds); + Assert.Equal(vectorStoreContent.VectorStoreId, tool.VectorStoreIds[0]); + } + + [Fact] + public void AsOpenAIResponseTool_WithHostedFileSearchToolWithMaxResults_ProducesValidFileSearchTool() + { + var fileSearchTool = new HostedFileSearchTool + { + MaximumResultCount = 10 + }; + + var result = fileSearchTool.AsOpenAIResponseTool(); + + Assert.NotNull(result); + var tool = Assert.IsType(result); + Assert.Equal(10, tool.MaxResultCount); + } + + [Fact] + public void AsOpenAIResponseTool_WithHostedCodeInterpreterTool_ProducesValidCodeInterpreterTool() + { + var codeTool = new HostedCodeInterpreterTool(); + + var result = codeTool.AsOpenAIResponseTool(); + + Assert.NotNull(result); + var tool = Assert.IsType(result); + } + + [Fact] + public void AsOpenAIResponseTool_WithHostedCodeInterpreterToolWithFiles_ProducesValidCodeInterpreterTool() + { + var fileContent = new HostedFileContent("file-123"); + var codeTool = new HostedCodeInterpreterTool + { + Inputs = [fileContent] + }; + + var result = codeTool.AsOpenAIResponseTool(); + + Assert.NotNull(result); + var tool = Assert.IsType(result); + var autoContainerConfig = Assert.IsType(tool.Container.ContainerConfiguration); + Assert.Single(autoContainerConfig.FileIds); + Assert.Equal(fileContent.FileId, autoContainerConfig.FileIds[0]); + } + + [Fact] + public void AsOpenAIResponseTool_WithHostedMcpServerTool_ProducesValidMcpTool() + { + var mcpTool = new HostedMcpServerTool("test-server", "http://localhost:8000"); + + var result = mcpTool.AsOpenAIResponseTool(); + + Assert.NotNull(result); + var tool = Assert.IsType(result); + Assert.Equal(new Uri("http://localhost:8000"), tool.ServerUri); + Assert.Equal("test-server", tool.ServerLabel); + } + + [Fact] + public void AsOpenAIResponseTool_WithHostedMcpServerToolWithDescription_ProducesValidMcpTool() + { + var mcpTool = new HostedMcpServerTool("test-server", "http://localhost:8000") + { + ServerDescription = "A test MCP server" + }; + + var result = mcpTool.AsOpenAIResponseTool(); + + Assert.NotNull(result); + var tool = Assert.IsType(result); + Assert.Equal("A test MCP server", tool.ServerDescription); + } + + [Fact] + public void AsOpenAIResponseTool_WithHostedMcpServerToolWithAuthToken_ProducesValidMcpTool() + { + var mcpTool = new HostedMcpServerTool("test-server", "http://localhost:8000") + { + AuthorizationToken = "test-token" + }; + + var result = mcpTool.AsOpenAIResponseTool(); + + Assert.NotNull(result); + var tool = Assert.IsType(result); + Assert.Equal("test-token", tool.AuthorizationToken); + } + + [Fact] + public void AsOpenAIResponseTool_WithHostedMcpServerToolWithUri_ProducesValidMcpTool() + { + var mcpTool = new HostedMcpServerTool("test-server", new Uri("http://localhost:8000")); + + var result = mcpTool.AsOpenAIResponseTool(); + + Assert.NotNull(result); + Assert.IsType(result); + } + + [Fact] + public void AsOpenAIResponseTool_WithUnknownToolType_ReturnsNull() + { + var unknownTool = new UnknownAITool(); + + var result = unknownTool.AsOpenAIResponseTool(); + + Assert.Null(result); + } + + [Fact] + public void AsOpenAIResponseTool_WithNullTool_ThrowsArgumentNullException() + { + Assert.Throws("tool", () => ((AITool)null!).AsOpenAIResponseTool()); + } + [Fact] public void AsOpenAIConversationFunctionTool_ProducesValidInstance() { @@ -1256,4 +1418,10 @@ private static async IAsyncEnumerable CreateAsyncEnumerable(IEnumerable } private static string RemoveWhitespace(string input) => Regex.Replace(input, @"\s+", ""); + + /// Helper class for testing unknown tool types. + private sealed class UnknownAITool : AITool + { + public override string Name => "unknown_tool"; + } } From df6ac55b68c89b151faeb3b8766df1112e7ab30d Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> Date: Fri, 24 Oct 2025 15:33:33 +0100 Subject: [PATCH 03/11] Update internals to utility --- .../OpenAIResponsesChatClient.cs | 92 +------------------ 1 file changed, 3 insertions(+), 89 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs index 02b780b99e3..6f46af072d0 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs @@ -591,96 +591,10 @@ private ResponseCreationOptions ToOpenAIResponseCreationOptions(ChatOptions? opt { foreach (AITool tool in tools) { - switch (tool) + var responseTool = ToResponseTool(tool, options); + if (responseTool is not null) { - case ResponseToolAITool rtat: - result.Tools.Add(rtat.Tool); - break; - - case AIFunctionDeclaration aiFunction: - result.Tools.Add(ToResponseTool(aiFunction, options)); - break; - - case HostedWebSearchTool webSearchTool: - WebSearchToolLocation? location = null; - if (webSearchTool.AdditionalProperties.TryGetValue(nameof(WebSearchToolLocation), out object? objLocation)) - { - location = objLocation as WebSearchToolLocation; - } - - WebSearchToolContextSize? size = null; - if (webSearchTool.AdditionalProperties.TryGetValue(nameof(WebSearchToolContextSize), out object? objSize) && - objSize is WebSearchToolContextSize) - { - size = (WebSearchToolContextSize)objSize; - } - - result.Tools.Add(ResponseTool.CreateWebSearchTool(location, size)); - break; - - case HostedFileSearchTool fileSearchTool: - result.Tools.Add(ResponseTool.CreateFileSearchTool( - fileSearchTool.Inputs?.OfType().Select(c => c.VectorStoreId) ?? [], - fileSearchTool.MaximumResultCount)); - break; - - case HostedCodeInterpreterTool codeTool: - result.Tools.Add( - ResponseTool.CreateCodeInterpreterTool( - new CodeInterpreterToolContainer(codeTool.Inputs?.OfType().Select(f => f.FileId).ToList() is { Count: > 0 } ids ? - CodeInterpreterToolContainerConfiguration.CreateAutomaticContainerConfiguration(ids) : - new()))); - break; - - case HostedMcpServerTool mcpTool: - McpTool responsesMcpTool = Uri.TryCreate(mcpTool.ServerAddress, UriKind.Absolute, out Uri? url) ? - ResponseTool.CreateMcpTool( - mcpTool.ServerName, - url, - mcpTool.AuthorizationToken, - mcpTool.ServerDescription) : - ResponseTool.CreateMcpTool( - mcpTool.ServerName, - new McpToolConnectorId(mcpTool.ServerAddress), - mcpTool.AuthorizationToken, - mcpTool.ServerDescription); - - if (mcpTool.AllowedTools is not null) - { - responsesMcpTool.AllowedTools = new(); - AddAllMcpFilters(mcpTool.AllowedTools, responsesMcpTool.AllowedTools); - } - - switch (mcpTool.ApprovalMode) - { - case HostedMcpServerToolAlwaysRequireApprovalMode: - responsesMcpTool.ToolCallApprovalPolicy = new McpToolCallApprovalPolicy(GlobalMcpToolCallApprovalPolicy.AlwaysRequireApproval); - break; - - case HostedMcpServerToolNeverRequireApprovalMode: - responsesMcpTool.ToolCallApprovalPolicy = new McpToolCallApprovalPolicy(GlobalMcpToolCallApprovalPolicy.NeverRequireApproval); - break; - - case HostedMcpServerToolRequireSpecificApprovalMode specificMode: - responsesMcpTool.ToolCallApprovalPolicy = new McpToolCallApprovalPolicy(new CustomMcpToolCallApprovalPolicy()); - - if (specificMode.AlwaysRequireApprovalToolNames is { Count: > 0 } alwaysRequireToolNames) - { - responsesMcpTool.ToolCallApprovalPolicy.CustomPolicy.ToolsAlwaysRequiringApproval = new(); - AddAllMcpFilters(alwaysRequireToolNames, responsesMcpTool.ToolCallApprovalPolicy.CustomPolicy.ToolsAlwaysRequiringApproval); - } - - if (specificMode.NeverRequireApprovalToolNames is { Count: > 0 } neverRequireToolNames) - { - responsesMcpTool.ToolCallApprovalPolicy.CustomPolicy.ToolsNeverRequiringApproval = new(); - AddAllMcpFilters(neverRequireToolNames, responsesMcpTool.ToolCallApprovalPolicy.CustomPolicy.ToolsNeverRequiringApproval); - } - - break; - } - - result.Tools.Add(responsesMcpTool); - break; + result.Tools.Add(responseTool); } } From 4eeef7cb85022a7e99ff57c311983e91bcc5a608 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> Date: Fri, 24 Oct 2025 15:44:38 +0100 Subject: [PATCH 04/11] Update src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs Co-authored-by: Stephen Toub --- .../OpenAIResponsesChatClient.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs index 6f46af072d0..cb47a0a2b88 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs @@ -591,8 +591,7 @@ private ResponseCreationOptions ToOpenAIResponseCreationOptions(ChatOptions? opt { foreach (AITool tool in tools) { - var responseTool = ToResponseTool(tool, options); - if (responseTool is not null) + if (ToResponseTool(tool, options) is { } responseTool) { result.Tools.Add(responseTool); } From 5e0d9d1e2fba280da4a0b930755ee9d286a68620 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> Date: Fri, 24 Oct 2025 16:00:06 +0100 Subject: [PATCH 05/11] Update src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIResponsesExtensions.cs Co-authored-by: Stephen Toub --- .../MicrosoftExtensionsAIResponsesExtensions.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIResponsesExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIResponsesExtensions.cs index 7ce897f7543..0562a34ca06 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIResponsesExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIResponsesExtensions.cs @@ -25,6 +25,10 @@ public static FunctionTool AsOpenAIResponseTool(this AIFunctionDeclaration funct /// The tool to convert. /// An OpenAI representing or if there is no mapping. /// is . + /// + /// This method is only able to create s for types + /// it's aware of, namely all of those available from the Microsoft.Extensions.AI.Abstractions library. + /// public static ResponseTool? AsOpenAIResponseTool(this AITool tool) => OpenAIResponsesChatClient.ToResponseTool(Throw.IfNull(tool)); From e71090ec3df340060a884f60808f903fbe319df1 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> Date: Fri, 24 Oct 2025 16:00:38 +0100 Subject: [PATCH 06/11] Update src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs Co-authored-by: Stephen Toub --- .../OpenAIResponsesChatClient.cs | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs index cb47a0a2b88..12dd8a12c42 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs @@ -439,15 +439,7 @@ void IDisposable.Dispose() return rtat.Tool; case AIFunctionDeclaration aiFunction: - bool? strict = - OpenAIClientExtensions.HasStrict(aiFunction.AdditionalProperties) ?? - OpenAIClientExtensions.HasStrict(options?.AdditionalProperties); - - return ResponseTool.CreateFunctionTool( - aiFunction.Name, - OpenAIClientExtensions.ToOpenAIFunctionParameters(aiFunction, strict), - strict, - aiFunction.Description); + return ToResponseTool(aiFunction, options); case HostedWebSearchTool webSearchTool: WebSearchToolLocation? location = null; From 54bfcdb4cb6b3dd0b1c35effe9ac269ef45670ff Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> Date: Fri, 24 Oct 2025 16:00:59 +0100 Subject: [PATCH 07/11] Update src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs Co-authored-by: Stephen Toub --- .../OpenAIResponsesChatClient.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs index 12dd8a12c42..c1617dc0d08 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs @@ -464,9 +464,9 @@ void IDisposable.Dispose() case HostedCodeInterpreterTool codeTool: return ResponseTool.CreateCodeInterpreterTool( - new CodeInterpreterToolContainer(codeTool.Inputs?.OfType().Select(f => f.FileId).ToList() is { Count: > 0 } ids ? - CodeInterpreterToolContainerConfiguration.CreateAutomaticContainerConfiguration(ids) : - new())); + new CodeInterpreterToolContainer(codeTool.Inputs?.OfType().Select(f => f.FileId).ToList() is { Count: > 0 } ids ? + CodeInterpreterToolContainerConfiguration.CreateAutomaticContainerConfiguration(ids) : + new())); case HostedMcpServerTool mcpTool: McpTool responsesMcpTool = Uri.TryCreate(mcpTool.ServerAddress, UriKind.Absolute, out Uri? url) ? From 0f8976c19231e91bd547f5ba091164e032ef68e2 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> Date: Fri, 24 Oct 2025 16:46:35 +0100 Subject: [PATCH 08/11] Update test to use the AITool extension directly --- .../OpenAIConversionTests.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs index 2ffc775a11a..27f943daaba 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs @@ -113,12 +113,14 @@ public void AsOpenAIResponseTool_ProducesValidInstance() } [Fact] - public void AsOpenAIResponseTool_WithAIFunctionDeclaration_ProducesValidFunctionTool() + public void AsOpenAIResponseTool_WithAIFunctionTool_ProducesValidFunctionTool() { - var tool = _testFunction.AsOpenAIResponseTool(); + var tool = MicrosoftExtensionsAIResponsesExtensions.AsOpenAIResponseTool(tool: _testFunction); Assert.NotNull(tool); - Assert.IsType(tool); + var functionTool = Assert.IsType(tool); + Assert.Equal("test_function", functionTool.FunctionName); + Assert.Equal("A test function for conversion", functionTool.FunctionDescription); } [Fact] From 6434d1b4808fa31251f036a9c9a3803ce9d71433 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> Date: Fri, 24 Oct 2025 17:17:44 +0100 Subject: [PATCH 09/11] Improve UT --- .../OpenAIConversionTests.cs | 56 +++++++++++++++++-- 1 file changed, 51 insertions(+), 5 deletions(-) diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs index 27f943daaba..608aceedbf9 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs @@ -131,18 +131,46 @@ public void AsOpenAIResponseTool_WithHostedWebSearchTool_ProducesValidWebSearchT var result = webSearchTool.AsOpenAIResponseTool(); Assert.NotNull(result); - Assert.IsType(result); + var tool = Assert.IsType(result); + Assert.NotNull(tool); + } + + [Fact] + public void AsOpenAIResponseTool_WithHostedWebSearchToolWithAdditionalProperties_ProducesValidWebSearchTool() + { + var location = WebSearchToolLocation.CreateApproximateLocation("US", "Region", "City", "UTC"); + var webSearchTool = new HostedWebSearchToolWithProperties(new Dictionary + { + [nameof(WebSearchToolLocation)] = location, + [nameof(WebSearchToolContextSize)] = WebSearchToolContextSize.High + }); + + var result = webSearchTool.AsOpenAIResponseTool(); + + Assert.NotNull(result); + + var tool = Assert.IsType(result); + Assert.Equal(WebSearchToolContextSize.High, tool.SearchContextSize); + Assert.NotNull(tool); + + var approximateLocation = Assert.IsType(tool.UserLocation); + Assert.Equal(location.Country, approximateLocation.Country); + Assert.Equal(location.Region, approximateLocation.Region); + Assert.Equal(location.City, approximateLocation.City); + Assert.Equal(location.Timezone, approximateLocation.Timezone); } [Fact] public void AsOpenAIResponseTool_WithHostedFileSearchTool_ProducesValidFileSearchTool() { - var fileSearchTool = new HostedFileSearchTool(); + var fileSearchTool = new HostedFileSearchTool { MaximumResultCount = 10 }; var result = fileSearchTool.AsOpenAIResponseTool(); Assert.NotNull(result); - Assert.IsType(result); + var tool = Assert.IsType(result); + Assert.Empty(tool.VectorStoreIds); + Assert.Equal(fileSearchTool.MaximumResultCount, tool.MaxResultCount); } [Fact] @@ -186,6 +214,8 @@ public void AsOpenAIResponseTool_WithHostedCodeInterpreterTool_ProducesValidCode Assert.NotNull(result); var tool = Assert.IsType(result); + Assert.NotNull(tool.Container); + Assert.NotNull(tool.Container.ContainerConfiguration); } [Fact] @@ -252,12 +282,15 @@ public void AsOpenAIResponseTool_WithHostedMcpServerToolWithAuthToken_ProducesVa [Fact] public void AsOpenAIResponseTool_WithHostedMcpServerToolWithUri_ProducesValidMcpTool() { - var mcpTool = new HostedMcpServerTool("test-server", new Uri("http://localhost:8000")); + var expectedUri = new Uri("http://localhost:8000"); + var mcpTool = new HostedMcpServerTool("test-server", expectedUri); var result = mcpTool.AsOpenAIResponseTool(); Assert.NotNull(result); - Assert.IsType(result); + var tool = Assert.IsType(result); + Assert.Equal(expectedUri, tool.ServerUri); + Assert.Equal("test-server", tool.ServerLabel); } [Fact] @@ -1426,4 +1459,17 @@ private sealed class UnknownAITool : AITool { public override string Name => "unknown_tool"; } + + /// Helper class for testing WebSearchTool with additional properties. + private sealed class HostedWebSearchToolWithProperties : HostedWebSearchTool + { + private readonly Dictionary _additionalProperties; + + public override IReadOnlyDictionary AdditionalProperties => _additionalProperties; + + public HostedWebSearchToolWithProperties(Dictionary additionalProperties) + { + _additionalProperties = additionalProperties; + } + } } From 08949d29a9de52a45218aa7d83e1bd5c58a8f62c Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> Date: Fri, 24 Oct 2025 17:26:31 +0100 Subject: [PATCH 10/11] Add MCP missing UT --- .../OpenAIConversionTests.cs | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs index 608aceedbf9..7fe1ceb8b57 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs @@ -293,6 +293,86 @@ public void AsOpenAIResponseTool_WithHostedMcpServerToolWithUri_ProducesValidMcp Assert.Equal("test-server", tool.ServerLabel); } + [Fact] + public void AsOpenAIResponseTool_WithHostedMcpServerToolWithAllowedTools_ProducesValidMcpTool() + { + var allowedTools = new List { "tool1", "tool2", "tool3" }; + var mcpTool = new HostedMcpServerTool("test-server", "http://localhost:8000") + { + AllowedTools = allowedTools + }; + + var result = mcpTool.AsOpenAIResponseTool(); + + Assert.NotNull(result); + var tool = Assert.IsType(result); + Assert.NotNull(tool.AllowedTools); + Assert.Equal(3, tool.AllowedTools.ToolNames.Count); + Assert.Contains("tool1", tool.AllowedTools.ToolNames); + Assert.Contains("tool2", tool.AllowedTools.ToolNames); + Assert.Contains("tool3", tool.AllowedTools.ToolNames); + } + + [Fact] + public void AsOpenAIResponseTool_WithHostedMcpServerToolWithAlwaysRequireApprovalMode_ProducesValidMcpTool() + { + var mcpTool = new HostedMcpServerTool("test-server", "http://localhost:8000") + { + ApprovalMode = HostedMcpServerToolApprovalMode.AlwaysRequire + }; + + var result = mcpTool.AsOpenAIResponseTool(); + + Assert.NotNull(result); + var tool = Assert.IsType(result); + Assert.NotNull(tool.ToolCallApprovalPolicy); + Assert.NotNull(tool.ToolCallApprovalPolicy.GlobalPolicy); + Assert.Equal(GlobalMcpToolCallApprovalPolicy.AlwaysRequireApproval, tool.ToolCallApprovalPolicy.GlobalPolicy); + } + + [Fact] + public void AsOpenAIResponseTool_WithHostedMcpServerToolWithNeverRequireApprovalMode_ProducesValidMcpTool() + { + var mcpTool = new HostedMcpServerTool("test-server", "http://localhost:8000") + { + ApprovalMode = HostedMcpServerToolApprovalMode.NeverRequire + }; + + var result = mcpTool.AsOpenAIResponseTool(); + + Assert.NotNull(result); + var tool = Assert.IsType(result); + Assert.NotNull(tool.ToolCallApprovalPolicy); + Assert.NotNull(tool.ToolCallApprovalPolicy.GlobalPolicy); + Assert.Equal(GlobalMcpToolCallApprovalPolicy.NeverRequireApproval, tool.ToolCallApprovalPolicy.GlobalPolicy); + } + + [Fact] + public void AsOpenAIResponseTool_WithHostedMcpServerToolWithRequireSpecificApprovalMode_ProducesValidMcpTool() + { + var alwaysRequireTools = new List { "tool1", "tool2" }; + var neverRequireTools = new List { "tool3" }; + var approvalMode = HostedMcpServerToolApprovalMode.RequireSpecific(alwaysRequireTools, neverRequireTools); + var mcpTool = new HostedMcpServerTool("test-server", "http://localhost:8000") + { + ApprovalMode = approvalMode + }; + + var result = mcpTool.AsOpenAIResponseTool(); + + Assert.NotNull(result); + var tool = Assert.IsType(result); + Assert.NotNull(tool.ToolCallApprovalPolicy); + Assert.NotNull(tool.ToolCallApprovalPolicy.CustomPolicy); + Assert.NotNull(tool.ToolCallApprovalPolicy.CustomPolicy.ToolsAlwaysRequiringApproval); + Assert.NotNull(tool.ToolCallApprovalPolicy.CustomPolicy.ToolsNeverRequiringApproval); + Assert.Equal(2, tool.ToolCallApprovalPolicy.CustomPolicy.ToolsAlwaysRequiringApproval.ToolNames.Count); + Assert.Single(tool.ToolCallApprovalPolicy.CustomPolicy.ToolsNeverRequiringApproval.ToolNames); + Assert.Contains("tool1", tool.ToolCallApprovalPolicy.CustomPolicy.ToolsAlwaysRequiringApproval.ToolNames); + Assert.Contains("tool2", tool.ToolCallApprovalPolicy.CustomPolicy.ToolsAlwaysRequiringApproval.ToolNames); + Assert.Contains("tool3", tool.ToolCallApprovalPolicy.CustomPolicy.ToolsNeverRequiringApproval.ToolNames); + } + [Fact] public void AsOpenAIResponseTool_WithUnknownToolType_ReturnsNull() { From ea2a2023ec75ff16f4aac46fa1edda6b3710f7c1 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> Date: Fri, 24 Oct 2025 17:27:08 +0100 Subject: [PATCH 11/11] Update src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIResponsesExtensions.cs Co-authored-by: Stephen Toub --- .../MicrosoftExtensionsAIResponsesExtensions.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIResponsesExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIResponsesExtensions.cs index 0562a34ca06..ea80ae8e794 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIResponsesExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIResponsesExtensions.cs @@ -29,8 +29,8 @@ public static FunctionTool AsOpenAIResponseTool(this AIFunctionDeclaration funct /// This method is only able to create s for types /// it's aware of, namely all of those available from the Microsoft.Extensions.AI.Abstractions library. /// - public static ResponseTool? AsOpenAIResponseTool(this AITool tool) - => OpenAIResponsesChatClient.ToResponseTool(Throw.IfNull(tool)); + public static ResponseTool? AsOpenAIResponseTool(this AITool tool) => + OpenAIResponsesChatClient.ToResponseTool(Throw.IfNull(tool)); /// /// Creates an OpenAI from a .