|
| 1 | +from __future__ import annotations |
| 2 | + |
| 3 | +from unittest import mock |
| 4 | + |
| 5 | +import httpx2 |
| 6 | +import pytest |
| 7 | + |
| 8 | +from openai import OpenAI, AsyncOpenAI, APIStatusError |
| 9 | + |
| 10 | + |
| 11 | +@pytest.mark.parametrize("async_mode", [False, True]) |
| 12 | +@pytest.mark.parametrize( |
| 13 | + "status,headers,delay", |
| 14 | + [ |
| 15 | + (429, {"retry-after": "1e999"}, None), |
| 16 | + (503, {"retry-after": "9" * 400}, None), |
| 17 | + (429, {"retry-after-ms": "1e999"}, None), |
| 18 | + (503, {"retry-after-ms": "9" * 400}, None), |
| 19 | + (401, {"retry-after": "1e999", "x-should-retry": "true"}, None), |
| 20 | + (429, {"retry-after": "inf"}, 0.5), |
| 21 | + (503, {"retry-after": " +Infinity "}, 0.5), |
| 22 | + (429, {"retry-after": "NaN"}, 0.5), |
| 23 | + (429, {"retry-after": "-1e999"}, 0.5), |
| 24 | + (429, {"retry-after-ms": "inf", "retry-after": "90"}, 0.5), |
| 25 | + (429, {"retry-after": "1e2"}, 100.0), |
| 26 | + (503, {"retry-after-ms": "1e5"}, 100.0), |
| 27 | + ], |
| 28 | +) |
| 29 | +async def test_retry_after_numeric_overflow( |
| 30 | + async_mode: bool, status: int, headers: dict[str, str], delay: float | None |
| 31 | +) -> None: |
| 32 | + attempts = 0 |
| 33 | + body = {"message": "Synthetic retry error", "type": "synthetic_error", "code": "synthetic_code"} |
| 34 | + |
| 35 | + def handle(request: httpx2.Request) -> httpx2.Response: |
| 36 | + nonlocal attempts |
| 37 | + attempts += 1 |
| 38 | + assert request.url.path == "/models/test" |
| 39 | + return httpx2.Response(status, headers={**headers, "x-request-id": "synthetic-id"}, json={"error": body}) |
| 40 | + |
| 41 | + with mock.patch("time.sleep") as sync_sleep, mock.patch("anyio.sleep") as async_sleep: |
| 42 | + with mock.patch("openai._base_client.random", return_value=0), pytest.raises(APIStatusError) as exc: |
| 43 | + if async_mode: |
| 44 | + async with AsyncOpenAI( |
| 45 | + api_key="synthetic-key", |
| 46 | + base_url="https://retry.test", |
| 47 | + max_retries=1, |
| 48 | + http_client=httpx2.AsyncClient(transport=httpx2.MockTransport(handle), trust_env=False), |
| 49 | + ) as async_client: |
| 50 | + await async_client.models.retrieve("test") |
| 51 | + else: |
| 52 | + with OpenAI( |
| 53 | + api_key="synthetic-key", |
| 54 | + base_url="https://retry.test", |
| 55 | + max_retries=1, |
| 56 | + http_client=httpx2.Client(transport=httpx2.MockTransport(handle), trust_env=False), |
| 57 | + ) as client: |
| 58 | + client.models.retrieve("test") |
| 59 | + |
| 60 | + assert attempts == (1 if delay is None else 2) |
| 61 | + assert (async_sleep if async_mode else sync_sleep).call_args_list == ([] if delay is None else [mock.call(delay)]) |
| 62 | + assert exc.value.status_code == status |
| 63 | + assert exc.value.body == body |
| 64 | + assert exc.value.request_id == "synthetic-id" |
| 65 | + assert all(exc.value.response.headers[key] == value for key, value in headers.items()) |
0 commit comments