mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-10 03:28:53 +00:00
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:
commit
f60e99c583
3 changed files with 89 additions and 0 deletions
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue