From fb9d30224b1d885fac17708144417b092fa6b51f Mon Sep 17 00:00:00 2001 From: milan Date: Fri, 24 Jul 2026 19:04:22 +0000 Subject: [PATCH 1/2] fix(responses): forward proxy client headers to the provider Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- litellm/responses/main.py | 7 +++ litellm/responses/utils.py | 15 +++++++ .../test_responses_api_request_body.py | 45 +++++++++++++++++++ 3 files changed, 67 insertions(+) diff --git a/litellm/responses/main.py b/litellm/responses/main.py index 944cf58df1c6..aa5da40a02a9 100644 --- a/litellm/responses/main.py +++ b/litellm/responses/main.py @@ -927,6 +927,13 @@ def responses( _is_async = kwargs.pop("aresponses", False) is True use_chat_completions_api = _pop_use_chat_completions_api_kw(kwargs) + client_headers = kwargs.get("headers") + extra_headers = ResponsesAPIRequestUtils.merge_client_forwarded_headers( + extra_headers=extra_headers, + client_headers=client_headers if isinstance(client_headers, dict) else None, + ) + local_vars["extra_headers"] = extra_headers + # Convert text_format to text parameter if provided text = ResponsesAPIRequestUtils.convert_text_format_to_text_param(text_format=text_format, text=text) if text is not None: diff --git a/litellm/responses/utils.py b/litellm/responses/utils.py index 12c890ec91d9..096103b3faec 100644 --- a/litellm/responses/utils.py +++ b/litellm/responses/utils.py @@ -89,6 +89,21 @@ def merge_prompt_management_input( ) return [*merged_input] + @staticmethod + def merge_client_forwarded_headers( + extra_headers: dict[str, Any] | None, + client_headers: dict[str, str] | None, + ) -> dict[str, Any] | None: + """ + Merge headers forwarded by the proxy (`headers` kwarg, set when + `forward_client_headers_to_llm_api` is enabled) into `extra_headers`. + + `extra_headers` wins on conflicts, since it is set explicitly by the caller. + """ + if not client_headers: + return extra_headers + return {**client_headers, **(extra_headers or {})} + @staticmethod def _check_valid_arg( supported_params: Optional[List[str]], diff --git a/tests/test_litellm/responses/test_responses_api_request_body.py b/tests/test_litellm/responses/test_responses_api_request_body.py index 44dfa240d423..0d067a99b97b 100644 --- a/tests/test_litellm/responses/test_responses_api_request_body.py +++ b/tests/test_litellm/responses/test_responses_api_request_body.py @@ -259,3 +259,48 @@ async def test_aresponses_bedrock_mantle_service_tier_raises_without_drop_params mock_post.assert_not_called() assert "drop_params" in str(excinfo.value) assert "priority" in str(excinfo.value) + + +async def _aresponses_and_get_request_headers(**request_kwargs) -> dict: + with patch( + "litellm.llms.custom_httpx.http_handler.AsyncHTTPHandler.post", + new_callable=AsyncMock, + ) as mock_post: + mock_post.return_value = MockResponse(_minimal_responses_api_payload("resp_headers_test", "gpt-4o"), 200) + + await litellm.aresponses( + model="openai/gpt-4o", + api_key="fake-api-key", + input="hi", + **request_kwargs, + ) + + mock_post.assert_called_once() + return dict(mock_post.call_args.kwargs["headers"]) + + +@pytest.mark.asyncio +async def test_aresponses_forwards_client_headers_kwarg_to_provider(): + """ + The proxy passes client headers it forwards (`forward_client_headers_to_llm_api`) + as a `headers` kwarg; those must reach the provider request. + """ + request_headers = await _aresponses_and_get_request_headers(headers={"x-my-new-header": "hello-from-client"}) + + assert request_headers["x-my-new-header"] == "hello-from-client" + + +@pytest.mark.asyncio +async def test_aresponses_merges_client_headers_with_extra_headers(): + """ + A `headers` kwarg and an explicit `extra_headers` are merged, with + `extra_headers` winning on conflicts. + """ + request_headers = await _aresponses_and_get_request_headers( + headers={"x-my-new-header": "hello-from-client", "x-shared": "from-client"}, + extra_headers={"x-explicit": "from-caller", "x-shared": "from-caller"}, + ) + + assert request_headers["x-my-new-header"] == "hello-from-client" + assert request_headers["x-explicit"] == "from-caller" + assert request_headers["x-shared"] == "from-caller" From 7716e47519426bda2ba55791e9518a0a5459a209 Mon Sep 17 00:00:00 2001 From: milan Date: Fri, 24 Jul 2026 20:16:25 +0000 Subject: [PATCH 2/2] fix(responses): compare forwarded header names case-insensitively Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- litellm/responses/utils.py | 9 ++++++++- .../responses/test_responses_api_request_body.py | 15 +++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/litellm/responses/utils.py b/litellm/responses/utils.py index 096103b3faec..25a355edbc16 100644 --- a/litellm/responses/utils.py +++ b/litellm/responses/utils.py @@ -99,10 +99,17 @@ def merge_client_forwarded_headers( `forward_client_headers_to_llm_api` is enabled) into `extra_headers`. `extra_headers` wins on conflicts, since it is set explicitly by the caller. + Header names are compared case-insensitively, as HTTP defines them. """ if not client_headers: return extra_headers - return {**client_headers, **(extra_headers or {})} + if not extra_headers: + return dict(client_headers) + explicit_names = frozenset(name.lower() for name in extra_headers) + return { + **{name: value for name, value in client_headers.items() if name.lower() not in explicit_names}, + **extra_headers, + } @staticmethod def _check_valid_arg( diff --git a/tests/test_litellm/responses/test_responses_api_request_body.py b/tests/test_litellm/responses/test_responses_api_request_body.py index 0d067a99b97b..1c217d1a67f9 100644 --- a/tests/test_litellm/responses/test_responses_api_request_body.py +++ b/tests/test_litellm/responses/test_responses_api_request_body.py @@ -304,3 +304,18 @@ async def test_aresponses_merges_client_headers_with_extra_headers(): assert request_headers["x-my-new-header"] == "hello-from-client" assert request_headers["x-explicit"] == "from-caller" assert request_headers["x-shared"] == "from-caller" + + +@pytest.mark.asyncio +async def test_aresponses_client_header_conflict_is_case_insensitive(): + """ + HTTP header names are case-insensitive, so a differently cased client header + must not survive alongside the explicit `extra_headers` value. + """ + request_headers = await _aresponses_and_get_request_headers( + headers={"X-Shared": "from-client"}, + extra_headers={"x-shared": "from-caller"}, + ) + + assert [name for name in request_headers if name.lower() == "x-shared"] == ["x-shared"] + assert request_headers["x-shared"] == "from-caller"