Files
2026-07-13 13:12:33 +08:00

129 lines
3.8 KiB
Python

"""Retry-After header threading: adapter 429/5xx errors carry parsed seconds.
Fully offline via ``httpx.MockTransport`` — same pattern as the golden
harness. Synthetic credentials and models only.
"""
from __future__ import annotations
from typing import Any
import httpx
import pytest
from opensquilla.provider.selector import build_provider
from opensquilla.provider.types import ChatConfig, ErrorEvent, Message
FAKE_API_KEY = "sk-test-000"
def _patch_transport(
monkeypatch: pytest.MonkeyPatch, response_factory: Any
) -> None:
transport = httpx.MockTransport(lambda request: response_factory())
real_async_client = httpx.AsyncClient
def patched_async_client(*args: Any, **kwargs: Any) -> httpx.AsyncClient:
kwargs["transport"] = transport
return real_async_client(*args, **kwargs)
monkeypatch.setattr(httpx, "AsyncClient", patched_async_client)
async def _chat_errors(provider: Any) -> list[ErrorEvent]:
messages = [Message(role="user", content="ping")]
events = [
event
async for event in provider.chat(messages, tools=None, config=ChatConfig())
]
return [event for event in events if isinstance(event, ErrorEvent)]
async def test_openai_compat_429_carries_retry_after_seconds(
monkeypatch: pytest.MonkeyPatch,
) -> None:
_patch_transport(
monkeypatch,
lambda: httpx.Response(
429,
headers={"Retry-After": "7"},
json={"error": {"message": "synthetic rate limit"}},
),
)
provider = build_provider("openai", "test-chat-model", api_key=FAKE_API_KEY)
errors = await _chat_errors(provider)
assert errors
assert errors[0].code == "429"
assert errors[0].retry_after_s == 7.0
async def test_openai_compat_503_carries_retry_after_seconds(
monkeypatch: pytest.MonkeyPatch,
) -> None:
_patch_transport(
monkeypatch,
lambda: httpx.Response(
503,
headers={"Retry-After": "12"},
json={"error": {"message": "synthetic overload"}},
),
)
provider = build_provider("openai", "test-chat-model", api_key=FAKE_API_KEY)
errors = await _chat_errors(provider)
assert errors
assert errors[0].code == "503"
assert errors[0].retry_after_s == 12.0
async def test_openai_compat_400_never_carries_retry_after(
monkeypatch: pytest.MonkeyPatch,
) -> None:
_patch_transport(
monkeypatch,
lambda: httpx.Response(
400,
headers={"Retry-After": "7"},
json={"error": {"message": "synthetic bad request"}},
),
)
provider = build_provider("openai", "test-chat-model", api_key=FAKE_API_KEY)
errors = await _chat_errors(provider)
assert errors
assert errors[0].code == "400"
assert errors[0].retry_after_s is None
async def test_anthropic_429_carries_retry_after_seconds(
monkeypatch: pytest.MonkeyPatch,
) -> None:
_patch_transport(
monkeypatch,
lambda: httpx.Response(
429,
headers={"Retry-After": "9"},
json={"error": {"type": "rate_limit_error", "message": "synthetic"}},
),
)
provider = build_provider("anthropic", "claude-sonnet-4-6", api_key=FAKE_API_KEY)
errors = await _chat_errors(provider)
assert errors
assert errors[0].code == "429"
assert errors[0].retry_after_s == 9.0
async def test_missing_header_leaves_retry_after_none(
monkeypatch: pytest.MonkeyPatch,
) -> None:
_patch_transport(
monkeypatch,
lambda: httpx.Response(
429,
json={"error": {"message": "synthetic rate limit"}},
),
)
provider = build_provider("openai", "test-chat-model", api_key=FAKE_API_KEY)
errors = await _chat_errors(provider)
assert errors
assert errors[0].code == "429"
assert errors[0].retry_after_s is None