From 6a6ea3f3931849e8313616eaa8348a8d34300c39 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Mon, 6 Oct 2025 22:48:45 -0400 Subject: [PATCH] Fix duplication with OpenAI Assistants pre-configured tools Assistants lets you configure an agent with function signatures it's implicitly aware of, rather than them needing to be provided per request. That, however, creates complication for callers, as if they provide that function in ChatTools.Options, it leads to the function's signature being sent as part of the request, and the duplication of it with the pre-configured function signature results in an error. It's possible to work around this, by simply not including that function in the request, but it's a natural thing for a developer to do, especially if they want the function to be automatically invoked when the model requests it. You can achieve that by putting the function into the FunctionInvocationChatClient's AdditionalTools, which exists for this kind of purpose, but that's harder to discover. Rather than try something more complicated, this commit simply deduplicates all tools by putting them into a set, deduplicating any duplicates provided. --- .../OpenAIAssistantsChatClient.cs | 36 +++++++++++++++---- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantsChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantsChatClient.cs index 20bc87dd9f3..1de5dd79d4a 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantsChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantsChatClient.cs @@ -306,6 +306,8 @@ internal static FunctionToolDefinition ToOpenAIAssistantsFunctionToolDefinition( if (options.Tools is { Count: > 0 } tools) { + HashSet toolsOverride = new(ToolDefinitionNameEqualityComparer.Instance); + // If the caller has provided any tool overrides, we'll assume they don't want to use the assistant's tools. // But if they haven't, the only way we can provide our tools is via an override, whereas we'd really like to // just add them. To handle that, we'll get all of the assistant's tools and add them to the override list @@ -318,10 +320,7 @@ internal static FunctionToolDefinition ToOpenAIAssistantsFunctionToolDefinition( _assistantTools = assistant.Value.Tools; } - foreach (var tool in _assistantTools) - { - runOptions.ToolsOverride.Add(tool); - } + toolsOverride.UnionWith(_assistantTools); } // The caller can provide tools in the supplied ThreadAndRunOptions. Augment it with any supplied via ChatOptions.Tools. @@ -330,12 +329,12 @@ internal static FunctionToolDefinition ToOpenAIAssistantsFunctionToolDefinition( switch (tool) { case AIFunctionDeclaration aiFunction: - runOptions.ToolsOverride.Add(ToOpenAIAssistantsFunctionToolDefinition(aiFunction, options)); + _ = toolsOverride.Add(ToOpenAIAssistantsFunctionToolDefinition(aiFunction, options)); break; case HostedCodeInterpreterTool codeInterpreterTool: var interpreterToolDef = ToolDefinition.CreateCodeInterpreter(); - runOptions.ToolsOverride.Add(interpreterToolDef); + _ = toolsOverride.Add(interpreterToolDef); if (codeInterpreterTool.Inputs?.Count is > 0) { @@ -358,7 +357,7 @@ internal static FunctionToolDefinition ToOpenAIAssistantsFunctionToolDefinition( break; case HostedFileSearchTool fileSearchTool: - runOptions.ToolsOverride.Add(ToolDefinition.CreateFileSearch(fileSearchTool.MaximumResultCount)); + _ = toolsOverride.Add(ToolDefinition.CreateFileSearch(fileSearchTool.MaximumResultCount)); if (fileSearchTool.Inputs is { Count: > 0 } fileSearchInputs) { foreach (var input in fileSearchInputs) @@ -374,6 +373,11 @@ internal static FunctionToolDefinition ToOpenAIAssistantsFunctionToolDefinition( break; } } + + foreach (var tool in toolsOverride) + { + runOptions.ToolsOverride.Add(tool); + } } // Store the tool mode, if relevant. @@ -543,4 +547,22 @@ void AppendSystemInstructions(string? toAppend) return runId; } + + /// + /// Provides the same behavior as , except + /// for it compares names so that two function tool definitions with the + /// same name compare equally. + /// + private sealed class ToolDefinitionNameEqualityComparer : IEqualityComparer + { + public static ToolDefinitionNameEqualityComparer Instance { get; } = new(); + + public bool Equals(ToolDefinition? x, ToolDefinition? y) => + x is FunctionToolDefinition xFtd && y is FunctionToolDefinition yFtd ? xFtd.FunctionName.Equals(yFtd.FunctionName, StringComparison.Ordinal) : + EqualityComparer.Default.Equals(x, y); + + public int GetHashCode(ToolDefinition obj) => + obj is FunctionToolDefinition ftd ? ftd.FunctionName.GetHashCode(StringComparison.Ordinal) : + EqualityComparer.Default.GetHashCode(obj); + } }