fix(router): stamp actual retry count on exception when retry_policy sets retries to 0

When retry_policy (e.g. TimeoutErrorRetries=0) prevents retries, the router
raises immediately without entering the retry loop, so the post-loop setattr
that records actual retry counts was never reached. Meanwhile the @client
decorator in utils.py unconditionally injected litellm.num_retries (e.g. 3)
onto the exception, causing misleading "LiteLLM Retried: 3 times" in error
messages even though zero retries occurred.

Changes:
- router.py: stamp max_retries=0 / num_retries=0 on the exception in the
  else-raise path so error messages always reflect the real retry count
- utils.py: skip the num_retries injection for router/proxy calls; the router
  already manages retry counts itself via the post-loop setattr
- exceptions.py: change `if self.num_retries` / `if self.max_retries` to
  `is not None` checks so a count of 0 is displayed rather than suppressed

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
jungraekim-gif 2026-06-08 13:36:23 +08:00
parent 3d2b8fed32
commit 672698c5eb
3 changed files with 83 additions and 67 deletions

View file

@ -60,17 +60,17 @@ class AuthenticationError(openai.AuthenticationError): # type: ignore
def __str__(self):
_message = self.message
if self.num_retries:
if self.num_retries is not None:
_message += f" LiteLLM Retried: {self.num_retries} times"
if self.max_retries:
if self.max_retries is not None:
_message += f", LiteLLM Max Retries: {self.max_retries}"
return _message
def __repr__(self):
_message = self.message
if self.num_retries:
if self.num_retries is not None:
_message += f" LiteLLM Retried: {self.num_retries} times"
if self.max_retries:
if self.max_retries is not None:
_message += f", LiteLLM Max Retries: {self.max_retries}"
return _message
@ -106,17 +106,17 @@ class NotFoundError(openai.NotFoundError): # type: ignore
def __str__(self):
_message = self.message
if self.num_retries:
if self.num_retries is not None:
_message += f" LiteLLM Retried: {self.num_retries} times"
if self.max_retries:
if self.max_retries is not None:
_message += f", LiteLLM Max Retries: {self.max_retries}"
return _message
def __repr__(self):
_message = self.message
if self.num_retries:
if self.num_retries is not None:
_message += f" LiteLLM Retried: {self.num_retries} times"
if self.max_retries:
if self.max_retries is not None:
_message += f", LiteLLM Max Retries: {self.max_retries}"
return _message
@ -157,17 +157,17 @@ class BadRequestError(openai.BadRequestError): # type: ignore
def __str__(self):
_message = self.message
if self.num_retries:
if self.num_retries is not None:
_message += f" LiteLLM Retried: {self.num_retries} times"
if self.max_retries:
if self.max_retries is not None:
_message += f", LiteLLM Max Retries: {self.max_retries}"
return _message
def __repr__(self):
_message = self.message
if self.num_retries:
if self.num_retries is not None:
_message += f" LiteLLM Retried: {self.num_retries} times"
if self.max_retries:
if self.max_retries is not None:
_message += f", LiteLLM Max Retries: {self.max_retries}"
return _message
@ -220,17 +220,17 @@ class UnprocessableEntityError(openai.UnprocessableEntityError): # type: ignore
def __str__(self):
_message = self.message
if self.num_retries:
if self.num_retries is not None:
_message += f" LiteLLM Retried: {self.num_retries} times"
if self.max_retries:
if self.max_retries is not None:
_message += f", LiteLLM Max Retries: {self.max_retries}"
return _message
def __repr__(self):
_message = self.message
if self.num_retries:
if self.num_retries is not None:
_message += f" LiteLLM Retried: {self.num_retries} times"
if self.max_retries:
if self.max_retries is not None:
_message += f", LiteLLM Max Retries: {self.max_retries}"
return _message
@ -266,17 +266,17 @@ class Timeout(openai.APITimeoutError): # type: ignore
# custom function to convert to str
def __str__(self):
_message = self.message
if self.num_retries:
if self.num_retries is not None:
_message += f" LiteLLM Retried: {self.num_retries} times"
if self.max_retries:
if self.max_retries is not None:
_message += f", LiteLLM Max Retries: {self.max_retries}"
return _message
def __repr__(self):
_message = self.message
if self.num_retries:
if self.num_retries is not None:
_message += f" LiteLLM Retried: {self.num_retries} times"
if self.max_retries:
if self.max_retries is not None:
_message += f", LiteLLM Max Retries: {self.max_retries}"
return _message
@ -305,17 +305,17 @@ class PermissionDeniedError(openai.PermissionDeniedError): # type: ignore
def __str__(self):
_message = self.message
if self.num_retries:
if self.num_retries is not None:
_message += f" LiteLLM Retried: {self.num_retries} times"
if self.max_retries:
if self.max_retries is not None:
_message += f", LiteLLM Max Retries: {self.max_retries}"
return _message
def __repr__(self):
_message = self.message
if self.num_retries:
if self.num_retries is not None:
_message += f" LiteLLM Retried: {self.num_retries} times"
if self.max_retries:
if self.max_retries is not None:
_message += f", LiteLLM Max Retries: {self.max_retries}"
return _message
@ -357,17 +357,17 @@ class RateLimitError(openai.RateLimitError): # type: ignore
def __str__(self):
_message = self.message
if self.num_retries:
if self.num_retries is not None:
_message += f" LiteLLM Retried: {self.num_retries} times"
if self.max_retries:
if self.max_retries is not None:
_message += f", LiteLLM Max Retries: {self.max_retries}"
return _message
def __repr__(self):
_message = self.message
if self.num_retries:
if self.num_retries is not None:
_message += f" LiteLLM Retried: {self.num_retries} times"
if self.max_retries:
if self.max_retries is not None:
_message += f", LiteLLM Max Retries: {self.max_retries}"
return _message
@ -399,17 +399,17 @@ class ContextWindowExceededError(BadRequestError): # type: ignore
def __str__(self):
_message = self.message
if self.num_retries:
if self.num_retries is not None:
_message += f" LiteLLM Retried: {self.num_retries} times"
if self.max_retries:
if self.max_retries is not None:
_message += f", LiteLLM Max Retries: {self.max_retries}"
return _message
def __repr__(self):
_message = self.message
if self.num_retries:
if self.num_retries is not None:
_message += f" LiteLLM Retried: {self.num_retries} times"
if self.max_retries:
if self.max_retries is not None:
_message += f", LiteLLM Max Retries: {self.max_retries}"
return _message
@ -442,17 +442,17 @@ class RejectedRequestError(BadRequestError): # type: ignore
def __str__(self):
_message = self.message
if self.num_retries:
if self.num_retries is not None:
_message += f" LiteLLM Retried: {self.num_retries} times"
if self.max_retries:
if self.max_retries is not None:
_message += f", LiteLLM Max Retries: {self.max_retries}"
return _message
def __repr__(self):
_message = self.message
if self.num_retries:
if self.num_retries is not None:
_message += f" LiteLLM Retried: {self.num_retries} times"
if self.max_retries:
if self.max_retries is not None:
_message += f", LiteLLM Max Retries: {self.max_retries}"
return _message
@ -495,9 +495,9 @@ class ContentPolicyViolationError(BadRequestError): # type: ignore
Transform the error to a string
"""
_message = self.message
if self.num_retries:
if self.num_retries is not None:
_message += f" LiteLLM Retried: {self.num_retries} times"
if self.max_retries:
if self.max_retries is not None:
_message += f", LiteLLM Max Retries: {self.max_retries}"
return _message
@ -537,17 +537,17 @@ class ServiceUnavailableError(openai.APIStatusError): # type: ignore
def __str__(self):
_message = self.message
if self.num_retries:
if self.num_retries is not None:
_message += f" LiteLLM Retried: {self.num_retries} times"
if self.max_retries:
if self.max_retries is not None:
_message += f", LiteLLM Max Retries: {self.max_retries}"
return _message
def __repr__(self):
_message = self.message
if self.num_retries:
if self.num_retries is not None:
_message += f" LiteLLM Retried: {self.num_retries} times"
if self.max_retries:
if self.max_retries is not None:
_message += f", LiteLLM Max Retries: {self.max_retries}"
return _message
@ -587,17 +587,17 @@ class BadGatewayError(openai.APIStatusError): # type: ignore
def __str__(self):
_message = self.message
if self.num_retries:
if self.num_retries is not None:
_message += f" LiteLLM Retried: {self.num_retries} times"
if self.max_retries:
if self.max_retries is not None:
_message += f", LiteLLM Max Retries: {self.max_retries}"
return _message
def __repr__(self):
_message = self.message
if self.num_retries:
if self.num_retries is not None:
_message += f" LiteLLM Retried: {self.num_retries} times"
if self.max_retries:
if self.max_retries is not None:
_message += f", LiteLLM Max Retries: {self.max_retries}"
return _message
@ -637,17 +637,17 @@ class InternalServerError(openai.InternalServerError): # type: ignore
def __str__(self):
_message = self.message
if self.num_retries:
if self.num_retries is not None:
_message += f" LiteLLM Retried: {self.num_retries} times"
if self.max_retries:
if self.max_retries is not None:
_message += f", LiteLLM Max Retries: {self.max_retries}"
return _message
def __repr__(self):
_message = self.message
if self.num_retries:
if self.num_retries is not None:
_message += f" LiteLLM Retried: {self.num_retries} times"
if self.max_retries:
if self.max_retries is not None:
_message += f", LiteLLM Max Retries: {self.max_retries}"
return _message
@ -678,17 +678,17 @@ class APIError(openai.APIError): # type: ignore
def __str__(self):
_message = self.message
if self.num_retries:
if self.num_retries is not None:
_message += f" LiteLLM Retried: {self.num_retries} times"
if self.max_retries:
if self.max_retries is not None:
_message += f", LiteLLM Max Retries: {self.max_retries}"
return _message
def __repr__(self):
_message = self.message
if self.num_retries:
if self.num_retries is not None:
_message += f" LiteLLM Retried: {self.num_retries} times"
if self.max_retries:
if self.max_retries is not None:
_message += f", LiteLLM Max Retries: {self.max_retries}"
return _message
@ -717,17 +717,17 @@ class APIConnectionError(openai.APIConnectionError): # type: ignore
def __str__(self):
_message = self.message
if self.num_retries:
if self.num_retries is not None:
_message += f" LiteLLM Retried: {self.num_retries} times"
if self.max_retries:
if self.max_retries is not None:
_message += f", LiteLLM Max Retries: {self.max_retries}"
return _message
def __repr__(self):
_message = self.message
if self.num_retries:
if self.num_retries is not None:
_message += f" LiteLLM Retried: {self.num_retries} times"
if self.max_retries:
if self.max_retries is not None:
_message += f", LiteLLM Max Retries: {self.max_retries}"
return _message
@ -755,17 +755,17 @@ class APIResponseValidationError(openai.APIResponseValidationError): # type: ig
def __str__(self):
_message = self.message
if self.num_retries:
if self.num_retries is not None:
_message += f" LiteLLM Retried: {self.num_retries} times"
if self.max_retries:
if self.max_retries is not None:
_message += f", LiteLLM Max Retries: {self.max_retries}"
return _message
def __repr__(self):
_message = self.message
if self.num_retries:
if self.num_retries is not None:
_message += f" LiteLLM Retried: {self.num_retries} times"
if self.max_retries:
if self.max_retries is not None:
_message += f", LiteLLM Max Retries: {self.max_retries}"
return _message
@ -1005,9 +1005,9 @@ class MidStreamFallbackError(ServiceUnavailableError): # type: ignore
def __str__(self):
_message = self.message
if self.num_retries:
if self.num_retries is not None:
_message += f" LiteLLM Retried: {self.num_retries} times"
if self.max_retries:
if self.max_retries is not None:
_message += f", LiteLLM Max Retries: {self.max_retries}"
if self.original_exception:
_message += f" Original exception: {type(self.original_exception).__name__}: {str(self.original_exception)}"

View file

@ -5792,6 +5792,12 @@ class Router:
if num_retries > 0:
kwargs = self.log_retry(kwargs=kwargs, e=original_exception)
else:
# No retries will occur; stamp the exception with the actual counts
# (0) before raising so error messages reflect reality instead of
# carrying a stale value injected by the @client decorator.
if type(original_exception) in litellm.LITELLM_EXCEPTION_TYPES:
setattr(original_exception, "max_retries", 0)
setattr(original_exception, "num_retries", 0)
raise
verbose_router_logger.debug(

View file

@ -2088,9 +2088,19 @@ def client(original_function): # noqa: PLR0915
except Exception:
pass
setattr(
e, "num_retries", num_retries
) ## IMPORTANT: returns the deployment's num_retries to the router
# Only set num_retries on the exception for non-router calls.
# When the router is in use, it manages retry counts itself and stamps
# the actual attempted count via setattr after the retry loop completes.
# Injecting litellm.num_retries here for router calls causes misleading
# "LiteLLM Retried: N times" in error messages even when zero retries
# occurred (e.g. TimeoutErrorRetries=0 in retry_policy).
_is_router_or_proxy_call = "model_group" in (
kwargs.get("metadata") or {}
)
if not _is_router_or_proxy_call:
setattr(
e, "num_retries", num_retries
) ## returns the deployment's num_retries for non-router retry logic
timeout = _get_wrapper_timeout(kwargs=kwargs, exception=e)
setattr(e, "timeout", timeout)