From 0659bbfdafbb15a5c4816cdf6a6f9976ee2e886b Mon Sep 17 00:00:00 2001 From: vaibhav-patel Date: Sat, 20 Jun 2026 15:37:30 +0530 Subject: [PATCH] Python: Strip tools from Foundry agent request on the preview path The Foundry service rejects requests that include tool declarations when an agent is specified (HTTP 400 invalid_payload, "Not allowed when agent is specified."). RawFoundryAgentChatClient._prepare_options stripped tools, tool_choice, and parallel_tool_calls only on the non-preview path, so when allow_preview=True (where the agent identity is bound on the OpenAI client via get_openai_client(agent_name=...)) the tool fields were still sent and the call failed. This client always targets a pre-provisioned agent, so it must never send tool declarations. Drop the tool fields unconditionally and log a single warning when the caller supplied tools, noting they are used only for client-side function dispatch. The non-agent FoundryChatClient (model-based) is unaffected. Fixes #5130. --- .../foundry/agent_framework_foundry/_agent.py | 25 ++-- .../tests/foundry/test_foundry_agent.py | 113 ++++++++++++++++-- 2 files changed, 121 insertions(+), 17 deletions(-) diff --git a/python/packages/foundry/agent_framework_foundry/_agent.py b/python/packages/foundry/agent_framework_foundry/_agent.py index 370e034bca..550466ba18 100644 --- a/python/packages/foundry/agent_framework_foundry/_agent.py +++ b/python/packages/foundry/agent_framework_foundry/_agent.py @@ -375,13 +375,24 @@ async def _prepare_options( run_options.pop("isolation_key", None) - # Strip tools from request body - Foundry API rejects requests with both - # agent endpoint and tools present. FunctionTools are invoked client-side - # by the function invocation layer, not sent to the service. - if not self.allow_preview: - run_options.pop("tools", None) - run_options.pop("tool_choice", None) - run_options.pop("parallel_tool_calls", None) + # Strip tool fields from the request body. This client always targets a pre-provisioned + # Foundry agent (agent_name is required), and the service rejects requests that carry both + # an agent reference and tool declarations with HTTP 400 "Not allowed when agent is + # specified." (issue #5130). The agent already owns its tool schema server-side; any + # FunctionTools passed here are used only for client-side dispatch by the function + # invocation layer. This must run on both the non-preview path (agent injected via + # ``agent_reference`` in extra_body) and the preview path (agent injected via the OpenAI + # client kwarg ``agent_name``) — both specify an agent, so neither may send tools. + stripped_tools = run_options.pop("tools", None) + run_options.pop("tool_choice", None) + run_options.pop("parallel_tool_calls", None) + if stripped_tools: + logger.warning( + "Foundry agent '%s' was provided tools, but tool declarations cannot be sent when an " + "agent is specified; they are omitted from the request and used only for client-side " + "function dispatch. Define tool schemas on the Foundry agent definition in the service.", + self.agent_name, + ) return run_options diff --git a/python/packages/foundry/tests/foundry/test_foundry_agent.py b/python/packages/foundry/tests/foundry/test_foundry_agent.py index f14c757d7c..ee3feff729 100644 --- a/python/packages/foundry/tests/foundry/test_foundry_agent.py +++ b/python/packages/foundry/tests/foundry/test_foundry_agent.py @@ -288,7 +288,9 @@ def my_func() -> str: } -async def test_raw_foundry_agent_chat_client_prepare_options_strips_client_side_fields() -> None: +async def test_raw_foundry_agent_chat_client_prepare_options_strips_client_side_fields( + caplog: pytest.LogCaptureFixture, +) -> None: """Test that _prepare_options strips client-side fields for Prompt Agent requests.""" mock_project = MagicMock() @@ -306,15 +308,18 @@ def my_func() -> str: return "ok" - with patch( - "agent_framework_openai._chat_client.RawOpenAIChatClient._prepare_options", - new_callable=AsyncMock, - return_value={ - "model": "gpt-4.1", - "tools": [{"type": "function", "function": {"name": "my_func"}}], - "tool_choice": "auto", - "parallel_tool_calls": True, - }, + with ( + patch( + "agent_framework_openai._chat_client.RawOpenAIChatClient._prepare_options", + new_callable=AsyncMock, + return_value={ + "model": "gpt-4.1", + "tools": [{"type": "function", "function": {"name": "my_func"}}], + "tool_choice": "auto", + "parallel_tool_calls": True, + }, + ), + caplog.at_level("WARNING", logger="agent_framework.foundry"), ): result = await client._prepare_options( messages=[Message(role="user", contents="hi")], @@ -329,6 +334,94 @@ def my_func() -> str: assert result == { "extra_body": {"agent_reference": {"name": "test-agent", "type": "agent_reference"}}, } + # A single warning is emitted because the caller supplied tools that cannot be sent (#5130). + assert sum("cannot be sent when an agent is specified" in record.message for record in caplog.records) == 1 + + +async def test_raw_foundry_agent_chat_client_prepare_options_strips_tools_when_allow_preview( + caplog: pytest.LogCaptureFixture, +) -> None: + """Hosted-agent (allow_preview=True) requests must also strip tool fields. + + Regression test for https://github.com/microsoft/agent-framework/issues/5130. On the preview + path the agent identity is injected via ``project_client.get_openai_client(agent_name=...)``, + so an agent is still specified and the Foundry service rejects any ``tools`` in the body with + HTTP 400 "Not allowed when agent is specified." The earlier fix (#5101) only stripped tools on + the non-preview path, leaving this branch broken. + """ + + mock_project = MagicMock() + mock_project.get_openai_client.return_value = MagicMock() + + client = RawFoundryAgentChatClient( + project_client=mock_project, + agent_name="hosted-agent", + allow_preview=True, + ) + + @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={ + "model": "gpt-4.1", + "tools": [{"type": "function", "function": {"name": "my_func"}}], + "tool_choice": "auto", + "parallel_tool_calls": True, + }, + ), + caplog.at_level("WARNING", logger="agent_framework.foundry"), + ): + result = await client._prepare_options( + messages=[Message(role="user", contents="hi")], + options={"tools": [my_func]}, + ) + + # Tool fields must be gone even though allow_preview=True. + assert "tools" not in result + assert "tool_choice" not in result + assert "parallel_tool_calls" not in result + # Preview path keeps model and does not inject agent_reference (identity is on the OpenAI client). + assert result["model"] == "gpt-4.1" + assert "extra_body" not in result + # Exactly one warning explaining that tools are dropped. + assert sum("cannot be sent when an agent is specified" in record.message for record in caplog.records) == 1 + + +async def test_raw_foundry_agent_chat_client_prepare_options_no_tool_warning_when_no_tools( + caplog: pytest.LogCaptureFixture, +) -> None: + """No warning should be emitted when the caller did not supply any tools.""" + + mock_project = MagicMock() + mock_project.get_openai_client.return_value = MagicMock() + + client = RawFoundryAgentChatClient( + project_client=mock_project, + agent_name="test-agent", + ) + + with ( + patch( + "agent_framework_openai._chat_client.RawOpenAIChatClient._prepare_options", + new_callable=AsyncMock, + return_value={"model": "gpt-4.1"}, + ), + caplog.at_level("WARNING", logger="agent_framework.foundry"), + ): + result = await client._prepare_options( + messages=[Message(role="user", contents="hi")], + options={}, + ) + + assert "tools" not in result + assert not any("cannot be sent when an agent is specified" in record.message for record in caplog.records) async def test_raw_foundry_agent_chat_client_prepare_options_strips_model_for_hosted_session() -> None: