From 6103ca9303dad25272a8c5d3449769457d1fb344 Mon Sep 17 00:00:00 2001 From: panjianxing <3207880464@qq.com> Date: Thu, 8 Oct 2026 10:01:55 +0800 Subject: [PATCH 1/8] fix(retries): honor provider retry-after in completion --- litellm/main.py | 39 ++++++++++++++++++++++++++------- litellm/utils.py | 16 ++++++++++---- tests/unit/test_main.py | 48 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 91 insertions(+), 12 deletions(-) diff --git a/litellm/main.py b/litellm/main.py index 5c2e4d2b2a1..fd4a5c46258 100644 --- a/litellm/main.py +++ b/litellm/main.py @@ -32,6 +32,7 @@ from litellm._logging import redact_string from litellm._uuid import uuid if TYPE_CHECKING: + import tenacity from aiohttp import ClientSession import dotenv @@ -160,6 +161,7 @@ from litellm.utils import ( TextCompletionStreamWrapper, TranscriptionResponse, Usage, + _get_retry_after_from_exception_header, add_provider_specific_params_to_optional_params, async_mock_completion_streaming_obj, convert_to_model_response_object, @@ -6109,6 +6111,25 @@ def completion( ) +def _completion_retry_wait( + retry_state: "tenacity.RetryCallState", + fallback_wait: Callable[["tenacity.RetryCallState"], float], +) -> float: + outcome: Final = retry_state.outcome + error: Final = outcome.exception() if outcome is not None else None + if isinstance(error, Exception): + headers: Final = ( + getattr(getattr(error, "response", None), "headers", None) + or getattr(error, "litellm_response_headers", None) + or getattr(error, "headers", None) + ) + if headers is not None: + retry_after: Final = _get_retry_after_from_exception_header(headers) + if 0 < retry_after <= 60: + return retry_after + return fallback_wait(retry_state) + + def completion_with_retries(*args, **kwargs): """ Executes a litellm.completion() with 3 retries @@ -6124,14 +6145,16 @@ def completion_with_retries(*args, **kwargs): kwargs["num_retries"] = 0 retry_strategy: Final[RetryStrategy] = kwargs.pop("retry_strategy", "constant_retry") original_function: Final = kwargs.pop("original_function", completion) - if retry_strategy == "exponential_backoff_retry": - retryer = tenacity.Retrying( - wait=tenacity.wait_exponential(multiplier=1, max=10), - stop=tenacity.stop_after_attempt(num_retries), - reraise=True, - ) - else: - retryer = tenacity.Retrying(stop=tenacity.stop_after_attempt(num_retries), reraise=True) + fallback_wait: Final = ( + tenacity.wait_exponential(multiplier=1, max=10) + if retry_strategy == "exponential_backoff_retry" + else tenacity.wait_fixed(0) + ) + retryer = tenacity.Retrying( + wait=partial(_completion_retry_wait, fallback_wait=fallback_wait), + stop=tenacity.stop_after_attempt(num_retries), + reraise=True, + ) return retryer(original_function, *args, **kwargs) diff --git a/litellm/utils.py b/litellm/utils.py index dec57245ab2..e21193a2382 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -7251,7 +7251,7 @@ _should_retry = should_retry def _get_retry_after_from_exception_header( response_headers: httpx.Headers | None = None, -): +) -> float: """ Reimplementation of openai's calculate retry after, since that one can't be imported. https://github.com/openai/openai-python/blob/af67cfab4210d8e497c05390ce14f39105c77519/src/openai/_base_client.py#L631 @@ -7264,16 +7264,24 @@ def _get_retry_after_from_exception_header( # ". See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After#syntax for # details. if response_headers is not None: - retry_header: Final[str] = response_headers.get("retry-after") + retry_ms_header: Final = response_headers.get("retry-after-ms") try: - retry_after = int(retry_header) + return float(retry_ms_header) / 1000 + except (TypeError, ValueError): + pass + + retry_header: Final[str | None] = response_headers.get("retry-after") + if retry_header is None: + return -1.0 + try: + retry_after = float(retry_header) except Exception: retry_date_tuple: Final = email.utils.parsedate_tz(retry_header) if retry_date_tuple is None: retry_after = -1 else: retry_date: Final = email.utils.mktime_tz(retry_date_tuple) - retry_after = int(retry_date - time.time()) + retry_after = float(retry_date - time.time()) else: retry_after = -1 diff --git a/tests/unit/test_main.py b/tests/unit/test_main.py index c1b72bf8ff1..a0123440d1e 100644 --- a/tests/unit/test_main.py +++ b/tests/unit/test_main.py @@ -2575,6 +2575,54 @@ def throw_retryable_error(*_, **__): raise RuntimeError("BOOM") +@pytest.mark.parametrize( + ("headers", "retry_strategy", "expected_wait"), + [ + ({"retry-after": "20"}, "constant_retry", 20), + ({"retry-after-ms": "1500"}, "exponential_backoff_retry", 1.5), + ({"retry-after-ms": "1500", "retry-after": "20"}, "constant_retry", 1.5), + ({"retry-after-ms": "invalid", "retry-after": "2.5"}, "constant_retry", 2.5), + ({"retry-after": "60"}, "constant_retry", 60), + ({"retry-after": "61"}, "exponential_backoff_retry", 1), + ({"retry-after": "invalid"}, "constant_retry", 0), + ({}, "constant_retry", 0), + ], +) +def test_completion_retries_respect_provider_wait( + headers: dict[str, str], retry_strategy: str, expected_wait: float +) -> None: + response = httpx.Response( + status_code=429, + headers=headers, + request=httpx.Request("POST", "https://example.com/v1/chat/completions"), + ) + error = litellm.RateLimitError( + message="rate limited", + llm_provider="openai", + model="test-model", + response=response, + headers={"x-request-id": "request-1"}, + ) + calls: list[dict[str, object]] = [] + + def completion_stub(**kwargs: object) -> str: + calls.append(kwargs) + if len(calls) == 1: + raise error + return "recovered" + + with patch("tenacity.nap.time.sleep") as sleep: + result = litellm_main.completion_with_retries( + num_retries=2, + retry_strategy=retry_strategy, + original_function=completion_stub, + ) + + assert result == "recovered" + assert len(calls) == 2 + sleep.assert_called_once_with(expected_wait) + + @pytest.mark.asyncio async def test_retrying() -> None: litellm.num_retries = 10 From 164f5387710a3ee704b522825206c3438e365549 Mon Sep 17 00:00:00 2001 From: panjianxing <3207880464@qq.com> Date: Thu, 8 Oct 2026 10:56:37 +0800 Subject: [PATCH 2/8] fix(retries): honor first provider delay in completion --- litellm/main.py | 29 ++++++++++++++++++++--------- litellm/utils.py | 1 + tests/unit/test_main.py | 30 ++++++++++++++++++++++++++++++ 3 files changed, 51 insertions(+), 9 deletions(-) diff --git a/litellm/main.py b/litellm/main.py index fd4a5c46258..0c92bc384fd 100644 --- a/litellm/main.py +++ b/litellm/main.py @@ -6111,6 +6111,18 @@ def completion( ) +def _completion_retry_after(error: Exception) -> float | None: + headers: Final = ( + getattr(getattr(error, "response", None), "headers", None) + or getattr(error, "litellm_response_headers", None) + or getattr(error, "headers", None) + ) + if headers is None: + return None + retry_after: Final = _get_retry_after_from_exception_header(headers) + return retry_after if 0 < retry_after <= 60 else None + + def _completion_retry_wait( retry_state: "tenacity.RetryCallState", fallback_wait: Callable[["tenacity.RetryCallState"], float], @@ -6118,15 +6130,9 @@ def _completion_retry_wait( outcome: Final = retry_state.outcome error: Final = outcome.exception() if outcome is not None else None if isinstance(error, Exception): - headers: Final = ( - getattr(getattr(error, "response", None), "headers", None) - or getattr(error, "litellm_response_headers", None) - or getattr(error, "headers", None) - ) - if headers is not None: - retry_after: Final = _get_retry_after_from_exception_header(headers) - if 0 < retry_after <= 60: - return retry_after + retry_after: Final = _completion_retry_after(error) + if retry_after is not None: + return retry_after return fallback_wait(retry_state) @@ -6140,6 +6146,7 @@ def completion_with_retries(*args, **kwargs): raise Exception(f"tenacity import failed please run `pip install tenacity`. Error{e}") num_retries: Final = kwargs.pop("num_retries", 3) + initial_exception: Final = kwargs.pop("_initial_retry_exception", None) # reset retries in .completion() kwargs["max_retries"] = 0 kwargs["num_retries"] = 0 @@ -6155,6 +6162,10 @@ def completion_with_retries(*args, **kwargs): stop=tenacity.stop_after_attempt(num_retries), reraise=True, ) + if isinstance(initial_exception, Exception): + initial_retry_after: Final = _completion_retry_after(initial_exception) + if initial_retry_after is not None: + retryer.sleep(initial_retry_after) return retryer(original_function, *args, **kwargs) diff --git a/litellm/utils.py b/litellm/utils.py index e21193a2382..a95c08ef8d9 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -1937,6 +1937,7 @@ def client(original_function): or isinstance(e, openai.APIConnectionError) ): kwargs["num_retries"] = num_retries + kwargs["_initial_retry_exception"] = e return litellm.completion_with_retries(*args, **kwargs) elif ( isinstance(e, litellm.exceptions.ContextWindowExceededError) diff --git a/tests/unit/test_main.py b/tests/unit/test_main.py index a0123440d1e..9d369bb9d9c 100644 --- a/tests/unit/test_main.py +++ b/tests/unit/test_main.py @@ -2623,6 +2623,36 @@ def test_completion_retries_respect_provider_wait( sleep.assert_called_once_with(expected_wait) +def test_completion_first_retry_respects_provider_wait() -> None: + from litellm.utils import client + + response = httpx.Response( + status_code=429, + headers={"retry-after": "2"}, + request=httpx.Request("POST", "https://example.com/v1/chat/completions"), + ) + error = litellm.RateLimitError( + message="rate limited", llm_provider="openai", model="gpt-4o-mini", response=response + ) + attempts: list[int] = [] + + def completion(**kwargs: object) -> None: + attempts.append(1) + raise error + + with patch("tenacity.nap.time.sleep") as sleep, pytest.raises(litellm.RateLimitError): + client(completion)( + model="gpt-4o-mini", + messages=[{"role": "user", "content": "hi"}], + num_retries=1, + original_function=completion, + litellm_logging_obj=_completion_logging_obj("first-retry"), + ) + + assert len(attempts) == 2 + sleep.assert_called_once_with(2.0) + + @pytest.mark.asyncio async def test_retrying() -> None: litellm.num_retries = 10 From 28371ba4f004ba6c694c936700362d7286a5f16d Mon Sep 17 00:00:00 2001 From: panjianxing <3207880464@qq.com> Date: Fri, 9 Oct 2026 16:07:41 +0800 Subject: [PATCH 3/8] test(retries): follow immutable regression test conventions --- tests/unit/test_main.py | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/tests/unit/test_main.py b/tests/unit/test_main.py index 9d369bb9d9c..2c3af5eb468 100644 --- a/tests/unit/test_main.py +++ b/tests/unit/test_main.py @@ -2591,56 +2591,56 @@ def throw_retryable_error(*_, **__): def test_completion_retries_respect_provider_wait( headers: dict[str, str], retry_strategy: str, expected_wait: float ) -> None: - response = httpx.Response( + response: Final = httpx.Response( status_code=429, headers=headers, request=httpx.Request("POST", "https://example.com/v1/chat/completions"), ) - error = litellm.RateLimitError( + error: Final = litellm.RateLimitError( message="rate limited", llm_provider="openai", model="test-model", response=response, headers={"x-request-id": "request-1"}, ) - calls: list[dict[str, object]] = [] + outcomes: Final = iter((error, "recovered")) + sleep: Final = MagicMock() def completion_stub(**kwargs: object) -> str: - calls.append(kwargs) - if len(calls) == 1: - raise error - return "recovered" + outcome: Final = next(outcomes) + if isinstance(outcome, Exception): + raise outcome + return outcome - with patch("tenacity.nap.time.sleep") as sleep: - result = litellm_main.completion_with_retries( + with patch("tenacity.nap.time.sleep", sleep): + result: Final = litellm_main.completion_with_retries( num_retries=2, retry_strategy=retry_strategy, original_function=completion_stub, ) assert result == "recovered" - assert len(calls) == 2 sleep.assert_called_once_with(expected_wait) def test_completion_first_retry_respects_provider_wait() -> None: from litellm.utils import client - response = httpx.Response( + response: Final = httpx.Response( status_code=429, headers={"retry-after": "2"}, request=httpx.Request("POST", "https://example.com/v1/chat/completions"), ) - error = litellm.RateLimitError( + error: Final = litellm.RateLimitError( message="rate limited", llm_provider="openai", model="gpt-4o-mini", response=response ) - attempts: list[int] = [] + errors: Final = iter((error, error)) + sleep: Final = MagicMock() def completion(**kwargs: object) -> None: - attempts.append(1) - raise error + raise next(errors) - with patch("tenacity.nap.time.sleep") as sleep, pytest.raises(litellm.RateLimitError): + with patch("tenacity.nap.time.sleep", sleep), pytest.raises(litellm.RateLimitError): client(completion)( model="gpt-4o-mini", messages=[{"role": "user", "content": "hi"}], @@ -2649,7 +2649,7 @@ def test_completion_first_retry_respects_provider_wait() -> None: litellm_logging_obj=_completion_logging_obj("first-retry"), ) - assert len(attempts) == 2 + assert next(errors, None) is None sleep.assert_called_once_with(2.0) From 3878c1a1e403098dc94c98c64c84cc061c8480cc Mon Sep 17 00:00:00 2001 From: panjianxing <3207880464@qq.com> Date: Fri, 9 Oct 2026 16:19:16 +0800 Subject: [PATCH 4/8] fix(retries): avoid mutating retry kwargs --- litellm/main.py | 31 ++++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/litellm/main.py b/litellm/main.py index 0c92bc384fd..8f35cc652d9 100644 --- a/litellm/main.py +++ b/litellm/main.py @@ -6145,19 +6145,32 @@ def completion_with_retries(*args, **kwargs): except Exception as e: raise Exception(f"tenacity import failed please run `pip install tenacity`. Error{e}") - num_retries: Final = kwargs.pop("num_retries", 3) - initial_exception: Final = kwargs.pop("_initial_retry_exception", None) - # reset retries in .completion() - kwargs["max_retries"] = 0 - kwargs["num_retries"] = 0 - retry_strategy: Final[RetryStrategy] = kwargs.pop("retry_strategy", "constant_retry") - original_function: Final = kwargs.pop("original_function", completion) + num_retries: Final = kwargs.get("num_retries", 3) + initial_exception: Final = kwargs.get("_initial_retry_exception") + retry_strategy: Final[RetryStrategy] = kwargs.get("retry_strategy", "constant_retry") + original_function: Final = kwargs.get("original_function", completion) + completion_kwargs: Final = { + **{ + key: value + for key, value in kwargs.items() + if key + not in ( + "num_retries", + "_initial_retry_exception", + "retry_strategy", + "original_function", + "max_retries", + ) + }, + "max_retries": 0, + "num_retries": 0, + } fallback_wait: Final = ( tenacity.wait_exponential(multiplier=1, max=10) if retry_strategy == "exponential_backoff_retry" else tenacity.wait_fixed(0) ) - retryer = tenacity.Retrying( + retryer: Final = tenacity.Retrying( wait=partial(_completion_retry_wait, fallback_wait=fallback_wait), stop=tenacity.stop_after_attempt(num_retries), reraise=True, @@ -6166,7 +6179,7 @@ def completion_with_retries(*args, **kwargs): initial_retry_after: Final = _completion_retry_after(initial_exception) if initial_retry_after is not None: retryer.sleep(initial_retry_after) - return retryer(original_function, *args, **kwargs) + return retryer(original_function, *args, **completion_kwargs) async def acompletion_with_retries(*args, **kwargs): From b7714a4b32a3ff8137041b11181e45d1fb6a3294 Mon Sep 17 00:00:00 2001 From: panjianxing <3207880464@qq.com> Date: Fri, 9 Oct 2026 16:31:44 +0800 Subject: [PATCH 5/8] fix(retries): reject nonfinite provider delays --- litellm/utils.py | 44 +++++++++++++++++++++------------------- tests/unit/test_utils.py | 19 +++++++++++++++++ 2 files changed, 42 insertions(+), 21 deletions(-) diff --git a/litellm/utils.py b/litellm/utils.py index a95c08ef8d9..fbafb50095e 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -16,6 +16,7 @@ import io import itertools import json import logging +import math import os import random import re @@ -7264,32 +7265,33 @@ def _get_retry_after_from_exception_header( # # ". See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After#syntax for # details. - if response_headers is not None: - retry_ms_header: Final = response_headers.get("retry-after-ms") - try: - return float(retry_ms_header) / 1000 - except (TypeError, ValueError): - pass + if response_headers is None: + return -1.0 - retry_header: Final[str | None] = response_headers.get("retry-after") - if retry_header is None: - return -1.0 - try: - retry_after = float(retry_header) - except Exception: - retry_date_tuple: Final = email.utils.parsedate_tz(retry_header) - if retry_date_tuple is None: - retry_after = -1 - else: - retry_date: Final = email.utils.mktime_tz(retry_date_tuple) - retry_after = float(retry_date - time.time()) + retry_ms_header: Final = response_headers.get("retry-after-ms") + try: + retry_after_ms: Final = float(retry_ms_header) / 1000 + except (TypeError, ValueError): + pass else: - retry_after = -1 + if math.isfinite(retry_after_ms): + return retry_after_ms - return retry_after + retry_header: Final[str | None] = response_headers.get("retry-after") + if retry_header is None: + return -1.0 + try: + retry_after: Final = float(retry_header) + except (TypeError, ValueError): + retry_date_tuple: Final = email.utils.parsedate_tz(retry_header) + if retry_date_tuple is None: + return -1.0 + retry_date: Final = email.utils.mktime_tz(retry_date_tuple) + return float(retry_date - time.time()) + return retry_after if math.isfinite(retry_after) else -1.0 except Exception: - retry_after = -1 + return -1.0 def calculate_retry_after( diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index 58fd2250d2a..0126fc95ee6 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -117,6 +117,25 @@ def test_get_base_model_from_metadata_returns_unvalidated_root_value(): assert get_base_model_from_metadata({"litellm_params": {"base_model": 42}}) == 42 +@pytest.mark.parametrize( + ("headers", "expected"), + ( + ((("retry-after", "inf"),), -1.0), + ((("retry-after", "nan"),), -1.0), + ((("retry-after-ms", "inf"),), -1.0), + ((("retry-after-ms", "nan"), ("retry-after", "2")), 2.0), + ((("retry-after-ms", "1e309"), ("retry-after", "2")), 2.0), + ((("retry-after-ms", "1500"),), 1.5), + ), +) +def test_retry_after_parser_rejects_nonfinite_values( + headers: tuple[tuple[str, str], ...], expected: float +) -> None: + from litellm.utils import _get_retry_after_from_exception_header + + assert _get_retry_after_from_exception_header(httpx.Headers(dict(headers))) == expected + + # Adds the parent directory to the system path From 79a247408cc83fad2278123b362352116da38052 Mon Sep 17 00:00:00 2001 From: panjianxing <3207880464@qq.com> Date: Fri, 9 Oct 2026 16:54:38 +0800 Subject: [PATCH 6/8] fix(retries): pass initial error as explicit argument --- litellm/main.py | 39 +++++++++++++-------------------------- litellm/utils.py | 5 ++--- 2 files changed, 15 insertions(+), 29 deletions(-) diff --git a/litellm/main.py b/litellm/main.py index 8f35cc652d9..5ff591c28e6 100644 --- a/litellm/main.py +++ b/litellm/main.py @@ -161,7 +161,7 @@ from litellm.utils import ( TextCompletionStreamWrapper, TranscriptionResponse, Usage, - _get_retry_after_from_exception_header, + _get_retry_after_from_exception_header, # pyright: ignore[reportPrivateUsage] # shared internal retry parser add_provider_specific_params_to_optional_params, async_mock_completion_streaming_obj, convert_to_model_response_object, @@ -6119,7 +6119,9 @@ def _completion_retry_after(error: Exception) -> float | None: ) if headers is None: return None - retry_after: Final = _get_retry_after_from_exception_header(headers) + retry_after: Final = _get_retry_after_from_exception_header( + headers # pyright: ignore[reportAny] # provider exceptions expose untyped headers + ) return retry_after if 0 < retry_after <= 60 else None @@ -6136,7 +6138,7 @@ def _completion_retry_wait( return fallback_wait(retry_state) -def completion_with_retries(*args, **kwargs): +def completion_with_retries(*args, _initial_retry_exception: Exception | None = None, **kwargs): """ Executes a litellm.completion() with 3 retries """ @@ -6145,26 +6147,11 @@ def completion_with_retries(*args, **kwargs): except Exception as e: raise Exception(f"tenacity import failed please run `pip install tenacity`. Error{e}") - num_retries: Final = kwargs.get("num_retries", 3) - initial_exception: Final = kwargs.get("_initial_retry_exception") - retry_strategy: Final[RetryStrategy] = kwargs.get("retry_strategy", "constant_retry") - original_function: Final = kwargs.get("original_function", completion) - completion_kwargs: Final = { - **{ - key: value - for key, value in kwargs.items() - if key - not in ( - "num_retries", - "_initial_retry_exception", - "retry_strategy", - "original_function", - "max_retries", - ) - }, - "max_retries": 0, - "num_retries": 0, - } + num_retries: Final = kwargs.pop("num_retries", 3) + kwargs["max_retries"] = 0 + kwargs["num_retries"] = 0 + retry_strategy: Final[RetryStrategy] = kwargs.pop("retry_strategy", "constant_retry") + original_function: Final = kwargs.pop("original_function", completion) fallback_wait: Final = ( tenacity.wait_exponential(multiplier=1, max=10) if retry_strategy == "exponential_backoff_retry" @@ -6175,11 +6162,11 @@ def completion_with_retries(*args, **kwargs): stop=tenacity.stop_after_attempt(num_retries), reraise=True, ) - if isinstance(initial_exception, Exception): - initial_retry_after: Final = _completion_retry_after(initial_exception) + if _initial_retry_exception is not None: + initial_retry_after: Final = _completion_retry_after(_initial_retry_exception) if initial_retry_after is not None: retryer.sleep(initial_retry_after) - return retryer(original_function, *args, **completion_kwargs) + return retryer(original_function, *args, **kwargs) async def acompletion_with_retries(*args, **kwargs): diff --git a/litellm/utils.py b/litellm/utils.py index fbafb50095e..139965fc0e5 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -1938,8 +1938,7 @@ def client(original_function): or isinstance(e, openai.APIConnectionError) ): kwargs["num_retries"] = num_retries - kwargs["_initial_retry_exception"] = e - return litellm.completion_with_retries(*args, **kwargs) + return litellm.completion_with_retries(*args, _initial_retry_exception=e, **kwargs) elif ( isinstance(e, litellm.exceptions.ContextWindowExceededError) and context_window_fallback_dict @@ -7306,7 +7305,7 @@ def calculate_retry_after( jitter: Final = JITTER * random.random() # If the API asks us to wait a certain amount of time (and it's a reasonable amount), just do what it says. - if retry_after is not None and 0 < retry_after <= 60: + if 0 < retry_after <= 60: return retry_after + jitter # Calculate exponential backoff From 8f116dbf8bc1d801d95c06edfc615383cbf8d318 Mon Sep 17 00:00:00 2001 From: panjianxing <3207880464@qq.com> Date: Fri, 9 Oct 2026 17:17:52 +0800 Subject: [PATCH 7/8] fix(retries): type retry wait without optional Tenacity stubs --- litellm/main.py | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/litellm/main.py b/litellm/main.py index 5ff591c28e6..541fec9b24a 100644 --- a/litellm/main.py +++ b/litellm/main.py @@ -32,7 +32,6 @@ from litellm._logging import redact_string from litellm._uuid import uuid if TYPE_CHECKING: - import tenacity from aiohttp import ClientSession import dotenv @@ -6111,6 +6110,15 @@ def completion( ) +class _CompletionRetryOutcome(Protocol): + def exception(self) -> BaseException | None: ... + + +class _CompletionRetryState(Protocol): + @property + def outcome(self) -> _CompletionRetryOutcome | None: ... + + def _completion_retry_after(error: Exception) -> float | None: headers: Final = ( getattr(getattr(error, "response", None), "headers", None) @@ -6126,8 +6134,8 @@ def _completion_retry_after(error: Exception) -> float | None: def _completion_retry_wait( - retry_state: "tenacity.RetryCallState", - fallback_wait: Callable[["tenacity.RetryCallState"], float], + retry_state: _CompletionRetryState, + fallback_wait: Callable[[_CompletionRetryState], float], ) -> float: outcome: Final = retry_state.outcome error: Final = outcome.exception() if outcome is not None else None @@ -6138,6 +6146,10 @@ def _completion_retry_wait( return fallback_wait(retry_state) +def _completion_constant_wait(_: _CompletionRetryState) -> float: + return 0.0 + + def completion_with_retries(*args, _initial_retry_exception: Exception | None = None, **kwargs): """ Executes a litellm.completion() with 3 retries @@ -6152,10 +6164,10 @@ def completion_with_retries(*args, _initial_retry_exception: Exception | None = kwargs["num_retries"] = 0 retry_strategy: Final[RetryStrategy] = kwargs.pop("retry_strategy", "constant_retry") original_function: Final = kwargs.pop("original_function", completion) - fallback_wait: Final = ( + fallback_wait: Final[Callable[[_CompletionRetryState], float]] = ( tenacity.wait_exponential(multiplier=1, max=10) if retry_strategy == "exponential_backoff_retry" - else tenacity.wait_fixed(0) + else _completion_constant_wait ) retryer: Final = tenacity.Retrying( wait=partial(_completion_retry_wait, fallback_wait=fallback_wait), @@ -6165,7 +6177,7 @@ def completion_with_retries(*args, _initial_retry_exception: Exception | None = if _initial_retry_exception is not None: initial_retry_after: Final = _completion_retry_after(_initial_retry_exception) if initial_retry_after is not None: - retryer.sleep(initial_retry_after) + time.sleep(initial_retry_after) return retryer(original_function, *args, **kwargs) From 4cf456a65f0990daa27362954d396d377ed383c1 Mon Sep 17 00:00:00 2001 From: panjianxing <3207880464@qq.com> Date: Sat, 17 Oct 2026 18:22:40 +0800 Subject: [PATCH 8/8] fix(retries): narrow optional retry-after-ms header --- litellm/utils.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/litellm/utils.py b/litellm/utils.py index 156ed66abb2..c9265b27400 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -7357,13 +7357,14 @@ def _get_retry_after_from_exception_header( return -1.0 retry_ms_header: Final = response_headers.get("retry-after-ms") - try: - retry_after_ms: Final = float(retry_ms_header) / 1000 - except (TypeError, ValueError): - pass - else: - if math.isfinite(retry_after_ms): - return retry_after_ms + if retry_ms_header is not None: + try: + retry_after_ms: Final = float(retry_ms_header) / 1000 + except (TypeError, ValueError): + pass + else: + if math.isfinite(retry_after_ms): + return retry_after_ms retry_header: Final[str | None] = response_headers.get("retry-after") if retry_header is None: