diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIResponsesExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIResponsesExtensions.cs
index 0d119a742e2..ea80ae8e794 100644
--- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIResponsesExtensions.cs
+++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIResponsesExtensions.cs
@@ -21,6 +21,17 @@ 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 .
+ ///
+ /// 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));
+
///
/// 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..c1617dc0d08 100644
--- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs
+++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs
@@ -431,6 +431,97 @@ 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:
+ return ToResponseTool(aiFunction, options);
+
+ 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 =
@@ -492,96 +583,9 @@ private ResponseCreationOptions ToOpenAIResponseCreationOptions(ChatOptions? opt
{
foreach (AITool tool in tools)
{
- switch (tool)
+ if (ToResponseTool(tool, options) is { } responseTool)
{
- 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);
}
}
diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs
index 844fb5618ed..7fe1ceb8b57 100644
--- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs
+++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs
@@ -112,6 +112,283 @@ public void AsOpenAIResponseTool_ProducesValidInstance()
Assert.NotNull(tool);
}
+ [Fact]
+ public void AsOpenAIResponseTool_WithAIFunctionTool_ProducesValidFunctionTool()
+ {
+ var tool = MicrosoftExtensionsAIResponsesExtensions.AsOpenAIResponseTool(tool: _testFunction);
+
+ Assert.NotNull(tool);
+ var functionTool = Assert.IsType(tool);
+ Assert.Equal("test_function", functionTool.FunctionName);
+ Assert.Equal("A test function for conversion", functionTool.FunctionDescription);
+ }
+
+ [Fact]
+ public void AsOpenAIResponseTool_WithHostedWebSearchTool_ProducesValidWebSearchTool()
+ {
+ var webSearchTool = new HostedWebSearchTool();
+
+ var result = webSearchTool.AsOpenAIResponseTool();
+
+ Assert.NotNull(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 { MaximumResultCount = 10 };
+
+ var result = fileSearchTool.AsOpenAIResponseTool();
+
+ Assert.NotNull(result);
+ var tool = Assert.IsType(result);
+ Assert.Empty(tool.VectorStoreIds);
+ Assert.Equal(fileSearchTool.MaximumResultCount, tool.MaxResultCount);
+ }
+
+ [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);
+ Assert.NotNull(tool.Container);
+ Assert.NotNull(tool.Container.ContainerConfiguration);
+ }
+
+ [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 expectedUri = new Uri("http://localhost:8000");
+ var mcpTool = new HostedMcpServerTool("test-server", expectedUri);
+
+ var result = mcpTool.AsOpenAIResponseTool();
+
+ Assert.NotNull(result);
+ var tool = Assert.IsType(result);
+ Assert.Equal(expectedUri, tool.ServerUri);
+ 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()
+ {
+ 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 +1533,23 @@ 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";
+ }
+
+ /// 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;
+ }
+ }
}