fix: forward retry-after header from upstream providers to clients

When upstream LLM providers return 429 responses with a retry-after
header, the proxy only forwarded it as llm_provider-retry-after.
The bare retry-after header was missing, which prevented clients
using the OpenAI SDK from respecting the provider's retry timing.

Preserve the retry-after header as-is in get_response_headers(),
following the same pattern used for x-ratelimit-* headers.

Fixes #21553
This commit is contained in:
hzt 2026-02-20 13:00:17 +08:00
parent 37c98f8325
commit a7d59070c3
3 changed files with 90 additions and 0 deletions

View file

@ -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}

View file

@ -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"

View file

@ -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"