From 03a721149086b2eaf3bf4a41334e44fd40b3c13c Mon Sep 17 00:00:00 2001 From: Justin Driemeyer Date: Wed, 21 May 2025 17:48:20 -0700 Subject: [PATCH 01/31] chore(tests): improve ci test names --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4ce23d78..4fd5e4d1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -56,7 +56,7 @@ jobs: test-others: timeout-minutes: 10 - name: test + name: test-others runs-on: ${{ github.repository == 'stainless-sdks/togetherai-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} steps: - uses: actions/checkout@v4 @@ -79,7 +79,7 @@ jobs: test-integration: timeout-minutes: 10 - name: test + name: test-intergration runs-on: ${{ github.repository == 'stainless-sdks/togetherai-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} steps: - uses: actions/checkout@v4 From 145d6aa10007741baad9eadf5cdae4adcf6f6e87 Mon Sep 17 00:00:00 2001 From: Justin Driemeyer Date: Wed, 28 May 2025 18:13:00 -0700 Subject: [PATCH 02/31] Move the files upload into files resource. --- src/together/__init__.py | 14 +++++++ src/together/lib/__init__.py | 23 +++++++++++ src/together/lib/cli/api/files.py | 5 +-- src/together/lib/resources/__init__.py | 13 +++++++ src/together/lib/resources/files.py | 45 +++------------------- src/together/lib/types/__init__.py | 8 +++- src/together/lib/types/error.py | 2 +- src/together/resources/files.py | 53 ++++++++++++++++++++++++++ 8 files changed, 118 insertions(+), 45 deletions(-) create mode 100644 src/together/lib/__init__.py diff --git a/src/together/__init__.py b/src/together/__init__.py index 8adebcd9..f3169ab8 100644 --- a/src/together/__init__.py +++ b/src/together/__init__.py @@ -38,6 +38,14 @@ ) from ._base_client import DefaultHttpxClient, DefaultAsyncHttpxClient from ._utils._logs import setup_logging as _setup_logging +from .lib import ( + create_finetune_request, + DownloadManager, + UploadManager, + FinetuneTrainingLimits, + DownloadError, + FileTypeError, +) __all__ = [ "types", @@ -78,6 +86,12 @@ "DEFAULT_CONNECTION_LIMITS", "DefaultHttpxClient", "DefaultAsyncHttpxClient", + "create_finetune_request", + "DownloadManager", + "UploadManager", + "FinetuneTrainingLimits", + "DownloadError", + "FileTypeError", ] if not _t.TYPE_CHECKING: diff --git a/src/together/lib/__init__.py b/src/together/lib/__init__.py new file mode 100644 index 00000000..108469f0 --- /dev/null +++ b/src/together/lib/__init__.py @@ -0,0 +1,23 @@ +from .resources import ( + create_finetune_request, + DownloadManager, + UploadManager, +) +from .types import ( + FinetuneTrainingLimits, + DownloadError, + FileTypeError, +) +from .utils import ( + check_file, +) + +__all__ = [ + "create_finetune_request", + "DownloadManager", + "UploadManager", + "FinetuneTrainingLimits", + "DownloadError", + "FileTypeError", + "check_file", +] diff --git a/src/together/lib/cli/api/files.py b/src/together/lib/cli/api/files.py index 80370fb9..3b56aa1d 100644 --- a/src/together/lib/cli/api/files.py +++ b/src/together/lib/cli/api/files.py @@ -7,7 +7,6 @@ from tabulate import tabulate from together import Together -from together.lib.resources.files import Files from ...utils import check_file, convert_bytes, convert_unix_timestamp @@ -43,9 +42,7 @@ def upload(ctx: click.Context, file: pathlib.Path, purpose: str, check: bool) -> client: Together = ctx.obj - files_resource = Files(client) - - response = files_resource.upload(file=file, purpose=purpose, check=check) + response = client.files.upload(file=file, purpose=purpose, check=check) click.echo(json.dumps(response.model_dump(exclude_none=True), indent=4)) diff --git a/src/together/lib/resources/__init__.py b/src/together/lib/resources/__init__.py index e69de29b..36c31a47 100644 --- a/src/together/lib/resources/__init__.py +++ b/src/together/lib/resources/__init__.py @@ -0,0 +1,13 @@ +from .fine_tune import ( + create_finetune_request, +) +from .files import ( + DownloadManager, + UploadManager, +) + +__all__ = [ + "create_finetune_request", + "DownloadManager", + "UploadManager", +] diff --git a/src/together/lib/resources/files.py b/src/together/lib/resources/files.py index df8fbf74..13c5fc73 100644 --- a/src/together/lib/resources/files.py +++ b/src/together/lib/resources/files.py @@ -5,8 +5,7 @@ import uuid import shutil import tempfile -from pprint import pformat -from typing import Tuple, cast, get_args +from typing import Tuple from pathlib import Path from functools import partial @@ -15,8 +14,9 @@ from filelock import FileLock from tqdm.utils import CallbackIOWrapper -from ... import Together, APIStatusError, RequestOptions, AuthenticationError -from ..utils import check_file +from ..._exceptions import APIStatusError, AuthenticationError +from ..._client import RequestOptions +from ..._resource import SyncAPIResource from ...types import FileType, FilePurpose, FileRetrieveResponse from ..constants import DISABLE_TQDM, DOWNLOAD_BLOCK_SIZE from ..types.error import DownloadError, FileTypeError @@ -94,9 +94,7 @@ def _prepare_output( return Path(remote_name) -class DownloadManager: - def __init__(self, client: Together) -> None: - self._client = client +class DownloadManager(SyncAPIResource): def get_file_metadata( self, @@ -209,9 +207,7 @@ def download( return str(file_path.resolve()), file_size -class UploadManager: - def __init__(self, client: Together) -> None: - self._client = client +class UploadManager(SyncAPIResource): def get_upload_url( self, @@ -326,32 +322,3 @@ def upload( assert isinstance(response, FileRetrieveResponse) # type: ignore return response - - -class Files: - def __init__(self, client: Together) -> None: - self._client = client - - def upload( - self, - file: Path | str, - *, - purpose: str = "fine-tune", - check: bool = True, - ) -> FileRetrieveResponse: - upload_manager = UploadManager(self._client) - - if check: - report_dict = check_file(file) - if not report_dict["is_check_passed"]: - raise FileTypeError(f"Invalid file supplied, failed to upload. Report:\n{pformat(report_dict)}") - - if isinstance(file, str): - file = Path(file) - - if purpose not in get_args(FilePurpose): - raise ValueError(f"Invalid purpose '{purpose}'. Must be one of: {get_args(FilePurpose)}") - - purpose = cast(FilePurpose, purpose) - - return upload_manager.upload("files", file, purpose=purpose, redirect=True) diff --git a/src/together/lib/types/__init__.py b/src/together/lib/types/__init__.py index d95fce3c..6a0e474a 100644 --- a/src/together/lib/types/__init__.py +++ b/src/together/lib/types/__init__.py @@ -1,7 +1,13 @@ -from together.lib.types.fine_tune import ( +from .fine_tune import ( FinetuneTrainingLimits, ) +from .error import ( + DownloadError, + FileTypeError, +) __all__ = [ "FinetuneTrainingLimits", + "DownloadError", + "FileTypeError", ] diff --git a/src/together/lib/types/error.py b/src/together/lib/types/error.py index 55ba9da4..c482216f 100644 --- a/src/together/lib/types/error.py +++ b/src/together/lib/types/error.py @@ -1,6 +1,6 @@ from typing import Any -from ... import TogetherError +from ..._exceptions import TogetherError class DownloadError(TogetherError): diff --git a/src/together/resources/files.py b/src/together/resources/files.py index 9ffc8199..42e937e0 100644 --- a/src/together/resources/files.py +++ b/src/together/resources/files.py @@ -3,6 +3,9 @@ from __future__ import annotations import httpx +from pprint import pformat +from typing import cast, get_args +from pathlib import Path from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven from .._compat import cached_property @@ -22,9 +25,11 @@ async_to_custom_streamed_response_wrapper, ) from .._base_client import make_request_options +from ..lib import FileTypeError, UploadManager, check_file from ..types.file_list_response import FileListResponse from ..types.file_delete_response import FileDeleteResponse from ..types.file_retrieve_response import FileRetrieveResponse +from ..types.file_purpose import FilePurpose __all__ = ["FilesResource", "AsyncFilesResource"] @@ -134,6 +139,30 @@ def delete( cast_to=FileDeleteResponse, ) + def upload( + self, + file: Path | str, + *, + purpose: str = "fine-tune", + check: bool = True, + ) -> FileRetrieveResponse: + upload_manager = UploadManager(self._client) + + if check: + report_dict = check_file(file) + if not report_dict["is_check_passed"]: + raise FileTypeError(f"Invalid file supplied, failed to upload. Report:\n{pformat(report_dict)}") + + if isinstance(file, str): + file = Path(file) + + if purpose not in get_args(FilePurpose): + raise ValueError(f"Invalid purpose '{purpose}'. Must be one of: {get_args(FilePurpose)}") + + purpose = cast(FilePurpose, purpose) + + return upload_manager.upload("files", file, purpose=purpose, redirect=True) + def content( self, id: str, @@ -274,6 +303,30 @@ async def delete( cast_to=FileDeleteResponse, ) + def upload( + self, + file: Path | str, + *, + purpose: str = "fine-tune", + check: bool = True, + ) -> FileRetrieveResponse: + upload_manager = UploadManager(self._client) + + if check: + report_dict = check_file(file) + if not report_dict["is_check_passed"]: + raise FileTypeError(f"Invalid file supplied, failed to upload. Report:\n{pformat(report_dict)}") + + if isinstance(file, str): + file = Path(file) + + if purpose not in get_args(FilePurpose): + raise ValueError(f"Invalid purpose '{purpose}'. Must be one of: {get_args(FilePurpose)}") + + purpose = cast(FilePurpose, purpose) + + return upload_manager.upload("files", file, purpose=purpose, redirect=True) + async def content( self, id: str, From abd41e8dfa27976552f310e79f4df785886cd2f2 Mon Sep 17 00:00:00 2001 From: Justin Driemeyer Date: Wed, 28 May 2025 18:13:49 -0700 Subject: [PATCH 03/31] Format --- src/together/__init__.py | 16 ++++++++-------- src/together/lib/__init__.py | 12 ++++++------ src/together/lib/resources/__init__.py | 8 ++++---- src/together/lib/resources/files.py | 8 +++----- src/together/lib/types/__init__.py | 6 +++--- src/together/resources/files.py | 11 ++++++----- 6 files changed, 30 insertions(+), 31 deletions(-) diff --git a/src/together/__init__.py b/src/together/__init__.py index f3169ab8..c29376fd 100644 --- a/src/together/__init__.py +++ b/src/together/__init__.py @@ -3,6 +3,14 @@ import typing as _t from . import types +from .lib import ( + DownloadError, + FileTypeError, + UploadManager, + DownloadManager, + FinetuneTrainingLimits, + create_finetune_request, +) from ._types import NOT_GIVEN, Omit, NoneType, NotGiven, Transport, ProxiesTypes from ._utils import file_from_path from ._client import ( @@ -38,14 +46,6 @@ ) from ._base_client import DefaultHttpxClient, DefaultAsyncHttpxClient from ._utils._logs import setup_logging as _setup_logging -from .lib import ( - create_finetune_request, - DownloadManager, - UploadManager, - FinetuneTrainingLimits, - DownloadError, - FileTypeError, -) __all__ = [ "types", diff --git a/src/together/lib/__init__.py b/src/together/lib/__init__.py index 108469f0..24ef1d2a 100644 --- a/src/together/lib/__init__.py +++ b/src/together/lib/__init__.py @@ -1,16 +1,16 @@ -from .resources import ( - create_finetune_request, - DownloadManager, - UploadManager, -) from .types import ( - FinetuneTrainingLimits, DownloadError, FileTypeError, + FinetuneTrainingLimits, ) from .utils import ( check_file, ) +from .resources import ( + UploadManager, + DownloadManager, + create_finetune_request, +) __all__ = [ "create_finetune_request", diff --git a/src/together/lib/resources/__init__.py b/src/together/lib/resources/__init__.py index 36c31a47..695ec9d4 100644 --- a/src/together/lib/resources/__init__.py +++ b/src/together/lib/resources/__init__.py @@ -1,9 +1,9 @@ -from .fine_tune import ( - create_finetune_request, -) from .files import ( - DownloadManager, UploadManager, + DownloadManager, +) +from .fine_tune import ( + create_finetune_request, ) __all__ = [ diff --git a/src/together/lib/resources/files.py b/src/together/lib/resources/files.py index 13c5fc73..36ee2a0e 100644 --- a/src/together/lib/resources/files.py +++ b/src/together/lib/resources/files.py @@ -14,12 +14,12 @@ from filelock import FileLock from tqdm.utils import CallbackIOWrapper -from ..._exceptions import APIStatusError, AuthenticationError -from ..._client import RequestOptions -from ..._resource import SyncAPIResource from ...types import FileType, FilePurpose, FileRetrieveResponse +from ..._client import RequestOptions from ..constants import DISABLE_TQDM, DOWNLOAD_BLOCK_SIZE +from ..._resource import SyncAPIResource from ..types.error import DownloadError, FileTypeError +from ..._exceptions import APIStatusError, AuthenticationError def chmod_and_replace(src: Path, dst: Path) -> None: @@ -95,7 +95,6 @@ def _prepare_output( class DownloadManager(SyncAPIResource): - def get_file_metadata( self, url: str, @@ -208,7 +207,6 @@ def download( class UploadManager(SyncAPIResource): - def get_upload_url( self, url: str, diff --git a/src/together/lib/types/__init__.py b/src/together/lib/types/__init__.py index 6a0e474a..394716cd 100644 --- a/src/together/lib/types/__init__.py +++ b/src/together/lib/types/__init__.py @@ -1,10 +1,10 @@ -from .fine_tune import ( - FinetuneTrainingLimits, -) from .error import ( DownloadError, FileTypeError, ) +from .fine_tune import ( + FinetuneTrainingLimits, +) __all__ = [ "FinetuneTrainingLimits", diff --git a/src/together/resources/files.py b/src/together/resources/files.py index 42e937e0..63c52e27 100644 --- a/src/together/resources/files.py +++ b/src/together/resources/files.py @@ -2,11 +2,13 @@ from __future__ import annotations -import httpx from pprint import pformat from typing import cast, get_args from pathlib import Path +import httpx + +from ..lib import FileTypeError, UploadManager, check_file from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource @@ -25,11 +27,10 @@ async_to_custom_streamed_response_wrapper, ) from .._base_client import make_request_options -from ..lib import FileTypeError, UploadManager, check_file +from ..types.file_purpose import FilePurpose from ..types.file_list_response import FileListResponse from ..types.file_delete_response import FileDeleteResponse from ..types.file_retrieve_response import FileRetrieveResponse -from ..types.file_purpose import FilePurpose __all__ = ["FilesResource", "AsyncFilesResource"] @@ -162,7 +163,7 @@ def upload( purpose = cast(FilePurpose, purpose) return upload_manager.upload("files", file, purpose=purpose, redirect=True) - + def content( self, id: str, @@ -326,7 +327,7 @@ def upload( purpose = cast(FilePurpose, purpose) return upload_manager.upload("files", file, purpose=purpose, redirect=True) - + async def content( self, id: str, From 67bef5b456d4a6752908ad7ffdcd86535ace09ad Mon Sep 17 00:00:00 2001 From: Justin Driemeyer Date: Wed, 28 May 2025 18:17:34 -0700 Subject: [PATCH 04/31] Fix circular import --- src/together/lib/resources/files.py | 2 +- src/together/resources/files.py | 20 ++++---------------- 2 files changed, 5 insertions(+), 17 deletions(-) diff --git a/src/together/lib/resources/files.py b/src/together/lib/resources/files.py index 36ee2a0e..f5e8c2eb 100644 --- a/src/together/lib/resources/files.py +++ b/src/together/lib/resources/files.py @@ -15,7 +15,7 @@ from tqdm.utils import CallbackIOWrapper from ...types import FileType, FilePurpose, FileRetrieveResponse -from ..._client import RequestOptions +from ..._types import RequestOptions from ..constants import DISABLE_TQDM, DOWNLOAD_BLOCK_SIZE from ..._resource import SyncAPIResource from ..types.error import DownloadError, FileTypeError diff --git a/src/together/resources/files.py b/src/together/resources/files.py index 63c52e27..c8a3d4c5 100644 --- a/src/together/resources/files.py +++ b/src/together/resources/files.py @@ -311,22 +311,10 @@ def upload( purpose: str = "fine-tune", check: bool = True, ) -> FileRetrieveResponse: - upload_manager = UploadManager(self._client) - - if check: - report_dict = check_file(file) - if not report_dict["is_check_passed"]: - raise FileTypeError(f"Invalid file supplied, failed to upload. Report:\n{pformat(report_dict)}") - - if isinstance(file, str): - file = Path(file) - - if purpose not in get_args(FilePurpose): - raise ValueError(f"Invalid purpose '{purpose}'. Must be one of: {get_args(FilePurpose)}") - - purpose = cast(FilePurpose, purpose) - - return upload_manager.upload("files", file, purpose=purpose, redirect=True) + raise NotImplementedError( + "The `upload` method is not available in the async version of the FilesResource. " + "Use the `upload` method from the synchronous FilesResource instead." + ) async def content( self, From 271e713454c84591853ef31e8d3742213cdf8804 Mon Sep 17 00:00:00 2001 From: Justin Driemeyer Date: Thu, 29 May 2025 12:38:56 -0700 Subject: [PATCH 05/31] Add async upload support --- src/together/lib/__init__.py | 2 + src/together/lib/resources/__init__.py | 2 + src/together/lib/resources/files.py | 118 ++++++++++++++++++++++++- src/together/resources/files.py | 24 +++-- 4 files changed, 139 insertions(+), 7 deletions(-) diff --git a/src/together/lib/__init__.py b/src/together/lib/__init__.py index 24ef1d2a..2f027dac 100644 --- a/src/together/lib/__init__.py +++ b/src/together/lib/__init__.py @@ -9,12 +9,14 @@ from .resources import ( UploadManager, DownloadManager, + AsyncUploadManager, create_finetune_request, ) __all__ = [ "create_finetune_request", "DownloadManager", + "AsyncUploadManager", "UploadManager", "FinetuneTrainingLimits", "DownloadError", diff --git a/src/together/lib/resources/__init__.py b/src/together/lib/resources/__init__.py index 695ec9d4..e5db521c 100644 --- a/src/together/lib/resources/__init__.py +++ b/src/together/lib/resources/__init__.py @@ -1,6 +1,7 @@ from .files import ( UploadManager, DownloadManager, + AsyncUploadManager, ) from .fine_tune import ( create_finetune_request, @@ -10,4 +11,5 @@ "create_finetune_request", "DownloadManager", "UploadManager", + "AsyncUploadManager", ] diff --git a/src/together/lib/resources/files.py b/src/together/lib/resources/files.py index f5e8c2eb..bb351f0b 100644 --- a/src/together/lib/resources/files.py +++ b/src/together/lib/resources/files.py @@ -17,7 +17,7 @@ from ...types import FileType, FilePurpose, FileRetrieveResponse from ..._types import RequestOptions from ..constants import DISABLE_TQDM, DOWNLOAD_BLOCK_SIZE -from ..._resource import SyncAPIResource +from ..._resource import SyncAPIResource, AsyncAPIResource from ..types.error import DownloadError, FileTypeError from ..._exceptions import APIStatusError, AuthenticationError @@ -320,3 +320,119 @@ def upload( assert isinstance(response, FileRetrieveResponse) # type: ignore return response + + +class AsyncUploadManager(AsyncAPIResource): + async def get_upload_url( + self, + url: str, + file: Path, + purpose: FilePurpose, + filetype: FileType, + ) -> Tuple[str, str]: + data = { + "purpose": purpose, + "file_name": file.name, + "file_type": filetype, + } + + try: + response = await self._client.post( + path=url, + cast_to=httpx.Response, + body=data, + ) + except APIStatusError as e: + if e.response.status_code == 401: + raise AuthenticationError( + "This job would exceed your free trial credits. " + "Please upgrade to a paid account through " + "Settings -> Billing on api.together.ai to continue.", + response=e.response, + body=e.body, + ) from e + raise + + # Raise error for non 302 status codes + if response.status_code != 302: + raise APIStatusError( + f"Unexpected error raised by endpoint: {response.content.decode()}, headers: {response.headers}", + response=response, + body=response.content.decode(), + ) + + redirect_url = response.headers["Location"] + file_id = response.headers["X-Together-File-Id"] + + return redirect_url, file_id + + async def callback(self, url: str) -> FileRetrieveResponse: + response = self._client.post( + cast_to=FileRetrieveResponse, + path=url, + ) + + return await response + + async def upload( + self, + url: str, + file: Path, + purpose: FilePurpose, + redirect: bool = False, + ) -> FileRetrieveResponse: + file_id = None + + redirect_url = None + if redirect: + if file.suffix == ".jsonl": + filetype = "jsonl" + elif file.suffix == ".parquet": + filetype = "parquet" + else: + raise FileTypeError( + f"Unknown extension of file {file}. Only files with extensions .jsonl and .parquet are supported." + ) + redirect_url, file_id = await self.get_upload_url(url, file, purpose, filetype) # type: ignore + + file_size = os.stat(file.as_posix()).st_size + + with tqdm( + total=file_size, + unit="B", + unit_scale=True, + desc=f"Uploading file {file.name}", + disable=bool(DISABLE_TQDM), + ) as pbar: + with file.open("rb") as f: + wrapped_file = CallbackIOWrapper(pbar.update, f, "read") + + if redirect: + assert redirect_url is not None + callback_response = self._client.put( + cast_to=httpx.Response, + path=redirect_url, + body=wrapped_file, + ) + else: + response = self._client.put( + cast_to=FileRetrieveResponse, + path=url, + body=wrapped_file, + ) + + if redirect: + assert isinstance(callback_response, httpx.Response) # type: ignore + + if not callback_response.status_code == 200: + raise APIStatusError( + f"Error during file upload: {callback_response.content.decode()}, headers: {callback_response.headers}", + response=callback_response, + body=callback_response.content.decode(), + ) + + response = self.callback(f"{url}/{file_id}/preprocess") + + assert isinstance(response, FileRetrieveResponse) # type: ignore + + return response diff --git a/src/together/resources/files.py b/src/together/resources/files.py index c8a3d4c5..4fb0c3d1 100644 --- a/src/together/resources/files.py +++ b/src/together/resources/files.py @@ -8,7 +8,7 @@ import httpx -from ..lib import FileTypeError, UploadManager, check_file +from ..lib import FileTypeError, UploadManager, AsyncUploadManager, check_file from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource @@ -304,17 +304,29 @@ async def delete( cast_to=FileDeleteResponse, ) - def upload( + async def upload( self, file: Path | str, *, purpose: str = "fine-tune", check: bool = True, ) -> FileRetrieveResponse: - raise NotImplementedError( - "The `upload` method is not available in the async version of the FilesResource. " - "Use the `upload` method from the synchronous FilesResource instead." - ) + upload_manager = AsyncUploadManager(self._client) + + if check: + report_dict = check_file(file) + if not report_dict["is_check_passed"]: + raise FileTypeError(f"Invalid file supplied, failed to upload. Report:\n{pformat(report_dict)}") + + if isinstance(file, str): + file = Path(file) + + if purpose not in get_args(FilePurpose): + raise ValueError(f"Invalid purpose '{purpose}'. Must be one of: {get_args(FilePurpose)}") + + purpose = cast(FilePurpose, purpose) + + return await upload_manager.upload("files", file, purpose=purpose, redirect=True) async def content( self, From 65ee27b2ab0491989c17975688fbd68232ac0194 Mon Sep 17 00:00:00 2001 From: Justin Driemeyer Date: Thu, 29 May 2025 13:17:50 -0700 Subject: [PATCH 06/31] Remove top level exports of UploadManager, DownloadManager --- src/together/__init__.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/together/__init__.py b/src/together/__init__.py index c29376fd..50964a8c 100644 --- a/src/together/__init__.py +++ b/src/together/__init__.py @@ -6,8 +6,6 @@ from .lib import ( DownloadError, FileTypeError, - UploadManager, - DownloadManager, FinetuneTrainingLimits, create_finetune_request, ) @@ -87,8 +85,6 @@ "DefaultHttpxClient", "DefaultAsyncHttpxClient", "create_finetune_request", - "DownloadManager", - "UploadManager", "FinetuneTrainingLimits", "DownloadError", "FileTypeError", From 80d5ae03f2fee590266fe5504f738b6d49f5311c Mon Sep 17 00:00:00 2001 From: Justin Driemeyer Date: Thu, 29 May 2025 14:14:24 -0700 Subject: [PATCH 07/31] feat(api): move upload to be a method of existing files resource From b7c43be446e48390528994ee5a070699c490cec4 Mon Sep 17 00:00:00 2001 From: Justin Driemeyer Date: Thu, 29 May 2025 14:14:24 -0700 Subject: [PATCH 08/31] feat(api): move upload to be a method of existing files resource From b8bc1010e047ba0b1bd75a311cb1220f13366f04 Mon Sep 17 00:00:00 2001 From: Justin Driemeyer Date: Thu, 29 May 2025 16:16:44 -0700 Subject: [PATCH 09/31] fix(api): correct file reroute handling, error message --- src/together/lib/resources/files.py | 17 ++++++++--------- src/together/lib/types/error.py | 18 ++---------------- 2 files changed, 10 insertions(+), 25 deletions(-) diff --git a/src/together/lib/resources/files.py b/src/together/lib/resources/files.py index bb351f0b..141a6618 100644 --- a/src/together/lib/resources/files.py +++ b/src/together/lib/resources/files.py @@ -235,15 +235,14 @@ def get_upload_url( response=e.response, body=e.body, ) from e - raise - - # Raise error for non 302 status codes - if response.status_code != 302: - raise APIStatusError( - f"Unexpected error raised by endpoint: {response.content.decode()}, headers: {response.headers}", - response=response, - body=response.content.decode(), - ) + + if e.response.status_code != 302: + raise APIStatusError( + f"Unexpected error raised by endpoint: {e.response.content.decode()}, headers: {e.response.headers}", + response=e.response, + body=e.response.content.decode(), + ) + response = e.response redirect_url = response.headers["Location"] file_id = response.headers["X-Together-File-Id"] diff --git a/src/together/lib/types/error.py b/src/together/lib/types/error.py index c482216f..36d6dfbb 100644 --- a/src/together/lib/types/error.py +++ b/src/together/lib/types/error.py @@ -1,23 +1,9 @@ -from typing import Any - from ..._exceptions import TogetherError class DownloadError(TogetherError): - def __init__( - self, - message: str, - **kwargs: Any, - ) -> None: - self.message = message - super().__init__(**kwargs) + pass class FileTypeError(TogetherError): - def __init__( - self, - message: str, - **kwargs: Any, - ) -> None: - self.message = message - super().__init__(**kwargs) + pass From 1931f174b6ef8c778a20d1292b27ccbdb67491fb Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 2 Jun 2025 10:13:09 +0000 Subject: [PATCH 10/31] chore(docs): remove reference to rye shell --- CONTRIBUTING.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3380509e..8e0a033a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -17,8 +17,7 @@ $ rye sync --all-features You can then run scripts using `rye run python script.py` or by activating the virtual environment: ```sh -$ rye shell -# or manually activate - https://docs.python.org/3/library/venv.html#how-venvs-work +# Activate the virtual environment - https://docs.python.org/3/library/venv.html#how-venvs-work $ source .venv/bin/activate # now you can omit the `rye run` prefix From 6ed818ea9e78be560dce5f166d4ba492e4fd1ab3 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 2 Jun 2025 10:46:25 +0000 Subject: [PATCH 11/31] chore(docs): remove unnecessary param examples --- README.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/README.md b/README.md index da9b0b21..252f1ef9 100644 --- a/README.md +++ b/README.md @@ -153,10 +153,7 @@ chat_completion = client.chat.completions.create( } ], model="meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo", - response_format={ - "schema": {"foo": "bar"}, - "type": "json", - }, + response_format={}, ) print(chat_completion.response_format) ``` From b515197012ea3e342dfbe4a3f7d418fdc90828df Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 2 Jun 2025 17:33:58 +0000 Subject: [PATCH 12/31] feat(client): add follow_redirects request option --- tests/test_client.py | 54 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/tests/test_client.py b/tests/test_client.py index f68e1ba2..afa6dbf1 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -884,6 +884,33 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: assert response.http_request.headers.get("x-stainless-retry-count") == "42" + @pytest.mark.respx(base_url=base_url) + def test_follow_redirects(self, respx_mock: MockRouter) -> None: + # Test that the default follow_redirects=True allows following redirects + respx_mock.post("/redirect").mock( + return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"}) + ) + respx_mock.get("/redirected").mock(return_value=httpx.Response(200, json={"status": "ok"})) + + response = self.client.post("/redirect", body={"key": "value"}, cast_to=httpx.Response) + assert response.status_code == 200 + assert response.json() == {"status": "ok"} + + @pytest.mark.respx(base_url=base_url) + def test_follow_redirects_disabled(self, respx_mock: MockRouter) -> None: + # Test that follow_redirects=False prevents following redirects + respx_mock.post("/redirect").mock( + return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"}) + ) + + with pytest.raises(APIStatusError) as exc_info: + self.client.post( + "/redirect", body={"key": "value"}, options={"follow_redirects": False}, cast_to=httpx.Response + ) + + assert exc_info.value.response.status_code == 302 + assert exc_info.value.response.headers["Location"] == f"{base_url}/redirected" + class TestAsyncTogether: client = AsyncTogether(base_url=base_url, api_key=api_key, _strict_response_validation=True) @@ -1773,3 +1800,30 @@ async def test_main() -> None: raise AssertionError("calling get_platform using asyncify resulted in a hung process") time.sleep(0.1) + + @pytest.mark.respx(base_url=base_url) + async def test_follow_redirects(self, respx_mock: MockRouter) -> None: + # Test that the default follow_redirects=True allows following redirects + respx_mock.post("/redirect").mock( + return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"}) + ) + respx_mock.get("/redirected").mock(return_value=httpx.Response(200, json={"status": "ok"})) + + response = await self.client.post("/redirect", body={"key": "value"}, cast_to=httpx.Response) + assert response.status_code == 200 + assert response.json() == {"status": "ok"} + + @pytest.mark.respx(base_url=base_url) + async def test_follow_redirects_disabled(self, respx_mock: MockRouter) -> None: + # Test that follow_redirects=False prevents following redirects + respx_mock.post("/redirect").mock( + return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"}) + ) + + with pytest.raises(APIStatusError) as exc_info: + await self.client.post( + "/redirect", body={"key": "value"}, options={"follow_redirects": False}, cast_to=httpx.Response + ) + + assert exc_info.value.response.status_code == 302 + assert exc_info.value.response.headers["Location"] == f"{base_url}/redirected" From 3cff5ae5aeda8413075dd164d30cd3afbf66413f Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 3 Jun 2025 21:46:27 +0000 Subject: [PATCH 13/31] feat(api): api update --- .stats.yml | 4 +- api.md | 9 +-- src/together/types/chat/__init__.py | 9 --- ...tion_structured_message_image_url_param.py | 18 ------ ...ompletion_structured_message_text_param.py | 13 ---- ...tion_structured_message_video_url_param.py | 18 ------ .../types/chat/completion_create_params.py | 64 ++++++++++++++++--- 7 files changed, 58 insertions(+), 77 deletions(-) delete mode 100644 src/together/types/chat/chat_completion_structured_message_image_url_param.py delete mode 100644 src/together/types/chat/chat_completion_structured_message_text_param.py delete mode 100644 src/together/types/chat/chat_completion_structured_message_video_url_param.py diff --git a/.stats.yml b/.stats.yml index 7994808e..58cc1b50 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 29 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/togetherai%2Ftogetherai-1f9f08d53431133ec18d6a143dc148cd9707b4481d434ea72268b85fa09ace96.yml -openapi_spec_hash: 6d66a86c8d4ec97cb4d98b57a4238bc5 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/togetherai%2Ftogetherai-2cc1d49fa13b7d4a5cd42cc7440c1d3ef74763891b077d2cc28a872a41abd61f.yml +openapi_spec_hash: fa4a4788ab2d4438ef691c4b9f32a914 config_hash: 9c8f0f59a53f8837c6406b493a0c3db7 diff --git a/api.md b/api.md index fad7f1ac..7f1322d6 100644 --- a/api.md +++ b/api.md @@ -17,14 +17,7 @@ Methods: Types: ```python -from together.types.chat import ( - ChatCompletion, - ChatCompletionChunk, - ChatCompletionStructuredMessageImageURL, - ChatCompletionStructuredMessageText, - ChatCompletionStructuredMessageVideoURL, - ChatCompletionUsage, -) +from together.types.chat import ChatCompletion, ChatCompletionChunk, ChatCompletionUsage ``` Methods: diff --git a/src/together/types/chat/__init__.py b/src/together/types/chat/__init__.py index e72a90ae..795bddc9 100644 --- a/src/together/types/chat/__init__.py +++ b/src/together/types/chat/__init__.py @@ -6,12 +6,3 @@ from .chat_completion_chunk import ChatCompletionChunk as ChatCompletionChunk from .chat_completion_usage import ChatCompletionUsage as ChatCompletionUsage from .completion_create_params import CompletionCreateParams as CompletionCreateParams -from .chat_completion_structured_message_text_param import ( - ChatCompletionStructuredMessageTextParam as ChatCompletionStructuredMessageTextParam, -) -from .chat_completion_structured_message_image_url_param import ( - ChatCompletionStructuredMessageImageURLParam as ChatCompletionStructuredMessageImageURLParam, -) -from .chat_completion_structured_message_video_url_param import ( - ChatCompletionStructuredMessageVideoURLParam as ChatCompletionStructuredMessageVideoURLParam, -) diff --git a/src/together/types/chat/chat_completion_structured_message_image_url_param.py b/src/together/types/chat/chat_completion_structured_message_image_url_param.py deleted file mode 100644 index 25d737ca..00000000 --- a/src/together/types/chat/chat_completion_structured_message_image_url_param.py +++ /dev/null @@ -1,18 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing_extensions import Literal, Required, TypedDict - -__all__ = ["ChatCompletionStructuredMessageImageURLParam", "ImageURL"] - - -class ImageURL(TypedDict, total=False): - url: Required[str] - """The URL of the image""" - - -class ChatCompletionStructuredMessageImageURLParam(TypedDict, total=False): - image_url: ImageURL - - type: Literal["image_url"] diff --git a/src/together/types/chat/chat_completion_structured_message_text_param.py b/src/together/types/chat/chat_completion_structured_message_text_param.py deleted file mode 100644 index f18fc188..00000000 --- a/src/together/types/chat/chat_completion_structured_message_text_param.py +++ /dev/null @@ -1,13 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing_extensions import Literal, Required, TypedDict - -__all__ = ["ChatCompletionStructuredMessageTextParam"] - - -class ChatCompletionStructuredMessageTextParam(TypedDict, total=False): - text: Required[str] - - type: Required[Literal["text"]] diff --git a/src/together/types/chat/chat_completion_structured_message_video_url_param.py b/src/together/types/chat/chat_completion_structured_message_video_url_param.py deleted file mode 100644 index 3fa43ec4..00000000 --- a/src/together/types/chat/chat_completion_structured_message_video_url_param.py +++ /dev/null @@ -1,18 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing_extensions import Literal, Required, TypedDict - -__all__ = ["ChatCompletionStructuredMessageVideoURLParam", "VideoURL"] - - -class VideoURL(TypedDict, total=False): - url: Required[str] - """The URL of the video""" - - -class ChatCompletionStructuredMessageVideoURLParam(TypedDict, total=False): - type: Required[Literal["video_url"]] - - video_url: Required[VideoURL] diff --git a/src/together/types/chat/completion_create_params.py b/src/together/types/chat/completion_create_params.py index c1d305eb..549f9978 100644 --- a/src/together/types/chat/completion_create_params.py +++ b/src/together/types/chat/completion_create_params.py @@ -7,16 +7,18 @@ from ..tools_param import ToolsParam from ..tool_choice_param import ToolChoiceParam -from .chat_completion_structured_message_text_param import ChatCompletionStructuredMessageTextParam -from .chat_completion_structured_message_image_url_param import ChatCompletionStructuredMessageImageURLParam -from .chat_completion_structured_message_video_url_param import ChatCompletionStructuredMessageVideoURLParam __all__ = [ "CompletionCreateParamsBase", "Message", "MessageChatCompletionSystemMessageParam", "MessageChatCompletionUserMessageParam", - "MessageChatCompletionUserMessageParamContentUnionMember1", + "MessageChatCompletionUserMessageParamContentChatCompletionUserMessageContentMultimodal", + "MessageChatCompletionUserMessageParamContentChatCompletionUserMessageContentMultimodalUnionMember0", + "MessageChatCompletionUserMessageParamContentChatCompletionUserMessageContentMultimodalUnionMember1", + "MessageChatCompletionUserMessageParamContentChatCompletionUserMessageContentMultimodalUnionMember1ImageURL", + "MessageChatCompletionUserMessageParamContentChatCompletionUserMessageContentMultimodalVideo", + "MessageChatCompletionUserMessageParamContentChatCompletionUserMessageContentMultimodalVideoVideoURL", "MessageChatCompletionAssistantMessageParam", "MessageChatCompletionAssistantMessageParamFunctionCall", "MessageChatCompletionToolMessageParam", @@ -172,15 +174,59 @@ class MessageChatCompletionSystemMessageParam(TypedDict, total=False): name: str -MessageChatCompletionUserMessageParamContentUnionMember1: TypeAlias = Union[ - ChatCompletionStructuredMessageTextParam, - ChatCompletionStructuredMessageImageURLParam, - ChatCompletionStructuredMessageVideoURLParam, +class MessageChatCompletionUserMessageParamContentChatCompletionUserMessageContentMultimodalUnionMember0( + TypedDict, total=False +): + text: Required[str] + + type: Required[Literal["text"]] + + +class MessageChatCompletionUserMessageParamContentChatCompletionUserMessageContentMultimodalUnionMember1ImageURL( + TypedDict, total=False +): + url: Required[str] + """The URL of the image""" + + +class MessageChatCompletionUserMessageParamContentChatCompletionUserMessageContentMultimodalUnionMember1( + TypedDict, total=False +): + image_url: ( + MessageChatCompletionUserMessageParamContentChatCompletionUserMessageContentMultimodalUnionMember1ImageURL + ) + + type: Literal["image_url"] + + +class MessageChatCompletionUserMessageParamContentChatCompletionUserMessageContentMultimodalVideoVideoURL( + TypedDict, total=False +): + url: Required[str] + """The URL of the video""" + + +class MessageChatCompletionUserMessageParamContentChatCompletionUserMessageContentMultimodalVideo( + TypedDict, total=False +): + type: Required[Literal["video_url"]] + + video_url: Required[ + MessageChatCompletionUserMessageParamContentChatCompletionUserMessageContentMultimodalVideoVideoURL + ] + + +MessageChatCompletionUserMessageParamContentChatCompletionUserMessageContentMultimodal: TypeAlias = Union[ + MessageChatCompletionUserMessageParamContentChatCompletionUserMessageContentMultimodalUnionMember0, + MessageChatCompletionUserMessageParamContentChatCompletionUserMessageContentMultimodalUnionMember1, + MessageChatCompletionUserMessageParamContentChatCompletionUserMessageContentMultimodalVideo, ] class MessageChatCompletionUserMessageParam(TypedDict, total=False): - content: Required[Union[str, Iterable[MessageChatCompletionUserMessageParamContentUnionMember1]]] + content: Required[ + Union[str, Iterable[MessageChatCompletionUserMessageParamContentChatCompletionUserMessageContentMultimodal]] + ] """ The content of the message, which can either be a simple string or a structured format. From 48986d2c15b07b6761bc50c93a72a116ec73aed5 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 3 Jun 2025 21:47:07 +0000 Subject: [PATCH 14/31] feat(api): update spec / config to remove remaining codegen warnings --- .stats.yml | 8 +- api.md | 13 ++- src/together/resources/fine_tune.py | 79 +++++++++++++++++++ src/together/types/__init__.py | 3 + src/together/types/chat/__init__.py | 9 +++ ...tion_structured_message_image_url_param.py | 18 +++++ ...ompletion_structured_message_text_param.py | 13 +++ ...tion_structured_message_video_url_param.py | 18 +++++ .../types/chat/completion_create_params.py | 56 ++----------- ...fine_tune_retrieve_checkpoints_response.py | 21 +++++ src/together/types/job_list_response.py | 4 +- src/together/types/job_retrieve_response.py | 4 +- src/together/types/model_upload_response.py | 6 +- tests/api_resources/test_fine_tune.py | 77 ++++++++++++++++++ 14 files changed, 266 insertions(+), 63 deletions(-) create mode 100644 src/together/types/chat/chat_completion_structured_message_image_url_param.py create mode 100644 src/together/types/chat/chat_completion_structured_message_text_param.py create mode 100644 src/together/types/chat/chat_completion_structured_message_video_url_param.py create mode 100644 src/together/types/fine_tune_retrieve_checkpoints_response.py diff --git a/.stats.yml b/.stats.yml index 58cc1b50..82bdaf10 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 29 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/togetherai%2Ftogetherai-2cc1d49fa13b7d4a5cd42cc7440c1d3ef74763891b077d2cc28a872a41abd61f.yml -openapi_spec_hash: fa4a4788ab2d4438ef691c4b9f32a914 -config_hash: 9c8f0f59a53f8837c6406b493a0c3db7 +configured_endpoints: 30 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/togetherai%2Ftogetherai-48f3206278cd93af20d7d74aed1fd8e1513a04a60468505a40b0a15fbdab31a3.yml +openapi_spec_hash: 69c1236ff3815089881984840aa4d3f6 +config_hash: f39be209cf332e8d80f34099b178970a diff --git a/api.md b/api.md index 7f1322d6..5d832ea1 100644 --- a/api.md +++ b/api.md @@ -17,7 +17,14 @@ Methods: Types: ```python -from together.types.chat import ChatCompletion, ChatCompletionChunk, ChatCompletionUsage +from together.types.chat import ( + ChatCompletion, + ChatCompletionChunk, + ChatCompletionStructuredMessageImageURL, + ChatCompletionStructuredMessageText, + ChatCompletionStructuredMessageVideoURL, + ChatCompletionUsage, +) ``` Methods: @@ -29,7 +36,7 @@ Methods: Types: ```python -from together.types import Completion, CompletionChunk, CompletionUsage, LogProbs, ToolChoice, Tools +from together.types import Completion, CompletionChunk, LogProbs, ToolChoice, Tools ``` Methods: @@ -84,6 +91,7 @@ from together.types import ( FineTuneListResponse, FineTuneCancelResponse, FineTuneDownloadResponse, + FineTuneRetrieveCheckpointsResponse, ) ``` @@ -95,6 +103,7 @@ Methods: - client.fine_tune.cancel(id) -> FineTuneCancelResponse - client.fine_tune.download(\*\*params) -> FineTuneDownloadResponse - client.fine_tune.list_events(id) -> FineTuneEvent +- client.fine_tune.retrieve_checkpoints(id) -> FineTuneRetrieveCheckpointsResponse # CodeInterpreter diff --git a/src/together/resources/fine_tune.py b/src/together/resources/fine_tune.py index fdfad3a1..5e2aeefe 100644 --- a/src/together/resources/fine_tune.py +++ b/src/together/resources/fine_tune.py @@ -25,6 +25,7 @@ from ..types.fine_tune_cancel_response import FineTuneCancelResponse from ..types.fine_tune_create_response import FineTuneCreateResponse from ..types.fine_tune_download_response import FineTuneDownloadResponse +from ..types.fine_tune_retrieve_checkpoints_response import FineTuneRetrieveCheckpointsResponse __all__ = ["FineTuneResource", "AsyncFineTuneResource"] @@ -359,6 +360,39 @@ def list_events( cast_to=FineTuneEvent, ) + def retrieve_checkpoints( + self, + id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> FineTuneRetrieveCheckpointsResponse: + """ + List the checkpoints for a single fine-tuning job. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._get( + f"/fine-tunes/{id}/checkpoints", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=FineTuneRetrieveCheckpointsResponse, + ) + class AsyncFineTuneResource(AsyncAPIResource): @cached_property @@ -690,6 +724,39 @@ async def list_events( cast_to=FineTuneEvent, ) + async def retrieve_checkpoints( + self, + id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> FineTuneRetrieveCheckpointsResponse: + """ + List the checkpoints for a single fine-tuning job. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._get( + f"/fine-tunes/{id}/checkpoints", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=FineTuneRetrieveCheckpointsResponse, + ) + class FineTuneResourceWithRawResponse: def __init__(self, fine_tune: FineTuneResource) -> None: @@ -713,6 +780,9 @@ def __init__(self, fine_tune: FineTuneResource) -> None: self.list_events = to_raw_response_wrapper( fine_tune.list_events, ) + self.retrieve_checkpoints = to_raw_response_wrapper( + fine_tune.retrieve_checkpoints, + ) class AsyncFineTuneResourceWithRawResponse: @@ -737,6 +807,9 @@ def __init__(self, fine_tune: AsyncFineTuneResource) -> None: self.list_events = async_to_raw_response_wrapper( fine_tune.list_events, ) + self.retrieve_checkpoints = async_to_raw_response_wrapper( + fine_tune.retrieve_checkpoints, + ) class FineTuneResourceWithStreamingResponse: @@ -761,6 +834,9 @@ def __init__(self, fine_tune: FineTuneResource) -> None: self.list_events = to_streamed_response_wrapper( fine_tune.list_events, ) + self.retrieve_checkpoints = to_streamed_response_wrapper( + fine_tune.retrieve_checkpoints, + ) class AsyncFineTuneResourceWithStreamingResponse: @@ -785,3 +861,6 @@ def __init__(self, fine_tune: AsyncFineTuneResource) -> None: self.list_events = async_to_streamed_response_wrapper( fine_tune.list_events, ) + self.retrieve_checkpoints = async_to_streamed_response_wrapper( + fine_tune.retrieve_checkpoints, + ) diff --git a/src/together/types/__init__.py b/src/together/types/__init__.py index 8508287e..3f34ef34 100644 --- a/src/together/types/__init__.py +++ b/src/together/types/__init__.py @@ -47,3 +47,6 @@ from .endpoint_retrieve_response import EndpointRetrieveResponse as EndpointRetrieveResponse from .fine_tune_download_response import FineTuneDownloadResponse as FineTuneDownloadResponse from .code_interpreter_execute_params import CodeInterpreterExecuteParams as CodeInterpreterExecuteParams +from .fine_tune_retrieve_checkpoints_response import ( + FineTuneRetrieveCheckpointsResponse as FineTuneRetrieveCheckpointsResponse, +) diff --git a/src/together/types/chat/__init__.py b/src/together/types/chat/__init__.py index 795bddc9..e72a90ae 100644 --- a/src/together/types/chat/__init__.py +++ b/src/together/types/chat/__init__.py @@ -6,3 +6,12 @@ from .chat_completion_chunk import ChatCompletionChunk as ChatCompletionChunk from .chat_completion_usage import ChatCompletionUsage as ChatCompletionUsage from .completion_create_params import CompletionCreateParams as CompletionCreateParams +from .chat_completion_structured_message_text_param import ( + ChatCompletionStructuredMessageTextParam as ChatCompletionStructuredMessageTextParam, +) +from .chat_completion_structured_message_image_url_param import ( + ChatCompletionStructuredMessageImageURLParam as ChatCompletionStructuredMessageImageURLParam, +) +from .chat_completion_structured_message_video_url_param import ( + ChatCompletionStructuredMessageVideoURLParam as ChatCompletionStructuredMessageVideoURLParam, +) diff --git a/src/together/types/chat/chat_completion_structured_message_image_url_param.py b/src/together/types/chat/chat_completion_structured_message_image_url_param.py new file mode 100644 index 00000000..25d737ca --- /dev/null +++ b/src/together/types/chat/chat_completion_structured_message_image_url_param.py @@ -0,0 +1,18 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Literal, Required, TypedDict + +__all__ = ["ChatCompletionStructuredMessageImageURLParam", "ImageURL"] + + +class ImageURL(TypedDict, total=False): + url: Required[str] + """The URL of the image""" + + +class ChatCompletionStructuredMessageImageURLParam(TypedDict, total=False): + image_url: ImageURL + + type: Literal["image_url"] diff --git a/src/together/types/chat/chat_completion_structured_message_text_param.py b/src/together/types/chat/chat_completion_structured_message_text_param.py new file mode 100644 index 00000000..f18fc188 --- /dev/null +++ b/src/together/types/chat/chat_completion_structured_message_text_param.py @@ -0,0 +1,13 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Literal, Required, TypedDict + +__all__ = ["ChatCompletionStructuredMessageTextParam"] + + +class ChatCompletionStructuredMessageTextParam(TypedDict, total=False): + text: Required[str] + + type: Required[Literal["text"]] diff --git a/src/together/types/chat/chat_completion_structured_message_video_url_param.py b/src/together/types/chat/chat_completion_structured_message_video_url_param.py new file mode 100644 index 00000000..3fa43ec4 --- /dev/null +++ b/src/together/types/chat/chat_completion_structured_message_video_url_param.py @@ -0,0 +1,18 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Literal, Required, TypedDict + +__all__ = ["ChatCompletionStructuredMessageVideoURLParam", "VideoURL"] + + +class VideoURL(TypedDict, total=False): + url: Required[str] + """The URL of the video""" + + +class ChatCompletionStructuredMessageVideoURLParam(TypedDict, total=False): + type: Required[Literal["video_url"]] + + video_url: Required[VideoURL] diff --git a/src/together/types/chat/completion_create_params.py b/src/together/types/chat/completion_create_params.py index 549f9978..3983a449 100644 --- a/src/together/types/chat/completion_create_params.py +++ b/src/together/types/chat/completion_create_params.py @@ -7,6 +7,9 @@ from ..tools_param import ToolsParam from ..tool_choice_param import ToolChoiceParam +from .chat_completion_structured_message_text_param import ChatCompletionStructuredMessageTextParam +from .chat_completion_structured_message_image_url_param import ChatCompletionStructuredMessageImageURLParam +from .chat_completion_structured_message_video_url_param import ChatCompletionStructuredMessageVideoURLParam __all__ = [ "CompletionCreateParamsBase", @@ -14,11 +17,6 @@ "MessageChatCompletionSystemMessageParam", "MessageChatCompletionUserMessageParam", "MessageChatCompletionUserMessageParamContentChatCompletionUserMessageContentMultimodal", - "MessageChatCompletionUserMessageParamContentChatCompletionUserMessageContentMultimodalUnionMember0", - "MessageChatCompletionUserMessageParamContentChatCompletionUserMessageContentMultimodalUnionMember1", - "MessageChatCompletionUserMessageParamContentChatCompletionUserMessageContentMultimodalUnionMember1ImageURL", - "MessageChatCompletionUserMessageParamContentChatCompletionUserMessageContentMultimodalVideo", - "MessageChatCompletionUserMessageParamContentChatCompletionUserMessageContentMultimodalVideoVideoURL", "MessageChatCompletionAssistantMessageParam", "MessageChatCompletionAssistantMessageParamFunctionCall", "MessageChatCompletionToolMessageParam", @@ -174,52 +172,10 @@ class MessageChatCompletionSystemMessageParam(TypedDict, total=False): name: str -class MessageChatCompletionUserMessageParamContentChatCompletionUserMessageContentMultimodalUnionMember0( - TypedDict, total=False -): - text: Required[str] - - type: Required[Literal["text"]] - - -class MessageChatCompletionUserMessageParamContentChatCompletionUserMessageContentMultimodalUnionMember1ImageURL( - TypedDict, total=False -): - url: Required[str] - """The URL of the image""" - - -class MessageChatCompletionUserMessageParamContentChatCompletionUserMessageContentMultimodalUnionMember1( - TypedDict, total=False -): - image_url: ( - MessageChatCompletionUserMessageParamContentChatCompletionUserMessageContentMultimodalUnionMember1ImageURL - ) - - type: Literal["image_url"] - - -class MessageChatCompletionUserMessageParamContentChatCompletionUserMessageContentMultimodalVideoVideoURL( - TypedDict, total=False -): - url: Required[str] - """The URL of the video""" - - -class MessageChatCompletionUserMessageParamContentChatCompletionUserMessageContentMultimodalVideo( - TypedDict, total=False -): - type: Required[Literal["video_url"]] - - video_url: Required[ - MessageChatCompletionUserMessageParamContentChatCompletionUserMessageContentMultimodalVideoVideoURL - ] - - MessageChatCompletionUserMessageParamContentChatCompletionUserMessageContentMultimodal: TypeAlias = Union[ - MessageChatCompletionUserMessageParamContentChatCompletionUserMessageContentMultimodalUnionMember0, - MessageChatCompletionUserMessageParamContentChatCompletionUserMessageContentMultimodalUnionMember1, - MessageChatCompletionUserMessageParamContentChatCompletionUserMessageContentMultimodalVideo, + ChatCompletionStructuredMessageTextParam, + ChatCompletionStructuredMessageImageURLParam, + ChatCompletionStructuredMessageVideoURLParam, ] diff --git a/src/together/types/fine_tune_retrieve_checkpoints_response.py b/src/together/types/fine_tune_retrieve_checkpoints_response.py new file mode 100644 index 00000000..85720f2e --- /dev/null +++ b/src/together/types/fine_tune_retrieve_checkpoints_response.py @@ -0,0 +1,21 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List + +from .._models import BaseModel + +__all__ = ["FineTuneRetrieveCheckpointsResponse", "Data"] + + +class Data(BaseModel): + checkpoint_type: str + + created_at: str + + path: str + + step: int + + +class FineTuneRetrieveCheckpointsResponse(BaseModel): + data: List[Data] diff --git a/src/together/types/job_list_response.py b/src/together/types/job_list_response.py index 11281d23..d9be7851 100644 --- a/src/together/types/job_list_response.py +++ b/src/together/types/job_list_response.py @@ -14,9 +14,9 @@ class DataArgs(BaseModel): description: Optional[str] = None - api_model_name: Optional[str] = FieldInfo(alias="modelName", default=None) + x_model_name: Optional[str] = FieldInfo(alias="modelName", default=None) - api_model_source: Optional[str] = FieldInfo(alias="modelSource", default=None) + x_model_source: Optional[str] = FieldInfo(alias="modelSource", default=None) class DataStatusUpdate(BaseModel): diff --git a/src/together/types/job_retrieve_response.py b/src/together/types/job_retrieve_response.py index ded83144..7549e481 100644 --- a/src/together/types/job_retrieve_response.py +++ b/src/together/types/job_retrieve_response.py @@ -14,9 +14,9 @@ class Args(BaseModel): description: Optional[str] = None - api_model_name: Optional[str] = FieldInfo(alias="modelName", default=None) + x_model_name: Optional[str] = FieldInfo(alias="modelName", default=None) - api_model_source: Optional[str] = FieldInfo(alias="modelSource", default=None) + x_model_source: Optional[str] = FieldInfo(alias="modelSource", default=None) class StatusUpdate(BaseModel): diff --git a/src/together/types/model_upload_response.py b/src/together/types/model_upload_response.py index 9b8d9237..810220c3 100644 --- a/src/together/types/model_upload_response.py +++ b/src/together/types/model_upload_response.py @@ -10,11 +10,11 @@ class Data(BaseModel): job_id: str - api_model_id: str = FieldInfo(alias="model_id") + x_model_id: str = FieldInfo(alias="model_id") - api_model_name: str = FieldInfo(alias="model_name") + x_model_name: str = FieldInfo(alias="model_name") - api_model_source: str = FieldInfo(alias="model_source") + x_model_source: str = FieldInfo(alias="model_source") class ModelUploadResponse(BaseModel): diff --git a/tests/api_resources/test_fine_tune.py b/tests/api_resources/test_fine_tune.py index 0eba0103..dd5546d5 100644 --- a/tests/api_resources/test_fine_tune.py +++ b/tests/api_resources/test_fine_tune.py @@ -16,6 +16,7 @@ FineTuneCancelResponse, FineTuneCreateResponse, FineTuneDownloadResponse, + FineTuneRetrieveCheckpointsResponse, ) base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -275,6 +276,44 @@ def test_path_params_list_events(self, client: Together) -> None: "", ) + @parametrize + def test_method_retrieve_checkpoints(self, client: Together) -> None: + fine_tune = client.fine_tune.retrieve_checkpoints( + "id", + ) + assert_matches_type(FineTuneRetrieveCheckpointsResponse, fine_tune, path=["response"]) + + @parametrize + def test_raw_response_retrieve_checkpoints(self, client: Together) -> None: + response = client.fine_tune.with_raw_response.retrieve_checkpoints( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + fine_tune = response.parse() + assert_matches_type(FineTuneRetrieveCheckpointsResponse, fine_tune, path=["response"]) + + @parametrize + def test_streaming_response_retrieve_checkpoints(self, client: Together) -> None: + with client.fine_tune.with_streaming_response.retrieve_checkpoints( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + fine_tune = response.parse() + assert_matches_type(FineTuneRetrieveCheckpointsResponse, fine_tune, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_retrieve_checkpoints(self, client: Together) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.fine_tune.with_raw_response.retrieve_checkpoints( + "", + ) + class TestAsyncFineTune: parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) @@ -529,3 +568,41 @@ async def test_path_params_list_events(self, async_client: AsyncTogether) -> Non await async_client.fine_tune.with_raw_response.list_events( "", ) + + @parametrize + async def test_method_retrieve_checkpoints(self, async_client: AsyncTogether) -> None: + fine_tune = await async_client.fine_tune.retrieve_checkpoints( + "id", + ) + assert_matches_type(FineTuneRetrieveCheckpointsResponse, fine_tune, path=["response"]) + + @parametrize + async def test_raw_response_retrieve_checkpoints(self, async_client: AsyncTogether) -> None: + response = await async_client.fine_tune.with_raw_response.retrieve_checkpoints( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + fine_tune = await response.parse() + assert_matches_type(FineTuneRetrieveCheckpointsResponse, fine_tune, path=["response"]) + + @parametrize + async def test_streaming_response_retrieve_checkpoints(self, async_client: AsyncTogether) -> None: + async with async_client.fine_tune.with_streaming_response.retrieve_checkpoints( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + fine_tune = await response.parse() + assert_matches_type(FineTuneRetrieveCheckpointsResponse, fine_tune, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_retrieve_checkpoints(self, async_client: AsyncTogether) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.fine_tune.with_raw_response.retrieve_checkpoints( + "", + ) From 33f1198a5ed758e1c26846f52fe1d5a8ee627595 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 11 Jun 2025 17:35:45 +0000 Subject: [PATCH 15/31] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 144618cc..5e465ec1 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 30 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/togetherai%2Ftogetherai-653a4aa26fdd2b335d1ead9c2ea0672cbe48a7616b76bf350a2421a8def4e08d.yml -openapi_spec_hash: 1d5af8ab9d8c11d7f5225e19ebd1654a +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/togetherai%2Ftogetherai-4a4319b1903dc2ad6f0f9ff8925ae462ade92eed69e0a08181ced66e094541c7.yml +openapi_spec_hash: 0be27287275454e0ebdbc29c00c69305 config_hash: d15dd709dd3f87b0a8b83b00b4abc881 From fb534169df16d1557b0beb09ef63bdec8258c09e Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 11 Jun 2025 17:48:20 +0000 Subject: [PATCH 16/31] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 5e465ec1..b0eabb03 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 30 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/togetherai%2Ftogetherai-4a4319b1903dc2ad6f0f9ff8925ae462ade92eed69e0a08181ced66e094541c7.yml -openapi_spec_hash: 0be27287275454e0ebdbc29c00c69305 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/togetherai%2Ftogetherai-29df763b2f7896b4f214e444a58d7aba39a45f90380e5deb3e010ff964f6d14c.yml +openapi_spec_hash: 8706285e2ec0c0b777452a74ae015326 config_hash: d15dd709dd3f87b0a8b83b00b4abc881 From 7efb923a6802382cdfe676c1124e6b9dafd8e233 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 16 Jun 2025 16:45:35 +0000 Subject: [PATCH 17/31] chore(tests): run tests in parallel --- pyproject.toml | 3 ++- requirements-dev.lock | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 60483ff9..6d0a371c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -64,6 +64,7 @@ dev-dependencies = [ "importlib-metadata>=6.7.0", "rich>=13.7.1", "nest_asyncio==1.6.0", + "pytest-xdist>=3.6.1", "pytest-mock>=3.14.0", ] @@ -136,7 +137,7 @@ replacement = '[\1](https://github.com/togethercomputer/together-py/tree/main/\g [tool.pytest.ini_options] testpaths = ["tests"] -addopts = "--tb=short" +addopts = "--tb=short -n auto" xfail_strict = true asyncio_mode = "auto" asyncio_default_fixture_loop_scope = "session" diff --git a/requirements-dev.lock b/requirements-dev.lock index 81b0961a..07a67d4d 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -32,6 +32,8 @@ distro==1.8.0 exceptiongroup==1.2.2 # via anyio # via pytest +execnet==2.1.1 + # via pytest-xdist filelock==3.12.4 # via virtualenv h11==0.14.0 @@ -82,8 +84,10 @@ pygments==2.18.0 pyright==1.1.399 pytest==8.3.3 # via pytest-asyncio + # via pytest-xdist # via pytest-mock pytest-asyncio==0.24.0 +pytest-xdist==3.7.0 pytest-mock==3.14.0 python-dateutil==2.8.2 # via time-machine From 7b9486c29ef0eeb862460d1ee82417db9a8f801f Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 12 Jun 2025 16:43:59 +0000 Subject: [PATCH 18/31] fix(client): correctly parse binary response | stream --- src/together/_base_client.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/together/_base_client.py b/src/together/_base_client.py index ad80ed98..c1e33b25 100644 --- a/src/together/_base_client.py +++ b/src/together/_base_client.py @@ -1071,7 +1071,14 @@ def _process_response( ) -> ResponseT: origin = get_origin(cast_to) or cast_to - if inspect.isclass(origin) and issubclass(origin, BaseAPIResponse): + if ( + inspect.isclass(origin) + and issubclass(origin, BaseAPIResponse) + # we only want to actually return the custom BaseAPIResponse class if we're + # returning the raw response, or if we're not streaming SSE, as if we're streaming + # SSE then `cast_to` doesn't actively reflect the type we need to parse into + and (not stream or bool(response.request.headers.get(RAW_RESPONSE_HEADER))) + ): if not issubclass(origin, APIResponse): raise TypeError(f"API Response types must subclass {APIResponse}; Received {origin}") @@ -1574,7 +1581,14 @@ async def _process_response( ) -> ResponseT: origin = get_origin(cast_to) or cast_to - if inspect.isclass(origin) and issubclass(origin, BaseAPIResponse): + if ( + inspect.isclass(origin) + and issubclass(origin, BaseAPIResponse) + # we only want to actually return the custom BaseAPIResponse class if we're + # returning the raw response, or if we're not streaming SSE, as if we're streaming + # SSE then `cast_to` doesn't actively reflect the type we need to parse into + and (not stream or bool(response.request.headers.get(RAW_RESPONSE_HEADER))) + ): if not issubclass(origin, AsyncAPIResponse): raise TypeError(f"API Response types must subclass {AsyncAPIResponse}; Received {origin}") From 249669c03db384d71c04fe69f78a579b5235c54c Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 16 Jun 2025 09:22:13 +0000 Subject: [PATCH 19/31] feat(api): api update --- .stats.yml | 4 ++-- src/together/types/training_method_dpo.py | 8 ++++++++ src/together/types/training_method_dpo_param.py | 8 ++++++++ 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index b0eabb03..ef30b3bd 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 30 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/togetherai%2Ftogetherai-29df763b2f7896b4f214e444a58d7aba39a45f90380e5deb3e010ff964f6d14c.yml -openapi_spec_hash: 8706285e2ec0c0b777452a74ae015326 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/togetherai%2Ftogetherai-4951d9964e7f7647cd3f716f3f127706e0a60d3afedb1d339de8b8c41cfc19d4.yml +openapi_spec_hash: 306c08678a0677f1deb1d35def6f8713 config_hash: d15dd709dd3f87b0a8b83b00b4abc881 diff --git a/src/together/types/training_method_dpo.py b/src/together/types/training_method_dpo.py index 2b633178..6ded8b31 100644 --- a/src/together/types/training_method_dpo.py +++ b/src/together/types/training_method_dpo.py @@ -12,3 +12,11 @@ class TrainingMethodDpo(BaseModel): method: Literal["dpo"] dpo_beta: Optional[float] = None + + dpo_normalize_logratios_by_length: Optional[bool] = None + + dpo_reference_free: Optional[bool] = None + + rpo_alpha: Optional[float] = None + + simpo_gamma: Optional[float] = None diff --git a/src/together/types/training_method_dpo_param.py b/src/together/types/training_method_dpo_param.py index 812deb77..cd776600 100644 --- a/src/together/types/training_method_dpo_param.py +++ b/src/together/types/training_method_dpo_param.py @@ -11,3 +11,11 @@ class TrainingMethodDpoParam(TypedDict, total=False): method: Required[Literal["dpo"]] dpo_beta: float + + dpo_normalize_logratios_by_length: bool + + dpo_reference_free: bool + + rpo_alpha: float + + simpo_gamma: float From 30ba23e549ed87a82a7e49164b1809388486754b Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 16 Jun 2025 14:19:55 +0000 Subject: [PATCH 20/31] chore(tests): add tests for httpx client instantiation & proxies --- tests/test_client.py | 46 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/tests/test_client.py b/tests/test_client.py index afa6dbf1..e9cbceae 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -32,6 +32,8 @@ DEFAULT_TIMEOUT, HTTPX_DEFAULT_TIMEOUT, BaseClient, + DefaultHttpxClient, + DefaultAsyncHttpxClient, make_request_options, ) from together.types.chat.completion_create_params import CompletionCreateParamsNonStreaming @@ -884,6 +886,28 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: assert response.http_request.headers.get("x-stainless-retry-count") == "42" + def test_proxy_environment_variables(self, monkeypatch: pytest.MonkeyPatch) -> None: + # Test that the proxy environment variables are set correctly + monkeypatch.setenv("HTTPS_PROXY", "https://example.org") + + client = DefaultHttpxClient() + + mounts = tuple(client._mounts.items()) + assert len(mounts) == 1 + assert mounts[0][0].pattern == "https://" + + @pytest.mark.filterwarnings("ignore:.*deprecated.*:DeprecationWarning") + def test_default_client_creation(self) -> None: + # Ensure that the client can be initialized without any exceptions + DefaultHttpxClient( + verify=True, + cert=None, + trust_env=True, + http1=True, + http2=False, + limits=httpx.Limits(max_connections=100, max_keepalive_connections=20), + ) + @pytest.mark.respx(base_url=base_url) def test_follow_redirects(self, respx_mock: MockRouter) -> None: # Test that the default follow_redirects=True allows following redirects @@ -1801,6 +1825,28 @@ async def test_main() -> None: time.sleep(0.1) + async def test_proxy_environment_variables(self, monkeypatch: pytest.MonkeyPatch) -> None: + # Test that the proxy environment variables are set correctly + monkeypatch.setenv("HTTPS_PROXY", "https://example.org") + + client = DefaultAsyncHttpxClient() + + mounts = tuple(client._mounts.items()) + assert len(mounts) == 1 + assert mounts[0][0].pattern == "https://" + + @pytest.mark.filterwarnings("ignore:.*deprecated.*:DeprecationWarning") + async def test_default_client_creation(self) -> None: + # Ensure that the client can be initialized without any exceptions + DefaultAsyncHttpxClient( + verify=True, + cert=None, + trust_env=True, + http1=True, + http2=False, + limits=httpx.Limits(max_connections=100, max_keepalive_connections=20), + ) + @pytest.mark.respx(base_url=base_url) async def test_follow_redirects(self, respx_mock: MockRouter) -> None: # Test that the default follow_redirects=True allows following redirects From 2b13ac4298cc44c0515a3aa348cfdb4bc63d9cb2 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 16 Jun 2025 18:58:28 +0000 Subject: [PATCH 21/31] chore(internal): update conftest.py --- tests/conftest.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index b7e86792..991834d2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,3 +1,5 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + from __future__ import annotations import os From 6e4d972a3a3094fb2d8d468d1e3e89b173ce6ffd Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 17 Jun 2025 00:10:32 +0000 Subject: [PATCH 22/31] chore(ci): enable for pull requests --- .github/workflows/ci.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4fd5e4d1..66e067e1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,6 +7,10 @@ on: - 'integrated/**' - 'stl-preview-head/**' - 'stl-preview-base/**' + pull_request: + branches-ignore: + - 'stl-preview-head/**' + - 'stl-preview-base/**' jobs: lint: From acfabb57a60aab2853283f62d72897a8bb95a778 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 17 Jun 2025 14:52:29 +0000 Subject: [PATCH 23/31] chore(readme): update badges --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 35403102..be499079 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Together Python API library -[![PyPI version](https://img.shields.io/pypi/v/together.svg)](https://pypi.org/project/together/) +[![PyPI version]()](https://pypi.org/project/together/) The Together Python library provides convenient access to the Together REST API from any Python 3.8+ application. The library includes type definitions for all request params and response fields, From 82b2dcb43af96a7339b2305d02486d3084850303 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 17 Jun 2025 21:11:01 +0000 Subject: [PATCH 24/31] fix(tests): fix: tests which call HTTP endpoints directly with the example parameters --- tests/test_client.py | 133 ++++++++++++++----------------------------- 1 file changed, 44 insertions(+), 89 deletions(-) diff --git a/tests/test_client.py b/tests/test_client.py index e9cbceae..4263206f 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -23,9 +23,7 @@ from together import Together, AsyncTogether, APIResponseValidationError from together._types import Omit -from together._utils import maybe_transform from together._models import BaseModel, FinalRequestOptions -from together._constants import RAW_RESPONSE_HEADER from together._streaming import Stream, AsyncStream from together._exceptions import TogetherError, APIStatusError, APITimeoutError, APIResponseValidationError from together._base_client import ( @@ -36,7 +34,6 @@ DefaultAsyncHttpxClient, make_request_options, ) -from together.types.chat.completion_create_params import CompletionCreateParamsNonStreaming from .utils import update_env @@ -727,60 +724,37 @@ def test_parse_retry_after_header(self, remaining_retries: int, retry_after: str @mock.patch("together._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) - def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter) -> None: + def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter, client: Together) -> None: respx_mock.post("/chat/completions").mock(side_effect=httpx.TimeoutException("Test timeout error")) with pytest.raises(APITimeoutError): - self.client.post( - "/chat/completions", - body=cast( - object, - maybe_transform( - dict( - messages=[ - { - "role": "user", - "content": "Say this is a test", - } - ], - model="mistralai/Mixtral-8x7B-Instruct-v0.1", - ), - CompletionCreateParamsNonStreaming, - ), - ), - cast_to=httpx.Response, - options={"headers": {RAW_RESPONSE_HEADER: "stream"}}, - ) + client.chat.completions.with_streaming_response.create( + messages=[ + { + "content": "content", + "role": "system", + } + ], + model="meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo", + ).__enter__() assert _get_open_connections(self.client) == 0 @mock.patch("together._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) - def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter) -> None: + def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter, client: Together) -> None: respx_mock.post("/chat/completions").mock(return_value=httpx.Response(500)) with pytest.raises(APIStatusError): - self.client.post( - "/chat/completions", - body=cast( - object, - maybe_transform( - dict( - messages=[ - { - "role": "user", - "content": "Say this is a test", - } - ], - model="mistralai/Mixtral-8x7B-Instruct-v0.1", - ), - CompletionCreateParamsNonStreaming, - ), - ), - cast_to=httpx.Response, - options={"headers": {RAW_RESPONSE_HEADER: "stream"}}, - ) - + client.chat.completions.with_streaming_response.create( + messages=[ + { + "content": "content", + "role": "system", + } + ], + model="meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo", + ).__enter__() assert _get_open_connections(self.client) == 0 @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) @@ -1618,60 +1592,41 @@ async def test_parse_retry_after_header(self, remaining_retries: int, retry_afte @mock.patch("together._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) - async def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter) -> None: + async def test_retrying_timeout_errors_doesnt_leak( + self, respx_mock: MockRouter, async_client: AsyncTogether + ) -> None: respx_mock.post("/chat/completions").mock(side_effect=httpx.TimeoutException("Test timeout error")) with pytest.raises(APITimeoutError): - await self.client.post( - "/chat/completions", - body=cast( - object, - maybe_transform( - dict( - messages=[ - { - "role": "user", - "content": "Say this is a test", - } - ], - model="mistralai/Mixtral-8x7B-Instruct-v0.1", - ), - CompletionCreateParamsNonStreaming, - ), - ), - cast_to=httpx.Response, - options={"headers": {RAW_RESPONSE_HEADER: "stream"}}, - ) + await async_client.chat.completions.with_streaming_response.create( + messages=[ + { + "content": "content", + "role": "system", + } + ], + model="meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo", + ).__aenter__() assert _get_open_connections(self.client) == 0 @mock.patch("together._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) - async def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter) -> None: + async def test_retrying_status_errors_doesnt_leak( + self, respx_mock: MockRouter, async_client: AsyncTogether + ) -> None: respx_mock.post("/chat/completions").mock(return_value=httpx.Response(500)) with pytest.raises(APIStatusError): - await self.client.post( - "/chat/completions", - body=cast( - object, - maybe_transform( - dict( - messages=[ - { - "role": "user", - "content": "Say this is a test", - } - ], - model="mistralai/Mixtral-8x7B-Instruct-v0.1", - ), - CompletionCreateParamsNonStreaming, - ), - ), - cast_to=httpx.Response, - options={"headers": {RAW_RESPONSE_HEADER: "stream"}}, - ) - + await async_client.chat.completions.with_streaming_response.create( + messages=[ + { + "content": "content", + "role": "system", + } + ], + model="meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo", + ).__aenter__() assert _get_open_connections(self.client) == 0 @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) From bed4e88653ff35029c1921bd2d940abade5b00c0 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 18 Jun 2025 12:41:47 +0000 Subject: [PATCH 25/31] docs(client): fix httpx.Timeout documentation reference --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index be499079..6efa5cef 100644 --- a/README.md +++ b/README.md @@ -258,7 +258,7 @@ client.with_options(max_retries=5).chat.completions.create( ### Timeouts By default requests time out after 1 minute. You can configure this with a `timeout` option, -which accepts a float or an [`httpx.Timeout`](https://www.python-httpx.org/advanced/#fine-tuning-the-configuration) object: +which accepts a float or an [`httpx.Timeout`](https://www.python-httpx.org/advanced/timeouts/#fine-tuning-the-configuration) object: ```python from together import Together From 8fac9f3e12630ed88b68c6cb7d798ebcc6a88833 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 20 Jun 2025 17:34:27 +0000 Subject: [PATCH 26/31] chore: change publish docs url --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6efa5cef..d251b4a3 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ pip install git+ssh://git@github.com/togethercomputer/together-py.git ``` > [!NOTE] -> Once this package is [published to PyPI](https://app.stainless.com/docs/guides/publish), this will become: `pip install --pre together` +> Once this package is [published to PyPI](https://www.stainless.com/docs/guides/publish), this will become: `pip install --pre together` ## Usage From 8e4cedf646520031811a97f65460f41b61894dd9 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 20 Jun 2025 18:04:46 +0000 Subject: [PATCH 27/31] feat(client): add support for aiohttp --- README.md | 40 +++++++++++++++++ pyproject.toml | 3 ++ requirements-dev.lock | 31 ++++++++++++- requirements.lock | 27 ++++++++++++ src/together/__init__.py | 3 +- src/together/_base_client.py | 22 ++++++++++ tests/api_resources/chat/test_completions.py | 4 +- .../code_interpreter/test_sessions.py | 4 +- tests/api_resources/test_audio.py | 4 +- tests/api_resources/test_client.py | 4 +- tests/api_resources/test_code_interpreter.py | 4 +- tests/api_resources/test_completions.py | 4 +- tests/api_resources/test_embeddings.py | 4 +- tests/api_resources/test_endpoints.py | 4 +- tests/api_resources/test_files.py | 4 +- tests/api_resources/test_fine_tune.py | 4 +- tests/api_resources/test_hardware.py | 4 +- tests/api_resources/test_images.py | 4 +- tests/api_resources/test_jobs.py | 4 +- tests/api_resources/test_models.py | 4 +- tests/conftest.py | 43 ++++++++++++++++--- 21 files changed, 202 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index d251b4a3..3e330c27 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,46 @@ asyncio.run(main()) Functionality between the synchronous and asynchronous clients is otherwise identical. +### With aiohttp + +By default, the async client uses `httpx` for HTTP requests. However, for improved concurrency performance you may also use `aiohttp` as the HTTP backend. + +You can enable this by installing `aiohttp`: + +```sh +# install from the production repo +pip install 'together[aiohttp] @ git+ssh://git@github.com/togethercomputer/together-py.git' +``` + +Then you can enable it by instantiating the client with `http_client=DefaultAioHttpClient()`: + +```python +import os +import asyncio +from together import DefaultAioHttpClient +from together import AsyncTogether + + +async def main() -> None: + async with AsyncTogether( + api_key=os.environ.get("TOGETHER_API_KEY"), # This is the default and can be omitted + http_client=DefaultAioHttpClient(), + ) as client: + chat_completion = await client.chat.completions.create( + messages=[ + { + "role": "user", + "content": "Say this is a test!", + } + ], + model="mistralai/Mixtral-8x7B-Instruct-v0.1", + ) + print(chat_completion.choices) + + +asyncio.run(main()) +``` + ## Streaming responses We provide support for streaming responses using Server Side Events (SSE). diff --git a/pyproject.toml b/pyproject.toml index 6d0a371c..da2cba30 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,6 +45,9 @@ classifiers = [ Homepage = "https://github.com/togethercomputer/together-py" Repository = "https://github.com/togethercomputer/together-py" +[project.optional-dependencies] +aiohttp = ["aiohttp", "httpx_aiohttp>=0.1.6"] + [project.scripts] together = "together.lib.cli.cli:main" diff --git a/requirements-dev.lock b/requirements-dev.lock index 07a67d4d..6d7ab8dc 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -10,6 +10,13 @@ # universal: false -e file:. +aiohappyeyeballs==2.6.1 + # via aiohttp +aiohttp==3.12.13 + # via httpx-aiohttp + # via together +aiosignal==1.3.2 + # via aiohttp annotated-types==0.6.0 # via pydantic anyio==4.4.0 @@ -17,6 +24,10 @@ anyio==4.4.0 # via together argcomplete==3.1.2 # via nox +async-timeout==5.0.1 + # via aiohttp +attrs==25.3.0 + # via aiohttp certifi==2023.7.22 # via httpcore # via httpx @@ -36,16 +47,23 @@ execnet==2.1.1 # via pytest-xdist filelock==3.12.4 # via virtualenv +frozenlist==1.7.0 + # via aiohttp + # via aiosignal h11==0.14.0 # via httpcore httpcore==1.0.2 # via httpx httpx==0.28.1 + # via httpx-aiohttp # via respx # via together +httpx-aiohttp==0.1.6 + # via together idna==3.4 # via anyio # via httpx + # via yarl importlib-metadata==7.0.0 iniconfig==2.0.0 # via pytest @@ -53,6 +71,9 @@ markdown-it-py==3.0.0 # via rich mdurl==0.1.2 # via markdown-it-py +multidict==6.5.0 + # via aiohttp + # via yarl mypy==1.14.1 mypy-extensions==1.0.0 # via mypy @@ -71,6 +92,9 @@ platformdirs==3.11.0 # via virtualenv pluggy==1.5.0 # via pytest +propcache==0.3.2 + # via aiohttp + # via yarl pyarrow==16.1.0 # via together pyarrow-stubs==10.0.1.7 @@ -84,11 +108,11 @@ pygments==2.18.0 pyright==1.1.399 pytest==8.3.3 # via pytest-asyncio - # via pytest-xdist # via pytest-mock + # via pytest-xdist pytest-asyncio==0.24.0 -pytest-xdist==3.7.0 pytest-mock==3.14.0 +pytest-xdist==3.7.0 python-dateutil==2.8.2 # via time-machine pytz==2023.3.post1 @@ -119,6 +143,7 @@ types-tqdm==4.67.0.20250516 # via together typing-extensions==4.12.2 # via anyio + # via multidict # via mypy # via pydantic # via pydantic-core @@ -128,5 +153,7 @@ urllib3==2.4.0 # via types-requests virtualenv==20.24.5 # via nox +yarl==1.20.1 + # via aiohttp zipp==3.17.0 # via importlib-metadata diff --git a/requirements.lock b/requirements.lock index d7f9180b..ab4313ce 100644 --- a/requirements.lock +++ b/requirements.lock @@ -10,11 +10,22 @@ # universal: false -e file:. +aiohappyeyeballs==2.6.1 + # via aiohttp +aiohttp==3.12.13 + # via httpx-aiohttp + # via together +aiosignal==1.3.2 + # via aiohttp annotated-types==0.6.0 # via pydantic anyio==4.4.0 # via httpx # via together +async-timeout==5.0.1 + # via aiohttp +attrs==25.3.0 + # via aiohttp certifi==2023.7.22 # via httpcore # via httpx @@ -24,19 +35,32 @@ distro==1.8.0 # via together exceptiongroup==1.2.2 # via anyio +frozenlist==1.7.0 + # via aiohttp + # via aiosignal h11==0.14.0 # via httpcore httpcore==1.0.2 # via httpx httpx==0.28.1 + # via httpx-aiohttp + # via together +httpx-aiohttp==0.1.6 # via together idna==3.4 # via anyio # via httpx + # via yarl +multidict==6.5.0 + # via aiohttp + # via yarl numpy==2.0.0 # via pyarrow pillow==10.4.0 # via together +propcache==0.3.2 + # via aiohttp + # via yarl pyarrow==16.1.0 # via together pyarrow-stubs==10.0.1.7 @@ -60,8 +84,11 @@ types-tqdm==4.67.0.20250516 # via together typing-extensions==4.12.2 # via anyio + # via multidict # via pydantic # via pydantic-core # via together urllib3==2.4.0 # via types-requests +yarl==1.20.1 + # via aiohttp diff --git a/src/together/__init__.py b/src/together/__init__.py index 50964a8c..2fd17bda 100644 --- a/src/together/__init__.py +++ b/src/together/__init__.py @@ -42,7 +42,7 @@ UnprocessableEntityError, APIResponseValidationError, ) -from ._base_client import DefaultHttpxClient, DefaultAsyncHttpxClient +from ._base_client import DefaultHttpxClient, DefaultAioHttpClient, DefaultAsyncHttpxClient from ._utils._logs import setup_logging as _setup_logging __all__ = [ @@ -84,6 +84,7 @@ "DEFAULT_CONNECTION_LIMITS", "DefaultHttpxClient", "DefaultAsyncHttpxClient", + "DefaultAioHttpClient", "create_finetune_request", "FinetuneTrainingLimits", "DownloadError", diff --git a/src/together/_base_client.py b/src/together/_base_client.py index c1e33b25..b73339b5 100644 --- a/src/together/_base_client.py +++ b/src/together/_base_client.py @@ -1289,6 +1289,24 @@ def __init__(self, **kwargs: Any) -> None: super().__init__(**kwargs) +try: + import httpx_aiohttp +except ImportError: + + class _DefaultAioHttpClient(httpx.AsyncClient): + def __init__(self, **_kwargs: Any) -> None: + raise RuntimeError("To use the aiohttp client you must have installed the package with the `aiohttp` extra") +else: + + class _DefaultAioHttpClient(httpx_aiohttp.HttpxAiohttpClient): # type: ignore + def __init__(self, **kwargs: Any) -> None: + kwargs.setdefault("timeout", DEFAULT_TIMEOUT) + kwargs.setdefault("limits", DEFAULT_CONNECTION_LIMITS) + kwargs.setdefault("follow_redirects", True) + + super().__init__(**kwargs) + + if TYPE_CHECKING: DefaultAsyncHttpxClient = httpx.AsyncClient """An alias to `httpx.AsyncClient` that provides the same defaults that this SDK @@ -1297,8 +1315,12 @@ def __init__(self, **kwargs: Any) -> None: This is useful because overriding the `http_client` with your own instance of `httpx.AsyncClient` will result in httpx's defaults being used, not ours. """ + + DefaultAioHttpClient = httpx.AsyncClient + """An alias to `httpx.AsyncClient` that changes the default HTTP transport to `aiohttp`.""" else: DefaultAsyncHttpxClient = _DefaultAsyncHttpxClient + DefaultAioHttpClient = _DefaultAioHttpClient class AsyncHttpxClientWrapper(DefaultAsyncHttpxClient): diff --git a/tests/api_resources/chat/test_completions.py b/tests/api_resources/chat/test_completions.py index 82987709..8b4c6fb6 100644 --- a/tests/api_resources/chat/test_completions.py +++ b/tests/api_resources/chat/test_completions.py @@ -219,7 +219,9 @@ def test_streaming_response_create_overload_2(self, client: Together) -> None: class TestAsyncCompletions: - parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) @parametrize async def test_method_create_overload_1(self, async_client: AsyncTogether) -> None: diff --git a/tests/api_resources/code_interpreter/test_sessions.py b/tests/api_resources/code_interpreter/test_sessions.py index e53d7a4a..19313230 100644 --- a/tests/api_resources/code_interpreter/test_sessions.py +++ b/tests/api_resources/code_interpreter/test_sessions.py @@ -53,7 +53,9 @@ def test_streaming_response_list(self, client: Together) -> None: class TestAsyncSessions: - parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) @pytest.mark.skip( reason="currently no good way to test endpoints defining callbacks, Prism mock server will fail trying to reach the provided callback url" diff --git a/tests/api_resources/test_audio.py b/tests/api_resources/test_audio.py index 5f06c217..0e01c6a9 100644 --- a/tests/api_resources/test_audio.py +++ b/tests/api_resources/test_audio.py @@ -163,7 +163,9 @@ def test_streaming_response_create_overload_2(self, client: Together, respx_mock class TestAsyncAudio: - parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) @pytest.mark.skip(reason="AttributeError: BinaryAPIResponse object has no attribute response") @parametrize diff --git a/tests/api_resources/test_client.py b/tests/api_resources/test_client.py index e057e9ba..691c42c4 100644 --- a/tests/api_resources/test_client.py +++ b/tests/api_resources/test_client.py @@ -136,7 +136,9 @@ def test_streaming_response_rerank(self, client: Together) -> None: class TestAsyncClient: - parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) @parametrize async def test_method_rerank(self, async_client: AsyncTogether) -> None: diff --git a/tests/api_resources/test_code_interpreter.py b/tests/api_resources/test_code_interpreter.py index 17c1928c..f3f405b2 100644 --- a/tests/api_resources/test_code_interpreter.py +++ b/tests/api_resources/test_code_interpreter.py @@ -81,7 +81,9 @@ def test_streaming_response_execute(self, client: Together) -> None: class TestAsyncCodeInterpreter: - parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) @pytest.mark.skip( reason="currently no good way to test endpoints defining callbacks, Prism mock server will fail trying to reach the provided callback url" diff --git a/tests/api_resources/test_completions.py b/tests/api_resources/test_completions.py index ef05bb50..1440c691 100644 --- a/tests/api_resources/test_completions.py +++ b/tests/api_resources/test_completions.py @@ -143,7 +143,9 @@ def test_streaming_response_create_overload_2(self, client: Together) -> None: class TestAsyncCompletions: - parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) @parametrize async def test_method_create_overload_1(self, async_client: AsyncTogether) -> None: diff --git a/tests/api_resources/test_embeddings.py b/tests/api_resources/test_embeddings.py index 084ad480..779e505a 100644 --- a/tests/api_resources/test_embeddings.py +++ b/tests/api_resources/test_embeddings.py @@ -53,7 +53,9 @@ def test_streaming_response_create(self, client: Together) -> None: class TestAsyncEmbeddings: - parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) @parametrize async def test_method_create(self, async_client: AsyncTogether) -> None: diff --git a/tests/api_resources/test_endpoints.py b/tests/api_resources/test_endpoints.py index 59cbc6ab..5ab5c225 100644 --- a/tests/api_resources/test_endpoints.py +++ b/tests/api_resources/test_endpoints.py @@ -247,7 +247,9 @@ def test_path_params_delete(self, client: Together) -> None: class TestAsyncEndpoints: - parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) @parametrize async def test_method_create(self, async_client: AsyncTogether) -> None: diff --git a/tests/api_resources/test_files.py b/tests/api_resources/test_files.py index 27f7bb27..ec1b4dfe 100644 --- a/tests/api_resources/test_files.py +++ b/tests/api_resources/test_files.py @@ -230,7 +230,9 @@ def test_streaming_response_upload(self, client: Together) -> None: class TestAsyncFiles: - parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) @parametrize async def test_method_retrieve(self, async_client: AsyncTogether) -> None: diff --git a/tests/api_resources/test_fine_tune.py b/tests/api_resources/test_fine_tune.py index a8a64f3e..cf40ef0f 100644 --- a/tests/api_resources/test_fine_tune.py +++ b/tests/api_resources/test_fine_tune.py @@ -312,7 +312,9 @@ def test_path_params_retrieve_checkpoints(self, client: Together) -> None: class TestAsyncFineTune: - parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) @parametrize async def test_method_create(self, async_client: AsyncTogether) -> None: diff --git a/tests/api_resources/test_hardware.py b/tests/api_resources/test_hardware.py index aafe18f0..737d10f5 100644 --- a/tests/api_resources/test_hardware.py +++ b/tests/api_resources/test_hardware.py @@ -51,7 +51,9 @@ def test_streaming_response_list(self, client: Together) -> None: class TestAsyncHardware: - parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) @parametrize async def test_method_list(self, async_client: AsyncTogether) -> None: diff --git a/tests/api_resources/test_images.py b/tests/api_resources/test_images.py index d95acfcd..16851aaa 100644 --- a/tests/api_resources/test_images.py +++ b/tests/api_resources/test_images.py @@ -77,7 +77,9 @@ def test_streaming_response_create(self, client: Together) -> None: class TestAsyncImages: - parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) @parametrize async def test_method_create(self, async_client: AsyncTogether) -> None: diff --git a/tests/api_resources/test_jobs.py b/tests/api_resources/test_jobs.py index 110600d7..70711d0a 100644 --- a/tests/api_resources/test_jobs.py +++ b/tests/api_resources/test_jobs.py @@ -82,7 +82,9 @@ def test_streaming_response_list(self, client: Together) -> None: class TestAsyncJobs: - parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) @parametrize async def test_method_retrieve(self, async_client: AsyncTogether) -> None: diff --git a/tests/api_resources/test_models.py b/tests/api_resources/test_models.py index fbf910a0..c3689674 100644 --- a/tests/api_resources/test_models.py +++ b/tests/api_resources/test_models.py @@ -91,7 +91,9 @@ def test_streaming_response_upload(self, client: Together) -> None: class TestAsyncModels: - parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) @parametrize async def test_method_list(self, async_client: AsyncTogether) -> None: diff --git a/tests/conftest.py b/tests/conftest.py index 991834d2..97bce53e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,10 +6,12 @@ import logging from typing import TYPE_CHECKING, Iterator, AsyncIterator +import httpx import pytest from pytest_asyncio import is_async_test -from together import Together, AsyncTogether +from together import Together, AsyncTogether, DefaultAioHttpClient +from together._utils import is_dict if TYPE_CHECKING: from _pytest.fixtures import FixtureRequest # pyright: ignore[reportPrivateImportUsage] @@ -27,6 +29,19 @@ def pytest_collection_modifyitems(items: list[pytest.Function]) -> None: for async_test in pytest_asyncio_tests: async_test.add_marker(session_scope_marker, append=False) + # We skip tests that use both the aiohttp client and respx_mock as respx_mock + # doesn't support custom transports. + for item in items: + if "async_client" not in item.fixturenames or "respx_mock" not in item.fixturenames: + continue + + if not hasattr(item, "callspec"): + continue + + async_client_param = item.callspec.params.get("async_client") + if is_dict(async_client_param) and async_client_param.get("http_client") == "aiohttp": + item.add_marker(pytest.mark.skip(reason="aiohttp client is not compatible with respx_mock")) + base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -45,9 +60,25 @@ def client(request: FixtureRequest) -> Iterator[Together]: @pytest.fixture(scope="session") async def async_client(request: FixtureRequest) -> AsyncIterator[AsyncTogether]: - strict = getattr(request, "param", True) - if not isinstance(strict, bool): - raise TypeError(f"Unexpected fixture parameter type {type(strict)}, expected {bool}") - - async with AsyncTogether(base_url=base_url, api_key=api_key, _strict_response_validation=strict) as client: + param = getattr(request, "param", True) + + # defaults + strict = True + http_client: None | httpx.AsyncClient = None + + if isinstance(param, bool): + strict = param + elif is_dict(param): + strict = param.get("strict", True) + assert isinstance(strict, bool) + + http_client_type = param.get("http_client", "httpx") + if http_client_type == "aiohttp": + http_client = DefaultAioHttpClient() + else: + raise TypeError(f"Unexpected fixture parameter type {type(param)}, expected bool or dict") + + async with AsyncTogether( + base_url=base_url, api_key=api_key, _strict_response_validation=strict, http_client=http_client + ) as client: yield client From 07299cc337cb356076643df7fc070b2fd8e85c54 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 20 Jun 2025 21:59:56 +0000 Subject: [PATCH 28/31] feat(api): add batch api to config --- .stats.yml | 6 +- api.md | 14 + src/together/_client.py | 22 +- src/together/resources/__init__.py | 14 + src/together/resources/batches.py | 339 ++++++++++++++++++ src/together/types/__init__.py | 4 + src/together/types/batch_create_params.py | 24 ++ src/together/types/batch_create_response.py | 51 +++ src/together/types/batch_list_response.py | 48 +++ src/together/types/batch_retrieve_response.py | 45 +++ tests/api_resources/test_batches.py | 240 +++++++++++++ 11 files changed, 803 insertions(+), 4 deletions(-) create mode 100644 src/together/resources/batches.py create mode 100644 src/together/types/batch_create_params.py create mode 100644 src/together/types/batch_create_response.py create mode 100644 src/together/types/batch_list_response.py create mode 100644 src/together/types/batch_retrieve_response.py create mode 100644 tests/api_resources/test_batches.py diff --git a/.stats.yml b/.stats.yml index ef30b3bd..fcc638d2 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 30 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/togetherai%2Ftogetherai-4951d9964e7f7647cd3f716f3f127706e0a60d3afedb1d339de8b8c41cfc19d4.yml +configured_endpoints: 33 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/togetherai%2Ftogetherai-9ca24c17ccf9b0b4c2d27c09881dc74bf4cb44efc7a5ccb7d54fa15caee095d1.yml openapi_spec_hash: 306c08678a0677f1deb1d35def6f8713 -config_hash: d15dd709dd3f87b0a8b83b00b4abc881 +config_hash: fa59b4c1ab0a2e74aa855e7227b10c7d diff --git a/api.md b/api.md index 6a918eec..729fb65f 100644 --- a/api.md +++ b/api.md @@ -220,3 +220,17 @@ from together.types import HardwareListResponse Methods: - client.hardware.list(\*\*params) -> HardwareListResponse + +# Batches + +Types: + +```python +from together.types import BatchCreateResponse, BatchRetrieveResponse, BatchListResponse +``` + +Methods: + +- client.batches.create(\*\*params) -> BatchCreateResponse +- client.batches.retrieve(id) -> BatchRetrieveResponse +- client.batches.list() -> BatchListResponse diff --git a/src/together/_client.py b/src/together/_client.py index f2e64aed..0690aa27 100644 --- a/src/together/_client.py +++ b/src/together/_client.py @@ -36,7 +36,19 @@ async_to_raw_response_wrapper, async_to_streamed_response_wrapper, ) -from .resources import jobs, audio, files, images, models, hardware, endpoints, fine_tune, embeddings, completions +from .resources import ( + jobs, + audio, + files, + images, + models, + batches, + hardware, + endpoints, + fine_tune, + embeddings, + completions, +) from ._streaming import Stream as Stream, AsyncStream as AsyncStream from ._exceptions import TogetherError, APIStatusError from ._base_client import ( @@ -74,6 +86,7 @@ class Together(SyncAPIClient): jobs: jobs.JobsResource endpoints: endpoints.EndpointsResource hardware: hardware.HardwareResource + batches: batches.BatchesResource with_raw_response: TogetherWithRawResponse with_streaming_response: TogetherWithStreamedResponse @@ -145,6 +158,7 @@ def __init__( self.jobs = jobs.JobsResource(self) self.endpoints = endpoints.EndpointsResource(self) self.hardware = hardware.HardwareResource(self) + self.batches = batches.BatchesResource(self) self.with_raw_response = TogetherWithRawResponse(self) self.with_streaming_response = TogetherWithStreamedResponse(self) @@ -328,6 +342,7 @@ class AsyncTogether(AsyncAPIClient): jobs: jobs.AsyncJobsResource endpoints: endpoints.AsyncEndpointsResource hardware: hardware.AsyncHardwareResource + batches: batches.AsyncBatchesResource with_raw_response: AsyncTogetherWithRawResponse with_streaming_response: AsyncTogetherWithStreamedResponse @@ -399,6 +414,7 @@ def __init__( self.jobs = jobs.AsyncJobsResource(self) self.endpoints = endpoints.AsyncEndpointsResource(self) self.hardware = hardware.AsyncHardwareResource(self) + self.batches = batches.AsyncBatchesResource(self) self.with_raw_response = AsyncTogetherWithRawResponse(self) self.with_streaming_response = AsyncTogetherWithStreamedResponse(self) @@ -583,6 +599,7 @@ def __init__(self, client: Together) -> None: self.jobs = jobs.JobsResourceWithRawResponse(client.jobs) self.endpoints = endpoints.EndpointsResourceWithRawResponse(client.endpoints) self.hardware = hardware.HardwareResourceWithRawResponse(client.hardware) + self.batches = batches.BatchesResourceWithRawResponse(client.batches) self.rerank = to_raw_response_wrapper( client.rerank, @@ -603,6 +620,7 @@ def __init__(self, client: AsyncTogether) -> None: self.jobs = jobs.AsyncJobsResourceWithRawResponse(client.jobs) self.endpoints = endpoints.AsyncEndpointsResourceWithRawResponse(client.endpoints) self.hardware = hardware.AsyncHardwareResourceWithRawResponse(client.hardware) + self.batches = batches.AsyncBatchesResourceWithRawResponse(client.batches) self.rerank = async_to_raw_response_wrapper( client.rerank, @@ -623,6 +641,7 @@ def __init__(self, client: Together) -> None: self.jobs = jobs.JobsResourceWithStreamingResponse(client.jobs) self.endpoints = endpoints.EndpointsResourceWithStreamingResponse(client.endpoints) self.hardware = hardware.HardwareResourceWithStreamingResponse(client.hardware) + self.batches = batches.BatchesResourceWithStreamingResponse(client.batches) self.rerank = to_streamed_response_wrapper( client.rerank, @@ -645,6 +664,7 @@ def __init__(self, client: AsyncTogether) -> None: self.jobs = jobs.AsyncJobsResourceWithStreamingResponse(client.jobs) self.endpoints = endpoints.AsyncEndpointsResourceWithStreamingResponse(client.endpoints) self.hardware = hardware.AsyncHardwareResourceWithStreamingResponse(client.hardware) + self.batches = batches.AsyncBatchesResourceWithStreamingResponse(client.batches) self.rerank = async_to_streamed_response_wrapper( client.rerank, diff --git a/src/together/resources/__init__.py b/src/together/resources/__init__.py index bd3e4c51..c94aa657 100644 --- a/src/together/resources/__init__.py +++ b/src/together/resources/__init__.py @@ -48,6 +48,14 @@ ModelsResourceWithStreamingResponse, AsyncModelsResourceWithStreamingResponse, ) +from .batches import ( + BatchesResource, + AsyncBatchesResource, + BatchesResourceWithRawResponse, + AsyncBatchesResourceWithRawResponse, + BatchesResourceWithStreamingResponse, + AsyncBatchesResourceWithStreamingResponse, +) from .hardware import ( HardwareResource, AsyncHardwareResource, @@ -170,4 +178,10 @@ "AsyncHardwareResourceWithRawResponse", "HardwareResourceWithStreamingResponse", "AsyncHardwareResourceWithStreamingResponse", + "BatchesResource", + "AsyncBatchesResource", + "BatchesResourceWithRawResponse", + "AsyncBatchesResourceWithRawResponse", + "BatchesResourceWithStreamingResponse", + "AsyncBatchesResourceWithStreamingResponse", ] diff --git a/src/together/resources/batches.py b/src/together/resources/batches.py new file mode 100644 index 00000000..48fafe00 --- /dev/null +++ b/src/together/resources/batches.py @@ -0,0 +1,339 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import httpx + +from ..types import batch_create_params +from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven +from .._utils import maybe_transform, async_maybe_transform +from .._compat import cached_property +from .._resource import SyncAPIResource, AsyncAPIResource +from .._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from .._base_client import make_request_options +from ..types.batch_list_response import BatchListResponse +from ..types.batch_create_response import BatchCreateResponse +from ..types.batch_retrieve_response import BatchRetrieveResponse + +__all__ = ["BatchesResource", "AsyncBatchesResource"] + + +class BatchesResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> BatchesResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/togethercomputer/together-py#accessing-raw-response-data-eg-headers + """ + return BatchesResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> BatchesResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/togethercomputer/together-py#with_streaming_response + """ + return BatchesResourceWithStreamingResponse(self) + + def create( + self, + *, + endpoint: str, + input_file_id: str, + completion_window: str | NotGiven = NOT_GIVEN, + model_id: str | NotGiven = NOT_GIVEN, + priority: int | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> BatchCreateResponse: + """ + Create a new batch job with the given input file and endpoint + + Args: + endpoint: The endpoint to use for batch processing + + input_file_id: ID of the uploaded input file containing batch requests + + completion_window: Time window for batch completion (optional) + + model_id: Model to use for processing batch requests + + priority: Priority for batch processing (optional) + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._post( + "/batches", + body=maybe_transform( + { + "endpoint": endpoint, + "input_file_id": input_file_id, + "completion_window": completion_window, + "model_id": model_id, + "priority": priority, + }, + batch_create_params.BatchCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=BatchCreateResponse, + ) + + def retrieve( + self, + id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> BatchRetrieveResponse: + """ + Get details of a batch job by ID + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._get( + f"/batches/{id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=BatchRetrieveResponse, + ) + + def list( + self, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> BatchListResponse: + """List all batch jobs for the authenticated user""" + return self._get( + "/batches", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=BatchListResponse, + ) + + +class AsyncBatchesResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncBatchesResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/togethercomputer/together-py#accessing-raw-response-data-eg-headers + """ + return AsyncBatchesResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncBatchesResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/togethercomputer/together-py#with_streaming_response + """ + return AsyncBatchesResourceWithStreamingResponse(self) + + async def create( + self, + *, + endpoint: str, + input_file_id: str, + completion_window: str | NotGiven = NOT_GIVEN, + model_id: str | NotGiven = NOT_GIVEN, + priority: int | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> BatchCreateResponse: + """ + Create a new batch job with the given input file and endpoint + + Args: + endpoint: The endpoint to use for batch processing + + input_file_id: ID of the uploaded input file containing batch requests + + completion_window: Time window for batch completion (optional) + + model_id: Model to use for processing batch requests + + priority: Priority for batch processing (optional) + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return await self._post( + "/batches", + body=await async_maybe_transform( + { + "endpoint": endpoint, + "input_file_id": input_file_id, + "completion_window": completion_window, + "model_id": model_id, + "priority": priority, + }, + batch_create_params.BatchCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=BatchCreateResponse, + ) + + async def retrieve( + self, + id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> BatchRetrieveResponse: + """ + Get details of a batch job by ID + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._get( + f"/batches/{id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=BatchRetrieveResponse, + ) + + async def list( + self, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> BatchListResponse: + """List all batch jobs for the authenticated user""" + return await self._get( + "/batches", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=BatchListResponse, + ) + + +class BatchesResourceWithRawResponse: + def __init__(self, batches: BatchesResource) -> None: + self._batches = batches + + self.create = to_raw_response_wrapper( + batches.create, + ) + self.retrieve = to_raw_response_wrapper( + batches.retrieve, + ) + self.list = to_raw_response_wrapper( + batches.list, + ) + + +class AsyncBatchesResourceWithRawResponse: + def __init__(self, batches: AsyncBatchesResource) -> None: + self._batches = batches + + self.create = async_to_raw_response_wrapper( + batches.create, + ) + self.retrieve = async_to_raw_response_wrapper( + batches.retrieve, + ) + self.list = async_to_raw_response_wrapper( + batches.list, + ) + + +class BatchesResourceWithStreamingResponse: + def __init__(self, batches: BatchesResource) -> None: + self._batches = batches + + self.create = to_streamed_response_wrapper( + batches.create, + ) + self.retrieve = to_streamed_response_wrapper( + batches.retrieve, + ) + self.list = to_streamed_response_wrapper( + batches.list, + ) + + +class AsyncBatchesResourceWithStreamingResponse: + def __init__(self, batches: AsyncBatchesResource) -> None: + self._batches = batches + + self.create = async_to_streamed_response_wrapper( + batches.create, + ) + self.retrieve = async_to_streamed_response_wrapper( + batches.retrieve, + ) + self.list = async_to_streamed_response_wrapper( + batches.list, + ) diff --git a/src/together/types/__init__.py b/src/together/types/__init__.py index e3b99c70..401ca925 100644 --- a/src/together/types/__init__.py +++ b/src/together/types/__init__.py @@ -27,6 +27,8 @@ from .full_training_type import FullTrainingType as FullTrainingType from .lr_scheduler_param import LrSchedulerParam as LrSchedulerParam from .audio_create_params import AudioCreateParams as AudioCreateParams +from .batch_create_params import BatchCreateParams as BatchCreateParams +from .batch_list_response import BatchListResponse as BatchListResponse from .image_create_params import ImageCreateParams as ImageCreateParams from .lo_ra_training_type import LoRaTrainingType as LoRaTrainingType from .model_list_response import ModelListResponse as ModelListResponse @@ -38,6 +40,7 @@ from .file_delete_response import FileDeleteResponse as FileDeleteResponse from .file_upload_response import FileUploadResponse as FileUploadResponse from .hardware_list_params import HardwareListParams as HardwareListParams +from .batch_create_response import BatchCreateResponse as BatchCreateResponse from .job_retrieve_response import JobRetrieveResponse as JobRetrieveResponse from .model_upload_response import ModelUploadResponse as ModelUploadResponse from .endpoint_create_params import EndpointCreateParams as EndpointCreateParams @@ -45,6 +48,7 @@ from .endpoint_update_params import EndpointUpdateParams as EndpointUpdateParams from .file_retrieve_response import FileRetrieveResponse as FileRetrieveResponse from .hardware_list_response import HardwareListResponse as HardwareListResponse +from .batch_retrieve_response import BatchRetrieveResponse as BatchRetrieveResponse from .embedding_create_params import EmbeddingCreateParams as EmbeddingCreateParams from .fine_tune_create_params import FineTuneCreateParams as FineTuneCreateParams from .fine_tune_list_response import FineTuneListResponse as FineTuneListResponse diff --git a/src/together/types/batch_create_params.py b/src/together/types/batch_create_params.py new file mode 100644 index 00000000..8b696489 --- /dev/null +++ b/src/together/types/batch_create_params.py @@ -0,0 +1,24 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["BatchCreateParams"] + + +class BatchCreateParams(TypedDict, total=False): + endpoint: Required[str] + """The endpoint to use for batch processing""" + + input_file_id: Required[str] + """ID of the uploaded input file containing batch requests""" + + completion_window: str + """Time window for batch completion (optional)""" + + model_id: str + """Model to use for processing batch requests""" + + priority: int + """Priority for batch processing (optional)""" diff --git a/src/together/types/batch_create_response.py b/src/together/types/batch_create_response.py new file mode 100644 index 00000000..382f1548 --- /dev/null +++ b/src/together/types/batch_create_response.py @@ -0,0 +1,51 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from datetime import datetime +from typing_extensions import Literal + +from pydantic import Field as FieldInfo + +from .._models import BaseModel + +__all__ = ["BatchCreateResponse", "Job"] + + +class Job(BaseModel): + id: Optional[str] = None + + completed_at: Optional[datetime] = None + + created_at: Optional[datetime] = None + + endpoint: Optional[str] = None + + error: Optional[str] = None + + error_file_id: Optional[str] = None + + file_size_bytes: Optional[int] = None + """Size of input file in bytes""" + + input_file_id: Optional[str] = None + + job_deadline: Optional[datetime] = None + + x_model_id: Optional[str] = FieldInfo(alias="model_id", default=None) + """Model used for processing requests""" + + output_file_id: Optional[str] = None + + progress: Optional[float] = None + """Completion progress (0.0 to 100)""" + + status: Optional[Literal["VALIDATING", "IN_PROGRESS", "COMPLETED", "FAILED", "EXPIRED", "CANCELLED"]] = None + """Current status of the batch job""" + + user_id: Optional[str] = None + + +class BatchCreateResponse(BaseModel): + job: Optional[Job] = None + + warning: Optional[str] = None diff --git a/src/together/types/batch_list_response.py b/src/together/types/batch_list_response.py new file mode 100644 index 00000000..11b453c8 --- /dev/null +++ b/src/together/types/batch_list_response.py @@ -0,0 +1,48 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional +from datetime import datetime +from typing_extensions import Literal, TypeAlias + +from pydantic import Field as FieldInfo + +from .._models import BaseModel + +__all__ = ["BatchListResponse", "BatchListResponseItem"] + + +class BatchListResponseItem(BaseModel): + id: Optional[str] = None + + completed_at: Optional[datetime] = None + + created_at: Optional[datetime] = None + + endpoint: Optional[str] = None + + error: Optional[str] = None + + error_file_id: Optional[str] = None + + file_size_bytes: Optional[int] = None + """Size of input file in bytes""" + + input_file_id: Optional[str] = None + + job_deadline: Optional[datetime] = None + + x_model_id: Optional[str] = FieldInfo(alias="model_id", default=None) + """Model used for processing requests""" + + output_file_id: Optional[str] = None + + progress: Optional[float] = None + """Completion progress (0.0 to 100)""" + + status: Optional[Literal["VALIDATING", "IN_PROGRESS", "COMPLETED", "FAILED", "EXPIRED", "CANCELLED"]] = None + """Current status of the batch job""" + + user_id: Optional[str] = None + + +BatchListResponse: TypeAlias = List[BatchListResponseItem] diff --git a/src/together/types/batch_retrieve_response.py b/src/together/types/batch_retrieve_response.py new file mode 100644 index 00000000..81483615 --- /dev/null +++ b/src/together/types/batch_retrieve_response.py @@ -0,0 +1,45 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from datetime import datetime +from typing_extensions import Literal + +from pydantic import Field as FieldInfo + +from .._models import BaseModel + +__all__ = ["BatchRetrieveResponse"] + + +class BatchRetrieveResponse(BaseModel): + id: Optional[str] = None + + completed_at: Optional[datetime] = None + + created_at: Optional[datetime] = None + + endpoint: Optional[str] = None + + error: Optional[str] = None + + error_file_id: Optional[str] = None + + file_size_bytes: Optional[int] = None + """Size of input file in bytes""" + + input_file_id: Optional[str] = None + + job_deadline: Optional[datetime] = None + + x_model_id: Optional[str] = FieldInfo(alias="model_id", default=None) + """Model used for processing requests""" + + output_file_id: Optional[str] = None + + progress: Optional[float] = None + """Completion progress (0.0 to 100)""" + + status: Optional[Literal["VALIDATING", "IN_PROGRESS", "COMPLETED", "FAILED", "EXPIRED", "CANCELLED"]] = None + """Current status of the batch job""" + + user_id: Optional[str] = None diff --git a/tests/api_resources/test_batches.py b/tests/api_resources/test_batches.py new file mode 100644 index 00000000..5b1b0a90 --- /dev/null +++ b/tests/api_resources/test_batches.py @@ -0,0 +1,240 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from together import Together, AsyncTogether +from tests.utils import assert_matches_type +from together.types import BatchListResponse, BatchCreateResponse, BatchRetrieveResponse + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestBatches: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + def test_method_create(self, client: Together) -> None: + batch = client.batches.create( + endpoint="/v1/chat/completions", + input_file_id="file-abc123def456ghi789", + ) + assert_matches_type(BatchCreateResponse, batch, path=["response"]) + + @parametrize + def test_method_create_with_all_params(self, client: Together) -> None: + batch = client.batches.create( + endpoint="/v1/chat/completions", + input_file_id="file-abc123def456ghi789", + completion_window="24h", + model_id="meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo", + priority=1, + ) + assert_matches_type(BatchCreateResponse, batch, path=["response"]) + + @parametrize + def test_raw_response_create(self, client: Together) -> None: + response = client.batches.with_raw_response.create( + endpoint="/v1/chat/completions", + input_file_id="file-abc123def456ghi789", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + batch = response.parse() + assert_matches_type(BatchCreateResponse, batch, path=["response"]) + + @parametrize + def test_streaming_response_create(self, client: Together) -> None: + with client.batches.with_streaming_response.create( + endpoint="/v1/chat/completions", + input_file_id="file-abc123def456ghi789", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + batch = response.parse() + assert_matches_type(BatchCreateResponse, batch, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_method_retrieve(self, client: Together) -> None: + batch = client.batches.retrieve( + "id", + ) + assert_matches_type(BatchRetrieveResponse, batch, path=["response"]) + + @parametrize + def test_raw_response_retrieve(self, client: Together) -> None: + response = client.batches.with_raw_response.retrieve( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + batch = response.parse() + assert_matches_type(BatchRetrieveResponse, batch, path=["response"]) + + @parametrize + def test_streaming_response_retrieve(self, client: Together) -> None: + with client.batches.with_streaming_response.retrieve( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + batch = response.parse() + assert_matches_type(BatchRetrieveResponse, batch, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_retrieve(self, client: Together) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.batches.with_raw_response.retrieve( + "", + ) + + @parametrize + def test_method_list(self, client: Together) -> None: + batch = client.batches.list() + assert_matches_type(BatchListResponse, batch, path=["response"]) + + @parametrize + def test_raw_response_list(self, client: Together) -> None: + response = client.batches.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + batch = response.parse() + assert_matches_type(BatchListResponse, batch, path=["response"]) + + @parametrize + def test_streaming_response_list(self, client: Together) -> None: + with client.batches.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + batch = response.parse() + assert_matches_type(BatchListResponse, batch, path=["response"]) + + assert cast(Any, response.is_closed) is True + + +class TestAsyncBatches: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @parametrize + async def test_method_create(self, async_client: AsyncTogether) -> None: + batch = await async_client.batches.create( + endpoint="/v1/chat/completions", + input_file_id="file-abc123def456ghi789", + ) + assert_matches_type(BatchCreateResponse, batch, path=["response"]) + + @parametrize + async def test_method_create_with_all_params(self, async_client: AsyncTogether) -> None: + batch = await async_client.batches.create( + endpoint="/v1/chat/completions", + input_file_id="file-abc123def456ghi789", + completion_window="24h", + model_id="meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo", + priority=1, + ) + assert_matches_type(BatchCreateResponse, batch, path=["response"]) + + @parametrize + async def test_raw_response_create(self, async_client: AsyncTogether) -> None: + response = await async_client.batches.with_raw_response.create( + endpoint="/v1/chat/completions", + input_file_id="file-abc123def456ghi789", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + batch = await response.parse() + assert_matches_type(BatchCreateResponse, batch, path=["response"]) + + @parametrize + async def test_streaming_response_create(self, async_client: AsyncTogether) -> None: + async with async_client.batches.with_streaming_response.create( + endpoint="/v1/chat/completions", + input_file_id="file-abc123def456ghi789", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + batch = await response.parse() + assert_matches_type(BatchCreateResponse, batch, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_method_retrieve(self, async_client: AsyncTogether) -> None: + batch = await async_client.batches.retrieve( + "id", + ) + assert_matches_type(BatchRetrieveResponse, batch, path=["response"]) + + @parametrize + async def test_raw_response_retrieve(self, async_client: AsyncTogether) -> None: + response = await async_client.batches.with_raw_response.retrieve( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + batch = await response.parse() + assert_matches_type(BatchRetrieveResponse, batch, path=["response"]) + + @parametrize + async def test_streaming_response_retrieve(self, async_client: AsyncTogether) -> None: + async with async_client.batches.with_streaming_response.retrieve( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + batch = await response.parse() + assert_matches_type(BatchRetrieveResponse, batch, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_retrieve(self, async_client: AsyncTogether) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.batches.with_raw_response.retrieve( + "", + ) + + @parametrize + async def test_method_list(self, async_client: AsyncTogether) -> None: + batch = await async_client.batches.list() + assert_matches_type(BatchListResponse, batch, path=["response"]) + + @parametrize + async def test_raw_response_list(self, async_client: AsyncTogether) -> None: + response = await async_client.batches.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + batch = await response.parse() + assert_matches_type(BatchListResponse, batch, path=["response"]) + + @parametrize + async def test_streaming_response_list(self, async_client: AsyncTogether) -> None: + async with async_client.batches.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + batch = await response.parse() + assert_matches_type(BatchListResponse, batch, path=["response"]) + + assert cast(Any, response.is_closed) is True From 49a71b3b35ffaef63bc8100faba69d87d517cedb Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 23 Jun 2025 18:49:51 +0000 Subject: [PATCH 29/31] chore(tests): skip some failing tests on the latest python versions --- tests/test_client.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_client.py b/tests/test_client.py index 4263206f..8f5dfaf5 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -192,6 +192,7 @@ def test_copy_signature(self) -> None: copy_param = copy_signature.parameters.get(name) assert copy_param is not None, f"copy() signature is missing the {name} param" + @pytest.mark.skipif(sys.version_info >= (3, 10), reason="fails because of a memory leak that started from 3.12") def test_copy_build_request(self) -> None: options = FinalRequestOptions(method="get", url="/foo") @@ -1045,6 +1046,7 @@ def test_copy_signature(self) -> None: copy_param = copy_signature.parameters.get(name) assert copy_param is not None, f"copy() signature is missing the {name} param" + @pytest.mark.skipif(sys.version_info >= (3, 10), reason="fails because of a memory leak that started from 3.12") def test_copy_build_request(self) -> None: options = FinalRequestOptions(method="get", url="/foo") From 02c8f9ad850aed5aae7110ab37d33377aead1c47 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 23 Jun 2025 22:20:40 +0000 Subject: [PATCH 30/31] chore(api): re-enable audio unit tests Now that codegen issue is fixed, reenable the audio tests. --- .stats.yml | 2 +- tests/api_resources/test_audio.py | 16 ---------------- 2 files changed, 1 insertion(+), 17 deletions(-) diff --git a/.stats.yml b/.stats.yml index fcc638d2..f801d735 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 33 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/togetherai%2Ftogetherai-9ca24c17ccf9b0b4c2d27c09881dc74bf4cb44efc7a5ccb7d54fa15caee095d1.yml openapi_spec_hash: 306c08678a0677f1deb1d35def6f8713 -config_hash: fa59b4c1ab0a2e74aa855e7227b10c7d +config_hash: ae07f8cefe84a8a01ae2d0c8ddc2ef32 diff --git a/tests/api_resources/test_audio.py b/tests/api_resources/test_audio.py index 0e01c6a9..21a76ac0 100644 --- a/tests/api_resources/test_audio.py +++ b/tests/api_resources/test_audio.py @@ -23,7 +23,6 @@ class TestAudio: parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) - @pytest.mark.skip(reason="AttributeError: BinaryAPIResponse object has no attribute response") @parametrize @pytest.mark.respx(base_url=base_url) def test_method_create_overload_1(self, client: Together, respx_mock: MockRouter) -> None: @@ -38,7 +37,6 @@ def test_method_create_overload_1(self, client: Together, respx_mock: MockRouter assert cast(Any, audio.is_closed) is True assert isinstance(audio, BinaryAPIResponse) - @pytest.mark.skip(reason="AttributeError: BinaryAPIResponse object has no attribute response") @parametrize @pytest.mark.respx(base_url=base_url) def test_method_create_with_all_params_overload_1(self, client: Together, respx_mock: MockRouter) -> None: @@ -58,7 +56,6 @@ def test_method_create_with_all_params_overload_1(self, client: Together, respx_ assert cast(Any, audio.is_closed) is True assert isinstance(audio, BinaryAPIResponse) - @pytest.mark.skip(reason="AttributeError: BinaryAPIResponse object has no attribute response") @parametrize @pytest.mark.respx(base_url=base_url) def test_raw_response_create_overload_1(self, client: Together, respx_mock: MockRouter) -> None: @@ -75,7 +72,6 @@ def test_raw_response_create_overload_1(self, client: Together, respx_mock: Mock assert audio.json() == {"foo": "bar"} assert isinstance(audio, BinaryAPIResponse) - @pytest.mark.skip(reason="AttributeError: BinaryAPIResponse object has no attribute response") @parametrize @pytest.mark.respx(base_url=base_url) def test_streaming_response_create_overload_1(self, client: Together, respx_mock: MockRouter) -> None: @@ -94,7 +90,6 @@ def test_streaming_response_create_overload_1(self, client: Together, respx_mock assert cast(Any, audio.is_closed) is True - @pytest.mark.skip(reason="AttributeError: BinaryAPIResponse object has no attribute response") @parametrize @pytest.mark.respx(base_url=base_url) def test_method_create_overload_2(self, client: Together, respx_mock: MockRouter) -> None: @@ -107,7 +102,6 @@ def test_method_create_overload_2(self, client: Together, respx_mock: MockRouter ) audio_stream.response.close() - @pytest.mark.skip(reason="AttributeError: BinaryAPIResponse object has no attribute response") @parametrize @pytest.mark.respx(base_url=base_url) def test_method_create_with_all_params_overload_2(self, client: Together, respx_mock: MockRouter) -> None: @@ -124,7 +118,6 @@ def test_method_create_with_all_params_overload_2(self, client: Together, respx_ ) audio_stream.response.close() - @pytest.mark.skip(reason="AttributeError: BinaryAPIResponse object has no attribute response") @parametrize @pytest.mark.respx(base_url=base_url) def test_raw_response_create_overload_2(self, client: Together, respx_mock: MockRouter) -> None: @@ -141,7 +134,6 @@ def test_raw_response_create_overload_2(self, client: Together, respx_mock: Mock assert audio_stream.json() == {"foo": "bar"} assert isinstance(audio_stream, BinaryAPIResponse) - @pytest.mark.skip(reason="AttributeError: BinaryAPIResponse object has no attribute response") @parametrize @pytest.mark.respx(base_url=base_url) def test_streaming_response_create_overload_2(self, client: Together, respx_mock: MockRouter) -> None: @@ -167,7 +159,6 @@ class TestAsyncAudio: "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] ) - @pytest.mark.skip(reason="AttributeError: BinaryAPIResponse object has no attribute response") @parametrize @pytest.mark.respx(base_url=base_url) async def test_method_create_overload_1(self, async_client: AsyncTogether, respx_mock: MockRouter) -> None: @@ -182,7 +173,6 @@ async def test_method_create_overload_1(self, async_client: AsyncTogether, respx assert cast(Any, audio.is_closed) is True assert isinstance(audio, AsyncBinaryAPIResponse) - @pytest.mark.skip(reason="AttributeError: BinaryAPIResponse object has no attribute response") @parametrize @pytest.mark.respx(base_url=base_url) async def test_method_create_with_all_params_overload_1( @@ -204,7 +194,6 @@ async def test_method_create_with_all_params_overload_1( assert cast(Any, audio.is_closed) is True assert isinstance(audio, AsyncBinaryAPIResponse) - @pytest.mark.skip(reason="AttributeError: BinaryAPIResponse object has no attribute response") @parametrize @pytest.mark.respx(base_url=base_url) async def test_raw_response_create_overload_1(self, async_client: AsyncTogether, respx_mock: MockRouter) -> None: @@ -221,7 +210,6 @@ async def test_raw_response_create_overload_1(self, async_client: AsyncTogether, assert await audio.json() == {"foo": "bar"} assert isinstance(audio, AsyncBinaryAPIResponse) - @pytest.mark.skip(reason="AttributeError: BinaryAPIResponse object has no attribute response") @parametrize @pytest.mark.respx(base_url=base_url) async def test_streaming_response_create_overload_1( @@ -242,7 +230,6 @@ async def test_streaming_response_create_overload_1( assert cast(Any, audio.is_closed) is True - @pytest.mark.skip(reason="AttributeError: BinaryAPIResponse object has no attribute response") @parametrize @pytest.mark.respx(base_url=base_url) async def test_method_create_overload_2(self, async_client: AsyncTogether, respx_mock: MockRouter) -> None: @@ -255,7 +242,6 @@ async def test_method_create_overload_2(self, async_client: AsyncTogether, respx ) await audio_stream.response.aclose() - @pytest.mark.skip(reason="AttributeError: BinaryAPIResponse object has no attribute response") @parametrize @pytest.mark.respx(base_url=base_url) async def test_method_create_with_all_params_overload_2( @@ -274,7 +260,6 @@ async def test_method_create_with_all_params_overload_2( ) await audio_stream.response.aclose() - @pytest.mark.skip(reason="AttributeError: BinaryAPIResponse object has no attribute response") @parametrize @pytest.mark.respx(base_url=base_url) async def test_raw_response_create_overload_2(self, async_client: AsyncTogether, respx_mock: MockRouter) -> None: @@ -291,7 +276,6 @@ async def test_raw_response_create_overload_2(self, async_client: AsyncTogether, assert await audio_stream.json() == {"foo": "bar"} assert isinstance(audio_stream, AsyncBinaryAPIResponse) - @pytest.mark.skip(reason="AttributeError: BinaryAPIResponse object has no attribute response") @parametrize @pytest.mark.respx(base_url=base_url) async def test_streaming_response_create_overload_2( From 452579b66624264783222cd9cdaa2c872b205407 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 23 Jun 2025 22:20:58 +0000 Subject: [PATCH 31/31] release: 0.1.0-alpha.14 --- .release-please-manifest.json | 2 +- CHANGELOG.md | 9 +++++++++ pyproject.toml | 2 +- src/together/_version.py | 2 +- 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 000572ec..b0699969 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.1.0-alpha.13" + ".": "0.1.0-alpha.14" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 78685d07..4c4ba72b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## 0.1.0-alpha.14 (2025-06-23) + +Full Changelog: [v0.1.0-alpha.13...v0.1.0-alpha.14](https://github.com/togethercomputer/together-py/compare/v0.1.0-alpha.13...v0.1.0-alpha.14) + +### Chores + +* **api:** re-enable audio unit tests ([02c8f9a](https://github.com/togethercomputer/together-py/commit/02c8f9ad850aed5aae7110ab37d33377aead1c47)) +* **tests:** skip some failing tests on the latest python versions ([49a71b3](https://github.com/togethercomputer/together-py/commit/49a71b3b35ffaef63bc8100faba69d87d517cedb)) + ## 0.1.0-alpha.13 (2025-06-20) Full Changelog: [v0.1.0-alpha.12...v0.1.0-alpha.13](https://github.com/togethercomputer/together-py/compare/v0.1.0-alpha.12...v0.1.0-alpha.13) diff --git a/pyproject.toml b/pyproject.toml index a4dd32f6..29a58db5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "together" -version = "0.1.0-alpha.13" +version = "0.1.0-alpha.14" description = "The official Python library for the together API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/together/_version.py b/src/together/_version.py index 444bb44a..81a996aa 100644 --- a/src/together/_version.py +++ b/src/together/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "together" -__version__ = "0.1.0-alpha.13" # x-release-please-version +__version__ = "0.1.0-alpha.14" # x-release-please-version