From 840af0065f07726277730d6633b261e15eba4d57 Mon Sep 17 00:00:00 2001 From: karthik-0306 Date: Thu, 23 Jul 2026 15:41:46 +0530 Subject: [PATCH 1/3] Python: Fix FoundryAgent inheriting OPENAI_CHAT_MODEL for agent-reference requests (#7272) --- .../foundry/agent_framework_foundry/_agent.py | 3 +- .../tests/foundry/test_foundry_agent.py | 263 ++++++++++++++++++ 2 files changed, 265 insertions(+), 1 deletion(-) diff --git a/python/packages/foundry/agent_framework_foundry/_agent.py b/python/packages/foundry/agent_framework_foundry/_agent.py index 8e26bd47aa..180e033409 100644 --- a/python/packages/foundry/agent_framework_foundry/_agent.py +++ b/python/packages/foundry/agent_framework_foundry/_agent.py @@ -274,6 +274,7 @@ def __init__( tokenizer=tokenizer, additional_properties=additional_properties, ) + self.model = "" # Foundry agents resolve model server-side; ignore env vars (#7272) @override def as_agent( @@ -378,7 +379,7 @@ async def _prepare_options( if not self.allow_preview: extra_body.setdefault("agent_reference", _build_agent_reference(self.agent_name, self.agent_version)) should_strip_model = _uses_foundry_agent_session(conversation_id) or ( - conversation_id is None and options.get("model") is None + conversation_id is None and not options.get("model") # falsy catches None and "" (#7272) ) if should_strip_model: run_options.pop("model", None) diff --git a/python/packages/foundry/tests/foundry/test_foundry_agent.py b/python/packages/foundry/tests/foundry/test_foundry_agent.py index 6d335a17e4..7d5380d0a4 100644 --- a/python/packages/foundry/tests/foundry/test_foundry_agent.py +++ b/python/packages/foundry/tests/foundry/test_foundry_agent.py @@ -1704,3 +1704,266 @@ def test_parse_chunk_surfaces_oauth_consent_requested_event() -> None: assert consent_contents[0].consent_link == "https://consent-host.example.com/authorize?code=xyz" assert update.role == "assistant" assert update.raw_representation is mock_event + + +def test_client_model_not_set_from_openai_chat_model(monkeypatch: pytest.MonkeyPatch) -> None: + """client.model must be empty when OPENAI_CHAT_MODEL is set.""" + monkeypatch.setenv("OPENAI_CHAT_MODEL", "gpt-4.1-from-env") + + mock_project = MagicMock() + mock_project.get_openai_client.return_value = MagicMock(spec=AsyncOpenAI) + + client = RawFoundryAgentChatClient( + project_client=mock_project, + agent_name="my-prompt-agent", + ) + + assert client.model != "gpt-4.1-from-env", "client.model must not be sourced from OPENAI_CHAT_MODEL" + assert not client.model, f"Expected empty/falsy model, got {client.model!r}" + + +def test_agent_default_options_not_polluted_by_openai_chat_model( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Agent.default_options['model'] must not be the env-var value.""" + monkeypatch.setenv("OPENAI_CHAT_MODEL", "gpt-4.1-from-env") + + mock_project = MagicMock() + mock_project.get_openai_client.return_value = MagicMock(spec=AsyncOpenAI) + + client = RawFoundryAgentChatClient( + project_client=mock_project, + agent_name="my-prompt-agent", + ) + agent = Agent(client=client, instructions="test") + + model_in_options = agent.default_options.get("model") + assert model_in_options != "gpt-4.1-from-env", ( + f"default_options['model'] must not be 'gpt-4.1-from-env', got {model_in_options!r}" + ) + assert not model_in_options, f"default_options['model'] must be falsy (empty or absent), got {model_in_options!r}" + + +def test_client_model_not_set_from_openai_model(monkeypatch: pytest.MonkeyPatch) -> None: + """client.model must be empty when OPENAI_MODEL is set (no OPENAI_CHAT_MODEL).""" + monkeypatch.delenv("OPENAI_CHAT_MODEL", raising=False) + monkeypatch.setenv("OPENAI_MODEL", "generic-gpt-from-env") + + mock_project = MagicMock() + mock_project.get_openai_client.return_value = MagicMock(spec=AsyncOpenAI) + + client = RawFoundryAgentChatClient( + project_client=mock_project, + agent_name="my-prompt-agent", + ) + + assert client.model != "generic-gpt-from-env", "client.model must not be sourced from OPENAI_MODEL" + assert not client.model, f"Expected empty/falsy model, got {client.model!r}" + + +def test_raw_foundry_agent_chat_client_rejects_model_keyword_arg() -> None: + """RawFoundryAgentChatClient.__init__ does not accept a 'model' parameter.""" + mock_project = MagicMock() + mock_project.get_openai_client.return_value = MagicMock(spec=AsyncOpenAI) + + with pytest.raises(TypeError, match="unexpected keyword argument"): + RawFoundryAgentChatClient( + project_client=mock_project, + agent_name="my-prompt-agent", + model="gpt-5.4", # type: ignore[call-arg] + ) + + +def test_explicit_model_in_agent_default_options_survives( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """An explicit model in Agent(default_options=...) must survive into default_options.""" + monkeypatch.setenv("OPENAI_CHAT_MODEL", "gpt-4.1-from-env") + + mock_project = MagicMock() + mock_project.get_openai_client.return_value = MagicMock(spec=AsyncOpenAI) + + client = RawFoundryAgentChatClient( + project_client=mock_project, + agent_name="my-prompt-agent", + ) + agent = Agent( + client=client, + instructions="test", + default_options={"model": "gpt-5.4"}, + ) + + assert agent.default_options.get("model") == "gpt-5.4", ( + f"Explicit model override must survive in default_options, got {agent.default_options.get('model')!r}" + ) + + +def test_explicit_model_in_as_agent_default_options_survives( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """An explicit model passed to client.as_agent(default_options=...) survives.""" + monkeypatch.setenv("OPENAI_CHAT_MODEL", "gpt-4.1-from-env") + + mock_project = MagicMock() + mock_project.get_openai_client.return_value = MagicMock(spec=AsyncOpenAI) + + client = RawFoundryAgentChatClient( + project_client=mock_project, + agent_name="my-prompt-agent", + ) + agent = client.as_agent( + instructions="test", + default_options={"model": "gpt-5.4"}, + ) + + assert agent.default_options.get("model") == "gpt-5.4", ( + f"Explicit model in as_agent default_options must survive, got {agent.default_options.get('model')!r}" + ) + + +def test_allow_preview_client_model_not_set_from_env(monkeypatch: pytest.MonkeyPatch) -> None: + """allow_preview=True clients must also not inherit OPENAI_CHAT_MODEL env var into client.model.""" + monkeypatch.setenv("OPENAI_CHAT_MODEL", "gpt-4.1-from-env") + + mock_project = MagicMock() + mock_project.get_openai_client.return_value = MagicMock(spec=AsyncOpenAI) + + client = RawFoundryAgentChatClient( + project_client=mock_project, + agent_name="hosted-agent", + allow_preview=True, + ) + + assert client.model != "gpt-4.1-from-env", ( + "Hosted-agent client (allow_preview=True) must not inherit OPENAI_CHAT_MODEL" + ) + assert not client.model, f"Expected empty/falsy model, got {client.model!r}" + + +def test_allow_preview_binds_agent_name_to_get_openai_client() -> None: + """When allow_preview=True, agent_name is passed to get_openai_client for endpoint binding.""" + mock_project = MagicMock() + mock_project.get_openai_client.return_value = MagicMock() + + client = RawFoundryAgentChatClient( + project_client=mock_project, + agent_name="hosted-agent", + allow_preview=True, + ) + + mock_project.get_openai_client.assert_called_once_with(agent_name="hosted-agent") + assert client.allow_preview is True + + +async def test_allow_preview_get_response_executes_successfully_with_empty_model( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """End-to-end get_response test on allow_preview=True with OPENAI_CHAT_MODEL set in env.""" + monkeypatch.setenv("OPENAI_CHAT_MODEL", "gpt-4.1-from-env") + + mock_response = MagicMock() + mock_response.id = "resp_123" + mock_response.model = "test-model" + mock_response.created_at = 1000000000 + mock_response.output_parsed = None + mock_response.metadata = {} + mock_response.output = [] + mock_response.usage = None + mock_response.finish_reason = None + mock_response.conversation = None + mock_response.status = "completed" + mock_response.parse.return_value = mock_response + mock_response.headers = {} + + mock_openai = MagicMock() + mock_openai.responses.with_raw_response.create = AsyncMock(return_value=mock_response) + mock_project = MagicMock() + mock_project.get_openai_client.return_value = mock_openai + + client = RawFoundryAgentChatClient( + project_client=mock_project, + agent_name="hosted-agent", + allow_preview=True, + ) + + assert client.model == "" + + response = await client.get_response([Message(role="user", contents=["Hello hosted agent"])]) + + assert response.response_id == "resp_123" + + create_call = mock_openai.responses.with_raw_response.create.await_args + assert create_call is not None + kwargs = create_call.kwargs + + assert kwargs.get("model") != "gpt-4.1-from-env" + assert "extra_body" not in kwargs or "agent_reference" not in kwargs.get("extra_body", {}) + + +def test_azure_openai_chat_model_does_not_leak(monkeypatch: pytest.MonkeyPatch) -> None: + """AZURE_OPENAI_CHAT_MODEL must not leak into client.model.""" + monkeypatch.delenv("OPENAI_CHAT_MODEL", raising=False) + monkeypatch.delenv("OPENAI_MODEL", raising=False) + monkeypatch.setenv("AZURE_OPENAI_CHAT_MODEL", "azure-gpt-4.1-from-env") + + mock_project = MagicMock() + mock_project.get_openai_client.return_value = MagicMock(spec=AsyncOpenAI) + + client = RawFoundryAgentChatClient( + project_client=mock_project, + agent_name="my-prompt-agent", + ) + + assert client.model != "azure-gpt-4.1-from-env", "client.model must not be sourced from AZURE_OPENAI_CHAT_MODEL" + assert not client.model, f"Expected empty/falsy model, got {client.model!r}" + + +def test_azure_openai_model_does_not_leak(monkeypatch: pytest.MonkeyPatch) -> None: + """AZURE_OPENAI_MODEL must not leak into client.model.""" + monkeypatch.delenv("OPENAI_CHAT_MODEL", raising=False) + monkeypatch.delenv("OPENAI_MODEL", raising=False) + monkeypatch.setenv("AZURE_OPENAI_MODEL", "azure-model-from-env") + + mock_project = MagicMock() + mock_project.get_openai_client.return_value = MagicMock(spec=AsyncOpenAI) + + client = RawFoundryAgentChatClient( + project_client=mock_project, + agent_name="my-prompt-agent", + ) + + assert client.model != "azure-model-from-env", "client.model must not be sourced from AZURE_OPENAI_MODEL" + assert not client.model, f"Expected empty/falsy model, got {client.model!r}" + + +async def test_env_model_is_stripped_from_agent_reference_payload( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Even if '' survives into default_options, it must not appear in the API payload.""" + monkeypatch.setenv("OPENAI_CHAT_MODEL", "gpt-4.1-from-env") + + mock_project = MagicMock() + mock_project.get_openai_client.return_value = MagicMock(spec=AsyncOpenAI) + + client = RawFoundryAgentChatClient( + project_client=mock_project, + agent_name="my-prompt-agent", + agent_version="1.0", + ) + + with patch( + "agent_framework_openai._chat_client.RawOpenAIChatClient._prepare_options", + new_callable=AsyncMock, + return_value={ + "model": "", + "input": [{"role": "user", "content": [{"type": "input_text", "text": "hi"}]}], + }, + ): + result = await client._prepare_options( + messages=[Message(role="user", contents=["hi"])], + options={}, + ) + + assert "model" not in result or not result.get("model"), ( + f"Empty/falsy model must be stripped from outgoing payload, got model={result.get('model')!r}" + ) From b65ac55a925faa00009192acebb28d803068c76b Mon Sep 17 00:00:00 2001 From: karthik-0306 Date: Thu, 23 Jul 2026 17:43:26 +0530 Subject: [PATCH 2/3] Python: Fix FoundryAgent inheriting OPENAI_CHAT_MODEL for agent-reference requests --- .../foundry/agent_framework_foundry/_agent.py | 2 +- .../tests/foundry/test_foundry_agent.py | 34 +++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/python/packages/foundry/agent_framework_foundry/_agent.py b/python/packages/foundry/agent_framework_foundry/_agent.py index 180e033409..84af3b7f5a 100644 --- a/python/packages/foundry/agent_framework_foundry/_agent.py +++ b/python/packages/foundry/agent_framework_foundry/_agent.py @@ -379,7 +379,7 @@ async def _prepare_options( if not self.allow_preview: extra_body.setdefault("agent_reference", _build_agent_reference(self.agent_name, self.agent_version)) should_strip_model = _uses_foundry_agent_session(conversation_id) or ( - conversation_id is None and not options.get("model") # falsy catches None and "" (#7272) + conversation_id is None and not options.get("model") ) if should_strip_model: run_options.pop("model", None) diff --git a/python/packages/foundry/tests/foundry/test_foundry_agent.py b/python/packages/foundry/tests/foundry/test_foundry_agent.py index 7d5380d0a4..9579cc1dde 100644 --- a/python/packages/foundry/tests/foundry/test_foundry_agent.py +++ b/python/packages/foundry/tests/foundry/test_foundry_agent.py @@ -1967,3 +1967,37 @@ async def test_env_model_is_stripped_from_agent_reference_payload( assert "model" not in result or not result.get("model"), ( f"Empty/falsy model must be stripped from outgoing payload, got model={result.get('model')!r}" ) + + +async def test_continuation_turn_strips_empty_model( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Multi-turn continuation calls with conversation_id='resp_123' must also strip empty model strings.""" + monkeypatch.setenv("OPENAI_CHAT_MODEL", "gpt-4.1-from-env") + + mock_project = MagicMock() + mock_project.get_openai_client.return_value = MagicMock(spec=AsyncOpenAI) + + client = RawFoundryAgentChatClient( + project_client=mock_project, + agent_name="my-prompt-agent", + agent_version="1.0", + ) + + with patch( + "agent_framework_openai._chat_client.RawOpenAIChatClient._prepare_options", + new_callable=AsyncMock, + return_value={ + "model": "", + "previous_response_id": "resp_123", + "input": [{"role": "user", "content": [{"type": "input_text", "text": "hi"}]}], + }, + ): + result = await client._prepare_options( + messages=[Message(role="user", contents=["hi"])], + options={"conversation_id": "resp_123"}, + ) + + assert "model" not in result or not result.get("model"), ( + f"Empty/falsy model must be stripped on continuation turns, got model={result.get('model')!r}" + ) From 3015e7f420eb18b0a0ee7152d4adb43525745901 Mon Sep 17 00:00:00 2001 From: karthik-0306 Date: Thu, 23 Jul 2026 18:54:33 +0530 Subject: [PATCH 3/3] fix(foundry): update test typing annotations to pass mypy, pyrefly, and ty --- .../foundry/agent_framework_foundry/_agent.py | 2 +- .../foundry/tests/foundry/test_foundry_agent.py | 13 +++++++------ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/python/packages/foundry/agent_framework_foundry/_agent.py b/python/packages/foundry/agent_framework_foundry/_agent.py index 84af3b7f5a..f93ee54aae 100644 --- a/python/packages/foundry/agent_framework_foundry/_agent.py +++ b/python/packages/foundry/agent_framework_foundry/_agent.py @@ -274,7 +274,7 @@ def __init__( tokenizer=tokenizer, additional_properties=additional_properties, ) - self.model = "" # Foundry agents resolve model server-side; ignore env vars (#7272) + self.model = "" # Foundry agents resolve model server-side; ignore env vars (issue #7272). @override def as_agent( diff --git a/python/packages/foundry/tests/foundry/test_foundry_agent.py b/python/packages/foundry/tests/foundry/test_foundry_agent.py index 9579cc1dde..f837108de1 100644 --- a/python/packages/foundry/tests/foundry/test_foundry_agent.py +++ b/python/packages/foundry/tests/foundry/test_foundry_agent.py @@ -1735,7 +1735,7 @@ def test_agent_default_options_not_polluted_by_openai_chat_model( project_client=mock_project, agent_name="my-prompt-agent", ) - agent = Agent(client=client, instructions="test") + agent = Agent(client=cast(Any, client), instructions="test") model_in_options = agent.default_options.get("model") assert model_in_options != "gpt-4.1-from-env", ( @@ -1766,11 +1766,12 @@ def test_raw_foundry_agent_chat_client_rejects_model_keyword_arg() -> None: mock_project = MagicMock() mock_project.get_openai_client.return_value = MagicMock(spec=AsyncOpenAI) + cls = cast(Any, RawFoundryAgentChatClient) with pytest.raises(TypeError, match="unexpected keyword argument"): - RawFoundryAgentChatClient( + cls( project_client=mock_project, agent_name="my-prompt-agent", - model="gpt-5.4", # type: ignore[call-arg] + model="gpt-5.4", ) @@ -1788,9 +1789,9 @@ def test_explicit_model_in_agent_default_options_survives( agent_name="my-prompt-agent", ) agent = Agent( - client=client, + client=cast(Any, client), instructions="test", - default_options={"model": "gpt-5.4"}, + default_options=cast(Any, {"model": "gpt-5.4"}), ) assert agent.default_options.get("model") == "gpt-5.4", ( @@ -1813,7 +1814,7 @@ def test_explicit_model_in_as_agent_default_options_survives( ) agent = client.as_agent( instructions="test", - default_options={"model": "gpt-5.4"}, + default_options=cast(Any, {"model": "gpt-5.4"}), ) assert agent.default_options.get("model") == "gpt-5.4", (