Skip to content

Commit f992c1d

Browse files
committed
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
1 parent 56fb634 commit f992c1d

2 files changed

Lines changed: 76 additions & 1 deletion

File tree

python/packages/foundry/agent_framework_foundry/_agent.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,16 @@ async def _prepare_options(
351351
# agent endpoint and tools present. FunctionTools are invoked client-side
352352
# by the function invocation layer, not sent to the service.
353353
run_options.pop("model", None)
354+
# Strip text/text_format from the request body. The Foundry agent endpoint
355+
# rejects per-call ``text`` configuration when an agent is bound with
356+
# ``400 invalid_payload "Not allowed when agent is specified."``. The
357+
# original ``response_format`` remains in ``options`` and is honored
358+
# client-side via ``ChatResponse``'s lazy structured-value parsing in
359+
# ``_parse_response_from_openai``. The bound agent itself must be
360+
# configured to emit JSON matching the requested schema; otherwise
361+
# ``response.value`` will raise ``pydantic.ValidationError`` on access.
362+
run_options.pop("text", None)
363+
run_options.pop("text_format", None)
354364
if not self.allow_preview:
355365
run_options.pop("tools", None)
356366
run_options.pop("tool_choice", None)

python/packages/foundry/tests/foundry/test_foundry_agent.py

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ def my_func() -> str:
200200

201201

202202
async def test_raw_foundry_agent_chat_client_prepare_options_strips_client_side_fields() -> None:
203-
"""Test that _prepare_options strips model and tool-loop fields from run_options."""
203+
"""Test that _prepare_options strips model, tool-loop, and text fields from run_options."""
204204

205205
mock_project = MagicMock()
206206
mock_openai = MagicMock()
@@ -225,6 +225,8 @@ def my_func() -> str:
225225
"tools": [{"type": "function", "function": {"name": "my_func"}}],
226226
"tool_choice": "auto",
227227
"parallel_tool_calls": True,
228+
"text": {"format": {"type": "json_schema", "name": "x", "schema": {"type": "object"}}},
229+
"text_format": MagicMock(),
228230
},
229231
):
230232
result = await client._prepare_options(
@@ -236,9 +238,72 @@ def my_func() -> str:
236238
assert "tools" not in result
237239
assert "tool_choice" not in result
238240
assert "parallel_tool_calls" not in result
241+
assert "text" not in result
242+
assert "text_format" not in result
239243
assert result == {}
240244

241245

246+
async def test_raw_foundry_agent_chat_client_prepare_options_strips_text_for_runtime_response_format() -> None:
247+
"""Issue #5467: per-call response_format must not be sent to the Foundry agent endpoint.
248+
249+
The Foundry agent endpoint rejects requests that carry per-call ``text``
250+
when an agent is bound (``400 invalid_payload "Not allowed when agent is
251+
specified."``). The runtime ``response_format`` must instead be honored
252+
client-side via ``ChatResponse``'s lazy parsing path.
253+
"""
254+
from pydantic import BaseModel
255+
256+
class OutputStruct(BaseModel):
257+
location: str
258+
conditions: str
259+
260+
mock_project = MagicMock()
261+
mock_project.get_openai_client.return_value = MagicMock()
262+
263+
client = RawFoundryAgentChatClient(
264+
project_client=mock_project,
265+
agent_name="test-agent",
266+
)
267+
268+
# Simulate what the parent OpenAI Responses client produces for a Pydantic
269+
# ``response_format``: it sets ``text_format`` and would route through
270+
# ``responses.parse()``.
271+
with patch(
272+
"agent_framework_openai._chat_client.RawOpenAIChatClient._prepare_options",
273+
new_callable=AsyncMock,
274+
return_value={"text_format": OutputStruct},
275+
):
276+
result = await client._prepare_options(
277+
messages=[Message(role="user", contents="hi")],
278+
options={"response_format": OutputStruct},
279+
)
280+
281+
assert "text" not in result
282+
assert "text_format" not in result
283+
284+
# And for the dict / json_schema variant the parent populates ``text``.
285+
with patch(
286+
"agent_framework_openai._chat_client.RawOpenAIChatClient._prepare_options",
287+
new_callable=AsyncMock,
288+
return_value={
289+
"text": {
290+
"format": {
291+
"type": "json_schema",
292+
"name": "WeatherDigest",
293+
"schema": {"type": "object", "properties": {"location": {"type": "string"}}},
294+
}
295+
}
296+
},
297+
):
298+
result = await client._prepare_options(
299+
messages=[Message(role="user", contents="hi")],
300+
options={"response_format": {"type": "json_schema", "json_schema": {"name": "WeatherDigest"}}},
301+
)
302+
303+
assert "text" not in result
304+
assert "text_format" not in result
305+
306+
242307
async def test_raw_foundry_agent_chat_client_prepare_options_maps_agent_session_id_to_extra_body() -> None:
243308
"""Test that service_session_id is forwarded as agent_session_id for hosted sessions."""
244309

0 commit comments

Comments
 (0)