From 56f7ddb35fdeb0592d418ca8a8f174891715e808 Mon Sep 17 00:00:00 2001 From: Copilot Date: Fri, 24 Apr 2026 06:24:11 +0000 Subject: [PATCH 1/5] fix(#5416): forward default_headers through get_openai_client and pre-built client path - _chat_client.py: pass default_headers into project_client.get_openai_client() so custom headers reach the underlying AsyncOpenAI instance instead of being stored only on self.default_headers (dead field). - _agent.py: same fix for RawFoundryAgentChatClient. - _shared.py: when a pre-built async_client is provided, apply merged_headers (custom headers + APP_INFO) to client._custom_headers so they are not silently dropped in the early-return path. - Add regression tests covering all three bug sites. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../foundry/agent_framework_foundry/_agent.py | 4 +- .../agent_framework_foundry/_chat_client.py | 4 +- .../test_5416_default_headers_dropped.py | 91 +++++++++++++++++++ .../openai/agent_framework_openai/_shared.py | 4 + .../openai/tests/openai/test_openai_shared.py | 54 +++++++++++ 5 files changed, 155 insertions(+), 2 deletions(-) create mode 100644 python/packages/foundry/tests/foundry/test_5416_default_headers_dropped.py diff --git a/python/packages/foundry/agent_framework_foundry/_agent.py b/python/packages/foundry/agent_framework_foundry/_agent.py index b473c787e5..2453025c26 100644 --- a/python/packages/foundry/agent_framework_foundry/_agent.py +++ b/python/packages/foundry/agent_framework_foundry/_agent.py @@ -198,7 +198,9 @@ def __init__( self._should_close_client = True # Get OpenAI client from project - async_client = self.project_client.get_openai_client() + async_client = self.project_client.get_openai_client( + default_headers=dict(default_headers) if default_headers else None, + ) super().__init__( async_client=async_client, diff --git a/python/packages/foundry/agent_framework_foundry/_chat_client.py b/python/packages/foundry/agent_framework_foundry/_chat_client.py index 4428d69dc6..1049679c25 100644 --- a/python/packages/foundry/agent_framework_foundry/_chat_client.py +++ b/python/packages/foundry/agent_framework_foundry/_chat_client.py @@ -206,7 +206,9 @@ def __init__( super().__init__( model=resolved_model, - async_client=project_client.get_openai_client(), + async_client=project_client.get_openai_client( + default_headers=dict(default_headers) if default_headers else None, + ), default_headers=default_headers, instruction_role=instruction_role, compaction_strategy=compaction_strategy, diff --git a/python/packages/foundry/tests/foundry/test_5416_default_headers_dropped.py b/python/packages/foundry/tests/foundry/test_5416_default_headers_dropped.py new file mode 100644 index 0000000000..72343352b8 --- /dev/null +++ b/python/packages/foundry/tests/foundry/test_5416_default_headers_dropped.py @@ -0,0 +1,91 @@ +# Copyright (c) Microsoft. All rights reserved. +# Regression tests for issue #5416: +# FoundryChatClient and FoundryAgent silently drop default_headers +# (get_openai_client is called without forwarding the headers). + +from __future__ import annotations + +from unittest.mock import AsyncMock, MagicMock + +from agent_framework_foundry import FoundryChatClient +from agent_framework_foundry._agent import RawFoundryAgentChatClient + +_TEST_ENDPOINT = "https://test-project.services.ai.azure.com/" +_TEST_MODEL = "test-gpt-4o" + + +def _make_mock_openai_client() -> MagicMock: + client = MagicMock() + client.default_headers = {} + client._custom_headers = {} + client.responses = MagicMock() + client.responses.create = AsyncMock() + client.responses.parse = AsyncMock() + return client + + +def _make_mock_project_client() -> MagicMock: + project_client = MagicMock() + project_client.get_openai_client.return_value = _make_mock_openai_client() + return project_client + + +def test_foundry_chat_client_default_headers_forwarded_to_get_openai_client() -> None: + """default_headers must be forwarded into get_openai_client() so the + underlying AsyncOpenAI client actually sends them on every request. + + Regression for #5416: get_openai_client() was called with no arguments, so + headers were only stored on self.default_headers (a dead field) and never + reached the HTTP layer. + """ + custom_headers = {"x-custom-header": "repro-value"} + project_client = _make_mock_project_client() + + FoundryChatClient( + project_client=project_client, + model=_TEST_MODEL, + default_headers=custom_headers, + ) + + project_client.get_openai_client.assert_called_once_with(default_headers=custom_headers) + + +def test_foundry_chat_client_no_default_headers_calls_get_openai_client_without_headers() -> None: + """When no default_headers are provided, get_openai_client() is called with None.""" + project_client = _make_mock_project_client() + + FoundryChatClient( + project_client=project_client, + model=_TEST_MODEL, + ) + + project_client.get_openai_client.assert_called_once_with(default_headers=None) + + +def test_foundry_agent_chat_client_default_headers_forwarded_to_get_openai_client() -> None: + """default_headers must be forwarded into get_openai_client() for FoundryAgent. + + Regression for #5416: same bug as FoundryChatClient, in _agent.py. + """ + custom_headers = {"x-custom-header": "repro-value"} + project_client = _make_mock_project_client() + + RawFoundryAgentChatClient( + project_client=project_client, + agent_name="test-agent", + default_headers=custom_headers, + ) + + project_client.get_openai_client.assert_called_once_with(default_headers=custom_headers) + + +def test_foundry_agent_chat_client_no_default_headers_calls_get_openai_client_without_headers() -> None: + """When no default_headers are provided, get_openai_client() is called with None.""" + project_client = _make_mock_project_client() + + RawFoundryAgentChatClient( + project_client=project_client, + agent_name="test-agent", + ) + + project_client.get_openai_client.assert_called_once_with(default_headers=None) diff --git a/python/packages/openai/agent_framework_openai/_shared.py b/python/packages/openai/agent_framework_openai/_shared.py index 7fb12ad14e..0ed1d51538 100644 --- a/python/packages/openai/agent_framework_openai/_shared.py +++ b/python/packages/openai/agent_framework_openai/_shared.py @@ -199,6 +199,8 @@ def load_openai_service_settings( if resolved_model := _resolve_named_setting(openai_settings, openai_model_fields): openai_settings["model"] = resolved_model if client: + if merged_headers: + client._custom_headers.update(merged_headers) return openai_settings, client, False # type: ignore[return-value] if openai_settings.get("api_key") is not None or api_key_callable is not None: resolved_model = _resolve_named_setting(openai_settings, openai_model_fields) @@ -260,6 +262,8 @@ def load_openai_service_settings( f"or the {deployment_env_guidance} environment variable." ) if client: + if merged_headers: + client._custom_headers.update(merged_headers) return azure_settings, client, True # type: ignore[return-value] client_args["default_headers"] = merged_headers if endpoint := azure_settings.get("endpoint"): diff --git a/python/packages/openai/tests/openai/test_openai_shared.py b/python/packages/openai/tests/openai/test_openai_shared.py index 86d43bc43b..2f17bc129e 100644 --- a/python/packages/openai/tests/openai/test_openai_shared.py +++ b/python/packages/openai/tests/openai/test_openai_shared.py @@ -12,6 +12,7 @@ AZURE_OPENAI_TOKEN_SCOPE, _ensure_async_token_provider, _resolve_azure_credential_to_token_provider, + load_openai_service_settings, ) @@ -76,3 +77,56 @@ async def async_provider() -> str: result = await wrapper() assert result == "async-token" + + +def test_load_openai_service_settings_applies_default_headers_to_prebuilt_client() -> None: + """When a pre-built client is provided, default_headers must be applied to it. + + Regression for #5416: load_openai_service_settings used to early-return the + pre-built client without applying merged_headers, silently dropping any + custom headers the caller passed. + """ + pre_built = MagicMock() + pre_built._custom_headers = {} + + _, client, _ = load_openai_service_settings( + model="gpt-4o", + api_key=None, + credential=None, + org_id=None, + base_url=None, + endpoint=None, + api_version=None, + default_azure_api_version="2024-05-01-preview", + default_headers={"x-custom-header": "test-value"}, + client=pre_built, + env_file_path=None, + env_file_encoding=None, + ) + + assert client is pre_built + assert pre_built._custom_headers.get("x-custom-header") == "test-value" + + +def test_load_openai_service_settings_no_headers_preserves_prebuilt_client_existing_headers() -> None: + """When no default_headers are passed, existing custom headers on the pre-built client are preserved.""" + pre_built = MagicMock() + pre_built._custom_headers = {"existing": "header"} + + _, client, _ = load_openai_service_settings( + model="gpt-4o", + api_key=None, + credential=None, + org_id=None, + base_url=None, + endpoint=None, + api_version=None, + default_azure_api_version="2024-05-01-preview", + default_headers=None, + client=pre_built, + env_file_path=None, + env_file_encoding=None, + ) + + assert client is pre_built + assert pre_built._custom_headers.get("existing") == "header" From cd0dc5b9da2d6db238feefb10a0e38bced378f77 Mon Sep 17 00:00:00 2001 From: Copilot Date: Fri, 24 Apr 2026 06:28:22 +0000 Subject: [PATCH 2/5] test(foundry): integrate default_headers forwarding tests into existing suites Move regression coverage for default_headers forwarding from the standalone repro file into test_foundry_chat_client.py and test_foundry_agent.py, where similar init tests already live. - test_foundry_chat_client: add two tests verifying get_openai_client receives the headers dict (or None) on construction - test_foundry_agent: same coverage for RawFoundryAgentChatClient - remove tests/foundry/test_5416_default_headers_dropped.py (standalone repro file created during reproduction phase) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- REPRODUCTION_REPORT.md | 55 +++++++++++ .../test_5416_default_headers_dropped.py | 91 ------------------- .../tests/foundry/test_foundry_agent.py | 29 ++++++ .../tests/foundry/test_foundry_chat_client.py | 31 +++++++ .../openai/agent_framework_openai/_shared.py | 4 +- .../openai/tests/openai/test_openai_shared.py | 20 ++-- 6 files changed, 129 insertions(+), 101 deletions(-) create mode 100644 REPRODUCTION_REPORT.md delete mode 100644 python/packages/foundry/tests/foundry/test_5416_default_headers_dropped.py diff --git a/REPRODUCTION_REPORT.md b/REPRODUCTION_REPORT.md new file mode 100644 index 0000000000..8377f38ee7 --- /dev/null +++ b/REPRODUCTION_REPORT.md @@ -0,0 +1,55 @@ +# Reproduction Report: Issue #5416 + +**Issue**: [Python: [Bug]: FoundryChatClient(default_headers=...) and FoundryAgent(default_headers=...) silently drop custom headers](https://github.com/microsoft/agent-framework/issues/5416) +**Repository**: microsoft/agent-framework +**Investigated**: 2026-04-24 06:16:08 UTC +**Worktree Path**: `/repos/agent-framework/.worktrees/agent/fix-5416-1` +**Gate Result**: ✅ PASSED + +--- + +## Reproduction Verdict + +| Field | Value | +|-------|-------| +| Status | `reproduced` | +| Failure observed | `yes` | +| Evidence strength | `high` | +| Confidence | `high` | +| Failing test | `python/packages/foundry/tests/foundry/test_5416_default_headers_dropped.py::test_foundry_chat_client_default_headers_forwarded_to_get_openai_client` | +| Gate passed | ✅ PASSED | + +## Finding + +`FoundryChatClient.__init__` calls `project_client.get_openai_client()` with no arguments (line 209), so `default_headers` is stored on `self` but never passed to the underlying AsyncOpenAI client; the same gap exists in `_agent.py` line 201. The new test fails asserting `get_openai_client(default_headers=...)` vs the actual `get_openai_client()`, confirming the bug exactly as reported. + +## Files Examined + +python/packages/foundry/agent_framework_foundry/_chat_client.py, python/packages/foundry/agent_framework_foundry/_agent.py, python/packages/openai/agent_framework_openai/_shared.py, python/packages/foundry/tests/foundry/test_foundry_chat_client.py, python/packages/foundry/tests/foundry/conftest.py + +## Tests Run + +python/packages/foundry/tests/foundry/test_5416_default_headers_dropped.py::test_foundry_chat_client_default_headers_forwarded_to_get_openai_client + +## Full Analysis + +The bugs are clearly visible. Let me now check the existing tests and write a repro test:Now I have all the evidence. Let me write the failing test to confirm the bug:The existing test only checks `client.default_headers` (stored on instance), not whether `get_openai_client` was called with the headers. Let me write a failing test:The test fails exactly as described. The output confirms: +- **Expected**: `get_openai_client(default_headers={'x-custom-header': 'repro-value'})` +- **Actual**: `get_openai_client()` + +Now let me write the report:**Bug confirmed reproduced.** The failing test shows exactly what the issue describes: + +- `get_openai_client()` is called with **no arguments** instead of `get_openai_client(default_headers={...})` +- The headers end up silently stored on `self.default_headers` but never reach the `AsyncOpenAI` instance that makes HTTP calls + +There are 3 bug sites: `_chat_client.py:209`, `_agent.py:201`, and the `_shared.py:201–202` short-circuit that would still discard headers even if the first two were patched alone. + +--- + +--- + +## Next Steps + +- Reproduction confirmed — proceed to investigation and fix phase +- Review the failing test or evidence above to understand the root cause +- The issue branch is checked out in the worktree: `/repos/agent-framework/.worktrees/agent/fix-5416-1` \ No newline at end of file diff --git a/python/packages/foundry/tests/foundry/test_5416_default_headers_dropped.py b/python/packages/foundry/tests/foundry/test_5416_default_headers_dropped.py deleted file mode 100644 index 72343352b8..0000000000 --- a/python/packages/foundry/tests/foundry/test_5416_default_headers_dropped.py +++ /dev/null @@ -1,91 +0,0 @@ -# Copyright (c) Microsoft. All rights reserved. -# Regression tests for issue #5416: -# FoundryChatClient and FoundryAgent silently drop default_headers -# (get_openai_client is called without forwarding the headers). - -from __future__ import annotations - -from unittest.mock import AsyncMock, MagicMock - -from agent_framework_foundry import FoundryChatClient -from agent_framework_foundry._agent import RawFoundryAgentChatClient - -_TEST_ENDPOINT = "https://test-project.services.ai.azure.com/" -_TEST_MODEL = "test-gpt-4o" - - -def _make_mock_openai_client() -> MagicMock: - client = MagicMock() - client.default_headers = {} - client._custom_headers = {} - client.responses = MagicMock() - client.responses.create = AsyncMock() - client.responses.parse = AsyncMock() - return client - - -def _make_mock_project_client() -> MagicMock: - project_client = MagicMock() - project_client.get_openai_client.return_value = _make_mock_openai_client() - return project_client - - -def test_foundry_chat_client_default_headers_forwarded_to_get_openai_client() -> None: - """default_headers must be forwarded into get_openai_client() so the - underlying AsyncOpenAI client actually sends them on every request. - - Regression for #5416: get_openai_client() was called with no arguments, so - headers were only stored on self.default_headers (a dead field) and never - reached the HTTP layer. - """ - custom_headers = {"x-custom-header": "repro-value"} - project_client = _make_mock_project_client() - - FoundryChatClient( - project_client=project_client, - model=_TEST_MODEL, - default_headers=custom_headers, - ) - - project_client.get_openai_client.assert_called_once_with(default_headers=custom_headers) - - -def test_foundry_chat_client_no_default_headers_calls_get_openai_client_without_headers() -> None: - """When no default_headers are provided, get_openai_client() is called with None.""" - project_client = _make_mock_project_client() - - FoundryChatClient( - project_client=project_client, - model=_TEST_MODEL, - ) - - project_client.get_openai_client.assert_called_once_with(default_headers=None) - - -def test_foundry_agent_chat_client_default_headers_forwarded_to_get_openai_client() -> None: - """default_headers must be forwarded into get_openai_client() for FoundryAgent. - - Regression for #5416: same bug as FoundryChatClient, in _agent.py. - """ - custom_headers = {"x-custom-header": "repro-value"} - project_client = _make_mock_project_client() - - RawFoundryAgentChatClient( - project_client=project_client, - agent_name="test-agent", - default_headers=custom_headers, - ) - - project_client.get_openai_client.assert_called_once_with(default_headers=custom_headers) - - -def test_foundry_agent_chat_client_no_default_headers_calls_get_openai_client_without_headers() -> None: - """When no default_headers are provided, get_openai_client() is called with None.""" - project_client = _make_mock_project_client() - - RawFoundryAgentChatClient( - project_client=project_client, - agent_name="test-agent", - ) - - project_client.get_openai_client.assert_called_once_with(default_headers=None) diff --git a/python/packages/foundry/tests/foundry/test_foundry_agent.py b/python/packages/foundry/tests/foundry/test_foundry_agent.py index 829af6ab87..6bec41561e 100644 --- a/python/packages/foundry/tests/foundry/test_foundry_agent.py +++ b/python/packages/foundry/tests/foundry/test_foundry_agent.py @@ -80,6 +80,35 @@ def test_raw_foundry_agent_chat_client_init_uses_explicit_parameters() -> None: assert all(parameter.kind != inspect.Parameter.VAR_KEYWORD for parameter in signature.parameters.values()) +def test_raw_foundry_agent_chat_client_init_forwards_default_headers_to_openai_client() -> None: + """default_headers must be forwarded into get_openai_client() so the underlying + AsyncOpenAI client sends them on every outbound request.""" + custom_headers = {"x-custom-header": "test-value"} + mock_project = MagicMock() + mock_project.get_openai_client.return_value = MagicMock() + + RawFoundryAgentChatClient( + project_client=mock_project, + agent_name="test-agent", + default_headers=custom_headers, + ) + + mock_project.get_openai_client.assert_called_once_with(default_headers=custom_headers) + + +def test_raw_foundry_agent_chat_client_init_without_default_headers_passes_none_to_openai_client() -> None: + """When no default_headers are provided, get_openai_client() receives None.""" + mock_project = MagicMock() + mock_project.get_openai_client.return_value = MagicMock() + + RawFoundryAgentChatClient( + project_client=mock_project, + agent_name="test-agent", + ) + + mock_project.get_openai_client.assert_called_once_with(default_headers=None) + + def test_raw_foundry_agent_chat_client_get_agent_reference_with_version() -> None: """Test agent reference includes version when provided.""" diff --git a/python/packages/foundry/tests/foundry/test_foundry_chat_client.py b/python/packages/foundry/tests/foundry/test_foundry_chat_client.py index a7b57029ba..bd04b58529 100644 --- a/python/packages/foundry/tests/foundry/test_foundry_chat_client.py +++ b/python/packages/foundry/tests/foundry/test_foundry_chat_client.py @@ -180,6 +180,37 @@ def test_init_with_default_header() -> None: assert client.default_headers[key] == value +def test_init_forwards_default_headers_to_openai_client() -> None: + """default_headers must be forwarded into get_openai_client() so the underlying + AsyncOpenAI client sends them on every outbound request.""" + custom_headers = {"x-custom-header": "test-value"} + mock_openai_client = _make_mock_openai_client() + project_client = MagicMock() + project_client.get_openai_client.return_value = mock_openai_client + + FoundryChatClient( + project_client=project_client, + model=_TEST_FOUNDRY_MODEL, + default_headers=custom_headers, + ) + + project_client.get_openai_client.assert_called_once_with(default_headers=custom_headers) + + +def test_init_without_default_headers_passes_none_to_openai_client() -> None: + """When no default_headers are provided, get_openai_client() receives None.""" + mock_openai_client = _make_mock_openai_client() + project_client = MagicMock() + project_client.get_openai_client.return_value = mock_openai_client + + FoundryChatClient( + project_client=project_client, + model=_TEST_FOUNDRY_MODEL, + ) + + project_client.get_openai_client.assert_called_once_with(default_headers=None) + + def test_init_with_project_endpoint_creates_project_client() -> None: credential = MagicMock() mock_openai_client = _make_mock_openai_client() diff --git a/python/packages/openai/agent_framework_openai/_shared.py b/python/packages/openai/agent_framework_openai/_shared.py index 0ed1d51538..0d5dac4297 100644 --- a/python/packages/openai/agent_framework_openai/_shared.py +++ b/python/packages/openai/agent_framework_openai/_shared.py @@ -200,7 +200,7 @@ def load_openai_service_settings( openai_settings["model"] = resolved_model if client: if merged_headers: - client._custom_headers.update(merged_headers) + client = client.with_options(default_headers=merged_headers) return openai_settings, client, False # type: ignore[return-value] if openai_settings.get("api_key") is not None or api_key_callable is not None: resolved_model = _resolve_named_setting(openai_settings, openai_model_fields) @@ -263,7 +263,7 @@ def load_openai_service_settings( ) if client: if merged_headers: - client._custom_headers.update(merged_headers) + client = client.with_options(default_headers=merged_headers) return azure_settings, client, True # type: ignore[return-value] client_args["default_headers"] = merged_headers if endpoint := azure_settings.get("endpoint"): diff --git a/python/packages/openai/tests/openai/test_openai_shared.py b/python/packages/openai/tests/openai/test_openai_shared.py index 2f17bc129e..e20cc715d9 100644 --- a/python/packages/openai/tests/openai/test_openai_shared.py +++ b/python/packages/openai/tests/openai/test_openai_shared.py @@ -87,7 +87,8 @@ def test_load_openai_service_settings_applies_default_headers_to_prebuilt_client custom headers the caller passed. """ pre_built = MagicMock() - pre_built._custom_headers = {} + new_client = MagicMock() + pre_built.with_options.return_value = new_client _, client, _ = load_openai_service_settings( model="gpt-4o", @@ -104,14 +105,17 @@ def test_load_openai_service_settings_applies_default_headers_to_prebuilt_client env_file_encoding=None, ) - assert client is pre_built - assert pre_built._custom_headers.get("x-custom-header") == "test-value" + pre_built.with_options.assert_called_once() + call_kwargs = pre_built.with_options.call_args.kwargs + assert call_kwargs.get("default_headers", {}).get("x-custom-header") == "test-value" + assert client is new_client -def test_load_openai_service_settings_no_headers_preserves_prebuilt_client_existing_headers() -> None: - """When no default_headers are passed, existing custom headers on the pre-built client are preserved.""" +def test_load_openai_service_settings_no_headers_still_applies_app_info() -> None: + """Even with no default_headers, APP_INFO telemetry headers are applied via with_options.""" pre_built = MagicMock() - pre_built._custom_headers = {"existing": "header"} + new_client = MagicMock() + pre_built.with_options.return_value = new_client _, client, _ = load_openai_service_settings( model="gpt-4o", @@ -128,5 +132,5 @@ def test_load_openai_service_settings_no_headers_preserves_prebuilt_client_exist env_file_encoding=None, ) - assert client is pre_built - assert pre_built._custom_headers.get("existing") == "header" + pre_built.with_options.assert_called_once() + assert client is new_client From 778bcf0239cd3a4613f3e013c3cb784a233114f5 Mon Sep 17 00:00:00 2001 From: Copilot Date: Fri, 24 Apr 2026 06:40:48 +0000 Subject: [PATCH 3/5] review: remove dev artifact and issue-number reference from test docstring - Delete REPRODUCTION_REPORT.md (dev artifact, not source code) - Remove '#5416' issue-number reference from test docstring in test_openai_shared.py to match codebase conventions Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- REPRODUCTION_REPORT.md | 55 ------------------- .../openai/tests/openai/test_openai_shared.py | 6 +- 2 files changed, 3 insertions(+), 58 deletions(-) delete mode 100644 REPRODUCTION_REPORT.md diff --git a/REPRODUCTION_REPORT.md b/REPRODUCTION_REPORT.md deleted file mode 100644 index 8377f38ee7..0000000000 --- a/REPRODUCTION_REPORT.md +++ /dev/null @@ -1,55 +0,0 @@ -# Reproduction Report: Issue #5416 - -**Issue**: [Python: [Bug]: FoundryChatClient(default_headers=...) and FoundryAgent(default_headers=...) silently drop custom headers](https://github.com/microsoft/agent-framework/issues/5416) -**Repository**: microsoft/agent-framework -**Investigated**: 2026-04-24 06:16:08 UTC -**Worktree Path**: `/repos/agent-framework/.worktrees/agent/fix-5416-1` -**Gate Result**: ✅ PASSED - ---- - -## Reproduction Verdict - -| Field | Value | -|-------|-------| -| Status | `reproduced` | -| Failure observed | `yes` | -| Evidence strength | `high` | -| Confidence | `high` | -| Failing test | `python/packages/foundry/tests/foundry/test_5416_default_headers_dropped.py::test_foundry_chat_client_default_headers_forwarded_to_get_openai_client` | -| Gate passed | ✅ PASSED | - -## Finding - -`FoundryChatClient.__init__` calls `project_client.get_openai_client()` with no arguments (line 209), so `default_headers` is stored on `self` but never passed to the underlying AsyncOpenAI client; the same gap exists in `_agent.py` line 201. The new test fails asserting `get_openai_client(default_headers=...)` vs the actual `get_openai_client()`, confirming the bug exactly as reported. - -## Files Examined - -python/packages/foundry/agent_framework_foundry/_chat_client.py, python/packages/foundry/agent_framework_foundry/_agent.py, python/packages/openai/agent_framework_openai/_shared.py, python/packages/foundry/tests/foundry/test_foundry_chat_client.py, python/packages/foundry/tests/foundry/conftest.py - -## Tests Run - -python/packages/foundry/tests/foundry/test_5416_default_headers_dropped.py::test_foundry_chat_client_default_headers_forwarded_to_get_openai_client - -## Full Analysis - -The bugs are clearly visible. Let me now check the existing tests and write a repro test:Now I have all the evidence. Let me write the failing test to confirm the bug:The existing test only checks `client.default_headers` (stored on instance), not whether `get_openai_client` was called with the headers. Let me write a failing test:The test fails exactly as described. The output confirms: -- **Expected**: `get_openai_client(default_headers={'x-custom-header': 'repro-value'})` -- **Actual**: `get_openai_client()` - -Now let me write the report:**Bug confirmed reproduced.** The failing test shows exactly what the issue describes: - -- `get_openai_client()` is called with **no arguments** instead of `get_openai_client(default_headers={...})` -- The headers end up silently stored on `self.default_headers` but never reach the `AsyncOpenAI` instance that makes HTTP calls - -There are 3 bug sites: `_chat_client.py:209`, `_agent.py:201`, and the `_shared.py:201–202` short-circuit that would still discard headers even if the first two were patched alone. - ---- - ---- - -## Next Steps - -- Reproduction confirmed — proceed to investigation and fix phase -- Review the failing test or evidence above to understand the root cause -- The issue branch is checked out in the worktree: `/repos/agent-framework/.worktrees/agent/fix-5416-1` \ No newline at end of file diff --git a/python/packages/openai/tests/openai/test_openai_shared.py b/python/packages/openai/tests/openai/test_openai_shared.py index e20cc715d9..155646fad1 100644 --- a/python/packages/openai/tests/openai/test_openai_shared.py +++ b/python/packages/openai/tests/openai/test_openai_shared.py @@ -82,9 +82,9 @@ async def async_provider() -> str: def test_load_openai_service_settings_applies_default_headers_to_prebuilt_client() -> None: """When a pre-built client is provided, default_headers must be applied to it. - Regression for #5416: load_openai_service_settings used to early-return the - pre-built client without applying merged_headers, silently dropping any - custom headers the caller passed. + load_openai_service_settings used to early-return the pre-built client + without applying merged_headers, silently dropping any custom headers the + caller passed. """ pre_built = MagicMock() new_client = MagicMock() From 77f70c91268742efd646be79ca140c5f6aef4e19 Mon Sep 17 00:00:00 2001 From: Copilot Date: Fri, 24 Apr 2026 06:47:36 +0000 Subject: [PATCH 4/5] fix(openai): patch APP_INFO in test to avoid telemetry-disabled flakiness (#5416) The test_load_openai_service_settings_no_headers_still_applies_app_info test was implicitly relying on APP_INFO being non-None. When a developer runs with AGENT_FRAMEWORK_USER_AGENT_DISABLED=true, APP_INFO is None, merged_headers stays empty, with_options is never called, and the assert_called_once() assertion fails. Fix: wrap the load_openai_service_settings call in so the test is deterministic regardless of environment configuration. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../openai/tests/openai/test_openai_shared.py | 29 ++++++++++--------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/python/packages/openai/tests/openai/test_openai_shared.py b/python/packages/openai/tests/openai/test_openai_shared.py index 155646fad1..476c394d07 100644 --- a/python/packages/openai/tests/openai/test_openai_shared.py +++ b/python/packages/openai/tests/openai/test_openai_shared.py @@ -117,20 +117,21 @@ def test_load_openai_service_settings_no_headers_still_applies_app_info() -> Non new_client = MagicMock() pre_built.with_options.return_value = new_client - _, client, _ = load_openai_service_settings( - model="gpt-4o", - api_key=None, - credential=None, - org_id=None, - base_url=None, - endpoint=None, - api_version=None, - default_azure_api_version="2024-05-01-preview", - default_headers=None, - client=pre_built, - env_file_path=None, - env_file_encoding=None, - ) + with patch("agent_framework_openai._shared.APP_INFO", {"User-Agent": "test-agent"}): + _, client, _ = load_openai_service_settings( + model="gpt-4o", + api_key=None, + credential=None, + org_id=None, + base_url=None, + endpoint=None, + api_version=None, + default_azure_api_version="2024-05-01-preview", + default_headers=None, + client=pre_built, + env_file_path=None, + env_file_encoding=None, + ) pre_built.with_options.assert_called_once() assert client is new_client From 7ce747df25222afd50f068807302fee1d88fb888 Mon Sep 17 00:00:00 2001 From: Copilot Date: Fri, 24 Apr 2026 07:00:57 +0000 Subject: [PATCH 5/5] Address review feedback for #5416: Python: [Bug]: FoundryChatClient(default_headers=...) and FoundryAgent(default_headers=...) silently drop custom headers --- python/packages/openai/tests/openai/test_openai_shared.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/python/packages/openai/tests/openai/test_openai_shared.py b/python/packages/openai/tests/openai/test_openai_shared.py index 476c394d07..3a148626a5 100644 --- a/python/packages/openai/tests/openai/test_openai_shared.py +++ b/python/packages/openai/tests/openai/test_openai_shared.py @@ -117,7 +117,10 @@ def test_load_openai_service_settings_no_headers_still_applies_app_info() -> Non new_client = MagicMock() pre_built.with_options.return_value = new_client - with patch("agent_framework_openai._shared.APP_INFO", {"User-Agent": "test-agent"}): + with ( + patch("agent_framework_openai._shared.APP_INFO", {"agent-framework-version": "python/test-version"}), + patch("agent_framework._telemetry.AGENT_FRAMEWORK_USER_AGENT", "agent-framework-python/test-version"), + ): _, client, _ = load_openai_service_settings( model="gpt-4o", api_key=None, @@ -134,4 +137,6 @@ def test_load_openai_service_settings_no_headers_still_applies_app_info() -> Non ) pre_built.with_options.assert_called_once() + call_kwargs = pre_built.with_options.call_args.kwargs + assert call_kwargs.get("default_headers", {}).get("User-Agent") == "agent-framework-python/test-version" assert client is new_client