together 1.5.30__py3-none-any.whl → 1.5.32__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
together/__init__.py CHANGED
@@ -1,5 +1,56 @@
1
1
  from __future__ import annotations
2
2
 
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
+
3
54
  from contextvars import ContextVar
4
55
  from typing import TYPE_CHECKING, Callable
5
56
 
together/cli/api/chat.py CHANGED
@@ -2,7 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import cmd
4
4
  import json
5
- from typing import List, Tuple
5
+ from typing import Any, Dict, List, Tuple
6
6
 
7
7
  import click
8
8
 
@@ -181,6 +181,12 @@ def interactive(
181
181
  "--frequency-penalty", type=float, help="Frequency penalty sampling method"
182
182
  )
183
183
  @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
+ )
184
190
  @click.option("--no-stream", is_flag=True, help="Disable streaming")
185
191
  @click.option("--logprobs", type=int, help="Return logprobs. Only works with --raw.")
186
192
  @click.option("--echo", is_flag=True, help="Echo prompt. Only works with --raw.")
@@ -200,6 +206,7 @@ def chat(
200
206
  presence_penalty: float | None = None,
201
207
  frequency_penalty: float | None = None,
202
208
  min_p: float | None = None,
209
+ audio_url: List[str] | None = None,
203
210
  no_stream: bool = False,
204
211
  logprobs: int | None = None,
205
212
  echo: bool | None = None,
@@ -210,7 +217,22 @@ def chat(
210
217
  """Generate chat completions from messages"""
211
218
  client: Together = ctx.obj
212
219
 
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
+ )
214
236
 
215
237
  response = client.chat.completions.create(
216
238
  model=model,
@@ -137,8 +137,7 @@ def endpoints(ctx: click.Context) -> None:
137
137
  help="Start endpoint in specified availability zone (e.g., us-central-4b)",
138
138
  )
139
139
  @click.option(
140
- "--wait",
141
- is_flag=True,
140
+ "--wait/--no-wait",
142
141
  default=True,
143
142
  help="Wait for the endpoint to be ready after creation",
144
143
  )
@@ -284,7 +283,9 @@ def fetch_and_print_hardware_options(
284
283
  @endpoints.command()
285
284
  @click.argument("endpoint-id", required=True)
286
285
  @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",
288
289
  )
289
290
  @click.pass_obj
290
291
  @handle_api_errors
@@ -307,7 +308,9 @@ def stop(client: Together, endpoint_id: str, wait: bool) -> None:
307
308
  @endpoints.command()
308
309
  @click.argument("endpoint-id", required=True)
309
310
  @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",
311
314
  )
312
315
  @click.pass_obj
313
316
  @handle_api_errors
@@ -17,6 +17,8 @@ from together.types.finetune import (
17
17
  DownloadCheckpointType,
18
18
  FinetuneEventType,
19
19
  FinetuneTrainingLimits,
20
+ FullTrainingType,
21
+ LoRATrainingType,
20
22
  )
21
23
  from together.utils import (
22
24
  finetune_price_to_dollars,
@@ -29,13 +31,21 @@ from together.utils import (
29
31
 
30
32
  _CONFIRMATION_MESSAGE = (
31
33
  "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 "
34
+ "The estimated price of this job is {price}. "
35
+ "The actual cost of your job will be determined by the model size, the number of tokens "
33
36
  "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"
37
+ "the number of evaluations. Visit https://www.together.ai/pricing to learn more about fine-tuning pricing.\n"
38
+ "{warning}"
35
39
  "You can pass `-y` or `--confirm` to your command to skip this message.\n\n"
36
40
  "Do you want to proceed?"
37
41
  )
38
42
 
43
+ _WARNING_MESSAGE_INSUFFICIENT_FUNDS = (
44
+ "The estimated price of this job is significantly greater than your current credit limit and balance combined. "
45
+ "It will likely get cancelled due to insufficient funds. "
46
+ "Consider increasing your credit limit at https://api.together.xyz/settings/profile\n"
47
+ )
48
+
39
49
 
40
50
  class DownloadCheckpointTypeChoice(click.Choice):
41
51
  def __init__(self) -> None:
@@ -357,12 +367,36 @@ def create(
357
367
  "You have specified a number of evaluation loops but no validation file."
358
368
  )
359
369
 
360
- if confirm or click.confirm(_CONFIRMATION_MESSAGE, default=True, show_default=True):
370
+ finetune_price_estimation_result = client.fine_tuning.estimate_price(
371
+ training_file=training_file,
372
+ validation_file=validation_file,
373
+ model=model,
374
+ n_epochs=n_epochs,
375
+ n_evals=n_evals,
376
+ training_type="lora" if lora else "full",
377
+ training_method=training_method,
378
+ )
379
+
380
+ price = click.style(
381
+ f"${finetune_price_estimation_result.estimated_total_price:.2f}",
382
+ bold=True,
383
+ )
384
+
385
+ if not finetune_price_estimation_result.allowed_to_proceed:
386
+ warning = click.style(_WARNING_MESSAGE_INSUFFICIENT_FUNDS, fg="red", bold=True)
387
+ else:
388
+ warning = ""
389
+
390
+ confirmation_message = _CONFIRMATION_MESSAGE.format(
391
+ price=price,
392
+ warning=warning,
393
+ )
394
+
395
+ if confirm or click.confirm(confirmation_message, default=True, show_default=True):
361
396
  response = client.fine_tuning.create(
362
397
  **training_args,
363
398
  verbose=True,
364
399
  )
365
-
366
400
  report_string = f"Successfully submitted a fine-tuning job {response.id}"
367
401
  if response.created_at is not None:
368
402
  created_time = datetime.strptime(
together/constants.py CHANGED
@@ -20,13 +20,13 @@ MAX_CONCURRENT_PARTS = 4 # Maximum concurrent parts for multipart upload
20
20
 
21
21
  # Multipart upload constants
22
22
  MIN_PART_SIZE_MB = 5 # Minimum part size (S3 requirement)
23
- TARGET_PART_SIZE_MB = 100 # Target part size for optimal performance
24
- MAX_MULTIPART_PARTS = 250 # Maximum parts per upload (S3 limit)
23
+ TARGET_PART_SIZE_MB = 250 # Target part size
24
+ MAX_MULTIPART_PARTS = 250 # Maximum parts per upload
25
25
  MULTIPART_UPLOAD_TIMEOUT = 300 # Timeout in seconds for uploading each part
26
26
  MULTIPART_THRESHOLD_GB = 5.0 # threshold for switching to multipart upload
27
27
 
28
28
  # maximum number of GB sized files we support finetuning for
29
- MAX_FILE_SIZE_GB = 25.0
29
+ MAX_FILE_SIZE_GB = 50.1
30
30
 
31
31
 
32
32
  # Messages
together/filemanager.py CHANGED
@@ -6,10 +6,10 @@ import shutil
6
6
  import stat
7
7
  import tempfile
8
8
  import uuid
9
- from concurrent.futures import ThreadPoolExecutor, as_completed
9
+ from concurrent.futures import Future, ThreadPoolExecutor, as_completed
10
10
  from functools import partial
11
11
  from pathlib import Path
12
- from typing import Any, Dict, List, Tuple
12
+ from typing import Any, BinaryIO, Dict, List, Tuple
13
13
 
14
14
  import requests
15
15
  from filelock import FileLock
@@ -212,6 +212,7 @@ class DownloadManager:
212
212
  ),
213
213
  remaining_retries=MAX_RETRIES,
214
214
  stream=True,
215
+ request_timeout=3600,
215
216
  )
216
217
 
217
218
  try:
@@ -512,6 +513,18 @@ class MultipartUploadManager:
512
513
 
513
514
  return response.data
514
515
 
516
+ def _submit_part(
517
+ self,
518
+ executor: ThreadPoolExecutor,
519
+ f: BinaryIO,
520
+ part_info: Dict[str, Any],
521
+ part_size: int,
522
+ ) -> Future[str]:
523
+ """Submit a single part for upload and return the future"""
524
+ f.seek((part_info["PartNumber"] - 1) * part_size)
525
+ part_data = f.read(part_size)
526
+ return executor.submit(self._upload_single_part, part_info, part_data)
527
+
515
528
  def _upload_parts_concurrent(
516
529
  self, file: Path, upload_info: Dict[str, Any], part_size: int
517
530
  ) -> List[Dict[str, Any]]:
@@ -522,29 +535,39 @@ class MultipartUploadManager:
522
535
 
523
536
  with ThreadPoolExecutor(max_workers=self.max_concurrent_parts) as executor:
524
537
  with tqdm(total=len(parts), desc="Uploading parts", unit="part") as pbar:
525
- future_to_part = {}
526
-
527
538
  with open(file, "rb") as f:
528
- for part_info in parts:
529
- f.seek((part_info["PartNumber"] - 1) * part_size)
530
- part_data = f.read(part_size)
539
+ future_to_part = {}
540
+ part_index = 0
531
541
 
532
- future = executor.submit(
533
- self._upload_single_part, part_info, part_data
534
- )
542
+ # Submit initial batch limited by max_concurrent_parts
543
+ for _ in range(min(self.max_concurrent_parts, len(parts))):
544
+ part_info = parts[part_index]
545
+ future = self._submit_part(executor, f, part_info, part_size)
535
546
  future_to_part[future] = part_info["PartNumber"]
536
-
537
- # Collect results
538
- for future in as_completed(future_to_part):
539
- part_number = future_to_part[future]
540
- try:
541
- etag = future.result()
542
- completed_parts.append(
543
- {"part_number": part_number, "etag": etag}
544
- )
545
- pbar.update(1)
546
- except Exception as e:
547
- raise Exception(f"Failed to upload part {part_number}: {e}")
547
+ part_index += 1
548
+
549
+ # Process completions and submit new parts (sliding window)
550
+ while future_to_part:
551
+ done_future = next(as_completed(future_to_part))
552
+ part_number = future_to_part.pop(done_future)
553
+
554
+ try:
555
+ etag = done_future.result()
556
+ completed_parts.append(
557
+ {"part_number": part_number, "etag": etag}
558
+ )
559
+ pbar.update(1)
560
+ except Exception as e:
561
+ raise Exception(f"Failed to upload part {part_number}: {e}")
562
+
563
+ # Submit next part if available
564
+ if part_index < len(parts):
565
+ part_info = parts[part_index]
566
+ future = self._submit_part(
567
+ executor, f, part_info, part_size
568
+ )
569
+ future_to_part[future] = part_info["PartNumber"]
570
+ part_index += 1
548
571
 
549
572
  completed_parts.sort(key=lambda x: x["part_number"])
550
573
  return completed_parts
@@ -20,6 +20,8 @@ from together.types import (
20
20
  FinetuneLRScheduler,
21
21
  FinetuneRequest,
22
22
  FinetuneResponse,
23
+ FinetunePriceEstimationRequest,
24
+ FinetunePriceEstimationResponse,
23
25
  FinetuneTrainingLimits,
24
26
  FullTrainingType,
25
27
  LinearLRScheduler,
@@ -31,7 +33,7 @@ from together.types import (
31
33
  TrainingMethodSFT,
32
34
  TrainingType,
33
35
  )
34
- from together.types.finetune import DownloadCheckpointType
36
+ from together.types.finetune import DownloadCheckpointType, TrainingMethod
35
37
  from together.utils import log_warn_once, normalize_key
36
38
 
37
39
 
@@ -42,6 +44,12 @@ AVAILABLE_TRAINING_METHODS = {
42
44
  TrainingMethodSFT().method,
43
45
  TrainingMethodDPO().method,
44
46
  }
47
+ _WARNING_MESSAGE_INSUFFICIENT_FUNDS = (
48
+ "The estimated price of the fine-tuning job is {} which is significantly "
49
+ "greater than your current credit limit and balance combined. "
50
+ "It will likely get cancelled due to insufficient funds. "
51
+ "Proceed at your own risk."
52
+ )
45
53
 
46
54
 
47
55
  def create_finetune_request(
@@ -473,12 +481,34 @@ class FineTuning:
473
481
  hf_api_token=hf_api_token,
474
482
  hf_output_repo_name=hf_output_repo_name,
475
483
  )
484
+ if from_checkpoint is None and from_hf_model is None:
485
+ price_estimation_result = self.estimate_price(
486
+ training_file=training_file,
487
+ validation_file=validation_file,
488
+ model=model_name,
489
+ n_epochs=finetune_request.n_epochs,
490
+ n_evals=finetune_request.n_evals,
491
+ training_type="lora" if lora else "full",
492
+ training_method=training_method,
493
+ )
494
+ price_limit_passed = price_estimation_result.allowed_to_proceed
495
+ else:
496
+ # unsupported case
497
+ price_limit_passed = True
476
498
 
477
499
  if verbose:
478
500
  rprint(
479
501
  "Submitting a fine-tuning job with the following parameters:",
480
502
  finetune_request,
481
503
  )
504
+ if not price_limit_passed:
505
+ rprint(
506
+ "[red]"
507
+ + _WARNING_MESSAGE_INSUFFICIENT_FUNDS.format(
508
+ price_estimation_result.estimated_total_price
509
+ )
510
+ + "[/red]",
511
+ )
482
512
  parameter_payload = finetune_request.model_dump(exclude_none=True)
483
513
 
484
514
  response, _, _ = requestor.request(
@@ -493,6 +523,81 @@ class FineTuning:
493
523
 
494
524
  return FinetuneResponse(**response.data)
495
525
 
526
+ def estimate_price(
527
+ self,
528
+ *,
529
+ training_file: str,
530
+ model: str,
531
+ validation_file: str | None = None,
532
+ n_epochs: int | None = 1,
533
+ n_evals: int | None = 0,
534
+ training_type: str = "lora",
535
+ training_method: str = "sft",
536
+ ) -> FinetunePriceEstimationResponse:
537
+ """
538
+ Estimates the price of a fine-tuning job
539
+
540
+ Args:
541
+ training_file (str): File-ID of a file uploaded to the Together API
542
+ model (str): Name of the base model to run fine-tune job on
543
+ validation_file (str, optional): File ID of a file uploaded to the Together API for validation.
544
+ n_epochs (int, optional): Number of epochs for fine-tuning. Defaults to 1.
545
+ n_evals (int, optional): Number of evaluation loops to run. Defaults to 0.
546
+ training_type (str, optional): Training type. Defaults to "lora".
547
+ training_method (str, optional): Training method. Defaults to "sft".
548
+
549
+ Returns:
550
+ FinetunePriceEstimationResponse: Object containing the price estimation result.
551
+ """
552
+ training_type_cls: TrainingType
553
+ training_method_cls: TrainingMethod
554
+
555
+ if training_method == "sft":
556
+ training_method_cls = TrainingMethodSFT(method="sft")
557
+ elif training_method == "dpo":
558
+ training_method_cls = TrainingMethodDPO(method="dpo")
559
+ else:
560
+ raise ValueError(f"Unknown training method: {training_method}")
561
+
562
+ if training_type.lower() == "lora":
563
+ # parameters of lora are unused in price estimation
564
+ # but we need to set them to valid values
565
+ training_type_cls = LoRATrainingType(
566
+ type="Lora",
567
+ lora_r=16,
568
+ lora_alpha=16,
569
+ lora_dropout=0.0,
570
+ lora_trainable_modules="all-linear",
571
+ )
572
+ elif training_type.lower() == "full":
573
+ training_type_cls = FullTrainingType(type="Full")
574
+ else:
575
+ raise ValueError(f"Unknown training type: {training_type}")
576
+
577
+ request = FinetunePriceEstimationRequest(
578
+ training_file=training_file,
579
+ validation_file=validation_file,
580
+ model=model,
581
+ n_epochs=n_epochs,
582
+ n_evals=n_evals,
583
+ training_type=training_type_cls,
584
+ training_method=training_method_cls,
585
+ )
586
+ parameter_payload = request.model_dump(exclude_none=True)
587
+ requestor = api_requestor.APIRequestor(
588
+ client=self._client,
589
+ )
590
+
591
+ response, _, _ = requestor.request(
592
+ options=TogetherRequest(
593
+ method="POST", url="fine-tunes/estimate-price", params=parameter_payload
594
+ ),
595
+ stream=False,
596
+ )
597
+ assert isinstance(response, TogetherResponse)
598
+
599
+ return FinetunePriceEstimationResponse(**response.data)
600
+
496
601
  def list(self) -> FinetuneList:
497
602
  """
498
603
  Lists fine-tune job history
@@ -941,11 +1046,34 @@ class AsyncFineTuning:
941
1046
  hf_output_repo_name=hf_output_repo_name,
942
1047
  )
943
1048
 
1049
+ if from_checkpoint is None and from_hf_model is None:
1050
+ price_estimation_result = await self.estimate_price(
1051
+ training_file=training_file,
1052
+ validation_file=validation_file,
1053
+ model=model_name,
1054
+ n_epochs=finetune_request.n_epochs,
1055
+ n_evals=finetune_request.n_evals,
1056
+ training_type="lora" if lora else "full",
1057
+ training_method=training_method,
1058
+ )
1059
+ price_limit_passed = price_estimation_result.allowed_to_proceed
1060
+ else:
1061
+ # unsupported case
1062
+ price_limit_passed = True
1063
+
944
1064
  if verbose:
945
1065
  rprint(
946
1066
  "Submitting a fine-tuning job with the following parameters:",
947
1067
  finetune_request,
948
1068
  )
1069
+ if not price_limit_passed:
1070
+ rprint(
1071
+ "[red]"
1072
+ + _WARNING_MESSAGE_INSUFFICIENT_FUNDS.format(
1073
+ price_estimation_result.estimated_total_price
1074
+ )
1075
+ + "[/red]",
1076
+ )
949
1077
  parameter_payload = finetune_request.model_dump(exclude_none=True)
950
1078
 
951
1079
  response, _, _ = await requestor.arequest(
@@ -961,6 +1089,81 @@ class AsyncFineTuning:
961
1089
 
962
1090
  return FinetuneResponse(**response.data)
963
1091
 
1092
+ async def estimate_price(
1093
+ self,
1094
+ *,
1095
+ training_file: str,
1096
+ model: str,
1097
+ validation_file: str | None = None,
1098
+ n_epochs: int | None = 1,
1099
+ n_evals: int | None = 0,
1100
+ training_type: str = "lora",
1101
+ training_method: str = "sft",
1102
+ ) -> FinetunePriceEstimationResponse:
1103
+ """
1104
+ Estimates the price of a fine-tuning job
1105
+
1106
+ Args:
1107
+ training_file (str): File-ID of a file uploaded to the Together API
1108
+ model (str): Name of the base model to run fine-tune job on
1109
+ validation_file (str, optional): File ID of a file uploaded to the Together API for validation.
1110
+ n_epochs (int, optional): Number of epochs for fine-tuning. Defaults to 1.
1111
+ n_evals (int, optional): Number of evaluation loops to run. Defaults to 0.
1112
+ training_type (str, optional): Training type. Defaults to "lora".
1113
+ training_method (str, optional): Training method. Defaults to "sft".
1114
+
1115
+ Returns:
1116
+ FinetunePriceEstimationResponse: Object containing the price estimation result.
1117
+ """
1118
+ training_type_cls: TrainingType
1119
+ training_method_cls: TrainingMethod
1120
+
1121
+ if training_method == "sft":
1122
+ training_method_cls = TrainingMethodSFT(method="sft")
1123
+ elif training_method == "dpo":
1124
+ training_method_cls = TrainingMethodDPO(method="dpo")
1125
+ else:
1126
+ raise ValueError(f"Unknown training method: {training_method}")
1127
+
1128
+ if training_type.lower() == "lora":
1129
+ # parameters of lora are unused in price estimation
1130
+ # but we need to set them to valid values
1131
+ training_type_cls = LoRATrainingType(
1132
+ type="Lora",
1133
+ lora_r=16,
1134
+ lora_alpha=16,
1135
+ lora_dropout=0.0,
1136
+ lora_trainable_modules="all-linear",
1137
+ )
1138
+ elif training_type.lower() == "full":
1139
+ training_type_cls = FullTrainingType(type="Full")
1140
+ else:
1141
+ raise ValueError(f"Unknown training type: {training_type}")
1142
+
1143
+ request = FinetunePriceEstimationRequest(
1144
+ training_file=training_file,
1145
+ validation_file=validation_file,
1146
+ model=model,
1147
+ n_epochs=n_epochs,
1148
+ n_evals=n_evals,
1149
+ training_type=training_type_cls,
1150
+ training_method=training_method_cls,
1151
+ )
1152
+ parameter_payload = request.model_dump(exclude_none=True)
1153
+ requestor = api_requestor.APIRequestor(
1154
+ client=self._client,
1155
+ )
1156
+
1157
+ response, _, _ = await requestor.arequest(
1158
+ options=TogetherRequest(
1159
+ method="POST", url="fine-tunes/estimate-price", params=parameter_payload
1160
+ ),
1161
+ stream=False,
1162
+ )
1163
+ assert isinstance(response, TogetherResponse)
1164
+
1165
+ return FinetunePriceEstimationResponse(**response.data)
1166
+
964
1167
  async def list(self) -> FinetuneList:
965
1168
  """
966
1169
  Async method to list fine-tune job history
@@ -54,6 +54,8 @@ from together.types.finetune import (
54
54
  FinetuneListEvents,
55
55
  FinetuneRequest,
56
56
  FinetuneResponse,
57
+ FinetunePriceEstimationRequest,
58
+ FinetunePriceEstimationResponse,
57
59
  FinetuneDeleteResponse,
58
60
  FinetuneTrainingLimits,
59
61
  FullTrainingType,
@@ -103,6 +105,8 @@ __all__ = [
103
105
  "FinetuneDeleteResponse",
104
106
  "FinetuneDownloadResult",
105
107
  "FinetuneLRScheduler",
108
+ "FinetunePriceEstimationRequest",
109
+ "FinetunePriceEstimationResponse",
106
110
  "LinearLRScheduler",
107
111
  "LinearLRSchedulerArgs",
108
112
  "CosineLRScheduler",
@@ -46,6 +46,7 @@ class ChatCompletionMessageContentType(str, Enum):
46
46
  TEXT = "text"
47
47
  IMAGE_URL = "image_url"
48
48
  VIDEO_URL = "video_url"
49
+ AUDIO_URL = "audio_url"
49
50
 
50
51
 
51
52
  class ChatCompletionMessageContentImageURL(BaseModel):
@@ -56,11 +57,16 @@ class ChatCompletionMessageContentVideoURL(BaseModel):
56
57
  url: str
57
58
 
58
59
 
60
+ class ChatCompletionMessageContentAudioURL(BaseModel):
61
+ url: str
62
+
63
+
59
64
  class ChatCompletionMessageContent(BaseModel):
60
65
  type: ChatCompletionMessageContentType
61
66
  text: str | None = None
62
67
  image_url: ChatCompletionMessageContentImageURL | None = None
63
68
  video_url: ChatCompletionMessageContentVideoURL | None = None
69
+ audio_url: ChatCompletionMessageContentAudioURL | None = None
64
70
 
65
71
 
66
72
  class ChatCompletionMessage(BaseModel):
together/types/common.py CHANGED
@@ -26,6 +26,7 @@ class UsageData(BaseModel):
26
26
 
27
27
 
28
28
  class ObjectType(str, Enum):
29
+ TextCompletion = "text_completion"
29
30
  Completion = "text.completion"
30
31
  CompletionChunk = "completion.chunk"
31
32
  ChatCompletion = "chat.completion"
together/types/files.py CHANGED
@@ -15,6 +15,7 @@ class FilePurpose(str, Enum):
15
15
  FineTune = "fine-tune"
16
16
  BatchAPI = "batch-api"
17
17
  Eval = "eval"
18
+ EvalOutput = "eval-output"
18
19
 
19
20
 
20
21
  class FileType(str, Enum):
@@ -308,6 +308,32 @@ class FinetuneResponse(BaseModel):
308
308
  raise ValueError("Unknown training type")
309
309
 
310
310
 
311
+ class FinetunePriceEstimationRequest(BaseModel):
312
+ """
313
+ Fine-tune price estimation request type
314
+ """
315
+
316
+ training_file: str
317
+ validation_file: str | None = None
318
+ model: str
319
+ n_epochs: int
320
+ n_evals: int
321
+ training_type: TrainingType
322
+ training_method: TrainingMethod
323
+
324
+
325
+ class FinetunePriceEstimationResponse(BaseModel):
326
+ """
327
+ Fine-tune price estimation response type
328
+ """
329
+
330
+ estimated_total_price: float
331
+ user_limit: float
332
+ estimated_train_token_count: int
333
+ estimated_eval_token_count: int
334
+ allowed_to_proceed: bool
335
+
336
+
311
337
  class FinetuneList(BaseModel):
312
338
  # object type
313
339
  object: Literal["list"] | None = None
together/utils/files.py CHANGED
@@ -7,6 +7,7 @@ from pathlib import Path
7
7
  from traceback import format_exc
8
8
  from typing import Any, Dict, List
9
9
 
10
+ from tqdm import tqdm
10
11
 
11
12
  from together.constants import (
12
13
  MAX_FILE_SIZE_GB,
@@ -363,14 +364,19 @@ def _check_utf8(file: Path) -> Dict[str, Any]:
363
364
  Dict[str, Any]: A dictionary with the results of the check.
364
365
  """
365
366
  report_dict: Dict[str, Any] = {}
367
+
366
368
  try:
369
+ # Dry-run UTF-8 decode: iterate through file to validate encoding
367
370
  with file.open(encoding="utf-8") as f:
368
- f.read()
371
+ for _ in f:
372
+ pass
373
+
369
374
  report_dict["utf8"] = True
370
375
  except UnicodeDecodeError as e:
371
376
  report_dict["utf8"] = False
372
377
  report_dict["message"] = f"File is not UTF-8 encoded. Error raised: {e}."
373
378
  report_dict["is_check_passed"] = False
379
+
374
380
  return report_dict
375
381
 
376
382
 
@@ -470,7 +476,7 @@ def _check_jsonl(file: Path, purpose: FilePurpose | str) -> Dict[str, Any]:
470
476
  with file.open() as f:
471
477
  idx = -1
472
478
  try:
473
- for idx, line in enumerate(f):
479
+ for idx, line in tqdm(enumerate(f), desc="Validating file", unit=" lines"):
474
480
  json_line = json.loads(line)
475
481
 
476
482
  if not isinstance(json_line, dict):
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: together
3
- Version: 1.5.30
4
- Summary: Python client for Together's Cloud Platform!
3
+ Version: 1.5.32
4
+ Summary: Python client for Together's Cloud Platform! Note: SDK 2.0 is now available at https://github.com/togethercomputer/together-py
5
5
  License: Apache-2.0
6
6
  License-File: LICENSE
7
7
  Author: Together AI
@@ -42,6 +42,34 @@ Description-Content-Type: text/markdown
42
42
  </a>
43
43
  </div>
44
44
 
45
+ > [!NOTE]
46
+ > ## 🚀 Together Python SDK 2.0 is now available!
47
+ >
48
+ > Check out the new SDK: **[together-py](https://github.com/togethercomputer/together-py)**
49
+ >
50
+ > 📖 **Migration Guide:** [https://docs.together.ai/docs/pythonv2-migration-guide](https://docs.together.ai/docs/pythonv2-migration-guide)
51
+ >
52
+ > ### Install the Beta
53
+ >
54
+ > **Using uv (Recommended):**
55
+ > ```bash
56
+ > # Install uv if you haven't already
57
+ > curl -LsSf https://astral.sh/uv/install.sh | sh
58
+ >
59
+ > # Install together python SDK
60
+ > uv add together --prerelease allow
61
+ >
62
+ > # Or upgrade an existing installation
63
+ > uv sync --upgrade-package together --prerelease allow
64
+ > ```
65
+ >
66
+ > **Using pip:**
67
+ > ```bash
68
+ > pip install --pre together
69
+ > ```
70
+ >
71
+ > This package will be maintained until January 2026.
72
+
45
73
  # Together Python API library
46
74
 
47
75
  [![PyPI version](https://img.shields.io/pypi/v/together.svg)](https://pypi.org/project/together/)
@@ -1,22 +1,22 @@
1
- together/__init__.py,sha256=B8T7ybZ7D6jJNRTuFDVjOFlImCNag8tNZXpZdXz7xNM,1530
1
+ together/__init__.py,sha256=QTmcsqfUFqf6pWGK_NuXKupuK5m5jGGj24HF0za0_yA,3715
2
2
  together/abstract/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
3
  together/abstract/api_requestor.py,sha256=CPFsQXEqIoXDcqxlDQyumbTMtGmL7CQYtSYrkb3binU,27556
4
4
  together/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
5
  together/cli/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
- together/cli/api/chat.py,sha256=2PHRb-9T-lUEKhUJFtc7SxJv3shCVx40gq_8pzfsewM,9234
6
+ together/cli/api/chat.py,sha256=auJh0WZwpY16vFLJojkzLJYnjU1IgNz_ybf7sQtKga0,9941
7
7
  together/cli/api/completions.py,sha256=l-Zw5t7hojL3w8xd_mitS2NRB72i5Z0xwkzH0rT5XMc,4263
8
- together/cli/api/endpoints.py,sha256=ShQQuMRwg70bclEqplk2aru_IlwOdp4DEuLZ1kG1KvA,14622
8
+ together/cli/api/endpoints.py,sha256=S3px19iGTKy5KS1nuKrvUUMoqc_KtrZHyIwjwjqX7uQ,14624
9
9
  together/cli/api/evaluation.py,sha256=36SsujC5qicf-8l8GA8wqRtEC8NKzsAjL-_nYhePpQM,14691
10
10
  together/cli/api/files.py,sha256=QLYEXRkY8J2Gg1SbTCtzGfoTMvosoeACNK83L_oLubs,3397
11
- together/cli/api/finetune.py,sha256=zG8Peg7DuptMpT5coqqGbRdaxM5SxQgte9tIv7tMJbM,18437
11
+ together/cli/api/finetune.py,sha256=Hmn8UrDNCPiLPDilnKPjnx8V27WliAVTZgQKb6SnHwc,19625
12
12
  together/cli/api/images.py,sha256=GADSeaNUHUVMtWovmccGuKc28IJ9E_v4vAEwYHJhu5o,2645
13
13
  together/cli/api/models.py,sha256=BRWRiguuJ8OwAD8crajpZ7RyCHA35tyOZvi3iLWQ7k4,3679
14
14
  together/cli/api/utils.py,sha256=IuqYWPnLI38_Bqd7lj8V_SnGdYc59pRmMbQmciS4FsM,1326
15
15
  together/cli/cli.py,sha256=PVahUjOfAQIjo209FoPKljcCA_OIpOYQ9MAsCjfEMu0,2134
16
16
  together/client.py,sha256=KD33kAPkWTcnXjge4rLK_L3UsJYsxNUkvL6b9SgTEf0,6324
17
- together/constants.py,sha256=yloKFcO6sIt-Vpk2tDIanJrFiXQUg5Vm0vmU5Cl703U,1999
17
+ together/constants.py,sha256=IaKMIamFia9nyq8jPrmqu5y0YL5mC_474AAIUXYFsdk,1964
18
18
  together/error.py,sha256=HU6247CyzCFjaxL9A0XYbXZ6fY_ebRg0FEYjI4Skogs,5515
19
- together/filemanager.py,sha256=ebVjksV676Kzvd9iDgraWTjatD8ZLfLT44rjcXveRZo,18326
19
+ together/filemanager.py,sha256=bynQp2yGoFMZcgVtgFlkYxTbnk6n_GxdiEpY0q50kbk,19448
20
20
  together/legacy/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
21
  together/legacy/base.py,sha256=ehrX1SCfRbK5OA83wL1q7-tfF-yuZOUxzjxYfFtdvvQ,727
22
22
  together/legacy/complete.py,sha256=NRJX-vjnkg4HrgDo9LS3jFfhwfXpeGxcl24dcrLPK3A,2439
@@ -40,26 +40,26 @@ together/resources/embeddings.py,sha256=PTvLb82yjG_-iQOyuhsilp77Fr7gZ0o6WD2KeRnK
40
40
  together/resources/endpoints.py,sha256=BP75wUEcOtpiUbfLAQH5GX2RL8_RnM522-D8Iz7_LUU,20378
41
41
  together/resources/evaluation.py,sha256=eYSs9HUpW51XZjX-yNlFZlLapsuEDINJ8BjxJoYa4U0,31443
42
42
  together/resources/files.py,sha256=_uK5xzriXNOGNw3tQGuTbCaxBRo6Az6_cXOUtBNFzDk,5434
43
- together/resources/finetune.py,sha256=VeMyPG-PA16d2UAzqNTQEAKBgMvVApj97lTAHEuR0kc,44890
43
+ together/resources/finetune.py,sha256=rOclrA4GCu1wrE-D0-hc0ac7lJksucbIW6OOxQT0q7I,52981
44
44
  together/resources/images.py,sha256=FHXkcnzyj2JLw4YF1NH56hgISEeCO0Sg_SvTCcTJaOo,4831
45
45
  together/resources/models.py,sha256=WpP-x25AXYpmu-VKu_X4Up-zHwpWBBvPRpbV4FsWQrU,8266
46
46
  together/resources/rerank.py,sha256=3Ju_aRSyZ1s_3zCSNZnSnEJErUVmt2xa3M8z1nvejMA,3931
47
47
  together/resources/videos.py,sha256=Dn7vslH1pZVw4WYvH-69fjzqLZdKHkTK-lIbFkxh0w0,11144
48
48
  together/together_response.py,sha256=a3dgKMPDrlfKQwxYENfNt2T4l2vSZxRWMixhHSy-q3E,1308
49
- together/types/__init__.py,sha256=eK8DXMzHp78kieDv7JpXNbcS2k3aWvyQrgLdYUtL_qM,4342
49
+ together/types/__init__.py,sha256=nh6yT1mmlmkLGQE3DYeJYNkSAIIIxNep15jwZWICz40,4492
50
50
  together/types/abstract.py,sha256=1lFQI_3WjsR_t1128AeKW0aTk6EiM6Gh1J3ZuyLLPao,642
51
51
  together/types/audio_speech.py,sha256=pUzqpx7NCjtPIq91xO2k0psetzLz29NTHHm6DS0k8Xg,9682
52
52
  together/types/batch.py,sha256=KiI5i1En7cyIUxHhVIGoQk6Wlw19c0PXSqDWwc2KZ2c,1140
53
- together/types/chat_completions.py,sha256=NxJ7tFlWynxoLsRtQHzM7Ka3QxKVjRs6EvtOTYZ79bM,5340
53
+ together/types/chat_completions.py,sha256=OkEk4_Z5cf36Ae775epG_lIQ6dkKPSSKujc9wQ-tzQs,5504
54
54
  together/types/code_interpreter.py,sha256=cjF8TKgRkJllHS4i24dWQZBGTRsG557eHSewOiip0Kk,1770
55
- together/types/common.py,sha256=kxZ-N9xtBsGYZBmbIWnZ0rfT3Pn8PFB7sAbp3iv96pw,1525
55
+ together/types/common.py,sha256=c2_CeyjOBWbJ0RIAsWB13DG8j6N3ATU-6yH-CnFitVY,1564
56
56
  together/types/completions.py,sha256=o3FR5ixsTUj-a3pmOUzbSQg-hESVhpqrC9UD__VCqr4,2971
57
57
  together/types/embeddings.py,sha256=J7grkYYn7xhqeKaBO2T-8XQRtHhkzYzymovtGdIUK5A,751
58
58
  together/types/endpoints.py,sha256=EzNhHOoQ_D9fUdNQtxQPeSWiFzdFLqpNodN0YLmv_h0,4393
59
59
  together/types/error.py,sha256=OVlCs3cx_2WhZK4JzHT8SQyRIIqKOP1AZQ4y1PydjAE,370
60
60
  together/types/evaluation.py,sha256=9gCAgzAwFD95MWnSgvxnSYFF27wKOTqIGn-wSOpFt2M,2385
61
- together/types/files.py,sha256=XCimmKDaSEEfavOtp0UH-ZrRxrmHoCTYLlmmhshbr7A,1994
62
- together/types/finetune.py,sha256=EQAJVXqK1Ne2V2dCfUiJgOwK9_x_7TwQRrjWavap698,11396
61
+ together/types/files.py,sha256=_pB_q8kU5QH7WE3Y8Uro6LGsgK_5zrGYzJREZL9cRH0,2025
62
+ together/types/finetune.py,sha256=vpbmyRRV0gJryi0F7YUIbUk5Ya8CPmi0mJ95ZjpfpbE,11959
63
63
  together/types/images.py,sha256=IsrmIM2FVeG-kP4vhZUx5fG5EhOJ-d8fefrAmOVKNDs,926
64
64
  together/types/models.py,sha256=V8bcy1c3uTmqwnTVphbYLF2AJ6l2P2724njl36TzfHQ,2878
65
65
  together/types/rerank.py,sha256=qZfuXOn7MZ6ly8hpJ_MZ7OU_Bi1-cgYNSB20Wja8Qkk,1061
@@ -67,11 +67,11 @@ together/types/videos.py,sha256=KCLk8CF0kbA_51qnHOzAWg5VA6HTlwnY-sTZ2lUR0Eo,1861
67
67
  together/utils/__init__.py,sha256=5fqvj4KT2rHxKSQot2TSyV_HcvkvkGiqAiaYuJwqtm0,786
68
68
  together/utils/_log.py,sha256=5IYNI-jYzxyIS-pUvhb0vE_Muo3MA7GgBhsu66TKP2w,1951
69
69
  together/utils/api_helpers.py,sha256=2K0O6qeEQ2zVFvi5NBN5m2kjZJaS3-JfKFecQ7SmGaw,3746
70
- together/utils/files.py,sha256=oFmQZZHud6sMlT1OCUMx2Ab6t7ScBcZ72em0KQ75BJI,24879
70
+ together/utils/files.py,sha256=mWFFpsgVPDQg1ZCb-oTrDUFv3aXg1AItgtwXvDsFegI,25047
71
71
  together/utils/tools.py,sha256=H2MTJhEqtBllaDvOyZehIO_IVNK3P17rSDeILtJIVag,2964
72
72
  together/version.py,sha256=p03ivHyE0SyWU4jAnRTBi_sOwywVWoZPU4g2gzRgG-Y,126
73
- together-1.5.30.dist-info/METADATA,sha256=w7u0mFGUl4wpYgwCXtfeEk6A6_ArlRvju65HRPOyAD4,16583
74
- together-1.5.30.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
75
- together-1.5.30.dist-info/entry_points.txt,sha256=G-b5NKW6lUUf1V1fH8IPTBb7jXnK7lhbX9H1zTEJXPs,50
76
- together-1.5.30.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
77
- together-1.5.30.dist-info/RECORD,,
73
+ together-1.5.32.dist-info/METADATA,sha256=lQExfe_6VE3LiQDX6E3zbVVsNwlPZ2vzQMuxtTaV7M8,17415
74
+ together-1.5.32.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
75
+ together-1.5.32.dist-info/entry_points.txt,sha256=G-b5NKW6lUUf1V1fH8IPTBb7jXnK7lhbX9H1zTEJXPs,50
76
+ together-1.5.32.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
77
+ together-1.5.32.dist-info/RECORD,,