From fa8cfb75673eb2f8f3a9870b2d95e1a435ac3dc3 Mon Sep 17 00:00:00 2001 From: Benke Qu <89610947+benke520@users.noreply.github.com> Date: Tue, 2 Jun 2026 11:30:04 -0700 Subject: [PATCH] Python: Fix FoundryAgent stripping model from PromptAgent requests (#5526) * Fix FoundryAgent stripping model from PromptAgent requests Move run_options.pop('model', None) inside the _uses_foundry_agent_session() conditional so that model is only stripped for hosted agent sessions (where the server manages the model) and preserved for PromptAgent requests that require it in the Responses API call. Fixes #5525 * test: add coverage for resp_* continuation preserving model Adds test_raw_foundry_agent_chat_client_prepare_options_preserves_model_for_resp_continuation to explicitly verify that HostedAgent v1 / v2-no-session paths (where conversation_id starts with resp_) preserve model and previous_response_id without triggering the hosted-session gate. --------- Co-authored-by: Benke Qu Co-authored-by: Evan Mattson <35585003+moonbox3@users.noreply.github.com> --- .../foundry/agent_framework_foundry/_agent.py | 2 +- .../tests/foundry/test_foundry_agent.py | 74 ++++++++++++++++++- 2 files changed, 71 insertions(+), 5 deletions(-) diff --git a/python/packages/foundry/agent_framework_foundry/_agent.py b/python/packages/foundry/agent_framework_foundry/_agent.py index 1e1157d05a..433380580d 100644 --- a/python/packages/foundry/agent_framework_foundry/_agent.py +++ b/python/packages/foundry/agent_framework_foundry/_agent.py @@ -351,6 +351,7 @@ class RawFoundryAgentChatClient( # type: ignore[misc] if _uses_foundry_agent_session(conversation_id): run_options.pop("previous_response_id", None) run_options.pop("conversation", None) + run_options.pop("model", None) extra_body["agent_session_id"] = conversation_id # Non-preview Prompt/Hosted Agent calls need agent_reference in the request body to # tell the Responses API which Foundry agent (and version) is in use, since ``model`` @@ -366,7 +367,6 @@ class RawFoundryAgentChatClient( # type: ignore[misc] # 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. - run_options.pop("model", 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 44bc744f64..2cbf491c16 100644 --- a/python/packages/foundry/tests/foundry/test_foundry_agent.py +++ b/python/packages/foundry/tests/foundry/test_foundry_agent.py @@ -203,7 +203,7 @@ async def test_raw_foundry_agent_chat_client_prepare_options_accepts_function_to 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 tool-loop fields but preserves model for non-session requests.""" mock_project = MagicMock() mock_openai = MagicMock() @@ -235,16 +235,49 @@ async def test_raw_foundry_agent_chat_client_prepare_options_strips_client_side_ options={"tools": [my_func]}, ) - assert "model" not in result + # model is preserved for non-session (PromptAgent) requests + assert result["model"] == "gpt-4.1" assert "tools" not in result assert "tool_choice" not in result assert "parallel_tool_calls" not in result # agent_reference is required so the Responses API can resolve model server-side; see #5582. assert result == { + "model": "gpt-4.1", "extra_body": {"agent_reference": {"name": "test-agent", "type": "agent_reference"}}, } +async def test_raw_foundry_agent_chat_client_prepare_options_strips_model_for_hosted_session() -> None: + """Test that model is stripped when using a hosted agent session (not a PromptAgent).""" + + 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", + ) + + with patch( + "agent_framework_openai._chat_client.RawOpenAIChatClient._prepare_options", + new_callable=AsyncMock, + return_value={ + "model": "gpt-4.1", + "previous_response_id": "resp_abc", + }, + ): + result = await client._prepare_options( + messages=[Message(role="user", contents="hi")], + options={"conversation_id": "agent-session-123"}, + ) + + assert "model" not in result + assert "previous_response_id" not in result + assert result["extra_body"]["agent_session_id"] == "agent-session-123" + assert result["extra_body"]["agent_reference"] == {"name": "test-agent", "type": "agent_reference"} + + async def test_raw_foundry_agent_chat_client_prepare_options_injects_agent_reference_first_turn() -> None: """First-turn (no conversation_id) Prompt Agent calls must carry agent_reference in extra_body. @@ -272,7 +305,6 @@ async def test_raw_foundry_agent_chat_client_prepare_options_injects_agent_refer options={}, ) - assert "model" not in result assert result["extra_body"] == { "agent_reference": {"name": "test-agent", "type": "agent_reference", "version": "2"}, } @@ -333,7 +365,8 @@ async def test_raw_foundry_agent_chat_client_prepare_options_skips_agent_referen options={}, ) - assert "model" not in result + # model is preserved for non-session requests (platform tolerates it for hosted agents) + assert result["model"] == "gpt-4.1" # No extra_body at all is the cleanest signal — agent_reference must not be injected here. assert "extra_body" not in result @@ -363,6 +396,39 @@ async def test_raw_foundry_agent_chat_client_prepare_options_respects_caller_age assert result["extra_body"]["agent_reference"] == caller_reference +async def test_raw_foundry_agent_chat_client_prepare_options_preserves_model_for_resp_continuation() -> None: + """Test that model is preserved when conversation_id is a resp_* continuation (HostedAgent v1 / v2-no-session).""" + + 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", + ) + + with patch( + "agent_framework_openai._chat_client.RawOpenAIChatClient._prepare_options", + new_callable=AsyncMock, + return_value={ + "model": "gpt-4.1", + "previous_response_id": "resp_abc123", + }, + ): + result = await client._prepare_options( + messages=[Message(role="user", contents="hi")], + options={"conversation_id": "resp_abc123"}, + ) + + # model preserved — resp_* is standard Responses API continuity, not a hosted session + assert result["model"] == "gpt-4.1" + # previous_response_id preserved — not stripped outside hosted session path + assert result["previous_response_id"] == "resp_abc123" + # no agent_session_id injected + assert "extra_body" not in result or "agent_session_id" not in result.get("extra_body", {}) + + 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."""