From eae629e457d4433974c1ccd75012684a962b3f42 Mon Sep 17 00:00:00 2001 From: Blaine Kasten Date: Fri, 20 Feb 2026 09:32:12 -0600 Subject: [PATCH 01/10] fix(cli): Improve output when downloading an incomplete finetune job (#273) --- .../lib/cli/api/fine_tuning/download.py | 27 +++++++++++++------ 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/src/together/lib/cli/api/fine_tuning/download.py b/src/together/lib/cli/api/fine_tuning/download.py index da4bc147..c5c57aea 100644 --- a/src/together/lib/cli/api/fine_tuning/download.py +++ b/src/together/lib/cli/api/fine_tuning/download.py @@ -7,8 +7,9 @@ import click -from together import NOT_GIVEN, NotGiven, Together +from together import NOT_GIVEN, APIError, NotGiven, Together, APIStatusError from together.lib import DownloadManager +from together.lib.cli.api._utils import handle_api_errors from together.types.finetune_response import TrainingTypeFullTrainingType, TrainingTypeLoRaTrainingType _FT_JOB_WITH_STEP_REGEX = r"^ft-[\dabcdef-]+:\d+$" @@ -40,6 +41,7 @@ default="merged", help="Specifies checkpoint type. 'merged' and 'adapter' options work only for LoRA jobs.", ) +@handle_api_errors("Fine-tuning") def download( ctx: click.Context, fine_tune_id: str, @@ -84,11 +86,20 @@ def download( if isinstance(output_dir, str): output = Path(output_dir) - file_path, file_size = DownloadManager(client).download( - url=url, - output=output, - remote_name=remote_name, - fetch_metadata=True, - ) + try: + file_path, file_size = DownloadManager(client).download( + url=url, + output=output, + remote_name=remote_name, + fetch_metadata=True, + ) - click.echo(json.dumps({"object": "local", "id": fine_tune_id, "filename": file_path, "size": file_size}, indent=4)) + click.echo( + json.dumps({"object": "local", "id": fine_tune_id, "filename": file_path, "size": file_size}, indent=4) + ) + except APIStatusError as e: + raise APIError( + "Training job is not downloadable. This may be because the job is not in a completed state.", + request=e.request, + body=None, + ) from e From a73f525550fa19268c1e222076e0618345ca3910 Mon Sep 17 00:00:00 2001 From: Blaine Kasten Date: Fri, 20 Feb 2026 10:34:32 -0600 Subject: [PATCH 02/10] chore(cli): Improve output for `fine-tuning list` and `files list` commands (#274) --- src/together/lib/cli/api/_utils.py | 37 ++++++++++-- src/together/lib/cli/api/files/list.py | 30 +++++++--- src/together/lib/cli/api/fine_tuning/list.py | 60 +++++++++++++------- src/together/lib/utils/tools.py | 14 ++++- uv.lock | 2 +- 5 files changed, 110 insertions(+), 33 deletions(-) diff --git a/src/together/lib/cli/api/_utils.py b/src/together/lib/cli/api/_utils.py index 9dc4f471..fedbf231 100644 --- a/src/together/lib/cli/api/_utils.py +++ b/src/together/lib/cli/api/_utils.py @@ -90,6 +90,38 @@ def _human_readable_time(timedelta: float) -> str: return " ".join(parts) if parts else "0s" +def generate_progress_text( + finetune_job: Union[Data, FinetuneResponse, _FinetuneResponse], current_time: datetime +) -> str: + """Generate a progress text for a finetune job. + Args: + finetune_job: The finetune job to generate a progress text for. + current_time: The current time. + Returns: + A string representing the progress text. + """ + time_text = "" + if getattr(finetune_job, "started_at", None) is not None and isinstance(finetune_job.started_at, datetime): + started_at = finetune_job.started_at.astimezone() + + if finetune_job.progress is not None: + if current_time < started_at: + return "" + + if not finetune_job.progress.estimate_available: + return "" + + if finetune_job.progress.seconds_remaining <= 0: + return "" + + elapsed_time = (current_time - started_at).total_seconds() + time_left = "N/A" + if finetune_job.progress.seconds_remaining > elapsed_time: + time_left = _human_readable_time(finetune_job.progress.seconds_remaining - elapsed_time) + time_text = f"{time_left} left" + return time_text + + def generate_progress_bar( finetune_job: Union[Data, FinetuneResponse, _FinetuneResponse], current_time: datetime, use_rich: bool = False ) -> str: @@ -122,10 +154,7 @@ def generate_progress_bar( percentage = ratio_filled * 100 filled = math.ceil(ratio_filled * _PROGRESS_BAR_WIDTH) bar = "█" * filled + "░" * (_PROGRESS_BAR_WIDTH - filled) - time_left = "N/A" - if finetune_job.progress.seconds_remaining > elapsed_time: - time_left = _human_readable_time(finetune_job.progress.seconds_remaining - elapsed_time) - time_text = f"{time_left} left" + time_text = generate_progress_text(finetune_job, current_time) progress = f"Progress: {bar} [bold]{percentage:>3.0f}%[/bold] [yellow]{time_text}[/yellow]" if use_rich: diff --git a/src/together/lib/cli/api/files/list.py b/src/together/lib/cli/api/files/list.py index 722749f1..d48910dd 100644 --- a/src/together/lib/cli/api/files/list.py +++ b/src/together/lib/cli/api/files/list.py @@ -1,33 +1,47 @@ from typing import Any, Dict, List -from textwrap import wrap +from datetime import datetime, timezone import click from tabulate import tabulate from together import Together from together.lib.utils import convert_bytes, convert_unix_timestamp +from together._utils._json import openapi_dumps +from together.lib.utils.tools import format_timestamp from together.lib.cli.api._utils import handle_api_errors @click.command() @click.pass_context +@click.option("--json", is_flag=True, help="Print output in JSON format") @handle_api_errors("Files") -def list(ctx: click.Context) -> None: +def list(ctx: click.Context, json: bool) -> None: """List files""" client: Together = ctx.obj response = client.files.list() + response.data = response.data or [] + + # Use a default datetime for None values to make sure the key function always returns a comparable value + # Sort newest to oldest + epoch_start = datetime.fromtimestamp(0, tz=timezone.utc) + response.data.sort(key=lambda x: x.created_at or epoch_start, reverse=True) + + if json: + click.echo(openapi_dumps(response.data)) + return + display_list: List[Dict[str, Any]] = [] - for i in response.data or []: + for i in response.data: display_list.append( { - "File name": "\n".join(wrap(i.filename or "", width=30)), - "File ID": i.id, - "Size": convert_bytes(float(str(i.bytes))), # convert to string for mypy typing - "Created At": convert_unix_timestamp(i.created_at or 0), + "ID": click.style(i.id, fg="blue"), + "File name": click.style(i.filename or "", fg="blue"), + "Size": click.style(convert_bytes(float(str(i.bytes))), fg="blue"), # convert to string for mypy typing + "Created At": click.style(format_timestamp(convert_unix_timestamp(i.created_at or 0)), fg="blue"), } ) - table = tabulate(display_list, headers="keys", tablefmt="grid", showindex=True) + table = tabulate(display_list, headers="keys") click.echo(table) diff --git a/src/together/lib/cli/api/fine_tuning/list.py b/src/together/lib/cli/api/fine_tuning/list.py index c1b5f96e..c11aa980 100644 --- a/src/together/lib/cli/api/fine_tuning/list.py +++ b/src/together/lib/cli/api/fine_tuning/list.py @@ -1,14 +1,14 @@ from typing import Any, Dict, List from datetime import datetime, timezone -from textwrap import wrap import click from tabulate import tabulate from together import Together from together.lib.utils import finetune_price_to_dollars -from together.lib.cli.api._utils import handle_api_errors, generate_progress_bar -from together.lib.utils.serializer import datetime_serializer +from together._utils._json import openapi_dumps +from together.lib.utils.tools import format_datetime +from together.lib.cli.api._utils import handle_api_errors, generate_progress_text @click.command() @@ -21,32 +21,54 @@ def list(ctx: click.Context, json: bool) -> None: response = client.fine_tuning.list() - if json: - from json import dumps - - click.echo(dumps(response.model_dump(exclude_none=True), indent=2, default=datetime_serializer)) - return - response.data = response.data or [] # Use a default datetime for None values to make sure the key function always returns a comparable value + # Sort newest to oldest epoch_start = datetime.fromtimestamp(0, tz=timezone.utc) - response.data.sort(key=lambda x: x.created_at or epoch_start) + response.data.sort(key=lambda x: x.created_at or epoch_start, reverse=True) + + if json: + click.echo(openapi_dumps(response.data)) + return display_list: List[Dict[str, Any]] = [] for i in response.data: + price = finetune_price_to_dollars(float(str(i.total_price))) # convert to string for mypy typing + + # Show the progress text if the job is running + status = str(i.status) # Convert to string for mypy typing + status_color = status_colors[i.status] if i.status in status_colors else "white" + if i.status == "running": + status += f": {generate_progress_text(i, datetime.now(timezone.utc))}" + display_list.append( { - "Fine-tune ID": i.id, - "Model Output Name": "\n".join(wrap(i.x_model_output_name or "", width=30)), - "Status": i.status, - "Created At": i.created_at, - "Price": f"""${ - finetune_price_to_dollars(float(str(i.total_price))) - }""", # convert to string for mypy typing - "Progress": generate_progress_bar(i, datetime.now().astimezone(), use_rich=False), + "ID": click.style(i.id, fg=status_color), + "Base Model": click.style(i.model or "", fg=status_color), + "Suffix": click.style(i.suffix or "", fg=status_color), + "Status": click.style(status, fg=status_color), + "Price": click.style(f"${price:,.2f}", fg=status_color), + "Created At": click.style(format_datetime(i.created_at), fg=status_color), } ) - table = tabulate(display_list, headers="keys", tablefmt="grid", showindex=True) + table = tabulate(display_list, headers="keys") click.echo(table) + + +status_colors = { + # Active status are yellow + "pending": "yellow", + "queued": "yellow", + "running": "yellow", + "compressing": "yellow", + "uploading": "yellow", + "cancel_requested": "yellow", + # Bad ending states are red + "cancelled": "red", + "error": "red", + "user_error": "red", + # good ending states are green + "completed": "green", +} diff --git a/src/together/lib/utils/tools.py b/src/together/lib/utils/tools.py index bb1e76b9..2c727d64 100644 --- a/src/together/lib/utils/tools.py +++ b/src/together/lib/utils/tools.py @@ -85,6 +85,18 @@ def format_timestamp(timestamp_str: str) -> str: """ try: timestamp = parse_timestamp(timestamp_str) - return timestamp.strftime("%m/%d/%Y, %I:%M %p") + return format_datetime(timestamp) except ValueError: return "" + + +def format_datetime(datetime_obj: datetime) -> str: + """Format datetime object to a readable date string. + + Args: + datetime_obj: A datetime object + + Returns: + str: Formatted timestamp string (MM/DD/YYYY, HH:MM AM/PM) + """ + return datetime_obj.strftime("%m/%d/%Y, %I:%M %p") diff --git a/uv.lock b/uv.lock index 50042913..9c9bbc83 100644 --- a/uv.lock +++ b/uv.lock @@ -2040,7 +2040,7 @@ wheels = [ [[package]] name = "together" -version = "2.1.1" +version = "2.2.0" source = { editable = "." } dependencies = [ { name = "anyio" }, From bae00658a55f112e5f5d9a548b379129ed1d5f2c Mon Sep 17 00:00:00 2001 From: Blaine Kasten Date: Fri, 20 Feb 2026 10:44:00 -0600 Subject: [PATCH 03/10] fix(cli): Improve error output message when model/checkpoint is not provided in finetune create (#271) --- src/together/lib/cli/api/fine_tuning/create.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/together/lib/cli/api/fine_tuning/create.py b/src/together/lib/cli/api/fine_tuning/create.py index 4ec8804b..a83a06c3 100644 --- a/src/together/lib/cli/api/fine_tuning/create.py +++ b/src/together/lib/cli/api/fine_tuning/create.py @@ -285,7 +285,10 @@ def create( ) if model is None and from_checkpoint is None: - raise click.BadParameter("You must specify either a model or a checkpoint") + raise click.MissingParameter( + "", + param_type="option --model or --from-checkpoint", + ) model_name = model if from_checkpoint is not None: From b9bb6e0188373ee882781d0259327985103abcfc Mon Sep 17 00:00:00 2001 From: Blaine Kasten Date: Sat, 21 Feb 2026 13:36:30 -0600 Subject: [PATCH 04/10] feat(cli): Add --json to `fine-tuning retrieve` (#272) --- src/together/lib/cli/api/fine_tuning/retrieve.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/together/lib/cli/api/fine_tuning/retrieve.py b/src/together/lib/cli/api/fine_tuning/retrieve.py index 3625a7f8..5ce8d0a2 100644 --- a/src/together/lib/cli/api/fine_tuning/retrieve.py +++ b/src/together/lib/cli/api/fine_tuning/retrieve.py @@ -5,6 +5,7 @@ from rich.json import JSON from together import Together +from together._utils._json import openapi_dumps from together.lib.cli.api._utils import handle_api_errors, generate_progress_bar from together.lib.utils.serializer import datetime_serializer @@ -12,13 +13,18 @@ @click.command() @click.pass_context @click.argument("fine_tune_id", type=str, required=True) +@click.option("--json", is_flag=True, help="Output the response in JSON format") @handle_api_errors("Fine-tuning") -def retrieve(ctx: click.Context, fine_tune_id: str) -> None: +def retrieve(ctx: click.Context, fine_tune_id: str, json: bool) -> None: """Retrieve fine-tuning job details""" client: Together = ctx.obj response = client.fine_tuning.retrieve(fine_tune_id) + if json: + click.echo(openapi_dumps(response.model_dump(exclude_none=True))) + return + # remove events from response for cleaner output response.events = None From 089d4b9705b3f01fc1de67c395ada62dd69c21b7 Mon Sep 17 00:00:00 2001 From: Blaine Kasten Date: Mon, 23 Feb 2026 06:58:17 -0600 Subject: [PATCH 05/10] chore(cli): Improve output for file uploads and fine-tuning create (#277) --- src/together/lib/cli/api/files/upload.py | 18 ++++++-- .../lib/cli/api/fine_tuning/create.py | 44 ++++++++++--------- 2 files changed, 38 insertions(+), 24 deletions(-) diff --git a/src/together/lib/cli/api/files/upload.py b/src/together/lib/cli/api/files/upload.py index 0781f058..1a79560e 100644 --- a/src/together/lib/cli/api/files/upload.py +++ b/src/together/lib/cli/api/files/upload.py @@ -1,4 +1,3 @@ -import json import pathlib from typing import get_args @@ -6,6 +5,7 @@ from together import Together from together.types import FilePurpose +from together._utils._json import openapi_dumps from together.lib.cli.api._utils import handle_api_errors @@ -27,12 +27,24 @@ default=True, help="Whether to check the file before uploading.", ) +@click.option( + "--json", + is_flag=True, + help="Output the response in JSON format", +) @handle_api_errors("Files") -def upload(ctx: click.Context, file: pathlib.Path, purpose: FilePurpose, check: bool) -> None: +def upload(ctx: click.Context, file: pathlib.Path, purpose: FilePurpose, check: bool, json: bool) -> None: """Upload file""" client: Together = ctx.obj response = client.files.upload(file=file, purpose=purpose, check=check) - click.echo(json.dumps(response.model_dump(exclude_none=True), indent=4)) + if json: + click.echo(openapi_dumps(response.model_dump(exclude_none=True))) + return + + click.echo( + click.style("> Success! ", fg="blue") + + f"File uploaded for {click.style(response.purpose, bold=True)}. File ID: {click.style(response.id, fg='green', bold=True)}" + ) diff --git a/src/together/lib/cli/api/fine_tuning/create.py b/src/together/lib/cli/api/fine_tuning/create.py index a83a06c3..d3e665f1 100644 --- a/src/together/lib/cli/api/fine_tuning/create.py +++ b/src/together/lib/cli/api/fine_tuning/create.py @@ -4,7 +4,6 @@ from pathlib import Path import click -from rich import print as rprint from click.core import ParameterSource from together import Together @@ -13,19 +12,21 @@ from together.lib.cli.api._utils import INT_WITH_MAX, BOOL_WITH_AUTO, handle_api_errors from together.lib.resources.fine_tuning import get_model_limits -_CONFIRMATION_MESSAGE = ( - "You are about to create a fine-tuning job. " - "The estimated price of this job is {price}. " - "The actual cost of your job will be determined by the model size, the number of tokens " - "in the training file, the number of tokens in the validation file, the number of epochs, and " - "the number of evaluations. Visit https://www.together.ai/pricing to learn more about pricing.\n" - "{warning}" - "You can pass `-y` or `--confirm` to your command to skip this message.\n\n" - "Do you want to proceed?" -) + +def get_confirmation_message(price: str, warning: str) -> str: + return ( + "\nYou are about to create a fine-tuning job. The estimated price of this job is " + + f"{click.style(f'{price}', fg='blue', bold=True)}\n\n" + + "The actual cost of your job will be determined by the model size, the number of tokens" + + "in the training file, the number of tokens in the validation file, the number of epochs, and " + + "the number of evaluations. Visit https://www.together.ai/pricing to learn more about pricing.\n" + + warning + + "\nDo you want to proceed?" + ) + _WARNING_MESSAGE_INSUFFICIENT_FUNDS = ( - "The estimated price of this job is significantly greater than your current credit limit and balance combined. " + "\nThe estimated price of this job is significantly greater than your current credit limit and balance combined. " "It will likely get cancelled due to insufficient funds. " "Consider increasing your credit limit at https://api.together.xyz/settings/profile\n" ) @@ -346,9 +347,11 @@ def create( training_method_cls: pe_params.TrainingMethod if training_method == "sft": + train_on_inputs = train_on_inputs or "auto" + training_args["train_on_inputs"] = train_on_inputs training_method_cls = pe_params.TrainingMethodTrainingMethodSft( method="sft", - train_on_inputs=train_on_inputs or "auto", + train_on_inputs=train_on_inputs, ) else: training_method_cls = pe_params.TrainingMethodTrainingMethodDpo( @@ -399,7 +402,7 @@ def create( else: warning = "" - confirmation_message = _CONFIRMATION_MESSAGE.format( + confirmation_message = get_confirmation_message( price=price, warning=warning, ) @@ -410,13 +413,12 @@ def create( verbose=True, ) - report_string = f"Successfully submitted a fine-tuning job {response.id}" - # created_at reports UTC time, we use .astimezone() to convert to local time - formatted_time = response.created_at.astimezone().strftime("%m/%d/%Y, %H:%M:%S") - report_string += f" at {formatted_time}" - rprint(report_string) - else: - click.echo("No confirmation received, stopping job launch") + click.echo( + click.style(f"\n\nSuccess!", fg="green", bold=True) + + click.style(" Your fine-tuning job ", fg="green") + + click.style(response.id, fg="blue", bold=True) + + click.style(" has been submitted.", fg="green") + ) def _check_path_exists(path_string: str) -> bool: From 174bf4dd0c704ad2744d294ae6cb5ed4c9099bee Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 13:34:26 +0000 Subject: [PATCH 06/10] chore(internal): add request options to SSE classes --- src/together/_response.py | 3 +++ src/together/_streaming.py | 11 ++++++++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/together/_response.py b/src/together/_response.py index 7188c3e4..ace8625a 100644 --- a/src/together/_response.py +++ b/src/together/_response.py @@ -152,6 +152,7 @@ def _parse(self, *, to: type[_T] | None = None) -> R | _T: ), response=self.http_response, client=cast(Any, self._client), + options=self._options, ), ) @@ -162,6 +163,7 @@ def _parse(self, *, to: type[_T] | None = None) -> R | _T: cast_to=extract_stream_chunk_type(self._stream_cls), response=self.http_response, client=cast(Any, self._client), + options=self._options, ), ) @@ -175,6 +177,7 @@ def _parse(self, *, to: type[_T] | None = None) -> R | _T: cast_to=cast_to, response=self.http_response, client=cast(Any, self._client), + options=self._options, ), ) diff --git a/src/together/_streaming.py b/src/together/_streaming.py index 191f29af..3a8edbef 100644 --- a/src/together/_streaming.py +++ b/src/together/_streaming.py @@ -4,7 +4,7 @@ import json import inspect from types import TracebackType -from typing import TYPE_CHECKING, Any, Generic, TypeVar, Iterator, AsyncIterator, cast +from typing import TYPE_CHECKING, Any, Generic, TypeVar, Iterator, Optional, AsyncIterator, cast from typing_extensions import Self, Protocol, TypeGuard, override, get_origin, runtime_checkable import httpx @@ -14,6 +14,7 @@ if TYPE_CHECKING: from ._client import Together, AsyncTogether + from ._models import FinalRequestOptions _T = TypeVar("_T") @@ -23,7 +24,7 @@ class Stream(Generic[_T]): """Provides the core interface to iterate over a synchronous stream response.""" response: httpx.Response - + _options: Optional[FinalRequestOptions] = None _decoder: SSEBytesDecoder def __init__( @@ -32,10 +33,12 @@ def __init__( cast_to: type[_T], response: httpx.Response, client: Together, + options: Optional[FinalRequestOptions] = None, ) -> None: self.response = response self._cast_to = cast_to self._client = client + self._options = options self._decoder = client._make_sse_decoder() self._iterator = self.__stream__() @@ -105,7 +108,7 @@ class AsyncStream(Generic[_T]): """Provides the core interface to iterate over an asynchronous stream response.""" response: httpx.Response - + _options: Optional[FinalRequestOptions] = None _decoder: SSEDecoder | SSEBytesDecoder def __init__( @@ -114,10 +117,12 @@ def __init__( cast_to: type[_T], response: httpx.Response, client: AsyncTogether, + options: Optional[FinalRequestOptions] = None, ) -> None: self.response = response self._cast_to = cast_to self._client = client + self._options = options self._decoder = client._make_sse_decoder() self._iterator = self.__stream__() From 5d5aa42fb9c8d9d53f6e13fe5135b4ac4f458c3d Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 19:19:42 +0000 Subject: [PATCH 07/10] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 64b08e45..fc28ae7d 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 74 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/togetherai%2Ftogetherai-da5b9df3bfe0d31a76c91444c9eba060ad607d7d5a4e7483c5cc3fe2cac0f25e.yml -openapi_spec_hash: 7efd2ae2111f3a9bf190485828a13252 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/togetherai%2Ftogetherai-41003d5ea35fdc4ec5c03cd7933dc5fee020d818abb1ea71f1d20d767cce06a0.yml +openapi_spec_hash: 651ade27191eb5ad232ec508418fe4cb config_hash: b66198d27b4d5c152688ff6cccfdeab5 From eb89afdd88624b69df0cf27a8aeff85ba2071812 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 20:30:59 +0000 Subject: [PATCH 08/10] chore(internal): make `test_proxy_environment_variables` more resilient --- tests/test_client.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/test_client.py b/tests/test_client.py index 011fef4b..88a1a55d 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -1001,6 +1001,8 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: 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") + # Delete in case our environment has this set + monkeypatch.delenv("HTTP_PROXY", raising=False) client = DefaultHttpxClient() @@ -1960,6 +1962,8 @@ async def test_get_platform(self) -> None: 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") + # Delete in case our environment has this set + monkeypatch.delenv("HTTP_PROXY", raising=False) client = DefaultAsyncHttpxClient() From 0bf71ae8af070a04d6257e7fbe5c10faccd18f13 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 24 Feb 2026 17:47:59 +0000 Subject: [PATCH 09/10] chore(internal): make `test_proxy_environment_variables` more resilient to env --- tests/test_client.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/tests/test_client.py b/tests/test_client.py index 88a1a55d..443fce74 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -1001,8 +1001,14 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: 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") - # Delete in case our environment has this set + # Delete in case our environment has any proxy env vars set monkeypatch.delenv("HTTP_PROXY", raising=False) + monkeypatch.delenv("ALL_PROXY", raising=False) + monkeypatch.delenv("NO_PROXY", raising=False) + monkeypatch.delenv("http_proxy", raising=False) + monkeypatch.delenv("https_proxy", raising=False) + monkeypatch.delenv("all_proxy", raising=False) + monkeypatch.delenv("no_proxy", raising=False) client = DefaultHttpxClient() @@ -1962,8 +1968,14 @@ async def test_get_platform(self) -> None: 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") - # Delete in case our environment has this set + # Delete in case our environment has any proxy env vars set monkeypatch.delenv("HTTP_PROXY", raising=False) + monkeypatch.delenv("ALL_PROXY", raising=False) + monkeypatch.delenv("NO_PROXY", raising=False) + monkeypatch.delenv("http_proxy", raising=False) + monkeypatch.delenv("https_proxy", raising=False) + monkeypatch.delenv("all_proxy", raising=False) + monkeypatch.delenv("no_proxy", raising=False) client = DefaultAsyncHttpxClient() From 015e56782caa9d600931de158a8418008f07656f Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 24 Feb 2026 17:48:22 +0000 Subject: [PATCH 10/10] release: 2.3.0 --- .release-please-manifest.json | 2 +- CHANGELOG.md | 23 +++++++++++++++++++++++ pyproject.toml | 2 +- src/together/_version.py | 2 +- 4 files changed, 26 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index bfc26f9c..75ec52fc 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "2.2.0" + ".": "2.3.0" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 463657d3..25d12e50 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,28 @@ # Changelog +## 2.3.0 (2026-02-24) + +Full Changelog: [v2.2.0...v2.3.0](https://github.com/togethercomputer/together-py/compare/v2.2.0...v2.3.0) + +### Features + +* **cli:** Add --json to `fine-tuning retrieve` ([#272](https://github.com/togethercomputer/together-py/issues/272)) ([b9bb6e0](https://github.com/togethercomputer/together-py/commit/b9bb6e0188373ee882781d0259327985103abcfc)) + + +### Bug Fixes + +* **cli:** Improve error output message when model/checkpoint is not provided in finetune create ([#271](https://github.com/togethercomputer/together-py/issues/271)) ([bae0065](https://github.com/togethercomputer/together-py/commit/bae00658a55f112e5f5d9a548b379129ed1d5f2c)) +* **cli:** Improve output when downloading an incomplete finetune job ([#273](https://github.com/togethercomputer/together-py/issues/273)) ([eae629e](https://github.com/togethercomputer/together-py/commit/eae629e457d4433974c1ccd75012684a962b3f42)) + + +### Chores + +* **cli:** Improve output for `fine-tuning list` and `files list` commands ([#274](https://github.com/togethercomputer/together-py/issues/274)) ([a73f525](https://github.com/togethercomputer/together-py/commit/a73f525550fa19268c1e222076e0618345ca3910)) +* **cli:** Improve output for file uploads and fine-tuning create ([#277](https://github.com/togethercomputer/together-py/issues/277)) ([089d4b9](https://github.com/togethercomputer/together-py/commit/089d4b9705b3f01fc1de67c395ada62dd69c21b7)) +* **internal:** add request options to SSE classes ([174bf4d](https://github.com/togethercomputer/together-py/commit/174bf4dd0c704ad2744d294ae6cb5ed4c9099bee)) +* **internal:** make `test_proxy_environment_variables` more resilient ([eb89afd](https://github.com/togethercomputer/together-py/commit/eb89afdd88624b69df0cf27a8aeff85ba2071812)) +* **internal:** make `test_proxy_environment_variables` more resilient to env ([0bf71ae](https://github.com/togethercomputer/together-py/commit/0bf71ae8af070a04d6257e7fbe5c10faccd18f13)) + ## 2.2.0 (2026-02-19) Full Changelog: [v2.1.1...v2.2.0](https://github.com/togethercomputer/together-py/compare/v2.1.1...v2.2.0) diff --git a/pyproject.toml b/pyproject.toml index a8ffb9e2..a674be27 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "together" -version = "2.2.0" +version = "2.3.0" 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 636c0c9b..fa8c178b 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__ = "2.2.0" # x-release-please-version +__version__ = "2.3.0" # x-release-please-version