diff --git a/litellm/litellm_core_utils/llm_response_utils/get_headers.py b/litellm/litellm_core_utils/llm_response_utils/get_headers.py index cd49b5a4a87..287d22d0348 100644 --- a/litellm/litellm_core_utils/llm_response_utils/get_headers.py +++ b/litellm/litellm_core_utils/llm_response_utils/get_headers.py @@ -35,6 +35,8 @@ def get_response_headers(_response_headers: Optional[dict] = None) -> dict: openai_headers["x-ratelimit-remaining-tokens"] = _response_headers[ "x-ratelimit-remaining-tokens" ] + if "retry-after" in _response_headers: + openai_headers["retry-after"] = _response_headers["retry-after"] llm_provider_headers = _get_llm_provider_headers(_response_headers) return {**llm_provider_headers, **openai_headers} diff --git a/tests/llm_translation/test_llm_response_utils/test_get_headers.py b/tests/llm_translation/test_llm_response_utils/test_get_headers.py index f0cc7ca61f1..e7efc9d1d1c 100644 --- a/tests/llm_translation/test_llm_response_utils/test_get_headers.py +++ b/tests/llm_translation/test_llm_response_utils/test_get_headers.py @@ -77,3 +77,42 @@ def test_get_llm_provider_headers(): } result = _get_llm_provider_headers(input_headers) assert result == expected_output, "Unexpected output for _get_llm_provider_headers" + +def test_get_response_headers_with_retry_after(): + """ + When upstream provider returns retry-after header (e.g. on 429), + it should be forwarded as both 'retry-after' and 'llm_provider-retry-after'. + + Ref: https://github.com/BerriAI/litellm/issues/21553 + """ + input_headers = { + "retry-after": "57", + "x-ratelimit-limit-tokens": "100000", + "content-type": "application/json", + } + result = get_response_headers(input_headers) + + # retry-after should be preserved as a bare header + assert result.get("retry-after") == "57" + # retry-after should also be present as llm_provider-retry-after + assert result.get("llm_provider-retry-after") == "57" + # OpenAI headers should still work + assert result.get("x-ratelimit-limit-tokens") == "100000" + # Other headers should only be prefixed + assert result.get("llm_provider-content-type") == "application/json" + assert "content-type" not in result + + +def test_get_response_headers_without_retry_after(): + """ + When upstream provider does NOT return retry-after header, + the output should not contain a retry-after key. + """ + input_headers = { + "x-ratelimit-limit-tokens": "100000", + "content-type": "application/json", + } + result = get_response_headers(input_headers) + + assert "retry-after" not in result + assert result.get("x-ratelimit-limit-tokens") == "100000" diff --git a/tests/test_litellm/test_exception_header_preservation.py b/tests/test_litellm/test_exception_header_preservation.py index d3e33fa13b3..0a73bfbaa8c 100644 --- a/tests/test_litellm/test_exception_header_preservation.py +++ b/tests/test_litellm/test_exception_header_preservation.py @@ -268,3 +268,52 @@ class TestProxyHeaderExtraction: # Verify headers are extracted and prefixed correctly assert headers.get("llm_provider-x-request-id") == "req-abc123" assert headers.get("llm_provider-x-ms-region") == "eastus" + + def test_proxy_forwards_retry_after_from_upstream_429(self): + """ + When upstream provider returns 429 with retry-after header, + proxy should forward it as a bare 'retry-after' header (not just + 'llm_provider-retry-after'). + + Ref: https://github.com/BerriAI/litellm/issues/21553 + """ + from litellm.litellm_core_utils.llm_response_utils.get_headers import ( + get_response_headers, + ) + from litellm.exceptions import RateLimitError + + mock_response = httpx.Response( + status_code=429, + headers={ + "retry-after": "57", + "x-ratelimit-limit-tokens": "100000", + "x-ratelimit-remaining-tokens": "0", + "content-type": "application/json", + }, + request=httpx.Request( + "POST", "https://api.openai.com/v1/chat/completions" + ), + ) + error = RateLimitError( + message="Rate limit exceeded", + model="gpt-4", + llm_provider="azure", + response=mock_response, + ) + + # Simulate proxy header extraction logic from + # common_request_processing.py _handle_llm_api_exception + headers = getattr(error, "headers", None) or {} + if not headers: + _response = getattr(error, "response", None) + if _response is not None: + _response_headers = getattr(_response, "headers", None) + if _response_headers: + headers = get_response_headers(dict(_response_headers)) + + # The bare retry-after header should be present for OpenAI SDK compat + assert headers.get("retry-after") == "57" + # The prefixed version should also be present + assert headers.get("llm_provider-retry-after") == "57" + # OpenAI-compatible rate limit headers should be preserved + assert headers.get("x-ratelimit-limit-tokens") == "100000"