Merge pull request #34531 from BerriAI/litellm_forward_client_headers_responses_api

fix(responses): forward client headers to the provider on /v1/responses
This commit is contained in:
Mateo Wang 2026-08-03 20:02:21 -07:00 • committed by GitHub
commit f60e99c583
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 89 additions and 0 deletions

View file

@ -918,6 +918,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:

View file

@ -98,6 +98,28 @@ class ResponsesAPIRequestUtils:
)
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.
Header names are compared case-insensitively, as HTTP defines them.
"""
if not client_headers:
return extra_headers
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(
supported_params: list[str] | None,

View file

@ -307,3 +307,63 @@ 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"
@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"