Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 23 additions & 5 deletions litellm/responses/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -653,6 +653,18 @@ def _pop_use_chat_completions_api_kw(kwargs: dict[str, Any]) -> bool:
return bool(use_cc)


def _merge_forwarded_client_headers(
extra_headers: dict[str, Any] | None,
kwargs: dict[str, Any],
) -> dict[str, Any] | None:
"""Merge the proxy's forwarded client headers (`headers` kwarg) into `extra_headers`."""
client_headers = kwargs.get("headers")
return ResponsesAPIRequestUtils.merge_client_forwarded_headers(
extra_headers=extra_headers,
client_headers=client_headers if isinstance(client_headers, dict) else None,
)


def _resolve_model_provider_for_responses(
model: str,
custom_llm_provider: str | None,
Expand Down Expand Up @@ -918,11 +930,7 @@ 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,
)
extra_headers = _merge_forwarded_client_headers(extra_headers, kwargs)
local_vars["extra_headers"] = extra_headers

# Convert text_format to text parameter if provided
Expand Down Expand Up @@ -1241,6 +1249,8 @@ def delete_responses(
litellm_logging_obj: LiteLLMLoggingObj = kwargs.get("litellm_logging_obj") # type: ignore
litellm_call_id: str | None = kwargs.get("litellm_call_id", None)
_is_async = kwargs.pop("adelete_responses", False) is True
extra_headers = _merge_forwarded_client_headers(extra_headers, kwargs)
local_vars["extra_headers"] = extra_headers

# get llm provider logic
litellm_params = GenericLiteLLMParams(**kwargs)
Expand Down Expand Up @@ -1412,6 +1422,8 @@ def get_responses(
litellm_logging_obj: LiteLLMLoggingObj = kwargs.get("litellm_logging_obj") # type: ignore
litellm_call_id: str | None = kwargs.get("litellm_call_id", None)
_is_async = kwargs.pop("aget_responses", False) is True
extra_headers = _merge_forwarded_client_headers(extra_headers, kwargs)
local_vars["extra_headers"] = extra_headers

# get llm provider logic
litellm_params = GenericLiteLLMParams(**kwargs)
Expand Down Expand Up @@ -1561,6 +1573,8 @@ def list_input_items(
litellm_logging_obj: LiteLLMLoggingObj = kwargs.get("litellm_logging_obj") # type: ignore
litellm_call_id: str | None = kwargs.get("litellm_call_id", None)
_is_async = kwargs.pop("alist_input_items", False) is True
extra_headers = _merge_forwarded_client_headers(extra_headers, kwargs)
local_vars["extra_headers"] = extra_headers

litellm_params = GenericLiteLLMParams(**kwargs)

Expand Down Expand Up @@ -1705,6 +1719,8 @@ def cancel_responses(
litellm_logging_obj: LiteLLMLoggingObj = kwargs.get("litellm_logging_obj") # type: ignore
litellm_call_id: str | None = kwargs.get("litellm_call_id", None)
_is_async = kwargs.pop("acancel_responses", False) is True
extra_headers = _merge_forwarded_client_headers(extra_headers, kwargs)
local_vars["extra_headers"] = extra_headers

# get llm provider logic
litellm_params = GenericLiteLLMParams(**kwargs)
Expand Down Expand Up @@ -1877,6 +1893,8 @@ def compact_responses(
litellm_logging_obj: LiteLLMLoggingObj = kwargs.get("litellm_logging_obj") # type: ignore
litellm_call_id: str | None = kwargs.get("litellm_call_id", None)
_is_async = kwargs.pop("acompact_responses", False) is True
extra_headers = _merge_forwarded_client_headers(extra_headers, kwargs)
local_vars["extra_headers"] = extra_headers

# get llm provider logic
litellm_params = GenericLiteLLMParams(**kwargs)
Expand Down
66 changes: 66 additions & 0 deletions tests/test_litellm/responses/test_responses_api_request_body.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,11 @@ def __init__(self, json_data, status_code=200):
def json(self):
return self._json_data

def raise_for_status(self) -> None:
if self.status_code >= 400:
request = httpx.Request("GET", "http://mock")
raise httpx.HTTPStatusError(self.text, request=request, response=httpx.Response(self.status_code))


def _assert_request_body_matches(request_body: dict, expected_body: dict) -> None:
for key, expected_value in expected_body.items():
Expand Down Expand Up @@ -367,3 +372,64 @@ async def test_aresponses_client_header_conflict_is_case_insensitive():

assert [name for name in request_headers if name.lower() == "x-shared"] == ["x-shared"]
assert request_headers["x-shared"] == "from-caller"


_MANAGEMENT_ROUTES = (
(litellm.aget_responses, "get", _minimal_responses_api_payload("resp_123", "gpt-4o")),
(litellm.adelete_responses, "delete", {"id": "resp_123", "object": "response", "deleted": True}),
(litellm.acancel_responses, "post", _minimal_responses_api_payload("resp_123", "gpt-4o")),
(litellm.alist_input_items, "get", {"object": "list", "data": [], "has_more": False}),
)


@pytest.mark.parametrize("route,http_method,payload", _MANAGEMENT_ROUTES)
@pytest.mark.asyncio
async def test_responses_management_routes_forward_client_headers_to_provider(route, http_method, payload):
"""
The response management routes must forward the proxy's `headers` kwarg
(`forward_client_headers_to_llm_api`) to the provider, like creating a response does.
"""
with patch(
f"litellm.llms.custom_httpx.http_handler.AsyncHTTPHandler.{http_method}",
new_callable=AsyncMock,
) as mock_request:
mock_request.return_value = MockResponse(payload, 200)

await route(
response_id="resp_123",
custom_llm_provider="openai",
api_key="fake-api-key",
headers={"x-my-new-header": "hello-from-client"},
)

mock_request.assert_called_once()
request_headers = dict(mock_request.call_args.kwargs["headers"])

assert request_headers["x-my-new-header"] == "hello-from-client"


@pytest.mark.parametrize("route,http_method,payload", _MANAGEMENT_ROUTES)
@pytest.mark.asyncio
async def test_responses_management_routes_extra_headers_win_over_client_headers(route, http_method, payload):
"""
Explicit `extra_headers` beat the forwarded client headers on the management routes too,
case-insensitively.
"""
with patch(
f"litellm.llms.custom_httpx.http_handler.AsyncHTTPHandler.{http_method}",
new_callable=AsyncMock,
) as mock_request:
mock_request.return_value = MockResponse(payload, 200)

await route(
response_id="resp_123",
custom_llm_provider="openai",
api_key="fake-api-key",
headers={"X-Shared": "from-client"},
extra_headers={"x-shared": "from-caller"},
)

request_headers = dict(mock_request.call_args.kwargs["headers"])

assert [name for name in request_headers if name.lower() == "x-shared"] == ["x-shared"]
assert request_headers["x-shared"] == "from-caller"
Loading