From 49561d88279628bc400d1b09aa98765b67018ef1 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 13 Jan 2026 18:37:21 +0000 Subject: [PATCH 01/10] feat(client): add support for binary request streaming --- src/openai/_base_client.py | 145 +++++++++++++++++++++++++--- src/openai/_models.py | 17 +++- src/openai/_types.py | 9 ++ tests/test_client.py | 187 ++++++++++++++++++++++++++++++++++++- 4 files changed, 344 insertions(+), 14 deletions(-) diff --git a/src/openai/_base_client.py b/src/openai/_base_client.py index 9e536410d6..d34208abef 100644 --- a/src/openai/_base_client.py +++ b/src/openai/_base_client.py @@ -9,6 +9,7 @@ import inspect import logging import platform +import warnings import email.utils from types import TracebackType from random import random @@ -51,9 +52,11 @@ ResponseT, AnyMapping, PostParser, + BinaryTypes, RequestFiles, HttpxSendArgs, RequestOptions, + AsyncBinaryTypes, HttpxRequestFiles, ModelBuilderProtocol, not_given, @@ -479,8 +482,19 @@ def _build_request( retries_taken: int = 0, ) -> httpx.Request: if log.isEnabledFor(logging.DEBUG): - log.debug("Request options: %s", model_dump(options, exclude_unset=True)) - + log.debug( + "Request options: %s", + model_dump( + options, + exclude_unset=True, + # Pydantic v1 can't dump every type we support in content, so we exclude it for now. + exclude={ + "content", + } + if PYDANTIC_V1 + else {}, + ), + ) kwargs: dict[str, Any] = {} json_data = options.json_data @@ -534,7 +548,13 @@ def _build_request( is_body_allowed = options.method.lower() != "get" if is_body_allowed: - if isinstance(json_data, bytes): + if options.content is not None and json_data is not None: + raise TypeError("Passing both `content` and `json_data` is not supported") + if options.content is not None and files is not None: + raise TypeError("Passing both `content` and `files` is not supported") + if options.content is not None: + kwargs["content"] = options.content + elif isinstance(json_data, bytes): kwargs["content"] = json_data else: kwargs["json"] = json_data if is_given(json_data) else None @@ -1211,6 +1231,7 @@ def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, options: RequestOptions = {}, files: RequestFiles | None = None, stream: Literal[False] = False, @@ -1223,6 +1244,7 @@ def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, options: RequestOptions = {}, files: RequestFiles | None = None, stream: Literal[True], @@ -1236,6 +1258,7 @@ def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, options: RequestOptions = {}, files: RequestFiles | None = None, stream: bool, @@ -1248,13 +1271,25 @@ def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, options: RequestOptions = {}, files: RequestFiles | None = None, stream: bool = False, stream_cls: type[_StreamT] | None = None, ) -> ResponseT | _StreamT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if files is not None and content is not None: + raise TypeError("Passing both `files` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) opts = FinalRequestOptions.construct( - method="post", url=path, json_data=body, files=to_httpx_files(files), **options + method="post", url=path, json_data=body, content=content, files=to_httpx_files(files), **options ) return cast(ResponseT, self.request(cast_to, opts, stream=stream, stream_cls=stream_cls)) @@ -1264,11 +1299,23 @@ def patch( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, ) -> ResponseT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if files is not None and content is not None: + raise TypeError("Passing both `files` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) opts = FinalRequestOptions.construct( - method="patch", url=path, json_data=body, files=to_httpx_files(files), **options + method="patch", url=path, json_data=body, content=content, files=to_httpx_files(files), **options ) return self.request(cast_to, opts) @@ -1278,11 +1325,23 @@ def put( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, ) -> ResponseT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if files is not None and content is not None: + raise TypeError("Passing both `files` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) opts = FinalRequestOptions.construct( - method="put", url=path, json_data=body, files=to_httpx_files(files), **options + method="put", url=path, json_data=body, content=content, files=to_httpx_files(files), **options ) return self.request(cast_to, opts) @@ -1292,9 +1351,19 @@ def delete( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, options: RequestOptions = {}, ) -> ResponseT: - opts = FinalRequestOptions.construct(method="delete", url=path, json_data=body, **options) + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) + opts = FinalRequestOptions.construct(method="delete", url=path, json_data=body, content=content, **options) return self.request(cast_to, opts) def get_api_list( @@ -1749,6 +1818,7 @@ async def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, stream: Literal[False] = False, @@ -1761,6 +1831,7 @@ async def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, stream: Literal[True], @@ -1774,6 +1845,7 @@ async def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, stream: bool, @@ -1786,13 +1858,25 @@ async def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, stream: bool = False, stream_cls: type[_AsyncStreamT] | None = None, ) -> ResponseT | _AsyncStreamT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if files is not None and content is not None: + raise TypeError("Passing both `files` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) opts = FinalRequestOptions.construct( - method="post", url=path, json_data=body, files=await async_to_httpx_files(files), **options + method="post", url=path, json_data=body, content=content, files=await async_to_httpx_files(files), **options ) return await self.request(cast_to, opts, stream=stream, stream_cls=stream_cls) @@ -1802,11 +1886,28 @@ async def patch( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, ) -> ResponseT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if files is not None and content is not None: + raise TypeError("Passing both `files` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) opts = FinalRequestOptions.construct( - method="patch", url=path, json_data=body, files=await async_to_httpx_files(files), **options + method="patch", + url=path, + json_data=body, + content=content, + files=await async_to_httpx_files(files), + **options, ) return await self.request(cast_to, opts) @@ -1816,11 +1917,23 @@ async def put( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, ) -> ResponseT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if files is not None and content is not None: + raise TypeError("Passing both `files` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) opts = FinalRequestOptions.construct( - method="put", url=path, json_data=body, files=await async_to_httpx_files(files), **options + method="put", url=path, json_data=body, content=content, files=await async_to_httpx_files(files), **options ) return await self.request(cast_to, opts) @@ -1830,9 +1943,19 @@ async def delete( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, options: RequestOptions = {}, ) -> ResponseT: - opts = FinalRequestOptions.construct(method="delete", url=path, json_data=body, **options) + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) + opts = FinalRequestOptions.construct(method="delete", url=path, json_data=body, content=content, **options) return await self.request(cast_to, opts) def get_api_list( diff --git a/src/openai/_models.py b/src/openai/_models.py index fac59c2cb8..57b5b448be 100644 --- a/src/openai/_models.py +++ b/src/openai/_models.py @@ -3,7 +3,20 @@ import os import inspect import weakref -from typing import TYPE_CHECKING, Any, Type, Tuple, Union, Generic, TypeVar, Callable, Optional, cast +from typing import ( + IO, + TYPE_CHECKING, + Any, + Type, Tuple, + Union, + Generic, + TypeVar, + Callable, + Iterable, + Optional, + AsyncIterable, + cast, +) from datetime import date, datetime from typing_extensions import ( List, @@ -827,6 +840,7 @@ class FinalRequestOptionsInput(TypedDict, total=False): timeout: float | Timeout | None files: HttpxRequestFiles | None idempotency_key: str + content: Union[bytes, bytearray, IO[bytes], Iterable[bytes], AsyncIterable[bytes], None] json_data: Body extra_json: AnyMapping follow_redirects: bool @@ -845,6 +859,7 @@ class FinalRequestOptions(pydantic.BaseModel): post_parser: Union[Callable[[Any], Any], NotGiven] = NotGiven() follow_redirects: Union[bool, None] = None + content: Union[bytes, bytearray, IO[bytes], Iterable[bytes], AsyncIterable[bytes], None] = None # It should be noted that we cannot use `json` here as that would override # a BaseModel method in an incompatible fashion. json_data: Union[Body, None] = None diff --git a/src/openai/_types.py b/src/openai/_types.py index d7e2eaac5f..42f9df2373 100644 --- a/src/openai/_types.py +++ b/src/openai/_types.py @@ -13,9 +13,11 @@ Mapping, TypeVar, Callable, + Iterable, Iterator, Optional, Sequence, + AsyncIterable, ) from typing_extensions import ( Set, @@ -57,6 +59,13 @@ else: Base64FileInput = Union[IO[bytes], PathLike] FileContent = Union[IO[bytes], bytes, PathLike] # PathLike is not subscriptable in Python 3.8. + + +# Used for sending raw binary data / streaming data in request bodies +# e.g. for file uploads without multipart encoding +BinaryTypes = Union[bytes, bytearray, IO[bytes], Iterable[bytes]] +AsyncBinaryTypes = Union[bytes, bytearray, IO[bytes], AsyncIterable[bytes]] + FileTypes = Union[ # file (or bytes) FileContent, diff --git a/tests/test_client.py b/tests/test_client.py index e8d62f17f7..bd243f68dc 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -8,10 +8,11 @@ import json import asyncio import inspect +import dataclasses import tracemalloc -from typing import Any, Union, Protocol, cast +from typing import Any, Union, Protocol, TypeVar, Callable, Iterable, Iterator, Optional, Coroutine, cast from unittest import mock -from typing_extensions import Literal +from typing_extensions import Literal, AsyncIterator, override import httpx import pytest @@ -37,6 +38,7 @@ from .utils import update_env +T = TypeVar("T") base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") api_key = "My API Key" @@ -55,6 +57,57 @@ def _low_retry_timeout(*_args: Any, **_kwargs: Any) -> float: return 0.1 +def mirror_request_content(request: httpx.Request) -> httpx.Response: + return httpx.Response(200, content=request.content) + + +# note: we can't use the httpx.MockTransport class as it consumes the request +# body itself, which means we can't test that the body is read lazily +class MockTransport(httpx.BaseTransport, httpx.AsyncBaseTransport): + def __init__( + self, + handler: Callable[[httpx.Request], httpx.Response] + | Callable[[httpx.Request], Coroutine[Any, Any, httpx.Response]], + ) -> None: + self.handler = handler + + @override + def handle_request( + self, + request: httpx.Request, + ) -> httpx.Response: + assert not inspect.iscoroutinefunction(self.handler), "handler must not be a coroutine function" + assert inspect.isfunction(self.handler), "handler must be a function" + return self.handler(request) + + @override + async def handle_async_request( + self, + request: httpx.Request, + ) -> httpx.Response: + assert inspect.iscoroutinefunction(self.handler), "handler must be a coroutine function" + return await self.handler(request) + + +@dataclasses.dataclass +class Counter: + value: int = 0 + + +def _make_sync_iterator(iterable: Iterable[T], counter: Optional[Counter] = None) -> Iterator[T]: + for item in iterable: + if counter: + counter.value += 1 + yield item + + +async def _make_async_iterator(iterable: Iterable[T], counter: Optional[Counter] = None) -> AsyncIterator[T]: + for item in iterable: + if counter: + counter.value += 1 + yield item + + def _get_open_connections(client: OpenAI | AsyncOpenAI) -> int: transport = client._client._transport assert isinstance(transport, httpx.HTTPTransport) or isinstance(transport, httpx.AsyncHTTPTransport) @@ -507,6 +560,70 @@ def test_multipart_repeating_array(self, client: OpenAI) -> None: b"", ] + @pytest.mark.respx(base_url=base_url) + def test_binary_content_upload(self, respx_mock: MockRouter, client: OpenAI) -> None: + respx_mock.post("/upload").mock(side_effect=mirror_request_content) + + file_content = b"Hello, this is a test file." + + response = client.post( + "/upload", + content=file_content, + cast_to=httpx.Response, + options={"headers": {"Content-Type": "application/octet-stream"}}, + ) + + assert response.status_code == 200 + assert response.request.headers["Content-Type"] == "application/octet-stream" + assert response.content == file_content + + def test_binary_content_upload_with_iterator(self) -> None: + file_content = b"Hello, this is a test file." + counter = Counter() + iterator = _make_sync_iterator([file_content], counter=counter) + + def mock_handler(request: httpx.Request) -> httpx.Response: + assert counter.value == 0, "the request body should not have been read" + return httpx.Response(200, content=request.read()) + + with OpenAI( + base_url=base_url, + api_key=api_key, + _strict_response_validation=True, + http_client=httpx.Client(transport=MockTransport(handler=mock_handler)), + ) as client: + response = client.post( + "/upload", + content=iterator, + cast_to=httpx.Response, + options={"headers": {"Content-Type": "application/octet-stream"}}, + ) + + assert response.status_code == 200 + assert response.request.headers["Content-Type"] == "application/octet-stream" + assert response.content == file_content + assert counter.value == 1 + + @pytest.mark.respx(base_url=base_url) + def test_binary_content_upload_with_body_is_deprecated(self, respx_mock: MockRouter, client: OpenAI) -> None: + respx_mock.post("/upload").mock(side_effect=mirror_request_content) + + file_content = b"Hello, this is a test file." + + with pytest.deprecated_call( + match="Passing raw bytes as `body` is deprecated and will be removed in a future version. Please pass raw bytes via the `content` parameter instead." + ): + response = client.post( + "/upload", + body=file_content, + cast_to=httpx.Response, + options={"headers": {"Content-Type": "application/octet-stream"}}, + ) + + assert response.status_code == 200 + assert response.request.headers["Content-Type"] == "application/octet-stream" + assert response.content == file_content + @pytest.mark.respx(base_url=base_url) def test_basic_union_response(self, respx_mock: MockRouter, client: OpenAI) -> None: class Model1(BaseModel): @@ -1467,6 +1584,72 @@ def test_multipart_repeating_array(self, async_client: AsyncOpenAI) -> None: b"", ] + @pytest.mark.respx(base_url=base_url) + async def test_binary_content_upload(self, respx_mock: MockRouter, async_client: AsyncOpenAI) -> None: + respx_mock.post("/upload").mock(side_effect=mirror_request_content) + + file_content = b"Hello, this is a test file." + + response = await async_client.post( + "/upload", + content=file_content, + cast_to=httpx.Response, + options={"headers": {"Content-Type": "application/octet-stream"}}, + ) + + assert response.status_code == 200 + assert response.request.headers["Content-Type"] == "application/octet-stream" + assert response.content == file_content + + async def test_binary_content_upload_with_asynciterator(self) -> None: + file_content = b"Hello, this is a test file." + counter = Counter() + iterator = _make_async_iterator([file_content], counter=counter) + + async def mock_handler(request: httpx.Request) -> httpx.Response: + assert counter.value == 0, "the request body should not have been read" + return httpx.Response(200, content=await request.aread()) + + async with AsyncOpenAI( + base_url=base_url, + api_key=api_key, + _strict_response_validation=True, + http_client=httpx.AsyncClient(transport=MockTransport(handler=mock_handler)), + ) as client: + response = await client.post( + "/upload", + content=iterator, + cast_to=httpx.Response, + options={"headers": {"Content-Type": "application/octet-stream"}}, + ) + + assert response.status_code == 200 + assert response.request.headers["Content-Type"] == "application/octet-stream" + assert response.content == file_content + assert counter.value == 1 + + @pytest.mark.respx(base_url=base_url) + async def test_binary_content_upload_with_body_is_deprecated( + self, respx_mock: MockRouter, async_client: AsyncOpenAI + ) -> None: + respx_mock.post("/upload").mock(side_effect=mirror_request_content) + + file_content = b"Hello, this is a test file." + + with pytest.deprecated_call( + match="Passing raw bytes as `body` is deprecated and will be removed in a future version. Please pass raw bytes via the `content` parameter instead." + ): + response = await async_client.post( + "/upload", + body=file_content, + cast_to=httpx.Response, + options={"headers": {"Content-Type": "application/octet-stream"}}, + ) + + assert response.status_code == 200 + assert response.request.headers["Content-Type"] == "application/octet-stream" + assert response.content == file_content + @pytest.mark.respx(base_url=base_url) async def test_basic_union_response(self, respx_mock: MockRouter, async_client: AsyncOpenAI) -> None: class Model1(BaseModel): From f2767144c11833070c0579063ed33918089b4617 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 16 Jan 2026 18:34:06 +0000 Subject: [PATCH 02/10] chore(internal): update `actions/checkout` version --- .github/workflows/ci.yml | 8 ++++---- .github/workflows/create-releases.yml | 2 +- .github/workflows/detect-breaking-changes.yml | 2 +- .github/workflows/publish-pypi.yml | 2 +- .github/workflows/release-doctor.yml | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4c617a6f19..0dc13082f6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,7 @@ jobs: runs-on: ${{ github.repository == 'stainless-sdks/openai-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} if: github.event_name == 'push' || github.event.pull_request.head.repo.fork steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install Rye run: | @@ -44,7 +44,7 @@ jobs: id-token: write runs-on: ${{ github.repository == 'stainless-sdks/openai-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install Rye run: | @@ -81,7 +81,7 @@ jobs: runs-on: ${{ github.repository == 'stainless-sdks/openai-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} if: github.event_name == 'push' || github.event.pull_request.head.repo.fork steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install Rye run: | @@ -104,7 +104,7 @@ jobs: if: github.repository == 'openai/openai-python' && (github.event_name == 'push' || github.event.pull_request.head.repo.fork) steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install Rye run: | diff --git a/.github/workflows/create-releases.yml b/.github/workflows/create-releases.yml index b3e1c679d4..f0ef434d02 100644 --- a/.github/workflows/create-releases.yml +++ b/.github/workflows/create-releases.yml @@ -14,7 +14,7 @@ jobs: environment: publish steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: stainless-api/trigger-release-please@v1 id: release diff --git a/.github/workflows/detect-breaking-changes.yml b/.github/workflows/detect-breaking-changes.yml index 6626d1c376..5f4ecdd97f 100644 --- a/.github/workflows/detect-breaking-changes.yml +++ b/.github/workflows/detect-breaking-changes.yml @@ -15,7 +15,7 @@ jobs: run: | echo "FETCH_DEPTH=$(expr ${{ github.event.pull_request.commits }} + 1)" >> $GITHUB_ENV - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: # Ensure we can check out the pull request base in the script below. fetch-depth: ${{ env.FETCH_DEPTH }} diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index 32bd6929e2..b9c959ecf3 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -11,7 +11,7 @@ jobs: environment: publish steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install Rye run: | diff --git a/.github/workflows/release-doctor.yml b/.github/workflows/release-doctor.yml index e078964a6f..be7db8df12 100644 --- a/.github/workflows/release-doctor.yml +++ b/.github/workflows/release-doctor.yml @@ -13,7 +13,7 @@ jobs: if: github.repository == 'openai/openai-python' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'release-please') || github.head_ref == 'next') steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Check release environment run: | From b97f9f26b9c46ca4519130e60a8bf12ad8d52bf3 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 19 Jan 2026 16:36:17 +0000 Subject: [PATCH 03/10] feat(api): api update --- .stats.yml | 4 +-- src/openai/resources/files.py | 26 +++++++++++++------ src/openai/types/file_create_params.py | 14 +++++----- .../conversation_item_create_event.py | 12 ++++++--- .../conversation_item_create_event_param.py | 12 ++++++--- 5 files changed, 44 insertions(+), 24 deletions(-) diff --git a/.stats.yml b/.stats.yml index fe01e150f9..7234d96b78 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 137 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/openai%2Fopenai-9442fa9212dd61aac2bb0edd19744bee381e75888712f9098bc6ebb92c52b557.yml -openapi_spec_hash: f87823d164b7a8f72a42eba04e482a99 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/openai%2Fopenai-2dede2c933d4c80020715c5e1a21c86b353de336e4dd2c6119125e3eaca6f904.yml +openapi_spec_hash: 52ed6a83d460d3b2bf78e54bac8c503d config_hash: ad7136f7366fddec432ec378939e58a7 diff --git a/src/openai/resources/files.py b/src/openai/resources/files.py index cc117e7f15..202315be6c 100644 --- a/src/openai/resources/files.py +++ b/src/openai/resources/files.py @@ -91,10 +91,15 @@ def create( Args: file: The File object (not file name) to be uploaded. - purpose: The intended purpose of the uploaded file. One of: - `assistants`: Used in the - Assistants API - `batch`: Used in the Batch API - `fine-tune`: Used for - fine-tuning - `vision`: Images used for vision fine-tuning - `user_data`: - Flexible file type for any purpose - `evals`: Used for eval data sets + purpose: + The intended purpose of the uploaded file. One of: + + - `assistants`: Used in the Assistants API + - `batch`: Used in the Batch API + - `fine-tune`: Used for fine-tuning + - `vision`: Images used for vision fine-tuning + - `user_data`: Flexible file type for any purpose + - `evals`: Used for eval data sets expires_after: The expiration policy for a file. By default, files with `purpose=batch` expire after 30 days and all other files are persisted until they are manually deleted. @@ -407,10 +412,15 @@ async def create( Args: file: The File object (not file name) to be uploaded. - purpose: The intended purpose of the uploaded file. One of: - `assistants`: Used in the - Assistants API - `batch`: Used in the Batch API - `fine-tune`: Used for - fine-tuning - `vision`: Images used for vision fine-tuning - `user_data`: - Flexible file type for any purpose - `evals`: Used for eval data sets + purpose: + The intended purpose of the uploaded file. One of: + + - `assistants`: Used in the Assistants API + - `batch`: Used in the Batch API + - `fine-tune`: Used for fine-tuning + - `vision`: Images used for vision fine-tuning + - `user_data`: Flexible file type for any purpose + - `evals`: Used for eval data sets expires_after: The expiration policy for a file. By default, files with `purpose=batch` expire after 30 days and all other files are persisted until they are manually deleted. diff --git a/src/openai/types/file_create_params.py b/src/openai/types/file_create_params.py index f4367f7a7d..5e2afc0655 100644 --- a/src/openai/types/file_create_params.py +++ b/src/openai/types/file_create_params.py @@ -15,12 +15,14 @@ class FileCreateParams(TypedDict, total=False): """The File object (not file name) to be uploaded.""" purpose: Required[FilePurpose] - """The intended purpose of the uploaded file. - - One of: - `assistants`: Used in the Assistants API - `batch`: Used in the Batch - API - `fine-tune`: Used for fine-tuning - `vision`: Images used for vision - fine-tuning - `user_data`: Flexible file type for any purpose - `evals`: Used - for eval data sets + """The intended purpose of the uploaded file. One of: + + - `assistants`: Used in the Assistants API + - `batch`: Used in the Batch API + - `fine-tune`: Used for fine-tuning + - `vision`: Images used for vision fine-tuning + - `user_data`: Flexible file type for any purpose + - `evals`: Used for eval data sets """ expires_after: ExpiresAfter diff --git a/src/openai/types/realtime/conversation_item_create_event.py b/src/openai/types/realtime/conversation_item_create_event.py index bf2d129744..fd0fc00fa2 100644 --- a/src/openai/types/realtime/conversation_item_create_event.py +++ b/src/openai/types/realtime/conversation_item_create_event.py @@ -32,8 +32,12 @@ class ConversationItemCreateEvent(BaseModel): previous_item_id: Optional[str] = None """The ID of the preceding item after which the new item will be inserted. - If not set, the new item will be appended to the end of the conversation. If set - to `root`, the new item will be added to the beginning of the conversation. If - set to an existing ID, it allows an item to be inserted mid-conversation. If the - ID cannot be found, an error will be returned and the item will not be added. + If not set, the new item will be appended to the end of the conversation. + + If set to `root`, the new item will be added to the beginning of the + conversation. + + If set to an existing ID, it allows an item to be inserted mid-conversation. If + the ID cannot be found, an error will be returned and the item will not be + added. """ diff --git a/src/openai/types/realtime/conversation_item_create_event_param.py b/src/openai/types/realtime/conversation_item_create_event_param.py index be7f0ff011..e991e37c3b 100644 --- a/src/openai/types/realtime/conversation_item_create_event_param.py +++ b/src/openai/types/realtime/conversation_item_create_event_param.py @@ -32,8 +32,12 @@ class ConversationItemCreateEventParam(TypedDict, total=False): previous_item_id: str """The ID of the preceding item after which the new item will be inserted. - If not set, the new item will be appended to the end of the conversation. If set - to `root`, the new item will be added to the beginning of the conversation. If - set to an existing ID, it allows an item to be inserted mid-conversation. If the - ID cannot be found, an error will be returned and the item will not be added. + If not set, the new item will be appended to the end of the conversation. + + If set to `root`, the new item will be added to the beginning of the + conversation. + + If set to an existing ID, it allows an item to be inserted mid-conversation. If + the ID cannot be found, an error will be returned and the item will not be + added. """ From b2481f59f49d56c2f5d738ef8c18142c601360ea Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 22 Jan 2026 05:15:30 +0000 Subject: [PATCH 04/10] codegen metadata --- .stats.yml | 4 ++-- src/openai/_models.py | 3 ++- src/openai/lib/_realtime.py | 4 ++-- tests/test_client.py | 2 +- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/.stats.yml b/.stats.yml index 7234d96b78..61563b1404 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 137 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/openai%2Fopenai-2dede2c933d4c80020715c5e1a21c86b353de336e4dd2c6119125e3eaca6f904.yml -openapi_spec_hash: 52ed6a83d460d3b2bf78e54bac8c503d +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/openai%2Fopenai-51075c225b913785fbb988c5d94e2da629241bf0c414b67b6301ddfffb685a83.yml +openapi_spec_hash: 43be3e1dc6823299a6db37d999aa6f11 config_hash: ad7136f7366fddec432ec378939e58a7 diff --git a/src/openai/_models.py b/src/openai/_models.py index 57b5b448be..5cca20c6f9 100644 --- a/src/openai/_models.py +++ b/src/openai/_models.py @@ -7,7 +7,8 @@ IO, TYPE_CHECKING, Any, - Type, Tuple, + Type, + Tuple, Union, Generic, TypeVar, diff --git a/src/openai/lib/_realtime.py b/src/openai/lib/_realtime.py index 999d1e4463..3771b52986 100644 --- a/src/openai/lib/_realtime.py +++ b/src/openai/lib/_realtime.py @@ -34,7 +34,7 @@ def create( extra_headers = {"Accept": "application/sdp", "Content-Type": "application/sdp", **(extra_headers or {})} return self._post( "/realtime/calls", - body=sdp.encode("utf-8"), + content=sdp.encode("utf-8"), options=make_request_options(extra_headers=extra_headers, extra_query=extra_query, timeout=timeout), cast_to=_legacy_response.HttpxBinaryResponseContent, ) @@ -71,7 +71,7 @@ async def create( extra_headers = {"Accept": "application/sdp", "Content-Type": "application/sdp", **(extra_headers or {})} return await self._post( "/realtime/calls", - body=sdp.encode("utf-8"), + content=sdp.encode("utf-8"), options=make_request_options(extra_headers=extra_headers, extra_query=extra_query, timeout=timeout), cast_to=_legacy_response.HttpxBinaryResponseContent, ) diff --git a/tests/test_client.py b/tests/test_client.py index bd243f68dc..396f6dea99 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -10,7 +10,7 @@ import inspect import dataclasses import tracemalloc -from typing import Any, Union, Protocol, TypeVar, Callable, Iterable, Iterator, Optional, Coroutine, cast +from typing import Any, Union, TypeVar, Callable, Iterable, Iterator, Optional, Protocol, Coroutine, cast from unittest import mock from typing_extensions import Literal, AsyncIterator, override From a2746531ba67efb97e296df00e271c6ef48e48a3 Mon Sep 17 00:00:00 2001 From: Charlie Guo Date: Thu, 22 Jan 2026 15:07:34 -0800 Subject: [PATCH 05/10] Update README models to gpt-5.2 --- README.md | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index b8050a4cd6..7e4f0ae657 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ client = OpenAI( ) response = client.responses.create( - model="gpt-4o", + model="gpt-5.2", instructions="You are a coding assistant that talks like a pirate.", input="How do I check if a Python object is an instance of a class?", ) @@ -52,7 +52,7 @@ from openai import OpenAI client = OpenAI() completion = client.chat.completions.create( - model="gpt-4o", + model="gpt-5.2", messages=[ {"role": "developer", "content": "Talk like a pirate."}, { @@ -80,7 +80,7 @@ prompt = "What is in this image?" img_url = "https://upload.wikimedia.org/wikipedia/commons/thumb/d/d5/2023_06_08_Raccoon1.jpg/1599px-2023_06_08_Raccoon1.jpg" response = client.responses.create( - model="gpt-4o-mini", + model="gpt-5.2", input=[ { "role": "user", @@ -106,7 +106,7 @@ with open("path/to/image.png", "rb") as image_file: b64_image = base64.b64encode(image_file.read()).decode("utf-8") response = client.responses.create( - model="gpt-4o-mini", + model="gpt-5.2", input=[ { "role": "user", @@ -136,7 +136,7 @@ client = AsyncOpenAI( async def main() -> None: response = await client.responses.create( - model="gpt-4o", input="Explain disestablishmentarianism to a smart five year old." + model="gpt-5.2", input="Explain disestablishmentarianism to a smart five year old." ) print(response.output_text) @@ -178,7 +178,7 @@ async def main() -> None: "content": "Say this is a test", } ], - model="gpt-4o", + model="gpt-5.2", ) @@ -195,7 +195,7 @@ from openai import OpenAI client = OpenAI() stream = client.responses.create( - model="gpt-4o", + model="gpt-5.2", input="Write a one-sentence bedtime story about a unicorn.", stream=True, ) @@ -215,7 +215,7 @@ client = AsyncOpenAI() async def main(): stream = await client.responses.create( - model="gpt-4o", + model="gpt-5.2", input="Write a one-sentence bedtime story about a unicorn.", stream=True, ) @@ -386,7 +386,7 @@ response = client.chat.responses.create( "content": "How much ?", } ], - model="gpt-4o", + model="gpt-5.2", response_format={"type": "json_object"}, ) ``` @@ -541,7 +541,7 @@ All object responses in the SDK provide a `_request_id` property which is added ```python response = await client.responses.create( - model="gpt-4o-mini", + model="gpt-5.2", input="Say 'this is a test'.", ) print(response._request_id) # req_123 @@ -559,7 +559,7 @@ import openai try: completion = await client.chat.completions.create( - messages=[{"role": "user", "content": "Say this is a test"}], model="gpt-4" + messages=[{"role": "user", "content": "Say this is a test"}], model="gpt-5.2" ) except openai.APIStatusError as exc: print(exc.request_id) # req_123 @@ -591,7 +591,7 @@ client.with_options(max_retries=5).chat.completions.create( "content": "How can I get the name of the current day in JavaScript?", } ], - model="gpt-4o", + model="gpt-5.2", ) ``` @@ -622,7 +622,7 @@ client.with_options(timeout=5.0).chat.completions.create( "content": "How can I list all files in a directory using Python?", } ], - model="gpt-4o", + model="gpt-5.2", ) ``` @@ -669,7 +669,7 @@ response = client.chat.completions.with_raw_response.create( "role": "user", "content": "Say this is a test", }], - model="gpt-4o", + model="gpt-5.2", ) print(response.headers.get('X-My-Header')) @@ -702,7 +702,7 @@ with client.chat.completions.with_streaming_response.create( "content": "Say this is a test", } ], - model="gpt-4o", + model="gpt-5.2", ) as response: print(response.headers.get("X-My-Header")) From 5139f13ef35e64dadc65f2ba2bab736977985769 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 23 Jan 2026 18:18:03 +0000 Subject: [PATCH 06/10] chore(ci): upgrade `actions/github-script` --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0dc13082f6..d087636a64 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -63,7 +63,7 @@ jobs: - name: Get GitHub OIDC Token if: github.repository == 'stainless-sdks/openai-python' id: github-oidc - uses: actions/github-script@v6 + uses: actions/github-script@v8 with: script: core.setOutput('github_token', await core.getIDToken()); From 0419cbcbf1021131c7492321436ed01ca4337835 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 23 Jan 2026 21:31:03 +0000 Subject: [PATCH 07/10] fix(api): mark assistants as deprecated --- .stats.yml | 4 +- src/openai/resources/beta/assistants.py | 131 +++-- src/openai/resources/files.py | 8 +- src/openai/resources/videos.py | 10 +- src/openai/types/video_create_params.py | 5 +- tests/api_resources/beta/test_assistants.py | 532 +++++++++++--------- tests/api_resources/test_videos.py | 2 + 7 files changed, 408 insertions(+), 284 deletions(-) diff --git a/.stats.yml b/.stats.yml index 61563b1404..4f516a48fa 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 137 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/openai%2Fopenai-51075c225b913785fbb988c5d94e2da629241bf0c414b67b6301ddfffb685a83.yml -openapi_spec_hash: 43be3e1dc6823299a6db37d999aa6f11 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/openai%2Fopenai-a47fcdd0fd85e2910e56b34ab3239edbb50957af8dca11db4184d3ba2cae9ad8.yml +openapi_spec_hash: ff61f44f41561b462da4a930c4eb84df config_hash: ad7136f7366fddec432ec378939e58a7 diff --git a/src/openai/resources/beta/assistants.py b/src/openai/resources/beta/assistants.py index ab0947abf4..8c69700059 100644 --- a/src/openai/resources/beta/assistants.py +++ b/src/openai/resources/beta/assistants.py @@ -2,6 +2,7 @@ from __future__ import annotations +import typing_extensions from typing import Union, Iterable, Optional from typing_extensions import Literal @@ -51,6 +52,7 @@ def with_streaming_response(self) -> AssistantsWithStreamingResponse: """ return AssistantsWithStreamingResponse(self) + @typing_extensions.deprecated("deprecated") def create( self, *, @@ -183,6 +185,7 @@ def create( cast_to=Assistant, ) + @typing_extensions.deprecated("deprecated") def retrieve( self, assistant_id: str, @@ -217,6 +220,7 @@ def retrieve( cast_to=Assistant, ) + @typing_extensions.deprecated("deprecated") def update( self, assistant_id: str, @@ -400,6 +404,7 @@ def update( cast_to=Assistant, ) + @typing_extensions.deprecated("deprecated") def list( self, *, @@ -465,6 +470,7 @@ def list( model=Assistant, ) + @typing_extensions.deprecated("deprecated") def delete( self, assistant_id: str, @@ -520,6 +526,7 @@ def with_streaming_response(self) -> AsyncAssistantsWithStreamingResponse: """ return AsyncAssistantsWithStreamingResponse(self) + @typing_extensions.deprecated("deprecated") async def create( self, *, @@ -652,6 +659,7 @@ async def create( cast_to=Assistant, ) + @typing_extensions.deprecated("deprecated") async def retrieve( self, assistant_id: str, @@ -686,6 +694,7 @@ async def retrieve( cast_to=Assistant, ) + @typing_extensions.deprecated("deprecated") async def update( self, assistant_id: str, @@ -869,6 +878,7 @@ async def update( cast_to=Assistant, ) + @typing_extensions.deprecated("deprecated") def list( self, *, @@ -934,6 +944,7 @@ def list( model=Assistant, ) + @typing_extensions.deprecated("deprecated") async def delete( self, assistant_id: str, @@ -973,20 +984,30 @@ class AssistantsWithRawResponse: def __init__(self, assistants: Assistants) -> None: self._assistants = assistants - self.create = _legacy_response.to_raw_response_wrapper( - assistants.create, + self.create = ( # pyright: ignore[reportDeprecated] + _legacy_response.to_raw_response_wrapper( + assistants.create, # pyright: ignore[reportDeprecated], + ) ) - self.retrieve = _legacy_response.to_raw_response_wrapper( - assistants.retrieve, + self.retrieve = ( # pyright: ignore[reportDeprecated] + _legacy_response.to_raw_response_wrapper( + assistants.retrieve, # pyright: ignore[reportDeprecated], + ) ) - self.update = _legacy_response.to_raw_response_wrapper( - assistants.update, + self.update = ( # pyright: ignore[reportDeprecated] + _legacy_response.to_raw_response_wrapper( + assistants.update, # pyright: ignore[reportDeprecated], + ) ) - self.list = _legacy_response.to_raw_response_wrapper( - assistants.list, + self.list = ( # pyright: ignore[reportDeprecated] + _legacy_response.to_raw_response_wrapper( + assistants.list, # pyright: ignore[reportDeprecated], + ) ) - self.delete = _legacy_response.to_raw_response_wrapper( - assistants.delete, + self.delete = ( # pyright: ignore[reportDeprecated] + _legacy_response.to_raw_response_wrapper( + assistants.delete, # pyright: ignore[reportDeprecated], + ) ) @@ -994,20 +1015,30 @@ class AsyncAssistantsWithRawResponse: def __init__(self, assistants: AsyncAssistants) -> None: self._assistants = assistants - self.create = _legacy_response.async_to_raw_response_wrapper( - assistants.create, + self.create = ( # pyright: ignore[reportDeprecated] + _legacy_response.async_to_raw_response_wrapper( + assistants.create, # pyright: ignore[reportDeprecated], + ) ) - self.retrieve = _legacy_response.async_to_raw_response_wrapper( - assistants.retrieve, + self.retrieve = ( # pyright: ignore[reportDeprecated] + _legacy_response.async_to_raw_response_wrapper( + assistants.retrieve, # pyright: ignore[reportDeprecated], + ) ) - self.update = _legacy_response.async_to_raw_response_wrapper( - assistants.update, + self.update = ( # pyright: ignore[reportDeprecated] + _legacy_response.async_to_raw_response_wrapper( + assistants.update, # pyright: ignore[reportDeprecated], + ) ) - self.list = _legacy_response.async_to_raw_response_wrapper( - assistants.list, + self.list = ( # pyright: ignore[reportDeprecated] + _legacy_response.async_to_raw_response_wrapper( + assistants.list, # pyright: ignore[reportDeprecated], + ) ) - self.delete = _legacy_response.async_to_raw_response_wrapper( - assistants.delete, + self.delete = ( # pyright: ignore[reportDeprecated] + _legacy_response.async_to_raw_response_wrapper( + assistants.delete, # pyright: ignore[reportDeprecated], + ) ) @@ -1015,20 +1046,30 @@ class AssistantsWithStreamingResponse: def __init__(self, assistants: Assistants) -> None: self._assistants = assistants - self.create = to_streamed_response_wrapper( - assistants.create, + self.create = ( # pyright: ignore[reportDeprecated] + to_streamed_response_wrapper( + assistants.create, # pyright: ignore[reportDeprecated], + ) ) - self.retrieve = to_streamed_response_wrapper( - assistants.retrieve, + self.retrieve = ( # pyright: ignore[reportDeprecated] + to_streamed_response_wrapper( + assistants.retrieve, # pyright: ignore[reportDeprecated], + ) ) - self.update = to_streamed_response_wrapper( - assistants.update, + self.update = ( # pyright: ignore[reportDeprecated] + to_streamed_response_wrapper( + assistants.update, # pyright: ignore[reportDeprecated], + ) ) - self.list = to_streamed_response_wrapper( - assistants.list, + self.list = ( # pyright: ignore[reportDeprecated] + to_streamed_response_wrapper( + assistants.list, # pyright: ignore[reportDeprecated], + ) ) - self.delete = to_streamed_response_wrapper( - assistants.delete, + self.delete = ( # pyright: ignore[reportDeprecated] + to_streamed_response_wrapper( + assistants.delete, # pyright: ignore[reportDeprecated], + ) ) @@ -1036,18 +1077,28 @@ class AsyncAssistantsWithStreamingResponse: def __init__(self, assistants: AsyncAssistants) -> None: self._assistants = assistants - self.create = async_to_streamed_response_wrapper( - assistants.create, + self.create = ( # pyright: ignore[reportDeprecated] + async_to_streamed_response_wrapper( + assistants.create, # pyright: ignore[reportDeprecated], + ) ) - self.retrieve = async_to_streamed_response_wrapper( - assistants.retrieve, + self.retrieve = ( # pyright: ignore[reportDeprecated] + async_to_streamed_response_wrapper( + assistants.retrieve, # pyright: ignore[reportDeprecated], + ) ) - self.update = async_to_streamed_response_wrapper( - assistants.update, + self.update = ( # pyright: ignore[reportDeprecated] + async_to_streamed_response_wrapper( + assistants.update, # pyright: ignore[reportDeprecated], + ) ) - self.list = async_to_streamed_response_wrapper( - assistants.list, + self.list = ( # pyright: ignore[reportDeprecated] + async_to_streamed_response_wrapper( + assistants.list, # pyright: ignore[reportDeprecated], + ) ) - self.delete = async_to_streamed_response_wrapper( - assistants.delete, + self.delete = ( # pyright: ignore[reportDeprecated] + async_to_streamed_response_wrapper( + assistants.delete, # pyright: ignore[reportDeprecated], + ) ) diff --git a/src/openai/resources/files.py b/src/openai/resources/files.py index 202315be6c..964d6505e7 100644 --- a/src/openai/resources/files.py +++ b/src/openai/resources/files.py @@ -68,8 +68,8 @@ def create( """Upload a file that can be used across various endpoints. Individual files can be - up to 512 MB, and the size of all files uploaded by one organization can be up - to 1 TB. + up to 512 MB, and each project can store up to 2.5 TB of files in total. There + is no organization-wide storage limit. - The Assistants API supports files up to 2 million tokens and of specific file types. See the @@ -389,8 +389,8 @@ async def create( """Upload a file that can be used across various endpoints. Individual files can be - up to 512 MB, and the size of all files uploaded by one organization can be up - to 1 TB. + up to 512 MB, and each project can store up to 2.5 TB of files in total. There + is no organization-wide storage limit. - The Assistants API supports files up to 2 million tokens and of specific file types. See the diff --git a/src/openai/resources/videos.py b/src/openai/resources/videos.py index 9f74c942bc..8d038ed6d3 100644 --- a/src/openai/resources/videos.py +++ b/src/openai/resources/videos.py @@ -16,7 +16,7 @@ video_create_params, video_download_content_params, ) -from .._types import Body, Omit, Query, Headers, NotGiven, FileTypes, omit, not_given +from .._types import Body, Omit, Query, Headers, NotGiven, FileTypes, SequenceNotStr, omit, not_given from .._utils import extract_files, maybe_transform, deepcopy_minimal, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource @@ -64,6 +64,7 @@ def create( self, *, prompt: str, + character_ids: SequenceNotStr[str] | Omit = omit, input_reference: FileTypes | Omit = omit, model: VideoModelParam | Omit = omit, seconds: VideoSeconds | Omit = omit, @@ -81,6 +82,8 @@ def create( Args: prompt: Text prompt that describes the video to generate. + character_ids: Character IDs to include in the generation. + input_reference: Optional image reference that guides generation. model: The video generation model to use (allowed values: sora-2, sora-2-pro). Defaults @@ -102,6 +105,7 @@ def create( body = deepcopy_minimal( { "prompt": prompt, + "character_ids": character_ids, "input_reference": input_reference, "model": model, "seconds": seconds, @@ -419,6 +423,7 @@ async def create( self, *, prompt: str, + character_ids: SequenceNotStr[str] | Omit = omit, input_reference: FileTypes | Omit = omit, model: VideoModelParam | Omit = omit, seconds: VideoSeconds | Omit = omit, @@ -436,6 +441,8 @@ async def create( Args: prompt: Text prompt that describes the video to generate. + character_ids: Character IDs to include in the generation. + input_reference: Optional image reference that guides generation. model: The video generation model to use (allowed values: sora-2, sora-2-pro). Defaults @@ -457,6 +464,7 @@ async def create( body = deepcopy_minimal( { "prompt": prompt, + "character_ids": character_ids, "input_reference": input_reference, "model": model, "seconds": seconds, diff --git a/src/openai/types/video_create_params.py b/src/openai/types/video_create_params.py index d787aaeddd..b5f931af4f 100644 --- a/src/openai/types/video_create_params.py +++ b/src/openai/types/video_create_params.py @@ -4,7 +4,7 @@ from typing_extensions import Required, TypedDict -from .._types import FileTypes +from .._types import FileTypes, SequenceNotStr from .video_size import VideoSize from .video_seconds import VideoSeconds from .video_model_param import VideoModelParam @@ -16,6 +16,9 @@ class VideoCreateParams(TypedDict, total=False): prompt: Required[str] """Text prompt that describes the video to generate.""" + character_ids: SequenceNotStr[str] + """Character IDs to include in the generation.""" + input_reference: FileTypes """Optional image reference that guides generation.""" diff --git a/tests/api_resources/beta/test_assistants.py b/tests/api_resources/beta/test_assistants.py index 2557735426..3e85b56dcc 100644 --- a/tests/api_resources/beta/test_assistants.py +++ b/tests/api_resources/beta/test_assistants.py @@ -15,6 +15,8 @@ AssistantDeleted, ) +# pyright: reportDeprecated=false + base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -23,45 +25,50 @@ class TestAssistants: @parametrize def test_method_create(self, client: OpenAI) -> None: - assistant = client.beta.assistants.create( - model="gpt-4o", - ) + with pytest.warns(DeprecationWarning): + assistant = client.beta.assistants.create( + model="gpt-4o", + ) + assert_matches_type(Assistant, assistant, path=["response"]) @parametrize def test_method_create_with_all_params(self, client: OpenAI) -> None: - assistant = client.beta.assistants.create( - model="gpt-4o", - description="description", - instructions="instructions", - metadata={"foo": "string"}, - name="name", - reasoning_effort="none", - response_format="auto", - temperature=1, - tool_resources={ - "code_interpreter": {"file_ids": ["string"]}, - "file_search": { - "vector_store_ids": ["string"], - "vector_stores": [ - { - "chunking_strategy": {"type": "auto"}, - "file_ids": ["string"], - "metadata": {"foo": "string"}, - } - ], + with pytest.warns(DeprecationWarning): + assistant = client.beta.assistants.create( + model="gpt-4o", + description="description", + instructions="instructions", + metadata={"foo": "string"}, + name="name", + reasoning_effort="none", + response_format="auto", + temperature=1, + tool_resources={ + "code_interpreter": {"file_ids": ["string"]}, + "file_search": { + "vector_store_ids": ["string"], + "vector_stores": [ + { + "chunking_strategy": {"type": "auto"}, + "file_ids": ["string"], + "metadata": {"foo": "string"}, + } + ], + }, }, - }, - tools=[{"type": "code_interpreter"}], - top_p=1, - ) + tools=[{"type": "code_interpreter"}], + top_p=1, + ) + assert_matches_type(Assistant, assistant, path=["response"]) @parametrize def test_raw_response_create(self, client: OpenAI) -> None: - response = client.beta.assistants.with_raw_response.create( - model="gpt-4o", - ) + with pytest.warns(DeprecationWarning): + response = client.beta.assistants.with_raw_response.create( + model="gpt-4o", + ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -70,29 +77,33 @@ def test_raw_response_create(self, client: OpenAI) -> None: @parametrize def test_streaming_response_create(self, client: OpenAI) -> None: - with client.beta.assistants.with_streaming_response.create( - model="gpt-4o", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" + with pytest.warns(DeprecationWarning): + with client.beta.assistants.with_streaming_response.create( + model="gpt-4o", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" - assistant = response.parse() - assert_matches_type(Assistant, assistant, path=["response"]) + assistant = response.parse() + assert_matches_type(Assistant, assistant, path=["response"]) assert cast(Any, response.is_closed) is True @parametrize def test_method_retrieve(self, client: OpenAI) -> None: - assistant = client.beta.assistants.retrieve( - "assistant_id", - ) + with pytest.warns(DeprecationWarning): + assistant = client.beta.assistants.retrieve( + "assistant_id", + ) + assert_matches_type(Assistant, assistant, path=["response"]) @parametrize def test_raw_response_retrieve(self, client: OpenAI) -> None: - response = client.beta.assistants.with_raw_response.retrieve( - "assistant_id", - ) + with pytest.warns(DeprecationWarning): + response = client.beta.assistants.with_raw_response.retrieve( + "assistant_id", + ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -101,57 +112,64 @@ def test_raw_response_retrieve(self, client: OpenAI) -> None: @parametrize def test_streaming_response_retrieve(self, client: OpenAI) -> None: - with client.beta.assistants.with_streaming_response.retrieve( - "assistant_id", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" + with pytest.warns(DeprecationWarning): + with client.beta.assistants.with_streaming_response.retrieve( + "assistant_id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" - assistant = response.parse() - assert_matches_type(Assistant, assistant, path=["response"]) + assistant = response.parse() + assert_matches_type(Assistant, assistant, path=["response"]) assert cast(Any, response.is_closed) is True @parametrize def test_path_params_retrieve(self, client: OpenAI) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `assistant_id` but received ''"): - client.beta.assistants.with_raw_response.retrieve( - "", - ) + with pytest.warns(DeprecationWarning): + with pytest.raises(ValueError, match=r"Expected a non-empty value for `assistant_id` but received ''"): + client.beta.assistants.with_raw_response.retrieve( + "", + ) @parametrize def test_method_update(self, client: OpenAI) -> None: - assistant = client.beta.assistants.update( - assistant_id="assistant_id", - ) + with pytest.warns(DeprecationWarning): + assistant = client.beta.assistants.update( + assistant_id="assistant_id", + ) + assert_matches_type(Assistant, assistant, path=["response"]) @parametrize def test_method_update_with_all_params(self, client: OpenAI) -> None: - assistant = client.beta.assistants.update( - assistant_id="assistant_id", - description="description", - instructions="instructions", - metadata={"foo": "string"}, - model="string", - name="name", - reasoning_effort="none", - response_format="auto", - temperature=1, - tool_resources={ - "code_interpreter": {"file_ids": ["string"]}, - "file_search": {"vector_store_ids": ["string"]}, - }, - tools=[{"type": "code_interpreter"}], - top_p=1, - ) + with pytest.warns(DeprecationWarning): + assistant = client.beta.assistants.update( + assistant_id="assistant_id", + description="description", + instructions="instructions", + metadata={"foo": "string"}, + model="string", + name="name", + reasoning_effort="none", + response_format="auto", + temperature=1, + tool_resources={ + "code_interpreter": {"file_ids": ["string"]}, + "file_search": {"vector_store_ids": ["string"]}, + }, + tools=[{"type": "code_interpreter"}], + top_p=1, + ) + assert_matches_type(Assistant, assistant, path=["response"]) @parametrize def test_raw_response_update(self, client: OpenAI) -> None: - response = client.beta.assistants.with_raw_response.update( - assistant_id="assistant_id", - ) + with pytest.warns(DeprecationWarning): + response = client.beta.assistants.with_raw_response.update( + assistant_id="assistant_id", + ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -160,42 +178,49 @@ def test_raw_response_update(self, client: OpenAI) -> None: @parametrize def test_streaming_response_update(self, client: OpenAI) -> None: - with client.beta.assistants.with_streaming_response.update( - assistant_id="assistant_id", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" + with pytest.warns(DeprecationWarning): + with client.beta.assistants.with_streaming_response.update( + assistant_id="assistant_id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" - assistant = response.parse() - assert_matches_type(Assistant, assistant, path=["response"]) + assistant = response.parse() + assert_matches_type(Assistant, assistant, path=["response"]) assert cast(Any, response.is_closed) is True @parametrize def test_path_params_update(self, client: OpenAI) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `assistant_id` but received ''"): - client.beta.assistants.with_raw_response.update( - assistant_id="", - ) + with pytest.warns(DeprecationWarning): + with pytest.raises(ValueError, match=r"Expected a non-empty value for `assistant_id` but received ''"): + client.beta.assistants.with_raw_response.update( + assistant_id="", + ) @parametrize def test_method_list(self, client: OpenAI) -> None: - assistant = client.beta.assistants.list() + with pytest.warns(DeprecationWarning): + assistant = client.beta.assistants.list() + assert_matches_type(SyncCursorPage[Assistant], assistant, path=["response"]) @parametrize def test_method_list_with_all_params(self, client: OpenAI) -> None: - assistant = client.beta.assistants.list( - after="after", - before="before", - limit=0, - order="asc", - ) + with pytest.warns(DeprecationWarning): + assistant = client.beta.assistants.list( + after="after", + before="before", + limit=0, + order="asc", + ) + assert_matches_type(SyncCursorPage[Assistant], assistant, path=["response"]) @parametrize def test_raw_response_list(self, client: OpenAI) -> None: - response = client.beta.assistants.with_raw_response.list() + with pytest.warns(DeprecationWarning): + response = client.beta.assistants.with_raw_response.list() assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -204,27 +229,31 @@ def test_raw_response_list(self, client: OpenAI) -> None: @parametrize def test_streaming_response_list(self, client: OpenAI) -> None: - with client.beta.assistants.with_streaming_response.list() as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" + with pytest.warns(DeprecationWarning): + with client.beta.assistants.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" - assistant = response.parse() - assert_matches_type(SyncCursorPage[Assistant], assistant, path=["response"]) + assistant = response.parse() + assert_matches_type(SyncCursorPage[Assistant], assistant, path=["response"]) assert cast(Any, response.is_closed) is True @parametrize def test_method_delete(self, client: OpenAI) -> None: - assistant = client.beta.assistants.delete( - "assistant_id", - ) + with pytest.warns(DeprecationWarning): + assistant = client.beta.assistants.delete( + "assistant_id", + ) + assert_matches_type(AssistantDeleted, assistant, path=["response"]) @parametrize def test_raw_response_delete(self, client: OpenAI) -> None: - response = client.beta.assistants.with_raw_response.delete( - "assistant_id", - ) + with pytest.warns(DeprecationWarning): + response = client.beta.assistants.with_raw_response.delete( + "assistant_id", + ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -233,23 +262,25 @@ def test_raw_response_delete(self, client: OpenAI) -> None: @parametrize def test_streaming_response_delete(self, client: OpenAI) -> None: - with client.beta.assistants.with_streaming_response.delete( - "assistant_id", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" + with pytest.warns(DeprecationWarning): + with client.beta.assistants.with_streaming_response.delete( + "assistant_id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" - assistant = response.parse() - assert_matches_type(AssistantDeleted, assistant, path=["response"]) + assistant = response.parse() + assert_matches_type(AssistantDeleted, assistant, path=["response"]) assert cast(Any, response.is_closed) is True @parametrize def test_path_params_delete(self, client: OpenAI) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `assistant_id` but received ''"): - client.beta.assistants.with_raw_response.delete( - "", - ) + with pytest.warns(DeprecationWarning): + with pytest.raises(ValueError, match=r"Expected a non-empty value for `assistant_id` but received ''"): + client.beta.assistants.with_raw_response.delete( + "", + ) class TestAsyncAssistants: @@ -259,45 +290,50 @@ class TestAsyncAssistants: @parametrize async def test_method_create(self, async_client: AsyncOpenAI) -> None: - assistant = await async_client.beta.assistants.create( - model="gpt-4o", - ) + with pytest.warns(DeprecationWarning): + assistant = await async_client.beta.assistants.create( + model="gpt-4o", + ) + assert_matches_type(Assistant, assistant, path=["response"]) @parametrize async def test_method_create_with_all_params(self, async_client: AsyncOpenAI) -> None: - assistant = await async_client.beta.assistants.create( - model="gpt-4o", - description="description", - instructions="instructions", - metadata={"foo": "string"}, - name="name", - reasoning_effort="none", - response_format="auto", - temperature=1, - tool_resources={ - "code_interpreter": {"file_ids": ["string"]}, - "file_search": { - "vector_store_ids": ["string"], - "vector_stores": [ - { - "chunking_strategy": {"type": "auto"}, - "file_ids": ["string"], - "metadata": {"foo": "string"}, - } - ], + with pytest.warns(DeprecationWarning): + assistant = await async_client.beta.assistants.create( + model="gpt-4o", + description="description", + instructions="instructions", + metadata={"foo": "string"}, + name="name", + reasoning_effort="none", + response_format="auto", + temperature=1, + tool_resources={ + "code_interpreter": {"file_ids": ["string"]}, + "file_search": { + "vector_store_ids": ["string"], + "vector_stores": [ + { + "chunking_strategy": {"type": "auto"}, + "file_ids": ["string"], + "metadata": {"foo": "string"}, + } + ], + }, }, - }, - tools=[{"type": "code_interpreter"}], - top_p=1, - ) + tools=[{"type": "code_interpreter"}], + top_p=1, + ) + assert_matches_type(Assistant, assistant, path=["response"]) @parametrize async def test_raw_response_create(self, async_client: AsyncOpenAI) -> None: - response = await async_client.beta.assistants.with_raw_response.create( - model="gpt-4o", - ) + with pytest.warns(DeprecationWarning): + response = await async_client.beta.assistants.with_raw_response.create( + model="gpt-4o", + ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -306,29 +342,33 @@ async def test_raw_response_create(self, async_client: AsyncOpenAI) -> None: @parametrize async def test_streaming_response_create(self, async_client: AsyncOpenAI) -> None: - async with async_client.beta.assistants.with_streaming_response.create( - model="gpt-4o", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" + with pytest.warns(DeprecationWarning): + async with async_client.beta.assistants.with_streaming_response.create( + model="gpt-4o", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" - assistant = await response.parse() - assert_matches_type(Assistant, assistant, path=["response"]) + assistant = await response.parse() + assert_matches_type(Assistant, assistant, path=["response"]) assert cast(Any, response.is_closed) is True @parametrize async def test_method_retrieve(self, async_client: AsyncOpenAI) -> None: - assistant = await async_client.beta.assistants.retrieve( - "assistant_id", - ) + with pytest.warns(DeprecationWarning): + assistant = await async_client.beta.assistants.retrieve( + "assistant_id", + ) + assert_matches_type(Assistant, assistant, path=["response"]) @parametrize async def test_raw_response_retrieve(self, async_client: AsyncOpenAI) -> None: - response = await async_client.beta.assistants.with_raw_response.retrieve( - "assistant_id", - ) + with pytest.warns(DeprecationWarning): + response = await async_client.beta.assistants.with_raw_response.retrieve( + "assistant_id", + ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -337,57 +377,64 @@ async def test_raw_response_retrieve(self, async_client: AsyncOpenAI) -> None: @parametrize async def test_streaming_response_retrieve(self, async_client: AsyncOpenAI) -> None: - async with async_client.beta.assistants.with_streaming_response.retrieve( - "assistant_id", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" + with pytest.warns(DeprecationWarning): + async with async_client.beta.assistants.with_streaming_response.retrieve( + "assistant_id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" - assistant = await response.parse() - assert_matches_type(Assistant, assistant, path=["response"]) + assistant = await response.parse() + assert_matches_type(Assistant, assistant, path=["response"]) assert cast(Any, response.is_closed) is True @parametrize async def test_path_params_retrieve(self, async_client: AsyncOpenAI) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `assistant_id` but received ''"): - await async_client.beta.assistants.with_raw_response.retrieve( - "", - ) + with pytest.warns(DeprecationWarning): + with pytest.raises(ValueError, match=r"Expected a non-empty value for `assistant_id` but received ''"): + await async_client.beta.assistants.with_raw_response.retrieve( + "", + ) @parametrize async def test_method_update(self, async_client: AsyncOpenAI) -> None: - assistant = await async_client.beta.assistants.update( - assistant_id="assistant_id", - ) + with pytest.warns(DeprecationWarning): + assistant = await async_client.beta.assistants.update( + assistant_id="assistant_id", + ) + assert_matches_type(Assistant, assistant, path=["response"]) @parametrize async def test_method_update_with_all_params(self, async_client: AsyncOpenAI) -> None: - assistant = await async_client.beta.assistants.update( - assistant_id="assistant_id", - description="description", - instructions="instructions", - metadata={"foo": "string"}, - model="string", - name="name", - reasoning_effort="none", - response_format="auto", - temperature=1, - tool_resources={ - "code_interpreter": {"file_ids": ["string"]}, - "file_search": {"vector_store_ids": ["string"]}, - }, - tools=[{"type": "code_interpreter"}], - top_p=1, - ) + with pytest.warns(DeprecationWarning): + assistant = await async_client.beta.assistants.update( + assistant_id="assistant_id", + description="description", + instructions="instructions", + metadata={"foo": "string"}, + model="string", + name="name", + reasoning_effort="none", + response_format="auto", + temperature=1, + tool_resources={ + "code_interpreter": {"file_ids": ["string"]}, + "file_search": {"vector_store_ids": ["string"]}, + }, + tools=[{"type": "code_interpreter"}], + top_p=1, + ) + assert_matches_type(Assistant, assistant, path=["response"]) @parametrize async def test_raw_response_update(self, async_client: AsyncOpenAI) -> None: - response = await async_client.beta.assistants.with_raw_response.update( - assistant_id="assistant_id", - ) + with pytest.warns(DeprecationWarning): + response = await async_client.beta.assistants.with_raw_response.update( + assistant_id="assistant_id", + ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -396,42 +443,49 @@ async def test_raw_response_update(self, async_client: AsyncOpenAI) -> None: @parametrize async def test_streaming_response_update(self, async_client: AsyncOpenAI) -> None: - async with async_client.beta.assistants.with_streaming_response.update( - assistant_id="assistant_id", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" + with pytest.warns(DeprecationWarning): + async with async_client.beta.assistants.with_streaming_response.update( + assistant_id="assistant_id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" - assistant = await response.parse() - assert_matches_type(Assistant, assistant, path=["response"]) + assistant = await response.parse() + assert_matches_type(Assistant, assistant, path=["response"]) assert cast(Any, response.is_closed) is True @parametrize async def test_path_params_update(self, async_client: AsyncOpenAI) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `assistant_id` but received ''"): - await async_client.beta.assistants.with_raw_response.update( - assistant_id="", - ) + with pytest.warns(DeprecationWarning): + with pytest.raises(ValueError, match=r"Expected a non-empty value for `assistant_id` but received ''"): + await async_client.beta.assistants.with_raw_response.update( + assistant_id="", + ) @parametrize async def test_method_list(self, async_client: AsyncOpenAI) -> None: - assistant = await async_client.beta.assistants.list() + with pytest.warns(DeprecationWarning): + assistant = await async_client.beta.assistants.list() + assert_matches_type(AsyncCursorPage[Assistant], assistant, path=["response"]) @parametrize async def test_method_list_with_all_params(self, async_client: AsyncOpenAI) -> None: - assistant = await async_client.beta.assistants.list( - after="after", - before="before", - limit=0, - order="asc", - ) + with pytest.warns(DeprecationWarning): + assistant = await async_client.beta.assistants.list( + after="after", + before="before", + limit=0, + order="asc", + ) + assert_matches_type(AsyncCursorPage[Assistant], assistant, path=["response"]) @parametrize async def test_raw_response_list(self, async_client: AsyncOpenAI) -> None: - response = await async_client.beta.assistants.with_raw_response.list() + with pytest.warns(DeprecationWarning): + response = await async_client.beta.assistants.with_raw_response.list() assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -440,27 +494,31 @@ async def test_raw_response_list(self, async_client: AsyncOpenAI) -> None: @parametrize async def test_streaming_response_list(self, async_client: AsyncOpenAI) -> None: - async with async_client.beta.assistants.with_streaming_response.list() as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" + with pytest.warns(DeprecationWarning): + async with async_client.beta.assistants.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" - assistant = await response.parse() - assert_matches_type(AsyncCursorPage[Assistant], assistant, path=["response"]) + assistant = await response.parse() + assert_matches_type(AsyncCursorPage[Assistant], assistant, path=["response"]) assert cast(Any, response.is_closed) is True @parametrize async def test_method_delete(self, async_client: AsyncOpenAI) -> None: - assistant = await async_client.beta.assistants.delete( - "assistant_id", - ) + with pytest.warns(DeprecationWarning): + assistant = await async_client.beta.assistants.delete( + "assistant_id", + ) + assert_matches_type(AssistantDeleted, assistant, path=["response"]) @parametrize async def test_raw_response_delete(self, async_client: AsyncOpenAI) -> None: - response = await async_client.beta.assistants.with_raw_response.delete( - "assistant_id", - ) + with pytest.warns(DeprecationWarning): + response = await async_client.beta.assistants.with_raw_response.delete( + "assistant_id", + ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -469,20 +527,22 @@ async def test_raw_response_delete(self, async_client: AsyncOpenAI) -> None: @parametrize async def test_streaming_response_delete(self, async_client: AsyncOpenAI) -> None: - async with async_client.beta.assistants.with_streaming_response.delete( - "assistant_id", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" + with pytest.warns(DeprecationWarning): + async with async_client.beta.assistants.with_streaming_response.delete( + "assistant_id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" - assistant = await response.parse() - assert_matches_type(AssistantDeleted, assistant, path=["response"]) + assistant = await response.parse() + assert_matches_type(AssistantDeleted, assistant, path=["response"]) assert cast(Any, response.is_closed) is True @parametrize async def test_path_params_delete(self, async_client: AsyncOpenAI) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `assistant_id` but received ''"): - await async_client.beta.assistants.with_raw_response.delete( - "", - ) + with pytest.warns(DeprecationWarning): + with pytest.raises(ValueError, match=r"Expected a non-empty value for `assistant_id` but received ''"): + await async_client.beta.assistants.with_raw_response.delete( + "", + ) diff --git a/tests/api_resources/test_videos.py b/tests/api_resources/test_videos.py index b785e03ca5..2f1bce5f84 100644 --- a/tests/api_resources/test_videos.py +++ b/tests/api_resources/test_videos.py @@ -38,6 +38,7 @@ def test_method_create(self, client: OpenAI) -> None: def test_method_create_with_all_params(self, client: OpenAI) -> None: video = client.videos.create( prompt="x", + character_ids=["char_123"], input_reference=b"raw file contents", model="string", seconds="4", @@ -296,6 +297,7 @@ async def test_method_create(self, async_client: AsyncOpenAI) -> None: async def test_method_create_with_all_params(self, async_client: AsyncOpenAI) -> None: video = await async_client.videos.create( prompt="x", + character_ids=["char_123"], input_reference=b"raw file contents", model="string", seconds="4", From c514c752067b50ffb81e1747c2fb8782a17c2e32 Mon Sep 17 00:00:00 2001 From: Alex Chang Date: Fri, 23 Jan 2026 16:41:22 -0500 Subject: [PATCH 08/10] fix helper --- src/openai/resources/videos.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/openai/resources/videos.py b/src/openai/resources/videos.py index 8d038ed6d3..e201177baa 100644 --- a/src/openai/resources/videos.py +++ b/src/openai/resources/videos.py @@ -132,6 +132,7 @@ def create_and_poll( self, *, prompt: str, + character_ids: SequenceNotStr[str] | Omit = omit, input_reference: FileTypes | Omit = omit, model: VideoModelParam | Omit = omit, seconds: VideoSeconds | Omit = omit, @@ -148,6 +149,7 @@ def create_and_poll( video = self.create( model=model, prompt=prompt, + character_ids=character_ids, input_reference=input_reference, seconds=seconds, size=size, @@ -491,6 +493,7 @@ async def create_and_poll( self, *, prompt: str, + character_ids: SequenceNotStr[str] | Omit = omit, input_reference: FileTypes | Omit = omit, model: VideoModelParam | Omit = omit, seconds: VideoSeconds | Omit = omit, @@ -507,6 +510,7 @@ async def create_and_poll( video = await self.create( model=model, prompt=prompt, + character_ids=character_ids, input_reference=input_reference, seconds=seconds, size=size, From 382cdfa2f3e1783703cf7cb59c0a6327f6de9f64 Mon Sep 17 00:00:00 2001 From: Alex Chang Date: Fri, 23 Jan 2026 22:09:40 -0500 Subject: [PATCH 09/10] fix breaking change detection with deprecations --- .github/workflows/detect-breaking-changes.yml | 4 +--- pyproject.toml | 2 +- scripts/detect-breaking-changes | 2 +- scripts/pyrightconfig.breaking-changes.json | 4 ++++ scripts/run-pyright | 8 ++++++++ 5 files changed, 15 insertions(+), 5 deletions(-) create mode 100644 scripts/pyrightconfig.breaking-changes.json create mode 100755 scripts/run-pyright diff --git a/.github/workflows/detect-breaking-changes.yml b/.github/workflows/detect-breaking-changes.yml index 5f4ecdd97f..87c02061c2 100644 --- a/.github/workflows/detect-breaking-changes.yml +++ b/.github/workflows/detect-breaking-changes.yml @@ -36,9 +36,7 @@ jobs: - name: Detect breaking changes run: | - # Try to check out previous versions of the breaking change detection script. This ensures that - # we still detect breaking changes when entire files and their tests are removed. - git checkout "${{ github.event.pull_request.base.sha }}" -- ./scripts/detect-breaking-changes 2>/dev/null || true + test -f ./scripts/detect-breaking-changes || { echo "Missing scripts/detect-breaking-changes"; exit 1; } ./scripts/detect-breaking-changes ${{ github.event.pull_request.base.sha }} agents_sdk: diff --git a/pyproject.toml b/pyproject.toml index 2d835b05af..8bdc519acf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -101,7 +101,7 @@ typecheck = { chain = [ "typecheck:pyright", "typecheck:mypy" ]} -"typecheck:pyright" = "pyright" +"typecheck:pyright" = "scripts/run-pyright" "typecheck:verify-types" = "pyright --verifytypes openai --ignoreexternal" "typecheck:mypy" = "mypy ." diff --git a/scripts/detect-breaking-changes b/scripts/detect-breaking-changes index 833872ef3a..25b6aa485f 100755 --- a/scripts/detect-breaking-changes +++ b/scripts/detect-breaking-changes @@ -21,4 +21,4 @@ done # Instead of running the tests, use the linter to check if an # older test is no longer compatible with the latest SDK. -./scripts/lint +PYRIGHT_PROJECT=scripts/pyrightconfig.breaking-changes.json ./scripts/lint diff --git a/scripts/pyrightconfig.breaking-changes.json b/scripts/pyrightconfig.breaking-changes.json new file mode 100644 index 0000000000..87bfd3bbc0 --- /dev/null +++ b/scripts/pyrightconfig.breaking-changes.json @@ -0,0 +1,4 @@ +{ + "extends": "../pyproject.toml", + "reportDeprecated": false +} diff --git a/scripts/run-pyright b/scripts/run-pyright new file mode 100755 index 0000000000..1c71ba0587 --- /dev/null +++ b/scripts/run-pyright @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +set -euo pipefail + +cd "$(dirname "$0")/.." + +CONFIG=${PYRIGHT_PROJECT:-pyproject.toml} +exec pyright -p "$CONFIG" "$@" From d8d6702e0670dcad8670b317632e167f3b1b521d Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sun, 25 Jan 2026 05:10:41 +0000 Subject: [PATCH 10/10] release: 2.16.0 --- .release-please-manifest.json | 2 +- CHANGELOG.md | 20 ++++++++++++++++++++ pyproject.toml | 2 +- src/openai/_version.py | 2 +- 4 files changed, 23 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index cff01f26f0..b258565371 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "2.15.0" + ".": "2.16.0" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 4630e50610..aa9a082926 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,25 @@ # Changelog +## 2.16.0 (2026-01-25) + +Full Changelog: [v2.15.0...v2.16.0](https://github.com/openai/openai-python/compare/v2.15.0...v2.16.0) + +### Features + +* **api:** api update ([b97f9f2](https://github.com/openai/openai-python/commit/b97f9f26b9c46ca4519130e60a8bf12ad8d52bf3)) +* **client:** add support for binary request streaming ([49561d8](https://github.com/openai/openai-python/commit/49561d88279628bc400d1b09aa98765b67018ef1)) + + +### Bug Fixes + +* **api:** mark assistants as deprecated ([0419cbc](https://github.com/openai/openai-python/commit/0419cbcbf1021131c7492321436ed01ca4337835)) + + +### Chores + +* **ci:** upgrade `actions/github-script` ([5139f13](https://github.com/openai/openai-python/commit/5139f13ef35e64dadc65f2ba2bab736977985769)) +* **internal:** update `actions/checkout` version ([f276714](https://github.com/openai/openai-python/commit/f2767144c11833070c0579063ed33918089b4617)) + ## 2.15.0 (2026-01-09) Full Changelog: [v2.14.0...v2.15.0](https://github.com/openai/openai-python/compare/v2.14.0...v2.15.0) diff --git a/pyproject.toml b/pyproject.toml index 8bdc519acf..bd75d0096d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "openai" -version = "2.15.0" +version = "2.16.0" description = "The official Python library for the openai API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/openai/_version.py b/src/openai/_version.py index 6f0ccf7d72..eb61bdd2c6 100644 --- a/src/openai/_version.py +++ b/src/openai/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "openai" -__version__ = "2.15.0" # x-release-please-version +__version__ = "2.16.0" # x-release-please-version