From 18912921f6448cd7e034d8963bbfdfce98f301cb Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Sat, 4 Apr 2026 22:41:20 -0700 Subject: [PATCH] Python: Strip tools from FoundryAgent request when agent_reference is present _prepare_options() now removes tools, tool_choice, and parallel_tool_calls from run_options after injecting agent_reference. The Foundry API rejects requests containing both fields. FunctionTools are still invoked client-side by the function invocation layer. Fixes #5087 Co-Authored-By: Claude Opus 4.6 (1M context) --- .../foundry/agent_framework_foundry/_agent.py | 7 ++++ .../tests/foundry/test_foundry_agent.py | 39 +++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/python/packages/foundry/agent_framework_foundry/_agent.py b/python/packages/foundry/agent_framework_foundry/_agent.py index a499abf6f4..bf5d936d9d 100644 --- a/python/packages/foundry/agent_framework_foundry/_agent.py +++ b/python/packages/foundry/agent_framework_foundry/_agent.py @@ -293,6 +293,13 @@ async def _prepare_options( # Inject agent reference run_options["extra_body"] = {"agent_reference": self._get_agent_reference()} + # Strip tools from request body - Foundry API rejects requests with both + # agent_reference and tools present. FunctionTools are invoked client-side + # by the function invocation layer, not sent to the service. + run_options.pop("tools", None) + run_options.pop("tool_choice", None) + run_options.pop("parallel_tool_calls", None) + return run_options @override diff --git a/python/packages/foundry/tests/foundry/test_foundry_agent.py b/python/packages/foundry/tests/foundry/test_foundry_agent.py index 09a31f941b..829af6ab87 100644 --- a/python/packages/foundry/tests/foundry/test_foundry_agent.py +++ b/python/packages/foundry/tests/foundry/test_foundry_agent.py @@ -200,6 +200,45 @@ def my_func() -> str: assert result["extra_body"]["agent_reference"]["name"] == "test-agent" +async def test_raw_foundry_agent_chat_client_prepare_options_strips_tools() -> None: + """Test that _prepare_options strips tools, tool_choice, and parallel_tool_calls from run_options.""" + + mock_project = MagicMock() + mock_openai = MagicMock() + mock_project.get_openai_client.return_value = mock_openai + + client = RawFoundryAgentChatClient( + project_client=mock_project, + agent_name="test-agent", + ) + + @tool(approval_mode="never_require") + def my_func() -> str: + """A test function.""" + + return "ok" + + with patch( + "agent_framework_openai._chat_client.RawOpenAIChatClient._prepare_options", + new_callable=AsyncMock, + return_value={ + "tools": [{"type": "function", "function": {"name": "my_func"}}], + "tool_choice": "auto", + "parallel_tool_calls": True, + }, + ): + result = await client._prepare_options( + messages=[Message(role="user", contents="hi")], + options={"tools": [my_func]}, + ) + + assert "tools" not in result + assert "tool_choice" not in result + assert "parallel_tool_calls" not in result + assert "extra_body" in result + assert result["extra_body"]["agent_reference"]["name"] == "test-agent" + + def test_raw_foundry_agent_chat_client_check_model_presence_is_noop() -> None: """Test that _check_model_presence does nothing (model is on service)."""