From 8577d63ff5212173aa5b30bdfe8118b666c74179 Mon Sep 17 00:00:00 2001 From: yassin Date: Sun, 13 Sep 2026 00:14:49 +0000 Subject: [PATCH] fix(proxy): forward provider request id headers on mapped error responses Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- litellm/proxy/common_request_processing.py | 10 +++--- .../proxy/test_common_request_processing.py | 35 +++++++++++++++++++ 2 files changed, 39 insertions(+), 6 deletions(-) diff --git a/litellm/proxy/common_request_processing.py b/litellm/proxy/common_request_processing.py index e6ed60ba177..9b8a98ca0e7 100644 --- a/litellm/proxy/common_request_processing.py +++ b/litellm/proxy/common_request_processing.py @@ -3445,15 +3445,13 @@ class ProxyBaseLLMRequestProcessing: # a failed request reports no timing, matching /v1/chat/completions read_timing_from_logging_obj=False, ) - # Extract headers from exception - check both e.headers and e.response.headers headers = getattr(e, "headers", None) or {} if not headers: - # Try to get headers from e.response.headers (httpx.Response) _response: Final = attribute_of(e, "response") - if _response is not None: - _response_headers: Final = getattr(_response, "headers", None) - if _response_headers: - headers = get_response_headers(dict(_response_headers)) + _response_headers: Final = getattr(_response, "headers", None) if _response is not None else None + _provider_headers: Final = _response_headers or getattr(e, "litellm_response_headers", None) + if _provider_headers: + headers = get_response_headers(dict(_provider_headers)) headers.update(custom_headers) # Call response headers hook for failure diff --git a/tests/test_litellm/proxy/test_common_request_processing.py b/tests/test_litellm/proxy/test_common_request_processing.py index efbb5eedad4..45e436f756a 100644 --- a/tests/test_litellm/proxy/test_common_request_processing.py +++ b/tests/test_litellm/proxy/test_common_request_processing.py @@ -8386,6 +8386,41 @@ async def test_handle_llm_api_exception_forwards_provider_headers_on_http_status assert exc_info.value.headers["llm_provider-x-amzn-requestid"] == "req-passthrough-500" +@pytest.mark.asyncio +async def test_handle_llm_api_exception_forwards_litellm_response_headers_when_response_is_synthetic(): + """Exception mapping hands the proxy a mapped error whose ``response`` is a synthetic empty + ``httpx.Response`` and parks the provider's real headers on ``litellm_response_headers``. + The client must still get the provider request id, as it does on a 200. + """ + import httpx + + from litellm.proxy._types import ProxyException, UserAPIKeyAuth + + mapped = litellm.BadRequestError( + message="OpenAIException - max_tokens is too large: 999999999.", + model="gpt-4o-mini", + llm_provider="openai", + ) + mapped.litellm_response_headers = httpx.Headers({"x-request-id": "req_openai_400"}) + assert dict(mapped.response.headers) == {} + + processor = ProxyBaseLLMRequestProcessing(data={}) + proxy_logging_obj = MagicMock() + proxy_logging_obj.post_call_failure_hook = AsyncMock(return_value=None) + proxy_logging_obj.post_call_response_headers_hook = AsyncMock(return_value={}) + + with pytest.raises(ProxyException) as exc_info: + await processor._handle_llm_api_exception( + e=mapped, + user_api_key_dict=UserAPIKeyAuth(api_key="sk-test"), + proxy_logging_obj=proxy_logging_obj, + ) + + assert exc_info.value.code == "400" + assert "max_tokens is too large: 999999999." in exc_info.value.message + assert exc_info.value.headers["llm_provider-x-request-id"] == "req_openai_400" + + class TestBackgroundResponseRetrievalGovernance: """LIT-7175: retrieving a background Response attaches the model's post_call policy pipelines."""