From 9aedefdb8c9b54a7bf2acb6e191e768898976104 Mon Sep 17 00:00:00 2001 From: Lucas Alencar Xisto Date: Sun, 31 Aug 2025 21:33:09 -0300 Subject: [PATCH 1/3] tests(images): add coverage for optional content_filter_results on Image --- tests/test_images_missing_fields.py | 50 +++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 tests/test_images_missing_fields.py diff --git a/tests/test_images_missing_fields.py b/tests/test_images_missing_fields.py new file mode 100644 index 0000000000..3baef8ce21 --- /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 From 6ae5e2b1afefb7f743ba16b8a1752501212d1283 Mon Sep 17 00:00:00 2001 From: Lucas Alencar Xisto Date: Sat, 6 Sep 2025 21:27:14 -0300 Subject: [PATCH 2/3] test: dedupe retries/timeouts, add conftest with fake OPENAI_API_KEY --- tests/conftest.py | 8 +++ tests/retries/__init__.py | 2 + tests/retries/test_retry_after.py | 42 ++++++++++++ tests/test_images_missing_fields.py | 100 ++++++++++++++-------------- tests/timeouts/__init__.py | 2 + tests/timeouts/_util.py | 19 ++++++ tests/timeouts/test_overrides.py | 28 ++++++++ 7 files changed, 151 insertions(+), 50 deletions(-) create mode 100644 tests/retries/__init__.py create mode 100644 tests/retries/test_retry_after.py create mode 100644 tests/timeouts/__init__.py create mode 100644 tests/timeouts/_util.py create mode 100644 tests/timeouts/test_overrides.py 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 index 3baef8ce21..3dfe1bff04 100644 --- a/tests/test_images_missing_fields.py +++ b/tests/test_images_missing_fields.py @@ -1,50 +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 +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) + From 4a8456a936531d92d6cd0c927062f01a65512d09 Mon Sep 17 00:00:00 2001 From: Lucas Alencar Xisto Date: Sun, 7 Sep 2025 02:33:29 -0300 Subject: [PATCH 3/3] chore: snapshot before archive --- README.md | 31 ++++++++++++++++++++++++++++++- src/openai/types/image.py | 9 ++++++++- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d4b8d8d170..96fd309859 100644 --- a/README.md +++ b/README.md @@ -376,7 +376,7 @@ from openai import OpenAI client = OpenAI() -response = client.chat.responses.create( +response = client.responses.create( input=[ { "role": "user", @@ -622,6 +622,35 @@ client.with_options(timeout=5.0).chat.completions.create( model="gpt-4o", ) ``` +- from openai import OpenAI ++ import httpx ++ from openai import OpenAI + +import httpx +from openai import OpenAI + +# Configure the default for all requests: +client = OpenAI( + # 20 seconds (default is 10 minutes) + timeout=20.0, +) + +# More granular control: +client = OpenAI( + timeout=httpx.Timeout(60.0, read=5.0, write=10.0, connect=2.0), +) + +# Override per-request: +client.with_options(timeout=5.0).chat.completions.create( + messages=[ + { + "role": "user", + "content": "How can I list all files in a directory using Python?", + } + ], + model="gpt-4o", +) + On timeout, an `APITimeoutError` is thrown. diff --git a/src/openai/types/image.py b/src/openai/types/image.py index ecaef3fd58..c3334365d2 100644 --- a/src/openai/types/image.py +++ b/src/openai/types/image.py @@ -1,6 +1,6 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import Optional +from typing import Optional, Dict, Any from .._models import BaseModel @@ -24,3 +24,10 @@ class Image(BaseModel): `response_format` is set to `url` (default value). Unsupported for `gpt-image-1`. """ + + content_filter_results: Optional[Dict[str, Any]] = None + """Optional content filter metadata returned by the API. + + Includes safety-related categories (e.g. sexual_minors, violence, etc.) + indicating whether the image was flagged or filtered. + """