mirror of
https://github.com/usestrix/strix.git
synced 2026-08-20 10:33:34 +02:00
The SDK's http_status retry policy only retries errors carrying a known HTTP status code, but quota/billing (and other provider-side) failures often surface inside a streamed response as a bare error with no status code, so they were failing on the first attempt. Add a statusless retry policy to DEFAULT_MODEL_RETRY so they are retried (before any content is streamed; user aborts are never retried), restoring the pre-SDK engine's resilience. If the provider is genuinely exhausted, the error still propagates and fails the scan after retries.
70 lines
2.4 KiB
Python
70 lines
2.4 KiB
Python
"""Tests for the model retry policy used by every agent model call.
|
|
|
|
The SDK's built-in ``http_status`` policy only retries errors that carry a known
|
|
HTTP status code. Quota/billing (and other provider-side) failures often surface
|
|
*inside* a streamed response as a bare error with no status code, so Strix adds a
|
|
statusless retry policy to ``DEFAULT_MODEL_RETRY`` to keep them recoverable — the
|
|
behavior the pre-SDK engine had.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
|
|
from agents.retry import ModelRetryNormalizedError, RetryPolicyContext
|
|
|
|
from strix.config.models import DEFAULT_MODEL_RETRY, _retry_statusless_provider_errors
|
|
|
|
|
|
def _context(normalized: ModelRetryNormalizedError) -> RetryPolicyContext:
|
|
return RetryPolicyContext(
|
|
error=RuntimeError("boom"),
|
|
attempt=1,
|
|
max_retries=5,
|
|
stream=True,
|
|
normalized=normalized,
|
|
provider_advice=None,
|
|
)
|
|
|
|
|
|
def _retries(normalized: ModelRetryNormalizedError) -> bool:
|
|
"""Evaluate the composed DEFAULT_MODEL_RETRY policy for a normalized error."""
|
|
policy = DEFAULT_MODEL_RETRY.policy
|
|
assert policy is not None
|
|
decision = asyncio.run(policy(_context(normalized)))
|
|
return bool(getattr(decision, "retry", decision))
|
|
|
|
|
|
def test_statusless_error_is_retried() -> None:
|
|
# A mid-stream quota/billing error arrives with no HTTP status code.
|
|
assert _retries(ModelRetryNormalizedError(status_code=None)) is True
|
|
|
|
|
|
def test_statusless_abort_is_not_retried() -> None:
|
|
# A user/client cancellation must never be retried.
|
|
assert _retries(ModelRetryNormalizedError(status_code=None, is_abort=True)) is False
|
|
|
|
|
|
def test_client_error_is_not_retried() -> None:
|
|
# A definitive 4xx client error (bad request/auth) is not recoverable.
|
|
assert _retries(ModelRetryNormalizedError(status_code=400)) is False
|
|
|
|
|
|
def test_rate_limit_and_server_errors_are_retried() -> None:
|
|
for status in (429, 500, 502, 503, 504):
|
|
assert _retries(ModelRetryNormalizedError(status_code=status)) is True
|
|
|
|
|
|
def test_policy_helper_matches_statusless_only() -> None:
|
|
assert _retry_statusless_provider_errors(_context(ModelRetryNormalizedError())) is True
|
|
assert (
|
|
_retry_statusless_provider_errors(_context(ModelRetryNormalizedError(status_code=400)))
|
|
is False
|
|
)
|
|
assert (
|
|
_retry_statusless_provider_errors(
|
|
_context(ModelRetryNormalizedError(status_code=None, is_abort=True))
|
|
)
|
|
is False
|
|
)
|