From f992c1d9249264578c30d7bea1c1ade50985d9ff Mon Sep 17 00:00:00 2001 From: Aashish Ghimire Date: Sat, 25 Apr 2026 22:44:39 -0700 Subject: [PATCH 1/2] Python: fix runtime response_format on foundry agent endpoint The Foundry agent endpoint rejects per-call `text` configuration when an agent is bound (400 invalid_payload, "Not allowed when agent is specified."). Strip `text` and `text_format` from the request body in `_RawFoundryAgentChatClient._prepare_options` so the user-supplied `response_format` is honored client-side via ChatResponse's lazy structured-value parsing instead of failing the request. Fixes #5467 --- .../foundry/agent_framework_foundry/_agent.py | 10 +++ .../tests/foundry/test_foundry_agent.py | 67 ++++++++++++++++++- 2 files changed, 76 insertions(+), 1 deletion(-) diff --git a/python/packages/foundry/agent_framework_foundry/_agent.py b/python/packages/foundry/agent_framework_foundry/_agent.py index fbf165ab21..74724a508f 100644 --- a/python/packages/foundry/agent_framework_foundry/_agent.py +++ b/python/packages/foundry/agent_framework_foundry/_agent.py @@ -351,6 +351,16 @@ async def _prepare_options( # agent endpoint and tools present. FunctionTools are invoked client-side # by the function invocation layer, not sent to the service. run_options.pop("model", None) + # Strip text/text_format from the request body. The Foundry agent endpoint + # rejects per-call ``text`` configuration when an agent is bound with + # ``400 invalid_payload "Not allowed when agent is specified."``. The + # original ``response_format`` remains in ``options`` and is honored + # client-side via ``ChatResponse``'s lazy structured-value parsing in + # ``_parse_response_from_openai``. The bound agent itself must be + # configured to emit JSON matching the requested schema; otherwise + # ``response.value`` will raise ``pydantic.ValidationError`` on access. + run_options.pop("text", None) + run_options.pop("text_format", None) if not self.allow_preview: run_options.pop("tools", None) run_options.pop("tool_choice", None) diff --git a/python/packages/foundry/tests/foundry/test_foundry_agent.py b/python/packages/foundry/tests/foundry/test_foundry_agent.py index e110e540fe..c2f4f6870b 100644 --- a/python/packages/foundry/tests/foundry/test_foundry_agent.py +++ b/python/packages/foundry/tests/foundry/test_foundry_agent.py @@ -200,7 +200,7 @@ def my_func() -> str: async def test_raw_foundry_agent_chat_client_prepare_options_strips_client_side_fields() -> None: - """Test that _prepare_options strips model and tool-loop fields from run_options.""" + """Test that _prepare_options strips model, tool-loop, and text fields from run_options.""" mock_project = MagicMock() mock_openai = MagicMock() @@ -225,6 +225,8 @@ def my_func() -> str: "tools": [{"type": "function", "function": {"name": "my_func"}}], "tool_choice": "auto", "parallel_tool_calls": True, + "text": {"format": {"type": "json_schema", "name": "x", "schema": {"type": "object"}}}, + "text_format": MagicMock(), }, ): result = await client._prepare_options( @@ -236,9 +238,72 @@ def my_func() -> str: assert "tools" not in result assert "tool_choice" not in result assert "parallel_tool_calls" not in result + assert "text" not in result + assert "text_format" not in result assert result == {} +async def test_raw_foundry_agent_chat_client_prepare_options_strips_text_for_runtime_response_format() -> None: + """Issue #5467: per-call response_format must not be sent to the Foundry agent endpoint. + + The Foundry agent endpoint rejects requests that carry per-call ``text`` + when an agent is bound (``400 invalid_payload "Not allowed when agent is + specified."``). The runtime ``response_format`` must instead be honored + client-side via ``ChatResponse``'s lazy parsing path. + """ + from pydantic import BaseModel + + class OutputStruct(BaseModel): + location: str + conditions: str + + mock_project = MagicMock() + mock_project.get_openai_client.return_value = MagicMock() + + client = RawFoundryAgentChatClient( + project_client=mock_project, + agent_name="test-agent", + ) + + # Simulate what the parent OpenAI Responses client produces for a Pydantic + # ``response_format``: it sets ``text_format`` and would route through + # ``responses.parse()``. + with patch( + "agent_framework_openai._chat_client.RawOpenAIChatClient._prepare_options", + new_callable=AsyncMock, + return_value={"text_format": OutputStruct}, + ): + result = await client._prepare_options( + messages=[Message(role="user", contents="hi")], + options={"response_format": OutputStruct}, + ) + + assert "text" not in result + assert "text_format" not in result + + # And for the dict / json_schema variant the parent populates ``text``. + with patch( + "agent_framework_openai._chat_client.RawOpenAIChatClient._prepare_options", + new_callable=AsyncMock, + return_value={ + "text": { + "format": { + "type": "json_schema", + "name": "WeatherDigest", + "schema": {"type": "object", "properties": {"location": {"type": "string"}}}, + } + } + }, + ): + result = await client._prepare_options( + messages=[Message(role="user", contents="hi")], + options={"response_format": {"type": "json_schema", "json_schema": {"name": "WeatherDigest"}}}, + ) + + assert "text" not in result + assert "text_format" not in result + + async def test_raw_foundry_agent_chat_client_prepare_options_maps_agent_session_id_to_extra_body() -> None: """Test that service_session_id is forwarded as agent_session_id for hosted sessions.""" From 75b3ae7f5872964741677ad80f2c287ea6335cf7 Mon Sep 17 00:00:00 2001 From: Aashish Ghimire Date: Sun, 26 Apr 2026 10:41:11 -0700 Subject: [PATCH 2/2] Address Copilot review: clarify dict/json_schema parsing behavior in comment --- python/packages/foundry/agent_framework_foundry/_agent.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/python/packages/foundry/agent_framework_foundry/_agent.py b/python/packages/foundry/agent_framework_foundry/_agent.py index 74724a508f..a34928fe0c 100644 --- a/python/packages/foundry/agent_framework_foundry/_agent.py +++ b/python/packages/foundry/agent_framework_foundry/_agent.py @@ -357,8 +357,12 @@ async def _prepare_options( # original ``response_format`` remains in ``options`` and is honored # client-side via ``ChatResponse``'s lazy structured-value parsing in # ``_parse_response_from_openai``. The bound agent itself must be - # configured to emit JSON matching the requested schema; otherwise - # ``response.value`` will raise ``pydantic.ValidationError`` on access. + # configured to emit JSON compatible with the requested format. For a + # Pydantic ``response_format``, accessing ``response.value`` may raise + # ``pydantic.ValidationError`` if the output does not validate. For a + # dict / ``json_schema`` ``response_format``, the lazy path only parses + # JSON and may raise ``ValueError`` on invalid JSON; it does not + # validate against the schema. run_options.pop("text", None) run_options.pop("text_format", None) if not self.allow_preview: