Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion src/agents/run_internal/model_retry.py
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,21 @@ def _default_retry_delay(
else DEFAULT_BACKOFF_JITTER
)

base = min(initial_delay * (multiplier ** max(attempt - 1, 0)), max_delay)
exponent = max(attempt - 1, 0)
# Compute the exponential growth defensively. With a large `attempt` or `multiplier`
# the intermediate `multiplier ** exponent` can raise OverflowError even though the
# result would be immediately capped at max_delay. Short-circuit to max_delay in that
# case so a high retry count does not surface a crash from the backoff helper itself.
# `initial_delay == 0` is a valid way to request immediate retries; preserve that
# rather than substituting `max_delay` on overflow.
if initial_delay == 0:
scaled = 0.0
else:
try:
scaled = initial_delay * (multiplier**exponent)
except OverflowError:
scaled = max_delay
base = min(scaled, max_delay)
if not use_jitter:
return base
return min(max(base * (0.875 + random.random() * 0.25), 0.0), max_delay)
Expand Down
35 changes: 34 additions & 1 deletion tests/models/test_model_retry.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,11 @@
RetryPolicyContext,
retry_policies,
)
from agents.run_internal.model_retry import get_response_with_retry, stream_response_with_retry
from agents.run_internal.model_retry import (
_default_retry_delay,
get_response_with_retry,
stream_response_with_retry,
)
from agents.usage import Usage
from tests.test_responses import get_text_message

Expand Down Expand Up @@ -2383,3 +2387,32 @@ async def rewind() -> None:
await outer_stream.aclose()

assert stream.close_calls == 1


def test_default_retry_delay_caps_overflow_at_max_delay() -> None:
# A pathologically large multiplier would overflow `multiplier ** exponent` even
# though the result would be capped at max_delay; the helper must not propagate
# OverflowError to the retry loop.
backoff = ModelRetryBackoffSettings(
initial_delay=1.0, max_delay=5.0, multiplier=100.0, jitter=False
)
assert _default_retry_delay(200, backoff) == 5.0


def test_default_retry_delay_handles_large_attempt_with_default_multiplier() -> None:
# With the default 2.0 multiplier, attempts above ~1075 overflow `2.0 ** exponent`.
# The helper should still return a finite delay capped at max_delay.
backoff = ModelRetryBackoffSettings(
initial_delay=0.25, max_delay=2.0, multiplier=2.0, jitter=False
)
assert _default_retry_delay(2000, backoff) == 2.0


def test_default_retry_delay_preserves_zero_initial_delay_on_overflow() -> None:
# `initial_delay=0` requests immediate retries; an overflow in
# `multiplier ** exponent` must not promote that to max_delay.
backoff = ModelRetryBackoffSettings(
initial_delay=0.0, max_delay=5.0, multiplier=100.0, jitter=False
)
assert _default_retry_delay(200, backoff) == 0.0
assert _default_retry_delay(1, backoff) == 0.0