Skip to content

Commit 3360266

Browse files
authored
Merge branch 'main' into timkpaine-patch-2
2 parents d03930b + 137b84e commit 3360266

19 files changed

Lines changed: 1103 additions & 51 deletions

README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,34 @@
44
</a>
55
</div>
66

7+
> [!NOTE]
8+
> ## 🚀 Together Python SDK 2.0 is now available!
9+
>
10+
> Check out the new SDK: **[together-py](https://github.com/togethercomputer/together-py)**
11+
>
12+
> 📖 **Migration Guide:** [https://docs.together.ai/docs/pythonv2-migration-guide](https://docs.together.ai/docs/pythonv2-migration-guide)
13+
>
14+
> ### Install the Beta
15+
>
16+
> **Using uv (Recommended):**
17+
> ```bash
18+
> # Install uv if you haven't already
19+
> curl -LsSf https://astral.sh/uv/install.sh | sh
20+
>
21+
> # Install together python SDK
22+
> uv add together --prerelease allow
23+
>
24+
> # Or upgrade an existing installation
25+
> uv sync --upgrade-package together --prerelease allow
26+
> ```
27+
>
28+
> **Using pip:**
29+
> ```bash
30+
> pip install --pre together
31+
> ```
32+
>
33+
> This package will be maintained until January 2026.
34+
735
# Together Python API library
836
937
[![PyPI version](https://img.shields.io/pypi/v/together.svg)](https://pypi.org/project/together/)

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@ build-backend = "poetry.masonry.api"
1212

1313
[tool.poetry]
1414
name = "together"
15-
version = "1.5.30"
15+
version = "1.5.33"
1616
authors = ["Together AI <support@together.ai>"]
17-
description = "Python client for Together's Cloud Platform!"
17+
description = "Python client for Together's Cloud Platform! Note: SDK 2.0 is now available at https://github.com/togethercomputer/together-py"
1818
readme = "README.md"
1919
license = "Apache-2.0"
2020
classifiers = [

src/together/__init__.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,56 @@
11
from __future__ import annotations
22

3+
import os
4+
import sys
5+
6+
# =============================================================================
7+
# SDK 2.0 ANNOUNCEMENT
8+
# =============================================================================
9+
_ANNOUNCEMENT_MESSAGE = """
10+
================================================================================
11+
Together Python SDK 2.0 is now available!
12+
13+
Install: pip install --pre together
14+
New SDK: https://github.com/togethercomputer/together-py
15+
Migration guide: https://docs.together.ai/docs/pythonv2-migration-guide
16+
17+
This package will be maintained until January 2026.
18+
================================================================================
19+
"""
20+
21+
# Show info banner (unless suppressed)
22+
if not os.environ.get("TOGETHER_NO_BANNER"):
23+
try:
24+
from rich.console import Console
25+
from rich.panel import Panel
26+
27+
console = Console(stderr=True)
28+
console.print(
29+
Panel(
30+
"[bold cyan]Together Python SDK 2.0 is now available![/bold cyan]\n\n"
31+
"Install the beta:\n"
32+
"[green]pip install --pre together[/green] or "
33+
"[green]uv add together --prerelease allow[/green]\n\n"
34+
"New SDK: [link=https://github.com/togethercomputer/together-py]"
35+
"https://github.com/togethercomputer/together-py[/link]\n"
36+
"Migration guide: [link=https://docs.together.ai/docs/pythonv2-migration-guide]"
37+
"https://docs.together.ai/docs/pythonv2-migration-guide[/link]\n\n"
38+
"[dim]This package will be maintained until January 2026.\n"
39+
"Set TOGETHER_NO_BANNER=1 to hide this message.[/dim]",
40+
title="🚀 New SDK Available",
41+
border_style="cyan",
42+
)
43+
)
44+
except Exception:
45+
# Fallback for any error (ImportError, OSError in daemons, rich errors, etc.)
46+
# Banner display should never break module imports
47+
try:
48+
print(_ANNOUNCEMENT_MESSAGE, file=sys.stderr)
49+
except Exception:
50+
pass # Silently ignore if even stderr is unavailable
51+
52+
# =============================================================================
53+
354
from contextvars import ContextVar
455
from typing import TYPE_CHECKING, Callable
556

src/together/cli/api/chat.py

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import cmd
44
import json
5-
from typing import List, Tuple
5+
from typing import Any, Dict, List, Tuple
66

77
import click
88

@@ -181,6 +181,12 @@ def interactive(
181181
"--frequency-penalty", type=float, help="Frequency penalty sampling method"
182182
)
183183
@click.option("--min-p", type=float, help="Min p sampling")
184+
@click.option(
185+
"--audio-url",
186+
type=str,
187+
multiple=True,
188+
help="Audio URL to attach to the last user message",
189+
)
184190
@click.option("--no-stream", is_flag=True, help="Disable streaming")
185191
@click.option("--logprobs", type=int, help="Return logprobs. Only works with --raw.")
186192
@click.option("--echo", is_flag=True, help="Echo prompt. Only works with --raw.")
@@ -200,6 +206,7 @@ def chat(
200206
presence_penalty: float | None = None,
201207
frequency_penalty: float | None = None,
202208
min_p: float | None = None,
209+
audio_url: List[str] | None = None,
203210
no_stream: bool = False,
204211
logprobs: int | None = None,
205212
echo: bool | None = None,
@@ -210,7 +217,22 @@ def chat(
210217
"""Generate chat completions from messages"""
211218
client: Together = ctx.obj
212219

213-
messages = [{"role": msg[0], "content": msg[1]} for msg in message]
220+
messages: List[Dict[str, Any]] = [
221+
{"role": msg[0], "content": msg[1]} for msg in message
222+
]
223+
224+
if audio_url and messages:
225+
last_msg = messages[-1]
226+
if last_msg["role"] == "user":
227+
# Convert content to list if it is string
228+
if isinstance(last_msg["content"], str):
229+
last_msg["content"] = [{"type": "text", "text": last_msg["content"]}]
230+
231+
# Append audio URLs
232+
for url in audio_url:
233+
last_msg["content"].append(
234+
{"type": "audio_url", "audio_url": {"url": url}}
235+
)
214236

215237
response = client.chat.completions.create(
216238
model=model,

src/together/cli/api/endpoints.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -137,8 +137,7 @@ def endpoints(ctx: click.Context) -> None:
137137
help="Start endpoint in specified availability zone (e.g., us-central-4b)",
138138
)
139139
@click.option(
140-
"--wait",
141-
is_flag=True,
140+
"--wait/--no-wait",
142141
default=True,
143142
help="Wait for the endpoint to be ready after creation",
144143
)
@@ -284,7 +283,9 @@ def fetch_and_print_hardware_options(
284283
@endpoints.command()
285284
@click.argument("endpoint-id", required=True)
286285
@click.option(
287-
"--wait", is_flag=True, default=True, help="Wait for the endpoint to stop"
286+
"--wait/--no-wait",
287+
default=True,
288+
help="Wait for the endpoint to stop",
288289
)
289290
@click.pass_obj
290291
@handle_api_errors
@@ -307,7 +308,9 @@ def stop(client: Together, endpoint_id: str, wait: bool) -> None:
307308
@endpoints.command()
308309
@click.argument("endpoint-id", required=True)
309310
@click.option(
310-
"--wait", is_flag=True, default=True, help="Wait for the endpoint to start"
311+
"--wait/--no-wait",
312+
default=True,
313+
help="Wait for the endpoint to start",
311314
)
312315
@click.pass_obj
313316
@handle_api_errors

src/together/cli/api/finetune.py

Lines changed: 52 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,17 @@
99
import click
1010
from click.core import ParameterSource # type: ignore[attr-defined]
1111
from rich import print as rprint
12+
from rich.json import JSON
1213
from tabulate import tabulate
1314

1415
from together import Together
15-
from together.cli.api.utils import BOOL_WITH_AUTO, INT_WITH_MAX
16+
from together.cli.api.utils import BOOL_WITH_AUTO, INT_WITH_MAX, generate_progress_bar
1617
from together.types.finetune import (
1718
DownloadCheckpointType,
1819
FinetuneEventType,
1920
FinetuneTrainingLimits,
21+
FullTrainingType,
22+
LoRATrainingType,
2023
)
2124
from together.utils import (
2225
finetune_price_to_dollars,
@@ -29,13 +32,21 @@
2932

3033
_CONFIRMATION_MESSAGE = (
3134
"You are about to create a fine-tuning job. "
32-
"The cost of your job will be determined by the model size, the number of tokens "
35+
"The estimated price of this job is {price}. "
36+
"The actual cost of your job will be determined by the model size, the number of tokens "
3337
"in the training file, the number of tokens in the validation file, the number of epochs, and "
34-
"the number of evaluations. Visit https://www.together.ai/pricing to get a price estimate.\n"
38+
"the number of evaluations. Visit https://www.together.ai/pricing to learn more about fine-tuning pricing.\n"
39+
"{warning}"
3540
"You can pass `-y` or `--confirm` to your command to skip this message.\n\n"
3641
"Do you want to proceed?"
3742
)
3843

44+
_WARNING_MESSAGE_INSUFFICIENT_FUNDS = (
45+
"The estimated price of this job is significantly greater than your current credit limit and balance combined. "
46+
"It will likely get cancelled due to insufficient funds. "
47+
"Consider increasing your credit limit at https://api.together.xyz/settings/profile\n"
48+
)
49+
3950

4051
class DownloadCheckpointTypeChoice(click.Choice):
4152
def __init__(self) -> None:
@@ -357,12 +368,36 @@ def create(
357368
"You have specified a number of evaluation loops but no validation file."
358369
)
359370

360-
if confirm or click.confirm(_CONFIRMATION_MESSAGE, default=True, show_default=True):
371+
finetune_price_estimation_result = client.fine_tuning.estimate_price(
372+
training_file=training_file,
373+
validation_file=validation_file,
374+
model=model,
375+
n_epochs=n_epochs,
376+
n_evals=n_evals,
377+
training_type="lora" if lora else "full",
378+
training_method=training_method,
379+
)
380+
381+
price = click.style(
382+
f"${finetune_price_estimation_result.estimated_total_price:.2f}",
383+
bold=True,
384+
)
385+
386+
if not finetune_price_estimation_result.allowed_to_proceed:
387+
warning = click.style(_WARNING_MESSAGE_INSUFFICIENT_FUNDS, fg="red", bold=True)
388+
else:
389+
warning = ""
390+
391+
confirmation_message = _CONFIRMATION_MESSAGE.format(
392+
price=price,
393+
warning=warning,
394+
)
395+
396+
if confirm or click.confirm(confirmation_message, default=True, show_default=True):
361397
response = client.fine_tuning.create(
362398
**training_args,
363399
verbose=True,
364400
)
365-
366401
report_string = f"Successfully submitted a fine-tuning job {response.id}"
367402
if response.created_at is not None:
368403
created_time = datetime.strptime(
@@ -401,6 +436,9 @@ def list(ctx: click.Context) -> None:
401436
"Price": f"""${
402437
finetune_price_to_dollars(float(str(i.total_price)))
403438
}""", # convert to string for mypy typing
439+
"Progress": generate_progress_bar(
440+
i, datetime.now().astimezone(), use_rich=False
441+
),
404442
}
405443
)
406444
table = tabulate(display_list, headers="keys", tablefmt="grid", showindex=True)
@@ -420,7 +458,15 @@ def retrieve(ctx: click.Context, fine_tune_id: str) -> None:
420458
# remove events from response for cleaner output
421459
response.events = None
422460

423-
click.echo(json.dumps(response.model_dump(exclude_none=True), indent=4))
461+
rprint(JSON.from_data(response.model_dump(exclude_none=True)))
462+
progress_text = generate_progress_bar(
463+
response, datetime.now().astimezone(), use_rich=True
464+
)
465+
status = "Unknown"
466+
if response.status is not None:
467+
status = response.status.value
468+
prefix = f"Status: [bold]{status}[/bold],"
469+
rprint(f"{prefix} {progress_text}")
424470

425471

426472
@fine_tuning.command()

0 commit comments

Comments
 (0)