6b7e6b44f1
gh-pages / build (push) Waiting to run
Python Publish (pypi) / Upload release to PyPI (push) Waiting to run
Spellcheck / spellcheck (push) Waiting to run
Python Build and Type Check / python-ci (ubuntu-latest, 3.11) (push) Has been cancelled
Python Build and Type Check / python-ci (ubuntu-latest, 3.13) (push) Has been cancelled
Python Build and Type Check / python-ci (windows-latest, 3.11) (push) Has been cancelled
Python Build and Type Check / python-ci (windows-latest, 3.13) (push) Has been cancelled
Python Integration Tests / python-ci (ubuntu-latest, 3.13) (push) Has been cancelled
Python Integration Tests / python-ci (windows-latest, 3.13) (push) Has been cancelled
Python Notebook Tests / python-ci (ubuntu-latest, 3.13) (push) Has been cancelled
Python Notebook Tests / python-ci (windows-latest, 3.13) (push) Has been cancelled
Python Smoke Tests / python-ci (ubuntu-latest, 3.13) (push) Has been cancelled
Python Smoke Tests / python-ci (windows-latest, 3.13) (push) Has been cancelled
Python Unit Tests / python-ci (ubuntu-latest, 3.13) (push) Has been cancelled
Python Unit Tests / python-ci (windows-latest, 3.13) (push) Has been cancelled
244 lines
6.2 KiB
Python
244 lines
6.2 KiB
Python
# Copyright (c) 2024 Microsoft Corporation.
|
|
# Licensed under the MIT License
|
|
|
|
"""Test LiteLLM Retries."""
|
|
|
|
import time
|
|
from typing import Any
|
|
|
|
import httpx
|
|
import litellm.exceptions as exceptions
|
|
import pytest
|
|
from graphrag_llm.config import RetryConfig, RetryType
|
|
from graphrag_llm.retry import create_retry
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("config", "max_retries", "expected_time"),
|
|
[
|
|
(
|
|
RetryConfig(
|
|
type=RetryType.ExponentialBackoff,
|
|
max_retries=3,
|
|
base_delay=2.0,
|
|
jitter=False,
|
|
),
|
|
3,
|
|
2 + 4 + 8, # No jitter, so exact times
|
|
),
|
|
(
|
|
RetryConfig(
|
|
type=RetryType.Immediate,
|
|
max_retries=3,
|
|
),
|
|
3,
|
|
0, # Immediate retry, so no delay
|
|
),
|
|
],
|
|
)
|
|
def test_retries(config: RetryConfig, max_retries: int, expected_time: float) -> None:
|
|
"""
|
|
Test various retry strategies with various configurations.
|
|
"""
|
|
retry_service = create_retry(config)
|
|
|
|
# start at -1 because the first call is not a retry
|
|
retries = -1
|
|
|
|
def mock_func():
|
|
nonlocal retries
|
|
retries += 1
|
|
msg = "Mock error for testing retries"
|
|
raise ValueError(msg)
|
|
|
|
start_time = time.time()
|
|
with pytest.raises(ValueError, match="Mock error for testing retries"):
|
|
retry_service.retry(func=mock_func, input_args={})
|
|
elapsed_time = time.time() - start_time
|
|
|
|
assert retries == max_retries, f"Expected {max_retries} retries, got {retries}"
|
|
assert elapsed_time >= expected_time, (
|
|
f"Expected elapsed time >= {expected_time}, got {elapsed_time}"
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("config", "max_retries", "expected_time"),
|
|
[
|
|
(
|
|
RetryConfig(
|
|
type=RetryType.ExponentialBackoff,
|
|
max_retries=3,
|
|
base_delay=2.0,
|
|
jitter=False,
|
|
),
|
|
3,
|
|
2 + 4 + 8, # No jitter, so exact times
|
|
),
|
|
(
|
|
RetryConfig(
|
|
type=RetryType.Immediate,
|
|
max_retries=3,
|
|
),
|
|
3,
|
|
0, # Immediate retry, so no delay
|
|
),
|
|
],
|
|
)
|
|
async def test_retries_async(
|
|
config: RetryConfig, max_retries: int, expected_time: float
|
|
) -> None:
|
|
"""
|
|
Test various retry strategies with various configurations.
|
|
"""
|
|
retry_service = create_retry(config)
|
|
|
|
# start at -1 because the first call is not a retry
|
|
retries = -1
|
|
|
|
def mock_func():
|
|
nonlocal retries
|
|
retries += 1
|
|
msg = "Mock error for testing retries"
|
|
raise ValueError(msg)
|
|
|
|
start_time = time.time()
|
|
with pytest.raises(ValueError, match="Mock error for testing retries"):
|
|
await retry_service.retry_async(func=mock_func, input_args={})
|
|
elapsed_time = time.time() - start_time
|
|
|
|
assert retries == max_retries, f"Expected {max_retries} retries, got {retries}"
|
|
assert elapsed_time >= expected_time, (
|
|
f"Expected elapsed time >= {expected_time}, got {elapsed_time}"
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"config",
|
|
[
|
|
(
|
|
RetryConfig(
|
|
type=RetryType.ExponentialBackoff,
|
|
max_retries=3,
|
|
base_delay=2.0,
|
|
jitter=False,
|
|
)
|
|
),
|
|
(
|
|
RetryConfig(
|
|
type=RetryType.Immediate,
|
|
max_retries=3,
|
|
)
|
|
),
|
|
],
|
|
)
|
|
@pytest.mark.parametrize(
|
|
("exception", "exception_args"),
|
|
[
|
|
(
|
|
"BadRequestError",
|
|
["Oh no!", "", ""],
|
|
),
|
|
(
|
|
"UnsupportedParamsError",
|
|
["Oh no!", "", ""],
|
|
),
|
|
(
|
|
"ContextWindowExceededError",
|
|
["Oh no!", "", ""],
|
|
),
|
|
(
|
|
"ContentPolicyViolationError",
|
|
["Oh no!", "", ""],
|
|
),
|
|
(
|
|
"ImageFetchError",
|
|
["Oh no!", "", ""],
|
|
),
|
|
(
|
|
"InvalidRequestError",
|
|
["Oh no!", "", ""],
|
|
),
|
|
(
|
|
"AuthenticationError",
|
|
["Oh no!", "", ""],
|
|
),
|
|
(
|
|
"PermissionDeniedError",
|
|
[
|
|
"Oh no!",
|
|
"",
|
|
"",
|
|
httpx.Response(
|
|
status_code=403,
|
|
request=httpx.Request(
|
|
method="GET", url="https://litellm.ai"
|
|
), # mock request object
|
|
),
|
|
],
|
|
),
|
|
(
|
|
"NotFoundError",
|
|
["Oh no!", "", ""],
|
|
),
|
|
(
|
|
"UnprocessableEntityError",
|
|
[
|
|
"Oh no!",
|
|
"",
|
|
"",
|
|
httpx.Response(
|
|
status_code=403,
|
|
request=httpx.Request(
|
|
method="GET", url="https://litellm.ai"
|
|
), # mock request object
|
|
),
|
|
],
|
|
),
|
|
(
|
|
"APIConnectionError",
|
|
["Oh no!", "", ""],
|
|
),
|
|
(
|
|
"APIError",
|
|
[500, "Oh no!", "", ""],
|
|
),
|
|
(
|
|
"ServiceUnavailableError",
|
|
["Oh no!", "", ""],
|
|
),
|
|
(
|
|
"APIResponseValidationError",
|
|
["Oh no!", "", ""],
|
|
),
|
|
(
|
|
"BudgetExceededError",
|
|
["Oh no!", "", ""],
|
|
),
|
|
],
|
|
)
|
|
def test_exponential_backoff_skipping_exceptions(
|
|
config: RetryConfig, exception: str, exception_args: list[Any]
|
|
) -> None:
|
|
"""
|
|
Test skipping retries for exceptions that should not cause a retry.
|
|
"""
|
|
retry_service = create_retry(config)
|
|
|
|
# start at -1 because the first call is not a retry
|
|
retries = -1
|
|
exception_cls = exceptions.__dict__[exception]
|
|
|
|
def mock_func():
|
|
nonlocal retries
|
|
retries += 1
|
|
raise exception_cls(*exception_args)
|
|
|
|
with pytest.raises(exception_cls, match="Oh no!"):
|
|
retry_service.retry(func=mock_func, input_args={})
|
|
|
|
# subtract 1 from retries because the first call is not a retry
|
|
assert retries == 0, (
|
|
f"Expected not to retry for '{exception}' exception. Got {retries} retries."
|
|
)
|