Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions python/packages/foundry/agent_framework_foundry/_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,20 @@ 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 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)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The 400 going away isn't equivalent to runtime response_format being supported: silently stripping it turns the option into a client-side parse hint, which contradicts the FoundryAgent design (settings at creation time).

run_options.pop("text_format", None)
if not self.allow_preview:
run_options.pop("tools", None)
run_options.pop("tool_choice", None)
Expand Down
67 changes: 66 additions & 1 deletion python/packages/foundry/tests/foundry/test_foundry_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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(
Expand All @@ -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."""

Expand Down
Loading