retry
retry decides what to do after a failed model call: retry (and when) or fail. The same exception always gets the same decision, and the backoff jitter comes from a hash instead of a random number generator.
retry.classify(error, *, attempt=1, policy=RetryPolicy()) -> RetryDecisionretry.backoff_delay(attempt, *, policy=RetryPolicy()) -> floatretry.call(fn, *args, policy=..., sleep=time.sleep, on_retry=None, **kwargs)>>> from beacon import retry>>> retry.classify(openai.RateLimitError(...))RetryDecision(action=<RetryAction.RETRY>, reason=<RetryReason.RATE_LIMIT>, delay_seconds=0.959, attempt=1, retryable=True)>>> retry.classify(openai.AuthenticationError(...))RetryDecision(action=<RetryAction.FAIL>, reason=<RetryReason.AUTH_ERROR>, delay_seconds=0.0, ...)Try it
Section titled “Try it”Typed retry decisions. The jitter comes from a hash of your key, so the same failure always gets the same schedule.
Classification
Section titled “Classification”Checked in this fixed order:
-
HTTP status code, read from
status_code,status,http_statusorresponse.status_code(or pass a bareint):Status Reason Retried 429 rate_limityes 503, 529 overloadedyes other 5xx server_erroryes 408 timeoutyes 409 conflictyes 401, 403 auth_errorno 400, 422 bad_requestno 404 not_foundno 413 request_too_largeno other 4xx client_errorno -
Class names in the exception’s MRO, most-derived first, from any module, so
openai,anthropic,httpx,requests,google.api_coreand builtin exceptions all work:RateLimitError,OverloadedError,APITimeoutError,ReadTimeout,TimeoutError,APIConnectionError,ConnectionError,AuthenticationError,BadRequestError,NotFoundError,ResourceExhausted,ServiceUnavailable,DeadlineExceeded, … -
Anything else is
unknown_error, which is not retried.
Delays
Section titled “Delays”- A numeric
retry-after-msorretry-afterresponse header wins, capped atmax_delay_ms. HTTP-date values are ignored because using them would mean reading the clock. - Otherwise:
min(max_delay_ms, base_delay_ms × 2^(attempt−1)), minus jitter. - Jitter:
blake3(jitter_key ‖ attempt) mod (span + 1), wherespan = delay × jitter_percent / 100. - All arithmetic is in integer milliseconds.
Policy
Section titled “Policy”RetryPolicy(max_attempts=5, base_delay_ms=1000, max_delay_ms=60_000, jitter_percent=20, jitter_key="")action is RETRY only if the reason is retryable and attempt < max_attempts; retryable tells you whether the error kind could be retried at all.
retry.call
Section titled “retry.call”result = retry.call( client.chat.completions.create, model="gpt-4o", messages=msgs, policy=RetryPolicy(max_attempts=4, jitter_key=request_id), on_retry=lambda exc, d: log.warning("retrying %s in %.2fs", d.reason.value, d.delay_seconds),)The last exception is re-raised when the error isn’t retryable or attempts run out. Inject sleep= to make tests instant: the decisions are deterministic, and only the waiting happens in real time.