Summary
anthropic.lib._retry.backoff(attempt, *, cap, base=2.0) returns min(cap, base ** attempt). Because base is the float 2.0, 2.0 ** attempt overflows a C double at attempt >= 1024 and raises OverflowError before min() can clamp it — so instead of the documented cap (the docstring says "capped at cap") the call raises.
Reproduction
from anthropic.lib._retry import backoff
print(backoff(1023, cap=60.0)) # 60.0
print(backoff(1024, cap=60.0)) # OverflowError: (34, 'Result too large')
Where it bites
The only caller is the environments poller — lib/environments/_poller.py uses _backoff(attempt) = backoff(attempt, cap=60.0) in its retry loop. poll/ack reset the attempt counter to 0 on any success, so triggering this needs ~1024 consecutive transient failures. At that point the run-forever poller dies with an uncaught OverflowError and won't recover once the outage clears, instead of continuing to retry at the 60s cap.
Note
The core client already guards this exact case — in _base_client.py:
# Also cap retry count to 1000 to avoid any potential overflows with `pow`
nb_retries = min(max_retries - remaining_retries, 1000)
...
sleep_seconds = min(INITIAL_RETRY_DELAY * pow(2.0, nb_retries), MAX_RETRY_DELAY)
The extracted backoff() helper simply missed the same exponent guard. Happy to send a PR mirroring it (min(cap, base ** min(attempt, 1000))) — the returned value is unchanged for every attempt (the result is already cap long before the overflow point), it only removes the crash.
Summary
anthropic.lib._retry.backoff(attempt, *, cap, base=2.0)returnsmin(cap, base ** attempt). Becausebaseis the float2.0,2.0 ** attemptoverflows a C double atattempt >= 1024and raisesOverflowErrorbeforemin()can clamp it — so instead of the documented cap (the docstring says "capped atcap") the call raises.Reproduction
Where it bites
The only caller is the environments poller —
lib/environments/_poller.pyuses_backoff(attempt) = backoff(attempt, cap=60.0)in its retry loop.poll/ackreset the attempt counter to 0 on any success, so triggering this needs ~1024 consecutive transient failures. At that point the run-forever poller dies with an uncaughtOverflowErrorand won't recover once the outage clears, instead of continuing to retry at the 60s cap.Note
The core client already guards this exact case — in
_base_client.py:The extracted
backoff()helper simply missed the same exponent guard. Happy to send a PR mirroring it (min(cap, base ** min(attempt, 1000))) — the returned value is unchanged for every attempt (the result is alreadycaplong before the overflow point), it only removes the crash.