Source code for modelly.llm.retry

"""Retries with exponential backoff for rate limits, server errors and lost connections.

The wait is deterministic (no jitter) and goes through an injectable ``sleeper``, so a
test can check the delays without sleeping.
"""

from __future__ import annotations

import time
from collections.abc import Callable
from dataclasses import dataclass, field

from modelly.llm.errors import ProviderError
from modelly.llm.request import Request, Response

DEFAULT_RETRIES = 3
DEFAULT_BASE_DELAY = 1.0
DEFAULT_MAX_DELAY = 30.0
BACKOFF_FACTOR = 2


[docs] @dataclass class RetryPolicy: """How many times to retry a failed call and how long to wait in between. Args: max_retries: Extra tries after the first; 0 never retries. base_delay: Seconds before the first retry; doubled each time after. max_delay: The longest wait, also capping a server's ``Retry-After``. sleeper: Called with the seconds to wait; ``time.sleep`` by default. Example: >>> RetryPolicy(base_delay=1.0).delay(2, None) 4.0 """ max_retries: int = DEFAULT_RETRIES base_delay: float = DEFAULT_BASE_DELAY max_delay: float = DEFAULT_MAX_DELAY sleeper: Callable[[float], None] = field(default=time.sleep, repr=False)
[docs] def delay(self, attempt: int, retry_after: float | None) -> float: """Seconds to wait before retry number ``attempt`` (counting from 0).""" if retry_after is not None: return min(retry_after, self.max_delay) return min(self.base_delay * BACKOFF_FACTOR**attempt, self.max_delay)
[docs] def run( self, send: Callable[[Request], Response], request: Request ) -> tuple[Response, int]: """``send(request)``, retried on a retryable error; return the reply and the retries.""" attempt = 0 while True: try: return send(request), attempt except ProviderError as error: if not error.retryable or attempt >= self.max_retries: raise self.sleeper(self.delay(attempt, error.retry_after)) attempt += 1
def __getstate__(self) -> dict: """The numbers; a custom sleeper is replaced by ``time.sleep`` on load.""" return {k: v for k, v in vars(self).items() if k != "sleeper"} def __setstate__(self, state: dict) -> None: """Restore the numbers with ``time.sleep``.""" vars(self).update(state) self.sleeper = time.sleep