diff --git a/tests/conftest.py b/tests/conftest.py index 408bcf76c0..4cd6109426 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -21,6 +21,14 @@ logging.getLogger("openai").setLevel(logging.DEBUG) +# Autouse fixture to ensure an API key is always set for tests +@pytest.fixture(autouse=True) +def _fake_openai_key(monkeypatch: pytest.MonkeyPatch) -> None: + # evita dependĂȘncia real de credencial + monkeypatch.setenv("OPENAI_API_KEY", "test") + yield + + # automatically add `pytest.mark.asyncio()` to all of our async tests # so we don't have to add that boilerplate everywhere def pytest_collection_modifyitems(items: list[pytest.Function]) -> None: diff --git a/tests/retries/__init__.py b/tests/retries/__init__.py new file mode 100644 index 0000000000..5c8018e448 --- /dev/null +++ b/tests/retries/__init__.py @@ -0,0 +1,2 @@ +"""Tests related to retry behavior.""" + diff --git a/tests/retries/test_retry_after.py b/tests/retries/test_retry_after.py new file mode 100644 index 0000000000..d6b03602fd --- /dev/null +++ b/tests/retries/test_retry_after.py @@ -0,0 +1,42 @@ +from __future__ import annotations + +import os +from unittest import mock + +import httpx +import pytest +from respx import MockRouter + +from openai import OpenAI + + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +def _low_retry_timeout(*_args, **_kwargs) -> float: + return 0.01 + + +@mock.patch("openai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) +@pytest.mark.respx(base_url=base_url) +def test_retry_after_header_is_respected(respx_mock: MockRouter, client: OpenAI) -> None: + attempts = {"n": 0} + + def handler(request: httpx.Request) -> httpx.Response: + attempts["n"] += 1 + if attempts["n"] == 1: + return httpx.Response(429, headers={"Retry-After": "2"}, json={"err": "rate"}) + return httpx.Response(200, json={"ok": True}) + + respx_mock.post("/chat/completions").mock(side_effect=handler) + + client = client.with_options(max_retries=3) + + response = client.chat.completions.with_raw_response.create( + messages=[{"content": "hi", "role": "user"}], + model="gpt-4o", + ) + + assert response.retries_taken == 1 + assert int(response.http_request.headers.get("x-stainless-retry-count")) == 1 + diff --git a/tests/test_images_missing_fields.py b/tests/test_images_missing_fields.py new file mode 100644 index 0000000000..3dfe1bff04 --- /dev/null +++ b/tests/test_images_missing_fields.py @@ -0,0 +1,50 @@ +import httpx +import pytest +from openai import AsyncOpenAI, DefaultAsyncHttpxClient + +@pytest.mark.anyio +async def test_images_generate_includes_content_filter_results_async(): + """ + Ensure the Image model exposes optional fields returned by the API, + specifically `content_filter_results` (keeping `revised_prompt` coverage). + """ + mock_json = { + "created": 1711111111, + "data": [ + { + "url": "https://example.test/cat.png", + "revised_prompt": "a cute cat wearing sunglasses", + "content_filter_results": { + "sexual_minors": {"filtered": False}, + "violence": {"filtered": False}, + }, + } + ], + } + + # Async handler because we'll use AsyncOpenAI (httpx.AsyncClient under the hood) + async def ahandler(request: httpx.Request) -> httpx.Response: + assert "images" in str(request.url).lower() + return httpx.Response(200, json=mock_json) + + atransport = httpx.MockTransport(ahandler) + + client = AsyncOpenAI( + api_key="test", + http_client=DefaultAsyncHttpxClient(transport=atransport), + timeout=10.0, + ) + + resp = await client.images.generate(model="gpt-image-1", prompt="cat with glasses") # type: ignore + + assert hasattr(resp, "data") and isinstance(resp.data, list) and resp.data + item = resp.data[0] + + # existing field + assert item.revised_prompt == "a cute cat wearing sunglasses" + + # new optional field + cfr = item.content_filter_results + assert isinstance(cfr, dict), f"content_filter_results should be dict, got {type(cfr)}" + assert cfr.get("violence", {}).get("filtered") is False + assert cfr.get("sexual_minors", {}).get("filtered") is False diff --git a/tests/timeouts/__init__.py b/tests/timeouts/__init__.py new file mode 100644 index 0000000000..dec9aed6b3 --- /dev/null +++ b/tests/timeouts/__init__.py @@ -0,0 +1,2 @@ +"""Tests related to timeout behavior.""" + diff --git a/tests/timeouts/_util.py b/tests/timeouts/_util.py new file mode 100644 index 0000000000..f37fc027cb --- /dev/null +++ b/tests/timeouts/_util.py @@ -0,0 +1,19 @@ +from __future__ import annotations + +def assert_timeout_eq(value, expected: float) -> None: + """Assert that a timeout-like value equals the expected seconds. + + Supports plain numeric timeouts or httpx.Timeout instances. + """ + from httpx import Timeout + + if isinstance(value, (int, float)): + assert float(value) == expected + elif isinstance(value, Timeout): + assert any( + getattr(value, f, None) in (None, expected) + for f in ("read", "connect", "write") + ), f"Timeout fields do not match {expected}: {value!r}" + else: + raise AssertionError(f"Unexpected timeout type: {type(value)}") + diff --git a/tests/timeouts/test_overrides.py b/tests/timeouts/test_overrides.py new file mode 100644 index 0000000000..649a2df36f --- /dev/null +++ b/tests/timeouts/test_overrides.py @@ -0,0 +1,28 @@ +from __future__ import annotations + +import os + +import httpx +import pytest + +from openai import OpenAI +from openai._models import FinalRequestOptions +from openai._base_client import DEFAULT_TIMEOUT + + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +def test_per_request_timeout_overrides_default(client: OpenAI) -> None: + # default timeout applied when none provided per-request + request = client._build_request(FinalRequestOptions(method="get", url="/foo")) + timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore[arg-type] + assert timeout == DEFAULT_TIMEOUT + + # per-request timeout overrides the default + request = client._build_request( + FinalRequestOptions(method="get", url="/foo", timeout=httpx.Timeout(100.0)) + ) + timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore[arg-type] + assert timeout == httpx.Timeout(100.0) +