Skip to content

.NET: [Bug]: Hosted agents exposed via the Responses API ignore tools defined in the request body #6416

Closed

Description

Description

An agent mapped to a Responses API endpoint with app.MapOpenAIResponses(agent) never produces a function call for tools defined in the request body, even when the request includes those tools and sets tool_choice: "required".

Minimal reproduction

Register and map an agent:

// Program.cs

builder.AddOpenAIResponses();

IChatClient chatClient = new OpenAIClient(apiKey).GetChatClient(modelId).AsIChatClient();
var agent = builder.AddAIAgent("openai-agent-from-chat-client", instructions: null, chatClient: chatClient);

var app = builder.Build();

app.MapOpenAIResponses(agent);

Call the Responses endpoint with a tool and tool_choice: "required":

curl --location 'https://localhost:7205/openai-agent-from-chat-client/v1/responses' \
--header 'Content-Type: application/json' \
--data '{
    "stream": false,
    "input": [
        { "role": "user", "content": "What'\''s the current weather in Valencia? Use celsius and the get_weather tool" }
    ],
    "tools": [
        {
            "type": "function",
            "name": "get_weather",
            "description": "Retrieves current weather for the given location.",
            "parameters": {
                "type": "object",
                "properties": {
                    "location": { "type": "string", "description": "City and country e.g. Bogotá, Colombia" },
                    "units": { "type": "string", "enum": ["celsius", "fahrenheit"], "description": "Units the temperature will be returned in." }
                },
                "required": ["location", "units"],
                "additionalProperties": false
            },
            "strict": true
        }
    ],
    "tool_choice": "required"
}'

Expected behavior

The agent requests the get_weather function call (the response contains a function call item).

Actual behavior

No function call is ever emitted. The response is a plain text answer and the tools / tool_choice from the request appear to be ignored.
E.g:

"output": [
    {
        "role": "assistant",
        "content": [
            {
                "type": "output_text",
                "text": "Calling weather tool for Valencia, Celsius.\nI'm unable to call the weather tool in this environment right now.\n\nIf you'd like, I can:\n- show you how to check Valencia weather in Celsius quickly, or\n- help interpret a weather report if you paste it here.",
                "annotations": [],
                "logprobs": []
            }
        ],
        "type": "message",
        "status": "completed",
        "id": "msg_U3lQzbV6GbpqKgnwoCZEst4H9qwPv6PQEZdZBn8e19GoRHM3"
    }
],

Environment

  • OS: Windows 11 (x64)
  • .NET 10

Reproduce using the following versions in 2 separate tests:

Package Test A Test B
Microsoft.Agents.AI 1.9.0 1.6.1
Microsoft.Agents.AI.OpenAI 1.9.0 1.6.1
Microsoft.Agents.AI.Hosting.OpenAI 1.9.0-alpha.260603.1 1.7.0-alpha.260526.1

Code Sample

Error Messages / Stack Traces

Package Versions

Microsoft.Agents.AI: 1.9.0, Microsoft.Agents.AI.OpenAI: 1.9.0, Microsoft.Agents.AI.Hosting.OpenAI: 1.9.0-alpha.260603.1

.NET Version

.NET 10

Additional Context

The same request body sent directly to OpenAI (https://api.openai.com/v1/responses) does return a function_call:

"output": [
    {
        "id": "fc_0a6aa1d6b09e8366006a28014c4e6c8191bd92c7a54b751d16",
        "type": "function_call",
        "status": "completed",
        "arguments": "{\"location\":\"Valencia, Spain\",\"units\":\"celsius\"}",
        "call_id": "call_sPjLPnyYIgwhyl6fXZRROIGH",
        "name": "get_weather"
    }
]

Also, I tried this using the same agent mapped to a Chat Completions endpoint with app.MapOpenAIChatCompletions(agent) and it does use tools defined in the request body (using an equivalent request adapted for chat completions).

I also observed that the AIAgentResponseExecutor (

) doesn't map the tools received in the requests to the ChatOptions object that's later used to run the agent. Not sure if that's the only thing causing this issue, but I'd be happy to try and send a PR!

Reference repo with the full reproduction: https://github.com/pawap90/AgentFramework.Hosting.OpenAI.Tests

Activity

  1. added
    bugUsage: [Issues], Target: all issues (Legacy, prefer issue type: bug)
    .NETUsage: [Issues, PRs], Target: .Net
    on Jun 9, 2026
  2. added
    triageUsage: [Issues], Target: All issues that still need to be triaged
    on Jun 9, 2026
  3. added
    needs-maintainer-triageUsage: [Issues], Target: all issues that the automated triage flow failed to process
    on Jun 9, 2026
  4. removed
    triageUsage: [Issues], Target: All issues that still need to be triaged
    on Jun 9, 2026
  5. XiongHaoTrigger commented on Jun 10, 2026

    @XiongHaoTrigger
    Contributor

    Paula Santamaría (@pawap90)
    There is no mapping of "Tools" and "ToolChoice" in the file dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/AIAgentResponseExecutor.cs and dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/HostedAgentResponseExecutor.cs

    I also attempted to fix these two areas.

    In dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/AIAgentResponseExecutor.cs

    public async IAsyncEnumerable<StreamingResponseEvent> ExecuteAsync(
            AgentInvocationContext context,
            CreateResponse request,
            IReadOnlyList<ChatMessage>? conversationHistory = null,
            [EnumeratorCancellation] CancellationToken cancellationToken = default)
        {
            // Create options with properties from the request
            // var chatOptions = new ChatOptions
            // {
                // Note: We intentionally do NOT set ConversationId on ChatOptions here.
                // The conversation ID from the client request is used by the hosting layer
                // to manage conversation storage, but should not be forwarded to the underlying
                // IChatClient as it has its own concept of conversations (or none at all).
                // ---
                // ConversationId = request.Conversation?.Id,
    
               // Temperature = (float?)request.Temperature,
               // TopP = (float?)request.TopP,
               // MaxOutputTokens = request.MaxOutputTokens,
               // Instructions = request.Instructions,
               // ModelId = request.Model,
            // };
            // var options = new ChatClientAgentRunOptions(chatOptions);
            var options = request.ToChatClientAgentRunOptions();
            ...
        }

    In dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/HostedAgentResponseExecutor.cs

    public async IAsyncEnumerable<StreamingResponseEvent> ExecuteAsync(
            AgentInvocationContext context,
            CreateResponse request,
            IReadOnlyList<ChatMessage>? conversationHistory = null,
            [EnumeratorCancellation] CancellationToken cancellationToken = default)
        {
            string agentName = GetAgentName(request)!;
            AIAgent agent = this._serviceProvider.GetRequiredKeyedService<AIAgent>(agentName);
            // Create options with properties from the request
            // var chatOptions = new ChatOptions
            // {
                // Note: We intentionally do NOT set ConversationId on ChatOptions here.
                // The conversation ID from the client request is used by the hosting layer
                // to manage conversation storage, but should not be forwarded to the underlying
                // IChatClient as it has its own concept of conversations (or none at all).
                // ---
                // ConversationId = request.Conversation?.Id,
    
               // Temperature = (float?)request.Temperature,
               // TopP = (float?)request.TopP,
               // MaxOutputTokens = request.MaxOutputTokens,
               // Instructions = request.Instructions,
               // ModelId = request.Model,
            // };
            // var options = new ChatClientAgentRunOptions(chatOptions);
            var options = request.ToChatClientAgentRunOptions();
            ...
        }

    The new extension method

    internal static class CreateResponseChatClientAgentRunOptionsExtensions
    {
        private static readonly JsonElement s_emptyJson = JsonElement.Parse("{}");
    
        public static ChatClientAgentRunOptions ToChatClientAgentRunOptions(this CreateResponse request)
        {
            ChatOptions chatOptions = new()
            {
                // Note: We intentionally do NOT set ConversationId on ChatOptions here.
                // The conversation ID from the client request is used by the hosting layer
                // to manage conversation storage, but should not be forwarded to the underlying
                // IChatClient as it has its own concept of conversations (or none at all).
                // ---
                // ConversationId = request.Conversation?.Id,
                Temperature = (float?)request.Temperature,
                TopP = (float?)request.TopP,
                MaxOutputTokens = request.MaxOutputTokens,
                Instructions = request.Instructions,
                ModelId = request.Model,
                AllowMultipleToolCalls = request.ParallelToolCalls,
            };
    
            if (request.ToolChoice is { } toolChoice)
            {
                chatOptions.ToolMode = toolChoice.ToChatToolMode();
            }
    
            if (request.Tools is { Count: > 0 })
            {
                List<AITool> tools = [];
                foreach (JsonElement tool in request.Tools)
                {
                    if (tool.ToAITool() is { } aiTool)
                    {
                        tools.Add(aiTool);
                    }
                }
    
                if (tools.Count > 0)
                {
                    chatOptions.Tools = tools;
                }
            }
    
            return new ChatClientAgentRunOptions(chatOptions);
        }
    
        private static AITool? ToAITool(this JsonElement tool)
        {
            if (tool.ValueKind != JsonValueKind.Object ||
                !tool.TryGetProperty("type", out JsonElement typeProperty) ||
                typeProperty.GetString() is not { } type)
            {
                return null;
            }
    
            if (string.Equals(type, "function", StringComparison.Ordinal))
            {
                return ToAIFunction(tool);
            }
    
            if (string.Equals(type, "custom", StringComparison.Ordinal) &&
                TryGetToolName(tool, out string? name))
            {
                return new CustomAITool(name, TryGetStringProperty(tool, "description"), additionalProperties: null);
            }
    
            return null;
        }
    
        private static AIFunctionDeclaration? ToAIFunction(JsonElement tool)
        {
            JsonElement function = tool.TryGetProperty("function", out JsonElement functionProperty) &&
                functionProperty.ValueKind == JsonValueKind.Object
                ? functionProperty
                : tool;
    
            if (!TryGetToolName(function, out string? name))
            {
                return null;
            }
    
            JsonElement parameters = function.TryGetProperty("parameters", out JsonElement parametersProperty)
                ? parametersProperty
                : s_emptyJson;
    
            return AIFunctionFactory.CreateDeclaration(
                name,
                TryGetStringProperty(function, "description"),
                parameters);
        }
    
        private static ChatToolMode? ToChatToolMode(this JsonElement toolChoice)
        {
            if (toolChoice.ValueKind == JsonValueKind.String)
            {
                return toolChoice.GetString() switch
                {
                    "auto" => ChatToolMode.Auto,
                    "none" => ChatToolMode.None,
                    "required" => ChatToolMode.RequireAny,
                    _ => null
                };
            }
    
            if (toolChoice.ValueKind != JsonValueKind.Object ||
                !toolChoice.TryGetProperty("type", out JsonElement typeProperty) ||
                typeProperty.GetString() is not { } type)
            {
                return null;
            }
    
            if (string.Equals(type, "allowed_tools", StringComparison.Ordinal) &&
                toolChoice.TryGetProperty("allowed_tools", out JsonElement allowedTools) &&
                allowedTools.TryGetProperty("mode", out JsonElement modeProperty))
            {
                return modeProperty.GetString() switch
                {
                    "auto" => ChatToolMode.Auto,
                    "required" => ChatToolMode.RequireAny,
                    _ => null
                };
            }
    
            return TryGetToolName(toolChoice, out string? name) ? ChatToolMode.RequireSpecific(name) : null;
        }
    
        private static bool TryGetToolName(JsonElement element, out string name)
        {
            if (element.TryGetProperty("name", out JsonElement nameProperty) &&
                nameProperty.ValueKind == JsonValueKind.String &&
                nameProperty.GetString() is { } directName)
            {
                name = directName;
                return true;
            }
    
            if (element.TryGetProperty("function", out JsonElement functionProperty) &&
                functionProperty.ValueKind == JsonValueKind.Object &&
                functionProperty.TryGetProperty("name", out JsonElement functionNameProperty) &&
                functionNameProperty.ValueKind == JsonValueKind.String &&
                functionNameProperty.GetString() is { } functionName)
            {
                name = functionName;
                return true;
            }
    
            if (element.TryGetProperty("custom", out JsonElement customProperty) &&
                customProperty.ValueKind == JsonValueKind.Object &&
                customProperty.TryGetProperty("name", out JsonElement customNameProperty) &&
                customNameProperty.ValueKind == JsonValueKind.String &&
                customNameProperty.GetString() is { } customName)
            {
                name = customName;
                return true;
            }
    
            name = string.Empty;
            return false;
        }
    
        private static string? TryGetStringProperty(JsonElement element, string propertyName)
            => element.TryGetProperty(propertyName, out JsonElement property) &&
                property.ValueKind == JsonValueKind.String
                ? property.GetString()
                : null;
    }
  6. alliscode commented on Jun 15, 2026

    @alliscode
    Member

    Hi Paula Santamaría (@pawap90), Hao Xiong (@XiongHaoTrigger) thank you for filing this.
    Can you help me understand the scenario that you have where you want to enable client-side tools?
    In general, a deployed Hosted Agent is a black box to clients, they don't know what model or even what provider the Hosted Agent is using internally. I can think of many cases in which passing through things like model-id, temperature. tool-definitions etc. from the client would not be desired or even functional. It would be helpful to understand the end-to-end scenario you are trying to solve for.

  7. XiongHaoTrigger commented on Jun 16, 2026

    @XiongHaoTrigger
    Contributor

    Ben Thomas (@alliscode) Currently, my codebase does not actually have this business requirement. I just happened to come across this issue and checked the source code, and it seems that this value is not being passed through at the moment.

    However, I am currently building an agent-centric testing framework, where the agent will invoke tools that control external hardware devices. In this scenario, I believe it is necessary for the client to be able to restrict tool invocation through configuration, such as tool_choice.

    That said, I have not implemented this part yet. Alternatively, to follow the current framework design principles, I may need to keep tool invocation restrictions inside the hosting implementation.

  8. pawap90 commented on Jun 16, 2026

    @pawap90
    Author

    Ben Thomas (@alliscode)

    Thanks for following up! Happy to give you the full picture.

    Our platform allows users to configure their own AI agents through a low-code approach, and invoke them via our API. We support a set of server-side tools for the Agents. However, that set is necessarily limited and isolated from the client systems, so clients often ask to plug in client-side tools so their agents can interact with their own systems (website, database, internal APIs).

    The hosted agents in Agent Framework were one of our top reasons for migrating over from Semantic Kernel. We're working on wrapping our agents into MAF AIAgents so we can expose them as Hosted Agents through OpenAI-compatible endpoints and AG-UI.

    For clients who aren't looking to adopt AG-UI, this feature would be a great alternative. It lets them supply tools that are already integrated with their own systems, while the OpenAI-compatible endpoints are well known and easy to integrate into what they already have.

    It seems like AIAgents (both MAF's hosted agents and ours) could let the client choose to enable client-side tools, so they stay in control.

  9. removed
    bugUsage: [Issues], Target: all issues (Legacy, prefer issue type: bug)
    on Jun 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

.NETUsage: [Issues, PRs], Target: .Netneeds-maintainer-triageUsage: [Issues], Target: all issues that the automated triage flow failed to process

Type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions