together 1.5.31__py3-none-any.whl → 1.5.33__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
 
@@ -9,14 +9,17 @@ from typing import Any, Literal
9
9
  import click
10
10
  from click.core import ParameterSource # type: ignore[attr-defined]
11
11
  from rich import print as rprint
12
+ from rich.json import JSON
12
13
  from tabulate import tabulate
13
14
 
14
15
  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
16
17
  from together.types.finetune import (
17
18
  DownloadCheckpointType,
18
19
  FinetuneEventType,
19
20
  FinetuneTrainingLimits,
21
+ FullTrainingType,
22
+ LoRATrainingType,
20
23
  )
21
24
  from together.utils import (
22
25
  finetune_price_to_dollars,
@@ -29,13 +32,21 @@ from together.utils import (
29
32
 
30
33
  _CONFIRMATION_MESSAGE = (
31
34
  "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 "
33
37
  "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}"
35
40
  "You can pass `-y` or `--confirm` to your command to skip this message.\n\n"
36
41
  "Do you want to proceed?"
37
42
  )
38
43
 
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
+
39
50
 
40
51
  class DownloadCheckpointTypeChoice(click.Choice):
41
52
  def __init__(self) -> None:
@@ -357,12 +368,36 @@ def create(
357
368
  "You have specified a number of evaluation loops but no validation file."
358
369
  )
359
370
 
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):
361
397
  response = client.fine_tuning.create(
362
398
  **training_args,
363
399
  verbose=True,
364
400
  )
365
-
366
401
  report_string = f"Successfully submitted a fine-tuning job {response.id}"
367
402
  if response.created_at is not None:
368
403
  created_time = datetime.strptime(
@@ -401,6 +436,9 @@ def list(ctx: click.Context) -> None:
401
436
  "Price": f"""${
402
437
  finetune_price_to_dollars(float(str(i.total_price)))
403
438
  }""", # convert to string for mypy typing
439
+ "Progress": generate_progress_bar(
440
+ i, datetime.now().astimezone(), use_rich=False
441
+ ),
404
442
  }
405
443
  )
406
444
  table = tabulate(display_list, headers="keys", tablefmt="grid", showindex=True)
@@ -420,7 +458,15 @@ def retrieve(ctx: click.Context, fine_tune_id: str) -> None:
420
458
  # remove events from response for cleaner output
421
459
  response.events = None
422
460
 
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}")
424
470
 
425
471
 
426
472
  @fine_tuning.command()
together/cli/api/utils.py CHANGED
@@ -1,10 +1,17 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import math
4
+ import re
3
5
  from gettext import gettext as _
4
6
  from typing import Literal
7
+ from datetime import datetime
5
8
 
6
9
  import click
7
10
 
11
+ from together.types.finetune import FinetuneResponse, COMPLETED_STATUSES
12
+
13
+ _PROGRESS_BAR_WIDTH = 40
14
+
8
15
 
9
16
  class AutoIntParamType(click.ParamType):
10
17
  name = "integer_or_max"
@@ -49,3 +56,84 @@ class BooleanWithAutoParamType(click.ParamType):
49
56
 
50
57
  INT_WITH_MAX = AutoIntParamType()
51
58
  BOOL_WITH_AUTO = BooleanWithAutoParamType()
59
+
60
+
61
+ def _human_readable_time(timedelta: float) -> str:
62
+ """Convert a timedelta to a compact human-readble string
63
+ Examples:
64
+ 00:00:10 -> 10s
65
+ 01:23:45 -> 1h 23min 45s
66
+ 1 Month 23 days 04:56:07 -> 1month 23d 4h 56min 7s
67
+ Args:
68
+ timedelta (float): The timedelta in seconds to convert.
69
+ Returns:
70
+ A string representing the timedelta in a human-readable format.
71
+ """
72
+ units = [
73
+ (30 * 24 * 60 * 60, "month"), # 30 days
74
+ (24 * 60 * 60, "d"),
75
+ (60 * 60, "h"),
76
+ (60, "min"),
77
+ (1, "s"),
78
+ ]
79
+
80
+ total_seconds = int(timedelta)
81
+ parts = []
82
+
83
+ for unit_seconds, unit_name in units:
84
+ if total_seconds >= unit_seconds:
85
+ value = total_seconds // unit_seconds
86
+ total_seconds %= unit_seconds
87
+ parts.append(f"{value}{unit_name}")
88
+
89
+ return " ".join(parts) if parts else "0s"
90
+
91
+
92
+ def generate_progress_bar(
93
+ finetune_job: FinetuneResponse, current_time: datetime, use_rich: bool = False
94
+ ) -> str:
95
+ """Generate a progress bar for a finetune job.
96
+ Args:
97
+ finetune_job: The finetune job to generate a progress bar for.
98
+ current_time: The current time.
99
+ use_rich: Whether to use rich formatting.
100
+ Returns:
101
+ A string representing the progress bar.
102
+ """
103
+ progress = "Progress: [bold red]unavailable[/bold red]"
104
+ if finetune_job.status in COMPLETED_STATUSES:
105
+ progress = "Progress: [bold green]completed[/bold green]"
106
+ elif finetune_job.updated_at is not None:
107
+ # Replace 'Z' with '+00:00' for Python 3.10 compatibility
108
+ updated_at_str = finetune_job.updated_at.replace("Z", "+00:00")
109
+ update_at = datetime.fromisoformat(updated_at_str).astimezone()
110
+
111
+ if finetune_job.progress is not None:
112
+ if current_time < update_at:
113
+ return progress
114
+
115
+ if not finetune_job.progress.estimate_available:
116
+ return progress
117
+
118
+ if finetune_job.progress.seconds_remaining <= 0:
119
+ return progress
120
+
121
+ elapsed_time = (current_time - update_at).total_seconds()
122
+ ratio_filled = min(
123
+ elapsed_time / finetune_job.progress.seconds_remaining, 1.0
124
+ )
125
+ percentage = ratio_filled * 100
126
+ filled = math.ceil(ratio_filled * _PROGRESS_BAR_WIDTH)
127
+ bar = "█" * filled + "░" * (_PROGRESS_BAR_WIDTH - filled)
128
+ time_left = "N/A"
129
+ if finetune_job.progress.seconds_remaining > elapsed_time:
130
+ time_left = _human_readable_time(
131
+ finetune_job.progress.seconds_remaining - elapsed_time
132
+ )
133
+ time_text = f"{time_left} left"
134
+ progress = f"Progress: {bar} [bold]{percentage:>3.0f}%[/bold] [yellow]{time_text}[/yellow]"
135
+
136
+ if use_rich:
137
+ return progress
138
+
139
+ return re.sub(r"\[/?[^\]]+\]", "", progress)
@@ -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",
@@ -28,6 +28,14 @@ class FinetuneJobStatus(str, Enum):
28
28
  STATUS_COMPLETED = "completed"
29
29
 
30
30
 
31
+ COMPLETED_STATUSES = [
32
+ FinetuneJobStatus.STATUS_ERROR,
33
+ FinetuneJobStatus.STATUS_USER_ERROR,
34
+ FinetuneJobStatus.STATUS_COMPLETED,
35
+ FinetuneJobStatus.STATUS_CANCELLED,
36
+ ]
37
+
38
+
31
39
  class FinetuneEventLevels(str, Enum):
32
40
  """
33
41
  Fine-tune job event status levels
@@ -167,6 +175,15 @@ class TrainingMethodDPO(TrainingMethod):
167
175
  simpo_gamma: float | None = None
168
176
 
169
177
 
178
+ class FinetuneProgress(BaseModel):
179
+ """
180
+ Fine-tune job progress
181
+ """
182
+
183
+ estimate_available: bool = False
184
+ seconds_remaining: float = 0
185
+
186
+
170
187
  class FinetuneRequest(BaseModel):
171
188
  """
172
189
  Fine-tune request type
@@ -297,6 +314,8 @@ class FinetuneResponse(BaseModel):
297
314
  train_on_inputs: StrictBool | Literal["auto"] | None = "auto"
298
315
  from_checkpoint: str | None = None
299
316
 
317
+ progress: FinetuneProgress | None = None
318
+
300
319
  @field_validator("training_type")
301
320
  @classmethod
302
321
  def validate_training_type(cls, v: TrainingType) -> TrainingType:
@@ -308,6 +327,32 @@ class FinetuneResponse(BaseModel):
308
327
  raise ValueError("Unknown training type")
309
328
 
310
329
 
330
+ class FinetunePriceEstimationRequest(BaseModel):
331
+ """
332
+ Fine-tune price estimation request type
333
+ """
334
+
335
+ training_file: str
336
+ validation_file: str | None = None
337
+ model: str
338
+ n_epochs: int
339
+ n_evals: int
340
+ training_type: LoRATrainingType | FullTrainingType
341
+ training_method: TrainingMethodSFT | TrainingMethodDPO
342
+
343
+
344
+ class FinetunePriceEstimationResponse(BaseModel):
345
+ """
346
+ Fine-tune price estimation response type
347
+ """
348
+
349
+ estimated_total_price: float
350
+ user_limit: float
351
+ estimated_train_token_count: int
352
+ estimated_eval_token_count: int
353
+ allowed_to_proceed: bool
354
+
355
+
311
356
  class FinetuneList(BaseModel):
312
357
  # object type
313
358
  object: Literal["list"] | None = None
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: together
3
- Version: 1.5.31
4
- Summary: Python client for Together's Cloud Platform!
3
+ Version: 1.5.33
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,4 +1,4 @@
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
@@ -8,10 +8,10 @@ together/cli/api/completions.py,sha256=l-Zw5t7hojL3w8xd_mitS2NRB72i5Z0xwkzH0rT5X
8
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=FQtMioZkgD_c2Qy4cftSw5IICcGgidbh8tAiGWPN88Q,20097
12
12
  together/cli/api/images.py,sha256=GADSeaNUHUVMtWovmccGuKc28IJ9E_v4vAEwYHJhu5o,2645
13
13
  together/cli/api/models.py,sha256=BRWRiguuJ8OwAD8crajpZ7RyCHA35tyOZvi3iLWQ7k4,3679
14
- together/cli/api/utils.py,sha256=IuqYWPnLI38_Bqd7lj8V_SnGdYc59pRmMbQmciS4FsM,1326
14
+ together/cli/api/utils.py,sha256=MRK6siAmDtVuXz4Vi0Jkf9RhlSwB2Sjk88OK34Z-I6E,4388
15
15
  together/cli/cli.py,sha256=PVahUjOfAQIjo209FoPKljcCA_OIpOYQ9MAsCjfEMu0,2134
16
16
  together/client.py,sha256=KD33kAPkWTcnXjge4rLK_L3UsJYsxNUkvL6b9SgTEf0,6324
17
17
  together/constants.py,sha256=IaKMIamFia9nyq8jPrmqu5y0YL5mC_474AAIUXYFsdk,1964
@@ -40,13 +40,13 @@ 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
@@ -59,7 +59,7 @@ together/types/endpoints.py,sha256=EzNhHOoQ_D9fUdNQtxQPeSWiFzdFLqpNodN0YLmv_h0,4
59
59
  together/types/error.py,sha256=OVlCs3cx_2WhZK4JzHT8SQyRIIqKOP1AZQ4y1PydjAE,370
60
60
  together/types/evaluation.py,sha256=9gCAgzAwFD95MWnSgvxnSYFF27wKOTqIGn-wSOpFt2M,2385
61
61
  together/types/files.py,sha256=_pB_q8kU5QH7WE3Y8Uro6LGsgK_5zrGYzJREZL9cRH0,2025
62
- together/types/finetune.py,sha256=EQAJVXqK1Ne2V2dCfUiJgOwK9_x_7TwQRrjWavap698,11396
62
+ together/types/finetune.py,sha256=HQ1FL16DkFIaC84bdbwEWEJUwvFnPTcarEwYtI0VNQg,12386
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
@@ -70,8 +70,8 @@ together/utils/api_helpers.py,sha256=2K0O6qeEQ2zVFvi5NBN5m2kjZJaS3-JfKFecQ7SmGaw
70
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.31.dist-info/METADATA,sha256=VXwb3-wFmS-Zika1_4B1i9aHy3F3AD7-zeSiSQJYiTQ,16583
74
- together-1.5.31.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
75
- together-1.5.31.dist-info/entry_points.txt,sha256=G-b5NKW6lUUf1V1fH8IPTBb7jXnK7lhbX9H1zTEJXPs,50
76
- together-1.5.31.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
77
- together-1.5.31.dist-info/RECORD,,
73
+ together-1.5.33.dist-info/METADATA,sha256=9syOtPDummSAPlru8OCACrQH4L1A6hY_VtA66JD8qKw,17415
74
+ together-1.5.33.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
75
+ together-1.5.33.dist-info/entry_points.txt,sha256=G-b5NKW6lUUf1V1fH8IPTBb7jXnK7lhbX9H1zTEJXPs,50
76
+ together-1.5.33.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
77
+ together-1.5.33.dist-info/RECORD,,