litellm/tests/local_testing/test_router_retries.py
yuneng-jiang 6a0d03914c
test: drop the cwd-relative sys.path.insert calls from the test suite (#37802)
* test: drop the cwd-relative sys.path.insert calls from the test suite

TQ003 stands at 1,077 across 1,058 files, and 1,015 of them are the same shape:
sys.path.insert(0, os.path.abspath("../..")) and its deeper siblings. The
argument resolves against the working directory rather than the file, so from
the repo root, where every job runs pytest, it inserts the directory two levels
above the checkout. It has never pointed at litellm. The package is installed
into the environment anyway, which is what actually makes the import work, and
what the rule's message has said all along.

Removing them leaves 1,634 imports of sys and os with no remaining reference,
and those go too, except where another test module imports the name back out of
the file. The rest of TQ003 is 62 call sites that resolve against __file__ or a
variable, which are a different question and are left alone.

Collection is identical either way: 45,871 tests and the same 51 pre-existing
collection errors before and after, and ruff reports no new undefined name.

* test: drop the duplicate imports the sys.path sweep exposed to F811

* test(pre-call-utils): restore the os import the new bedrock tests need
2026-08-22 09:25:58 -07:00

1004 lines
32 KiB
Python

#### What this tests ####
# This tests calling router with fallback models
import asyncio
import os
import time
import traceback
import pytest
import httpx
import openai
import litellm
from litellm import Router
from litellm.integrations.custom_logger import CustomLogger
class MyCustomHandler(CustomLogger):
success: bool = False
failure: bool = False
previous_models: int = 0
def log_pre_api_call(self, model, messages, kwargs):
print(f"Pre-API Call")
print(
f"previous_models: {kwargs['litellm_params']['metadata'].get('previous_models', None)}"
)
self.previous_models = len(
kwargs["litellm_params"]["metadata"].get("previous_models", [])
) # {"previous_models": [{"model": litellm_model_name, "exception_type": AuthenticationError, "exception_string": <complete_traceback>}]}
print(f"self.previous_models: {self.previous_models}")
def log_post_api_call(self, kwargs, response_obj, start_time, end_time):
print(
f"Post-API Call - response object: {response_obj}; model: {kwargs['model']}"
)
def log_stream_event(self, kwargs, response_obj, start_time, end_time):
print(f"On Stream")
def async_log_stream_event(self, kwargs, response_obj, start_time, end_time):
print(f"On Stream")
def log_success_event(self, kwargs, response_obj, start_time, end_time):
print(f"On Success")
async def async_log_success_event(self, kwargs, response_obj, start_time, end_time):
print(f"On Success")
def log_failure_event(self, kwargs, response_obj, start_time, end_time):
print(f"On Failure")
"""
Test sync + async
- Authorization Errors
- Random API Error
"""
@pytest.mark.parametrize("sync_mode", [True, False])
@pytest.mark.parametrize("error_type", ["API Error", "Authorization Error"])
@pytest.mark.asyncio
async def test_router_retries_errors(sync_mode, error_type):
"""
- Auth Error -> 0 retries
- API Error -> 2 retries
"""
_api_key = (
"bad-key" if error_type == "Authorization Error" else os.getenv("AZURE_API_KEY")
)
print(f"_api_key: {_api_key}")
model_list = [
{
"model_name": "azure/gpt-3.5-turbo", # openai model name
"litellm_params": { # params for litellm completion/embedding call
"model": "azure/chatgpt-functioncalling",
"api_key": _api_key,
"api_version": os.getenv("AZURE_API_VERSION"),
"api_base": os.getenv("AZURE_API_BASE"),
},
"tpm": 240000,
"rpm": 1800,
},
{
"model_name": "azure/gpt-3.5-turbo", # openai model name
"litellm_params": { # params for litellm completion/embedding call
"model": "azure/chatgpt-functioncalling",
"api_key": _api_key,
"api_version": os.getenv("AZURE_API_VERSION"),
"api_base": os.getenv("AZURE_API_BASE"),
},
"tpm": 240000,
"rpm": 1800,
},
]
router = Router(model_list=model_list, set_verbose=True, debug_level="DEBUG")
customHandler = MyCustomHandler()
litellm.callbacks = [customHandler]
user_message = "Hello, how are you?"
messages = [{"content": user_message, "role": "user"}]
kwargs = {
"model": "azure/gpt-3.5-turbo",
"messages": messages,
"mock_response": (
None
if error_type == "Authorization Error"
else Exception("Invalid Request")
),
}
for _ in range(4):
response = await router.acompletion(
model="azure/gpt-3.5-turbo",
messages=messages,
mock_response="1st success to ensure deployment is healthy",
)
try:
if sync_mode:
response = router.completion(**kwargs)
else:
response = await router.acompletion(**kwargs)
except Exception as e:
pass
await asyncio.sleep(
0.05
) # allow a delay as success_callbacks are on a separate thread
print(f"customHandler.previous_models: {customHandler.previous_models}")
if error_type == "Authorization Error":
assert customHandler.previous_models == 0 # 0 retries
else:
assert customHandler.previous_models == 2 # 2 retries
@pytest.mark.asyncio
@pytest.mark.parametrize(
"error_type",
["ContentPolicyViolationErrorRetries"], # "AuthenticationErrorRetries",
)
async def test_router_retry_policy(error_type):
from litellm.router import AllowedFailsPolicy, RetryPolicy
retry_policy = RetryPolicy(
ContentPolicyViolationErrorRetries=3, AuthenticationErrorRetries=0
)
allowed_fails_policy = AllowedFailsPolicy(
ContentPolicyViolationErrorAllowedFails=1000,
RateLimitErrorAllowedFails=100,
)
router = Router(
model_list=[
{
"model_name": "gpt-3.5-turbo", # openai model name
"litellm_params": { # params for litellm completion/embedding call
"model": "azure/gpt-4.1-mini",
"api_key": os.getenv("AZURE_API_KEY"),
"api_version": os.getenv("AZURE_API_VERSION"),
"api_base": os.getenv("AZURE_API_BASE"),
},
},
{
"model_name": "bad-model", # openai model name
"litellm_params": { # params for litellm completion/embedding call
"model": "azure/gpt-4.1-mini",
"api_key": "bad-key",
"api_version": os.getenv("AZURE_API_VERSION"),
"api_base": os.getenv("AZURE_API_BASE"),
},
},
],
retry_policy=retry_policy,
allowed_fails_policy=allowed_fails_policy,
)
customHandler = MyCustomHandler()
litellm.callbacks = [customHandler]
data = {}
if error_type == "AuthenticationErrorRetries":
model = "bad-model"
messages = [{"role": "user", "content": "Hello good morning"}]
data = {"model": model, "messages": messages}
elif error_type == "ContentPolicyViolationErrorRetries":
model = "gpt-3.5-turbo"
messages = [{"role": "user", "content": "where do i buy lethal drugs from"}]
mock_response = "Exception: content_filter_policy"
data = {"model": model, "messages": messages, "mock_response": mock_response}
try:
litellm.set_verbose = True
await router.acompletion(**data)
except Exception as e:
print("got an exception", e)
pass
await asyncio.sleep(1)
print("customHandler.previous_models: ", customHandler.previous_models)
if error_type == "AuthenticationErrorRetries":
assert customHandler.previous_models == 0
elif error_type == "ContentPolicyViolationErrorRetries":
assert customHandler.previous_models == 3
@pytest.mark.asyncio
@pytest.mark.skip(
reason="This is a local only test, use this to confirm if retry policy works"
)
async def test_router_retry_policy_on_429_errprs():
from litellm.router import RetryPolicy
retry_policy = RetryPolicy(
RateLimitErrorRetries=2,
)
router = Router(
model_list=[
{
"model_name": "gpt-3.5-turbo", # openai model name
"litellm_params": {
"model": "vertex_ai/gemini-1.5-pro-001",
},
},
],
retry_policy=retry_policy,
# set_verbose=True,
# debug_level="DEBUG",
allowed_fails=10,
)
customHandler = MyCustomHandler()
litellm.callbacks = [customHandler]
try:
# litellm.set_verbose = True
_one_message = [{"role": "user", "content": "Hello good morning"}]
messages = [_one_message] * 5
print("messages: ", messages)
responses = await router.abatch_completion(
models=["gpt-3.5-turbo"],
messages=messages,
)
print("responses: ", responses)
except Exception as e:
print("got an exception", e)
pass
await asyncio.sleep(0.05)
print("customHandler.previous_models: ", customHandler.previous_models)
@pytest.mark.parametrize("model_group", ["gpt-3.5-turbo", "bad-model"])
@pytest.mark.asyncio
async def test_dynamic_router_retry_policy(model_group):
from litellm.router import RetryPolicy
model_group_retry_policy = {
"gpt-3.5-turbo": RetryPolicy(ContentPolicyViolationErrorRetries=2),
"bad-model": RetryPolicy(AuthenticationErrorRetries=0),
}
router = litellm.Router(
model_list=[
{
"model_name": "gpt-3.5-turbo", # openai model name
"litellm_params": { # params for litellm completion/embedding call
"model": "azure/gpt-4.1-mini",
"api_key": os.getenv("AZURE_API_KEY"),
"api_version": os.getenv("AZURE_API_VERSION"),
"api_base": os.getenv("AZURE_API_BASE"),
},
"model_info": {
"id": "model-0",
},
},
{
"model_name": "gpt-3.5-turbo", # openai model name
"litellm_params": { # params for litellm completion/embedding call
"model": "azure/gpt-4.1-mini",
"api_key": os.getenv("AZURE_API_KEY"),
"api_version": os.getenv("AZURE_API_VERSION"),
"api_base": os.getenv("AZURE_API_BASE"),
},
"model_info": {
"id": "model-1",
},
},
{
"model_name": "gpt-3.5-turbo", # openai model name
"litellm_params": { # params for litellm completion/embedding call
"model": "azure/gpt-4.1-mini",
"api_key": os.getenv("AZURE_API_KEY"),
"api_version": os.getenv("AZURE_API_VERSION"),
"api_base": os.getenv("AZURE_API_BASE"),
},
"model_info": {
"id": "model-2",
},
},
{
"model_name": "bad-model", # openai model name
"litellm_params": { # params for litellm completion/embedding call
"model": "azure/gpt-4.1-mini",
"api_key": "bad-key",
"api_version": os.getenv("AZURE_API_VERSION"),
"api_base": os.getenv("AZURE_API_BASE"),
},
},
],
model_group_retry_policy=model_group_retry_policy,
)
customHandler = MyCustomHandler()
litellm.callbacks = [customHandler]
data = {}
if model_group == "bad-model":
model = "bad-model"
messages = [{"role": "user", "content": "Hello good morning"}]
data = {"model": model, "messages": messages}
elif model_group == "gpt-3.5-turbo":
model = "gpt-3.5-turbo"
messages = [{"role": "user", "content": "where do i buy lethal drugs from"}]
data = {
"model": model,
"messages": messages,
"mock_response": "Exception: content_filter_policy",
}
try:
litellm.set_verbose = True
response = await router.acompletion(**data)
except Exception as e:
print("got an exception", e)
pass
await asyncio.sleep(0.05)
print("customHandler.previous_models: ", customHandler.previous_models)
if model_group == "bad-model":
assert customHandler.previous_models == 0
elif model_group == "gpt-3.5-turbo":
assert customHandler.previous_models == 2
"""
Unit Tests for Router Retry Logic
Test 1. Retry Rate Limit Errors when there are other healthy deployments
Test 2. Do not retry rate limit errors when - there are no fallbacks and no healthy deployments
"""
rate_limit_error = openai.RateLimitError(
message="Rate limit exceeded",
response=httpx.Response(
status_code=429,
request=httpx.Request(method="POST", url="https://api.openai.com/v1"),
),
body={
"error": {
"type": "rate_limit_exceeded",
"param": None,
"code": "rate_limit_exceeded",
}
},
)
def test_retry_rate_limit_error_with_healthy_deployments():
"""
Test 1. It SHOULD retry when there is a rate limit error and len(healthy_deployments) > 0
"""
healthy_deployments = [
"deployment1",
"deployment2",
] # multiple healthy deployments mocked up
router = litellm.Router(
model_list=[
{
"model_name": "gpt-3.5-turbo",
"litellm_params": {
"model": "azure/gpt-4.1-mini",
"api_key": os.getenv("AZURE_API_KEY"),
"api_version": os.getenv("AZURE_API_VERSION"),
"api_base": os.getenv("AZURE_API_BASE"),
},
}
]
)
# Act & Assert
try:
response = router.should_retry_this_error(
error=rate_limit_error, healthy_deployments=healthy_deployments
)
print("response from should_retry_this_error: ", response)
except Exception as e:
pytest.fail(
"Should not have raised an error, since there are healthy deployments. Raises",
e,
)
def test_do_retry_rate_limit_error_with_no_fallbacks_and_no_healthy_deployments():
"""
Test 2. It SHOULD NOT Retry, when healthy_deployments is [] and fallbacks is None
"""
healthy_deployments = []
router = Router(
model_list=[
{
"model_name": "gpt-3.5-turbo",
"litellm_params": {
"model": "azure/gpt-4.1-mini",
"api_key": os.getenv("AZURE_API_KEY"),
"api_version": os.getenv("AZURE_API_VERSION"),
"api_base": os.getenv("AZURE_API_BASE"),
},
}
]
)
# Act & Assert
try:
response = router.should_retry_this_error(
error=rate_limit_error, healthy_deployments=healthy_deployments
)
pytest.fail("Should have raised an error")
except Exception as e:
print("got an exception", e)
pass
def test_raise_context_window_exceeded_error():
"""
Trigger Context Window fallback, when context_window_fallbacks is not None
"""
context_window_error = litellm.ContextWindowExceededError(
message="Context window exceeded",
response=httpx.Response(
status_code=400,
request=httpx.Request(method="POST", url="https://api.openai.com/v1"),
),
llm_provider="azure",
model="gpt-3.5-turbo",
)
context_window_fallbacks = [{"gpt-3.5-turbo": ["azure/gpt-4.1-mini"]}]
router = Router(
model_list=[
{
"model_name": "gpt-3.5-turbo",
"litellm_params": {
"model": "azure/gpt-4.1-mini",
"api_key": os.getenv("AZURE_API_KEY"),
"api_version": os.getenv("AZURE_API_VERSION"),
"api_base": os.getenv("AZURE_API_BASE"),
},
}
]
)
try:
response = router.should_retry_this_error(
error=context_window_error,
healthy_deployments=None,
context_window_fallbacks=context_window_fallbacks,
)
pytest.fail(
"Expected to raise context window exceeded error -> trigger fallback"
)
except Exception as e:
pass
def test_raise_context_window_exceeded_error_no_retry():
"""
Do not Retry Context Window Exceeded Error, when context_window_fallbacks is None
"""
context_window_error = litellm.ContextWindowExceededError(
message="Context window exceeded",
response=httpx.Response(
status_code=400,
request=httpx.Request(method="POST", url="https://api.openai.com/v1"),
),
llm_provider="azure",
model="gpt-3.5-turbo",
)
context_window_fallbacks = None
router = Router(
model_list=[
{
"model_name": "gpt-3.5-turbo",
"litellm_params": {
"model": "azure/gpt-4.1-mini",
"api_key": os.getenv("AZURE_API_KEY"),
"api_version": os.getenv("AZURE_API_VERSION"),
"api_base": os.getenv("AZURE_API_BASE"),
},
}
]
)
try:
response = router.should_retry_this_error(
error=context_window_error,
healthy_deployments=None,
context_window_fallbacks=context_window_fallbacks,
)
assert (
response == True
), "Should not have raised exception since we do not have context window fallbacks"
except litellm.ContextWindowExceededError:
pass
## Unit test time to back off for router retries
"""
1. Timeout is 0.0 when RateLimit Error and healthy deployments are > 0
2. Timeout is 0.0 when RateLimit Error and fallbacks are > 0
3. Timeout is > 0.0 when RateLimit Error and healthy deployments == 0 and fallbacks == None
"""
@pytest.mark.parametrize("num_deployments, expected_timeout", [(1, 60), (2, 0.0)])
def test_timeout_for_rate_limit_error_with_healthy_deployments(
num_deployments, expected_timeout
):
"""
Test 1. Timeout is 0.0 when RateLimit Error and healthy deployments are > 0
"""
cooldown_time = 60
rate_limit_error = litellm.RateLimitError(
message="{RouterErrors.no_deployments_available.value}. 12345 Passed model={model_group}. Deployments={deployment_dict}",
llm_provider="",
model="gpt-3.5-turbo",
response=httpx.Response(
status_code=429,
content="",
headers={"retry-after": str(cooldown_time)}, # type: ignore
request=httpx.Request(method="tpm_rpm_limits", url="https://github.com/BerriAI/litellm"), # type: ignore
),
)
model_list = [
{
"model_name": "gpt-3.5-turbo",
"litellm_params": {
"model": "azure/gpt-4.1-mini",
"api_key": os.getenv("AZURE_API_KEY"),
"api_version": os.getenv("AZURE_API_VERSION"),
"api_base": os.getenv("AZURE_API_BASE"),
},
}
]
if num_deployments == 2:
model_list.append(
{
"model_name": "gpt-4",
"litellm_params": {"model": "gpt-3.5-turbo"},
}
)
router = litellm.Router(model_list=model_list)
_timeout = router._time_to_sleep_before_retry(
e=rate_limit_error,
remaining_retries=2,
num_retries=2,
healthy_deployments=[
{
"model_name": "gpt-4",
"litellm_params": {
"api_key": "my-key",
"api_base": "https://openai-gpt-4-test-v-1.openai.azure.com",
"model": "azure/gpt-4.1-mini",
},
"model_info": {
"id": "0e30bc8a63fa91ae4415d4234e231b3f9e6dd900cac57d118ce13a720d95e9d6",
"db_model": False,
},
}
],
all_deployments=model_list,
)
if expected_timeout == 0.0:
assert _timeout == expected_timeout
else:
assert _timeout > 0.0
def test_timeout_for_rate_limit_error_with_no_healthy_deployments():
"""
Test 2. Timeout is > 0.0 when RateLimit Error and healthy deployments == 0
"""
healthy_deployments = []
model_list = [
{
"model_name": "gpt-3.5-turbo",
"litellm_params": {
"model": "azure/gpt-4.1-mini",
"api_key": os.getenv("AZURE_API_KEY"),
"api_version": os.getenv("AZURE_API_VERSION"),
"api_base": os.getenv("AZURE_API_BASE"),
},
}
]
router = litellm.Router(model_list=model_list)
_timeout = router._time_to_sleep_before_retry(
e=rate_limit_error,
remaining_retries=4,
num_retries=4,
healthy_deployments=healthy_deployments,
all_deployments=model_list,
)
print(
"timeout=",
_timeout,
"error is rate_limit_error and there are no healthy deployments",
)
assert _timeout > 0.0
def test_no_retry_for_not_found_error_404():
healthy_deployments = []
router = Router(
model_list=[
{
"model_name": "gpt-3.5-turbo",
"litellm_params": {
"model": "azure/gpt-4.1-mini",
"api_key": os.getenv("AZURE_API_KEY"),
"api_version": os.getenv("AZURE_API_VERSION"),
"api_base": os.getenv("AZURE_API_BASE"),
},
}
]
)
# Act & Assert
error = litellm.NotFoundError(
message="404 model not found",
model="gpt-12",
llm_provider="azure",
)
try:
response = router.should_retry_this_error(
error=error, healthy_deployments=healthy_deployments
)
pytest.fail(
"Should have raised an exception 404 NotFoundError should never be retried, it's typically model_not_found error"
)
except Exception as e:
print("got exception", e)
def test_no_retry_for_bad_request_error_400():
"""
Test that 400 BadRequestError is NOT retried, even if healthy deployments exist.
This tests the fix for GitHub issue #19216.
"""
healthy_deployments = ["deployment1", "deployment2"] # Multiple healthy deployments
router = Router(
model_list=[
{
"model_name": "gpt-3.5-turbo",
"litellm_params": {
"model": "azure/gpt-4.1-mini",
"api_key": os.getenv("AZURE_API_KEY"),
"api_version": os.getenv("AZURE_API_VERSION"),
"api_base": os.getenv("AZURE_API_BASE"),
},
}
]
)
# Act & Assert
error = litellm.BadRequestError(
message="400 Invalid request parameters",
model="gpt-3.5-turbo",
llm_provider="azure",
)
try:
response = router.should_retry_this_error(
error=error, healthy_deployments=healthy_deployments
)
pytest.fail(
"Should have raised BadRequestError - 400 errors should never be retried"
)
except litellm.BadRequestError as e:
print("Correctly raised BadRequestError without retry:", e)
def test_no_retry_for_unprocessable_entity_error_422():
"""
Test that 422 UnprocessableEntityError is NOT retried, even if healthy deployments exist.
"""
healthy_deployments = ["deployment1", "deployment2"] # Multiple healthy deployments
router = Router(
model_list=[
{
"model_name": "gpt-3.5-turbo",
"litellm_params": {
"model": "azure/gpt-4.1-mini",
"api_key": os.getenv("AZURE_API_KEY"),
"api_version": os.getenv("AZURE_API_VERSION"),
"api_base": os.getenv("AZURE_API_BASE"),
},
}
]
)
# Act & Assert
error = litellm.UnprocessableEntityError(
message="422 Unprocessable Entity",
model="gpt-3.5-turbo",
llm_provider="azure",
response=httpx.Response(
status_code=422,
request=httpx.Request(method="POST", url="https://api.openai.com/v1"),
),
)
try:
response = router.should_retry_this_error(
error=error, healthy_deployments=healthy_deployments
)
pytest.fail(
"Should have raised UnprocessableEntityError - 422 errors should never be retried"
)
except litellm.UnprocessableEntityError as e:
print("Correctly raised UnprocessableEntityError without retry:", e)
internal_server_error = litellm.InternalServerError(
message="internal server error",
model="gpt-12",
llm_provider="azure",
)
rate_limit_error = litellm.RateLimitError(
message="rate limit error",
model="gpt-12",
llm_provider="azure",
)
service_unavailable_error = litellm.ServiceUnavailableError(
message="service unavailable error",
model="gpt-12",
llm_provider="azure",
)
timeout_error = litellm.Timeout(
message="timeout error",
model="gpt-12",
llm_provider="azure",
)
def test_no_retry_when_no_healthy_deployments():
healthy_deployments = []
router = Router(
model_list=[
{
"model_name": "gpt-3.5-turbo",
"litellm_params": {
"model": "azure/gpt-4.1-mini",
"api_key": os.getenv("AZURE_API_KEY"),
"api_version": os.getenv("AZURE_API_VERSION"),
"api_base": os.getenv("AZURE_API_BASE"),
},
}
]
)
for error in [
internal_server_error,
rate_limit_error,
service_unavailable_error,
timeout_error,
]:
try:
response = router.should_retry_this_error(
error=error, healthy_deployments=healthy_deployments
)
pytest.fail(
"Should have raised an exception, there's no point retrying an error when there are 0 healthy deployments"
)
except Exception as e:
print("got exception", e)
@pytest.mark.asyncio
async def test_router_retries_model_specific_and_global():
from unittest.mock import patch, MagicMock
litellm.num_retries = 0
router = Router(
model_list=[
{
"model_name": "gpt-3.5-turbo",
"litellm_params": {
"model": "gpt-3.5-turbo",
"api_key": os.getenv("OPENAI_API_KEY"),
"num_retries": 1,
},
}
]
)
with patch.object(
router, "_time_to_sleep_before_retry"
) as mock_async_function_with_retries:
try:
await router.acompletion(
model="gpt-3.5-turbo",
messages=[{"role": "user", "content": "Hello, how are you?"}],
mock_response="litellm.RateLimitError",
)
except Exception as e:
print("got exception", e)
mock_async_function_with_retries.assert_called_once()
assert mock_async_function_with_retries.call_args.kwargs["num_retries"] == 1
@pytest.mark.asyncio
async def test_router_timeout_model_specific_and_global():
from unittest.mock import patch, MagicMock
from litellm.llms.custom_httpx.http_handler import HTTPHandler
router = Router(
model_list=[
{
"model_name": "anthropic-claude",
"litellm_params": {
"model": f"anthropic/{os.environ.get('CI_CD_DEFAULT_ANTHROPIC_MODEL', 'claude-haiku-4-5-20251001')}",
"timeout": 1,
},
}
],
timeout=10,
)
client = HTTPHandler()
with patch.object(client, "post") as mock_client:
try:
await router.acompletion(
model="anthropic-claude",
messages=[{"role": "user", "content": "Hello, how are you?"}],
client=client,
)
except Exception as e:
print("got exception", e)
mock_client.assert_called()
assert mock_client.call_args.kwargs["timeout"] == 1
@pytest.mark.asyncio
async def test_router_retry_num_retries_tracking():
"""
Test that num_retries attribute is correctly set on exceptions when all retries are exhausted.
This verifies the fix for the bug where num_retries was incorrectly set to current_attempt
(0-indexed) instead of the actual number of retries attempted.
"""
from unittest.mock import AsyncMock, patch
router = Router(
model_list=[
{
"model_name": "gpt-3.5-turbo",
"litellm_params": {
"model": "gpt-3.5-turbo",
"api_key": os.getenv("OPENAI_API_KEY"),
},
}
],
num_retries=3, # Set at router level to ensure it's used
)
# Mock make_call to always raise a RateLimitError
async def mock_make_call(*args, **kwargs):
raise litellm.RateLimitError(
message="Rate limit exceeded",
model="gpt-3.5-turbo",
llm_provider="openai",
)
with patch.object(router, "make_call", side_effect=mock_make_call):
with patch.object(
router,
"_async_get_healthy_deployments",
return_value=(
[{"model_info": {"id": "test-id"}}],
[{"model_info": {"id": "test-id"}}],
),
):
with patch.object(
router, "_time_to_sleep_before_retry", return_value=0.01
): # Fast retries for testing
with pytest.raises(litellm.RateLimitError) as exc_info:
await router.acompletion(
model="gpt-3.5-turbo",
messages=[{"role": "user", "content": "Hello"}],
)
e = exc_info.value
assert hasattr(
e, "num_retries"
), "Exception should have num_retries attribute"
assert hasattr(
e, "max_retries"
), "Exception should have max_retries attribute"
assert (
e.num_retries == 3
), f"Expected num_retries to be 3, got {e.num_retries}"
assert (
e.max_retries == 3
), f"Expected max_retries to be 3, got {e.max_retries}"
# Verify the error message includes correct retry information
error_str = str(e)
assert (
"LiteLLM Retried: 3 times" in error_str
), f"Error message should indicate 3 retries: {error_str}"
assert (
"LiteLLM Max Retries: 3" in error_str
), f"Error message should show max retries: {error_str}"
@pytest.mark.asyncio
async def test_router_retry_num_retries_single_retry():
"""
Test num_retries tracking with a single retry to verify edge case handling.
"""
from unittest.mock import patch
router = Router(
model_list=[
{
"model_name": "gpt-3.5-turbo",
"litellm_params": {
"model": "gpt-3.5-turbo",
"api_key": os.getenv("OPENAI_API_KEY"),
},
}
],
num_retries=1, # Set at router level - single retry
)
# Mock make_call to always raise a Timeout error
async def mock_make_call(*args, **kwargs):
raise litellm.Timeout(
message="Request timed out",
model="gpt-3.5-turbo",
llm_provider="openai",
)
with patch.object(router, "make_call", side_effect=mock_make_call):
with patch.object(
router,
"_async_get_healthy_deployments",
return_value=(
[{"model_info": {"id": "test-id"}}],
[{"model_info": {"id": "test-id"}}],
),
):
with patch.object(router, "_time_to_sleep_before_retry", return_value=0.01):
with pytest.raises(litellm.Timeout) as exc_info:
await router.acompletion(
model="gpt-3.5-turbo",
messages=[{"role": "user", "content": "Hello"}],
)
e = exc_info.value
assert (
e.num_retries == 1
), f"Expected num_retries to be 1, got {e.num_retries}"
assert (
e.max_retries == 1
), f"Expected max_retries to be 1, got {e.max_retries}"