railtracks.llm.retries
13class ExponentialRetry(RetryApproach): 14 """Exponential backoff: delay = ``base ** attempt`` seconds. 15 16 Caps ``max_tries`` at 20 to prevent runaway wait times. Warns if the 17 theoretical maximum delay exceeds 1 000 s. 18 19 Args: 20 max_tries: Total attempts (1–20). 21 base: Backoff multiplier (>= 1.0). Default ``2.0`` gives 1 s, 2 s, 4 s … 22 jitter: When ``True``, randomises the delay to ``uniform(0, base**attempt)`` 23 to spread load across concurrent callers. 24 """ 25 26 def __init__( 27 self, 28 max_tries: int, 29 base: float = 2.0, 30 jitter: bool = True, 31 ) -> None: 32 if max_tries < 1: 33 raise ValueError("max_tries must be >= 1") 34 if max_tries > _MAX_RETRY_TIMES_EXPONETIAL: 35 raise ValueError(f"max_tries must be <= {_MAX_RETRY_TIMES_EXPONETIAL}") 36 if base < 1: 37 raise ValueError("base must be >= 1") 38 39 if base**max_tries > _MAX_RECOMONDED_RETRY_TIME_EXPONETIAL: 40 warnings.warn( 41 f"With base={base} and max_tries={max_tries}, the maximum delay could exceed {_MAX_RECOMONDED_RETRY_TIME_EXPONETIAL} seconds, which may be too long for some applications." 42 ) 43 44 super().__init__( 45 max_tries=max_tries, 46 ) 47 self._base = base 48 self._jitter = jitter 49 50 @classmethod 51 def approach_name(cls) -> str: 52 return "exponential" 53 54 def _compute_delay(self, attempt: int) -> float: 55 delay = self._base**attempt 56 return random.uniform(0, delay) if self._jitter else delay
Exponential backoff: delay = base ** attempt seconds.
Caps max_tries at 20 to prevent runaway wait times. Warns if the
theoretical maximum delay exceeds 1 000 s.
Arguments:
- max_tries: Total attempts (1–20).
- base: Backoff multiplier (>= 1.0). Default
2.0gives 1 s, 2 s, 4 s … - jitter: When
True, randomises the delay touniform(0, base**attempt)to spread load across concurrent callers.
26 def __init__( 27 self, 28 max_tries: int, 29 base: float = 2.0, 30 jitter: bool = True, 31 ) -> None: 32 if max_tries < 1: 33 raise ValueError("max_tries must be >= 1") 34 if max_tries > _MAX_RETRY_TIMES_EXPONETIAL: 35 raise ValueError(f"max_tries must be <= {_MAX_RETRY_TIMES_EXPONETIAL}") 36 if base < 1: 37 raise ValueError("base must be >= 1") 38 39 if base**max_tries > _MAX_RECOMONDED_RETRY_TIME_EXPONETIAL: 40 warnings.warn( 41 f"With base={base} and max_tries={max_tries}, the maximum delay could exceed {_MAX_RECOMONDED_RETRY_TIME_EXPONETIAL} seconds, which may be too long for some applications." 42 ) 43 44 super().__init__( 45 max_tries=max_tries, 46 ) 47 self._base = base 48 self._jitter = jitter
7class FixedRetry(RetryApproach): 8 """Fixed backoff: waits the same ``delay`` seconds before every retry. 9 10 Useful when the provider's ``Retry-After`` header is unreliable or when 11 you want predictable, uniform pacing. Caps ``max_tries`` at 100. 12 13 Args: 14 max_tries: Total attempts (1–100). 15 delay: Seconds to wait between attempts (>= 0). Default ``1.0``. 16 """ 17 18 def __init__( 19 self, 20 max_tries: int, 21 delay: float = 1.0, 22 ) -> None: 23 if max_tries < 1: 24 raise ValueError("max_tries must be >= 1") 25 if max_tries > _MAX_RETRY_TIMES_FIXED: 26 raise ValueError(f"max_tries must be <= {_MAX_RETRY_TIMES_FIXED}") 27 if delay < 0: 28 raise ValueError("delay must be >= 0") 29 30 super().__init__(max_tries=max_tries) 31 self._delay = delay 32 33 @classmethod 34 def approach_name(cls) -> str: 35 return "fixed" 36 37 def _compute_delay(self, attempt: int) -> float: 38 return self._delay
Fixed backoff: waits the same delay seconds before every retry.
Useful when the provider's Retry-After header is unreliable or when
you want predictable, uniform pacing. Caps max_tries at 100.
Arguments:
- max_tries: Total attempts (1–100).
- delay: Seconds to wait between attempts (>= 0). Default
1.0.
18 def __init__( 19 self, 20 max_tries: int, 21 delay: float = 1.0, 22 ) -> None: 23 if max_tries < 1: 24 raise ValueError("max_tries must be >= 1") 25 if max_tries > _MAX_RETRY_TIMES_FIXED: 26 raise ValueError(f"max_tries must be <= {_MAX_RETRY_TIMES_FIXED}") 27 if delay < 0: 28 raise ValueError("delay must be >= 0") 29 30 super().__init__(max_tries=max_tries) 31 self._delay = delay
25class RetryApproach(ABC): 26 """Template base for retry strategies. 27 28 Subclasses implement ``_compute_delay`` to define the backoff schedule. 29 The retry loop and exception handling live here so subclasses stay minimal. 30 31 Args: 32 max_tries: Total attempts including the first call (not just retries). 33 """ 34 35 def __init__(self, max_tries: int) -> None: 36 if max_tries < 1: 37 raise ValueError("max_tries must be >= 1") 38 self._max_tries = max_tries 39 40 @classmethod 41 @abstractmethod 42 def approach_name(cls) -> str: 43 """Short identifier used in ``RetryError`` messages (e.g. ``"exponential"``).""" 44 45 @abstractmethod 46 def _compute_delay(self, attempt: int) -> float: 47 """Seconds to wait before attempt ``attempt + 1`` (0-indexed).""" 48 49 def call_with_retry( 50 self, 51 completion: Callable[[], _TResult], 52 *, 53 retry_on: tuple[type[Exception], ...] | None = None, 54 ) -> _TResult: 55 """Call ``completion`` up to ``max_tries`` times, sleeping between failures. 56 57 Args: 58 completion: Zero-arg callable to attempt. 59 retry_on: Exception types worth retrying; defaults to the transient 60 LLM provider errors. 61 62 Raises: 63 RetryError: All attempts failed; ``exception_list`` holds every error. 64 Exception: Any non-retryable exception propagates immediately. 65 """ 66 retryable = retry_on if retry_on is not None else _RETRYABLE_EXCEPTIONS 67 exceptions: list[Exception] = [] 68 69 for attempt in range(self._max_tries): 70 try: 71 return completion() 72 except retryable as e: 73 exceptions.append(e) 74 if attempt == self._max_tries - 1: 75 raise RetryError( 76 self.approach_name(), 77 "Max retries exceeded", 78 ["Examine exceptions to determine the root cause."], 79 exceptions, 80 ) from e 81 82 time.sleep(self._compute_delay(attempt)) 83 84 assert False, "Unreachable code" 85 86 async def acall_with_retry( 87 self, 88 completion: Callable[[], Awaitable[_TResult]], 89 *, 90 retry_on: tuple[type[Exception], ...] | None = None, 91 ) -> _TResult: 92 """Async mirror of ``call_with_retry`` — awaits ``completion`` each attempt.""" 93 retryable = retry_on if retry_on is not None else _RETRYABLE_EXCEPTIONS 94 exceptions: list[Exception] = [] 95 96 for attempt in range(self._max_tries): 97 try: 98 return await completion() 99 except retryable as e: 100 exceptions.append(e) 101 if attempt == self._max_tries - 1: 102 raise RetryError( 103 self.approach_name(), 104 "Max retries exceeded", 105 ["Examine exceptions to determine the root cause."], 106 exceptions, 107 ) from e 108 109 delay: float | None = None 110 111 if delay is not None: 112 await asyncio.sleep(delay) 113 else: 114 await asyncio.sleep(self._compute_delay(attempt)) 115 116 assert False, "Unreachable code"
Template base for retry strategies.
Subclasses implement _compute_delay to define the backoff schedule.
The retry loop and exception handling live here so subclasses stay minimal.
Arguments:
- max_tries: Total attempts including the first call (not just retries).
40 @classmethod 41 @abstractmethod 42 def approach_name(cls) -> str: 43 """Short identifier used in ``RetryError`` messages (e.g. ``"exponential"``)."""
Short identifier used in RetryError messages (e.g. "exponential").
49 def call_with_retry( 50 self, 51 completion: Callable[[], _TResult], 52 *, 53 retry_on: tuple[type[Exception], ...] | None = None, 54 ) -> _TResult: 55 """Call ``completion`` up to ``max_tries`` times, sleeping between failures. 56 57 Args: 58 completion: Zero-arg callable to attempt. 59 retry_on: Exception types worth retrying; defaults to the transient 60 LLM provider errors. 61 62 Raises: 63 RetryError: All attempts failed; ``exception_list`` holds every error. 64 Exception: Any non-retryable exception propagates immediately. 65 """ 66 retryable = retry_on if retry_on is not None else _RETRYABLE_EXCEPTIONS 67 exceptions: list[Exception] = [] 68 69 for attempt in range(self._max_tries): 70 try: 71 return completion() 72 except retryable as e: 73 exceptions.append(e) 74 if attempt == self._max_tries - 1: 75 raise RetryError( 76 self.approach_name(), 77 "Max retries exceeded", 78 ["Examine exceptions to determine the root cause."], 79 exceptions, 80 ) from e 81 82 time.sleep(self._compute_delay(attempt)) 83 84 assert False, "Unreachable code"
Call completion up to max_tries times, sleeping between failures.
Arguments:
- completion: Zero-arg callable to attempt.
- retry_on: Exception types worth retrying; defaults to the transient LLM provider errors.
Raises:
- RetryError: All attempts failed;
exception_listholds every error. - Exception: Any non-retryable exception propagates immediately.
86 async def acall_with_retry( 87 self, 88 completion: Callable[[], Awaitable[_TResult]], 89 *, 90 retry_on: tuple[type[Exception], ...] | None = None, 91 ) -> _TResult: 92 """Async mirror of ``call_with_retry`` — awaits ``completion`` each attempt.""" 93 retryable = retry_on if retry_on is not None else _RETRYABLE_EXCEPTIONS 94 exceptions: list[Exception] = [] 95 96 for attempt in range(self._max_tries): 97 try: 98 return await completion() 99 except retryable as e: 100 exceptions.append(e) 101 if attempt == self._max_tries - 1: 102 raise RetryError( 103 self.approach_name(), 104 "Max retries exceeded", 105 ["Examine exceptions to determine the root cause."], 106 exceptions, 107 ) from e 108 109 delay: float | None = None 110 111 if delay is not None: 112 await asyncio.sleep(delay) 113 else: 114 await asyncio.sleep(self._compute_delay(attempt)) 115 116 assert False, "Unreachable code"
Async mirror of call_with_retry — awaits completion each attempt.
9class LinearRetry(RetryApproach): 10 """Linear backoff: delay = ``step * (attempt + 1)`` seconds. 11 12 Grows steadily rather than exponentially — a middle ground between fixed 13 and exponential when you expect short-lived rate limits. Caps ``max_tries`` 14 at 100. 15 16 Args: 17 max_tries: Total attempts (1–100). 18 step: Base increment in seconds (>= 0). Default ``1.0`` gives 1 s, 2 s, 3 s … 19 jitter: When ``True``, randomises the delay to ``uniform(0, step * (attempt+1))``. 20 """ 21 22 def __init__( 23 self, 24 max_tries: int, 25 step: float = 1.0, 26 jitter: bool = True, 27 ) -> None: 28 if max_tries < 1: 29 raise ValueError("max_tries must be >= 1") 30 if max_tries > _MAX_RETRY_TIMES_LINEAR: 31 raise ValueError(f"max_tries must be <= {_MAX_RETRY_TIMES_LINEAR}") 32 if step < 0: 33 raise ValueError("step must be >= 0") 34 35 super().__init__(max_tries=max_tries) 36 self._step = step 37 self._jitter = jitter 38 39 @classmethod 40 def approach_name(cls) -> str: 41 return "linear" 42 43 def _compute_delay(self, attempt: int) -> float: 44 delay = self._step * (attempt + 1) 45 return random.uniform(0, delay) if self._jitter else delay
Linear backoff: delay = step * (attempt + 1) seconds.
Grows steadily rather than exponentially — a middle ground between fixed
and exponential when you expect short-lived rate limits. Caps max_tries
at 100.
Arguments:
- max_tries: Total attempts (1–100).
- step: Base increment in seconds (>= 0). Default
1.0gives 1 s, 2 s, 3 s … - jitter: When
True, randomises the delay touniform(0, step * (attempt+1)).
22 def __init__( 23 self, 24 max_tries: int, 25 step: float = 1.0, 26 jitter: bool = True, 27 ) -> None: 28 if max_tries < 1: 29 raise ValueError("max_tries must be >= 1") 30 if max_tries > _MAX_RETRY_TIMES_LINEAR: 31 raise ValueError(f"max_tries must be <= {_MAX_RETRY_TIMES_LINEAR}") 32 if step < 0: 33 raise ValueError("step must be >= 0") 34 35 super().__init__(max_tries=max_tries) 36 self._step = step 37 self._jitter = jitter