diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/CHANGELOG.md b/src/Libraries/Microsoft.Extensions.AI.Abstractions/CHANGELOG.md
index cc911fd64bb..526072374c4 100644
--- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/CHANGELOG.md
+++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/CHANGELOG.md
@@ -4,6 +4,7 @@
- Added new `ChatResponseFormat.ForJsonSchema` overloads that export a JSON schema from a .NET type.
- Updated `TextReasoningContent` to include `ProtectedData` for representing encrypted/redacted content.
+- Fixed `MinLength`/`MaxLength`/`Length` attribute mapping in nullable string properties during schema export.
## 9.9.0
diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/CHANGELOG.md b/src/Libraries/Microsoft.Extensions.AI.OpenAI/CHANGELOG.md
index 4d858edeb89..a50aea8dea3 100644
--- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/CHANGELOG.md
+++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/CHANGELOG.md
@@ -2,7 +2,8 @@
## NOT YET RELEASED
-- Added M.E.AI to OpenAI conversions for response format types
+- Added M.E.AI to OpenAI conversions for response format types.
+- Added `ResponseTool` to `AITool` conversions.
## 9.9.0-preview.1.25458.4
diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIResponsesExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIResponsesExtensions.cs
index ca4bfd3f736..c632f3c45c7 100644
--- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIResponsesExtensions.cs
+++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIResponsesExtensions.cs
@@ -93,4 +93,46 @@ public static OpenAIResponse AsOpenAIResponse(this ChatResponse response, ChatOp
previousResponseId: options?.ConversationId,
instructions: options?.Instructions);
}
+
+ /// Adds the to the list of s.
+ /// The list of s to which the provided tool should be added.
+ /// The to add.
+ ///
+ /// does not derive from , so it cannot be added directly to a list of s.
+ /// Instead, this method wraps the provided in an and adds that to the list.
+ /// The returned by will
+ /// be able to unwrap the when it processes the list of tools and use the provided as-is.
+ ///
+ public static void Add(this IList tools, ResponseTool tool)
+ {
+ _ = Throw.IfNull(tools);
+
+ tools.Add(AsAITool(tool));
+ }
+
+ /// Creates an to represent a raw .
+ /// The tool to wrap as an .
+ /// The wrapped as an .
+ ///
+ ///
+ /// The returned tool is only suitable for use with the returned by
+ /// (or s that delegate
+ /// to such an instance). It is likely to be ignored by any other implementation.
+ ///
+ ///
+ /// When a tool has a corresponding -derived type already defined in Microsoft.Extensions.AI,
+ /// such as , , , or
+ /// , those types should be preferred instead of this method, as they are more portable,
+ /// capable of being respected by any implementation. This method does not attempt to
+ /// map the supplied to any of those types, it simply wraps it as-is:
+ /// the returned by will
+ /// be able to unwrap the when it processes the list of tools.
+ ///
+ ///
+ public static AITool AsAITool(this ResponseTool tool)
+ {
+ _ = Throw.IfNull(tool);
+
+ return new OpenAIResponsesChatClient.ResponseToolAITool(tool);
+ }
}
diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs
index 36bf4ca68b4..e2fbfdf3f84 100644
--- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs
+++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs
@@ -414,6 +414,10 @@ private ResponseCreationOptions ToOpenAIResponseCreationOptions(ChatOptions? opt
{
switch (tool)
{
+ case ResponseToolAITool rtat:
+ result.Tools.Add(rtat.Tool);
+ break;
+
case AIFunctionDeclaration aiFunction:
result.Tools.Add(ToResponseTool(aiFunction, options));
break;
@@ -877,4 +881,11 @@ private static void AddAllMcpFilters(IList toolNames, McpToolFilter filt
filter.ToolNames.Add(toolName);
}
}
+
+ /// Provides an wrapper for a .
+ internal sealed class ResponseToolAITool(ResponseTool tool) : AITool
+ {
+ public ResponseTool Tool => tool;
+ public override string Name => Tool.GetType().Name;
+ }
}
diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs
index b919592da4d..7724ad98360 100644
--- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs
+++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs
@@ -316,7 +316,7 @@ public async Task AsChatResponse_ConvertsOpenAIStreamingChatCompletionUpdates()
updates.Add(update);
}
- ChatResponse response = updates.ToChatResponse();
+ var response = updates.ToChatResponse();
Assert.Equal("id", response.ResponseId);
Assert.Equal(ChatFinishReason.ToolCalls, response.FinishReason);
@@ -1176,6 +1176,31 @@ public void AsOpenAIResponse_WithBothModelIds_PrefersChatResponseModelId()
Assert.Equal("response-model-id", openAIResponse.Model);
}
+ [Fact]
+ public void ListAddResponseTool_AddsToolCorrectly()
+ {
+ Assert.Throws("tools", () => ((IList)null!).Add(ResponseTool.CreateWebSearchTool()));
+ Assert.Throws("tool", () => new List().Add((ResponseTool)null!));
+
+ Assert.Throws("tool", () => ((ResponseTool)null!).AsAITool());
+
+ ChatOptions options;
+
+ options = new()
+ {
+ Tools = new List { ResponseTool.CreateWebSearchTool() },
+ };
+ Assert.Single(options.Tools);
+ Assert.NotNull(options.Tools[0]);
+
+ options = new()
+ {
+ Tools = [ResponseTool.CreateWebSearchTool().AsAITool()],
+ };
+ Assert.Single(options.Tools);
+ Assert.NotNull(options.Tools[0]);
+ }
+
private static async IAsyncEnumerable CreateAsyncEnumerable(IEnumerable source)
{
foreach (var item in source)
diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs
index 9175b6afd57..866e43e172d 100644
--- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs
+++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs
@@ -825,8 +825,10 @@ public async Task MultipleOutputItems_NonStreaming()
Assert.Equal(36, response.Usage.TotalTokenCount);
}
- [Fact]
- public async Task McpToolCall_ApprovalNotRequired_NonStreaming()
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public async Task McpToolCall_ApprovalNotRequired_NonStreaming(bool rawTool)
{
const string Input = """
{
@@ -1031,13 +1033,16 @@ public async Task McpToolCall_ApprovalNotRequired_NonStreaming()
using HttpClient httpClient = new(handler);
using IChatClient client = CreateResponseClient(httpClient, "gpt-4o-mini");
+ AITool mcpTool = rawTool ?
+ ResponseTool.CreateMcpTool("deepwiki", new("https://mcp.deepwiki.com/mcp"), toolCallApprovalPolicy: new McpToolCallApprovalPolicy(GlobalMcpToolCallApprovalPolicy.NeverRequireApproval)).AsAITool() :
+ new HostedMcpServerTool("deepwiki", "https://mcp.deepwiki.com/mcp")
+ {
+ ApprovalMode = HostedMcpServerToolApprovalMode.NeverRequire,
+ };
+
ChatOptions chatOptions = new()
{
- Tools = [new HostedMcpServerTool("deepwiki", "https://mcp.deepwiki.com/mcp")
- {
- ApprovalMode = HostedMcpServerToolApprovalMode.NeverRequire,
- }
- ],
+ Tools = [mcpTool],
};
var response = await client.GetResponseAsync("Tell me the path to the README.md file for Microsoft.Extensions.AI.Abstractions in the dotnet/extensions repository", chatOptions);