From a442ee115dc9f27bb39cb0cfc7ce5cd8be37e931 Mon Sep 17 00:00:00 2001
From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com>
Date: Mon, 2 Mar 2026 22:12:55 +0000
Subject: [PATCH] .NET: AzureAI Package - Skip tool validation when
UseProvidedChatClientAsIs is true (#4389)
* Skip tool validation when UseProvidedChatClientAsIs is true (#3855)
When GetAIAgentAsync is called with ChatClientAgentOptions.UseProvidedChatClientAsIs = true,
skip requireInvocableTools validation so users can handle function calls manually
via custom ChatClient middleware without needing to provide matching AIFunction tools.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Simplify requireInvocableTools expression per review feedback
UseProvidedChatClientAsIs is a non-nullable bool, so use ! operator
instead of != true for clarity.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Decouple tool matching from validation and add tool preservation test (#3855)
Always match provided AIFunctions to server-side function definitions
regardless of requireInvocableTools flag. Only throw when validation
is required and no match is found. This ensures UseProvidedChatClientAsIs
still preserves user-provided AIFunction tools instead of falling back
to the broken ResponseToolAITool wrapper.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
.../AzureAIProjectChatClientExtensions.cs | 20 +++---
...AzureAIProjectChatClientExtensionsTests.cs | 64 +++++++++++++++++++
2 files changed, 75 insertions(+), 9 deletions(-)
diff --git a/dotnet/src/Microsoft.Agents.AI.AzureAI/AzureAIProjectChatClientExtensions.cs b/dotnet/src/Microsoft.Agents.AI.AzureAI/AzureAIProjectChatClientExtensions.cs
index 027eea1bca..a190f4b154 100644
--- a/dotnet/src/Microsoft.Agents.AI.AzureAI/AzureAIProjectChatClientExtensions.cs
+++ b/dotnet/src/Microsoft.Agents.AI.AzureAI/AzureAIProjectChatClientExtensions.cs
@@ -191,7 +191,7 @@ public static partial class AzureAIProjectChatClientExtensions
AgentRecord agentRecord = await GetAgentRecordByNameAsync(aiProjectClient, options.Name, cancellationToken).ConfigureAwait(false);
var agentVersion = agentRecord.Versions.Latest;
- var agentOptions = CreateChatClientAgentOptions(agentVersion, options, requireInvocableTools: true);
+ var agentOptions = CreateChatClientAgentOptions(agentVersion, options, requireInvocableTools: !options.UseProvidedChatClientAsIs);
return AsChatClientAgent(
aiProjectClient,
@@ -522,21 +522,23 @@ public static partial class AzureAIProjectChatClientExtensions
// Check function tools
foreach (ResponseTool responseTool in definitionTools)
{
- if (requireInvocableTools && responseTool is FunctionTool functionTool)
+ if (responseTool is FunctionTool functionTool)
{
// Check if a tool with the same type and name exists in the provided tools.
- // When invocable tools are required, match only AIFunction.
+ // Always prefer matching AIFunction when available, regardless of requireInvocableTools.
var matchingTool = chatOptions?.Tools?.FirstOrDefault(t => t is AIFunction tf && functionTool.FunctionName == tf.Name);
- if (matchingTool is null)
- {
- (missingTools ??= []).Add($"Function tool: {functionTool.FunctionName}");
- }
- else
+ if (matchingTool is not null)
{
(agentTools ??= []).Add(matchingTool!);
+ continue;
+ }
+
+ if (requireInvocableTools)
+ {
+ (missingTools ??= []).Add($"Function tool: {functionTool.FunctionName}");
+ continue;
}
- continue;
}
(agentTools ??= []).Add(responseTool.AsAITool());
diff --git a/dotnet/tests/Microsoft.Agents.AI.AzureAI.UnitTests/AzureAIProjectChatClientExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.AzureAI.UnitTests/AzureAIProjectChatClientExtensionsTests.cs
index 2f2e276ae9..a7b9c54aac 100644
--- a/dotnet/tests/Microsoft.Agents.AI.AzureAI.UnitTests/AzureAIProjectChatClientExtensionsTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.AzureAI.UnitTests/AzureAIProjectChatClientExtensionsTests.cs
@@ -2375,6 +2375,70 @@ public sealed class AzureAIProjectChatClientExtensionsTests
Assert.NotNull(agent);
}
+ ///
+ /// Verify that GetAIAgentAsync with UseProvidedChatClientAsIs=true skips tool validation
+ /// and does not throw even when server-side function tools exist without matching invocable tools.
+ ///
+ [Fact]
+ public async Task GetAIAgentAsync_WithUseProvidedChatClientAsIs_SkipsToolValidationAsync()
+ {
+ // Arrange
+ PromptAgentDefinition definition = new("test-model") { Instructions = "Test" };
+ definition.Tools.Add(ResponseTool.CreateFunctionTool("required_function", BinaryData.FromString("{}"), strictModeEnabled: false));
+
+ AIProjectClient client = this.CreateTestAgentClient(agentDefinitionResponse: definition);
+ var options = new ChatClientAgentOptions
+ {
+ Name = "test-agent",
+ ChatOptions = new ChatOptions { Instructions = "Test" },
+ UseProvidedChatClientAsIs = true
+ };
+
+ // Act - should not throw even without tools when UseProvidedChatClientAsIs is true
+ ChatClientAgent agent = await client.GetAIAgentAsync(options);
+
+ // Assert
+ Assert.NotNull(agent);
+ }
+
+ ///
+ /// Verify that GetAIAgentAsync with UseProvidedChatClientAsIs=true still matches provided AIFunction tools
+ /// to server-side function definitions, instead of falling back to the ResponseToolAITool wrapper.
+ ///
+ [Fact]
+ public async Task GetAIAgentAsync_WithUseProvidedChatClientAsIs_PreservesProvidedToolsAsync()
+ {
+ // Arrange
+ PromptAgentDefinition definition = new("test-model") { Instructions = "Test" };
+ definition.Tools.Add(ResponseTool.CreateFunctionTool("my_function", BinaryData.FromString("{}"), strictModeEnabled: false));
+
+ AIProjectClient client = this.CreateTestAgentClient(agentDefinitionResponse: definition);
+
+ var providedTool = AIFunctionFactory.Create(() => "test", "my_function", "A test function");
+ var options = new ChatClientAgentOptions
+ {
+ Name = "test-agent",
+ UseProvidedChatClientAsIs = true,
+ ChatOptions = new ChatOptions
+ {
+ Instructions = "Test",
+ Tools = [providedTool]
+ },
+ };
+
+ // Act - UseProvidedChatClientAsIs is true, but provided AIFunctions should still be matched and preserved
+ ChatClientAgent agent = await client.GetAIAgentAsync(options);
+
+ // Assert
+ Assert.NotNull(agent);
+
+ // Verify the provided AIFunction was matched and preserved in ChatOptions.Tools (not replaced by AsAITool wrapper)
+ var chatOptions = agent.GetService();
+ Assert.NotNull(chatOptions);
+ Assert.NotNull(chatOptions!.Tools);
+ Assert.Contains(chatOptions.Tools, t => t is AIFunction af && af.Name == "my_function");
+ }
+
#endregion
#region Empty Version and ID Handling Tests