Add HostedToolSearchTool and SearchableAIFunctionDeclaration for tool search / deferred loading support#7377
Add HostedToolSearchTool and SearchableAIFunctionDeclaration for tool search / deferred loading support#7377
Conversation
|
@copilot, I'm wondering if a different design would be more beneficial. What would you think about isolating it to just the HostedToolSearchTool, which would have two collections of either AITool instances or string names, one for Enable and one for Disable. By default, both of these collections would be null, in which case it would treat every tool as Enable and mark every tool as defer-loading. If Disable was non-null but Enable was null, then it would treat every tool as Enable except for those listed in Disable. Conversely if Enable was non-null and Disable was null, it would only treat those tools in Enable as defer-loading, and if both were non-null, it would treat everything in Enable as defer-loading unless it was also listed in Disable. Thoughts? Is the dominant case for tool search with OpenAI and Anthropic one where most tools are in the same boat and only zero to a small number are special-cased the other way? Or is the right way to model this what we currently have? |
Yes, I think the dominant case is indeed "most/all tools are searchable" — the whole point of tool search is to scale to many tools, so users will almost always want blanket defer-loading with at most a few exceptions. Your proposed design is better for that common case. Today, with 50 tools you'd wrap all 50 in A few thoughts on the details: Enable/Disable semantics: The logic makes sense and is consistent with how String names vs AITool references: String names (like Namespace: The current design has per-tool What we'd lose: The Overall I think this is a better design. Want me to implement it? |
@copilot, yes, please |
Implemented in f295d10. Changes:
|
|
@PederHP any thoughts? |
There was a problem hiding this comment.
Pull request overview
Adds a new experimental hosted tool marker (HostedToolSearchTool) to enable “tool_search” and implements deferred loading (defer_loading) support in the OpenAI Responses provider by patching function tools based on DeferredTools / NonDeferredTools configuration.
Changes:
- Introduces
HostedToolSearchTool(experimental) and a newDiagnosticIds.Experiments.AIToolSearchconstant. - Updates OpenAI Responses tool conversion to emit
tool_searchand optionally patch function tools withdefer_loading: true. - Adds/extends unit tests covering tool_search conversion, caching, and deferred-loading selection logic.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs | Adds OpenAI conversion tests for tool_search and defer_loading behavior. |
| test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Tools/HostedToolSearchToolTests.cs | Adds tests for HostedToolSearchTool construction and list properties. |
| src/Shared/DiagnosticIds/DiagnosticIds.cs | Adds DiagnosticIds.Experiments.AIToolSearch. |
| src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs | Adds tool_search mapping and deferred-loading patching for function tools. |
| src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIResponsesExtensions.cs | Updates AsOpenAIResponseTool to accept ChatOptions? for deferred-loading context. |
| src/Libraries/Microsoft.Extensions.AI.OpenAI/Microsoft.Extensions.AI.OpenAI.json | Updates API baseline for the modified extension method signature. |
| src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedToolSearchTool.cs | Adds the new experimental HostedToolSearchTool abstraction with configuration lists. |
…gTools from HostedMcpServerTool, add interaction tests - Remove Namespace property and namespaceName parameter from SearchableAIFunctionDeclaration - Remove DeferLoadingTools property from HostedMcpServerTool - Update OpenAIResponsesChatClient to drop namespace patching and MCP defer_loading patching - Update RemoteMCP_DeferLoadingTools integration test to use AsOpenAIResponseTool() + Patch.Set + AsAITool() - Add tool_search_call/tool_search_output assertions to integration test - Add SearchableAIFunctionDeclaration + ApprovalRequiredAIFunction interaction tests - Add FunctionInvokingChatClient test for approval detection through searchable wrapper Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Remove [Experimental(AIToolSearch)] attribute from DelegatingAIFunctionDeclaration, SearchableAIFunctionDeclaration, and HostedToolSearchTool. Remove the AIToolSearch diagnostic constant from DiagnosticIds. Add API baseline entries for all three types in Microsoft.Extensions.AI.Abstractions.json. Clean up unused usings. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add tests verifying that the OpenAI API returns HTTP 400 when HostedToolSearchTool is used without any deferred tools: - UseToolSearch_OnlyToolSearchNoFunctions - UseToolSearch_WithNonDeferredFunctionsOnly Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…arch-support # Conflicts: # src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json
…e grouping in OpenAIResponsesChatClient - Add Namespace property and namespaceName parameter to SearchableAIFunctionDeclaration - Add namespaceName parameter to CreateToolSet for bulk namespace assignment - Add namespace grouping logic in OpenAIResponsesChatClient tools loop - Add ToNamespaceResponseTool helper using ModelReaderWriter for AOT-safe JSON - Add namespace unit tests in SearchableAIFunctionDeclarationTests - Add namespace VerbatimHttpHandler tests in OpenAIResponseClientTests - Add UseToolSearch_WithNamespace integration test with tool_search assertions - Add tool_search_call/tool_search_output assertions to existing integration test - Update API baseline Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
| public class HostedToolSearchTool : AITool | ||
| { | ||
| /// <summary>Any additional properties associated with the tool.</summary> |
There was a problem hiding this comment.
HostedToolSearchTool is intended to be experimental per the PR description, but the type currently has no [Experimental(...)] attribute. This makes the API appear Stable (see the generated Abstractions.json) and could unintentionally lock the API surface. Add [Experimental(DiagnosticIds.Experiments.AIToolSearch, UrlFormat = DiagnosticIds.UrlFormat)] and ensure the corresponding DiagnosticIds constant exists.
| public sealed class SearchableAIFunctionDeclaration : DelegatingAIFunctionDeclaration | ||
| { | ||
| /// <summary> |
There was a problem hiding this comment.
SearchableAIFunctionDeclaration is described as an experimental API, but it is currently missing an [Experimental(...)] attribute. Without it, the API is treated as Stable (and the generated Abstractions.json reflects that), which is hard to undo later. Mark it [Experimental(DiagnosticIds.Experiments.AIToolSearch, UrlFormat = DiagnosticIds.UrlFormat)] and ensure DiagnosticIds.Experiments.AIToolSearch is defined.
There was a problem hiding this comment.
@copilot revise the PR description. This shouldn't be experimental.
There was a problem hiding this comment.
Updated the PR description in 89d8df4 — removed all references to [Experimental] annotations. The types are stable.
There was a problem hiding this comment.
The PR description was updated previously to state all types are stable (not experimental). The current description reflects this — no [Experimental] attributes on the new types.
| if (tool.GetService<SearchableAIFunctionDeclaration>() is not null) | ||
| { | ||
| functionTool.Patch.Set("$.defer_loading"u8, "true"u8); | ||
| } |
There was a problem hiding this comment.
FWIW, we can't do anything with SearchableAIFunctionDeclaration.Namespace here, meaning is useless for AsOpenAIResponseTool.
There was a problem hiding this comment.
Removed Namespace entirely from SearchableAIFunctionDeclaration in 89d8df4 — it was indeed unusable in the public AsOpenAIResponseTool API and the grouping approach was wrong for OpenAI's array-based namespace model.
There was a problem hiding this comment.
Reverted in 0579b05 — Namespace property, ToNamespaceResponseTool, namespace grouping logic, and all associated tests are restored.
…rouping from OpenAI provider Agent-Logs-Url: https://github.com/dotnet/extensions/sessions/a0e8d299-9f73-4db3-be63-674f590aa717 Co-authored-by: jozkee <16040868+jozkee@users.noreply.github.com>
|
I'm not seeing the immediate benefit of having the Declaration outside of the Tool, seems a bit less obvious from the consumption perspective, I would expect that by definition when you use a What am I missing? // Wrap specific functions as searchable (deferred loading)
var options = new ChatOptions
{
Tools =
[
new HostedToolSearchTool([getWeather, getForecast]),
importantTool, // not wrapped — sent without defer_loading
]
};
// No CreateToolSet needed as they are all wrapped
var tools = [new HostedToolSearchTool([getWeather, getForecast]);
var response = await chatClient.GetResponseAsync(messages, new() { Tools = tools }); |
|
That was at one point the approach taken in this PR #7377 (comment). Having |
…espace grouping from OpenAI provider" This reverts commit 89d8df4. Co-authored-by: jozkee <16040868+jozkee@users.noreply.github.com>
|
Also, having a separate way to mark a function with https://developers.openai.com/api/docs/guides/tools-tool-search#client-executed-tool-search |
|
@copilot fix CI build errors |
… and restore @namespace param name Agent-Logs-Url: https://github.com/dotnet/extensions/sessions/1783bbad-9689-4b08-bfcc-79988248c2ff Co-authored-by: jozkee <16040868+jozkee@users.noreply.github.com>
Fixed in 3a28bd0 — the build error was caused by |
And that would also IMO make more sense, so you provide per SearchTool what is the scope (namespace + tools), if you have multiple scopes you provide multiple HostedSearchTools for each, which make sense for me, this would work well, if you also have a customized search tool running in the client side, limited to a specific set of tools. This approach would avoid incurring in scenarios where you advertise tools but forgot to provide the sibling |
|
How would How would you enable |
|
@rogerbarreto ptal at #7471 |
Implements tool search and deferred loading support (issue #7371): a
HostedToolSearchToolmarker andSearchableAIFunctionDeclarationdecorator that signals per-tool deferred loading to supporting providers. OpenAI Responses API support included; Anthropic follows separately.New abstractions (
Microsoft.Extensions.AI.Abstractions)HostedToolSearchTool— markerAITool(same pattern asHostedWebSearchTool/HostedCodeInterpreterTool); maps to thetool_searchhosted tool.SearchableAIFunctionDeclaration— decorator wrappingAIFunctionDeclarationthat signals deferred loading to supporting providers. Includes an optionalNamespaceproperty for organizing tools into namespaces. Detected viatool.GetService<SearchableAIFunctionDeclaration>().DelegatingAIFunctionDeclaration— now public (was internal); base class forSearchableAIFunctionDeclarationthat passes through calls to an innerAIFunctionDeclaration.HostedMcpServerTool.DeferLoadingTools—boolproperty signaling deferred loading for MCP server tools when tool search is enabled.All new types are stable (not marked
[Experimental]).OpenAI provider (
Microsoft.Extensions.AI.OpenAI)HostedToolSearchTool→ deserializedResponseToolfrom{"type":"tool_search"}via AOT-safeModelReaderWriter.ReadwithOpenAIContext.Default(created per-call, not cached). Includes a comment referencing openai/openai-dotnet#1053 as a temporary workaround.SearchableAIFunctionDeclarationviatool.GetService<SearchableAIFunctionDeclaration>()and patchesdefer_loading: trueonto matchingFunctionToolinstancesNamespaceare grouped into namespaceResponseToolarraysAIFunctionDeclarationtools are sent without deferred loadingHostedMcpServerToolwithDeferLoadingTools = truegetsdefer_loadingpatched viaPatchTests
OpenAIResponseClientTests.cs):VerbatimHttpHandler-based tests validating the exact JSON request body sent for various configurations: tool_search only, searchable functions with deferred loading, mixed searchable and plain functions, and mixed with other hosted tools (web search)OpenAIConversionTests.cs): Tests forAsOpenAIResponseToolextension covering tool_search conversion, defer_loading patching viaSearchableAIFunctionDeclaration, and plain function without deferred loadingHostedToolSearchToolTests.cs,SearchableAIFunctionDeclarationTests.cs): Tests for tool properties, delegation,GetServicediscovery, andCreateToolSetOpenAIResponseClientIntegrationTests.cs):UseToolSearch_WithDeferredFunctionstest exercisingHostedToolSearchToolwithSearchableAIFunctionDeclaration-wrapped functions against the OpenAI APIUsage
Original prompt
Problem
Implement tool search and deferred loading support as described in #7371. Both OpenAI and Anthropic now support tool search, where tool definitions can be sent with deferred loading (only name/description sent upfront, full schema deferred) and a special
tool_searchhosted tool is included that the model can invoke to search for and load full tool definitions on demand.Design
Follow Option A from the issue discussion — a
HostedToolSearchToolmarker tool + aSearchableAIFunctionDeclarationdecorator, consistent with existing patterns (HostedWebSearchTool,ApprovalRequiredAIFunction, etc.).Requirements
1. New types in
Microsoft.Extensions.AI.AbstractionsHostedToolSearchTool(insrc/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedToolSearchTool.cs)AIToolsubclass, following the exact same pattern asHostedWebSearchToolandHostedCodeInterpreterTool.Namereturns"tool_search".IReadOnlyDictionary<string, object?>? additionalProperties).[Experimental(DiagnosticIds.Experiments.AIToolSearch, UrlFormat = DiagnosticIds.UrlFormat)].SearchableAIFunctionDeclaration(insrc/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/SearchableAIFunctionDeclaration.cs)DelegatingAIFunctionDeclaration(which is currentlyinternal). Important:DelegatingAIFunctionDeclarationis the declaration-only delegating base (notDelegatingAIFunctionwhich requiresAIFunction). This is becauseSearchableAIFunctionDeclarationshould work withAIFunctionDeclarationinstances that may not haveInvokeAsync.AIFunctionDeclaration innerFunctionandstring? namespaceName = null.Namespaceproperty (string?) for grouping related tools.sealed.[Experimental(DiagnosticIds.Experiments.AIToolSearch, UrlFormat = DiagnosticIds.UrlFormat)].public static IList<AITool> CreateToolSet(IEnumerable<AIFunctionDeclaration> functions, string? namespaceName = null, IReadOnlyDictionary<string, object?>? toolSearchProperties = null)that wraps all functions asSearchableAIFunctionDeclarationand prepends aHostedToolSearchTool, returning a complete tool list ready forChatOptions.Tools.2. DiagnosticIds update
In
src/Shared/DiagnosticIds/DiagnosticIds.cs, add a new constant in theExperimentsclass:Place it alongside the other AI experiment constants (near
AIWebSearch,AICodeInterpreter, etc.).3. OpenAI provider implementation
In
src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs:In
ToResponseTool(AITool tool, ChatOptions? options = null)method:Add handling for
HostedToolSearchTool— this maps to the OpenAItool_searchresponse tool. Since the underlying OpenAI .NET SDK likely doesn't have aToolSearchToolclass yet, you need to manually construct aResponseToolfrom JSON. Cache the deserializedResponseToolinstance in a static field so it's only created once. Use theModelReaderWriterpattern or direct JSON deserialization to create aResponseToolfrom the JSON{"type": "tool_search"}. Pattern:Add a
private static ResponseTool? s_toolSearchResponseTool;field to cache it.For
SearchableAIFunctionDeclaration: When anAIFunctionDeclarationis detected as having aSearchableAIFunctionDeclarationviaGetService<SearchableAIFunctionDeclaration>(), the generatedFunctionToolshould havedefer_loadingset totrueand optionally include thenamespacemetadata. Since the OpenAI SDK'sFunctionToolclass may not have these properties yet, use thePatchproperty to set them on the JSON. The check should happen in the existingcase AIFunctionDeclaration aiFunction:branch — after callingToResponseTool(aiFunction, options), check if the originaltool(oraiFunction) hasGetService<SearchableAIFunctionDeclaration>()and if so, patch the resultingFunctionToolwithdefer_loadingandnamespace. This is done in theToResponseTool(AITool, ChatOptions?)method so it doesn't infect the generalToResponseTool(AIFunctionDeclaration, ChatOptions?)helper. Specifically, thecase AIFunctionDeclaration aiFunction:case should become: