fix(router): honor ServiceUnavailableErrorRetries and InternalServerErrorRetries in retry policy

This commit is contained in:
Shivam Rawat 2026-08-04 16:51:21 -07:00
parent e4fd790f1c
commit 902a93d10d
6 changed files with 145 additions and 0 deletions

View file

@ -8,7 +8,9 @@ from litellm.exceptions import (
AuthenticationError,
BadRequestError,
ContentPolicyViolationError,
InternalServerError,
RateLimitError,
ServiceUnavailableError,
Timeout,
)
from litellm.types.router import RetryPolicy
@ -26,6 +28,8 @@ def get_num_retries_from_retry_policy(
TimeoutErrorRetries: Optional[int] = None
RateLimitErrorRetries: Optional[int] = None
ContentPolicyViolationErrorRetries: Optional[int] = None
InternalServerErrorRetries: Optional[int] = None
ServiceUnavailableErrorRetries: Optional[int] = None
"""
# if we can find the exception then in the retry policy -> return the number of retries
@ -48,6 +52,10 @@ def get_num_retries_from_retry_policy(
and retry_policy.ContentPolicyViolationErrorRetries is not None
):
return retry_policy.ContentPolicyViolationErrorRetries
if isinstance(exception, ServiceUnavailableError) and retry_policy.ServiceUnavailableErrorRetries is not None:
return retry_policy.ServiceUnavailableErrorRetries
if isinstance(exception, InternalServerError) and retry_policy.InternalServerErrorRetries is not None:
return retry_policy.InternalServerErrorRetries
if isinstance(exception, BadRequestError) and retry_policy.BadRequestErrorRetries is not None:
return retry_policy.BadRequestErrorRetries

View file

@ -95,6 +95,7 @@ class RetryPolicy(BaseModel):
RateLimitErrorRetries: Optional[int] = None
ContentPolicyViolationErrorRetries: Optional[int] = None
InternalServerErrorRetries: Optional[int] = None
ServiceUnavailableErrorRetries: Optional[int] = None
class UpdateRouterConfig(BaseModel):

View file

@ -0,0 +1,102 @@
import litellm
from litellm.router_utils.get_retry_from_policy import (
get_num_retries_from_retry_policy,
)
from litellm.types.router import RetryPolicy
def _service_unavailable_error() -> litellm.ServiceUnavailableError:
return litellm.ServiceUnavailableError(
message="model is down",
llm_provider="openai",
model="gpt-5.6",
)
def _internal_server_error() -> litellm.InternalServerError:
return litellm.InternalServerError(
message="upstream 500",
llm_provider="openai",
model="gpt-5.6",
)
def test_service_unavailable_error_retries_honored():
policy = RetryPolicy(ServiceUnavailableErrorRetries=0)
assert (
get_num_retries_from_retry_policy(
exception=_service_unavailable_error(),
retry_policy=policy,
)
== 0
)
def test_service_unavailable_error_retries_nonzero():
policy = RetryPolicy(ServiceUnavailableErrorRetries=4)
assert (
get_num_retries_from_retry_policy(
exception=_service_unavailable_error(),
retry_policy=policy,
)
== 4
)
def test_internal_server_error_retries_honored():
policy = RetryPolicy(InternalServerErrorRetries=0)
assert (
get_num_retries_from_retry_policy(
exception=_internal_server_error(),
retry_policy=policy,
)
== 0
)
def test_service_unavailable_not_covered_by_internal_server_error_retries():
policy = RetryPolicy(InternalServerErrorRetries=0)
assert (
get_num_retries_from_retry_policy(
exception=_service_unavailable_error(),
retry_policy=policy,
)
is None
)
def test_internal_server_error_not_covered_by_service_unavailable_retries():
policy = RetryPolicy(ServiceUnavailableErrorRetries=0)
assert (
get_num_retries_from_retry_policy(
exception=_internal_server_error(),
retry_policy=policy,
)
is None
)
def test_service_unavailable_error_retries_from_dict_policy():
assert (
get_num_retries_from_retry_policy(
exception=_service_unavailable_error(),
retry_policy={"ServiceUnavailableErrorRetries": 0},
)
== 0
)
def test_service_unavailable_error_retries_from_model_group_policy():
assert (
get_num_retries_from_retry_policy(
exception=_service_unavailable_error(),
model_group="gpt-5.6",
model_group_retry_policy={"gpt-5.6": RetryPolicy(ServiceUnavailableErrorRetries=1)},
)
== 1
)

View file

@ -6574,3 +6574,34 @@ def test_model_info_is_active_for_environment_matrix(monkeypatch):
monkeypatch.delenv("LITELLM_ENVIRONMENT")
with pytest.raises(ValueError, match="LITELLM_ENVIRONMENT"):
model_info_is_active_for_environment(model_info={"supported_environments": ["production"]})
@pytest.mark.asyncio
@pytest.mark.parametrize("policy_retries,expected_calls", [(0, 1), (1, 2)])
async def test_router_retry_policy_service_unavailable_retries(policy_retries, expected_calls):
from litellm.types.router import RetryPolicy
router = litellm.Router(
model_list=[
{
"model_name": "gpt-5.6",
"litellm_params": {"model": "openai/gpt-5.6", "api_key": "fake-key"},
}
],
retry_policy=RetryPolicy(ServiceUnavailableErrorRetries=policy_retries),
disable_cooldowns=True,
)
error = litellm.ServiceUnavailableError(
message="model is down",
llm_provider="openai",
model="gpt-5.6",
)
with patch.object(litellm, "acompletion", AsyncMock(side_effect=error)) as mock_acompletion:
with pytest.raises(litellm.ServiceUnavailableError):
await router.acompletion(
model="gpt-5.6",
messages=[{"role": "user", "content": "hi"}],
)
assert mock_acompletion.call_count == expected_calls

View file

@ -30,6 +30,7 @@ const retryPolicyMap: Record<string, string> = {
"RateLimitError (429)": "RateLimitErrorRetries",
"ContentPolicyViolationError (400)": "ContentPolicyViolationErrorRetries",
"InternalServerError (500)": "InternalServerErrorRetries",
"ServiceUnavailableError (503)": "ServiceUnavailableErrorRetries",
};
const ModelRetrySettingsTab = ({

View file

@ -30947,6 +30947,8 @@ export interface components {
InternalServerErrorRetries?: number | null;
/** Ratelimiterrorretries */
RateLimitErrorRetries?: number | null;
/** Serviceunavailableerrorretries */
ServiceUnavailableErrorRetries?: number | null;
/** Timeouterrorretries */
TimeoutErrorRetries?: number | null;
};