together 2.0.0a12__py3-none-any.whl → 2.0.0a14__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/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 +37 -1
- together/lib/cli/cli.py +2 -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/resources/chat/completions.py +20 -0
- 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/completion_create_params.py +4 -0
- {together-2.0.0a12.dist-info → together-2.0.0a14.dist-info}/METADATA +14 -8
- {together-2.0.0a12.dist-info → together-2.0.0a14.dist-info}/RECORD +36 -12
- {together-2.0.0a12.dist-info → together-2.0.0a14.dist-info}/WHEEL +0 -0
- {together-2.0.0a12.dist-info → together-2.0.0a14.dist-info}/entry_points.txt +0 -0
- {together-2.0.0a12.dist-info → together-2.0.0a14.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
|
@@ -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)
|
together/lib/cli/api/utils.py
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import re
|
|
4
|
+
import sys
|
|
4
5
|
import math
|
|
5
|
-
from typing import List, Union, Literal
|
|
6
|
+
from typing import Any, List, Union, Literal, TypeVar, Callable
|
|
6
7
|
from gettext import gettext as _
|
|
7
8
|
from datetime import datetime
|
|
9
|
+
from functools import wraps
|
|
8
10
|
|
|
9
11
|
import click
|
|
10
12
|
|
|
13
|
+
from together import APIError
|
|
11
14
|
from together.lib.types.fine_tuning import COMPLETED_STATUSES, FinetuneResponse
|
|
12
15
|
from together.types.finetune_response import FinetuneResponse as _FinetuneResponse
|
|
13
16
|
from together.types.fine_tuning_list_response import Data
|
|
@@ -129,3 +132,36 @@ def generate_progress_bar(
|
|
|
129
132
|
return progress
|
|
130
133
|
|
|
131
134
|
return re.sub(r"\[/?[^\]]+\]", "", progress)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
F = TypeVar("F", bound=Callable[..., Any])
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def handle_api_errors(prefix: str) -> Callable[[F], F]:
|
|
141
|
+
"""Decorator to handle common API errors in CLI commands."""
|
|
142
|
+
|
|
143
|
+
prefix_styled = click.style(f"{prefix}: ", fg="blue")
|
|
144
|
+
|
|
145
|
+
def decorator(f: F) -> F:
|
|
146
|
+
@wraps(f)
|
|
147
|
+
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
148
|
+
try:
|
|
149
|
+
return f(*args, **kwargs)
|
|
150
|
+
# User aborted the command
|
|
151
|
+
except click.Abort:
|
|
152
|
+
sys.exit(0)
|
|
153
|
+
except APIError as e:
|
|
154
|
+
click.echo(prefix_styled + click.style("Failed", fg="red"))
|
|
155
|
+
if e.body is not None:
|
|
156
|
+
click.echo(prefix_styled + click.style(getattr(e.body, "message", str(e.body)), fg="red"))
|
|
157
|
+
else:
|
|
158
|
+
click.echo(prefix_styled + click.style(str(e), fg="red"))
|
|
159
|
+
sys.exit(1)
|
|
160
|
+
except Exception as e:
|
|
161
|
+
click.echo(prefix_styled + click.style("Failed", fg="red"))
|
|
162
|
+
click.echo(prefix_styled + click.style(f"An unexpected error occurred - {str(e)}", fg="red"))
|
|
163
|
+
sys.exit(1)
|
|
164
|
+
|
|
165
|
+
return wrapper # type: ignore
|
|
166
|
+
|
|
167
|
+
return decorator # type: ignore
|
together/lib/cli/cli.py
CHANGED
|
@@ -11,6 +11,7 @@ from together._constants import DEFAULT_TIMEOUT
|
|
|
11
11
|
from together.lib.cli.api.evals import evals
|
|
12
12
|
from together.lib.cli.api.files import files
|
|
13
13
|
from together.lib.cli.api.models import models
|
|
14
|
+
from together.lib.cli.api.beta.beta import beta
|
|
14
15
|
from together.lib.cli.api.endpoints import endpoints
|
|
15
16
|
from together.lib.cli.api.fine_tuning import fine_tuning
|
|
16
17
|
|
|
@@ -66,6 +67,7 @@ main.add_command(fine_tuning)
|
|
|
66
67
|
main.add_command(models)
|
|
67
68
|
main.add_command(endpoints)
|
|
68
69
|
main.add_command(evals)
|
|
70
|
+
main.add_command(beta)
|
|
69
71
|
|
|
70
72
|
if __name__ == "__main__":
|
|
71
73
|
main()
|