together 2.0.0a13__py3-none-any.whl → 2.0.0a15__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/_client.py +38 -0
- together/_version.py +1 -1
- together/constants.py +34 -0
- together/error.py +16 -0
- together/lib/cli/api/beta/beta.py +12 -0
- together/lib/cli/api/beta/clusters.py +357 -0
- together/lib/cli/api/beta/clusters_storage.py +152 -0
- together/lib/cli/api/utils.py +41 -5
- together/lib/cli/cli.py +2 -0
- together/lib/types/fine_tuning.py +3 -0
- together/resources/__init__.py +14 -0
- together/resources/beta/__init__.py +33 -0
- together/resources/beta/beta.py +102 -0
- together/resources/beta/clusters/__init__.py +33 -0
- together/resources/beta/clusters/clusters.py +628 -0
- together/resources/beta/clusters/storage.py +490 -0
- together/types/__init__.py +12 -1
- together/types/beta/__init__.py +12 -0
- together/types/beta/cluster.py +93 -0
- together/types/beta/cluster_create_params.py +51 -0
- together/types/beta/cluster_create_response.py +9 -0
- together/types/beta/cluster_delete_response.py +9 -0
- together/types/beta/cluster_list_regions_response.py +21 -0
- together/types/beta/cluster_list_response.py +12 -0
- together/types/beta/cluster_update_params.py +13 -0
- together/types/beta/cluster_update_response.py +9 -0
- together/types/beta/clusters/__init__.py +10 -0
- together/types/beta/clusters/cluster_storage.py +13 -0
- together/types/beta/clusters/storage_create_params.py +17 -0
- together/types/beta/clusters/storage_create_response.py +9 -0
- together/types/beta/clusters/storage_delete_response.py +9 -0
- together/types/beta/clusters/storage_list_response.py +12 -0
- together/types/beta/clusters/storage_update_params.py +13 -0
- together/types/chat_completions.py +7 -0
- together/types/endpoints.py +4 -0
- together/types/files.py +8 -0
- together/types/fine_tuning_cancel_response.py +3 -0
- together/types/fine_tuning_list_response.py +3 -0
- together/types/finetune.py +27 -0
- together/types/finetune_response.py +2 -0
- together/types/models.py +2 -0
- {together-2.0.0a13.dist-info → together-2.0.0a15.dist-info}/METADATA +55 -8
- {together-2.0.0a13.dist-info → together-2.0.0a15.dist-info}/RECORD +46 -15
- {together-2.0.0a13.dist-info → together-2.0.0a15.dist-info}/WHEEL +0 -0
- {together-2.0.0a13.dist-info → together-2.0.0a15.dist-info}/entry_points.txt +0 -0
- {together-2.0.0a13.dist-info → together-2.0.0a15.dist-info}/licenses/LICENSE +0 -0
together/_client.py
CHANGED
|
@@ -32,6 +32,7 @@ from ._base_client import (
|
|
|
32
32
|
|
|
33
33
|
if TYPE_CHECKING:
|
|
34
34
|
from .resources import (
|
|
35
|
+
beta,
|
|
35
36
|
chat,
|
|
36
37
|
jobs,
|
|
37
38
|
audio,
|
|
@@ -58,6 +59,7 @@ if TYPE_CHECKING:
|
|
|
58
59
|
from .resources.videos import VideosResource, AsyncVideosResource
|
|
59
60
|
from .resources.batches import BatchesResource, AsyncBatchesResource
|
|
60
61
|
from .resources.hardware import HardwareResource, AsyncHardwareResource
|
|
62
|
+
from .resources.beta.beta import BetaResource, AsyncBetaResource
|
|
61
63
|
from .resources.chat.chat import ChatResource, AsyncChatResource
|
|
62
64
|
from .resources.endpoints import EndpointsResource, AsyncEndpointsResource
|
|
63
65
|
from .resources.embeddings import EmbeddingsResource, AsyncEmbeddingsResource
|
|
@@ -136,6 +138,12 @@ class Together(SyncAPIClient):
|
|
|
136
138
|
|
|
137
139
|
self._default_stream_cls = Stream
|
|
138
140
|
|
|
141
|
+
@cached_property
|
|
142
|
+
def beta(self) -> BetaResource:
|
|
143
|
+
from .resources.beta import BetaResource
|
|
144
|
+
|
|
145
|
+
return BetaResource(self)
|
|
146
|
+
|
|
139
147
|
@cached_property
|
|
140
148
|
def chat(self) -> ChatResource:
|
|
141
149
|
from .resources.chat import ChatResource
|
|
@@ -405,6 +413,12 @@ class AsyncTogether(AsyncAPIClient):
|
|
|
405
413
|
|
|
406
414
|
self._default_stream_cls = AsyncStream
|
|
407
415
|
|
|
416
|
+
@cached_property
|
|
417
|
+
def beta(self) -> AsyncBetaResource:
|
|
418
|
+
from .resources.beta import AsyncBetaResource
|
|
419
|
+
|
|
420
|
+
return AsyncBetaResource(self)
|
|
421
|
+
|
|
408
422
|
@cached_property
|
|
409
423
|
def chat(self) -> AsyncChatResource:
|
|
410
424
|
from .resources.chat import AsyncChatResource
|
|
@@ -622,6 +636,12 @@ class TogetherWithRawResponse:
|
|
|
622
636
|
def __init__(self, client: Together) -> None:
|
|
623
637
|
self._client = client
|
|
624
638
|
|
|
639
|
+
@cached_property
|
|
640
|
+
def beta(self) -> beta.BetaResourceWithRawResponse:
|
|
641
|
+
from .resources.beta import BetaResourceWithRawResponse
|
|
642
|
+
|
|
643
|
+
return BetaResourceWithRawResponse(self._client.beta)
|
|
644
|
+
|
|
625
645
|
@cached_property
|
|
626
646
|
def chat(self) -> chat.ChatResourceWithRawResponse:
|
|
627
647
|
from .resources.chat import ChatResourceWithRawResponse
|
|
@@ -725,6 +745,12 @@ class AsyncTogetherWithRawResponse:
|
|
|
725
745
|
def __init__(self, client: AsyncTogether) -> None:
|
|
726
746
|
self._client = client
|
|
727
747
|
|
|
748
|
+
@cached_property
|
|
749
|
+
def beta(self) -> beta.AsyncBetaResourceWithRawResponse:
|
|
750
|
+
from .resources.beta import AsyncBetaResourceWithRawResponse
|
|
751
|
+
|
|
752
|
+
return AsyncBetaResourceWithRawResponse(self._client.beta)
|
|
753
|
+
|
|
728
754
|
@cached_property
|
|
729
755
|
def chat(self) -> chat.AsyncChatResourceWithRawResponse:
|
|
730
756
|
from .resources.chat import AsyncChatResourceWithRawResponse
|
|
@@ -828,6 +854,12 @@ class TogetherWithStreamedResponse:
|
|
|
828
854
|
def __init__(self, client: Together) -> None:
|
|
829
855
|
self._client = client
|
|
830
856
|
|
|
857
|
+
@cached_property
|
|
858
|
+
def beta(self) -> beta.BetaResourceWithStreamingResponse:
|
|
859
|
+
from .resources.beta import BetaResourceWithStreamingResponse
|
|
860
|
+
|
|
861
|
+
return BetaResourceWithStreamingResponse(self._client.beta)
|
|
862
|
+
|
|
831
863
|
@cached_property
|
|
832
864
|
def chat(self) -> chat.ChatResourceWithStreamingResponse:
|
|
833
865
|
from .resources.chat import ChatResourceWithStreamingResponse
|
|
@@ -931,6 +963,12 @@ class AsyncTogetherWithStreamedResponse:
|
|
|
931
963
|
def __init__(self, client: AsyncTogether) -> None:
|
|
932
964
|
self._client = client
|
|
933
965
|
|
|
966
|
+
@cached_property
|
|
967
|
+
def beta(self) -> beta.AsyncBetaResourceWithStreamingResponse:
|
|
968
|
+
from .resources.beta import AsyncBetaResourceWithStreamingResponse
|
|
969
|
+
|
|
970
|
+
return AsyncBetaResourceWithStreamingResponse(self._client.beta)
|
|
971
|
+
|
|
934
972
|
@cached_property
|
|
935
973
|
def chat(self) -> chat.AsyncChatResourceWithStreamingResponse:
|
|
936
974
|
from .resources.chat import AsyncChatResourceWithStreamingResponse
|
together/_version.py
CHANGED
together/constants.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Manually added to minimize breaking changes from V1
|
|
2
|
+
from ._constants import (
|
|
3
|
+
MAX_RETRY_DELAY as MAX_RETRY_DELAY,
|
|
4
|
+
DEFAULT_MAX_RETRIES,
|
|
5
|
+
INITIAL_RETRY_DELAY as INITIAL_RETRY_DELAY,
|
|
6
|
+
)
|
|
7
|
+
from .lib.constants import (
|
|
8
|
+
MIN_SAMPLES as MIN_SAMPLES,
|
|
9
|
+
DISABLE_TQDM as DISABLE_TQDM,
|
|
10
|
+
MAX_IMAGE_BYTES as MAX_IMAGE_BYTES,
|
|
11
|
+
NUM_BYTES_IN_GB as NUM_BYTES_IN_GB,
|
|
12
|
+
MAX_FILE_SIZE_GB as MAX_FILE_SIZE_GB,
|
|
13
|
+
MIN_PART_SIZE_MB as MIN_PART_SIZE_MB,
|
|
14
|
+
DOWNLOAD_BLOCK_SIZE as DOWNLOAD_BLOCK_SIZE,
|
|
15
|
+
MAX_MULTIPART_PARTS as MAX_MULTIPART_PARTS,
|
|
16
|
+
TARGET_PART_SIZE_MB as TARGET_PART_SIZE_MB,
|
|
17
|
+
MAX_CONCURRENT_PARTS as MAX_CONCURRENT_PARTS,
|
|
18
|
+
MAX_IMAGES_PER_EXAMPLE as MAX_IMAGES_PER_EXAMPLE,
|
|
19
|
+
MULTIPART_THRESHOLD_GB as MULTIPART_THRESHOLD_GB,
|
|
20
|
+
MAX_BASE64_IMAGE_LENGTH as MAX_BASE64_IMAGE_LENGTH,
|
|
21
|
+
MULTIPART_UPLOAD_TIMEOUT as MULTIPART_UPLOAD_TIMEOUT,
|
|
22
|
+
PARQUET_EXPECTED_COLUMNS as PARQUET_EXPECTED_COLUMNS,
|
|
23
|
+
REQUIRED_COLUMNS_MESSAGE as REQUIRED_COLUMNS_MESSAGE,
|
|
24
|
+
JSONL_REQUIRED_COLUMNS_MAP as JSONL_REQUIRED_COLUMNS_MAP,
|
|
25
|
+
POSSIBLE_ROLES_CONVERSATION as POSSIBLE_ROLES_CONVERSATION,
|
|
26
|
+
DatasetFormat as DatasetFormat,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
TIMEOUT_SECS = 600
|
|
30
|
+
MAX_SESSION_LIFETIME_SECS = 180
|
|
31
|
+
MAX_CONNECTION_RETRIES = 2
|
|
32
|
+
MAX_RETRIES = DEFAULT_MAX_RETRIES
|
|
33
|
+
|
|
34
|
+
BASE_URL = "https://api.together.xyz/v1"
|
together/error.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
|
|
2
|
+
# Manually added to minimize breaking changes from V1
|
|
3
|
+
from ._exceptions import (
|
|
4
|
+
APIError as APIError,
|
|
5
|
+
RateLimitError as RateLimitError,
|
|
6
|
+
APITimeoutError,
|
|
7
|
+
BadRequestError,
|
|
8
|
+
APIConnectionError as APIConnectionError,
|
|
9
|
+
AuthenticationError as AuthenticationError,
|
|
10
|
+
APIResponseValidationError,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
Timeout = APITimeoutError
|
|
14
|
+
InvalidRequestError = BadRequestError
|
|
15
|
+
TogetherException = APIError
|
|
16
|
+
ResponseError = APIResponseValidationError
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json as json_lib
|
|
4
|
+
import getpass
|
|
5
|
+
from typing import Any, Dict, List, Literal
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
from rich import print
|
|
9
|
+
from tabulate import tabulate
|
|
10
|
+
|
|
11
|
+
from together import Together, omit
|
|
12
|
+
from together._response import APIResponse as APIResponse
|
|
13
|
+
from together.types.beta import Cluster, ClusterCreateParams
|
|
14
|
+
from together.lib.cli.api.utils import handle_api_errors
|
|
15
|
+
from together.types.beta.cluster_create_params import SharedVolume
|
|
16
|
+
from together.lib.cli.api.beta.clusters_storage import storage
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def print_clusters(clusters: List[Cluster]) -> None:
|
|
20
|
+
data: List[Dict[str, Any]] = []
|
|
21
|
+
for cluster in clusters:
|
|
22
|
+
data.append(
|
|
23
|
+
{
|
|
24
|
+
"ID": cluster.cluster_id,
|
|
25
|
+
"Name": cluster.cluster_name,
|
|
26
|
+
"Status": cluster.status,
|
|
27
|
+
"Region": cluster.region,
|
|
28
|
+
}
|
|
29
|
+
)
|
|
30
|
+
click.echo(tabulate(data, headers="keys", tablefmt="grid"))
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@click.group()
|
|
34
|
+
@click.pass_context
|
|
35
|
+
def clusters(ctx: click.Context) -> None:
|
|
36
|
+
"""Clusters API commands"""
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
clusters.add_command(storage)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@clusters.command()
|
|
44
|
+
@click.option(
|
|
45
|
+
"--json",
|
|
46
|
+
is_flag=True,
|
|
47
|
+
help="Output in JSON format",
|
|
48
|
+
)
|
|
49
|
+
@click.pass_context
|
|
50
|
+
def list(ctx: click.Context, json: bool) -> None:
|
|
51
|
+
"""List clusters"""
|
|
52
|
+
client: Together = ctx.obj
|
|
53
|
+
|
|
54
|
+
response = client.beta.clusters.list()
|
|
55
|
+
|
|
56
|
+
if json:
|
|
57
|
+
click.echo(json_lib.dumps(response.model_dump(exclude_none=True), indent=4))
|
|
58
|
+
else:
|
|
59
|
+
print_clusters(response.clusters)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@clusters.command()
|
|
63
|
+
@click.option(
|
|
64
|
+
"--name",
|
|
65
|
+
type=str,
|
|
66
|
+
help="Name of the cluster",
|
|
67
|
+
)
|
|
68
|
+
@click.option(
|
|
69
|
+
"--num-gpus",
|
|
70
|
+
type=int,
|
|
71
|
+
help="Number of GPUs to allocate in the cluster",
|
|
72
|
+
)
|
|
73
|
+
@click.option(
|
|
74
|
+
"--region",
|
|
75
|
+
type=str,
|
|
76
|
+
help="Region to create the cluster in",
|
|
77
|
+
)
|
|
78
|
+
@click.option(
|
|
79
|
+
"--billing-type",
|
|
80
|
+
type=str,
|
|
81
|
+
help="Billing type to use for the cluster",
|
|
82
|
+
)
|
|
83
|
+
@click.option(
|
|
84
|
+
"--driver-version",
|
|
85
|
+
type=str,
|
|
86
|
+
help="Driver version to use for the cluster",
|
|
87
|
+
)
|
|
88
|
+
@click.option(
|
|
89
|
+
"--duration-days",
|
|
90
|
+
type=int,
|
|
91
|
+
help="Duration in days to keep the cluster running for reserved clusters",
|
|
92
|
+
)
|
|
93
|
+
@click.option(
|
|
94
|
+
"--gpu-type",
|
|
95
|
+
type=str,
|
|
96
|
+
help="GPU type to use for the cluster. Find available gpu types for each region with the `list-regions` command.",
|
|
97
|
+
)
|
|
98
|
+
@click.option("--cluster-type", type=click.Choice(["KUBERNETES", "SLURM"]), help="Cluster type")
|
|
99
|
+
@click.option(
|
|
100
|
+
"--volume",
|
|
101
|
+
type=str,
|
|
102
|
+
help="Storage volume ID to use for the cluster",
|
|
103
|
+
)
|
|
104
|
+
@click.option(
|
|
105
|
+
"--json",
|
|
106
|
+
is_flag=True,
|
|
107
|
+
help="Output in JSON format",
|
|
108
|
+
)
|
|
109
|
+
@click.pass_context
|
|
110
|
+
@handle_api_errors("Clusters")
|
|
111
|
+
def create(
|
|
112
|
+
ctx: click.Context,
|
|
113
|
+
name: str | None = None,
|
|
114
|
+
num_gpus: int | None = None,
|
|
115
|
+
region: str | None = None,
|
|
116
|
+
billing_type: Literal["RESERVED", "ON_DEMAND"] | None = None,
|
|
117
|
+
driver_version: str | None = None,
|
|
118
|
+
duration_days: int | None = None,
|
|
119
|
+
gpu_type: str | None = None,
|
|
120
|
+
cluster_type: Literal["KUBERNETES", "SLURM"] | None = None,
|
|
121
|
+
volume: str | None = None,
|
|
122
|
+
json: bool = False,
|
|
123
|
+
) -> None:
|
|
124
|
+
"""Create a cluster"""
|
|
125
|
+
client: Together = ctx.obj
|
|
126
|
+
|
|
127
|
+
params = ClusterCreateParams(
|
|
128
|
+
cluster_name=name, # type: ignore
|
|
129
|
+
num_gpus=num_gpus, # type: ignore
|
|
130
|
+
region=region, # type: ignore
|
|
131
|
+
billing_type=billing_type, # type: ignore
|
|
132
|
+
driver_version=driver_version, # type: ignore
|
|
133
|
+
duration_days=duration_days, # type: ignore
|
|
134
|
+
gpu_type=gpu_type, # type: ignore
|
|
135
|
+
cluster_type=cluster_type, # type: ignore
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
# Lazily add this so its not put in the object as None - just looks bad aesthetically
|
|
139
|
+
if volume:
|
|
140
|
+
params["volume_id"] = volume
|
|
141
|
+
|
|
142
|
+
# JSON Mode skips hand holding through the argument setup
|
|
143
|
+
if not json:
|
|
144
|
+
if not name:
|
|
145
|
+
params["cluster_name"] = click.prompt("Clusters: Cluster name:", default=getpass.getuser(), type=str)
|
|
146
|
+
|
|
147
|
+
# TODO
|
|
148
|
+
# GPU should be queried first
|
|
149
|
+
# Validate region has the gpu selected.
|
|
150
|
+
|
|
151
|
+
if not gpu_type:
|
|
152
|
+
# TODO: Pull GPUS from region list and the region selected.
|
|
153
|
+
# TODO: Add instance_types to region list api
|
|
154
|
+
params["gpu_type"] = click.prompt(
|
|
155
|
+
"Clusters: Cluster GPU type:",
|
|
156
|
+
type=click.Choice(["H100_SXM", "H200_SXM", "RTX_6000_PCI", "L40_PCIE", "B200_SXM", "H100_SXM_INF"]),
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
if not region:
|
|
160
|
+
regions = client.beta.clusters.list_regions()
|
|
161
|
+
params["region"] = click.prompt(
|
|
162
|
+
"Clusters: Cluster region:",
|
|
163
|
+
default=regions.regions[0].name,
|
|
164
|
+
type=click.Choice([region.name for region in regions.regions]),
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
if num_gpus is None:
|
|
168
|
+
params["num_gpus"] = click.prompt("Clusters: Cluster GPUs count", type=click.IntRange(min=8, max=64))
|
|
169
|
+
|
|
170
|
+
if not billing_type:
|
|
171
|
+
params["billing_type"] = click.prompt(
|
|
172
|
+
"Clusters: Cluster billing type:", default="ON_DEMAND", type=click.Choice(["RESERVED", "ON_DEMAND"])
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
if not driver_version:
|
|
176
|
+
regions = client.beta.clusters.list_regions()
|
|
177
|
+
|
|
178
|
+
# Get the driver versions for the selected region
|
|
179
|
+
driver_versions: List[str] = []
|
|
180
|
+
for region_obj in regions.regions:
|
|
181
|
+
if region_obj.name == params["region"]:
|
|
182
|
+
driver_versions.extend(region_obj.driver_versions)
|
|
183
|
+
|
|
184
|
+
params["driver_version"] = click.prompt(
|
|
185
|
+
"Clusters: Cluster driver version:", default="CUDA_12_5_555", type=click.Choice(driver_versions)
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
if not duration_days and params["billing_type"] == "RESERVED":
|
|
189
|
+
params["duration_days"] = click.prompt("Clusters: Cluster reserved duration (1-90 days):", default=3)
|
|
190
|
+
|
|
191
|
+
if not cluster_type:
|
|
192
|
+
params["cluster_type"] = click.prompt(
|
|
193
|
+
"Clusters: Cluster type:", default="KUBERNETES", type=click.Choice(["KUBERNETES", "SLURM"])
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
# In our QA environment, we don't accept storage volume creation, so we skip the prompt
|
|
197
|
+
if not volume and "qa" not in client.base_url.host:
|
|
198
|
+
if click.confirm("Clusters: Create a new storage volume?"):
|
|
199
|
+
default_volume_name = f"{params['cluster_name']}-storage"
|
|
200
|
+
params["shared_volume"] = SharedVolume(
|
|
201
|
+
region=f"{params['region']}",
|
|
202
|
+
size_tib=1,
|
|
203
|
+
volume_name=default_volume_name,
|
|
204
|
+
)
|
|
205
|
+
params["shared_volume"]["volume_name"] = click.prompt(
|
|
206
|
+
"Clusters: Storage volume name:", default=default_volume_name, type=str
|
|
207
|
+
)
|
|
208
|
+
params["shared_volume"]["size_tib"] = click.prompt(
|
|
209
|
+
"Clusters: Storage volume size (TiB):", default=1, type=click.IntRange(min=1, max=1024)
|
|
210
|
+
)
|
|
211
|
+
else:
|
|
212
|
+
# TODO: We need bound status and region on the volume list from the API.
|
|
213
|
+
# Only show volumes in the region selected and that are not attached to a cluster.
|
|
214
|
+
volumes = client.beta.clusters.storage.list()
|
|
215
|
+
params["volume_id"] = click.prompt(
|
|
216
|
+
"Clusters: Which storage volume to use?",
|
|
217
|
+
default=volumes.volumes[0].volume_id,
|
|
218
|
+
type=click.Choice([volume.volume_id for volume in volumes.volumes]),
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
click.echo("Clusters: Creating cluster with the following parameters:")
|
|
222
|
+
print(ClusterCreateParams(**params)) # type: ignore
|
|
223
|
+
|
|
224
|
+
response = client.beta.clusters.create(**params)
|
|
225
|
+
|
|
226
|
+
if json:
|
|
227
|
+
click.echo(json_lib.dumps(response.model_dump(exclude_none=True), indent=4))
|
|
228
|
+
else:
|
|
229
|
+
click.echo(f"Clusters: Cluster created successfully")
|
|
230
|
+
click.echo(f"Clusters: {response.cluster_id}")
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
@clusters.command()
|
|
234
|
+
@click.argument("cluster-id", required=True)
|
|
235
|
+
@click.option(
|
|
236
|
+
"--json",
|
|
237
|
+
is_flag=True,
|
|
238
|
+
help="Output in JSON format",
|
|
239
|
+
)
|
|
240
|
+
@click.pass_context
|
|
241
|
+
@handle_api_errors("Clusters")
|
|
242
|
+
def retrieve(ctx: click.Context, cluster_id: str, json: bool) -> None:
|
|
243
|
+
"""Retrieve a cluster by ID"""
|
|
244
|
+
client: Together = ctx.obj
|
|
245
|
+
|
|
246
|
+
if not json:
|
|
247
|
+
click.echo(f"Clusters: Retrieving cluster...")
|
|
248
|
+
|
|
249
|
+
response = client.beta.clusters.retrieve(cluster_id)
|
|
250
|
+
|
|
251
|
+
if json:
|
|
252
|
+
click.echo(json_lib.dumps(response.model_dump(exclude_none=True), indent=4))
|
|
253
|
+
else:
|
|
254
|
+
print(response)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
@clusters.command()
|
|
258
|
+
@click.argument("cluster-id", required=True)
|
|
259
|
+
@click.option(
|
|
260
|
+
"--num-gpus",
|
|
261
|
+
type=int,
|
|
262
|
+
help="Number of GPUs to allocate in the cluster",
|
|
263
|
+
)
|
|
264
|
+
@click.option(
|
|
265
|
+
"--cluster-type",
|
|
266
|
+
type=click.Choice(["KUBERNETES", "SLURM"]),
|
|
267
|
+
help="Cluster type",
|
|
268
|
+
)
|
|
269
|
+
@click.option(
|
|
270
|
+
"--json",
|
|
271
|
+
is_flag=True,
|
|
272
|
+
help="Output in JSON format",
|
|
273
|
+
)
|
|
274
|
+
@click.pass_context
|
|
275
|
+
@handle_api_errors("Clusters")
|
|
276
|
+
def update(
|
|
277
|
+
ctx: click.Context,
|
|
278
|
+
cluster_id: str,
|
|
279
|
+
num_gpus: int | None = None,
|
|
280
|
+
cluster_type: Literal["KUBERNETES", "SLURM"] | None = None,
|
|
281
|
+
json: bool = False,
|
|
282
|
+
) -> None:
|
|
283
|
+
"""Update a cluster"""
|
|
284
|
+
client: Together = ctx.obj
|
|
285
|
+
|
|
286
|
+
if not json:
|
|
287
|
+
click.echo("Clusters: Updating cluster...")
|
|
288
|
+
|
|
289
|
+
client.beta.clusters.update(
|
|
290
|
+
cluster_id,
|
|
291
|
+
num_gpus=num_gpus if num_gpus is not None else omit,
|
|
292
|
+
cluster_type=cluster_type if cluster_type is not None else omit,
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
if json:
|
|
296
|
+
cluster = client.beta.clusters.retrieve(cluster_id)
|
|
297
|
+
click.echo(json_lib.dumps(cluster.model_dump(exclude_none=True), indent=4))
|
|
298
|
+
else:
|
|
299
|
+
click.echo("Clusters: Done")
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
@clusters.command()
|
|
303
|
+
@click.argument("cluster-id", required=True)
|
|
304
|
+
@click.option(
|
|
305
|
+
"--json",
|
|
306
|
+
is_flag=True,
|
|
307
|
+
help="Output in JSON format",
|
|
308
|
+
)
|
|
309
|
+
@click.pass_context
|
|
310
|
+
@handle_api_errors("Clusters")
|
|
311
|
+
def delete(ctx: click.Context, cluster_id: str, json: bool) -> None:
|
|
312
|
+
"""Delete a cluster by ID"""
|
|
313
|
+
client: Together = ctx.obj
|
|
314
|
+
|
|
315
|
+
if json:
|
|
316
|
+
response = client.beta.clusters.delete(cluster_id=cluster_id)
|
|
317
|
+
click.echo(json_lib.dumps(response.model_dump(), indent=2))
|
|
318
|
+
return
|
|
319
|
+
|
|
320
|
+
cluster = client.beta.clusters.retrieve(cluster_id=cluster_id)
|
|
321
|
+
print_clusters([cluster])
|
|
322
|
+
if not click.confirm(f"Clusters: Are you sure you want to delete cluster {cluster.cluster_name}?"):
|
|
323
|
+
return
|
|
324
|
+
|
|
325
|
+
click.echo("Clusters: Deleting cluster...")
|
|
326
|
+
response = client.beta.clusters.delete(cluster_id=cluster_id)
|
|
327
|
+
|
|
328
|
+
click.echo(f"Clusters: Deleted cluster {cluster.cluster_name}")
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
@clusters.command()
|
|
332
|
+
@click.option(
|
|
333
|
+
"--json",
|
|
334
|
+
is_flag=True,
|
|
335
|
+
help="Output in JSON format",
|
|
336
|
+
)
|
|
337
|
+
@click.pass_context
|
|
338
|
+
@handle_api_errors("Clusters")
|
|
339
|
+
def list_regions(ctx: click.Context, json: bool) -> None:
|
|
340
|
+
"""List regions"""
|
|
341
|
+
client: Together = ctx.obj
|
|
342
|
+
|
|
343
|
+
response = client.beta.clusters.list_regions()
|
|
344
|
+
|
|
345
|
+
if json:
|
|
346
|
+
click.echo(json_lib.dumps(response.model_dump(exclude_none=True), indent=4))
|
|
347
|
+
else:
|
|
348
|
+
data: List[Dict[str, Any]] = []
|
|
349
|
+
for region in response.regions:
|
|
350
|
+
data.append(
|
|
351
|
+
{
|
|
352
|
+
"Name": region.name,
|
|
353
|
+
"Availability Zones": ", ".join(region.availability_zones) if region.availability_zones else "",
|
|
354
|
+
"Driver Versions": ", ".join(region.driver_versions) if region.driver_versions else "",
|
|
355
|
+
}
|
|
356
|
+
)
|
|
357
|
+
click.echo(tabulate(data, headers="keys", tablefmt="grid"))
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import json as json_lib
|
|
2
|
+
from typing import Any, Dict, List
|
|
3
|
+
|
|
4
|
+
import click
|
|
5
|
+
from rich import print
|
|
6
|
+
from tabulate import tabulate
|
|
7
|
+
|
|
8
|
+
from together import Together
|
|
9
|
+
from together.lib.cli.api.utils import handle_api_errors
|
|
10
|
+
from together.types.beta.clusters import ClusterStorage
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def print_storage(storage: List[ClusterStorage]) -> None:
|
|
14
|
+
data: List[Dict[str, Any]] = []
|
|
15
|
+
for volume in storage:
|
|
16
|
+
data.append(
|
|
17
|
+
{
|
|
18
|
+
"ID": volume.volume_id,
|
|
19
|
+
"Name": volume.volume_name,
|
|
20
|
+
"Size": volume.size_tib,
|
|
21
|
+
}
|
|
22
|
+
)
|
|
23
|
+
click.echo(tabulate(data, headers="keys", tablefmt="grid"))
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@click.group()
|
|
27
|
+
@click.pass_context
|
|
28
|
+
def storage(ctx: click.Context) -> None:
|
|
29
|
+
"""Clusters Storage API commands"""
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@storage.command()
|
|
34
|
+
@click.option(
|
|
35
|
+
"--region",
|
|
36
|
+
required=True,
|
|
37
|
+
type=str,
|
|
38
|
+
help="Region to create the storage volume in",
|
|
39
|
+
)
|
|
40
|
+
@click.option(
|
|
41
|
+
"--size-tib",
|
|
42
|
+
required=True,
|
|
43
|
+
type=int,
|
|
44
|
+
help="Size of the storage volume in TiB",
|
|
45
|
+
)
|
|
46
|
+
@click.option(
|
|
47
|
+
"--volume-name",
|
|
48
|
+
required=True,
|
|
49
|
+
type=str,
|
|
50
|
+
help="Name of the storage volume",
|
|
51
|
+
)
|
|
52
|
+
@click.option(
|
|
53
|
+
"--json",
|
|
54
|
+
is_flag=True,
|
|
55
|
+
help="Output in JSON format",
|
|
56
|
+
)
|
|
57
|
+
@click.pass_context
|
|
58
|
+
@handle_api_errors("Clusters Storage")
|
|
59
|
+
def create(ctx: click.Context, region: str, size_tib: int, volume_name: str, json: bool) -> None:
|
|
60
|
+
"""Create a storage volume"""
|
|
61
|
+
client: Together = ctx.obj
|
|
62
|
+
|
|
63
|
+
response = client.beta.clusters.storage.create(
|
|
64
|
+
region=region,
|
|
65
|
+
size_tib=size_tib,
|
|
66
|
+
volume_name=volume_name,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
if json:
|
|
70
|
+
click.echo(json_lib.dumps(response.model_dump_json(), indent=2))
|
|
71
|
+
else:
|
|
72
|
+
click.echo(f"Storage volume created successfully")
|
|
73
|
+
click.echo(response.volume_id)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@storage.command()
|
|
77
|
+
@click.argument(
|
|
78
|
+
"volume-id",
|
|
79
|
+
required=True,
|
|
80
|
+
)
|
|
81
|
+
@click.option(
|
|
82
|
+
"--json",
|
|
83
|
+
is_flag=True,
|
|
84
|
+
help="Output in JSON format",
|
|
85
|
+
)
|
|
86
|
+
@click.pass_context
|
|
87
|
+
@handle_api_errors("Clusters Storage")
|
|
88
|
+
def retrieve(ctx: click.Context, volume_id: str, json: bool) -> None:
|
|
89
|
+
"""Retrieve a storage volume"""
|
|
90
|
+
client: Together = ctx.obj
|
|
91
|
+
|
|
92
|
+
if not json:
|
|
93
|
+
click.echo(f"Clusters Storage: Retrieving storage volume...")
|
|
94
|
+
|
|
95
|
+
response = client.beta.clusters.storage.retrieve(volume_id)
|
|
96
|
+
|
|
97
|
+
if json:
|
|
98
|
+
click.echo(json_lib.dumps(response.model_dump(), indent=2))
|
|
99
|
+
else:
|
|
100
|
+
print(response)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@storage.command()
|
|
104
|
+
@click.argument(
|
|
105
|
+
"volume-id",
|
|
106
|
+
required=True,
|
|
107
|
+
)
|
|
108
|
+
@click.option(
|
|
109
|
+
"--json",
|
|
110
|
+
is_flag=True,
|
|
111
|
+
help="Output in JSON format",
|
|
112
|
+
)
|
|
113
|
+
@click.pass_context
|
|
114
|
+
@handle_api_errors("Clusters Storage")
|
|
115
|
+
def delete(ctx: click.Context, volume_id: str, json: bool) -> None:
|
|
116
|
+
"""Delete a storage volume"""
|
|
117
|
+
client: Together = ctx.obj
|
|
118
|
+
|
|
119
|
+
if json:
|
|
120
|
+
response = client.beta.clusters.storage.delete(volume_id)
|
|
121
|
+
click.echo(json_lib.dumps(response.model_dump(), indent=2))
|
|
122
|
+
return
|
|
123
|
+
|
|
124
|
+
storage = client.beta.clusters.storage.retrieve(volume_id)
|
|
125
|
+
print_storage([storage])
|
|
126
|
+
if not click.confirm(f"Clusters Storage: Are you sure you want to delete storage volume {storage.volume_name}?"):
|
|
127
|
+
return
|
|
128
|
+
|
|
129
|
+
click.echo("Clusters Storage: Deleting storage volume...")
|
|
130
|
+
response = client.beta.clusters.storage.delete(volume_id)
|
|
131
|
+
|
|
132
|
+
click.echo(f"Clusters Storage: Deleted storage volume {storage.volume_name}")
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@storage.command()
|
|
136
|
+
@click.option(
|
|
137
|
+
"--json",
|
|
138
|
+
is_flag=True,
|
|
139
|
+
help="Output in JSON format",
|
|
140
|
+
)
|
|
141
|
+
@click.pass_context
|
|
142
|
+
@handle_api_errors("Clusters Storage")
|
|
143
|
+
def list(ctx: click.Context, json: bool) -> None:
|
|
144
|
+
"""List storage volumes"""
|
|
145
|
+
client: Together = ctx.obj
|
|
146
|
+
|
|
147
|
+
response = client.beta.clusters.storage.list()
|
|
148
|
+
|
|
149
|
+
if json:
|
|
150
|
+
click.echo(json_lib.dumps(response.model_dump(), indent=2))
|
|
151
|
+
else:
|
|
152
|
+
print_storage(response.volumes)
|