From 60e73b7aa343716caf4565aacaa93039f9ce099a Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Thu, 14 May 2026 19:14:04 +0000
Subject: [PATCH 01/14] feat(api): add cluster config/OIDC/add-ons params,
project filtering, update storage types
---
.stats.yml | 4 +-
api.md | 4 +-
.../resources/beta/clusters/clusters.py | 194 +++++++++-
.../resources/beta/clusters/storage.py | 78 ++--
src/together/types/beta/__init__.py | 1 +
src/together/types/beta/cluster.py | 349 +++++++++++++++++-
.../types/beta/cluster_create_params.py | 224 ++++++++++-
.../types/beta/cluster_list_params.py | 16 +
.../types/beta/cluster_update_params.py | 122 +++++-
src/together/types/beta/clusters/__init__.py | 1 +
.../types/beta/clusters/cluster_storage.py | 8 +-
.../beta/clusters/storage_create_params.py | 5 +-
.../beta/clusters/storage_list_params.py | 16 +
.../beta/clusters/storage_update_params.py | 8 +-
.../beta/clusters/test_storage.py | 64 +++-
tests/api_resources/beta/test_clusters.py | 182 ++++++++-
16 files changed, 1188 insertions(+), 88 deletions(-)
create mode 100644 src/together/types/beta/cluster_list_params.py
create mode 100644 src/together/types/beta/clusters/storage_list_params.py
diff --git a/.stats.yml b/.stats.yml
index 498815517..409398869 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 75
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/togetherai/togetherai-5f05c9669c67c3f4b0ebfe2317d2768cd96317424965ebb2acf06a7757a7d0ca.yml
-openapi_spec_hash: 84f45151f4d0eed68551b5ffda61595a
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/togetherai/togetherai-e398785d31ad51e94f158b377be469e74748e0229a964d3602149c6bf964581e.yml
+openapi_spec_hash: 3e4db6f27d100314926d87c9ae24cba2
config_hash: ec427df08d61d8888138f15cd53c6454
diff --git a/api.md b/api.md
index 1ea19fb05..c32e307a7 100644
--- a/api.md
+++ b/api.md
@@ -87,7 +87,7 @@ Methods:
- client.beta.clusters.create(\*\*params) -> Cluster
- client.beta.clusters.retrieve(cluster_id) -> Cluster
- client.beta.clusters.update(cluster_id, \*\*params) -> Cluster
-- client.beta.clusters.list() -> ClusterListResponse
+- client.beta.clusters.list(\*\*params) -> ClusterListResponse
- client.beta.clusters.delete(cluster_id) -> ClusterDeleteResponse
- client.beta.clusters.list_regions() -> ClusterListRegionsResponse
@@ -104,7 +104,7 @@ Methods:
- client.beta.clusters.storage.create(\*\*params) -> ClusterStorage
- client.beta.clusters.storage.retrieve(volume_id) -> ClusterStorage
- client.beta.clusters.storage.update(\*\*params) -> ClusterStorage
-- client.beta.clusters.storage.list() -> StorageListResponse
+- client.beta.clusters.storage.list(\*\*params) -> StorageListResponse
- client.beta.clusters.storage.delete(volume_id) -> StorageDeleteResponse
# Chat
diff --git a/src/together/resources/beta/clusters/clusters.py b/src/together/resources/beta/clusters/clusters.py
index 5014087ac..935d71153 100644
--- a/src/together/resources/beta/clusters/clusters.py
+++ b/src/together/resources/beta/clusters/clusters.py
@@ -2,7 +2,7 @@
from __future__ import annotations
-from typing import Union
+from typing import Union, Iterable
from datetime import datetime
from typing_extensions import Literal
@@ -26,7 +26,7 @@
async_to_raw_response_wrapper,
async_to_streamed_response_wrapper,
)
-from ....types.beta import cluster_create_params, cluster_update_params
+from ....types.beta import cluster_list_params, cluster_create_params, cluster_update_params
from ...._base_client import make_request_options
from ....types.beta.cluster import Cluster
from ....types.beta.cluster_list_response import ClusterListResponse
@@ -66,17 +66,26 @@ def create(
billing_type: Literal["RESERVED", "ON_DEMAND", "SCHEDULED_CAPACITY"],
cluster_name: str,
cuda_version: str,
+ duration_days: int,
gpu_type: Literal["H100_SXM", "H200_SXM", "RTX_6000_PCI", "L40_PCIE", "B200_SXM", "H100_SXM_INF"],
num_gpus: int,
nvidia_driver_version: str,
region: str,
+ acceptance_tests_params: cluster_create_params.AcceptanceTestsParams | Omit = omit,
+ add_ons: Iterable[cluster_create_params.AddOn] | Omit = omit,
+ auto_scale: bool | Omit = omit,
auto_scale_max_gpus: int | Omit = omit,
auto_scaled: bool | Omit = omit,
capacity_pool_id: str | Omit = omit,
+ cluster_config: cluster_create_params.ClusterConfig | Omit = omit,
cluster_type: Literal["KUBERNETES", "SLURM"] | Omit = omit,
- duration_days: int | Omit = omit,
gpu_node_failover_enabled: bool | Omit = omit,
install_traefik: bool | Omit = omit,
+ num_capacity_pool_gpus: int | Omit = omit,
+ num_preemptible_gpus: int | Omit = omit,
+ num_reserved_gpus: int | Omit = omit,
+ oidc_config: cluster_create_params.OidcConfig | Omit = omit,
+ project_id: str | Omit = omit,
reservation_end_time: Union[str, datetime] | Omit = omit,
reservation_start_time: Union[str, datetime] | Omit = omit,
shared_volume: cluster_create_params.SharedVolume | Omit = omit,
@@ -109,6 +118,8 @@ def create(
cuda_version: CUDA version for this cluster. For example, 12.5
+ duration_days: Duration in days to keep the cluster running.
+
gpu_type: Type of GPU to use in the cluster
num_gpus: Number of GPUs to allocate in the cluster. This must be multiple of 8. For
@@ -120,6 +131,15 @@ def create(
region: Region to create the GPU cluster in. Usable regions can be found from
`client.clusters.list_regions()`
+ acceptance_tests_params: AcceptanceTestsParams groups all GPU acceptance test options when enabled is
+ true.
+
+ add_ons: Add-ons to enable on the cluster at creation time.
+
+ auto_scale: Whether to enable auto-scaling for the cluster. If true, the cluster will
+ automatically scale the number of GPU worker nodes between num_gpus and
+ auto_scale_max_gpus based on the workload.
+
auto_scale_max_gpus: Maximum number of GPUs to which the cluster can be auto-scaled up. This field is
required if auto_scaled is true.
@@ -131,14 +151,26 @@ def create(
cluster_type: Type of cluster to create.
- duration_days: Duration in days to keep the cluster running.
-
gpu_node_failover_enabled: Whether automated GPU node failover should be enabled for this cluster. By
default, it is disabled.
install_traefik: Whether to install Traefik ingress controller in the cluster. This field is only
applicable for Kubernetes clusters and is false by default.
+ num_capacity_pool_gpus: Number of GPUs to allocate from the capacity pool. Must be a multiple of 8 and
+ not exceed num_gpus.
+
+ num_preemptible_gpus: Number of preemptible GPUs to request alongside on-demand capacity. Must be a
+ multiple of 8. Preemptible nodes are cheaper but may be reclaimed when on-demand
+ capacity is needed elsewhere; the system fulfills this asynchronously and
+ surfaces the actual count in allocated_preemptible_gpus.
+
+ num_reserved_gpus: Number of prepaid (PLG) reserved GPUs for this cluster. When omitted for
+ RESERVED billing on create, the server defaults this to num_gpus.
+
+ project_id: Project ID for the cluster. If not set, the project from the request context is
+ used.
+
reservation_end_time: Reservation end time of the cluster. This field is required for SCHEDULED
billing to specify the reservation end time for the cluster.
@@ -170,17 +202,26 @@ def create(
"billing_type": billing_type,
"cluster_name": cluster_name,
"cuda_version": cuda_version,
+ "duration_days": duration_days,
"gpu_type": gpu_type,
"num_gpus": num_gpus,
"nvidia_driver_version": nvidia_driver_version,
"region": region,
+ "acceptance_tests_params": acceptance_tests_params,
+ "add_ons": add_ons,
+ "auto_scale": auto_scale,
"auto_scale_max_gpus": auto_scale_max_gpus,
"auto_scaled": auto_scaled,
"capacity_pool_id": capacity_pool_id,
+ "cluster_config": cluster_config,
"cluster_type": cluster_type,
- "duration_days": duration_days,
"gpu_node_failover_enabled": gpu_node_failover_enabled,
"install_traefik": install_traefik,
+ "num_capacity_pool_gpus": num_capacity_pool_gpus,
+ "num_preemptible_gpus": num_preemptible_gpus,
+ "num_reserved_gpus": num_reserved_gpus,
+ "oidc_config": oidc_config,
+ "project_id": project_id,
"reservation_end_time": reservation_end_time,
"reservation_start_time": reservation_start_time,
"shared_volume": shared_volume,
@@ -235,8 +276,12 @@ def update(
self,
cluster_id: str,
*,
+ add_ons: Iterable[cluster_update_params.AddOn] | Omit = omit,
+ cluster_config: cluster_update_params.ClusterConfig | Omit = omit,
cluster_type: Literal["KUBERNETES", "SLURM"] | Omit = omit,
num_gpus: int | Omit = omit,
+ num_preemptible_gpus: int | Omit = omit,
+ num_reserved_gpus: int | Omit = omit,
reservation_end_time: Union[str, datetime] | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
@@ -251,10 +296,20 @@ def update(
Args:
cluster_id: The ID of the cluster to update
+ add_ons: Add-ons to update on the cluster. Each entry identifies an existing add-on by
+ name and provides the new external config to merge.
+
cluster_type: Type of cluster to update.
- num_gpus: Number of GPUs to allocate in the cluster. This must be multiple of 8. For
- example, 8, 16 or 24
+ num_gpus: Target GPU count for the cluster. When omitted, the server keeps the current GPU
+ count from cluster metadata (use for config-only or decommission-time-only
+ updates).
+
+ num_preemptible_gpus: Updated desired number of preemptible GPUs for the cluster. When omitted, the
+ current value is preserved. Must be a multiple of 8.
+
+ num_reserved_gpus: Number of reserved GPUs to update to. This field is only applicable for clusters
+ with RESERVED billing type.
reservation_end_time: Timestamp at which the cluster should be decommissioned. Only accepted for
prepaid clusters.
@@ -273,8 +328,12 @@ def update(
path_template("/compute/clusters/{cluster_id}", cluster_id=cluster_id),
body=maybe_transform(
{
+ "add_ons": add_ons,
+ "cluster_config": cluster_config,
"cluster_type": cluster_type,
"num_gpus": num_gpus,
+ "num_preemptible_gpus": num_preemptible_gpus,
+ "num_reserved_gpus": num_reserved_gpus,
"reservation_end_time": reservation_end_time,
},
cluster_update_params.ClusterUpdateParams,
@@ -288,6 +347,7 @@ def update(
def list(
self,
*,
+ project_id: str | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
extra_headers: Headers | None = None,
@@ -295,11 +355,30 @@ def list(
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> ClusterListResponse:
- """List all GPU clusters."""
+ """
+ List all GPU clusters.
+
+ Args:
+ project_id: Optional UMS project ID to filter clusters by. When set, only clusters belonging
+ to this project are returned. The caller must be a member of the project;
+ otherwise the result set will be empty.
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
return self._get(
"/compute/clusters",
options=make_request_options(
- extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=maybe_transform({"project_id": project_id}, cluster_list_params.ClusterListParams),
),
cast_to=ClusterListResponse,
)
@@ -389,17 +468,26 @@ async def create(
billing_type: Literal["RESERVED", "ON_DEMAND", "SCHEDULED_CAPACITY"],
cluster_name: str,
cuda_version: str,
+ duration_days: int,
gpu_type: Literal["H100_SXM", "H200_SXM", "RTX_6000_PCI", "L40_PCIE", "B200_SXM", "H100_SXM_INF"],
num_gpus: int,
nvidia_driver_version: str,
region: str,
+ acceptance_tests_params: cluster_create_params.AcceptanceTestsParams | Omit = omit,
+ add_ons: Iterable[cluster_create_params.AddOn] | Omit = omit,
+ auto_scale: bool | Omit = omit,
auto_scale_max_gpus: int | Omit = omit,
auto_scaled: bool | Omit = omit,
capacity_pool_id: str | Omit = omit,
+ cluster_config: cluster_create_params.ClusterConfig | Omit = omit,
cluster_type: Literal["KUBERNETES", "SLURM"] | Omit = omit,
- duration_days: int | Omit = omit,
gpu_node_failover_enabled: bool | Omit = omit,
install_traefik: bool | Omit = omit,
+ num_capacity_pool_gpus: int | Omit = omit,
+ num_preemptible_gpus: int | Omit = omit,
+ num_reserved_gpus: int | Omit = omit,
+ oidc_config: cluster_create_params.OidcConfig | Omit = omit,
+ project_id: str | Omit = omit,
reservation_end_time: Union[str, datetime] | Omit = omit,
reservation_start_time: Union[str, datetime] | Omit = omit,
shared_volume: cluster_create_params.SharedVolume | Omit = omit,
@@ -432,6 +520,8 @@ async def create(
cuda_version: CUDA version for this cluster. For example, 12.5
+ duration_days: Duration in days to keep the cluster running.
+
gpu_type: Type of GPU to use in the cluster
num_gpus: Number of GPUs to allocate in the cluster. This must be multiple of 8. For
@@ -443,6 +533,15 @@ async def create(
region: Region to create the GPU cluster in. Usable regions can be found from
`client.clusters.list_regions()`
+ acceptance_tests_params: AcceptanceTestsParams groups all GPU acceptance test options when enabled is
+ true.
+
+ add_ons: Add-ons to enable on the cluster at creation time.
+
+ auto_scale: Whether to enable auto-scaling for the cluster. If true, the cluster will
+ automatically scale the number of GPU worker nodes between num_gpus and
+ auto_scale_max_gpus based on the workload.
+
auto_scale_max_gpus: Maximum number of GPUs to which the cluster can be auto-scaled up. This field is
required if auto_scaled is true.
@@ -454,14 +553,26 @@ async def create(
cluster_type: Type of cluster to create.
- duration_days: Duration in days to keep the cluster running.
-
gpu_node_failover_enabled: Whether automated GPU node failover should be enabled for this cluster. By
default, it is disabled.
install_traefik: Whether to install Traefik ingress controller in the cluster. This field is only
applicable for Kubernetes clusters and is false by default.
+ num_capacity_pool_gpus: Number of GPUs to allocate from the capacity pool. Must be a multiple of 8 and
+ not exceed num_gpus.
+
+ num_preemptible_gpus: Number of preemptible GPUs to request alongside on-demand capacity. Must be a
+ multiple of 8. Preemptible nodes are cheaper but may be reclaimed when on-demand
+ capacity is needed elsewhere; the system fulfills this asynchronously and
+ surfaces the actual count in allocated_preemptible_gpus.
+
+ num_reserved_gpus: Number of prepaid (PLG) reserved GPUs for this cluster. When omitted for
+ RESERVED billing on create, the server defaults this to num_gpus.
+
+ project_id: Project ID for the cluster. If not set, the project from the request context is
+ used.
+
reservation_end_time: Reservation end time of the cluster. This field is required for SCHEDULED
billing to specify the reservation end time for the cluster.
@@ -493,17 +604,26 @@ async def create(
"billing_type": billing_type,
"cluster_name": cluster_name,
"cuda_version": cuda_version,
+ "duration_days": duration_days,
"gpu_type": gpu_type,
"num_gpus": num_gpus,
"nvidia_driver_version": nvidia_driver_version,
"region": region,
+ "acceptance_tests_params": acceptance_tests_params,
+ "add_ons": add_ons,
+ "auto_scale": auto_scale,
"auto_scale_max_gpus": auto_scale_max_gpus,
"auto_scaled": auto_scaled,
"capacity_pool_id": capacity_pool_id,
+ "cluster_config": cluster_config,
"cluster_type": cluster_type,
- "duration_days": duration_days,
"gpu_node_failover_enabled": gpu_node_failover_enabled,
"install_traefik": install_traefik,
+ "num_capacity_pool_gpus": num_capacity_pool_gpus,
+ "num_preemptible_gpus": num_preemptible_gpus,
+ "num_reserved_gpus": num_reserved_gpus,
+ "oidc_config": oidc_config,
+ "project_id": project_id,
"reservation_end_time": reservation_end_time,
"reservation_start_time": reservation_start_time,
"shared_volume": shared_volume,
@@ -558,8 +678,12 @@ async def update(
self,
cluster_id: str,
*,
+ add_ons: Iterable[cluster_update_params.AddOn] | Omit = omit,
+ cluster_config: cluster_update_params.ClusterConfig | Omit = omit,
cluster_type: Literal["KUBERNETES", "SLURM"] | Omit = omit,
num_gpus: int | Omit = omit,
+ num_preemptible_gpus: int | Omit = omit,
+ num_reserved_gpus: int | Omit = omit,
reservation_end_time: Union[str, datetime] | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
@@ -574,10 +698,20 @@ async def update(
Args:
cluster_id: The ID of the cluster to update
+ add_ons: Add-ons to update on the cluster. Each entry identifies an existing add-on by
+ name and provides the new external config to merge.
+
cluster_type: Type of cluster to update.
- num_gpus: Number of GPUs to allocate in the cluster. This must be multiple of 8. For
- example, 8, 16 or 24
+ num_gpus: Target GPU count for the cluster. When omitted, the server keeps the current GPU
+ count from cluster metadata (use for config-only or decommission-time-only
+ updates).
+
+ num_preemptible_gpus: Updated desired number of preemptible GPUs for the cluster. When omitted, the
+ current value is preserved. Must be a multiple of 8.
+
+ num_reserved_gpus: Number of reserved GPUs to update to. This field is only applicable for clusters
+ with RESERVED billing type.
reservation_end_time: Timestamp at which the cluster should be decommissioned. Only accepted for
prepaid clusters.
@@ -596,8 +730,12 @@ async def update(
path_template("/compute/clusters/{cluster_id}", cluster_id=cluster_id),
body=await async_maybe_transform(
{
+ "add_ons": add_ons,
+ "cluster_config": cluster_config,
"cluster_type": cluster_type,
"num_gpus": num_gpus,
+ "num_preemptible_gpus": num_preemptible_gpus,
+ "num_reserved_gpus": num_reserved_gpus,
"reservation_end_time": reservation_end_time,
},
cluster_update_params.ClusterUpdateParams,
@@ -611,6 +749,7 @@ async def update(
async def list(
self,
*,
+ project_id: str | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
extra_headers: Headers | None = None,
@@ -618,11 +757,30 @@ async def list(
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> ClusterListResponse:
- """List all GPU clusters."""
+ """
+ List all GPU clusters.
+
+ Args:
+ project_id: Optional UMS project ID to filter clusters by. When set, only clusters belonging
+ to this project are returned. The caller must be a member of the project;
+ otherwise the result set will be empty.
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
return await self._get(
"/compute/clusters",
options=make_request_options(
- extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=await async_maybe_transform({"project_id": project_id}, cluster_list_params.ClusterListParams),
),
cast_to=ClusterListResponse,
)
diff --git a/src/together/resources/beta/clusters/storage.py b/src/together/resources/beta/clusters/storage.py
index c6abf44e8..c295061f3 100644
--- a/src/together/resources/beta/clusters/storage.py
+++ b/src/together/resources/beta/clusters/storage.py
@@ -15,7 +15,7 @@
async_to_streamed_response_wrapper,
)
from ...._base_client import make_request_options
-from ....types.beta.clusters import storage_create_params, storage_update_params
+from ....types.beta.clusters import storage_list_params, storage_create_params, storage_update_params
from ....types.beta.clusters.cluster_storage import ClusterStorage
from ....types.beta.clusters.storage_list_response import StorageListResponse
from ....types.beta.clusters.storage_delete_response import StorageDeleteResponse
@@ -49,6 +49,7 @@ def create(
region: str,
size_tib: int,
volume_name: str,
+ is_lifecycle_independent: bool | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
extra_headers: Headers | None = None,
@@ -64,11 +65,9 @@ def create(
performance for shared storage.
Args:
- region: Region name. Usable regions can be found from `client.clusters.list_regions()`
-
size_tib: Volume size in whole tebibytes (TiB).
- volume_name: Customizable name of the volume to create.
+ is_lifecycle_independent: When true, the shared volume is not deleted when the cluster is decommissioned.
extra_headers: Send extra headers
@@ -85,6 +84,7 @@ def create(
"region": region,
"size_tib": size_tib,
"volume_name": volume_name,
+ "is_lifecycle_independent": is_lifecycle_independent,
},
storage_create_params.StorageCreateParams,
),
@@ -132,8 +132,8 @@ def retrieve(
def update(
self,
*,
- size_tib: int | Omit = omit,
- volume_id: str | Omit = omit,
+ size_tib: int,
+ volume_id: str,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
extra_headers: Headers | None = None,
@@ -145,10 +145,6 @@ def update(
Update the configuration of an existing shared volume.
Args:
- size_tib: Size of the volume in whole tebibytes (TiB).
-
- volume_id: ID of the volume to update.
-
extra_headers: Send extra headers
extra_query: Add additional query parameters to the request
@@ -175,6 +171,7 @@ def update(
def list(
self,
*,
+ project_id: str | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
extra_headers: Headers | None = None,
@@ -182,11 +179,30 @@ def list(
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> StorageListResponse:
- """List all shared volumes."""
+ """
+ List all shared volumes.
+
+ Args:
+ project_id: Optional UMS project ID to filter volumes by. When set, only volumes belonging
+ to this project are returned. The caller must be a member of the project;
+ otherwise the result set will be empty.
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
return self._get(
"/compute/clusters/storage/volumes",
options=make_request_options(
- extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=maybe_transform({"project_id": project_id}, storage_list_params.StorageListParams),
),
cast_to=StorageListResponse,
)
@@ -255,6 +271,7 @@ async def create(
region: str,
size_tib: int,
volume_name: str,
+ is_lifecycle_independent: bool | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
extra_headers: Headers | None = None,
@@ -270,11 +287,9 @@ async def create(
performance for shared storage.
Args:
- region: Region name. Usable regions can be found from `client.clusters.list_regions()`
-
size_tib: Volume size in whole tebibytes (TiB).
- volume_name: Customizable name of the volume to create.
+ is_lifecycle_independent: When true, the shared volume is not deleted when the cluster is decommissioned.
extra_headers: Send extra headers
@@ -291,6 +306,7 @@ async def create(
"region": region,
"size_tib": size_tib,
"volume_name": volume_name,
+ "is_lifecycle_independent": is_lifecycle_independent,
},
storage_create_params.StorageCreateParams,
),
@@ -338,8 +354,8 @@ async def retrieve(
async def update(
self,
*,
- size_tib: int | Omit = omit,
- volume_id: str | Omit = omit,
+ size_tib: int,
+ volume_id: str,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
extra_headers: Headers | None = None,
@@ -351,10 +367,6 @@ async def update(
Update the configuration of an existing shared volume.
Args:
- size_tib: Size of the volume in whole tebibytes (TiB).
-
- volume_id: ID of the volume to update.
-
extra_headers: Send extra headers
extra_query: Add additional query parameters to the request
@@ -381,6 +393,7 @@ async def update(
async def list(
self,
*,
+ project_id: str | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
extra_headers: Headers | None = None,
@@ -388,11 +401,30 @@ async def list(
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> StorageListResponse:
- """List all shared volumes."""
+ """
+ List all shared volumes.
+
+ Args:
+ project_id: Optional UMS project ID to filter volumes by. When set, only volumes belonging
+ to this project are returned. The caller must be a member of the project;
+ otherwise the result set will be empty.
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
return await self._get(
"/compute/clusters/storage/volumes",
options=make_request_options(
- extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=await async_maybe_transform({"project_id": project_id}, storage_list_params.StorageListParams),
),
cast_to=StorageListResponse,
)
diff --git a/src/together/types/beta/__init__.py b/src/together/types/beta/__init__.py
index ab66b141e..e7c1d8d94 100644
--- a/src/together/types/beta/__init__.py
+++ b/src/together/types/beta/__init__.py
@@ -8,6 +8,7 @@
from .jig_deploy_params import JigDeployParams as JigDeployParams
from .jig_list_response import JigListResponse as JigListResponse
from .jig_update_params import JigUpdateParams as JigUpdateParams
+from .cluster_list_params import ClusterListParams as ClusterListParams
from .cluster_create_params import ClusterCreateParams as ClusterCreateParams
from .cluster_list_response import ClusterListResponse as ClusterListResponse
from .cluster_update_params import ClusterUpdateParams as ClusterUpdateParams
diff --git a/src/together/types/beta/cluster.py b/src/together/types/beta/cluster.py
index 9d5f254c7..400108cfb 100644
--- a/src/together/types/beta/cluster.py
+++ b/src/together/types/beta/cluster.py
@@ -6,7 +6,85 @@
from ..._models import BaseModel
-__all__ = ["Cluster", "ControlPlaneNode", "GPUWorkerNode", "Volume"]
+__all__ = [
+ "Cluster",
+ "AddOn",
+ "AddOnConfig",
+ "AddOnConfigDashboard",
+ "AddOnConfigIngress",
+ "AddOnState",
+ "AddOnStateDashboard",
+ "AddOnStateIngress",
+ "ControlPlaneNode",
+ "ControlPlaneNodePhaseTransition",
+ "GPUWorkerNode",
+ "GPUWorkerNodePhaseTransition",
+ "GPUWorkerNodeLatestRemediation",
+ "PhaseTransition",
+ "Volume",
+ "ClusterConfig",
+ "ClusterConfigIngress",
+ "ClusterConfigObservability",
+ "ClusterConfigSlurmStartupScripts",
+ "OidcConfig",
+]
+
+
+class AddOnConfigDashboard(BaseModel):
+ enabled: Optional[bool] = None
+
+
+class AddOnConfigIngress(BaseModel):
+ enabled: Optional[bool] = None
+
+
+class AddOnConfig(BaseModel):
+ dashboard: Optional[AddOnConfigDashboard] = None
+
+ ingress: Optional[AddOnConfigIngress] = None
+
+
+class AddOnStateDashboard(BaseModel):
+ pass
+
+
+class AddOnStateIngress(BaseModel):
+ pass
+
+
+class AddOnState(BaseModel):
+ dashboard: Optional[AddOnStateDashboard] = None
+
+ ingress: Optional[AddOnStateIngress] = None
+
+
+class AddOn(BaseModel):
+ """AddOnInfo is returned in cluster responses and add-on CRUD operations."""
+
+ add_on_type: str
+
+ config: AddOnConfig
+
+ name: str
+
+ state: AddOnState
+
+
+class ControlPlaneNodePhaseTransition(BaseModel):
+ phase: Literal[
+ "NODE_PHASE_PENDING",
+ "NODE_PHASE_SCHEDULING",
+ "NODE_PHASE_BOOTING",
+ "NODE_PHASE_BOOTSTRAPPING",
+ "NODE_PHASE_RUNNING",
+ "NODE_PHASE_SUCCEEDED",
+ "NODE_PHASE_FAILED",
+ "NODE_PHASE_PAUSED",
+ ]
+ """Node phase."""
+
+ transition_time: datetime
+ """Timestamp when the phase transition occurred."""
class ControlPlaneNode(BaseModel):
@@ -22,9 +100,114 @@ class ControlPlaneNode(BaseModel):
num_cpu_cores: int
+ phase_transitions: List[ControlPlaneNodePhaseTransition]
+ """Phase transition history for this control plane node."""
+
status: str
+class GPUWorkerNodePhaseTransition(BaseModel):
+ phase: Literal[
+ "NODE_PHASE_PENDING",
+ "NODE_PHASE_SCHEDULING",
+ "NODE_PHASE_BOOTING",
+ "NODE_PHASE_BOOTSTRAPPING",
+ "NODE_PHASE_RUNNING",
+ "NODE_PHASE_SUCCEEDED",
+ "NODE_PHASE_FAILED",
+ "NODE_PHASE_PAUSED",
+ ]
+ """Node phase."""
+
+ transition_time: datetime
+ """Timestamp when the phase transition occurred."""
+
+
+class GPUWorkerNodeLatestRemediation(BaseModel):
+ """
+ Remediation represents a node remediation request for an instance.
+ An instance can have multiple remediations over time (e.g., failed attempts followed by retries).
+ """
+
+ id: str
+
+ cluster_id: str
+
+ instance_id: str
+
+ mode: Literal[
+ "REMEDIATION_MODE_VM_ONLY",
+ "REMEDIATION_MODE_HOST_AWARE",
+ "REMEDIATION_MODE_EVICT_WITHOUT_REPLACEMENT",
+ "REMEDIATION_MODE_REBOOT_VM",
+ ]
+ """Remediation mode specifies how the remediation should be performed.
+
+ - `REMEDIATION_MODE_VM_ONLY`: Deletes the VM and provisions a new one on any
+ available host.
+ - `REMEDIATION_MODE_HOST_AWARE`: Cordons the host, deletes the VM, and
+ provisions a new one on a different host.
+ """
+
+ state: Literal["PENDING_APPROVAL", "PENDING", "RUNNING", "SUCCEEDED", "FAILED", "CANCELLED", "AUTO_RESOLVED"]
+ """RemediationState represents the lifecycle state of a remediation.
+
+ - `PENDING_APPROVAL`: Awaiting approval before processing can begin.
+ - `PENDING`: Approved and queued for processing.
+ - `RUNNING`: Actively being processed.
+ - `SUCCEEDED`: Successfully completed.
+ - `FAILED`: Failed with an error.
+ - `CANCELLED`: Cancelled by user or system.
+ - `AUTO_RESOLVED`: The underlying issue was automatically resolved before
+ processing.
+ """
+
+ trigger: Literal["REMEDIATION_TRIGGER_MANUAL", "REMEDIATION_TRIGGER_AUTOMATED"]
+ """RemediationTrigger specifies how the remediation was triggered.
+
+ - `REMEDIATION_TRIGGER_MANUAL`: A user-initiated remediation (either via web UI
+ or API call).
+ - `REMEDIATION_TRIGGER_AUTOMATED`: A system-initiated remediation that requires
+ approval.
+ """
+
+ active_health_check_run_id: Optional[str] = None
+ """Active health check run ID (UUID) that triggered this remediation."""
+
+ create_time: Optional[datetime] = None
+ """When the remediation was created."""
+
+ end_time: Optional[datetime] = None
+ """When the remediation completed."""
+
+ error_message: Optional[str] = None
+ """Error message if the remediation failed."""
+
+ passive_health_check_event_id: Optional[str] = None
+ """Passive health check event ID that triggered this remediation."""
+
+ reason: Optional[str] = None
+ """User-provided reason for the remediation."""
+
+ requested_by: Optional[str] = None
+ """Who requested the remediation."""
+
+ review_comment: Optional[str] = None
+ """Review comment."""
+
+ review_time: Optional[datetime] = None
+ """When the remediation was reviewed."""
+
+ reviewed_by: Optional[str] = None
+ """Who reviewed the remediation."""
+
+ start_time: Optional[datetime] = None
+ """When processing started."""
+
+ update_time: Optional[datetime] = None
+ """When the remediation was last updated."""
+
+
class GPUWorkerNode(BaseModel):
host_name: str
@@ -40,10 +223,48 @@ class GPUWorkerNode(BaseModel):
num_gpus: int
+ phase_transitions: List[GPUWorkerNodePhaseTransition]
+ """Phase transition history for this GPU worker node."""
+
status: str
instance_id: Optional[str] = None
+ latest_remediation: Optional[GPUWorkerNodeLatestRemediation] = None
+ """
+ Remediation represents a node remediation request for an instance. An instance
+ can have multiple remediations over time (e.g., failed attempts followed by
+ retries).
+ """
+
+ slurm_worker_hostname: Optional[str] = None
+
+
+class PhaseTransition(BaseModel):
+ phase: Literal[
+ "CLUSTER_PHASE_QUEUED",
+ "CLUSTER_PHASE_SCHEDULED",
+ "CLUSTER_PHASE_WAITING_FOR_CONTROL_PLANE_NODES",
+ "CLUSTER_PHASE_WAITING_FOR_DATA_PLANE_NODES",
+ "CLUSTER_PHASE_WAITING_FOR_SUBNET",
+ "CLUSTER_PHASE_WAITING_FOR_SHARED_VOLUME",
+ "CLUSTER_PHASE_WAITING_FOR_AUTO_SCALER",
+ "CLUSTER_PHASE_INSTALLING_DRIVERS",
+ "CLUSTER_PHASE_RUNNING_ACCEPTANCE_TESTS",
+ "CLUSTER_PHASE_ACCEPTANCE_TESTS_FAILED",
+ "CLUSTER_PHASE_RUNNING_NCCL_TESTS",
+ "CLUSTER_PHASE_NCCL_TESTS_FAILED",
+ "CLUSTER_PHASE_READY",
+ "CLUSTER_PHASE_PAUSED",
+ "CLUSTER_PHASE_ON_DEMAND_COMPUTE_PAUSED",
+ "CLUSTER_PHASE_DEGRADED",
+ "CLUSTER_PHASE_DELETING",
+ ]
+ """Cluster phase."""
+
+ transition_time: datetime
+ """Timestamp when the phase transition occurred."""
+
class Volume(BaseModel):
size_tib: int
@@ -55,7 +276,115 @@ class Volume(BaseModel):
volume_name: str
+class ClusterConfigIngress(BaseModel):
+ enabled: Optional[bool] = None
+
+
+class ClusterConfigObservability(BaseModel):
+ enabled: Optional[bool] = None
+
+
+class ClusterConfigSlurmStartupScripts(BaseModel):
+ """
+ SlurmStartupScripts carries optional Slurm lifecycle scripts (prolog/epilog, init, extra conf).
+ """
+
+ controller_epilog: Optional[str] = None
+ """Slurm controller epilog script."""
+
+ controller_prolog: Optional[str] = None
+ """Slurm controller prolog script."""
+
+ extra_slurm_conf: Optional[str] = None
+ """Additional slurm.conf fragments."""
+
+ login_init_script: Optional[str] = None
+ """Script run on Slurm login node init."""
+
+ nodeset_init_script: Optional[str] = None
+ """Script run on Slurm nodeset init."""
+
+ worker_epilog: Optional[str] = None
+ """Slurm worker node epilog script."""
+
+ worker_prolog: Optional[str] = None
+ """Slurm worker node prolog script."""
+
+
+class ClusterConfig(BaseModel):
+ load_balancer: Literal["NONE", "TRAEFIK", "NGINX", "ISTIO"]
+
+ gpu_operator_version: Optional[str] = None
+ """NVIDIA GPU Operator chart/version for the tenant cluster (e.g.
+
+ v24.6.2). When omitted, a service default is applied.
+ """
+
+ ingress: Optional[ClusterConfigIngress] = None
+
+ jumphost_enabled: Optional[bool] = None
+
+ kubernetes_dashboard_enabled: Optional[bool] = None
+
+ observability: Optional[ClusterConfigObservability] = None
+
+ slurm_startup_scripts: Optional[ClusterConfigSlurmStartupScripts] = None
+ """
+ SlurmStartupScripts carries optional Slurm lifecycle scripts (prolog/epilog,
+ init, extra conf).
+ """
+
+
+class OidcConfig(BaseModel):
+ client_id: str
+ """OIDC client ID for authentication."""
+
+ group_claim: str
+ """JWT claim to use for user groups. For example, 'groups'"""
+
+ group_prefix: str
+ """Prefix to add to the group claim to form the final group name.
+
+ For example, 'oidc:'
+ """
+
+ issuer_url: str
+ """OIDC issuer URL for authentication. For example, https://accounts.google.com"""
+
+ username_claim: str
+ """JWT claim to use as the username. For example, 'sub' or 'email'"""
+
+ username_prefix: str
+ """Prefix to add to the username claim to form the final username.
+
+ For example, 'oidc:'
+ """
+
+ ca_cert: Optional[str] = None
+ """CA certificate in PEM format to validate the OIDC issuer's TLS certificate.
+
+ This field is optional but recommended if the issuer uses a private CA or
+ self-signed certificate.
+ """
+
+
class Cluster(BaseModel):
+ add_ons: List[AddOn]
+ """Enabled add-ons on this cluster.
+
+ Only add-ons with enabled=true in their config are returned.
+ """
+
+ allocated_preemptible_gpus: int
+ """Actual number of preemptible GPUs currently allocated to the cluster.
+
+ Updated asynchronously by the fulfillment and reclamation workers; may be less
+ than desired_preemptible_gpus when capacity is constrained.
+ """
+
+ billing_type: Literal["RESERVED", "ON_DEMAND", "SCHEDULED_CAPACITY"]
+ """Billing type for the cluster (RESERVED, ON_DEMAND, or SCHEDULED_CAPACITY)."""
+
cluster_id: str
cluster_name: str
@@ -67,16 +396,30 @@ class Cluster(BaseModel):
cuda_version: str
+ desired_preemptible_gpus: int
+ """Customer's requested number of preemptible GPUs.
+
+ Set on cluster create or update; persists until changed.
+ """
+
gpu_type: Literal["H100_SXM", "H200_SXM", "RTX_6000_PCI", "L40_PCIE", "B200_SXM", "H100_SXM_INF"]
gpu_worker_nodes: List[GPUWorkerNode]
kube_config: str
+ num_cpu_workers: int
+ """Number of CPU-only worker nodes in the cluster."""
+
num_gpus: int
nvidia_driver_version: str
+ phase_transitions: List[PhaseTransition]
+ """Cluster-level phase transition history."""
+
+ project_id: str
+
region: str
status: Literal[
@@ -98,12 +441,16 @@ class Cluster(BaseModel):
capacity_pool_id: Optional[str] = None
+ cluster_config: Optional[ClusterConfig] = None
+
created_at: Optional[datetime] = None
duration_hours: Optional[int] = None
install_traefik: Optional[bool] = None
+ oidc_config: Optional[OidcConfig] = None
+
reservation_end_time: Optional[datetime] = None
reservation_start_time: Optional[datetime] = None
diff --git a/src/together/types/beta/cluster_create_params.py b/src/together/types/beta/cluster_create_params.py
index e961052b7..4d9482da5 100644
--- a/src/together/types/beta/cluster_create_params.py
+++ b/src/together/types/beta/cluster_create_params.py
@@ -2,13 +2,26 @@
from __future__ import annotations
-from typing import Union
+from typing import Union, Iterable
from datetime import datetime
from typing_extensions import Literal, Required, Annotated, TypedDict
from ..._utils import PropertyInfo
-__all__ = ["ClusterCreateParams", "SharedVolume"]
+__all__ = [
+ "ClusterCreateParams",
+ "AcceptanceTestsParams",
+ "AddOn",
+ "AddOnConfig",
+ "AddOnConfigDashboard",
+ "AddOnConfigIngress",
+ "ClusterConfig",
+ "ClusterConfigIngress",
+ "ClusterConfigObservability",
+ "ClusterConfigSlurmStartupScripts",
+ "OidcConfig",
+ "SharedVolume",
+]
class ClusterCreateParams(TypedDict, total=False):
@@ -27,6 +40,9 @@ class ClusterCreateParams(TypedDict, total=False):
cuda_version: Required[str]
"""CUDA version for this cluster. For example, 12.5"""
+ duration_days: Required[int]
+ """Duration in days to keep the cluster running."""
+
gpu_type: Required[Literal["H100_SXM", "H200_SXM", "RTX_6000_PCI", "L40_PCIE", "B200_SXM", "H100_SXM_INF"]]
"""Type of GPU to use in the cluster"""
@@ -49,6 +65,22 @@ class ClusterCreateParams(TypedDict, total=False):
Usable regions can be found from `client.clusters.list_regions()`
"""
+ acceptance_tests_params: AcceptanceTestsParams
+ """
+ AcceptanceTestsParams groups all GPU acceptance test options when enabled is
+ true.
+ """
+
+ add_ons: Iterable[AddOn]
+ """Add-ons to enable on the cluster at creation time."""
+
+ auto_scale: bool
+ """Whether to enable auto-scaling for the cluster.
+
+ If true, the cluster will automatically scale the number of GPU worker nodes
+ between num_gpus and auto_scale_max_gpus based on the workload.
+ """
+
auto_scale_max_gpus: int
"""Maximum number of GPUs to which the cluster can be auto-scaled up.
@@ -68,12 +100,11 @@ class ClusterCreateParams(TypedDict, total=False):
capacity pool.
"""
+ cluster_config: ClusterConfig
+
cluster_type: Literal["KUBERNETES", "SLURM"]
"""Type of cluster to create."""
- duration_days: int
- """Duration in days to keep the cluster running."""
-
gpu_node_failover_enabled: bool
"""Whether automated GPU node failover should be enabled for this cluster.
@@ -86,6 +117,35 @@ class ClusterCreateParams(TypedDict, total=False):
This field is only applicable for Kubernetes clusters and is false by default.
"""
+ num_capacity_pool_gpus: int
+ """Number of GPUs to allocate from the capacity pool.
+
+ Must be a multiple of 8 and not exceed num_gpus.
+ """
+
+ num_preemptible_gpus: int
+ """Number of preemptible GPUs to request alongside on-demand capacity.
+
+ Must be a multiple of 8. Preemptible nodes are cheaper but may be reclaimed when
+ on-demand capacity is needed elsewhere; the system fulfills this asynchronously
+ and surfaces the actual count in allocated_preemptible_gpus.
+ """
+
+ num_reserved_gpus: int
+ """Number of prepaid (PLG) reserved GPUs for this cluster.
+
+ When omitted for RESERVED billing on create, the server defaults this to
+ num_gpus.
+ """
+
+ oidc_config: OidcConfig
+
+ project_id: str
+ """Project ID for the cluster.
+
+ If not set, the project from the request context is used.
+ """
+
reservation_end_time: Annotated[Union[str, datetime], PropertyInfo(format="iso8601")]
"""Reservation end time of the cluster.
@@ -116,14 +176,164 @@ class ClusterCreateParams(TypedDict, total=False):
"""ID of an existing volume to use with the cluster creation."""
+class AcceptanceTestsParams(TypedDict, total=False):
+ """
+ AcceptanceTestsParams groups all GPU acceptance test options when enabled is true.
+ """
+
+ dcgm_diag_level: Literal[
+ "DCGM_DIAG_LEVEL_SHORT", "DCGM_DIAG_LEVEL_MEDIUM", "DCGM_DIAG_LEVEL_LONG", "DCGM_DIAG_LEVEL_EXTENDED"
+ ]
+ """DCGM diagnostic depth.
+
+ SHORT = readiness; MEDIUM = default; LONG = system validation; EXTENDED =
+ memtest. An omitted value selects MEDIUM when enabled.
+ """
+
+ dcgm_diag_skipped: bool
+ """Skip DCGM diagnostics acceptance test."""
+
+ enabled: bool
+ """Whether to run GPU acceptance tests during cluster bring-up."""
+
+ gpu_burn_duration: int
+ """GPU burn duration in seconds; 0 means use the default when enabled."""
+
+ gpu_burn_skipped: bool
+ """Skip GPU burn acceptance test."""
+
+ nccl_multi_node_skipped: bool
+ """Skip NCCL multi-node acceptance test."""
+
+ nccl_single_node_skipped: bool
+ """Skip NCCL single-node acceptance test."""
+
+
+class AddOnConfigDashboard(TypedDict, total=False):
+ enabled: bool
+
+
+class AddOnConfigIngress(TypedDict, total=False):
+ enabled: bool
+
+
+class AddOnConfig(TypedDict, total=False):
+ dashboard: AddOnConfigDashboard
+
+ ingress: AddOnConfigIngress
+
+
+class AddOn(TypedDict, total=False):
+ add_on_type: Required[str]
+ """Type of add-on. Valid values: 'dashboard', 'ingress'."""
+
+ name: Required[str]
+ """Human-readable name for this add-on instance."""
+
+ config: AddOnConfig
+
+
+class ClusterConfigIngress(TypedDict, total=False):
+ enabled: bool
+
+
+class ClusterConfigObservability(TypedDict, total=False):
+ enabled: bool
+
+
+class ClusterConfigSlurmStartupScripts(TypedDict, total=False):
+ """
+ SlurmStartupScripts carries optional Slurm lifecycle scripts (prolog/epilog, init, extra conf).
+ """
+
+ controller_epilog: str
+ """Slurm controller epilog script."""
+
+ controller_prolog: str
+ """Slurm controller prolog script."""
+
+ extra_slurm_conf: str
+ """Additional slurm.conf fragments."""
+
+ login_init_script: str
+ """Script run on Slurm login node init."""
+
+ nodeset_init_script: str
+ """Script run on Slurm nodeset init."""
+
+ worker_epilog: str
+ """Slurm worker node epilog script."""
+
+ worker_prolog: str
+ """Slurm worker node prolog script."""
+
+
+class ClusterConfig(TypedDict, total=False):
+ load_balancer: Required[Literal["NONE", "TRAEFIK", "NGINX", "ISTIO"]]
+
+ gpu_operator_version: str
+ """NVIDIA GPU Operator chart/version for the tenant cluster (e.g.
+
+ v24.6.2). When omitted, a service default is applied.
+ """
+
+ ingress: ClusterConfigIngress
+
+ jumphost_enabled: bool
+
+ kubernetes_dashboard_enabled: bool
+
+ observability: ClusterConfigObservability
+
+ slurm_startup_scripts: ClusterConfigSlurmStartupScripts
+ """
+ SlurmStartupScripts carries optional Slurm lifecycle scripts (prolog/epilog,
+ init, extra conf).
+ """
+
+
+class OidcConfig(TypedDict, total=False):
+ client_id: Required[str]
+ """OIDC client ID for authentication."""
+
+ group_claim: Required[str]
+ """JWT claim to use for user groups. For example, 'groups'"""
+
+ group_prefix: Required[str]
+ """Prefix to add to the group claim to form the final group name.
+
+ For example, 'oidc:'
+ """
+
+ issuer_url: Required[str]
+ """OIDC issuer URL for authentication. For example, https://accounts.google.com"""
+
+ username_claim: Required[str]
+ """JWT claim to use as the username. For example, 'sub' or 'email'"""
+
+ username_prefix: Required[str]
+ """Prefix to add to the username claim to form the final username.
+
+ For example, 'oidc:'
+ """
+
+ ca_cert: str
+ """CA certificate in PEM format to validate the OIDC issuer's TLS certificate.
+
+ This field is optional but recommended if the issuer uses a private CA or
+ self-signed certificate.
+ """
+
+
class SharedVolume(TypedDict, total=False):
"""Inline configuration to create a shared volume with the cluster creation."""
region: Required[str]
- """Region name. Usable regions can be found from `client.clusters.list_regions()`"""
size_tib: Required[int]
"""Volume size in whole tebibytes (TiB)."""
volume_name: Required[str]
- """Customizable name of the volume to create."""
+
+ is_lifecycle_independent: bool
+ """When true, the shared volume is not deleted when the cluster is decommissioned."""
diff --git a/src/together/types/beta/cluster_list_params.py b/src/together/types/beta/cluster_list_params.py
new file mode 100644
index 000000000..69fa8e9e4
--- /dev/null
+++ b/src/together/types/beta/cluster_list_params.py
@@ -0,0 +1,16 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import TypedDict
+
+__all__ = ["ClusterListParams"]
+
+
+class ClusterListParams(TypedDict, total=False):
+ project_id: str
+ """Optional UMS project ID to filter clusters by.
+
+ When set, only clusters belonging to this project are returned. The caller must
+ be a member of the project; otherwise the result set will be empty.
+ """
diff --git a/src/together/types/beta/cluster_update_params.py b/src/together/types/beta/cluster_update_params.py
index a98f77653..46cf388aa 100644
--- a/src/together/types/beta/cluster_update_params.py
+++ b/src/together/types/beta/cluster_update_params.py
@@ -2,23 +2,55 @@
from __future__ import annotations
-from typing import Union
+from typing import Union, Iterable
from datetime import datetime
-from typing_extensions import Literal, Annotated, TypedDict
+from typing_extensions import Literal, Required, Annotated, TypedDict
from ..._utils import PropertyInfo
-__all__ = ["ClusterUpdateParams"]
+__all__ = [
+ "ClusterUpdateParams",
+ "AddOn",
+ "AddOnConfig",
+ "AddOnConfigDashboard",
+ "AddOnConfigIngress",
+ "ClusterConfig",
+ "ClusterConfigIngress",
+ "ClusterConfigObservability",
+ "ClusterConfigSlurmStartupScripts",
+]
class ClusterUpdateParams(TypedDict, total=False):
+ add_ons: Iterable[AddOn]
+ """Add-ons to update on the cluster.
+
+ Each entry identifies an existing add-on by name and provides the new external
+ config to merge.
+ """
+
+ cluster_config: ClusterConfig
+
cluster_type: Literal["KUBERNETES", "SLURM"]
"""Type of cluster to update."""
num_gpus: int
- """Number of GPUs to allocate in the cluster.
+ """Target GPU count for the cluster.
+
+ When omitted, the server keeps the current GPU count from cluster metadata (use
+ for config-only or decommission-time-only updates).
+ """
- This must be multiple of 8. For example, 8, 16 or 24
+ num_preemptible_gpus: int
+ """Updated desired number of preemptible GPUs for the cluster.
+
+ When omitted, the current value is preserved. Must be a multiple of 8.
+ """
+
+ num_reserved_gpus: int
+ """Number of reserved GPUs to update to.
+
+ This field is only applicable for clusters with RESERVED billing type.
"""
reservation_end_time: Annotated[Union[str, datetime], PropertyInfo(format="iso8601")]
@@ -26,3 +58,83 @@ class ClusterUpdateParams(TypedDict, total=False):
Only accepted for prepaid clusters.
"""
+
+
+class AddOnConfigDashboard(TypedDict, total=False):
+ enabled: bool
+
+
+class AddOnConfigIngress(TypedDict, total=False):
+ enabled: bool
+
+
+class AddOnConfig(TypedDict, total=False):
+ dashboard: AddOnConfigDashboard
+
+ ingress: AddOnConfigIngress
+
+
+class AddOn(TypedDict, total=False):
+ name: Required[str]
+ """Name of the add-on to update. Must match an existing add-on on the cluster."""
+
+ config: AddOnConfig
+
+
+class ClusterConfigIngress(TypedDict, total=False):
+ enabled: bool
+
+
+class ClusterConfigObservability(TypedDict, total=False):
+ enabled: bool
+
+
+class ClusterConfigSlurmStartupScripts(TypedDict, total=False):
+ """
+ SlurmStartupScripts carries optional Slurm lifecycle scripts (prolog/epilog, init, extra conf).
+ """
+
+ controller_epilog: str
+ """Slurm controller epilog script."""
+
+ controller_prolog: str
+ """Slurm controller prolog script."""
+
+ extra_slurm_conf: str
+ """Additional slurm.conf fragments."""
+
+ login_init_script: str
+ """Script run on Slurm login node init."""
+
+ nodeset_init_script: str
+ """Script run on Slurm nodeset init."""
+
+ worker_epilog: str
+ """Slurm worker node epilog script."""
+
+ worker_prolog: str
+ """Slurm worker node prolog script."""
+
+
+class ClusterConfig(TypedDict, total=False):
+ load_balancer: Required[Literal["NONE", "TRAEFIK", "NGINX", "ISTIO"]]
+
+ gpu_operator_version: str
+ """NVIDIA GPU Operator chart/version for the tenant cluster (e.g.
+
+ v24.6.2). When omitted, a service default is applied.
+ """
+
+ ingress: ClusterConfigIngress
+
+ jumphost_enabled: bool
+
+ kubernetes_dashboard_enabled: bool
+
+ observability: ClusterConfigObservability
+
+ slurm_startup_scripts: ClusterConfigSlurmStartupScripts
+ """
+ SlurmStartupScripts carries optional Slurm lifecycle scripts (prolog/epilog,
+ init, extra conf).
+ """
diff --git a/src/together/types/beta/clusters/__init__.py b/src/together/types/beta/clusters/__init__.py
index a85f4c49a..9c4f18a76 100644
--- a/src/together/types/beta/clusters/__init__.py
+++ b/src/together/types/beta/clusters/__init__.py
@@ -3,6 +3,7 @@
from __future__ import annotations
from .cluster_storage import ClusterStorage as ClusterStorage
+from .storage_list_params import StorageListParams as StorageListParams
from .storage_create_params import StorageCreateParams as StorageCreateParams
from .storage_list_response import StorageListResponse as StorageListResponse
from .storage_update_params import StorageUpdateParams as StorageUpdateParams
diff --git a/src/together/types/beta/clusters/cluster_storage.py b/src/together/types/beta/clusters/cluster_storage.py
index 6d7a0bfe4..22392753f 100644
--- a/src/together/types/beta/clusters/cluster_storage.py
+++ b/src/together/types/beta/clusters/cluster_storage.py
@@ -1,7 +1,5 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-from typing_extensions import Literal
-
from ...._models import BaseModel
__all__ = ["ClusterStorage"]
@@ -9,13 +7,9 @@
class ClusterStorage(BaseModel):
size_tib: int
- """Size of the volume in whole tebibytes (TiB)."""
- status: Literal["available", "bound", "provisioning"]
- """Deployment status of the volume."""
+ status: str
volume_id: str
- """ID of the volume."""
volume_name: str
- """Provided name of the volume."""
diff --git a/src/together/types/beta/clusters/storage_create_params.py b/src/together/types/beta/clusters/storage_create_params.py
index 5629cb113..2dfb9218f 100644
--- a/src/together/types/beta/clusters/storage_create_params.py
+++ b/src/together/types/beta/clusters/storage_create_params.py
@@ -9,10 +9,11 @@
class StorageCreateParams(TypedDict, total=False):
region: Required[str]
- """Region name. Usable regions can be found from `client.clusters.list_regions()`"""
size_tib: Required[int]
"""Volume size in whole tebibytes (TiB)."""
volume_name: Required[str]
- """Customizable name of the volume to create."""
+
+ is_lifecycle_independent: bool
+ """When true, the shared volume is not deleted when the cluster is decommissioned."""
diff --git a/src/together/types/beta/clusters/storage_list_params.py b/src/together/types/beta/clusters/storage_list_params.py
new file mode 100644
index 000000000..2dddfca79
--- /dev/null
+++ b/src/together/types/beta/clusters/storage_list_params.py
@@ -0,0 +1,16 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import TypedDict
+
+__all__ = ["StorageListParams"]
+
+
+class StorageListParams(TypedDict, total=False):
+ project_id: str
+ """Optional UMS project ID to filter volumes by.
+
+ When set, only volumes belonging to this project are returned. The caller must
+ be a member of the project; otherwise the result set will be empty.
+ """
diff --git a/src/together/types/beta/clusters/storage_update_params.py b/src/together/types/beta/clusters/storage_update_params.py
index 449a62661..c87e4cd61 100644
--- a/src/together/types/beta/clusters/storage_update_params.py
+++ b/src/together/types/beta/clusters/storage_update_params.py
@@ -2,14 +2,12 @@
from __future__ import annotations
-from typing_extensions import TypedDict
+from typing_extensions import Required, TypedDict
__all__ = ["StorageUpdateParams"]
class StorageUpdateParams(TypedDict, total=False):
- size_tib: int
- """Size of the volume in whole tebibytes (TiB)."""
+ size_tib: Required[int]
- volume_id: str
- """ID of the volume to update."""
+ volume_id: Required[str]
diff --git a/tests/api_resources/beta/clusters/test_storage.py b/tests/api_resources/beta/clusters/test_storage.py
index 7a78f0d7a..8403af1ae 100644
--- a/tests/api_resources/beta/clusters/test_storage.py
+++ b/tests/api_resources/beta/clusters/test_storage.py
@@ -30,6 +30,16 @@ def test_method_create(self, client: Together) -> None:
)
assert_matches_type(ClusterStorage, storage, path=["response"])
+ @parametrize
+ def test_method_create_with_all_params(self, client: Together) -> None:
+ storage = client.beta.clusters.storage.create(
+ region="region",
+ size_tib=0,
+ volume_name="volume_name",
+ is_lifecycle_independent=True,
+ )
+ assert_matches_type(ClusterStorage, storage, path=["response"])
+
@parametrize
def test_raw_response_create(self, client: Together) -> None:
response = client.beta.clusters.storage.with_raw_response.create(
@@ -98,11 +108,6 @@ def test_path_params_retrieve(self, client: Together) -> None:
@parametrize
def test_method_update(self, client: Together) -> None:
- storage = client.beta.clusters.storage.update()
- assert_matches_type(ClusterStorage, storage, path=["response"])
-
- @parametrize
- def test_method_update_with_all_params(self, client: Together) -> None:
storage = client.beta.clusters.storage.update(
size_tib=0,
volume_id="volume_id",
@@ -111,7 +116,10 @@ def test_method_update_with_all_params(self, client: Together) -> None:
@parametrize
def test_raw_response_update(self, client: Together) -> None:
- response = client.beta.clusters.storage.with_raw_response.update()
+ response = client.beta.clusters.storage.with_raw_response.update(
+ size_tib=0,
+ volume_id="volume_id",
+ )
assert response.is_closed is True
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -120,7 +128,10 @@ def test_raw_response_update(self, client: Together) -> None:
@parametrize
def test_streaming_response_update(self, client: Together) -> None:
- with client.beta.clusters.storage.with_streaming_response.update() as response:
+ with client.beta.clusters.storage.with_streaming_response.update(
+ size_tib=0,
+ volume_id="volume_id",
+ ) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -134,6 +145,13 @@ def test_method_list(self, client: Together) -> None:
storage = client.beta.clusters.storage.list()
assert_matches_type(StorageListResponse, storage, path=["response"])
+ @parametrize
+ def test_method_list_with_all_params(self, client: Together) -> None:
+ storage = client.beta.clusters.storage.list(
+ project_id="project_id",
+ )
+ assert_matches_type(StorageListResponse, storage, path=["response"])
+
@parametrize
def test_raw_response_list(self, client: Together) -> None:
response = client.beta.clusters.storage.with_raw_response.list()
@@ -207,6 +225,16 @@ async def test_method_create(self, async_client: AsyncTogether) -> None:
)
assert_matches_type(ClusterStorage, storage, path=["response"])
+ @parametrize
+ async def test_method_create_with_all_params(self, async_client: AsyncTogether) -> None:
+ storage = await async_client.beta.clusters.storage.create(
+ region="region",
+ size_tib=0,
+ volume_name="volume_name",
+ is_lifecycle_independent=True,
+ )
+ assert_matches_type(ClusterStorage, storage, path=["response"])
+
@parametrize
async def test_raw_response_create(self, async_client: AsyncTogether) -> None:
response = await async_client.beta.clusters.storage.with_raw_response.create(
@@ -275,11 +303,6 @@ async def test_path_params_retrieve(self, async_client: AsyncTogether) -> None:
@parametrize
async def test_method_update(self, async_client: AsyncTogether) -> None:
- storage = await async_client.beta.clusters.storage.update()
- assert_matches_type(ClusterStorage, storage, path=["response"])
-
- @parametrize
- async def test_method_update_with_all_params(self, async_client: AsyncTogether) -> None:
storage = await async_client.beta.clusters.storage.update(
size_tib=0,
volume_id="volume_id",
@@ -288,7 +311,10 @@ async def test_method_update_with_all_params(self, async_client: AsyncTogether)
@parametrize
async def test_raw_response_update(self, async_client: AsyncTogether) -> None:
- response = await async_client.beta.clusters.storage.with_raw_response.update()
+ response = await async_client.beta.clusters.storage.with_raw_response.update(
+ size_tib=0,
+ volume_id="volume_id",
+ )
assert response.is_closed is True
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -297,7 +323,10 @@ async def test_raw_response_update(self, async_client: AsyncTogether) -> None:
@parametrize
async def test_streaming_response_update(self, async_client: AsyncTogether) -> None:
- async with async_client.beta.clusters.storage.with_streaming_response.update() as response:
+ async with async_client.beta.clusters.storage.with_streaming_response.update(
+ size_tib=0,
+ volume_id="volume_id",
+ ) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -311,6 +340,13 @@ async def test_method_list(self, async_client: AsyncTogether) -> None:
storage = await async_client.beta.clusters.storage.list()
assert_matches_type(StorageListResponse, storage, path=["response"])
+ @parametrize
+ async def test_method_list_with_all_params(self, async_client: AsyncTogether) -> None:
+ storage = await async_client.beta.clusters.storage.list(
+ project_id="project_id",
+ )
+ assert_matches_type(StorageListResponse, storage, path=["response"])
+
@parametrize
async def test_raw_response_list(self, async_client: AsyncTogether) -> None:
response = await async_client.beta.clusters.storage.with_raw_response.list()
diff --git a/tests/api_resources/beta/test_clusters.py b/tests/api_resources/beta/test_clusters.py
index e1f452619..119b86f46 100644
--- a/tests/api_resources/beta/test_clusters.py
+++ b/tests/api_resources/beta/test_clusters.py
@@ -29,6 +29,7 @@ def test_method_create(self, client: Together) -> None:
billing_type="RESERVED",
cluster_name="cluster_name",
cuda_version="cuda_version",
+ duration_days=0,
gpu_type="H100_SXM",
num_gpus=0,
nvidia_driver_version="nvidia_driver_version",
@@ -42,23 +43,74 @@ def test_method_create_with_all_params(self, client: Together) -> None:
billing_type="RESERVED",
cluster_name="cluster_name",
cuda_version="cuda_version",
+ duration_days=0,
gpu_type="H100_SXM",
num_gpus=0,
nvidia_driver_version="nvidia_driver_version",
region="region",
+ acceptance_tests_params={
+ "dcgm_diag_level": "DCGM_DIAG_LEVEL_SHORT",
+ "dcgm_diag_skipped": True,
+ "enabled": True,
+ "gpu_burn_duration": 0,
+ "gpu_burn_skipped": True,
+ "nccl_multi_node_skipped": True,
+ "nccl_single_node_skipped": True,
+ },
+ add_ons=[
+ {
+ "add_on_type": "add_on_type",
+ "name": "name",
+ "config": {
+ "dashboard": {"enabled": True},
+ "ingress": {"enabled": True},
+ },
+ }
+ ],
+ auto_scale=True,
auto_scale_max_gpus=0,
auto_scaled=True,
capacity_pool_id="capacity_pool_id",
+ cluster_config={
+ "load_balancer": "NONE",
+ "gpu_operator_version": "gpu_operator_version",
+ "ingress": {"enabled": True},
+ "jumphost_enabled": True,
+ "kubernetes_dashboard_enabled": True,
+ "observability": {"enabled": True},
+ "slurm_startup_scripts": {
+ "controller_epilog": "controller_epilog",
+ "controller_prolog": "controller_prolog",
+ "extra_slurm_conf": "extra_slurm_conf",
+ "login_init_script": "login_init_script",
+ "nodeset_init_script": "nodeset_init_script",
+ "worker_epilog": "worker_epilog",
+ "worker_prolog": "worker_prolog",
+ },
+ },
cluster_type="KUBERNETES",
- duration_days=0,
gpu_node_failover_enabled=True,
install_traefik=True,
+ num_capacity_pool_gpus=0,
+ num_preemptible_gpus=0,
+ num_reserved_gpus=0,
+ oidc_config={
+ "client_id": "client_id",
+ "group_claim": "group_claim",
+ "group_prefix": "group_prefix",
+ "issuer_url": "issuer_url",
+ "username_claim": "username_claim",
+ "username_prefix": "username_prefix",
+ "ca_cert": "ca_cert",
+ },
+ project_id="project_id",
reservation_end_time=parse_datetime("2019-12-27T18:11:19.117Z"),
reservation_start_time=parse_datetime("2019-12-27T18:11:19.117Z"),
shared_volume={
"region": "region",
"size_tib": 0,
"volume_name": "volume_name",
+ "is_lifecycle_independent": True,
},
slurm_image="slurm_image",
slurm_shm_size_gib=0,
@@ -72,6 +124,7 @@ def test_raw_response_create(self, client: Together) -> None:
billing_type="RESERVED",
cluster_name="cluster_name",
cuda_version="cuda_version",
+ duration_days=0,
gpu_type="H100_SXM",
num_gpus=0,
nvidia_driver_version="nvidia_driver_version",
@@ -89,6 +142,7 @@ def test_streaming_response_create(self, client: Together) -> None:
billing_type="RESERVED",
cluster_name="cluster_name",
cuda_version="cuda_version",
+ duration_days=0,
gpu_type="H100_SXM",
num_gpus=0,
nvidia_driver_version="nvidia_driver_version",
@@ -151,8 +205,36 @@ def test_method_update(self, client: Together) -> None:
def test_method_update_with_all_params(self, client: Together) -> None:
cluster = client.beta.clusters.update(
cluster_id="cluster_id",
+ add_ons=[
+ {
+ "name": "name",
+ "config": {
+ "dashboard": {"enabled": True},
+ "ingress": {"enabled": True},
+ },
+ }
+ ],
+ cluster_config={
+ "load_balancer": "NONE",
+ "gpu_operator_version": "gpu_operator_version",
+ "ingress": {"enabled": True},
+ "jumphost_enabled": True,
+ "kubernetes_dashboard_enabled": True,
+ "observability": {"enabled": True},
+ "slurm_startup_scripts": {
+ "controller_epilog": "controller_epilog",
+ "controller_prolog": "controller_prolog",
+ "extra_slurm_conf": "extra_slurm_conf",
+ "login_init_script": "login_init_script",
+ "nodeset_init_script": "nodeset_init_script",
+ "worker_epilog": "worker_epilog",
+ "worker_prolog": "worker_prolog",
+ },
+ },
cluster_type="KUBERNETES",
num_gpus=0,
+ num_preemptible_gpus=0,
+ num_reserved_gpus=0,
reservation_end_time=parse_datetime("2019-12-27T18:11:19.117Z"),
)
assert_matches_type(Cluster, cluster, path=["response"])
@@ -193,6 +275,13 @@ def test_method_list(self, client: Together) -> None:
cluster = client.beta.clusters.list()
assert_matches_type(ClusterListResponse, cluster, path=["response"])
+ @parametrize
+ def test_method_list_with_all_params(self, client: Together) -> None:
+ cluster = client.beta.clusters.list(
+ project_id="project_id",
+ )
+ assert_matches_type(ClusterListResponse, cluster, path=["response"])
+
@parametrize
def test_raw_response_list(self, client: Together) -> None:
response = client.beta.clusters.with_raw_response.list()
@@ -288,6 +377,7 @@ async def test_method_create(self, async_client: AsyncTogether) -> None:
billing_type="RESERVED",
cluster_name="cluster_name",
cuda_version="cuda_version",
+ duration_days=0,
gpu_type="H100_SXM",
num_gpus=0,
nvidia_driver_version="nvidia_driver_version",
@@ -301,23 +391,74 @@ async def test_method_create_with_all_params(self, async_client: AsyncTogether)
billing_type="RESERVED",
cluster_name="cluster_name",
cuda_version="cuda_version",
+ duration_days=0,
gpu_type="H100_SXM",
num_gpus=0,
nvidia_driver_version="nvidia_driver_version",
region="region",
+ acceptance_tests_params={
+ "dcgm_diag_level": "DCGM_DIAG_LEVEL_SHORT",
+ "dcgm_diag_skipped": True,
+ "enabled": True,
+ "gpu_burn_duration": 0,
+ "gpu_burn_skipped": True,
+ "nccl_multi_node_skipped": True,
+ "nccl_single_node_skipped": True,
+ },
+ add_ons=[
+ {
+ "add_on_type": "add_on_type",
+ "name": "name",
+ "config": {
+ "dashboard": {"enabled": True},
+ "ingress": {"enabled": True},
+ },
+ }
+ ],
+ auto_scale=True,
auto_scale_max_gpus=0,
auto_scaled=True,
capacity_pool_id="capacity_pool_id",
+ cluster_config={
+ "load_balancer": "NONE",
+ "gpu_operator_version": "gpu_operator_version",
+ "ingress": {"enabled": True},
+ "jumphost_enabled": True,
+ "kubernetes_dashboard_enabled": True,
+ "observability": {"enabled": True},
+ "slurm_startup_scripts": {
+ "controller_epilog": "controller_epilog",
+ "controller_prolog": "controller_prolog",
+ "extra_slurm_conf": "extra_slurm_conf",
+ "login_init_script": "login_init_script",
+ "nodeset_init_script": "nodeset_init_script",
+ "worker_epilog": "worker_epilog",
+ "worker_prolog": "worker_prolog",
+ },
+ },
cluster_type="KUBERNETES",
- duration_days=0,
gpu_node_failover_enabled=True,
install_traefik=True,
+ num_capacity_pool_gpus=0,
+ num_preemptible_gpus=0,
+ num_reserved_gpus=0,
+ oidc_config={
+ "client_id": "client_id",
+ "group_claim": "group_claim",
+ "group_prefix": "group_prefix",
+ "issuer_url": "issuer_url",
+ "username_claim": "username_claim",
+ "username_prefix": "username_prefix",
+ "ca_cert": "ca_cert",
+ },
+ project_id="project_id",
reservation_end_time=parse_datetime("2019-12-27T18:11:19.117Z"),
reservation_start_time=parse_datetime("2019-12-27T18:11:19.117Z"),
shared_volume={
"region": "region",
"size_tib": 0,
"volume_name": "volume_name",
+ "is_lifecycle_independent": True,
},
slurm_image="slurm_image",
slurm_shm_size_gib=0,
@@ -331,6 +472,7 @@ async def test_raw_response_create(self, async_client: AsyncTogether) -> None:
billing_type="RESERVED",
cluster_name="cluster_name",
cuda_version="cuda_version",
+ duration_days=0,
gpu_type="H100_SXM",
num_gpus=0,
nvidia_driver_version="nvidia_driver_version",
@@ -348,6 +490,7 @@ async def test_streaming_response_create(self, async_client: AsyncTogether) -> N
billing_type="RESERVED",
cluster_name="cluster_name",
cuda_version="cuda_version",
+ duration_days=0,
gpu_type="H100_SXM",
num_gpus=0,
nvidia_driver_version="nvidia_driver_version",
@@ -410,8 +553,36 @@ async def test_method_update(self, async_client: AsyncTogether) -> None:
async def test_method_update_with_all_params(self, async_client: AsyncTogether) -> None:
cluster = await async_client.beta.clusters.update(
cluster_id="cluster_id",
+ add_ons=[
+ {
+ "name": "name",
+ "config": {
+ "dashboard": {"enabled": True},
+ "ingress": {"enabled": True},
+ },
+ }
+ ],
+ cluster_config={
+ "load_balancer": "NONE",
+ "gpu_operator_version": "gpu_operator_version",
+ "ingress": {"enabled": True},
+ "jumphost_enabled": True,
+ "kubernetes_dashboard_enabled": True,
+ "observability": {"enabled": True},
+ "slurm_startup_scripts": {
+ "controller_epilog": "controller_epilog",
+ "controller_prolog": "controller_prolog",
+ "extra_slurm_conf": "extra_slurm_conf",
+ "login_init_script": "login_init_script",
+ "nodeset_init_script": "nodeset_init_script",
+ "worker_epilog": "worker_epilog",
+ "worker_prolog": "worker_prolog",
+ },
+ },
cluster_type="KUBERNETES",
num_gpus=0,
+ num_preemptible_gpus=0,
+ num_reserved_gpus=0,
reservation_end_time=parse_datetime("2019-12-27T18:11:19.117Z"),
)
assert_matches_type(Cluster, cluster, path=["response"])
@@ -452,6 +623,13 @@ async def test_method_list(self, async_client: AsyncTogether) -> None:
cluster = await async_client.beta.clusters.list()
assert_matches_type(ClusterListResponse, cluster, path=["response"])
+ @parametrize
+ async def test_method_list_with_all_params(self, async_client: AsyncTogether) -> None:
+ cluster = await async_client.beta.clusters.list(
+ project_id="project_id",
+ )
+ assert_matches_type(ClusterListResponse, cluster, path=["response"])
+
@parametrize
async def test_raw_response_list(self, async_client: AsyncTogether) -> None:
response = await async_client.beta.clusters.with_raw_response.list()
From 0d83e7edbfaae7e54461df8714cbe30c9519aad0 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 15 May 2026 00:12:06 +0000
Subject: [PATCH 02/14] fix(types): correct status field to enum in
cluster_storage model
---
.stats.yml | 4 ++--
src/together/types/beta/clusters/cluster_storage.py | 7 ++++++-
2 files changed, 8 insertions(+), 3 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index 409398869..1e6d5f98c 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 75
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/togetherai/togetherai-e398785d31ad51e94f158b377be469e74748e0229a964d3602149c6bf964581e.yml
-openapi_spec_hash: 3e4db6f27d100314926d87c9ae24cba2
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/togetherai/togetherai-5ba14fb08cb2129befa3b72c8bd20b67a863c4be6e010368e2184fbe1b18321b.yml
+openapi_spec_hash: 5b1c21199bda07c852b8b48c87859784
config_hash: ec427df08d61d8888138f15cd53c6454
diff --git a/src/together/types/beta/clusters/cluster_storage.py b/src/together/types/beta/clusters/cluster_storage.py
index 22392753f..024b79c04 100644
--- a/src/together/types/beta/clusters/cluster_storage.py
+++ b/src/together/types/beta/clusters/cluster_storage.py
@@ -1,5 +1,7 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+from typing_extensions import Literal
+
from ...._models import BaseModel
__all__ = ["ClusterStorage"]
@@ -8,7 +10,10 @@
class ClusterStorage(BaseModel):
size_tib: int
- status: str
+ status: Literal[
+ "scheduled", "available", "bound", "provisioning", "deleting", "failed", "access_revoked", "unknown"
+ ]
+ """Current status of the shared volume."""
volume_id: str
From 0e48b36f27b2eb7357198a026e7a4c548b590eef Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 15 May 2026 15:25:37 +0000
Subject: [PATCH 03/14] feat(api): add h200-140gb gpu_type to jig deploy/update
methods
---
.stats.yml | 4 ++--
src/together/resources/beta/jig/jig.py | 8 ++++----
src/together/types/beta/deployment.py | 2 +-
src/together/types/beta/jig_deploy_params.py | 2 +-
src/together/types/beta/jig_update_params.py | 2 +-
5 files changed, 9 insertions(+), 9 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index 1e6d5f98c..88217d8a8 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 75
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/togetherai/togetherai-5ba14fb08cb2129befa3b72c8bd20b67a863c4be6e010368e2184fbe1b18321b.yml
-openapi_spec_hash: 5b1c21199bda07c852b8b48c87859784
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/togetherai/togetherai-8f89a16f91cc70112c8f61ccfa8e22d08f3d898afdcbb92d3dec0ad6efba8fb2.yml
+openapi_spec_hash: c4dd7519d9865e6e2fcf5b9414b06bab
config_hash: ec427df08d61d8888138f15cd53c6454
diff --git a/src/together/resources/beta/jig/jig.py b/src/together/resources/beta/jig/jig.py
index 64ba1862f..fd20c3995 100644
--- a/src/together/resources/beta/jig/jig.py
+++ b/src/together/resources/beta/jig/jig.py
@@ -128,7 +128,7 @@ def update(
description: str | Omit = omit,
environment_variables: Iterable[jig_update_params.EnvironmentVariable] | Omit = omit,
gpu_count: int | Omit = omit,
- gpu_type: Literal["h100-80gb", "h100-40gb-mig", "b200-192gb"] | Omit = omit,
+ gpu_type: Literal["h100-80gb", "h100-40gb-mig", "h200-140gb", "b200-192gb"] | Omit = omit,
health_check_path: str | Omit = omit,
image: str | Omit = omit,
max_replicas: int | Omit = omit,
@@ -262,7 +262,7 @@ def list(
def deploy(
self,
*,
- gpu_type: Literal["h100-80gb", "h100-40gb-mig", "b200-192gb"],
+ gpu_type: Literal["h100-80gb", "h100-40gb-mig", "h200-140gb", "b200-192gb"],
image: str,
name: str,
args: SequenceNotStr[str] | Omit = omit,
@@ -538,7 +538,7 @@ async def update(
description: str | Omit = omit,
environment_variables: Iterable[jig_update_params.EnvironmentVariable] | Omit = omit,
gpu_count: int | Omit = omit,
- gpu_type: Literal["h100-80gb", "h100-40gb-mig", "b200-192gb"] | Omit = omit,
+ gpu_type: Literal["h100-80gb", "h100-40gb-mig", "h200-140gb", "b200-192gb"] | Omit = omit,
health_check_path: str | Omit = omit,
image: str | Omit = omit,
max_replicas: int | Omit = omit,
@@ -672,7 +672,7 @@ async def list(
async def deploy(
self,
*,
- gpu_type: Literal["h100-80gb", "h100-40gb-mig", "b200-192gb"],
+ gpu_type: Literal["h100-80gb", "h100-40gb-mig", "h200-140gb", "b200-192gb"],
image: str,
name: str,
args: SequenceNotStr[str] | Omit = omit,
diff --git a/src/together/types/beta/deployment.py b/src/together/types/beta/deployment.py
index f7e36e0af..2ff5b0589 100644
--- a/src/together/types/beta/deployment.py
+++ b/src/together/types/beta/deployment.py
@@ -196,7 +196,7 @@ class Deployment(BaseModel):
gpu_count: Optional[int] = None
"""GPUCount is the number of GPUs allocated to each replica in this deployment"""
- gpu_type: Optional[Literal["h100-80gb", "h100-40gb-mig", "b200-192gb"]] = None
+ gpu_type: Optional[Literal["h100-80gb", "h100-40gb-mig", "h200-140gb", "b200-192gb"]] = None
"""GPUType specifies the type of GPU requested (if any) for this deployment"""
health_check_path: Optional[str] = None
diff --git a/src/together/types/beta/jig_deploy_params.py b/src/together/types/beta/jig_deploy_params.py
index c0bdc9c54..9682a7a3a 100644
--- a/src/together/types/beta/jig_deploy_params.py
+++ b/src/together/types/beta/jig_deploy_params.py
@@ -19,7 +19,7 @@
class JigDeployParams(TypedDict, total=False):
- gpu_type: Required[Literal["h100-80gb", "h100-40gb-mig", "b200-192gb"]]
+ gpu_type: Required[Literal["h100-80gb", "h100-40gb-mig", "h200-140gb", "b200-192gb"]]
"""GPUType specifies the GPU hardware to use (e.g., "h100-80gb")."""
image: Required[str]
diff --git a/src/together/types/beta/jig_update_params.py b/src/together/types/beta/jig_update_params.py
index c91e46d92..56ad5d58c 100644
--- a/src/together/types/beta/jig_update_params.py
+++ b/src/together/types/beta/jig_update_params.py
@@ -52,7 +52,7 @@ class JigUpdateParams(TypedDict, total=False):
gpu_count: int
"""GPUCount is the number of GPUs to allocate per container instance"""
- gpu_type: Literal["h100-80gb", "h100-40gb-mig", "b200-192gb"]
+ gpu_type: Literal["h100-80gb", "h100-40gb-mig", "h200-140gb", "b200-192gb"]
"""GPUType specifies the GPU hardware to use (e.g., "h100-80gb")"""
health_check_path: str
From da57778c1fc871c440b10b1f486a573d5adeea46 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 15 May 2026 16:11:23 +0000
Subject: [PATCH 04/14] fix(api): make duration_days optional in clusters
create, size_tib optional in storage update
---
.stats.yml | 4 ++--
.../resources/beta/clusters/clusters.py | 16 +++++++-------
.../resources/beta/clusters/storage.py | 8 +++----
.../types/beta/cluster_create_params.py | 6 ++---
.../beta/clusters/storage_update_params.py | 4 ++--
.../beta/clusters/test_storage.py | 22 ++++++++++++++-----
tests/api_resources/beta/test_clusters.py | 10 ++-------
7 files changed, 37 insertions(+), 33 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index 88217d8a8..c39ae67d5 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 75
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/togetherai/togetherai-8f89a16f91cc70112c8f61ccfa8e22d08f3d898afdcbb92d3dec0ad6efba8fb2.yml
-openapi_spec_hash: c4dd7519d9865e6e2fcf5b9414b06bab
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/togetherai/togetherai-e070ac8df7cf6579d5f9f5f7e2896a85de93f46f0ab4aed2f6b48aa4af092204.yml
+openapi_spec_hash: 4603775ed88b25a4307e5d87299a23bb
config_hash: ec427df08d61d8888138f15cd53c6454
diff --git a/src/together/resources/beta/clusters/clusters.py b/src/together/resources/beta/clusters/clusters.py
index 935d71153..7b8f2f67a 100644
--- a/src/together/resources/beta/clusters/clusters.py
+++ b/src/together/resources/beta/clusters/clusters.py
@@ -66,7 +66,6 @@ def create(
billing_type: Literal["RESERVED", "ON_DEMAND", "SCHEDULED_CAPACITY"],
cluster_name: str,
cuda_version: str,
- duration_days: int,
gpu_type: Literal["H100_SXM", "H200_SXM", "RTX_6000_PCI", "L40_PCIE", "B200_SXM", "H100_SXM_INF"],
num_gpus: int,
nvidia_driver_version: str,
@@ -79,6 +78,7 @@ def create(
capacity_pool_id: str | Omit = omit,
cluster_config: cluster_create_params.ClusterConfig | Omit = omit,
cluster_type: Literal["KUBERNETES", "SLURM"] | Omit = omit,
+ duration_days: int | Omit = omit,
gpu_node_failover_enabled: bool | Omit = omit,
install_traefik: bool | Omit = omit,
num_capacity_pool_gpus: int | Omit = omit,
@@ -118,8 +118,6 @@ def create(
cuda_version: CUDA version for this cluster. For example, 12.5
- duration_days: Duration in days to keep the cluster running.
-
gpu_type: Type of GPU to use in the cluster
num_gpus: Number of GPUs to allocate in the cluster. This must be multiple of 8. For
@@ -151,6 +149,8 @@ def create(
cluster_type: Type of cluster to create.
+ duration_days: Duration in days to keep the cluster running.
+
gpu_node_failover_enabled: Whether automated GPU node failover should be enabled for this cluster. By
default, it is disabled.
@@ -202,7 +202,6 @@ def create(
"billing_type": billing_type,
"cluster_name": cluster_name,
"cuda_version": cuda_version,
- "duration_days": duration_days,
"gpu_type": gpu_type,
"num_gpus": num_gpus,
"nvidia_driver_version": nvidia_driver_version,
@@ -215,6 +214,7 @@ def create(
"capacity_pool_id": capacity_pool_id,
"cluster_config": cluster_config,
"cluster_type": cluster_type,
+ "duration_days": duration_days,
"gpu_node_failover_enabled": gpu_node_failover_enabled,
"install_traefik": install_traefik,
"num_capacity_pool_gpus": num_capacity_pool_gpus,
@@ -468,7 +468,6 @@ async def create(
billing_type: Literal["RESERVED", "ON_DEMAND", "SCHEDULED_CAPACITY"],
cluster_name: str,
cuda_version: str,
- duration_days: int,
gpu_type: Literal["H100_SXM", "H200_SXM", "RTX_6000_PCI", "L40_PCIE", "B200_SXM", "H100_SXM_INF"],
num_gpus: int,
nvidia_driver_version: str,
@@ -481,6 +480,7 @@ async def create(
capacity_pool_id: str | Omit = omit,
cluster_config: cluster_create_params.ClusterConfig | Omit = omit,
cluster_type: Literal["KUBERNETES", "SLURM"] | Omit = omit,
+ duration_days: int | Omit = omit,
gpu_node_failover_enabled: bool | Omit = omit,
install_traefik: bool | Omit = omit,
num_capacity_pool_gpus: int | Omit = omit,
@@ -520,8 +520,6 @@ async def create(
cuda_version: CUDA version for this cluster. For example, 12.5
- duration_days: Duration in days to keep the cluster running.
-
gpu_type: Type of GPU to use in the cluster
num_gpus: Number of GPUs to allocate in the cluster. This must be multiple of 8. For
@@ -553,6 +551,8 @@ async def create(
cluster_type: Type of cluster to create.
+ duration_days: Duration in days to keep the cluster running.
+
gpu_node_failover_enabled: Whether automated GPU node failover should be enabled for this cluster. By
default, it is disabled.
@@ -604,7 +604,6 @@ async def create(
"billing_type": billing_type,
"cluster_name": cluster_name,
"cuda_version": cuda_version,
- "duration_days": duration_days,
"gpu_type": gpu_type,
"num_gpus": num_gpus,
"nvidia_driver_version": nvidia_driver_version,
@@ -617,6 +616,7 @@ async def create(
"capacity_pool_id": capacity_pool_id,
"cluster_config": cluster_config,
"cluster_type": cluster_type,
+ "duration_days": duration_days,
"gpu_node_failover_enabled": gpu_node_failover_enabled,
"install_traefik": install_traefik,
"num_capacity_pool_gpus": num_capacity_pool_gpus,
diff --git a/src/together/resources/beta/clusters/storage.py b/src/together/resources/beta/clusters/storage.py
index c295061f3..48f6ccba7 100644
--- a/src/together/resources/beta/clusters/storage.py
+++ b/src/together/resources/beta/clusters/storage.py
@@ -132,8 +132,8 @@ def retrieve(
def update(
self,
*,
- size_tib: int,
volume_id: str,
+ size_tib: int | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
extra_headers: Headers | None = None,
@@ -157,8 +157,8 @@ def update(
"/compute/clusters/storage/volumes",
body=maybe_transform(
{
- "size_tib": size_tib,
"volume_id": volume_id,
+ "size_tib": size_tib,
},
storage_update_params.StorageUpdateParams,
),
@@ -354,8 +354,8 @@ async def retrieve(
async def update(
self,
*,
- size_tib: int,
volume_id: str,
+ size_tib: int | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
extra_headers: Headers | None = None,
@@ -379,8 +379,8 @@ async def update(
"/compute/clusters/storage/volumes",
body=await async_maybe_transform(
{
- "size_tib": size_tib,
"volume_id": volume_id,
+ "size_tib": size_tib,
},
storage_update_params.StorageUpdateParams,
),
diff --git a/src/together/types/beta/cluster_create_params.py b/src/together/types/beta/cluster_create_params.py
index 4d9482da5..b950a246d 100644
--- a/src/together/types/beta/cluster_create_params.py
+++ b/src/together/types/beta/cluster_create_params.py
@@ -40,9 +40,6 @@ class ClusterCreateParams(TypedDict, total=False):
cuda_version: Required[str]
"""CUDA version for this cluster. For example, 12.5"""
- duration_days: Required[int]
- """Duration in days to keep the cluster running."""
-
gpu_type: Required[Literal["H100_SXM", "H200_SXM", "RTX_6000_PCI", "L40_PCIE", "B200_SXM", "H100_SXM_INF"]]
"""Type of GPU to use in the cluster"""
@@ -105,6 +102,9 @@ class ClusterCreateParams(TypedDict, total=False):
cluster_type: Literal["KUBERNETES", "SLURM"]
"""Type of cluster to create."""
+ duration_days: int
+ """Duration in days to keep the cluster running."""
+
gpu_node_failover_enabled: bool
"""Whether automated GPU node failover should be enabled for this cluster.
diff --git a/src/together/types/beta/clusters/storage_update_params.py b/src/together/types/beta/clusters/storage_update_params.py
index c87e4cd61..116645fa7 100644
--- a/src/together/types/beta/clusters/storage_update_params.py
+++ b/src/together/types/beta/clusters/storage_update_params.py
@@ -8,6 +8,6 @@
class StorageUpdateParams(TypedDict, total=False):
- size_tib: Required[int]
-
volume_id: Required[str]
+
+ size_tib: int
diff --git a/tests/api_resources/beta/clusters/test_storage.py b/tests/api_resources/beta/clusters/test_storage.py
index 8403af1ae..de120cc40 100644
--- a/tests/api_resources/beta/clusters/test_storage.py
+++ b/tests/api_resources/beta/clusters/test_storage.py
@@ -109,15 +109,21 @@ def test_path_params_retrieve(self, client: Together) -> None:
@parametrize
def test_method_update(self, client: Together) -> None:
storage = client.beta.clusters.storage.update(
- size_tib=0,
volume_id="volume_id",
)
assert_matches_type(ClusterStorage, storage, path=["response"])
+ @parametrize
+ def test_method_update_with_all_params(self, client: Together) -> None:
+ storage = client.beta.clusters.storage.update(
+ volume_id="volume_id",
+ size_tib=0,
+ )
+ assert_matches_type(ClusterStorage, storage, path=["response"])
+
@parametrize
def test_raw_response_update(self, client: Together) -> None:
response = client.beta.clusters.storage.with_raw_response.update(
- size_tib=0,
volume_id="volume_id",
)
@@ -129,7 +135,6 @@ def test_raw_response_update(self, client: Together) -> None:
@parametrize
def test_streaming_response_update(self, client: Together) -> None:
with client.beta.clusters.storage.with_streaming_response.update(
- size_tib=0,
volume_id="volume_id",
) as response:
assert not response.is_closed
@@ -304,15 +309,21 @@ async def test_path_params_retrieve(self, async_client: AsyncTogether) -> None:
@parametrize
async def test_method_update(self, async_client: AsyncTogether) -> None:
storage = await async_client.beta.clusters.storage.update(
- size_tib=0,
volume_id="volume_id",
)
assert_matches_type(ClusterStorage, storage, path=["response"])
+ @parametrize
+ async def test_method_update_with_all_params(self, async_client: AsyncTogether) -> None:
+ storage = await async_client.beta.clusters.storage.update(
+ volume_id="volume_id",
+ size_tib=0,
+ )
+ assert_matches_type(ClusterStorage, storage, path=["response"])
+
@parametrize
async def test_raw_response_update(self, async_client: AsyncTogether) -> None:
response = await async_client.beta.clusters.storage.with_raw_response.update(
- size_tib=0,
volume_id="volume_id",
)
@@ -324,7 +335,6 @@ async def test_raw_response_update(self, async_client: AsyncTogether) -> None:
@parametrize
async def test_streaming_response_update(self, async_client: AsyncTogether) -> None:
async with async_client.beta.clusters.storage.with_streaming_response.update(
- size_tib=0,
volume_id="volume_id",
) as response:
assert not response.is_closed
diff --git a/tests/api_resources/beta/test_clusters.py b/tests/api_resources/beta/test_clusters.py
index 119b86f46..4d84e3b2b 100644
--- a/tests/api_resources/beta/test_clusters.py
+++ b/tests/api_resources/beta/test_clusters.py
@@ -29,7 +29,6 @@ def test_method_create(self, client: Together) -> None:
billing_type="RESERVED",
cluster_name="cluster_name",
cuda_version="cuda_version",
- duration_days=0,
gpu_type="H100_SXM",
num_gpus=0,
nvidia_driver_version="nvidia_driver_version",
@@ -43,7 +42,6 @@ def test_method_create_with_all_params(self, client: Together) -> None:
billing_type="RESERVED",
cluster_name="cluster_name",
cuda_version="cuda_version",
- duration_days=0,
gpu_type="H100_SXM",
num_gpus=0,
nvidia_driver_version="nvidia_driver_version",
@@ -89,6 +87,7 @@ def test_method_create_with_all_params(self, client: Together) -> None:
},
},
cluster_type="KUBERNETES",
+ duration_days=0,
gpu_node_failover_enabled=True,
install_traefik=True,
num_capacity_pool_gpus=0,
@@ -124,7 +123,6 @@ def test_raw_response_create(self, client: Together) -> None:
billing_type="RESERVED",
cluster_name="cluster_name",
cuda_version="cuda_version",
- duration_days=0,
gpu_type="H100_SXM",
num_gpus=0,
nvidia_driver_version="nvidia_driver_version",
@@ -142,7 +140,6 @@ def test_streaming_response_create(self, client: Together) -> None:
billing_type="RESERVED",
cluster_name="cluster_name",
cuda_version="cuda_version",
- duration_days=0,
gpu_type="H100_SXM",
num_gpus=0,
nvidia_driver_version="nvidia_driver_version",
@@ -377,7 +374,6 @@ async def test_method_create(self, async_client: AsyncTogether) -> None:
billing_type="RESERVED",
cluster_name="cluster_name",
cuda_version="cuda_version",
- duration_days=0,
gpu_type="H100_SXM",
num_gpus=0,
nvidia_driver_version="nvidia_driver_version",
@@ -391,7 +387,6 @@ async def test_method_create_with_all_params(self, async_client: AsyncTogether)
billing_type="RESERVED",
cluster_name="cluster_name",
cuda_version="cuda_version",
- duration_days=0,
gpu_type="H100_SXM",
num_gpus=0,
nvidia_driver_version="nvidia_driver_version",
@@ -437,6 +432,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncTogether)
},
},
cluster_type="KUBERNETES",
+ duration_days=0,
gpu_node_failover_enabled=True,
install_traefik=True,
num_capacity_pool_gpus=0,
@@ -472,7 +468,6 @@ async def test_raw_response_create(self, async_client: AsyncTogether) -> None:
billing_type="RESERVED",
cluster_name="cluster_name",
cuda_version="cuda_version",
- duration_days=0,
gpu_type="H100_SXM",
num_gpus=0,
nvidia_driver_version="nvidia_driver_version",
@@ -490,7 +485,6 @@ async def test_streaming_response_create(self, async_client: AsyncTogether) -> N
billing_type="RESERVED",
cluster_name="cluster_name",
cuda_version="cuda_version",
- duration_days=0,
gpu_type="H100_SXM",
num_gpus=0,
nvidia_driver_version="nvidia_driver_version",
From f8b715ce616125d550a2bd1734e7fbef2d201788 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 15 May 2026 17:37:16 +0000
Subject: [PATCH 05/14] feat: Sync deployments OpenAPI spec
---
.stats.yml | 4 +-
api.md | 2 +-
src/together/resources/beta/jig/jig.py | 30 ++++++++++-
src/together/resources/beta/jig/volumes.py | 20 +++++--
src/together/types/beta/deployment.py | 6 +++
src/together/types/beta/jig/__init__.py | 1 +
.../types/beta/jig/volume_retrieve_params.py | 12 +++++
.../types/beta/jig_retrieve_logs_params.py | 9 ++++
tests/api_resources/beta/jig/test_volumes.py | 53 +++++++++++++------
tests/api_resources/beta/test_jig.py | 4 ++
10 files changed, 116 insertions(+), 25 deletions(-)
create mode 100644 src/together/types/beta/jig/volume_retrieve_params.py
diff --git a/.stats.yml b/.stats.yml
index c39ae67d5..bd80968e7 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 75
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/togetherai/togetherai-e070ac8df7cf6579d5f9f5f7e2896a85de93f46f0ab4aed2f6b48aa4af092204.yml
-openapi_spec_hash: 4603775ed88b25a4307e5d87299a23bb
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/togetherai/togetherai-a5de4cd2b7b07333c8566d7dc805319e5b1727078b57e5236f8076a7ab4925d2.yml
+openapi_spec_hash: d087f1b24b5623db2c9a8d4a2d987a2f
config_hash: ec427df08d61d8888138f15cd53c6454
diff --git a/api.md b/api.md
index c32e307a7..abdf1cddd 100644
--- a/api.md
+++ b/api.md
@@ -48,7 +48,7 @@ from together.types.beta.jig import Volume, VolumeListResponse
Methods:
- client.beta.jig.volumes.create(\*\*params) -> Volume
-- client.beta.jig.volumes.retrieve(id) -> Volume
+- client.beta.jig.volumes.retrieve(id, \*\*params) -> Volume
- client.beta.jig.volumes.update(id, \*\*params) -> Volume
- client.beta.jig.volumes.list() -> VolumeListResponse
- client.beta.jig.volumes.delete(id) -> object
diff --git a/src/together/resources/beta/jig/jig.py b/src/together/resources/beta/jig/jig.py
index fd20c3995..8beda0e29 100644
--- a/src/together/resources/beta/jig/jig.py
+++ b/src/together/resources/beta/jig/jig.py
@@ -422,6 +422,8 @@ def retrieve_logs(
id: str,
*,
replica_id: str | Omit = omit,
+ revision: str | Omit = omit,
+ version: str | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
extra_headers: Headers | None = None,
@@ -437,6 +439,11 @@ def retrieve_logs(
replica_id: Replica ID to filter logs
+ revision: Deployment revision (UUID) to filter logs
+
+ version: Deployment image version (tag or last 4 characters of image digest) to filter
+ logs
+
extra_headers: Send extra headers
extra_query: Add additional query parameters to the request
@@ -454,7 +461,14 @@ def retrieve_logs(
extra_query=extra_query,
extra_body=extra_body,
timeout=timeout,
- query=maybe_transform({"replica_id": replica_id}, jig_retrieve_logs_params.JigRetrieveLogsParams),
+ query=maybe_transform(
+ {
+ "replica_id": replica_id,
+ "revision": revision,
+ "version": version,
+ },
+ jig_retrieve_logs_params.JigRetrieveLogsParams,
+ ),
),
cast_to=DeploymentLogs,
)
@@ -832,6 +846,8 @@ async def retrieve_logs(
id: str,
*,
replica_id: str | Omit = omit,
+ revision: str | Omit = omit,
+ version: str | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
extra_headers: Headers | None = None,
@@ -847,6 +863,11 @@ async def retrieve_logs(
replica_id: Replica ID to filter logs
+ revision: Deployment revision (UUID) to filter logs
+
+ version: Deployment image version (tag or last 4 characters of image digest) to filter
+ logs
+
extra_headers: Send extra headers
extra_query: Add additional query parameters to the request
@@ -865,7 +886,12 @@ async def retrieve_logs(
extra_body=extra_body,
timeout=timeout,
query=await async_maybe_transform(
- {"replica_id": replica_id}, jig_retrieve_logs_params.JigRetrieveLogsParams
+ {
+ "replica_id": replica_id,
+ "revision": revision,
+ "version": version,
+ },
+ jig_retrieve_logs_params.JigRetrieveLogsParams,
),
),
cast_to=DeploymentLogs,
diff --git a/src/together/resources/beta/jig/volumes.py b/src/together/resources/beta/jig/volumes.py
index 3b94490b7..c70d3ae85 100644
--- a/src/together/resources/beta/jig/volumes.py
+++ b/src/together/resources/beta/jig/volumes.py
@@ -17,7 +17,7 @@
async_to_streamed_response_wrapper,
)
from ...._base_client import make_request_options
-from ....types.beta.jig import volume_create_params, volume_update_params
+from ....types.beta.jig import volume_create_params, volume_update_params, volume_retrieve_params
from ....types.beta.jig.volume import Volume
from ....types.beta.jig.volume_list_response import VolumeListResponse
@@ -95,6 +95,7 @@ def retrieve(
self,
id: str,
*,
+ version: int | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
extra_headers: Headers | None = None,
@@ -108,6 +109,8 @@ def retrieve(
Args:
id: Volume ID or name
+ version: Volume version to describe (defaults to current version)
+
extra_headers: Send extra headers
extra_query: Add additional query parameters to the request
@@ -121,7 +124,11 @@ def retrieve(
return self._get(
path_template("/deployments/storage/volumes/{id}", id=id),
options=make_request_options(
- extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=maybe_transform({"version": version}, volume_retrieve_params.VolumeRetrieveParams),
),
cast_to=Volume,
)
@@ -304,6 +311,7 @@ async def retrieve(
self,
id: str,
*,
+ version: int | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
extra_headers: Headers | None = None,
@@ -317,6 +325,8 @@ async def retrieve(
Args:
id: Volume ID or name
+ version: Volume version to describe (defaults to current version)
+
extra_headers: Send extra headers
extra_query: Add additional query parameters to the request
@@ -330,7 +340,11 @@ async def retrieve(
return await self._get(
path_template("/deployments/storage/volumes/{id}", id=id),
options=make_request_options(
- extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=await async_maybe_transform({"version": version}, volume_retrieve_params.VolumeRetrieveParams),
),
cast_to=Volume,
)
diff --git a/src/together/types/beta/deployment.py b/src/together/types/beta/deployment.py
index 2ff5b0589..3ce2f599a 100644
--- a/src/together/types/beta/deployment.py
+++ b/src/together/types/beta/deployment.py
@@ -244,6 +244,12 @@ class Deployment(BaseModel):
allocated to each replica
"""
+ termination_grace_period_seconds: Optional[int] = None
+ """
+ TerminationGracePeriodSeconds is the time in seconds to wait for graceful
+ shutdown before forcefully terminating the replica
+ """
+
updated_at: Optional[datetime] = None
"""UpdatedAt is the ISO8601 timestamp when this deployment was last updated"""
diff --git a/src/together/types/beta/jig/__init__.py b/src/together/types/beta/jig/__init__.py
index ef03b5652..f8c024918 100644
--- a/src/together/types/beta/jig/__init__.py
+++ b/src/together/types/beta/jig/__init__.py
@@ -17,4 +17,5 @@
from .queue_retrieve_params import QueueRetrieveParams as QueueRetrieveParams
from .queue_submit_response import QueueSubmitResponse as QueueSubmitResponse
from .queue_metrics_response import QueueMetricsResponse as QueueMetricsResponse
+from .volume_retrieve_params import VolumeRetrieveParams as VolumeRetrieveParams
from .queue_retrieve_response import QueueRetrieveResponse as QueueRetrieveResponse
diff --git a/src/together/types/beta/jig/volume_retrieve_params.py b/src/together/types/beta/jig/volume_retrieve_params.py
new file mode 100644
index 000000000..7b194db4d
--- /dev/null
+++ b/src/together/types/beta/jig/volume_retrieve_params.py
@@ -0,0 +1,12 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import TypedDict
+
+__all__ = ["VolumeRetrieveParams"]
+
+
+class VolumeRetrieveParams(TypedDict, total=False):
+ version: int
+ """Volume version to describe (defaults to current version)"""
diff --git a/src/together/types/beta/jig_retrieve_logs_params.py b/src/together/types/beta/jig_retrieve_logs_params.py
index 8afbea2d8..59e0ca3cb 100644
--- a/src/together/types/beta/jig_retrieve_logs_params.py
+++ b/src/together/types/beta/jig_retrieve_logs_params.py
@@ -10,3 +10,12 @@
class JigRetrieveLogsParams(TypedDict, total=False):
replica_id: str
"""Replica ID to filter logs"""
+
+ revision: str
+ """Deployment revision (UUID) to filter logs"""
+
+ version: str
+ """
+ Deployment image version (tag or last 4 characters of image digest) to filter
+ logs
+ """
diff --git a/tests/api_resources/beta/jig/test_volumes.py b/tests/api_resources/beta/jig/test_volumes.py
index bc547d755..a630f5a83 100644
--- a/tests/api_resources/beta/jig/test_volumes.py
+++ b/tests/api_resources/beta/jig/test_volumes.py
@@ -9,7 +9,10 @@
from together import Together, AsyncTogether
from tests.utils import assert_matches_type
-from together.types.beta.jig import Volume, VolumeListResponse
+from together.types.beta.jig import (
+ Volume,
+ VolumeListResponse,
+)
base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
@@ -21,7 +24,7 @@ class TestVolumes:
def test_method_create(self, client: Together) -> None:
volume = client.beta.jig.volumes.create(
content={},
- name="name",
+ name="x",
type="readOnly",
)
assert_matches_type(Volume, volume, path=["response"])
@@ -33,7 +36,7 @@ def test_method_create_with_all_params(self, client: Together) -> None:
"source_prefix": "models/",
"type": "files",
},
- name="name",
+ name="x",
type="readOnly",
)
assert_matches_type(Volume, volume, path=["response"])
@@ -42,7 +45,7 @@ def test_method_create_with_all_params(self, client: Together) -> None:
def test_raw_response_create(self, client: Together) -> None:
response = client.beta.jig.volumes.with_raw_response.create(
content={},
- name="name",
+ name="x",
type="readOnly",
)
@@ -55,7 +58,7 @@ def test_raw_response_create(self, client: Together) -> None:
def test_streaming_response_create(self, client: Together) -> None:
with client.beta.jig.volumes.with_streaming_response.create(
content={},
- name="name",
+ name="x",
type="readOnly",
) as response:
assert not response.is_closed
@@ -69,14 +72,22 @@ def test_streaming_response_create(self, client: Together) -> None:
@parametrize
def test_method_retrieve(self, client: Together) -> None:
volume = client.beta.jig.volumes.retrieve(
- "id",
+ id="id",
+ )
+ assert_matches_type(Volume, volume, path=["response"])
+
+ @parametrize
+ def test_method_retrieve_with_all_params(self, client: Together) -> None:
+ volume = client.beta.jig.volumes.retrieve(
+ id="id",
+ version=0,
)
assert_matches_type(Volume, volume, path=["response"])
@parametrize
def test_raw_response_retrieve(self, client: Together) -> None:
response = client.beta.jig.volumes.with_raw_response.retrieve(
- "id",
+ id="id",
)
assert response.is_closed is True
@@ -87,7 +98,7 @@ def test_raw_response_retrieve(self, client: Together) -> None:
@parametrize
def test_streaming_response_retrieve(self, client: Together) -> None:
with client.beta.jig.volumes.with_streaming_response.retrieve(
- "id",
+ id="id",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -101,7 +112,7 @@ def test_streaming_response_retrieve(self, client: Together) -> None:
def test_path_params_retrieve(self, client: Together) -> None:
with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
client.beta.jig.volumes.with_raw_response.retrieve(
- "",
+ id="",
)
@parametrize
@@ -228,7 +239,7 @@ class TestAsyncVolumes:
async def test_method_create(self, async_client: AsyncTogether) -> None:
volume = await async_client.beta.jig.volumes.create(
content={},
- name="name",
+ name="x",
type="readOnly",
)
assert_matches_type(Volume, volume, path=["response"])
@@ -240,7 +251,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncTogether)
"source_prefix": "models/",
"type": "files",
},
- name="name",
+ name="x",
type="readOnly",
)
assert_matches_type(Volume, volume, path=["response"])
@@ -249,7 +260,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncTogether)
async def test_raw_response_create(self, async_client: AsyncTogether) -> None:
response = await async_client.beta.jig.volumes.with_raw_response.create(
content={},
- name="name",
+ name="x",
type="readOnly",
)
@@ -262,7 +273,7 @@ async def test_raw_response_create(self, async_client: AsyncTogether) -> None:
async def test_streaming_response_create(self, async_client: AsyncTogether) -> None:
async with async_client.beta.jig.volumes.with_streaming_response.create(
content={},
- name="name",
+ name="x",
type="readOnly",
) as response:
assert not response.is_closed
@@ -276,14 +287,22 @@ async def test_streaming_response_create(self, async_client: AsyncTogether) -> N
@parametrize
async def test_method_retrieve(self, async_client: AsyncTogether) -> None:
volume = await async_client.beta.jig.volumes.retrieve(
- "id",
+ id="id",
+ )
+ assert_matches_type(Volume, volume, path=["response"])
+
+ @parametrize
+ async def test_method_retrieve_with_all_params(self, async_client: AsyncTogether) -> None:
+ volume = await async_client.beta.jig.volumes.retrieve(
+ id="id",
+ version=0,
)
assert_matches_type(Volume, volume, path=["response"])
@parametrize
async def test_raw_response_retrieve(self, async_client: AsyncTogether) -> None:
response = await async_client.beta.jig.volumes.with_raw_response.retrieve(
- "id",
+ id="id",
)
assert response.is_closed is True
@@ -294,7 +313,7 @@ async def test_raw_response_retrieve(self, async_client: AsyncTogether) -> None:
@parametrize
async def test_streaming_response_retrieve(self, async_client: AsyncTogether) -> None:
async with async_client.beta.jig.volumes.with_streaming_response.retrieve(
- "id",
+ id="id",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -308,7 +327,7 @@ async def test_streaming_response_retrieve(self, async_client: AsyncTogether) ->
async def test_path_params_retrieve(self, async_client: AsyncTogether) -> None:
with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
await async_client.beta.jig.volumes.with_raw_response.retrieve(
- "",
+ id="",
)
@parametrize
diff --git a/tests/api_resources/beta/test_jig.py b/tests/api_resources/beta/test_jig.py
index 30f5e8303..dabf106ca 100644
--- a/tests/api_resources/beta/test_jig.py
+++ b/tests/api_resources/beta/test_jig.py
@@ -290,6 +290,8 @@ def test_method_retrieve_logs_with_all_params(self, client: Together) -> None:
jig = client.beta.jig.retrieve_logs(
id="id",
replica_id="replica_id",
+ revision="revision",
+ version="version",
)
assert_matches_type(DeploymentLogs, jig, path=["response"])
@@ -599,6 +601,8 @@ async def test_method_retrieve_logs_with_all_params(self, async_client: AsyncTog
jig = await async_client.beta.jig.retrieve_logs(
id="id",
replica_id="replica_id",
+ revision="revision",
+ version="version",
)
assert_matches_type(DeploymentLogs, jig, path=["response"])
From d1b00a5652245b68c0be7ba6beb7623411b79fc9 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 15 May 2026 17:39:39 +0000
Subject: [PATCH 06/14] docs(api): add parameter descriptions to storage
methods and types
---
.stats.yml | 4 ++--
src/together/resources/beta/clusters/storage.py | 16 ++++++++++++++++
src/together/types/beta/cluster.py | 6 +++++-
src/together/types/beta/cluster_create_params.py | 2 ++
.../types/beta/clusters/cluster_storage.py | 3 +++
.../types/beta/clusters/storage_create_params.py | 2 ++
.../types/beta/clusters/storage_update_params.py | 2 ++
7 files changed, 32 insertions(+), 3 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index bd80968e7..232dc112a 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 75
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/togetherai/togetherai-a5de4cd2b7b07333c8566d7dc805319e5b1727078b57e5236f8076a7ab4925d2.yml
-openapi_spec_hash: d087f1b24b5623db2c9a8d4a2d987a2f
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/togetherai/togetherai-ae3cfb67611b42139b6d37809a00cf0c26b4803f974a3defd3ce64394fa08e1b.yml
+openapi_spec_hash: 50c3ce80ecc7d25283c96e3544567995
config_hash: ec427df08d61d8888138f15cd53c6454
diff --git a/src/together/resources/beta/clusters/storage.py b/src/together/resources/beta/clusters/storage.py
index 48f6ccba7..2493f96cf 100644
--- a/src/together/resources/beta/clusters/storage.py
+++ b/src/together/resources/beta/clusters/storage.py
@@ -65,8 +65,12 @@ def create(
performance for shared storage.
Args:
+ region: Region name. Usable regions can be found from `clusters.list_regions()`
+
size_tib: Volume size in whole tebibytes (TiB).
+ volume_name: User provided name of the volume.
+
is_lifecycle_independent: When true, the shared volume is not deleted when the cluster is decommissioned.
extra_headers: Send extra headers
@@ -145,6 +149,10 @@ def update(
Update the configuration of an existing shared volume.
Args:
+ volume_id: ID of the volume.
+
+ size_tib: Size of the volume in TiB.
+
extra_headers: Send extra headers
extra_query: Add additional query parameters to the request
@@ -287,8 +295,12 @@ async def create(
performance for shared storage.
Args:
+ region: Region name. Usable regions can be found from `clusters.list_regions()`
+
size_tib: Volume size in whole tebibytes (TiB).
+ volume_name: User provided name of the volume.
+
is_lifecycle_independent: When true, the shared volume is not deleted when the cluster is decommissioned.
extra_headers: Send extra headers
@@ -367,6 +379,10 @@ async def update(
Update the configuration of an existing shared volume.
Args:
+ volume_id: ID of the volume.
+
+ size_tib: Size of the volume in TiB.
+
extra_headers: Send extra headers
extra_query: Add additional query parameters to the request
diff --git a/src/together/types/beta/cluster.py b/src/together/types/beta/cluster.py
index 400108cfb..e78c9a07a 100644
--- a/src/together/types/beta/cluster.py
+++ b/src/together/types/beta/cluster.py
@@ -126,7 +126,7 @@ class GPUWorkerNodePhaseTransition(BaseModel):
class GPUWorkerNodeLatestRemediation(BaseModel):
"""
Remediation represents a node remediation request for an instance.
- An instance can have multiple remediations over time (e.g., failed attempts followed by retries).
+ An instance can have multiple remediations over time (e.g., failed attempts followed by retries).
"""
id: str
@@ -268,12 +268,16 @@ class PhaseTransition(BaseModel):
class Volume(BaseModel):
size_tib: int
+ """Size of the volume in TiB."""
status: str
+ """Current status of the volume."""
volume_id: str
+ """ID of the volume."""
volume_name: str
+ """User provided name of the volume."""
class ClusterConfigIngress(BaseModel):
diff --git a/src/together/types/beta/cluster_create_params.py b/src/together/types/beta/cluster_create_params.py
index b950a246d..9ac607436 100644
--- a/src/together/types/beta/cluster_create_params.py
+++ b/src/together/types/beta/cluster_create_params.py
@@ -329,11 +329,13 @@ class SharedVolume(TypedDict, total=False):
"""Inline configuration to create a shared volume with the cluster creation."""
region: Required[str]
+ """Region name. Usable regions can be found from `clusters.list_regions()`"""
size_tib: Required[int]
"""Volume size in whole tebibytes (TiB)."""
volume_name: Required[str]
+ """User provided name of the volume."""
is_lifecycle_independent: bool
"""When true, the shared volume is not deleted when the cluster is decommissioned."""
diff --git a/src/together/types/beta/clusters/cluster_storage.py b/src/together/types/beta/clusters/cluster_storage.py
index 024b79c04..8daaefc14 100644
--- a/src/together/types/beta/clusters/cluster_storage.py
+++ b/src/together/types/beta/clusters/cluster_storage.py
@@ -9,6 +9,7 @@
class ClusterStorage(BaseModel):
size_tib: int
+ """Size of the volume in TiB."""
status: Literal[
"scheduled", "available", "bound", "provisioning", "deleting", "failed", "access_revoked", "unknown"
@@ -16,5 +17,7 @@ class ClusterStorage(BaseModel):
"""Current status of the shared volume."""
volume_id: str
+ """ID of the volume."""
volume_name: str
+ """User provided name of the volume."""
diff --git a/src/together/types/beta/clusters/storage_create_params.py b/src/together/types/beta/clusters/storage_create_params.py
index 2dfb9218f..f368b0e8e 100644
--- a/src/together/types/beta/clusters/storage_create_params.py
+++ b/src/together/types/beta/clusters/storage_create_params.py
@@ -9,11 +9,13 @@
class StorageCreateParams(TypedDict, total=False):
region: Required[str]
+ """Region name. Usable regions can be found from `clusters.list_regions()`"""
size_tib: Required[int]
"""Volume size in whole tebibytes (TiB)."""
volume_name: Required[str]
+ """User provided name of the volume."""
is_lifecycle_independent: bool
"""When true, the shared volume is not deleted when the cluster is decommissioned."""
diff --git a/src/together/types/beta/clusters/storage_update_params.py b/src/together/types/beta/clusters/storage_update_params.py
index 116645fa7..6e6a0162a 100644
--- a/src/together/types/beta/clusters/storage_update_params.py
+++ b/src/together/types/beta/clusters/storage_update_params.py
@@ -9,5 +9,7 @@
class StorageUpdateParams(TypedDict, total=False):
volume_id: Required[str]
+ """ID of the volume."""
size_tib: int
+ """Size of the volume in TiB."""
From 5af43e9a8ccb6d62f1633adcc3394553fabd7ca8 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 15 May 2026 17:53:42 +0000
Subject: [PATCH 07/14] feat(api): Add node remediation APIs to clusters sdks
---
.stats.yml | 6 +-
api.md | 22 +
.../resources/beta/clusters/__init__.py | 14 +
.../resources/beta/clusters/clusters.py | 32 +
.../resources/beta/clusters/remediations.py | 849 ++++++++++++++++++
src/together/types/beta/clusters/__init__.py | 9 +
.../clusters/remediation_approve_params.py | 18 +
.../clusters/remediation_approve_response.py | 94 ++
.../clusters/remediation_cancel_response.py | 94 ++
.../clusters/remediation_create_params.py | 34 +
.../clusters/remediation_create_response.py | 94 ++
.../beta/clusters/remediation_list_params.py | 47 +
.../clusters/remediation_list_response.py | 107 +++
.../clusters/remediation_reject_params.py | 18 +
.../clusters/remediation_reject_response.py | 94 ++
.../beta/clusters/test_remediations.py | 682 ++++++++++++++
16 files changed, 2211 insertions(+), 3 deletions(-)
create mode 100644 src/together/resources/beta/clusters/remediations.py
create mode 100644 src/together/types/beta/clusters/remediation_approve_params.py
create mode 100644 src/together/types/beta/clusters/remediation_approve_response.py
create mode 100644 src/together/types/beta/clusters/remediation_cancel_response.py
create mode 100644 src/together/types/beta/clusters/remediation_create_params.py
create mode 100644 src/together/types/beta/clusters/remediation_create_response.py
create mode 100644 src/together/types/beta/clusters/remediation_list_params.py
create mode 100644 src/together/types/beta/clusters/remediation_list_response.py
create mode 100644 src/together/types/beta/clusters/remediation_reject_params.py
create mode 100644 src/together/types/beta/clusters/remediation_reject_response.py
create mode 100644 tests/api_resources/beta/clusters/test_remediations.py
diff --git a/.stats.yml b/.stats.yml
index 232dc112a..3b8397a01 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
-configured_endpoints: 75
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/togetherai/togetherai-ae3cfb67611b42139b6d37809a00cf0c26b4803f974a3defd3ce64394fa08e1b.yml
+configured_endpoints: 80
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/togetherai/togetherai-496df09bdf8bfe14dddc5ba6dc70e219336f51295d2f0671285b4056ec756f21.yml
openapi_spec_hash: 50c3ce80ecc7d25283c96e3544567995
-config_hash: ec427df08d61d8888138f15cd53c6454
+config_hash: 2ab62260e2c5e9527b47bef3f36e545d
diff --git a/api.md b/api.md
index abdf1cddd..1493164db 100644
--- a/api.md
+++ b/api.md
@@ -91,6 +91,28 @@ Methods:
- client.beta.clusters.delete(cluster_id) -> ClusterDeleteResponse
- client.beta.clusters.list_regions() -> ClusterListRegionsResponse
+### Remediations
+
+Types:
+
+```python
+from together.types.beta.clusters import (
+ RemediationCreateResponse,
+ RemediationListResponse,
+ RemediationApproveResponse,
+ RemediationCancelResponse,
+ RemediationRejectResponse,
+)
+```
+
+Methods:
+
+- client.beta.clusters.remediations.create(instance_id, \*, cluster_id, \*\*params) -> RemediationCreateResponse
+- client.beta.clusters.remediations.list(instance_id, \*, cluster_id, \*\*params) -> RemediationListResponse
+- client.beta.clusters.remediations.approve(remediation_id, \*, cluster_id, instance_id, \*\*params) -> RemediationApproveResponse
+- client.beta.clusters.remediations.cancel(remediation_id, \*, cluster_id, instance_id) -> RemediationCancelResponse
+- client.beta.clusters.remediations.reject(remediation_id, \*, cluster_id, instance_id, \*\*params) -> RemediationRejectResponse
+
### Storage
Types:
diff --git a/src/together/resources/beta/clusters/__init__.py b/src/together/resources/beta/clusters/__init__.py
index a428a2a51..58a0f2342 100644
--- a/src/together/resources/beta/clusters/__init__.py
+++ b/src/together/resources/beta/clusters/__init__.py
@@ -16,8 +16,22 @@
ClustersResourceWithStreamingResponse,
AsyncClustersResourceWithStreamingResponse,
)
+from .remediations import (
+ RemediationsResource,
+ AsyncRemediationsResource,
+ RemediationsResourceWithRawResponse,
+ AsyncRemediationsResourceWithRawResponse,
+ RemediationsResourceWithStreamingResponse,
+ AsyncRemediationsResourceWithStreamingResponse,
+)
__all__ = [
+ "RemediationsResource",
+ "AsyncRemediationsResource",
+ "RemediationsResourceWithRawResponse",
+ "AsyncRemediationsResourceWithRawResponse",
+ "RemediationsResourceWithStreamingResponse",
+ "AsyncRemediationsResourceWithStreamingResponse",
"StorageResource",
"AsyncStorageResource",
"StorageResourceWithRawResponse",
diff --git a/src/together/resources/beta/clusters/clusters.py b/src/together/resources/beta/clusters/clusters.py
index 7b8f2f67a..75efca2fe 100644
--- a/src/together/resources/beta/clusters/clusters.py
+++ b/src/together/resources/beta/clusters/clusters.py
@@ -26,6 +26,14 @@
async_to_raw_response_wrapper,
async_to_streamed_response_wrapper,
)
+from .remediations import (
+ RemediationsResource,
+ AsyncRemediationsResource,
+ RemediationsResourceWithRawResponse,
+ AsyncRemediationsResourceWithRawResponse,
+ RemediationsResourceWithStreamingResponse,
+ AsyncRemediationsResourceWithStreamingResponse,
+)
from ....types.beta import cluster_list_params, cluster_create_params, cluster_update_params
from ...._base_client import make_request_options
from ....types.beta.cluster import Cluster
@@ -37,6 +45,10 @@
class ClustersResource(SyncAPIResource):
+ @cached_property
+ def remediations(self) -> RemediationsResource:
+ return RemediationsResource(self._client)
+
@cached_property
def storage(self) -> StorageResource:
return StorageResource(self._client)
@@ -439,6 +451,10 @@ def list_regions(
class AsyncClustersResource(AsyncAPIResource):
+ @cached_property
+ def remediations(self) -> AsyncRemediationsResource:
+ return AsyncRemediationsResource(self._client)
+
@cached_property
def storage(self) -> AsyncStorageResource:
return AsyncStorageResource(self._client)
@@ -863,6 +879,10 @@ def __init__(self, clusters: ClustersResource) -> None:
clusters.list_regions,
)
+ @cached_property
+ def remediations(self) -> RemediationsResourceWithRawResponse:
+ return RemediationsResourceWithRawResponse(self._clusters.remediations)
+
@cached_property
def storage(self) -> StorageResourceWithRawResponse:
return StorageResourceWithRawResponse(self._clusters.storage)
@@ -891,6 +911,10 @@ def __init__(self, clusters: AsyncClustersResource) -> None:
clusters.list_regions,
)
+ @cached_property
+ def remediations(self) -> AsyncRemediationsResourceWithRawResponse:
+ return AsyncRemediationsResourceWithRawResponse(self._clusters.remediations)
+
@cached_property
def storage(self) -> AsyncStorageResourceWithRawResponse:
return AsyncStorageResourceWithRawResponse(self._clusters.storage)
@@ -919,6 +943,10 @@ def __init__(self, clusters: ClustersResource) -> None:
clusters.list_regions,
)
+ @cached_property
+ def remediations(self) -> RemediationsResourceWithStreamingResponse:
+ return RemediationsResourceWithStreamingResponse(self._clusters.remediations)
+
@cached_property
def storage(self) -> StorageResourceWithStreamingResponse:
return StorageResourceWithStreamingResponse(self._clusters.storage)
@@ -947,6 +975,10 @@ def __init__(self, clusters: AsyncClustersResource) -> None:
clusters.list_regions,
)
+ @cached_property
+ def remediations(self) -> AsyncRemediationsResourceWithStreamingResponse:
+ return AsyncRemediationsResourceWithStreamingResponse(self._clusters.remediations)
+
@cached_property
def storage(self) -> AsyncStorageResourceWithStreamingResponse:
return AsyncStorageResourceWithStreamingResponse(self._clusters.storage)
diff --git a/src/together/resources/beta/clusters/remediations.py b/src/together/resources/beta/clusters/remediations.py
new file mode 100644
index 000000000..c130fb213
--- /dev/null
+++ b/src/together/resources/beta/clusters/remediations.py
@@ -0,0 +1,849 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import List
+from typing_extensions import Literal
+
+import httpx
+
+from ...._types import Body, Omit, Query, Headers, NotGiven, omit, not_given
+from ...._utils import path_template, maybe_transform, async_maybe_transform
+from ...._compat import cached_property
+from ...._resource import SyncAPIResource, AsyncAPIResource
+from ...._response import (
+ to_raw_response_wrapper,
+ to_streamed_response_wrapper,
+ async_to_raw_response_wrapper,
+ async_to_streamed_response_wrapper,
+)
+from ...._base_client import make_request_options
+from ....types.beta.clusters import (
+ remediation_list_params,
+ remediation_create_params,
+ remediation_reject_params,
+ remediation_approve_params,
+)
+from ....types.beta.clusters.remediation_list_response import RemediationListResponse
+from ....types.beta.clusters.remediation_cancel_response import RemediationCancelResponse
+from ....types.beta.clusters.remediation_create_response import RemediationCreateResponse
+from ....types.beta.clusters.remediation_reject_response import RemediationRejectResponse
+from ....types.beta.clusters.remediation_approve_response import RemediationApproveResponse
+
+__all__ = ["RemediationsResource", "AsyncRemediationsResource"]
+
+
+class RemediationsResource(SyncAPIResource):
+ @cached_property
+ def with_raw_response(self) -> RemediationsResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/togethercomputer/together-py#accessing-raw-response-data-eg-headers
+ """
+ return RemediationsResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> RemediationsResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/togethercomputer/together-py#with_streaming_response
+ """
+ return RemediationsResourceWithStreamingResponse(self)
+
+ def create(
+ self,
+ instance_id: str,
+ *,
+ cluster_id: str,
+ mode: Literal[
+ "REMEDIATION_MODE_VM_ONLY",
+ "REMEDIATION_MODE_HOST_AWARE",
+ "REMEDIATION_MODE_EVICT_WITHOUT_REPLACEMENT",
+ "REMEDIATION_MODE_REBOOT_VM",
+ ],
+ remediation_id: str | Omit = omit,
+ reason: str | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> RemediationCreateResponse:
+ """
+ Creates a new remediation for an instance.
+
+ If mode is unspecified, it defaults to VM_ONLY. If trigger is unspecified, it
+ defaults to MANUAL.
+
+ For MANUAL triggers: The remediation goes directly to PENDING state.
+
+ For AUTOMATED triggers: The remediation is created with PENDING_APPROVAL state.
+ The caller must then use ApproveRemediation to start the remediation process.
+
+ Args:
+ cluster_id: The cluster ID.
+
+ instance_id: The instance ID.
+
+ mode: Remediation mode specifies how the remediation should be performed.
+
+ - `REMEDIATION_MODE_VM_ONLY`: Deletes the VM and provisions a new one on any
+ available host.
+ - `REMEDIATION_MODE_HOST_AWARE`: Cordons the host, deletes the VM, and
+ provisions a new one on a different host.
+
+ remediation_id: Optional. Client-specified ID for idempotency.
+
+ reason: User-provided reason for the remediation.
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not cluster_id:
+ raise ValueError(f"Expected a non-empty value for `cluster_id` but received {cluster_id!r}")
+ if not instance_id:
+ raise ValueError(f"Expected a non-empty value for `instance_id` but received {instance_id!r}")
+ return self._post(
+ path_template(
+ "/compute/clusters/{cluster_id}/instances/{instance_id}/remediations",
+ cluster_id=cluster_id,
+ instance_id=instance_id,
+ ),
+ body=maybe_transform(
+ {
+ "mode": mode,
+ "reason": reason,
+ },
+ remediation_create_params.RemediationCreateParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=maybe_transform(
+ {"remediation_id": remediation_id}, remediation_create_params.RemediationCreateParams
+ ),
+ ),
+ cast_to=RemediationCreateResponse,
+ )
+
+ def list(
+ self,
+ instance_id: str,
+ *,
+ cluster_id: str,
+ mode: Literal[
+ "REMEDIATION_MODE_VM_ONLY",
+ "REMEDIATION_MODE_HOST_AWARE",
+ "REMEDIATION_MODE_EVICT_WITHOUT_REPLACEMENT",
+ "REMEDIATION_MODE_REBOOT_VM",
+ ]
+ | Omit = omit,
+ order_by: str | Omit = omit,
+ page_size: int | Omit = omit,
+ page_token: str | Omit = omit,
+ state: List[
+ Literal["PENDING_APPROVAL", "PENDING", "RUNNING", "SUCCEEDED", "FAILED", "CANCELLED", "AUTO_RESOLVED"]
+ ]
+ | Omit = omit,
+ trigger: Literal["REMEDIATION_TRIGGER_MANUAL", "REMEDIATION_TRIGGER_AUTOMATED"] | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> RemediationListResponse:
+ """Lists remediations for an instance or cluster.
+
+ Use instances/- as wildcard to
+ list all remediations in a cluster.
+
+ Args:
+ cluster_id: The cluster ID.
+
+ instance_id: The instance ID.
+
+ mode: Optional. Filter by remediation mode. Returns only remediations matching the
+ specified mode.
+
+ order_by: Optional. Order by expression.
+
+ page_size: Optional. Maximum results to return.
+
+ page_token: Optional. Pagination token from previous request.
+
+ state: Optional. Filter by state(s). Returns remediations matching any of the specified
+ states.
+
+ trigger: Optional. Filter by trigger type. Returns only remediations matching the
+ specified trigger.
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not cluster_id:
+ raise ValueError(f"Expected a non-empty value for `cluster_id` but received {cluster_id!r}")
+ if not instance_id:
+ raise ValueError(f"Expected a non-empty value for `instance_id` but received {instance_id!r}")
+ return self._get(
+ path_template(
+ "/compute/clusters/{cluster_id}/instances/{instance_id}/remediations",
+ cluster_id=cluster_id,
+ instance_id=instance_id,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=maybe_transform(
+ {
+ "mode": mode,
+ "order_by": order_by,
+ "page_size": page_size,
+ "page_token": page_token,
+ "state": state,
+ "trigger": trigger,
+ },
+ remediation_list_params.RemediationListParams,
+ ),
+ ),
+ cast_to=RemediationListResponse,
+ )
+
+ def approve(
+ self,
+ remediation_id: str,
+ *,
+ cluster_id: str,
+ instance_id: str,
+ comment: str | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> RemediationApproveResponse:
+ """
+ Approves a pending remediation.
+
+ Only remediations with state PENDING_APPROVAL can be approved.
+
+ On APPROVE: state changes to PENDING and the remediation process begins. The
+ reviewed_by, review_time, and review_comment fields are populated on the
+ remediation after approval.
+
+ Args:
+ cluster_id: The cluster ID.
+
+ instance_id: The instance ID.
+
+ remediation_id: The remediation ID.
+
+ comment: Comment explaining the action.
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not cluster_id:
+ raise ValueError(f"Expected a non-empty value for `cluster_id` but received {cluster_id!r}")
+ if not instance_id:
+ raise ValueError(f"Expected a non-empty value for `instance_id` but received {instance_id!r}")
+ if not remediation_id:
+ raise ValueError(f"Expected a non-empty value for `remediation_id` but received {remediation_id!r}")
+ return self._post(
+ path_template(
+ "/compute/clusters/{cluster_id}/instances/{instance_id}/remediations/{remediation_id}/approve",
+ cluster_id=cluster_id,
+ instance_id=instance_id,
+ remediation_id=remediation_id,
+ ),
+ body=maybe_transform({"comment": comment}, remediation_approve_params.RemediationApproveParams),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=RemediationApproveResponse,
+ )
+
+ def cancel(
+ self,
+ remediation_id: str,
+ *,
+ cluster_id: str,
+ instance_id: str,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> RemediationCancelResponse:
+ """
+ Cancels a pending remediation.
+
+ Only remediations in PENDING_APPROVAL or PENDING state can be cancelled.
+
+ Args:
+ cluster_id: The cluster ID.
+
+ instance_id: The instance ID.
+
+ remediation_id: The remediation ID.
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not cluster_id:
+ raise ValueError(f"Expected a non-empty value for `cluster_id` but received {cluster_id!r}")
+ if not instance_id:
+ raise ValueError(f"Expected a non-empty value for `instance_id` but received {instance_id!r}")
+ if not remediation_id:
+ raise ValueError(f"Expected a non-empty value for `remediation_id` but received {remediation_id!r}")
+ return self._post(
+ path_template(
+ "/compute/clusters/{cluster_id}/instances/{instance_id}/remediations/{remediation_id}/cancel",
+ cluster_id=cluster_id,
+ instance_id=instance_id,
+ remediation_id=remediation_id,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=RemediationCancelResponse,
+ )
+
+ def reject(
+ self,
+ remediation_id: str,
+ *,
+ cluster_id: str,
+ instance_id: str,
+ comment: str | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> RemediationRejectResponse:
+ """
+ Rejects a pending remediation.
+
+ Only remediations with state PENDING_APPROVAL can be rejected.
+
+ On REJECT: state changes to CANCELLED. The reviewed_by, review_time, and
+ review_comment fields are populated on the remediation after rejection.
+
+ Args:
+ cluster_id: The cluster ID.
+
+ instance_id: The instance ID.
+
+ remediation_id: The remediation ID.
+
+ comment: Comment explaining the action.
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not cluster_id:
+ raise ValueError(f"Expected a non-empty value for `cluster_id` but received {cluster_id!r}")
+ if not instance_id:
+ raise ValueError(f"Expected a non-empty value for `instance_id` but received {instance_id!r}")
+ if not remediation_id:
+ raise ValueError(f"Expected a non-empty value for `remediation_id` but received {remediation_id!r}")
+ return self._post(
+ path_template(
+ "/compute/clusters/{cluster_id}/instances/{instance_id}/remediations/{remediation_id}/reject",
+ cluster_id=cluster_id,
+ instance_id=instance_id,
+ remediation_id=remediation_id,
+ ),
+ body=maybe_transform({"comment": comment}, remediation_reject_params.RemediationRejectParams),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=RemediationRejectResponse,
+ )
+
+
+class AsyncRemediationsResource(AsyncAPIResource):
+ @cached_property
+ def with_raw_response(self) -> AsyncRemediationsResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/togethercomputer/together-py#accessing-raw-response-data-eg-headers
+ """
+ return AsyncRemediationsResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> AsyncRemediationsResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/togethercomputer/together-py#with_streaming_response
+ """
+ return AsyncRemediationsResourceWithStreamingResponse(self)
+
+ async def create(
+ self,
+ instance_id: str,
+ *,
+ cluster_id: str,
+ mode: Literal[
+ "REMEDIATION_MODE_VM_ONLY",
+ "REMEDIATION_MODE_HOST_AWARE",
+ "REMEDIATION_MODE_EVICT_WITHOUT_REPLACEMENT",
+ "REMEDIATION_MODE_REBOOT_VM",
+ ],
+ remediation_id: str | Omit = omit,
+ reason: str | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> RemediationCreateResponse:
+ """
+ Creates a new remediation for an instance.
+
+ If mode is unspecified, it defaults to VM_ONLY. If trigger is unspecified, it
+ defaults to MANUAL.
+
+ For MANUAL triggers: The remediation goes directly to PENDING state.
+
+ For AUTOMATED triggers: The remediation is created with PENDING_APPROVAL state.
+ The caller must then use ApproveRemediation to start the remediation process.
+
+ Args:
+ cluster_id: The cluster ID.
+
+ instance_id: The instance ID.
+
+ mode: Remediation mode specifies how the remediation should be performed.
+
+ - `REMEDIATION_MODE_VM_ONLY`: Deletes the VM and provisions a new one on any
+ available host.
+ - `REMEDIATION_MODE_HOST_AWARE`: Cordons the host, deletes the VM, and
+ provisions a new one on a different host.
+
+ remediation_id: Optional. Client-specified ID for idempotency.
+
+ reason: User-provided reason for the remediation.
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not cluster_id:
+ raise ValueError(f"Expected a non-empty value for `cluster_id` but received {cluster_id!r}")
+ if not instance_id:
+ raise ValueError(f"Expected a non-empty value for `instance_id` but received {instance_id!r}")
+ return await self._post(
+ path_template(
+ "/compute/clusters/{cluster_id}/instances/{instance_id}/remediations",
+ cluster_id=cluster_id,
+ instance_id=instance_id,
+ ),
+ body=await async_maybe_transform(
+ {
+ "mode": mode,
+ "reason": reason,
+ },
+ remediation_create_params.RemediationCreateParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=await async_maybe_transform(
+ {"remediation_id": remediation_id}, remediation_create_params.RemediationCreateParams
+ ),
+ ),
+ cast_to=RemediationCreateResponse,
+ )
+
+ async def list(
+ self,
+ instance_id: str,
+ *,
+ cluster_id: str,
+ mode: Literal[
+ "REMEDIATION_MODE_VM_ONLY",
+ "REMEDIATION_MODE_HOST_AWARE",
+ "REMEDIATION_MODE_EVICT_WITHOUT_REPLACEMENT",
+ "REMEDIATION_MODE_REBOOT_VM",
+ ]
+ | Omit = omit,
+ order_by: str | Omit = omit,
+ page_size: int | Omit = omit,
+ page_token: str | Omit = omit,
+ state: List[
+ Literal["PENDING_APPROVAL", "PENDING", "RUNNING", "SUCCEEDED", "FAILED", "CANCELLED", "AUTO_RESOLVED"]
+ ]
+ | Omit = omit,
+ trigger: Literal["REMEDIATION_TRIGGER_MANUAL", "REMEDIATION_TRIGGER_AUTOMATED"] | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> RemediationListResponse:
+ """Lists remediations for an instance or cluster.
+
+ Use instances/- as wildcard to
+ list all remediations in a cluster.
+
+ Args:
+ cluster_id: The cluster ID.
+
+ instance_id: The instance ID.
+
+ mode: Optional. Filter by remediation mode. Returns only remediations matching the
+ specified mode.
+
+ order_by: Optional. Order by expression.
+
+ page_size: Optional. Maximum results to return.
+
+ page_token: Optional. Pagination token from previous request.
+
+ state: Optional. Filter by state(s). Returns remediations matching any of the specified
+ states.
+
+ trigger: Optional. Filter by trigger type. Returns only remediations matching the
+ specified trigger.
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not cluster_id:
+ raise ValueError(f"Expected a non-empty value for `cluster_id` but received {cluster_id!r}")
+ if not instance_id:
+ raise ValueError(f"Expected a non-empty value for `instance_id` but received {instance_id!r}")
+ return await self._get(
+ path_template(
+ "/compute/clusters/{cluster_id}/instances/{instance_id}/remediations",
+ cluster_id=cluster_id,
+ instance_id=instance_id,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=await async_maybe_transform(
+ {
+ "mode": mode,
+ "order_by": order_by,
+ "page_size": page_size,
+ "page_token": page_token,
+ "state": state,
+ "trigger": trigger,
+ },
+ remediation_list_params.RemediationListParams,
+ ),
+ ),
+ cast_to=RemediationListResponse,
+ )
+
+ async def approve(
+ self,
+ remediation_id: str,
+ *,
+ cluster_id: str,
+ instance_id: str,
+ comment: str | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> RemediationApproveResponse:
+ """
+ Approves a pending remediation.
+
+ Only remediations with state PENDING_APPROVAL can be approved.
+
+ On APPROVE: state changes to PENDING and the remediation process begins. The
+ reviewed_by, review_time, and review_comment fields are populated on the
+ remediation after approval.
+
+ Args:
+ cluster_id: The cluster ID.
+
+ instance_id: The instance ID.
+
+ remediation_id: The remediation ID.
+
+ comment: Comment explaining the action.
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not cluster_id:
+ raise ValueError(f"Expected a non-empty value for `cluster_id` but received {cluster_id!r}")
+ if not instance_id:
+ raise ValueError(f"Expected a non-empty value for `instance_id` but received {instance_id!r}")
+ if not remediation_id:
+ raise ValueError(f"Expected a non-empty value for `remediation_id` but received {remediation_id!r}")
+ return await self._post(
+ path_template(
+ "/compute/clusters/{cluster_id}/instances/{instance_id}/remediations/{remediation_id}/approve",
+ cluster_id=cluster_id,
+ instance_id=instance_id,
+ remediation_id=remediation_id,
+ ),
+ body=await async_maybe_transform({"comment": comment}, remediation_approve_params.RemediationApproveParams),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=RemediationApproveResponse,
+ )
+
+ async def cancel(
+ self,
+ remediation_id: str,
+ *,
+ cluster_id: str,
+ instance_id: str,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> RemediationCancelResponse:
+ """
+ Cancels a pending remediation.
+
+ Only remediations in PENDING_APPROVAL or PENDING state can be cancelled.
+
+ Args:
+ cluster_id: The cluster ID.
+
+ instance_id: The instance ID.
+
+ remediation_id: The remediation ID.
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not cluster_id:
+ raise ValueError(f"Expected a non-empty value for `cluster_id` but received {cluster_id!r}")
+ if not instance_id:
+ raise ValueError(f"Expected a non-empty value for `instance_id` but received {instance_id!r}")
+ if not remediation_id:
+ raise ValueError(f"Expected a non-empty value for `remediation_id` but received {remediation_id!r}")
+ return await self._post(
+ path_template(
+ "/compute/clusters/{cluster_id}/instances/{instance_id}/remediations/{remediation_id}/cancel",
+ cluster_id=cluster_id,
+ instance_id=instance_id,
+ remediation_id=remediation_id,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=RemediationCancelResponse,
+ )
+
+ async def reject(
+ self,
+ remediation_id: str,
+ *,
+ cluster_id: str,
+ instance_id: str,
+ comment: str | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> RemediationRejectResponse:
+ """
+ Rejects a pending remediation.
+
+ Only remediations with state PENDING_APPROVAL can be rejected.
+
+ On REJECT: state changes to CANCELLED. The reviewed_by, review_time, and
+ review_comment fields are populated on the remediation after rejection.
+
+ Args:
+ cluster_id: The cluster ID.
+
+ instance_id: The instance ID.
+
+ remediation_id: The remediation ID.
+
+ comment: Comment explaining the action.
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not cluster_id:
+ raise ValueError(f"Expected a non-empty value for `cluster_id` but received {cluster_id!r}")
+ if not instance_id:
+ raise ValueError(f"Expected a non-empty value for `instance_id` but received {instance_id!r}")
+ if not remediation_id:
+ raise ValueError(f"Expected a non-empty value for `remediation_id` but received {remediation_id!r}")
+ return await self._post(
+ path_template(
+ "/compute/clusters/{cluster_id}/instances/{instance_id}/remediations/{remediation_id}/reject",
+ cluster_id=cluster_id,
+ instance_id=instance_id,
+ remediation_id=remediation_id,
+ ),
+ body=await async_maybe_transform({"comment": comment}, remediation_reject_params.RemediationRejectParams),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=RemediationRejectResponse,
+ )
+
+
+class RemediationsResourceWithRawResponse:
+ def __init__(self, remediations: RemediationsResource) -> None:
+ self._remediations = remediations
+
+ self.create = to_raw_response_wrapper(
+ remediations.create,
+ )
+ self.list = to_raw_response_wrapper(
+ remediations.list,
+ )
+ self.approve = to_raw_response_wrapper(
+ remediations.approve,
+ )
+ self.cancel = to_raw_response_wrapper(
+ remediations.cancel,
+ )
+ self.reject = to_raw_response_wrapper(
+ remediations.reject,
+ )
+
+
+class AsyncRemediationsResourceWithRawResponse:
+ def __init__(self, remediations: AsyncRemediationsResource) -> None:
+ self._remediations = remediations
+
+ self.create = async_to_raw_response_wrapper(
+ remediations.create,
+ )
+ self.list = async_to_raw_response_wrapper(
+ remediations.list,
+ )
+ self.approve = async_to_raw_response_wrapper(
+ remediations.approve,
+ )
+ self.cancel = async_to_raw_response_wrapper(
+ remediations.cancel,
+ )
+ self.reject = async_to_raw_response_wrapper(
+ remediations.reject,
+ )
+
+
+class RemediationsResourceWithStreamingResponse:
+ def __init__(self, remediations: RemediationsResource) -> None:
+ self._remediations = remediations
+
+ self.create = to_streamed_response_wrapper(
+ remediations.create,
+ )
+ self.list = to_streamed_response_wrapper(
+ remediations.list,
+ )
+ self.approve = to_streamed_response_wrapper(
+ remediations.approve,
+ )
+ self.cancel = to_streamed_response_wrapper(
+ remediations.cancel,
+ )
+ self.reject = to_streamed_response_wrapper(
+ remediations.reject,
+ )
+
+
+class AsyncRemediationsResourceWithStreamingResponse:
+ def __init__(self, remediations: AsyncRemediationsResource) -> None:
+ self._remediations = remediations
+
+ self.create = async_to_streamed_response_wrapper(
+ remediations.create,
+ )
+ self.list = async_to_streamed_response_wrapper(
+ remediations.list,
+ )
+ self.approve = async_to_streamed_response_wrapper(
+ remediations.approve,
+ )
+ self.cancel = async_to_streamed_response_wrapper(
+ remediations.cancel,
+ )
+ self.reject = async_to_streamed_response_wrapper(
+ remediations.reject,
+ )
diff --git a/src/together/types/beta/clusters/__init__.py b/src/together/types/beta/clusters/__init__.py
index 9c4f18a76..0ac6cbb1b 100644
--- a/src/together/types/beta/clusters/__init__.py
+++ b/src/together/types/beta/clusters/__init__.py
@@ -7,4 +7,13 @@
from .storage_create_params import StorageCreateParams as StorageCreateParams
from .storage_list_response import StorageListResponse as StorageListResponse
from .storage_update_params import StorageUpdateParams as StorageUpdateParams
+from .remediation_list_params import RemediationListParams as RemediationListParams
from .storage_delete_response import StorageDeleteResponse as StorageDeleteResponse
+from .remediation_create_params import RemediationCreateParams as RemediationCreateParams
+from .remediation_list_response import RemediationListResponse as RemediationListResponse
+from .remediation_reject_params import RemediationRejectParams as RemediationRejectParams
+from .remediation_approve_params import RemediationApproveParams as RemediationApproveParams
+from .remediation_cancel_response import RemediationCancelResponse as RemediationCancelResponse
+from .remediation_create_response import RemediationCreateResponse as RemediationCreateResponse
+from .remediation_reject_response import RemediationRejectResponse as RemediationRejectResponse
+from .remediation_approve_response import RemediationApproveResponse as RemediationApproveResponse
diff --git a/src/together/types/beta/clusters/remediation_approve_params.py b/src/together/types/beta/clusters/remediation_approve_params.py
new file mode 100644
index 000000000..cb86457e7
--- /dev/null
+++ b/src/together/types/beta/clusters/remediation_approve_params.py
@@ -0,0 +1,18 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import Required, TypedDict
+
+__all__ = ["RemediationApproveParams"]
+
+
+class RemediationApproveParams(TypedDict, total=False):
+ cluster_id: Required[str]
+ """The cluster ID."""
+
+ instance_id: Required[str]
+ """The instance ID."""
+
+ comment: str
+ """Comment explaining the action."""
diff --git a/src/together/types/beta/clusters/remediation_approve_response.py b/src/together/types/beta/clusters/remediation_approve_response.py
new file mode 100644
index 000000000..040e6dab2
--- /dev/null
+++ b/src/together/types/beta/clusters/remediation_approve_response.py
@@ -0,0 +1,94 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+from datetime import datetime
+from typing_extensions import Literal
+
+from ...._models import BaseModel
+
+__all__ = ["RemediationApproveResponse"]
+
+
+class RemediationApproveResponse(BaseModel):
+ """
+ Remediation represents a node remediation request for an instance.
+ An instance can have multiple remediations over time (e.g., failed attempts followed by retries).
+ """
+
+ id: str
+
+ cluster_id: str
+
+ instance_id: str
+
+ mode: Literal[
+ "REMEDIATION_MODE_VM_ONLY",
+ "REMEDIATION_MODE_HOST_AWARE",
+ "REMEDIATION_MODE_EVICT_WITHOUT_REPLACEMENT",
+ "REMEDIATION_MODE_REBOOT_VM",
+ ]
+ """Remediation mode specifies how the remediation should be performed.
+
+ - `REMEDIATION_MODE_VM_ONLY`: Deletes the VM and provisions a new one on any
+ available host.
+ - `REMEDIATION_MODE_HOST_AWARE`: Cordons the host, deletes the VM, and
+ provisions a new one on a different host.
+ """
+
+ state: Literal["PENDING_APPROVAL", "PENDING", "RUNNING", "SUCCEEDED", "FAILED", "CANCELLED", "AUTO_RESOLVED"]
+ """RemediationState represents the lifecycle state of a remediation.
+
+ - `PENDING_APPROVAL`: Awaiting approval before processing can begin.
+ - `PENDING`: Approved and queued for processing.
+ - `RUNNING`: Actively being processed.
+ - `SUCCEEDED`: Successfully completed.
+ - `FAILED`: Failed with an error.
+ - `CANCELLED`: Cancelled by user or system.
+ - `AUTO_RESOLVED`: The underlying issue was automatically resolved before
+ processing.
+ """
+
+ trigger: Literal["REMEDIATION_TRIGGER_MANUAL", "REMEDIATION_TRIGGER_AUTOMATED"]
+ """RemediationTrigger specifies how the remediation was triggered.
+
+ - `REMEDIATION_TRIGGER_MANUAL`: A user-initiated remediation (either via web UI
+ or API call).
+ - `REMEDIATION_TRIGGER_AUTOMATED`: A system-initiated remediation that requires
+ approval.
+ """
+
+ active_health_check_run_id: Optional[str] = None
+ """Active health check run ID (UUID) that triggered this remediation."""
+
+ create_time: Optional[datetime] = None
+ """When the remediation was created."""
+
+ end_time: Optional[datetime] = None
+ """When the remediation completed."""
+
+ error_message: Optional[str] = None
+ """Error message if the remediation failed."""
+
+ passive_health_check_event_id: Optional[str] = None
+ """Passive health check event ID that triggered this remediation."""
+
+ reason: Optional[str] = None
+ """User-provided reason for the remediation."""
+
+ requested_by: Optional[str] = None
+ """Who requested the remediation."""
+
+ review_comment: Optional[str] = None
+ """Review comment."""
+
+ review_time: Optional[datetime] = None
+ """When the remediation was reviewed."""
+
+ reviewed_by: Optional[str] = None
+ """Who reviewed the remediation."""
+
+ start_time: Optional[datetime] = None
+ """When processing started."""
+
+ update_time: Optional[datetime] = None
+ """When the remediation was last updated."""
diff --git a/src/together/types/beta/clusters/remediation_cancel_response.py b/src/together/types/beta/clusters/remediation_cancel_response.py
new file mode 100644
index 000000000..ec8975c9b
--- /dev/null
+++ b/src/together/types/beta/clusters/remediation_cancel_response.py
@@ -0,0 +1,94 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+from datetime import datetime
+from typing_extensions import Literal
+
+from ...._models import BaseModel
+
+__all__ = ["RemediationCancelResponse"]
+
+
+class RemediationCancelResponse(BaseModel):
+ """
+ Remediation represents a node remediation request for an instance.
+ An instance can have multiple remediations over time (e.g., failed attempts followed by retries).
+ """
+
+ id: str
+
+ cluster_id: str
+
+ instance_id: str
+
+ mode: Literal[
+ "REMEDIATION_MODE_VM_ONLY",
+ "REMEDIATION_MODE_HOST_AWARE",
+ "REMEDIATION_MODE_EVICT_WITHOUT_REPLACEMENT",
+ "REMEDIATION_MODE_REBOOT_VM",
+ ]
+ """Remediation mode specifies how the remediation should be performed.
+
+ - `REMEDIATION_MODE_VM_ONLY`: Deletes the VM and provisions a new one on any
+ available host.
+ - `REMEDIATION_MODE_HOST_AWARE`: Cordons the host, deletes the VM, and
+ provisions a new one on a different host.
+ """
+
+ state: Literal["PENDING_APPROVAL", "PENDING", "RUNNING", "SUCCEEDED", "FAILED", "CANCELLED", "AUTO_RESOLVED"]
+ """RemediationState represents the lifecycle state of a remediation.
+
+ - `PENDING_APPROVAL`: Awaiting approval before processing can begin.
+ - `PENDING`: Approved and queued for processing.
+ - `RUNNING`: Actively being processed.
+ - `SUCCEEDED`: Successfully completed.
+ - `FAILED`: Failed with an error.
+ - `CANCELLED`: Cancelled by user or system.
+ - `AUTO_RESOLVED`: The underlying issue was automatically resolved before
+ processing.
+ """
+
+ trigger: Literal["REMEDIATION_TRIGGER_MANUAL", "REMEDIATION_TRIGGER_AUTOMATED"]
+ """RemediationTrigger specifies how the remediation was triggered.
+
+ - `REMEDIATION_TRIGGER_MANUAL`: A user-initiated remediation (either via web UI
+ or API call).
+ - `REMEDIATION_TRIGGER_AUTOMATED`: A system-initiated remediation that requires
+ approval.
+ """
+
+ active_health_check_run_id: Optional[str] = None
+ """Active health check run ID (UUID) that triggered this remediation."""
+
+ create_time: Optional[datetime] = None
+ """When the remediation was created."""
+
+ end_time: Optional[datetime] = None
+ """When the remediation completed."""
+
+ error_message: Optional[str] = None
+ """Error message if the remediation failed."""
+
+ passive_health_check_event_id: Optional[str] = None
+ """Passive health check event ID that triggered this remediation."""
+
+ reason: Optional[str] = None
+ """User-provided reason for the remediation."""
+
+ requested_by: Optional[str] = None
+ """Who requested the remediation."""
+
+ review_comment: Optional[str] = None
+ """Review comment."""
+
+ review_time: Optional[datetime] = None
+ """When the remediation was reviewed."""
+
+ reviewed_by: Optional[str] = None
+ """Who reviewed the remediation."""
+
+ start_time: Optional[datetime] = None
+ """When processing started."""
+
+ update_time: Optional[datetime] = None
+ """When the remediation was last updated."""
diff --git a/src/together/types/beta/clusters/remediation_create_params.py b/src/together/types/beta/clusters/remediation_create_params.py
new file mode 100644
index 000000000..84374cec9
--- /dev/null
+++ b/src/together/types/beta/clusters/remediation_create_params.py
@@ -0,0 +1,34 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import Literal, Required, TypedDict
+
+__all__ = ["RemediationCreateParams"]
+
+
+class RemediationCreateParams(TypedDict, total=False):
+ cluster_id: Required[str]
+ """The cluster ID."""
+
+ mode: Required[
+ Literal[
+ "REMEDIATION_MODE_VM_ONLY",
+ "REMEDIATION_MODE_HOST_AWARE",
+ "REMEDIATION_MODE_EVICT_WITHOUT_REPLACEMENT",
+ "REMEDIATION_MODE_REBOOT_VM",
+ ]
+ ]
+ """Remediation mode specifies how the remediation should be performed.
+
+ - `REMEDIATION_MODE_VM_ONLY`: Deletes the VM and provisions a new one on any
+ available host.
+ - `REMEDIATION_MODE_HOST_AWARE`: Cordons the host, deletes the VM, and
+ provisions a new one on a different host.
+ """
+
+ remediation_id: str
+ """Optional. Client-specified ID for idempotency."""
+
+ reason: str
+ """User-provided reason for the remediation."""
diff --git a/src/together/types/beta/clusters/remediation_create_response.py b/src/together/types/beta/clusters/remediation_create_response.py
new file mode 100644
index 000000000..2b148f932
--- /dev/null
+++ b/src/together/types/beta/clusters/remediation_create_response.py
@@ -0,0 +1,94 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+from datetime import datetime
+from typing_extensions import Literal
+
+from ...._models import BaseModel
+
+__all__ = ["RemediationCreateResponse"]
+
+
+class RemediationCreateResponse(BaseModel):
+ """
+ Remediation represents a node remediation request for an instance.
+ An instance can have multiple remediations over time (e.g., failed attempts followed by retries).
+ """
+
+ id: str
+
+ cluster_id: str
+
+ instance_id: str
+
+ mode: Literal[
+ "REMEDIATION_MODE_VM_ONLY",
+ "REMEDIATION_MODE_HOST_AWARE",
+ "REMEDIATION_MODE_EVICT_WITHOUT_REPLACEMENT",
+ "REMEDIATION_MODE_REBOOT_VM",
+ ]
+ """Remediation mode specifies how the remediation should be performed.
+
+ - `REMEDIATION_MODE_VM_ONLY`: Deletes the VM and provisions a new one on any
+ available host.
+ - `REMEDIATION_MODE_HOST_AWARE`: Cordons the host, deletes the VM, and
+ provisions a new one on a different host.
+ """
+
+ state: Literal["PENDING_APPROVAL", "PENDING", "RUNNING", "SUCCEEDED", "FAILED", "CANCELLED", "AUTO_RESOLVED"]
+ """RemediationState represents the lifecycle state of a remediation.
+
+ - `PENDING_APPROVAL`: Awaiting approval before processing can begin.
+ - `PENDING`: Approved and queued for processing.
+ - `RUNNING`: Actively being processed.
+ - `SUCCEEDED`: Successfully completed.
+ - `FAILED`: Failed with an error.
+ - `CANCELLED`: Cancelled by user or system.
+ - `AUTO_RESOLVED`: The underlying issue was automatically resolved before
+ processing.
+ """
+
+ trigger: Literal["REMEDIATION_TRIGGER_MANUAL", "REMEDIATION_TRIGGER_AUTOMATED"]
+ """RemediationTrigger specifies how the remediation was triggered.
+
+ - `REMEDIATION_TRIGGER_MANUAL`: A user-initiated remediation (either via web UI
+ or API call).
+ - `REMEDIATION_TRIGGER_AUTOMATED`: A system-initiated remediation that requires
+ approval.
+ """
+
+ active_health_check_run_id: Optional[str] = None
+ """Active health check run ID (UUID) that triggered this remediation."""
+
+ create_time: Optional[datetime] = None
+ """When the remediation was created."""
+
+ end_time: Optional[datetime] = None
+ """When the remediation completed."""
+
+ error_message: Optional[str] = None
+ """Error message if the remediation failed."""
+
+ passive_health_check_event_id: Optional[str] = None
+ """Passive health check event ID that triggered this remediation."""
+
+ reason: Optional[str] = None
+ """User-provided reason for the remediation."""
+
+ requested_by: Optional[str] = None
+ """Who requested the remediation."""
+
+ review_comment: Optional[str] = None
+ """Review comment."""
+
+ review_time: Optional[datetime] = None
+ """When the remediation was reviewed."""
+
+ reviewed_by: Optional[str] = None
+ """Who reviewed the remediation."""
+
+ start_time: Optional[datetime] = None
+ """When processing started."""
+
+ update_time: Optional[datetime] = None
+ """When the remediation was last updated."""
diff --git a/src/together/types/beta/clusters/remediation_list_params.py b/src/together/types/beta/clusters/remediation_list_params.py
new file mode 100644
index 000000000..18e6b884e
--- /dev/null
+++ b/src/together/types/beta/clusters/remediation_list_params.py
@@ -0,0 +1,47 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import List
+from typing_extensions import Literal, Required, TypedDict
+
+__all__ = ["RemediationListParams"]
+
+
+class RemediationListParams(TypedDict, total=False):
+ cluster_id: Required[str]
+ """The cluster ID."""
+
+ mode: Literal[
+ "REMEDIATION_MODE_VM_ONLY",
+ "REMEDIATION_MODE_HOST_AWARE",
+ "REMEDIATION_MODE_EVICT_WITHOUT_REPLACEMENT",
+ "REMEDIATION_MODE_REBOOT_VM",
+ ]
+ """Optional.
+
+ Filter by remediation mode. Returns only remediations matching the specified
+ mode.
+ """
+
+ order_by: str
+ """Optional. Order by expression."""
+
+ page_size: int
+ """Optional. Maximum results to return."""
+
+ page_token: str
+ """Optional. Pagination token from previous request."""
+
+ state: List[Literal["PENDING_APPROVAL", "PENDING", "RUNNING", "SUCCEEDED", "FAILED", "CANCELLED", "AUTO_RESOLVED"]]
+ """Optional.
+
+ Filter by state(s). Returns remediations matching any of the specified states.
+ """
+
+ trigger: Literal["REMEDIATION_TRIGGER_MANUAL", "REMEDIATION_TRIGGER_AUTOMATED"]
+ """Optional.
+
+ Filter by trigger type. Returns only remediations matching the specified
+ trigger.
+ """
diff --git a/src/together/types/beta/clusters/remediation_list_response.py b/src/together/types/beta/clusters/remediation_list_response.py
new file mode 100644
index 000000000..aaf15a9fb
--- /dev/null
+++ b/src/together/types/beta/clusters/remediation_list_response.py
@@ -0,0 +1,107 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import List, Optional
+from datetime import datetime
+from typing_extensions import Literal
+
+from ...._models import BaseModel
+
+__all__ = ["RemediationListResponse", "Remediation"]
+
+
+class Remediation(BaseModel):
+ """
+ Remediation represents a node remediation request for an instance.
+ An instance can have multiple remediations over time (e.g., failed attempts followed by retries).
+ """
+
+ id: str
+
+ cluster_id: str
+
+ instance_id: str
+
+ mode: Literal[
+ "REMEDIATION_MODE_VM_ONLY",
+ "REMEDIATION_MODE_HOST_AWARE",
+ "REMEDIATION_MODE_EVICT_WITHOUT_REPLACEMENT",
+ "REMEDIATION_MODE_REBOOT_VM",
+ ]
+ """Remediation mode specifies how the remediation should be performed.
+
+ - `REMEDIATION_MODE_VM_ONLY`: Deletes the VM and provisions a new one on any
+ available host.
+ - `REMEDIATION_MODE_HOST_AWARE`: Cordons the host, deletes the VM, and
+ provisions a new one on a different host.
+ """
+
+ state: Literal["PENDING_APPROVAL", "PENDING", "RUNNING", "SUCCEEDED", "FAILED", "CANCELLED", "AUTO_RESOLVED"]
+ """RemediationState represents the lifecycle state of a remediation.
+
+ - `PENDING_APPROVAL`: Awaiting approval before processing can begin.
+ - `PENDING`: Approved and queued for processing.
+ - `RUNNING`: Actively being processed.
+ - `SUCCEEDED`: Successfully completed.
+ - `FAILED`: Failed with an error.
+ - `CANCELLED`: Cancelled by user or system.
+ - `AUTO_RESOLVED`: The underlying issue was automatically resolved before
+ processing.
+ """
+
+ trigger: Literal["REMEDIATION_TRIGGER_MANUAL", "REMEDIATION_TRIGGER_AUTOMATED"]
+ """RemediationTrigger specifies how the remediation was triggered.
+
+ - `REMEDIATION_TRIGGER_MANUAL`: A user-initiated remediation (either via web UI
+ or API call).
+ - `REMEDIATION_TRIGGER_AUTOMATED`: A system-initiated remediation that requires
+ approval.
+ """
+
+ active_health_check_run_id: Optional[str] = None
+ """Active health check run ID (UUID) that triggered this remediation."""
+
+ create_time: Optional[datetime] = None
+ """When the remediation was created."""
+
+ end_time: Optional[datetime] = None
+ """When the remediation completed."""
+
+ error_message: Optional[str] = None
+ """Error message if the remediation failed."""
+
+ passive_health_check_event_id: Optional[str] = None
+ """Passive health check event ID that triggered this remediation."""
+
+ reason: Optional[str] = None
+ """User-provided reason for the remediation."""
+
+ requested_by: Optional[str] = None
+ """Who requested the remediation."""
+
+ review_comment: Optional[str] = None
+ """Review comment."""
+
+ review_time: Optional[datetime] = None
+ """When the remediation was reviewed."""
+
+ reviewed_by: Optional[str] = None
+ """Who reviewed the remediation."""
+
+ start_time: Optional[datetime] = None
+ """When processing started."""
+
+ update_time: Optional[datetime] = None
+ """When the remediation was last updated."""
+
+
+class RemediationListResponse(BaseModel):
+ """ListRemediationsResponse is the response for ListRemediations."""
+
+ has_next: bool
+ """Indicates if there are more results available."""
+
+ next_page_token: str
+ """Token for the next page."""
+
+ remediations: List[Remediation]
+ """The list of remediations."""
diff --git a/src/together/types/beta/clusters/remediation_reject_params.py b/src/together/types/beta/clusters/remediation_reject_params.py
new file mode 100644
index 000000000..de91e59f9
--- /dev/null
+++ b/src/together/types/beta/clusters/remediation_reject_params.py
@@ -0,0 +1,18 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import Required, TypedDict
+
+__all__ = ["RemediationRejectParams"]
+
+
+class RemediationRejectParams(TypedDict, total=False):
+ cluster_id: Required[str]
+ """The cluster ID."""
+
+ instance_id: Required[str]
+ """The instance ID."""
+
+ comment: str
+ """Comment explaining the action."""
diff --git a/src/together/types/beta/clusters/remediation_reject_response.py b/src/together/types/beta/clusters/remediation_reject_response.py
new file mode 100644
index 000000000..e90306bb6
--- /dev/null
+++ b/src/together/types/beta/clusters/remediation_reject_response.py
@@ -0,0 +1,94 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+from datetime import datetime
+from typing_extensions import Literal
+
+from ...._models import BaseModel
+
+__all__ = ["RemediationRejectResponse"]
+
+
+class RemediationRejectResponse(BaseModel):
+ """
+ Remediation represents a node remediation request for an instance.
+ An instance can have multiple remediations over time (e.g., failed attempts followed by retries).
+ """
+
+ id: str
+
+ cluster_id: str
+
+ instance_id: str
+
+ mode: Literal[
+ "REMEDIATION_MODE_VM_ONLY",
+ "REMEDIATION_MODE_HOST_AWARE",
+ "REMEDIATION_MODE_EVICT_WITHOUT_REPLACEMENT",
+ "REMEDIATION_MODE_REBOOT_VM",
+ ]
+ """Remediation mode specifies how the remediation should be performed.
+
+ - `REMEDIATION_MODE_VM_ONLY`: Deletes the VM and provisions a new one on any
+ available host.
+ - `REMEDIATION_MODE_HOST_AWARE`: Cordons the host, deletes the VM, and
+ provisions a new one on a different host.
+ """
+
+ state: Literal["PENDING_APPROVAL", "PENDING", "RUNNING", "SUCCEEDED", "FAILED", "CANCELLED", "AUTO_RESOLVED"]
+ """RemediationState represents the lifecycle state of a remediation.
+
+ - `PENDING_APPROVAL`: Awaiting approval before processing can begin.
+ - `PENDING`: Approved and queued for processing.
+ - `RUNNING`: Actively being processed.
+ - `SUCCEEDED`: Successfully completed.
+ - `FAILED`: Failed with an error.
+ - `CANCELLED`: Cancelled by user or system.
+ - `AUTO_RESOLVED`: The underlying issue was automatically resolved before
+ processing.
+ """
+
+ trigger: Literal["REMEDIATION_TRIGGER_MANUAL", "REMEDIATION_TRIGGER_AUTOMATED"]
+ """RemediationTrigger specifies how the remediation was triggered.
+
+ - `REMEDIATION_TRIGGER_MANUAL`: A user-initiated remediation (either via web UI
+ or API call).
+ - `REMEDIATION_TRIGGER_AUTOMATED`: A system-initiated remediation that requires
+ approval.
+ """
+
+ active_health_check_run_id: Optional[str] = None
+ """Active health check run ID (UUID) that triggered this remediation."""
+
+ create_time: Optional[datetime] = None
+ """When the remediation was created."""
+
+ end_time: Optional[datetime] = None
+ """When the remediation completed."""
+
+ error_message: Optional[str] = None
+ """Error message if the remediation failed."""
+
+ passive_health_check_event_id: Optional[str] = None
+ """Passive health check event ID that triggered this remediation."""
+
+ reason: Optional[str] = None
+ """User-provided reason for the remediation."""
+
+ requested_by: Optional[str] = None
+ """Who requested the remediation."""
+
+ review_comment: Optional[str] = None
+ """Review comment."""
+
+ review_time: Optional[datetime] = None
+ """When the remediation was reviewed."""
+
+ reviewed_by: Optional[str] = None
+ """Who reviewed the remediation."""
+
+ start_time: Optional[datetime] = None
+ """When processing started."""
+
+ update_time: Optional[datetime] = None
+ """When the remediation was last updated."""
diff --git a/tests/api_resources/beta/clusters/test_remediations.py b/tests/api_resources/beta/clusters/test_remediations.py
new file mode 100644
index 000000000..3a359fd9a
--- /dev/null
+++ b/tests/api_resources/beta/clusters/test_remediations.py
@@ -0,0 +1,682 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+import os
+from typing import Any, cast
+
+import pytest
+
+from together import Together, AsyncTogether
+from tests.utils import assert_matches_type
+from together.types.beta.clusters import (
+ RemediationListResponse,
+ RemediationCancelResponse,
+ RemediationCreateResponse,
+ RemediationRejectResponse,
+ RemediationApproveResponse,
+)
+
+base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
+
+
+class TestRemediations:
+ parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"])
+
+ @parametrize
+ def test_method_create(self, client: Together) -> None:
+ remediation = client.beta.clusters.remediations.create(
+ instance_id="instance_id",
+ cluster_id="cluster_id",
+ mode="REMEDIATION_MODE_VM_ONLY",
+ )
+ assert_matches_type(RemediationCreateResponse, remediation, path=["response"])
+
+ @parametrize
+ def test_method_create_with_all_params(self, client: Together) -> None:
+ remediation = client.beta.clusters.remediations.create(
+ instance_id="instance_id",
+ cluster_id="cluster_id",
+ mode="REMEDIATION_MODE_VM_ONLY",
+ remediation_id="remediation_id",
+ reason="reason",
+ )
+ assert_matches_type(RemediationCreateResponse, remediation, path=["response"])
+
+ @parametrize
+ def test_raw_response_create(self, client: Together) -> None:
+ response = client.beta.clusters.remediations.with_raw_response.create(
+ instance_id="instance_id",
+ cluster_id="cluster_id",
+ mode="REMEDIATION_MODE_VM_ONLY",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ remediation = response.parse()
+ assert_matches_type(RemediationCreateResponse, remediation, path=["response"])
+
+ @parametrize
+ def test_streaming_response_create(self, client: Together) -> None:
+ with client.beta.clusters.remediations.with_streaming_response.create(
+ instance_id="instance_id",
+ cluster_id="cluster_id",
+ mode="REMEDIATION_MODE_VM_ONLY",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ remediation = response.parse()
+ assert_matches_type(RemediationCreateResponse, remediation, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ def test_path_params_create(self, client: Together) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `cluster_id` but received ''"):
+ client.beta.clusters.remediations.with_raw_response.create(
+ instance_id="instance_id",
+ cluster_id="",
+ mode="REMEDIATION_MODE_VM_ONLY",
+ )
+
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `instance_id` but received ''"):
+ client.beta.clusters.remediations.with_raw_response.create(
+ instance_id="",
+ cluster_id="cluster_id",
+ mode="REMEDIATION_MODE_VM_ONLY",
+ )
+
+ @parametrize
+ def test_method_list(self, client: Together) -> None:
+ remediation = client.beta.clusters.remediations.list(
+ instance_id="instance_id",
+ cluster_id="cluster_id",
+ )
+ assert_matches_type(RemediationListResponse, remediation, path=["response"])
+
+ @parametrize
+ def test_method_list_with_all_params(self, client: Together) -> None:
+ remediation = client.beta.clusters.remediations.list(
+ instance_id="instance_id",
+ cluster_id="cluster_id",
+ mode="REMEDIATION_MODE_VM_ONLY",
+ order_by="order_by",
+ page_size=0,
+ page_token="page_token",
+ state=["PENDING_APPROVAL"],
+ trigger="REMEDIATION_TRIGGER_MANUAL",
+ )
+ assert_matches_type(RemediationListResponse, remediation, path=["response"])
+
+ @parametrize
+ def test_raw_response_list(self, client: Together) -> None:
+ response = client.beta.clusters.remediations.with_raw_response.list(
+ instance_id="instance_id",
+ cluster_id="cluster_id",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ remediation = response.parse()
+ assert_matches_type(RemediationListResponse, remediation, path=["response"])
+
+ @parametrize
+ def test_streaming_response_list(self, client: Together) -> None:
+ with client.beta.clusters.remediations.with_streaming_response.list(
+ instance_id="instance_id",
+ cluster_id="cluster_id",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ remediation = response.parse()
+ assert_matches_type(RemediationListResponse, remediation, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ def test_path_params_list(self, client: Together) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `cluster_id` but received ''"):
+ client.beta.clusters.remediations.with_raw_response.list(
+ instance_id="instance_id",
+ cluster_id="",
+ )
+
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `instance_id` but received ''"):
+ client.beta.clusters.remediations.with_raw_response.list(
+ instance_id="",
+ cluster_id="cluster_id",
+ )
+
+ @parametrize
+ def test_method_approve(self, client: Together) -> None:
+ remediation = client.beta.clusters.remediations.approve(
+ remediation_id="remediation_id",
+ cluster_id="cluster_id",
+ instance_id="instance_id",
+ )
+ assert_matches_type(RemediationApproveResponse, remediation, path=["response"])
+
+ @parametrize
+ def test_method_approve_with_all_params(self, client: Together) -> None:
+ remediation = client.beta.clusters.remediations.approve(
+ remediation_id="remediation_id",
+ cluster_id="cluster_id",
+ instance_id="instance_id",
+ comment="comment",
+ )
+ assert_matches_type(RemediationApproveResponse, remediation, path=["response"])
+
+ @parametrize
+ def test_raw_response_approve(self, client: Together) -> None:
+ response = client.beta.clusters.remediations.with_raw_response.approve(
+ remediation_id="remediation_id",
+ cluster_id="cluster_id",
+ instance_id="instance_id",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ remediation = response.parse()
+ assert_matches_type(RemediationApproveResponse, remediation, path=["response"])
+
+ @parametrize
+ def test_streaming_response_approve(self, client: Together) -> None:
+ with client.beta.clusters.remediations.with_streaming_response.approve(
+ remediation_id="remediation_id",
+ cluster_id="cluster_id",
+ instance_id="instance_id",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ remediation = response.parse()
+ assert_matches_type(RemediationApproveResponse, remediation, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ def test_path_params_approve(self, client: Together) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `cluster_id` but received ''"):
+ client.beta.clusters.remediations.with_raw_response.approve(
+ remediation_id="remediation_id",
+ cluster_id="",
+ instance_id="instance_id",
+ )
+
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `instance_id` but received ''"):
+ client.beta.clusters.remediations.with_raw_response.approve(
+ remediation_id="remediation_id",
+ cluster_id="cluster_id",
+ instance_id="",
+ )
+
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `remediation_id` but received ''"):
+ client.beta.clusters.remediations.with_raw_response.approve(
+ remediation_id="",
+ cluster_id="cluster_id",
+ instance_id="instance_id",
+ )
+
+ @parametrize
+ def test_method_cancel(self, client: Together) -> None:
+ remediation = client.beta.clusters.remediations.cancel(
+ remediation_id="remediation_id",
+ cluster_id="cluster_id",
+ instance_id="instance_id",
+ )
+ assert_matches_type(RemediationCancelResponse, remediation, path=["response"])
+
+ @parametrize
+ def test_raw_response_cancel(self, client: Together) -> None:
+ response = client.beta.clusters.remediations.with_raw_response.cancel(
+ remediation_id="remediation_id",
+ cluster_id="cluster_id",
+ instance_id="instance_id",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ remediation = response.parse()
+ assert_matches_type(RemediationCancelResponse, remediation, path=["response"])
+
+ @parametrize
+ def test_streaming_response_cancel(self, client: Together) -> None:
+ with client.beta.clusters.remediations.with_streaming_response.cancel(
+ remediation_id="remediation_id",
+ cluster_id="cluster_id",
+ instance_id="instance_id",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ remediation = response.parse()
+ assert_matches_type(RemediationCancelResponse, remediation, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ def test_path_params_cancel(self, client: Together) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `cluster_id` but received ''"):
+ client.beta.clusters.remediations.with_raw_response.cancel(
+ remediation_id="remediation_id",
+ cluster_id="",
+ instance_id="instance_id",
+ )
+
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `instance_id` but received ''"):
+ client.beta.clusters.remediations.with_raw_response.cancel(
+ remediation_id="remediation_id",
+ cluster_id="cluster_id",
+ instance_id="",
+ )
+
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `remediation_id` but received ''"):
+ client.beta.clusters.remediations.with_raw_response.cancel(
+ remediation_id="",
+ cluster_id="cluster_id",
+ instance_id="instance_id",
+ )
+
+ @parametrize
+ def test_method_reject(self, client: Together) -> None:
+ remediation = client.beta.clusters.remediations.reject(
+ remediation_id="remediation_id",
+ cluster_id="cluster_id",
+ instance_id="instance_id",
+ )
+ assert_matches_type(RemediationRejectResponse, remediation, path=["response"])
+
+ @parametrize
+ def test_method_reject_with_all_params(self, client: Together) -> None:
+ remediation = client.beta.clusters.remediations.reject(
+ remediation_id="remediation_id",
+ cluster_id="cluster_id",
+ instance_id="instance_id",
+ comment="comment",
+ )
+ assert_matches_type(RemediationRejectResponse, remediation, path=["response"])
+
+ @parametrize
+ def test_raw_response_reject(self, client: Together) -> None:
+ response = client.beta.clusters.remediations.with_raw_response.reject(
+ remediation_id="remediation_id",
+ cluster_id="cluster_id",
+ instance_id="instance_id",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ remediation = response.parse()
+ assert_matches_type(RemediationRejectResponse, remediation, path=["response"])
+
+ @parametrize
+ def test_streaming_response_reject(self, client: Together) -> None:
+ with client.beta.clusters.remediations.with_streaming_response.reject(
+ remediation_id="remediation_id",
+ cluster_id="cluster_id",
+ instance_id="instance_id",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ remediation = response.parse()
+ assert_matches_type(RemediationRejectResponse, remediation, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ def test_path_params_reject(self, client: Together) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `cluster_id` but received ''"):
+ client.beta.clusters.remediations.with_raw_response.reject(
+ remediation_id="remediation_id",
+ cluster_id="",
+ instance_id="instance_id",
+ )
+
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `instance_id` but received ''"):
+ client.beta.clusters.remediations.with_raw_response.reject(
+ remediation_id="remediation_id",
+ cluster_id="cluster_id",
+ instance_id="",
+ )
+
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `remediation_id` but received ''"):
+ client.beta.clusters.remediations.with_raw_response.reject(
+ remediation_id="",
+ cluster_id="cluster_id",
+ instance_id="instance_id",
+ )
+
+
+class TestAsyncRemediations:
+ parametrize = pytest.mark.parametrize(
+ "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"]
+ )
+
+ @parametrize
+ async def test_method_create(self, async_client: AsyncTogether) -> None:
+ remediation = await async_client.beta.clusters.remediations.create(
+ instance_id="instance_id",
+ cluster_id="cluster_id",
+ mode="REMEDIATION_MODE_VM_ONLY",
+ )
+ assert_matches_type(RemediationCreateResponse, remediation, path=["response"])
+
+ @parametrize
+ async def test_method_create_with_all_params(self, async_client: AsyncTogether) -> None:
+ remediation = await async_client.beta.clusters.remediations.create(
+ instance_id="instance_id",
+ cluster_id="cluster_id",
+ mode="REMEDIATION_MODE_VM_ONLY",
+ remediation_id="remediation_id",
+ reason="reason",
+ )
+ assert_matches_type(RemediationCreateResponse, remediation, path=["response"])
+
+ @parametrize
+ async def test_raw_response_create(self, async_client: AsyncTogether) -> None:
+ response = await async_client.beta.clusters.remediations.with_raw_response.create(
+ instance_id="instance_id",
+ cluster_id="cluster_id",
+ mode="REMEDIATION_MODE_VM_ONLY",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ remediation = await response.parse()
+ assert_matches_type(RemediationCreateResponse, remediation, path=["response"])
+
+ @parametrize
+ async def test_streaming_response_create(self, async_client: AsyncTogether) -> None:
+ async with async_client.beta.clusters.remediations.with_streaming_response.create(
+ instance_id="instance_id",
+ cluster_id="cluster_id",
+ mode="REMEDIATION_MODE_VM_ONLY",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ remediation = await response.parse()
+ assert_matches_type(RemediationCreateResponse, remediation, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ async def test_path_params_create(self, async_client: AsyncTogether) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `cluster_id` but received ''"):
+ await async_client.beta.clusters.remediations.with_raw_response.create(
+ instance_id="instance_id",
+ cluster_id="",
+ mode="REMEDIATION_MODE_VM_ONLY",
+ )
+
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `instance_id` but received ''"):
+ await async_client.beta.clusters.remediations.with_raw_response.create(
+ instance_id="",
+ cluster_id="cluster_id",
+ mode="REMEDIATION_MODE_VM_ONLY",
+ )
+
+ @parametrize
+ async def test_method_list(self, async_client: AsyncTogether) -> None:
+ remediation = await async_client.beta.clusters.remediations.list(
+ instance_id="instance_id",
+ cluster_id="cluster_id",
+ )
+ assert_matches_type(RemediationListResponse, remediation, path=["response"])
+
+ @parametrize
+ async def test_method_list_with_all_params(self, async_client: AsyncTogether) -> None:
+ remediation = await async_client.beta.clusters.remediations.list(
+ instance_id="instance_id",
+ cluster_id="cluster_id",
+ mode="REMEDIATION_MODE_VM_ONLY",
+ order_by="order_by",
+ page_size=0,
+ page_token="page_token",
+ state=["PENDING_APPROVAL"],
+ trigger="REMEDIATION_TRIGGER_MANUAL",
+ )
+ assert_matches_type(RemediationListResponse, remediation, path=["response"])
+
+ @parametrize
+ async def test_raw_response_list(self, async_client: AsyncTogether) -> None:
+ response = await async_client.beta.clusters.remediations.with_raw_response.list(
+ instance_id="instance_id",
+ cluster_id="cluster_id",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ remediation = await response.parse()
+ assert_matches_type(RemediationListResponse, remediation, path=["response"])
+
+ @parametrize
+ async def test_streaming_response_list(self, async_client: AsyncTogether) -> None:
+ async with async_client.beta.clusters.remediations.with_streaming_response.list(
+ instance_id="instance_id",
+ cluster_id="cluster_id",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ remediation = await response.parse()
+ assert_matches_type(RemediationListResponse, remediation, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ async def test_path_params_list(self, async_client: AsyncTogether) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `cluster_id` but received ''"):
+ await async_client.beta.clusters.remediations.with_raw_response.list(
+ instance_id="instance_id",
+ cluster_id="",
+ )
+
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `instance_id` but received ''"):
+ await async_client.beta.clusters.remediations.with_raw_response.list(
+ instance_id="",
+ cluster_id="cluster_id",
+ )
+
+ @parametrize
+ async def test_method_approve(self, async_client: AsyncTogether) -> None:
+ remediation = await async_client.beta.clusters.remediations.approve(
+ remediation_id="remediation_id",
+ cluster_id="cluster_id",
+ instance_id="instance_id",
+ )
+ assert_matches_type(RemediationApproveResponse, remediation, path=["response"])
+
+ @parametrize
+ async def test_method_approve_with_all_params(self, async_client: AsyncTogether) -> None:
+ remediation = await async_client.beta.clusters.remediations.approve(
+ remediation_id="remediation_id",
+ cluster_id="cluster_id",
+ instance_id="instance_id",
+ comment="comment",
+ )
+ assert_matches_type(RemediationApproveResponse, remediation, path=["response"])
+
+ @parametrize
+ async def test_raw_response_approve(self, async_client: AsyncTogether) -> None:
+ response = await async_client.beta.clusters.remediations.with_raw_response.approve(
+ remediation_id="remediation_id",
+ cluster_id="cluster_id",
+ instance_id="instance_id",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ remediation = await response.parse()
+ assert_matches_type(RemediationApproveResponse, remediation, path=["response"])
+
+ @parametrize
+ async def test_streaming_response_approve(self, async_client: AsyncTogether) -> None:
+ async with async_client.beta.clusters.remediations.with_streaming_response.approve(
+ remediation_id="remediation_id",
+ cluster_id="cluster_id",
+ instance_id="instance_id",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ remediation = await response.parse()
+ assert_matches_type(RemediationApproveResponse, remediation, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ async def test_path_params_approve(self, async_client: AsyncTogether) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `cluster_id` but received ''"):
+ await async_client.beta.clusters.remediations.with_raw_response.approve(
+ remediation_id="remediation_id",
+ cluster_id="",
+ instance_id="instance_id",
+ )
+
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `instance_id` but received ''"):
+ await async_client.beta.clusters.remediations.with_raw_response.approve(
+ remediation_id="remediation_id",
+ cluster_id="cluster_id",
+ instance_id="",
+ )
+
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `remediation_id` but received ''"):
+ await async_client.beta.clusters.remediations.with_raw_response.approve(
+ remediation_id="",
+ cluster_id="cluster_id",
+ instance_id="instance_id",
+ )
+
+ @parametrize
+ async def test_method_cancel(self, async_client: AsyncTogether) -> None:
+ remediation = await async_client.beta.clusters.remediations.cancel(
+ remediation_id="remediation_id",
+ cluster_id="cluster_id",
+ instance_id="instance_id",
+ )
+ assert_matches_type(RemediationCancelResponse, remediation, path=["response"])
+
+ @parametrize
+ async def test_raw_response_cancel(self, async_client: AsyncTogether) -> None:
+ response = await async_client.beta.clusters.remediations.with_raw_response.cancel(
+ remediation_id="remediation_id",
+ cluster_id="cluster_id",
+ instance_id="instance_id",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ remediation = await response.parse()
+ assert_matches_type(RemediationCancelResponse, remediation, path=["response"])
+
+ @parametrize
+ async def test_streaming_response_cancel(self, async_client: AsyncTogether) -> None:
+ async with async_client.beta.clusters.remediations.with_streaming_response.cancel(
+ remediation_id="remediation_id",
+ cluster_id="cluster_id",
+ instance_id="instance_id",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ remediation = await response.parse()
+ assert_matches_type(RemediationCancelResponse, remediation, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ async def test_path_params_cancel(self, async_client: AsyncTogether) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `cluster_id` but received ''"):
+ await async_client.beta.clusters.remediations.with_raw_response.cancel(
+ remediation_id="remediation_id",
+ cluster_id="",
+ instance_id="instance_id",
+ )
+
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `instance_id` but received ''"):
+ await async_client.beta.clusters.remediations.with_raw_response.cancel(
+ remediation_id="remediation_id",
+ cluster_id="cluster_id",
+ instance_id="",
+ )
+
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `remediation_id` but received ''"):
+ await async_client.beta.clusters.remediations.with_raw_response.cancel(
+ remediation_id="",
+ cluster_id="cluster_id",
+ instance_id="instance_id",
+ )
+
+ @parametrize
+ async def test_method_reject(self, async_client: AsyncTogether) -> None:
+ remediation = await async_client.beta.clusters.remediations.reject(
+ remediation_id="remediation_id",
+ cluster_id="cluster_id",
+ instance_id="instance_id",
+ )
+ assert_matches_type(RemediationRejectResponse, remediation, path=["response"])
+
+ @parametrize
+ async def test_method_reject_with_all_params(self, async_client: AsyncTogether) -> None:
+ remediation = await async_client.beta.clusters.remediations.reject(
+ remediation_id="remediation_id",
+ cluster_id="cluster_id",
+ instance_id="instance_id",
+ comment="comment",
+ )
+ assert_matches_type(RemediationRejectResponse, remediation, path=["response"])
+
+ @parametrize
+ async def test_raw_response_reject(self, async_client: AsyncTogether) -> None:
+ response = await async_client.beta.clusters.remediations.with_raw_response.reject(
+ remediation_id="remediation_id",
+ cluster_id="cluster_id",
+ instance_id="instance_id",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ remediation = await response.parse()
+ assert_matches_type(RemediationRejectResponse, remediation, path=["response"])
+
+ @parametrize
+ async def test_streaming_response_reject(self, async_client: AsyncTogether) -> None:
+ async with async_client.beta.clusters.remediations.with_streaming_response.reject(
+ remediation_id="remediation_id",
+ cluster_id="cluster_id",
+ instance_id="instance_id",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ remediation = await response.parse()
+ assert_matches_type(RemediationRejectResponse, remediation, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ async def test_path_params_reject(self, async_client: AsyncTogether) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `cluster_id` but received ''"):
+ await async_client.beta.clusters.remediations.with_raw_response.reject(
+ remediation_id="remediation_id",
+ cluster_id="",
+ instance_id="instance_id",
+ )
+
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `instance_id` but received ''"):
+ await async_client.beta.clusters.remediations.with_raw_response.reject(
+ remediation_id="remediation_id",
+ cluster_id="cluster_id",
+ instance_id="",
+ )
+
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `remediation_id` but received ''"):
+ await async_client.beta.clusters.remediations.with_raw_response.reject(
+ remediation_id="",
+ cluster_id="cluster_id",
+ instance_id="instance_id",
+ )
From 98e83aa7223452fd834e643b7681615220c1978a Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 15 May 2026 18:09:24 +0000
Subject: [PATCH 08/14] feat(api): manual updates
---
.stats.yml | 6 +-
api.md | 2 +
.../resources/beta/clusters/remediations.py | 115 +++++++++++++++++
src/together/types/beta/clusters/__init__.py | 1 +
.../clusters/remediation_retrieve_response.py | 94 ++++++++++++++
.../beta/clusters/test_remediations.py | 121 ++++++++++++++++++
6 files changed, 336 insertions(+), 3 deletions(-)
create mode 100644 src/together/types/beta/clusters/remediation_retrieve_response.py
diff --git a/.stats.yml b/.stats.yml
index 3b8397a01..fe90c6295 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
-configured_endpoints: 80
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/togetherai/togetherai-496df09bdf8bfe14dddc5ba6dc70e219336f51295d2f0671285b4056ec756f21.yml
+configured_endpoints: 81
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/togetherai/togetherai-b17511a0596b10a96c1af8ea8ab68f43b5a1215f9c8a3ba57ee043215a382b14.yml
openapi_spec_hash: 50c3ce80ecc7d25283c96e3544567995
-config_hash: 2ab62260e2c5e9527b47bef3f36e545d
+config_hash: 6d7d1c4ec367638d41671e1a4f84808c
diff --git a/api.md b/api.md
index 1493164db..0a1fb2a72 100644
--- a/api.md
+++ b/api.md
@@ -98,6 +98,7 @@ Types:
```python
from together.types.beta.clusters import (
RemediationCreateResponse,
+ RemediationRetrieveResponse,
RemediationListResponse,
RemediationApproveResponse,
RemediationCancelResponse,
@@ -108,6 +109,7 @@ from together.types.beta.clusters import (
Methods:
- client.beta.clusters.remediations.create(instance_id, \*, cluster_id, \*\*params) -> RemediationCreateResponse
+- client.beta.clusters.remediations.retrieve(remediation_id, \*, cluster_id, instance_id) -> RemediationRetrieveResponse
- client.beta.clusters.remediations.list(instance_id, \*, cluster_id, \*\*params) -> RemediationListResponse
- client.beta.clusters.remediations.approve(remediation_id, \*, cluster_id, instance_id, \*\*params) -> RemediationApproveResponse
- client.beta.clusters.remediations.cancel(remediation_id, \*, cluster_id, instance_id) -> RemediationCancelResponse
diff --git a/src/together/resources/beta/clusters/remediations.py b/src/together/resources/beta/clusters/remediations.py
index c130fb213..582665994 100644
--- a/src/together/resources/beta/clusters/remediations.py
+++ b/src/together/resources/beta/clusters/remediations.py
@@ -29,6 +29,7 @@
from ....types.beta.clusters.remediation_create_response import RemediationCreateResponse
from ....types.beta.clusters.remediation_reject_response import RemediationRejectResponse
from ....types.beta.clusters.remediation_approve_response import RemediationApproveResponse
+from ....types.beta.clusters.remediation_retrieve_response import RemediationRetrieveResponse
__all__ = ["RemediationsResource", "AsyncRemediationsResource"]
@@ -137,6 +138,57 @@ def create(
cast_to=RemediationCreateResponse,
)
+ def retrieve(
+ self,
+ remediation_id: str,
+ *,
+ cluster_id: str,
+ instance_id: str,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> RemediationRetrieveResponse:
+ """
+ Retrieve the status of a specific remdiation on a specific instance in a
+ specific cluster.
+
+ Args:
+ cluster_id: The cluster ID.
+
+ instance_id: The instance ID.
+
+ remediation_id: The remediation ID.
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not cluster_id:
+ raise ValueError(f"Expected a non-empty value for `cluster_id` but received {cluster_id!r}")
+ if not instance_id:
+ raise ValueError(f"Expected a non-empty value for `instance_id` but received {instance_id!r}")
+ if not remediation_id:
+ raise ValueError(f"Expected a non-empty value for `remediation_id` but received {remediation_id!r}")
+ return self._get(
+ path_template(
+ "/compute/clusters/{cluster_id}/instances/{instance_id}/remediations/{remediation_id}",
+ cluster_id=cluster_id,
+ instance_id=instance_id,
+ remediation_id=remediation_id,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=RemediationRetrieveResponse,
+ )
+
def list(
self,
instance_id: str,
@@ -503,6 +555,57 @@ async def create(
cast_to=RemediationCreateResponse,
)
+ async def retrieve(
+ self,
+ remediation_id: str,
+ *,
+ cluster_id: str,
+ instance_id: str,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> RemediationRetrieveResponse:
+ """
+ Retrieve the status of a specific remdiation on a specific instance in a
+ specific cluster.
+
+ Args:
+ cluster_id: The cluster ID.
+
+ instance_id: The instance ID.
+
+ remediation_id: The remediation ID.
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not cluster_id:
+ raise ValueError(f"Expected a non-empty value for `cluster_id` but received {cluster_id!r}")
+ if not instance_id:
+ raise ValueError(f"Expected a non-empty value for `instance_id` but received {instance_id!r}")
+ if not remediation_id:
+ raise ValueError(f"Expected a non-empty value for `remediation_id` but received {remediation_id!r}")
+ return await self._get(
+ path_template(
+ "/compute/clusters/{cluster_id}/instances/{instance_id}/remediations/{remediation_id}",
+ cluster_id=cluster_id,
+ instance_id=instance_id,
+ remediation_id=remediation_id,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=RemediationRetrieveResponse,
+ )
+
async def list(
self,
instance_id: str,
@@ -772,6 +875,9 @@ def __init__(self, remediations: RemediationsResource) -> None:
self.create = to_raw_response_wrapper(
remediations.create,
)
+ self.retrieve = to_raw_response_wrapper(
+ remediations.retrieve,
+ )
self.list = to_raw_response_wrapper(
remediations.list,
)
@@ -793,6 +899,9 @@ def __init__(self, remediations: AsyncRemediationsResource) -> None:
self.create = async_to_raw_response_wrapper(
remediations.create,
)
+ self.retrieve = async_to_raw_response_wrapper(
+ remediations.retrieve,
+ )
self.list = async_to_raw_response_wrapper(
remediations.list,
)
@@ -814,6 +923,9 @@ def __init__(self, remediations: RemediationsResource) -> None:
self.create = to_streamed_response_wrapper(
remediations.create,
)
+ self.retrieve = to_streamed_response_wrapper(
+ remediations.retrieve,
+ )
self.list = to_streamed_response_wrapper(
remediations.list,
)
@@ -835,6 +947,9 @@ def __init__(self, remediations: AsyncRemediationsResource) -> None:
self.create = async_to_streamed_response_wrapper(
remediations.create,
)
+ self.retrieve = async_to_streamed_response_wrapper(
+ remediations.retrieve,
+ )
self.list = async_to_streamed_response_wrapper(
remediations.list,
)
diff --git a/src/together/types/beta/clusters/__init__.py b/src/together/types/beta/clusters/__init__.py
index 0ac6cbb1b..4bf653714 100644
--- a/src/together/types/beta/clusters/__init__.py
+++ b/src/together/types/beta/clusters/__init__.py
@@ -17,3 +17,4 @@
from .remediation_create_response import RemediationCreateResponse as RemediationCreateResponse
from .remediation_reject_response import RemediationRejectResponse as RemediationRejectResponse
from .remediation_approve_response import RemediationApproveResponse as RemediationApproveResponse
+from .remediation_retrieve_response import RemediationRetrieveResponse as RemediationRetrieveResponse
diff --git a/src/together/types/beta/clusters/remediation_retrieve_response.py b/src/together/types/beta/clusters/remediation_retrieve_response.py
new file mode 100644
index 000000000..ffb6bd3b4
--- /dev/null
+++ b/src/together/types/beta/clusters/remediation_retrieve_response.py
@@ -0,0 +1,94 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+from datetime import datetime
+from typing_extensions import Literal
+
+from ...._models import BaseModel
+
+__all__ = ["RemediationRetrieveResponse"]
+
+
+class RemediationRetrieveResponse(BaseModel):
+ """
+ Remediation represents a node remediation request for an instance.
+ An instance can have multiple remediations over time (e.g., failed attempts followed by retries).
+ """
+
+ id: str
+
+ cluster_id: str
+
+ instance_id: str
+
+ mode: Literal[
+ "REMEDIATION_MODE_VM_ONLY",
+ "REMEDIATION_MODE_HOST_AWARE",
+ "REMEDIATION_MODE_EVICT_WITHOUT_REPLACEMENT",
+ "REMEDIATION_MODE_REBOOT_VM",
+ ]
+ """Remediation mode specifies how the remediation should be performed.
+
+ - `REMEDIATION_MODE_VM_ONLY`: Deletes the VM and provisions a new one on any
+ available host.
+ - `REMEDIATION_MODE_HOST_AWARE`: Cordons the host, deletes the VM, and
+ provisions a new one on a different host.
+ """
+
+ state: Literal["PENDING_APPROVAL", "PENDING", "RUNNING", "SUCCEEDED", "FAILED", "CANCELLED", "AUTO_RESOLVED"]
+ """RemediationState represents the lifecycle state of a remediation.
+
+ - `PENDING_APPROVAL`: Awaiting approval before processing can begin.
+ - `PENDING`: Approved and queued for processing.
+ - `RUNNING`: Actively being processed.
+ - `SUCCEEDED`: Successfully completed.
+ - `FAILED`: Failed with an error.
+ - `CANCELLED`: Cancelled by user or system.
+ - `AUTO_RESOLVED`: The underlying issue was automatically resolved before
+ processing.
+ """
+
+ trigger: Literal["REMEDIATION_TRIGGER_MANUAL", "REMEDIATION_TRIGGER_AUTOMATED"]
+ """RemediationTrigger specifies how the remediation was triggered.
+
+ - `REMEDIATION_TRIGGER_MANUAL`: A user-initiated remediation (either via web UI
+ or API call).
+ - `REMEDIATION_TRIGGER_AUTOMATED`: A system-initiated remediation that requires
+ approval.
+ """
+
+ active_health_check_run_id: Optional[str] = None
+ """Active health check run ID (UUID) that triggered this remediation."""
+
+ create_time: Optional[datetime] = None
+ """When the remediation was created."""
+
+ end_time: Optional[datetime] = None
+ """When the remediation completed."""
+
+ error_message: Optional[str] = None
+ """Error message if the remediation failed."""
+
+ passive_health_check_event_id: Optional[str] = None
+ """Passive health check event ID that triggered this remediation."""
+
+ reason: Optional[str] = None
+ """User-provided reason for the remediation."""
+
+ requested_by: Optional[str] = None
+ """Who requested the remediation."""
+
+ review_comment: Optional[str] = None
+ """Review comment."""
+
+ review_time: Optional[datetime] = None
+ """When the remediation was reviewed."""
+
+ reviewed_by: Optional[str] = None
+ """Who reviewed the remediation."""
+
+ start_time: Optional[datetime] = None
+ """When processing started."""
+
+ update_time: Optional[datetime] = None
+ """When the remediation was last updated."""
diff --git a/tests/api_resources/beta/clusters/test_remediations.py b/tests/api_resources/beta/clusters/test_remediations.py
index 3a359fd9a..277266d4d 100644
--- a/tests/api_resources/beta/clusters/test_remediations.py
+++ b/tests/api_resources/beta/clusters/test_remediations.py
@@ -15,6 +15,7 @@
RemediationCreateResponse,
RemediationRejectResponse,
RemediationApproveResponse,
+ RemediationRetrieveResponse,
)
base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
@@ -87,6 +88,66 @@ def test_path_params_create(self, client: Together) -> None:
mode="REMEDIATION_MODE_VM_ONLY",
)
+ @parametrize
+ def test_method_retrieve(self, client: Together) -> None:
+ remediation = client.beta.clusters.remediations.retrieve(
+ remediation_id="remediation_id",
+ cluster_id="cluster_id",
+ instance_id="instance_id",
+ )
+ assert_matches_type(RemediationRetrieveResponse, remediation, path=["response"])
+
+ @parametrize
+ def test_raw_response_retrieve(self, client: Together) -> None:
+ response = client.beta.clusters.remediations.with_raw_response.retrieve(
+ remediation_id="remediation_id",
+ cluster_id="cluster_id",
+ instance_id="instance_id",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ remediation = response.parse()
+ assert_matches_type(RemediationRetrieveResponse, remediation, path=["response"])
+
+ @parametrize
+ def test_streaming_response_retrieve(self, client: Together) -> None:
+ with client.beta.clusters.remediations.with_streaming_response.retrieve(
+ remediation_id="remediation_id",
+ cluster_id="cluster_id",
+ instance_id="instance_id",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ remediation = response.parse()
+ assert_matches_type(RemediationRetrieveResponse, remediation, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ def test_path_params_retrieve(self, client: Together) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `cluster_id` but received ''"):
+ client.beta.clusters.remediations.with_raw_response.retrieve(
+ remediation_id="remediation_id",
+ cluster_id="",
+ instance_id="instance_id",
+ )
+
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `instance_id` but received ''"):
+ client.beta.clusters.remediations.with_raw_response.retrieve(
+ remediation_id="remediation_id",
+ cluster_id="cluster_id",
+ instance_id="",
+ )
+
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `remediation_id` but received ''"):
+ client.beta.clusters.remediations.with_raw_response.retrieve(
+ remediation_id="",
+ cluster_id="cluster_id",
+ instance_id="instance_id",
+ )
+
@parametrize
def test_method_list(self, client: Together) -> None:
remediation = client.beta.clusters.remediations.list(
@@ -419,6 +480,66 @@ async def test_path_params_create(self, async_client: AsyncTogether) -> None:
mode="REMEDIATION_MODE_VM_ONLY",
)
+ @parametrize
+ async def test_method_retrieve(self, async_client: AsyncTogether) -> None:
+ remediation = await async_client.beta.clusters.remediations.retrieve(
+ remediation_id="remediation_id",
+ cluster_id="cluster_id",
+ instance_id="instance_id",
+ )
+ assert_matches_type(RemediationRetrieveResponse, remediation, path=["response"])
+
+ @parametrize
+ async def test_raw_response_retrieve(self, async_client: AsyncTogether) -> None:
+ response = await async_client.beta.clusters.remediations.with_raw_response.retrieve(
+ remediation_id="remediation_id",
+ cluster_id="cluster_id",
+ instance_id="instance_id",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ remediation = await response.parse()
+ assert_matches_type(RemediationRetrieveResponse, remediation, path=["response"])
+
+ @parametrize
+ async def test_streaming_response_retrieve(self, async_client: AsyncTogether) -> None:
+ async with async_client.beta.clusters.remediations.with_streaming_response.retrieve(
+ remediation_id="remediation_id",
+ cluster_id="cluster_id",
+ instance_id="instance_id",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ remediation = await response.parse()
+ assert_matches_type(RemediationRetrieveResponse, remediation, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ async def test_path_params_retrieve(self, async_client: AsyncTogether) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `cluster_id` but received ''"):
+ await async_client.beta.clusters.remediations.with_raw_response.retrieve(
+ remediation_id="remediation_id",
+ cluster_id="",
+ instance_id="instance_id",
+ )
+
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `instance_id` but received ''"):
+ await async_client.beta.clusters.remediations.with_raw_response.retrieve(
+ remediation_id="remediation_id",
+ cluster_id="cluster_id",
+ instance_id="",
+ )
+
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `remediation_id` but received ''"):
+ await async_client.beta.clusters.remediations.with_raw_response.retrieve(
+ remediation_id="",
+ cluster_id="cluster_id",
+ instance_id="instance_id",
+ )
+
@parametrize
async def test_method_list(self, async_client: AsyncTogether) -> None:
remediation = await async_client.beta.clusters.remediations.list(
From e6a7c375285216754eb7f531b40f7bbe843c1638 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 15 May 2026 18:18:45 +0000
Subject: [PATCH 09/14] fix(api): remove trigger parameter from remediations
list method
---
.stats.yml | 4 +-
.../resources/beta/clusters/remediations.py | 144 ++++++------------
src/together/resources/models/models.py | 6 +-
.../clusters/remediation_approve_params.py | 2 -
.../clusters/remediation_create_params.py | 3 +-
.../beta/clusters/remediation_list_params.py | 32 ++--
.../clusters/remediation_reject_params.py | 2 -
.../beta/clusters/test_remediations.py | 2 -
8 files changed, 72 insertions(+), 123 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index fe90c6295..d57a71e2c 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 81
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/togetherai/togetherai-b17511a0596b10a96c1af8ea8ab68f43b5a1215f9c8a3ba57ee043215a382b14.yml
-openapi_spec_hash: 50c3ce80ecc7d25283c96e3544567995
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/togetherai/togetherai-c8200db997f998a51b2b469b69fcc3a24032e50b45aaaf340c2c176473393adc.yml
+openapi_spec_hash: 2544f6840cf7ad23eae3fe8bffed4c4b
config_hash: 6d7d1c4ec367638d41671e1a4f84808c
diff --git a/src/together/resources/beta/clusters/remediations.py b/src/together/resources/beta/clusters/remediations.py
index 582665994..0f4a17b59 100644
--- a/src/together/resources/beta/clusters/remediations.py
+++ b/src/together/resources/beta/clusters/remediations.py
@@ -77,19 +77,14 @@ def create(
"""
Creates a new remediation for an instance.
- If mode is unspecified, it defaults to VM_ONLY. If trigger is unspecified, it
- defaults to MANUAL.
+ Remediations created via the API goes directly to PENDING state.
- For MANUAL triggers: The remediation goes directly to PENDING state.
-
- For AUTOMATED triggers: The remediation is created with PENDING_APPROVAL state.
- The caller must then use ApproveRemediation to start the remediation process.
+ Our system may trigger automated remediations that require approval. These
+ remediations are created with PENDING_APPROVAL state. The user must call
+ /approve to start the actual remediation process. These operations can also be
+ rejected by calling /reject.
Args:
- cluster_id: The cluster ID.
-
- instance_id: The instance ID.
-
mode: Remediation mode specifies how the remediation should be performed.
- `REMEDIATION_MODE_VM_ONLY`: Deletes the VM and provisions a new one on any
@@ -97,7 +92,7 @@ def create(
- `REMEDIATION_MODE_HOST_AWARE`: Cordons the host, deletes the VM, and
provisions a new one on a different host.
- remediation_id: Optional. Client-specified ID for idempotency.
+ remediation_id: Client-specified ID for idempotency.
reason: User-provided reason for the remediation.
@@ -156,12 +151,6 @@ def retrieve(
specific cluster.
Args:
- cluster_id: The cluster ID.
-
- instance_id: The instance ID.
-
- remediation_id: The remediation ID.
-
extra_headers: Send extra headers
extra_query: Add additional query parameters to the request
@@ -208,7 +197,6 @@ def list(
Literal["PENDING_APPROVAL", "PENDING", "RUNNING", "SUCCEEDED", "FAILED", "CANCELLED", "AUTO_RESOLVED"]
]
| Omit = omit,
- trigger: Literal["REMEDIATION_TRIGGER_MANUAL", "REMEDIATION_TRIGGER_AUTOMATED"] | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
extra_headers: Headers | None = None,
@@ -216,30 +204,33 @@ def list(
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> RemediationListResponse:
- """Lists remediations for an instance or cluster.
-
- Use instances/- as wildcard to
- list all remediations in a cluster.
+ """
+ Lists remediations for an instance or cluster.
Args:
- cluster_id: The cluster ID.
+ instance_id: To list remediations on a specific node, pass the node's instance ID. To list
+ remediations for all nodes in a cluster, pass `-` as a wildcard for the instance
+ ID.
- instance_id: The instance ID.
-
- mode: Optional. Filter by remediation mode. Returns only remediations matching the
- specified mode.
+ mode: Filter by remediation mode. Returns only remediations matching the specified
+ mode.
- order_by: Optional. Order by expression.
+ order_by: Order by expression.
- page_size: Optional. Maximum results to return.
+ page_size: Maximum results to return.
- page_token: Optional. Pagination token from previous request.
+ page_token: Pagination token from previous request.
- state: Optional. Filter by state(s). Returns remediations matching any of the specified
- states.
+ state: Filter by state(s). Returns remediations matching any of the specified states.
- trigger: Optional. Filter by trigger type. Returns only remediations matching the
- specified trigger.
+ - `PENDING_APPROVAL`: Awaiting approval before processing can begin.
+ - `PENDING`: Approved and queued for processing.
+ - `RUNNING`: Actively being processed.
+ - `SUCCEEDED`: Successfully completed.
+ - `FAILED`: Failed with an error.
+ - `CANCELLED`: Cancelled by user or system.
+ - `AUTO_RESOLVED`: The underlying issue was automatically resolved before
+ processing.
extra_headers: Send extra headers
@@ -271,7 +262,6 @@ def list(
"page_size": page_size,
"page_token": page_token,
"state": state,
- "trigger": trigger,
},
remediation_list_params.RemediationListParams,
),
@@ -303,12 +293,6 @@ def approve(
remediation after approval.
Args:
- cluster_id: The cluster ID.
-
- instance_id: The instance ID.
-
- remediation_id: The remediation ID.
-
comment: Comment explaining the action.
extra_headers: Send extra headers
@@ -414,12 +398,6 @@ def reject(
review_comment fields are populated on the remediation after rejection.
Args:
- cluster_id: The cluster ID.
-
- instance_id: The instance ID.
-
- remediation_id: The remediation ID.
-
comment: Comment explaining the action.
extra_headers: Send extra headers
@@ -494,19 +472,14 @@ async def create(
"""
Creates a new remediation for an instance.
- If mode is unspecified, it defaults to VM_ONLY. If trigger is unspecified, it
- defaults to MANUAL.
+ Remediations created via the API goes directly to PENDING state.
- For MANUAL triggers: The remediation goes directly to PENDING state.
-
- For AUTOMATED triggers: The remediation is created with PENDING_APPROVAL state.
- The caller must then use ApproveRemediation to start the remediation process.
+ Our system may trigger automated remediations that require approval. These
+ remediations are created with PENDING_APPROVAL state. The user must call
+ /approve to start the actual remediation process. These operations can also be
+ rejected by calling /reject.
Args:
- cluster_id: The cluster ID.
-
- instance_id: The instance ID.
-
mode: Remediation mode specifies how the remediation should be performed.
- `REMEDIATION_MODE_VM_ONLY`: Deletes the VM and provisions a new one on any
@@ -514,7 +487,7 @@ async def create(
- `REMEDIATION_MODE_HOST_AWARE`: Cordons the host, deletes the VM, and
provisions a new one on a different host.
- remediation_id: Optional. Client-specified ID for idempotency.
+ remediation_id: Client-specified ID for idempotency.
reason: User-provided reason for the remediation.
@@ -573,12 +546,6 @@ async def retrieve(
specific cluster.
Args:
- cluster_id: The cluster ID.
-
- instance_id: The instance ID.
-
- remediation_id: The remediation ID.
-
extra_headers: Send extra headers
extra_query: Add additional query parameters to the request
@@ -625,7 +592,6 @@ async def list(
Literal["PENDING_APPROVAL", "PENDING", "RUNNING", "SUCCEEDED", "FAILED", "CANCELLED", "AUTO_RESOLVED"]
]
| Omit = omit,
- trigger: Literal["REMEDIATION_TRIGGER_MANUAL", "REMEDIATION_TRIGGER_AUTOMATED"] | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
extra_headers: Headers | None = None,
@@ -633,30 +599,33 @@ async def list(
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> RemediationListResponse:
- """Lists remediations for an instance or cluster.
-
- Use instances/- as wildcard to
- list all remediations in a cluster.
+ """
+ Lists remediations for an instance or cluster.
Args:
- cluster_id: The cluster ID.
+ instance_id: To list remediations on a specific node, pass the node's instance ID. To list
+ remediations for all nodes in a cluster, pass `-` as a wildcard for the instance
+ ID.
- instance_id: The instance ID.
-
- mode: Optional. Filter by remediation mode. Returns only remediations matching the
- specified mode.
+ mode: Filter by remediation mode. Returns only remediations matching the specified
+ mode.
- order_by: Optional. Order by expression.
+ order_by: Order by expression.
- page_size: Optional. Maximum results to return.
+ page_size: Maximum results to return.
- page_token: Optional. Pagination token from previous request.
+ page_token: Pagination token from previous request.
- state: Optional. Filter by state(s). Returns remediations matching any of the specified
- states.
+ state: Filter by state(s). Returns remediations matching any of the specified states.
- trigger: Optional. Filter by trigger type. Returns only remediations matching the
- specified trigger.
+ - `PENDING_APPROVAL`: Awaiting approval before processing can begin.
+ - `PENDING`: Approved and queued for processing.
+ - `RUNNING`: Actively being processed.
+ - `SUCCEEDED`: Successfully completed.
+ - `FAILED`: Failed with an error.
+ - `CANCELLED`: Cancelled by user or system.
+ - `AUTO_RESOLVED`: The underlying issue was automatically resolved before
+ processing.
extra_headers: Send extra headers
@@ -688,7 +657,6 @@ async def list(
"page_size": page_size,
"page_token": page_token,
"state": state,
- "trigger": trigger,
},
remediation_list_params.RemediationListParams,
),
@@ -720,12 +688,6 @@ async def approve(
remediation after approval.
Args:
- cluster_id: The cluster ID.
-
- instance_id: The instance ID.
-
- remediation_id: The remediation ID.
-
comment: Comment explaining the action.
extra_headers: Send extra headers
@@ -831,12 +793,6 @@ async def reject(
review_comment fields are populated on the remediation after rejection.
Args:
- cluster_id: The cluster ID.
-
- instance_id: The instance ID.
-
- remediation_id: The remediation ID.
-
comment: Comment explaining the action.
extra_headers: Send extra headers
diff --git a/src/together/resources/models/models.py b/src/together/resources/models/models.py
index b14a0a630..a3486129c 100644
--- a/src/together/resources/models/models.py
+++ b/src/together/resources/models/models.py
@@ -68,7 +68,8 @@ def list(
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> ModelListResponse:
"""
- Lists all of Together's open-source models
+ Lists all of Together's open-source models and metadata including pricing, chat
+ template, and context.
Args:
dedicated: Filter models to only return dedicated models
@@ -195,7 +196,8 @@ async def list(
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> ModelListResponse:
"""
- Lists all of Together's open-source models
+ Lists all of Together's open-source models and metadata including pricing, chat
+ template, and context.
Args:
dedicated: Filter models to only return dedicated models
diff --git a/src/together/types/beta/clusters/remediation_approve_params.py b/src/together/types/beta/clusters/remediation_approve_params.py
index cb86457e7..721f96e08 100644
--- a/src/together/types/beta/clusters/remediation_approve_params.py
+++ b/src/together/types/beta/clusters/remediation_approve_params.py
@@ -9,10 +9,8 @@
class RemediationApproveParams(TypedDict, total=False):
cluster_id: Required[str]
- """The cluster ID."""
instance_id: Required[str]
- """The instance ID."""
comment: str
"""Comment explaining the action."""
diff --git a/src/together/types/beta/clusters/remediation_create_params.py b/src/together/types/beta/clusters/remediation_create_params.py
index 84374cec9..c32ade02f 100644
--- a/src/together/types/beta/clusters/remediation_create_params.py
+++ b/src/together/types/beta/clusters/remediation_create_params.py
@@ -9,7 +9,6 @@
class RemediationCreateParams(TypedDict, total=False):
cluster_id: Required[str]
- """The cluster ID."""
mode: Required[
Literal[
@@ -28,7 +27,7 @@ class RemediationCreateParams(TypedDict, total=False):
"""
remediation_id: str
- """Optional. Client-specified ID for idempotency."""
+ """Client-specified ID for idempotency."""
reason: str
"""User-provided reason for the remediation."""
diff --git a/src/together/types/beta/clusters/remediation_list_params.py b/src/together/types/beta/clusters/remediation_list_params.py
index 18e6b884e..215800be9 100644
--- a/src/together/types/beta/clusters/remediation_list_params.py
+++ b/src/together/types/beta/clusters/remediation_list_params.py
@@ -10,7 +10,6 @@
class RemediationListParams(TypedDict, total=False):
cluster_id: Required[str]
- """The cluster ID."""
mode: Literal[
"REMEDIATION_MODE_VM_ONLY",
@@ -18,30 +17,29 @@ class RemediationListParams(TypedDict, total=False):
"REMEDIATION_MODE_EVICT_WITHOUT_REPLACEMENT",
"REMEDIATION_MODE_REBOOT_VM",
]
- """Optional.
+ """Filter by remediation mode.
- Filter by remediation mode. Returns only remediations matching the specified
- mode.
+ Returns only remediations matching the specified mode.
"""
order_by: str
- """Optional. Order by expression."""
+ """Order by expression."""
page_size: int
- """Optional. Maximum results to return."""
+ """Maximum results to return."""
page_token: str
- """Optional. Pagination token from previous request."""
+ """Pagination token from previous request."""
state: List[Literal["PENDING_APPROVAL", "PENDING", "RUNNING", "SUCCEEDED", "FAILED", "CANCELLED", "AUTO_RESOLVED"]]
- """Optional.
-
- Filter by state(s). Returns remediations matching any of the specified states.
- """
-
- trigger: Literal["REMEDIATION_TRIGGER_MANUAL", "REMEDIATION_TRIGGER_AUTOMATED"]
- """Optional.
-
- Filter by trigger type. Returns only remediations matching the specified
- trigger.
+ """Filter by state(s). Returns remediations matching any of the specified states.
+
+ - `PENDING_APPROVAL`: Awaiting approval before processing can begin.
+ - `PENDING`: Approved and queued for processing.
+ - `RUNNING`: Actively being processed.
+ - `SUCCEEDED`: Successfully completed.
+ - `FAILED`: Failed with an error.
+ - `CANCELLED`: Cancelled by user or system.
+ - `AUTO_RESOLVED`: The underlying issue was automatically resolved before
+ processing.
"""
diff --git a/src/together/types/beta/clusters/remediation_reject_params.py b/src/together/types/beta/clusters/remediation_reject_params.py
index de91e59f9..cfe532aa3 100644
--- a/src/together/types/beta/clusters/remediation_reject_params.py
+++ b/src/together/types/beta/clusters/remediation_reject_params.py
@@ -9,10 +9,8 @@
class RemediationRejectParams(TypedDict, total=False):
cluster_id: Required[str]
- """The cluster ID."""
instance_id: Required[str]
- """The instance ID."""
comment: str
"""Comment explaining the action."""
diff --git a/tests/api_resources/beta/clusters/test_remediations.py b/tests/api_resources/beta/clusters/test_remediations.py
index 277266d4d..7fb96a882 100644
--- a/tests/api_resources/beta/clusters/test_remediations.py
+++ b/tests/api_resources/beta/clusters/test_remediations.py
@@ -166,7 +166,6 @@ def test_method_list_with_all_params(self, client: Together) -> None:
page_size=0,
page_token="page_token",
state=["PENDING_APPROVAL"],
- trigger="REMEDIATION_TRIGGER_MANUAL",
)
assert_matches_type(RemediationListResponse, remediation, path=["response"])
@@ -558,7 +557,6 @@ async def test_method_list_with_all_params(self, async_client: AsyncTogether) ->
page_size=0,
page_token="page_token",
state=["PENDING_APPROVAL"],
- trigger="REMEDIATION_TRIGGER_MANUAL",
)
assert_matches_type(RemediationListResponse, remediation, path=["response"])
From 598dbd011990fc2b3fc0738dd178a5fd3997e676 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 15 May 2026 18:27:15 +0000
Subject: [PATCH 10/14] feat(api): manual updates
---
.stats.yml | 2 +-
api.md | 19 ++--
.../resources/beta/clusters/remediations.py | 46 +++++----
src/together/types/beta/cluster.py | 89 +-----------------
src/together/types/beta/clusters/__init__.py | 6 +-
...tion_cancel_response.py => remediation.py} | 4 +-
.../clusters/remediation_approve_response.py | 94 -------------------
.../clusters/remediation_create_response.py | 94 -------------------
.../clusters/remediation_list_response.py | 92 +-----------------
.../clusters/remediation_reject_response.py | 94 -------------------
.../clusters/remediation_retrieve_response.py | 94 -------------------
.../beta/clusters/test_remediations.py | 78 ++++++++-------
12 files changed, 73 insertions(+), 639 deletions(-)
rename src/together/types/beta/clusters/{remediation_cancel_response.py => remediation.py} (97%)
delete mode 100644 src/together/types/beta/clusters/remediation_approve_response.py
delete mode 100644 src/together/types/beta/clusters/remediation_create_response.py
delete mode 100644 src/together/types/beta/clusters/remediation_reject_response.py
delete mode 100644 src/together/types/beta/clusters/remediation_retrieve_response.py
diff --git a/.stats.yml b/.stats.yml
index d57a71e2c..37b2bba11 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 81
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/togetherai/togetherai-c8200db997f998a51b2b469b69fcc3a24032e50b45aaaf340c2c176473393adc.yml
openapi_spec_hash: 2544f6840cf7ad23eae3fe8bffed4c4b
-config_hash: 6d7d1c4ec367638d41671e1a4f84808c
+config_hash: b35d5968fb07cce1c1be735f874898b1
diff --git a/api.md b/api.md
index 0a1fb2a72..156264d90 100644
--- a/api.md
+++ b/api.md
@@ -96,24 +96,17 @@ Methods:
Types:
```python
-from together.types.beta.clusters import (
- RemediationCreateResponse,
- RemediationRetrieveResponse,
- RemediationListResponse,
- RemediationApproveResponse,
- RemediationCancelResponse,
- RemediationRejectResponse,
-)
+from together.types.beta.clusters import Remediation, RemediationListResponse
```
Methods:
-- client.beta.clusters.remediations.create(instance_id, \*, cluster_id, \*\*params) -> RemediationCreateResponse
-- client.beta.clusters.remediations.retrieve(remediation_id, \*, cluster_id, instance_id) -> RemediationRetrieveResponse
+- client.beta.clusters.remediations.create(instance_id, \*, cluster_id, \*\*params) -> Remediation
+- client.beta.clusters.remediations.retrieve(remediation_id, \*, cluster_id, instance_id) -> Remediation
- client.beta.clusters.remediations.list(instance_id, \*, cluster_id, \*\*params) -> RemediationListResponse
-- client.beta.clusters.remediations.approve(remediation_id, \*, cluster_id, instance_id, \*\*params) -> RemediationApproveResponse
-- client.beta.clusters.remediations.cancel(remediation_id, \*, cluster_id, instance_id) -> RemediationCancelResponse
-- client.beta.clusters.remediations.reject(remediation_id, \*, cluster_id, instance_id, \*\*params) -> RemediationRejectResponse
+- client.beta.clusters.remediations.approve(remediation_id, \*, cluster_id, instance_id, \*\*params) -> Remediation
+- client.beta.clusters.remediations.cancel(remediation_id, \*, cluster_id, instance_id) -> Remediation
+- client.beta.clusters.remediations.reject(remediation_id, \*, cluster_id, instance_id, \*\*params) -> Remediation
### Storage
diff --git a/src/together/resources/beta/clusters/remediations.py b/src/together/resources/beta/clusters/remediations.py
index 0f4a17b59..33dff106e 100644
--- a/src/together/resources/beta/clusters/remediations.py
+++ b/src/together/resources/beta/clusters/remediations.py
@@ -24,12 +24,8 @@
remediation_reject_params,
remediation_approve_params,
)
+from ....types.beta.clusters.remediation import Remediation
from ....types.beta.clusters.remediation_list_response import RemediationListResponse
-from ....types.beta.clusters.remediation_cancel_response import RemediationCancelResponse
-from ....types.beta.clusters.remediation_create_response import RemediationCreateResponse
-from ....types.beta.clusters.remediation_reject_response import RemediationRejectResponse
-from ....types.beta.clusters.remediation_approve_response import RemediationApproveResponse
-from ....types.beta.clusters.remediation_retrieve_response import RemediationRetrieveResponse
__all__ = ["RemediationsResource", "AsyncRemediationsResource"]
@@ -73,7 +69,7 @@ def create(
extra_query: Query | None = None,
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
- ) -> RemediationCreateResponse:
+ ) -> Remediation:
"""
Creates a new remediation for an instance.
@@ -130,7 +126,7 @@ def create(
{"remediation_id": remediation_id}, remediation_create_params.RemediationCreateParams
),
),
- cast_to=RemediationCreateResponse,
+ cast_to=Remediation,
)
def retrieve(
@@ -145,7 +141,7 @@ def retrieve(
extra_query: Query | None = None,
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
- ) -> RemediationRetrieveResponse:
+ ) -> Remediation:
"""
Retrieve the status of a specific remdiation on a specific instance in a
specific cluster.
@@ -175,7 +171,7 @@ def retrieve(
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
- cast_to=RemediationRetrieveResponse,
+ cast_to=Remediation,
)
def list(
@@ -282,7 +278,7 @@ def approve(
extra_query: Query | None = None,
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
- ) -> RemediationApproveResponse:
+ ) -> Remediation:
"""
Approves a pending remediation.
@@ -320,7 +316,7 @@ def approve(
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
- cast_to=RemediationApproveResponse,
+ cast_to=Remediation,
)
def cancel(
@@ -335,7 +331,7 @@ def cancel(
extra_query: Query | None = None,
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
- ) -> RemediationCancelResponse:
+ ) -> Remediation:
"""
Cancels a pending remediation.
@@ -372,7 +368,7 @@ def cancel(
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
- cast_to=RemediationCancelResponse,
+ cast_to=Remediation,
)
def reject(
@@ -388,7 +384,7 @@ def reject(
extra_query: Query | None = None,
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
- ) -> RemediationRejectResponse:
+ ) -> Remediation:
"""
Rejects a pending remediation.
@@ -425,7 +421,7 @@ def reject(
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
- cast_to=RemediationRejectResponse,
+ cast_to=Remediation,
)
@@ -468,7 +464,7 @@ async def create(
extra_query: Query | None = None,
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
- ) -> RemediationCreateResponse:
+ ) -> Remediation:
"""
Creates a new remediation for an instance.
@@ -525,7 +521,7 @@ async def create(
{"remediation_id": remediation_id}, remediation_create_params.RemediationCreateParams
),
),
- cast_to=RemediationCreateResponse,
+ cast_to=Remediation,
)
async def retrieve(
@@ -540,7 +536,7 @@ async def retrieve(
extra_query: Query | None = None,
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
- ) -> RemediationRetrieveResponse:
+ ) -> Remediation:
"""
Retrieve the status of a specific remdiation on a specific instance in a
specific cluster.
@@ -570,7 +566,7 @@ async def retrieve(
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
- cast_to=RemediationRetrieveResponse,
+ cast_to=Remediation,
)
async def list(
@@ -677,7 +673,7 @@ async def approve(
extra_query: Query | None = None,
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
- ) -> RemediationApproveResponse:
+ ) -> Remediation:
"""
Approves a pending remediation.
@@ -715,7 +711,7 @@ async def approve(
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
- cast_to=RemediationApproveResponse,
+ cast_to=Remediation,
)
async def cancel(
@@ -730,7 +726,7 @@ async def cancel(
extra_query: Query | None = None,
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
- ) -> RemediationCancelResponse:
+ ) -> Remediation:
"""
Cancels a pending remediation.
@@ -767,7 +763,7 @@ async def cancel(
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
- cast_to=RemediationCancelResponse,
+ cast_to=Remediation,
)
async def reject(
@@ -783,7 +779,7 @@ async def reject(
extra_query: Query | None = None,
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
- ) -> RemediationRejectResponse:
+ ) -> Remediation:
"""
Rejects a pending remediation.
@@ -820,7 +816,7 @@ async def reject(
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
- cast_to=RemediationRejectResponse,
+ cast_to=Remediation,
)
diff --git a/src/together/types/beta/cluster.py b/src/together/types/beta/cluster.py
index e78c9a07a..2f46d55a0 100644
--- a/src/together/types/beta/cluster.py
+++ b/src/together/types/beta/cluster.py
@@ -5,6 +5,7 @@
from typing_extensions import Literal
from ..._models import BaseModel
+from .clusters.remediation import Remediation
__all__ = [
"Cluster",
@@ -19,7 +20,6 @@
"ControlPlaneNodePhaseTransition",
"GPUWorkerNode",
"GPUWorkerNodePhaseTransition",
- "GPUWorkerNodeLatestRemediation",
"PhaseTransition",
"Volume",
"ClusterConfig",
@@ -123,91 +123,6 @@ class GPUWorkerNodePhaseTransition(BaseModel):
"""Timestamp when the phase transition occurred."""
-class GPUWorkerNodeLatestRemediation(BaseModel):
- """
- Remediation represents a node remediation request for an instance.
- An instance can have multiple remediations over time (e.g., failed attempts followed by retries).
- """
-
- id: str
-
- cluster_id: str
-
- instance_id: str
-
- mode: Literal[
- "REMEDIATION_MODE_VM_ONLY",
- "REMEDIATION_MODE_HOST_AWARE",
- "REMEDIATION_MODE_EVICT_WITHOUT_REPLACEMENT",
- "REMEDIATION_MODE_REBOOT_VM",
- ]
- """Remediation mode specifies how the remediation should be performed.
-
- - `REMEDIATION_MODE_VM_ONLY`: Deletes the VM and provisions a new one on any
- available host.
- - `REMEDIATION_MODE_HOST_AWARE`: Cordons the host, deletes the VM, and
- provisions a new one on a different host.
- """
-
- state: Literal["PENDING_APPROVAL", "PENDING", "RUNNING", "SUCCEEDED", "FAILED", "CANCELLED", "AUTO_RESOLVED"]
- """RemediationState represents the lifecycle state of a remediation.
-
- - `PENDING_APPROVAL`: Awaiting approval before processing can begin.
- - `PENDING`: Approved and queued for processing.
- - `RUNNING`: Actively being processed.
- - `SUCCEEDED`: Successfully completed.
- - `FAILED`: Failed with an error.
- - `CANCELLED`: Cancelled by user or system.
- - `AUTO_RESOLVED`: The underlying issue was automatically resolved before
- processing.
- """
-
- trigger: Literal["REMEDIATION_TRIGGER_MANUAL", "REMEDIATION_TRIGGER_AUTOMATED"]
- """RemediationTrigger specifies how the remediation was triggered.
-
- - `REMEDIATION_TRIGGER_MANUAL`: A user-initiated remediation (either via web UI
- or API call).
- - `REMEDIATION_TRIGGER_AUTOMATED`: A system-initiated remediation that requires
- approval.
- """
-
- active_health_check_run_id: Optional[str] = None
- """Active health check run ID (UUID) that triggered this remediation."""
-
- create_time: Optional[datetime] = None
- """When the remediation was created."""
-
- end_time: Optional[datetime] = None
- """When the remediation completed."""
-
- error_message: Optional[str] = None
- """Error message if the remediation failed."""
-
- passive_health_check_event_id: Optional[str] = None
- """Passive health check event ID that triggered this remediation."""
-
- reason: Optional[str] = None
- """User-provided reason for the remediation."""
-
- requested_by: Optional[str] = None
- """Who requested the remediation."""
-
- review_comment: Optional[str] = None
- """Review comment."""
-
- review_time: Optional[datetime] = None
- """When the remediation was reviewed."""
-
- reviewed_by: Optional[str] = None
- """Who reviewed the remediation."""
-
- start_time: Optional[datetime] = None
- """When processing started."""
-
- update_time: Optional[datetime] = None
- """When the remediation was last updated."""
-
-
class GPUWorkerNode(BaseModel):
host_name: str
@@ -230,7 +145,7 @@ class GPUWorkerNode(BaseModel):
instance_id: Optional[str] = None
- latest_remediation: Optional[GPUWorkerNodeLatestRemediation] = None
+ latest_remediation: Optional[Remediation] = None
"""
Remediation represents a node remediation request for an instance. An instance
can have multiple remediations over time (e.g., failed attempts followed by
diff --git a/src/together/types/beta/clusters/__init__.py b/src/together/types/beta/clusters/__init__.py
index 4bf653714..89ca43ab3 100644
--- a/src/together/types/beta/clusters/__init__.py
+++ b/src/together/types/beta/clusters/__init__.py
@@ -2,6 +2,7 @@
from __future__ import annotations
+from .remediation import Remediation as Remediation
from .cluster_storage import ClusterStorage as ClusterStorage
from .storage_list_params import StorageListParams as StorageListParams
from .storage_create_params import StorageCreateParams as StorageCreateParams
@@ -13,8 +14,3 @@
from .remediation_list_response import RemediationListResponse as RemediationListResponse
from .remediation_reject_params import RemediationRejectParams as RemediationRejectParams
from .remediation_approve_params import RemediationApproveParams as RemediationApproveParams
-from .remediation_cancel_response import RemediationCancelResponse as RemediationCancelResponse
-from .remediation_create_response import RemediationCreateResponse as RemediationCreateResponse
-from .remediation_reject_response import RemediationRejectResponse as RemediationRejectResponse
-from .remediation_approve_response import RemediationApproveResponse as RemediationApproveResponse
-from .remediation_retrieve_response import RemediationRetrieveResponse as RemediationRetrieveResponse
diff --git a/src/together/types/beta/clusters/remediation_cancel_response.py b/src/together/types/beta/clusters/remediation.py
similarity index 97%
rename from src/together/types/beta/clusters/remediation_cancel_response.py
rename to src/together/types/beta/clusters/remediation.py
index ec8975c9b..fc606a0f7 100644
--- a/src/together/types/beta/clusters/remediation_cancel_response.py
+++ b/src/together/types/beta/clusters/remediation.py
@@ -6,10 +6,10 @@
from ...._models import BaseModel
-__all__ = ["RemediationCancelResponse"]
+__all__ = ["Remediation"]
-class RemediationCancelResponse(BaseModel):
+class Remediation(BaseModel):
"""
Remediation represents a node remediation request for an instance.
An instance can have multiple remediations over time (e.g., failed attempts followed by retries).
diff --git a/src/together/types/beta/clusters/remediation_approve_response.py b/src/together/types/beta/clusters/remediation_approve_response.py
deleted file mode 100644
index 040e6dab2..000000000
--- a/src/together/types/beta/clusters/remediation_approve_response.py
+++ /dev/null
@@ -1,94 +0,0 @@
-# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-
-from typing import Optional
-from datetime import datetime
-from typing_extensions import Literal
-
-from ...._models import BaseModel
-
-__all__ = ["RemediationApproveResponse"]
-
-
-class RemediationApproveResponse(BaseModel):
- """
- Remediation represents a node remediation request for an instance.
- An instance can have multiple remediations over time (e.g., failed attempts followed by retries).
- """
-
- id: str
-
- cluster_id: str
-
- instance_id: str
-
- mode: Literal[
- "REMEDIATION_MODE_VM_ONLY",
- "REMEDIATION_MODE_HOST_AWARE",
- "REMEDIATION_MODE_EVICT_WITHOUT_REPLACEMENT",
- "REMEDIATION_MODE_REBOOT_VM",
- ]
- """Remediation mode specifies how the remediation should be performed.
-
- - `REMEDIATION_MODE_VM_ONLY`: Deletes the VM and provisions a new one on any
- available host.
- - `REMEDIATION_MODE_HOST_AWARE`: Cordons the host, deletes the VM, and
- provisions a new one on a different host.
- """
-
- state: Literal["PENDING_APPROVAL", "PENDING", "RUNNING", "SUCCEEDED", "FAILED", "CANCELLED", "AUTO_RESOLVED"]
- """RemediationState represents the lifecycle state of a remediation.
-
- - `PENDING_APPROVAL`: Awaiting approval before processing can begin.
- - `PENDING`: Approved and queued for processing.
- - `RUNNING`: Actively being processed.
- - `SUCCEEDED`: Successfully completed.
- - `FAILED`: Failed with an error.
- - `CANCELLED`: Cancelled by user or system.
- - `AUTO_RESOLVED`: The underlying issue was automatically resolved before
- processing.
- """
-
- trigger: Literal["REMEDIATION_TRIGGER_MANUAL", "REMEDIATION_TRIGGER_AUTOMATED"]
- """RemediationTrigger specifies how the remediation was triggered.
-
- - `REMEDIATION_TRIGGER_MANUAL`: A user-initiated remediation (either via web UI
- or API call).
- - `REMEDIATION_TRIGGER_AUTOMATED`: A system-initiated remediation that requires
- approval.
- """
-
- active_health_check_run_id: Optional[str] = None
- """Active health check run ID (UUID) that triggered this remediation."""
-
- create_time: Optional[datetime] = None
- """When the remediation was created."""
-
- end_time: Optional[datetime] = None
- """When the remediation completed."""
-
- error_message: Optional[str] = None
- """Error message if the remediation failed."""
-
- passive_health_check_event_id: Optional[str] = None
- """Passive health check event ID that triggered this remediation."""
-
- reason: Optional[str] = None
- """User-provided reason for the remediation."""
-
- requested_by: Optional[str] = None
- """Who requested the remediation."""
-
- review_comment: Optional[str] = None
- """Review comment."""
-
- review_time: Optional[datetime] = None
- """When the remediation was reviewed."""
-
- reviewed_by: Optional[str] = None
- """Who reviewed the remediation."""
-
- start_time: Optional[datetime] = None
- """When processing started."""
-
- update_time: Optional[datetime] = None
- """When the remediation was last updated."""
diff --git a/src/together/types/beta/clusters/remediation_create_response.py b/src/together/types/beta/clusters/remediation_create_response.py
deleted file mode 100644
index 2b148f932..000000000
--- a/src/together/types/beta/clusters/remediation_create_response.py
+++ /dev/null
@@ -1,94 +0,0 @@
-# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-
-from typing import Optional
-from datetime import datetime
-from typing_extensions import Literal
-
-from ...._models import BaseModel
-
-__all__ = ["RemediationCreateResponse"]
-
-
-class RemediationCreateResponse(BaseModel):
- """
- Remediation represents a node remediation request for an instance.
- An instance can have multiple remediations over time (e.g., failed attempts followed by retries).
- """
-
- id: str
-
- cluster_id: str
-
- instance_id: str
-
- mode: Literal[
- "REMEDIATION_MODE_VM_ONLY",
- "REMEDIATION_MODE_HOST_AWARE",
- "REMEDIATION_MODE_EVICT_WITHOUT_REPLACEMENT",
- "REMEDIATION_MODE_REBOOT_VM",
- ]
- """Remediation mode specifies how the remediation should be performed.
-
- - `REMEDIATION_MODE_VM_ONLY`: Deletes the VM and provisions a new one on any
- available host.
- - `REMEDIATION_MODE_HOST_AWARE`: Cordons the host, deletes the VM, and
- provisions a new one on a different host.
- """
-
- state: Literal["PENDING_APPROVAL", "PENDING", "RUNNING", "SUCCEEDED", "FAILED", "CANCELLED", "AUTO_RESOLVED"]
- """RemediationState represents the lifecycle state of a remediation.
-
- - `PENDING_APPROVAL`: Awaiting approval before processing can begin.
- - `PENDING`: Approved and queued for processing.
- - `RUNNING`: Actively being processed.
- - `SUCCEEDED`: Successfully completed.
- - `FAILED`: Failed with an error.
- - `CANCELLED`: Cancelled by user or system.
- - `AUTO_RESOLVED`: The underlying issue was automatically resolved before
- processing.
- """
-
- trigger: Literal["REMEDIATION_TRIGGER_MANUAL", "REMEDIATION_TRIGGER_AUTOMATED"]
- """RemediationTrigger specifies how the remediation was triggered.
-
- - `REMEDIATION_TRIGGER_MANUAL`: A user-initiated remediation (either via web UI
- or API call).
- - `REMEDIATION_TRIGGER_AUTOMATED`: A system-initiated remediation that requires
- approval.
- """
-
- active_health_check_run_id: Optional[str] = None
- """Active health check run ID (UUID) that triggered this remediation."""
-
- create_time: Optional[datetime] = None
- """When the remediation was created."""
-
- end_time: Optional[datetime] = None
- """When the remediation completed."""
-
- error_message: Optional[str] = None
- """Error message if the remediation failed."""
-
- passive_health_check_event_id: Optional[str] = None
- """Passive health check event ID that triggered this remediation."""
-
- reason: Optional[str] = None
- """User-provided reason for the remediation."""
-
- requested_by: Optional[str] = None
- """Who requested the remediation."""
-
- review_comment: Optional[str] = None
- """Review comment."""
-
- review_time: Optional[datetime] = None
- """When the remediation was reviewed."""
-
- reviewed_by: Optional[str] = None
- """Who reviewed the remediation."""
-
- start_time: Optional[datetime] = None
- """When processing started."""
-
- update_time: Optional[datetime] = None
- """When the remediation was last updated."""
diff --git a/src/together/types/beta/clusters/remediation_list_response.py b/src/together/types/beta/clusters/remediation_list_response.py
index aaf15a9fb..174674960 100644
--- a/src/together/types/beta/clusters/remediation_list_response.py
+++ b/src/together/types/beta/clusters/remediation_list_response.py
@@ -1,97 +1,11 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-from typing import List, Optional
-from datetime import datetime
-from typing_extensions import Literal
+from typing import List
from ...._models import BaseModel
+from .remediation import Remediation
-__all__ = ["RemediationListResponse", "Remediation"]
-
-
-class Remediation(BaseModel):
- """
- Remediation represents a node remediation request for an instance.
- An instance can have multiple remediations over time (e.g., failed attempts followed by retries).
- """
-
- id: str
-
- cluster_id: str
-
- instance_id: str
-
- mode: Literal[
- "REMEDIATION_MODE_VM_ONLY",
- "REMEDIATION_MODE_HOST_AWARE",
- "REMEDIATION_MODE_EVICT_WITHOUT_REPLACEMENT",
- "REMEDIATION_MODE_REBOOT_VM",
- ]
- """Remediation mode specifies how the remediation should be performed.
-
- - `REMEDIATION_MODE_VM_ONLY`: Deletes the VM and provisions a new one on any
- available host.
- - `REMEDIATION_MODE_HOST_AWARE`: Cordons the host, deletes the VM, and
- provisions a new one on a different host.
- """
-
- state: Literal["PENDING_APPROVAL", "PENDING", "RUNNING", "SUCCEEDED", "FAILED", "CANCELLED", "AUTO_RESOLVED"]
- """RemediationState represents the lifecycle state of a remediation.
-
- - `PENDING_APPROVAL`: Awaiting approval before processing can begin.
- - `PENDING`: Approved and queued for processing.
- - `RUNNING`: Actively being processed.
- - `SUCCEEDED`: Successfully completed.
- - `FAILED`: Failed with an error.
- - `CANCELLED`: Cancelled by user or system.
- - `AUTO_RESOLVED`: The underlying issue was automatically resolved before
- processing.
- """
-
- trigger: Literal["REMEDIATION_TRIGGER_MANUAL", "REMEDIATION_TRIGGER_AUTOMATED"]
- """RemediationTrigger specifies how the remediation was triggered.
-
- - `REMEDIATION_TRIGGER_MANUAL`: A user-initiated remediation (either via web UI
- or API call).
- - `REMEDIATION_TRIGGER_AUTOMATED`: A system-initiated remediation that requires
- approval.
- """
-
- active_health_check_run_id: Optional[str] = None
- """Active health check run ID (UUID) that triggered this remediation."""
-
- create_time: Optional[datetime] = None
- """When the remediation was created."""
-
- end_time: Optional[datetime] = None
- """When the remediation completed."""
-
- error_message: Optional[str] = None
- """Error message if the remediation failed."""
-
- passive_health_check_event_id: Optional[str] = None
- """Passive health check event ID that triggered this remediation."""
-
- reason: Optional[str] = None
- """User-provided reason for the remediation."""
-
- requested_by: Optional[str] = None
- """Who requested the remediation."""
-
- review_comment: Optional[str] = None
- """Review comment."""
-
- review_time: Optional[datetime] = None
- """When the remediation was reviewed."""
-
- reviewed_by: Optional[str] = None
- """Who reviewed the remediation."""
-
- start_time: Optional[datetime] = None
- """When processing started."""
-
- update_time: Optional[datetime] = None
- """When the remediation was last updated."""
+__all__ = ["RemediationListResponse"]
class RemediationListResponse(BaseModel):
diff --git a/src/together/types/beta/clusters/remediation_reject_response.py b/src/together/types/beta/clusters/remediation_reject_response.py
deleted file mode 100644
index e90306bb6..000000000
--- a/src/together/types/beta/clusters/remediation_reject_response.py
+++ /dev/null
@@ -1,94 +0,0 @@
-# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-
-from typing import Optional
-from datetime import datetime
-from typing_extensions import Literal
-
-from ...._models import BaseModel
-
-__all__ = ["RemediationRejectResponse"]
-
-
-class RemediationRejectResponse(BaseModel):
- """
- Remediation represents a node remediation request for an instance.
- An instance can have multiple remediations over time (e.g., failed attempts followed by retries).
- """
-
- id: str
-
- cluster_id: str
-
- instance_id: str
-
- mode: Literal[
- "REMEDIATION_MODE_VM_ONLY",
- "REMEDIATION_MODE_HOST_AWARE",
- "REMEDIATION_MODE_EVICT_WITHOUT_REPLACEMENT",
- "REMEDIATION_MODE_REBOOT_VM",
- ]
- """Remediation mode specifies how the remediation should be performed.
-
- - `REMEDIATION_MODE_VM_ONLY`: Deletes the VM and provisions a new one on any
- available host.
- - `REMEDIATION_MODE_HOST_AWARE`: Cordons the host, deletes the VM, and
- provisions a new one on a different host.
- """
-
- state: Literal["PENDING_APPROVAL", "PENDING", "RUNNING", "SUCCEEDED", "FAILED", "CANCELLED", "AUTO_RESOLVED"]
- """RemediationState represents the lifecycle state of a remediation.
-
- - `PENDING_APPROVAL`: Awaiting approval before processing can begin.
- - `PENDING`: Approved and queued for processing.
- - `RUNNING`: Actively being processed.
- - `SUCCEEDED`: Successfully completed.
- - `FAILED`: Failed with an error.
- - `CANCELLED`: Cancelled by user or system.
- - `AUTO_RESOLVED`: The underlying issue was automatically resolved before
- processing.
- """
-
- trigger: Literal["REMEDIATION_TRIGGER_MANUAL", "REMEDIATION_TRIGGER_AUTOMATED"]
- """RemediationTrigger specifies how the remediation was triggered.
-
- - `REMEDIATION_TRIGGER_MANUAL`: A user-initiated remediation (either via web UI
- or API call).
- - `REMEDIATION_TRIGGER_AUTOMATED`: A system-initiated remediation that requires
- approval.
- """
-
- active_health_check_run_id: Optional[str] = None
- """Active health check run ID (UUID) that triggered this remediation."""
-
- create_time: Optional[datetime] = None
- """When the remediation was created."""
-
- end_time: Optional[datetime] = None
- """When the remediation completed."""
-
- error_message: Optional[str] = None
- """Error message if the remediation failed."""
-
- passive_health_check_event_id: Optional[str] = None
- """Passive health check event ID that triggered this remediation."""
-
- reason: Optional[str] = None
- """User-provided reason for the remediation."""
-
- requested_by: Optional[str] = None
- """Who requested the remediation."""
-
- review_comment: Optional[str] = None
- """Review comment."""
-
- review_time: Optional[datetime] = None
- """When the remediation was reviewed."""
-
- reviewed_by: Optional[str] = None
- """Who reviewed the remediation."""
-
- start_time: Optional[datetime] = None
- """When processing started."""
-
- update_time: Optional[datetime] = None
- """When the remediation was last updated."""
diff --git a/src/together/types/beta/clusters/remediation_retrieve_response.py b/src/together/types/beta/clusters/remediation_retrieve_response.py
deleted file mode 100644
index ffb6bd3b4..000000000
--- a/src/together/types/beta/clusters/remediation_retrieve_response.py
+++ /dev/null
@@ -1,94 +0,0 @@
-# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-
-from typing import Optional
-from datetime import datetime
-from typing_extensions import Literal
-
-from ...._models import BaseModel
-
-__all__ = ["RemediationRetrieveResponse"]
-
-
-class RemediationRetrieveResponse(BaseModel):
- """
- Remediation represents a node remediation request for an instance.
- An instance can have multiple remediations over time (e.g., failed attempts followed by retries).
- """
-
- id: str
-
- cluster_id: str
-
- instance_id: str
-
- mode: Literal[
- "REMEDIATION_MODE_VM_ONLY",
- "REMEDIATION_MODE_HOST_AWARE",
- "REMEDIATION_MODE_EVICT_WITHOUT_REPLACEMENT",
- "REMEDIATION_MODE_REBOOT_VM",
- ]
- """Remediation mode specifies how the remediation should be performed.
-
- - `REMEDIATION_MODE_VM_ONLY`: Deletes the VM and provisions a new one on any
- available host.
- - `REMEDIATION_MODE_HOST_AWARE`: Cordons the host, deletes the VM, and
- provisions a new one on a different host.
- """
-
- state: Literal["PENDING_APPROVAL", "PENDING", "RUNNING", "SUCCEEDED", "FAILED", "CANCELLED", "AUTO_RESOLVED"]
- """RemediationState represents the lifecycle state of a remediation.
-
- - `PENDING_APPROVAL`: Awaiting approval before processing can begin.
- - `PENDING`: Approved and queued for processing.
- - `RUNNING`: Actively being processed.
- - `SUCCEEDED`: Successfully completed.
- - `FAILED`: Failed with an error.
- - `CANCELLED`: Cancelled by user or system.
- - `AUTO_RESOLVED`: The underlying issue was automatically resolved before
- processing.
- """
-
- trigger: Literal["REMEDIATION_TRIGGER_MANUAL", "REMEDIATION_TRIGGER_AUTOMATED"]
- """RemediationTrigger specifies how the remediation was triggered.
-
- - `REMEDIATION_TRIGGER_MANUAL`: A user-initiated remediation (either via web UI
- or API call).
- - `REMEDIATION_TRIGGER_AUTOMATED`: A system-initiated remediation that requires
- approval.
- """
-
- active_health_check_run_id: Optional[str] = None
- """Active health check run ID (UUID) that triggered this remediation."""
-
- create_time: Optional[datetime] = None
- """When the remediation was created."""
-
- end_time: Optional[datetime] = None
- """When the remediation completed."""
-
- error_message: Optional[str] = None
- """Error message if the remediation failed."""
-
- passive_health_check_event_id: Optional[str] = None
- """Passive health check event ID that triggered this remediation."""
-
- reason: Optional[str] = None
- """User-provided reason for the remediation."""
-
- requested_by: Optional[str] = None
- """Who requested the remediation."""
-
- review_comment: Optional[str] = None
- """Review comment."""
-
- review_time: Optional[datetime] = None
- """When the remediation was reviewed."""
-
- reviewed_by: Optional[str] = None
- """Who reviewed the remediation."""
-
- start_time: Optional[datetime] = None
- """When processing started."""
-
- update_time: Optional[datetime] = None
- """When the remediation was last updated."""
diff --git a/tests/api_resources/beta/clusters/test_remediations.py b/tests/api_resources/beta/clusters/test_remediations.py
index 7fb96a882..7c50553d5 100644
--- a/tests/api_resources/beta/clusters/test_remediations.py
+++ b/tests/api_resources/beta/clusters/test_remediations.py
@@ -10,12 +10,8 @@
from together import Together, AsyncTogether
from tests.utils import assert_matches_type
from together.types.beta.clusters import (
+ Remediation,
RemediationListResponse,
- RemediationCancelResponse,
- RemediationCreateResponse,
- RemediationRejectResponse,
- RemediationApproveResponse,
- RemediationRetrieveResponse,
)
base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
@@ -31,7 +27,7 @@ def test_method_create(self, client: Together) -> None:
cluster_id="cluster_id",
mode="REMEDIATION_MODE_VM_ONLY",
)
- assert_matches_type(RemediationCreateResponse, remediation, path=["response"])
+ assert_matches_type(Remediation, remediation, path=["response"])
@parametrize
def test_method_create_with_all_params(self, client: Together) -> None:
@@ -42,7 +38,7 @@ def test_method_create_with_all_params(self, client: Together) -> None:
remediation_id="remediation_id",
reason="reason",
)
- assert_matches_type(RemediationCreateResponse, remediation, path=["response"])
+ assert_matches_type(Remediation, remediation, path=["response"])
@parametrize
def test_raw_response_create(self, client: Together) -> None:
@@ -55,7 +51,7 @@ def test_raw_response_create(self, client: Together) -> None:
assert response.is_closed is True
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
remediation = response.parse()
- assert_matches_type(RemediationCreateResponse, remediation, path=["response"])
+ assert_matches_type(Remediation, remediation, path=["response"])
@parametrize
def test_streaming_response_create(self, client: Together) -> None:
@@ -68,7 +64,7 @@ def test_streaming_response_create(self, client: Together) -> None:
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
remediation = response.parse()
- assert_matches_type(RemediationCreateResponse, remediation, path=["response"])
+ assert_matches_type(Remediation, remediation, path=["response"])
assert cast(Any, response.is_closed) is True
@@ -95,7 +91,7 @@ def test_method_retrieve(self, client: Together) -> None:
cluster_id="cluster_id",
instance_id="instance_id",
)
- assert_matches_type(RemediationRetrieveResponse, remediation, path=["response"])
+ assert_matches_type(Remediation, remediation, path=["response"])
@parametrize
def test_raw_response_retrieve(self, client: Together) -> None:
@@ -108,7 +104,7 @@ def test_raw_response_retrieve(self, client: Together) -> None:
assert response.is_closed is True
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
remediation = response.parse()
- assert_matches_type(RemediationRetrieveResponse, remediation, path=["response"])
+ assert_matches_type(Remediation, remediation, path=["response"])
@parametrize
def test_streaming_response_retrieve(self, client: Together) -> None:
@@ -121,7 +117,7 @@ def test_streaming_response_retrieve(self, client: Together) -> None:
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
remediation = response.parse()
- assert_matches_type(RemediationRetrieveResponse, remediation, path=["response"])
+ assert_matches_type(Remediation, remediation, path=["response"])
assert cast(Any, response.is_closed) is True
@@ -216,7 +212,7 @@ def test_method_approve(self, client: Together) -> None:
cluster_id="cluster_id",
instance_id="instance_id",
)
- assert_matches_type(RemediationApproveResponse, remediation, path=["response"])
+ assert_matches_type(Remediation, remediation, path=["response"])
@parametrize
def test_method_approve_with_all_params(self, client: Together) -> None:
@@ -226,7 +222,7 @@ def test_method_approve_with_all_params(self, client: Together) -> None:
instance_id="instance_id",
comment="comment",
)
- assert_matches_type(RemediationApproveResponse, remediation, path=["response"])
+ assert_matches_type(Remediation, remediation, path=["response"])
@parametrize
def test_raw_response_approve(self, client: Together) -> None:
@@ -239,7 +235,7 @@ def test_raw_response_approve(self, client: Together) -> None:
assert response.is_closed is True
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
remediation = response.parse()
- assert_matches_type(RemediationApproveResponse, remediation, path=["response"])
+ assert_matches_type(Remediation, remediation, path=["response"])
@parametrize
def test_streaming_response_approve(self, client: Together) -> None:
@@ -252,7 +248,7 @@ def test_streaming_response_approve(self, client: Together) -> None:
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
remediation = response.parse()
- assert_matches_type(RemediationApproveResponse, remediation, path=["response"])
+ assert_matches_type(Remediation, remediation, path=["response"])
assert cast(Any, response.is_closed) is True
@@ -286,7 +282,7 @@ def test_method_cancel(self, client: Together) -> None:
cluster_id="cluster_id",
instance_id="instance_id",
)
- assert_matches_type(RemediationCancelResponse, remediation, path=["response"])
+ assert_matches_type(Remediation, remediation, path=["response"])
@parametrize
def test_raw_response_cancel(self, client: Together) -> None:
@@ -299,7 +295,7 @@ def test_raw_response_cancel(self, client: Together) -> None:
assert response.is_closed is True
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
remediation = response.parse()
- assert_matches_type(RemediationCancelResponse, remediation, path=["response"])
+ assert_matches_type(Remediation, remediation, path=["response"])
@parametrize
def test_streaming_response_cancel(self, client: Together) -> None:
@@ -312,7 +308,7 @@ def test_streaming_response_cancel(self, client: Together) -> None:
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
remediation = response.parse()
- assert_matches_type(RemediationCancelResponse, remediation, path=["response"])
+ assert_matches_type(Remediation, remediation, path=["response"])
assert cast(Any, response.is_closed) is True
@@ -346,7 +342,7 @@ def test_method_reject(self, client: Together) -> None:
cluster_id="cluster_id",
instance_id="instance_id",
)
- assert_matches_type(RemediationRejectResponse, remediation, path=["response"])
+ assert_matches_type(Remediation, remediation, path=["response"])
@parametrize
def test_method_reject_with_all_params(self, client: Together) -> None:
@@ -356,7 +352,7 @@ def test_method_reject_with_all_params(self, client: Together) -> None:
instance_id="instance_id",
comment="comment",
)
- assert_matches_type(RemediationRejectResponse, remediation, path=["response"])
+ assert_matches_type(Remediation, remediation, path=["response"])
@parametrize
def test_raw_response_reject(self, client: Together) -> None:
@@ -369,7 +365,7 @@ def test_raw_response_reject(self, client: Together) -> None:
assert response.is_closed is True
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
remediation = response.parse()
- assert_matches_type(RemediationRejectResponse, remediation, path=["response"])
+ assert_matches_type(Remediation, remediation, path=["response"])
@parametrize
def test_streaming_response_reject(self, client: Together) -> None:
@@ -382,7 +378,7 @@ def test_streaming_response_reject(self, client: Together) -> None:
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
remediation = response.parse()
- assert_matches_type(RemediationRejectResponse, remediation, path=["response"])
+ assert_matches_type(Remediation, remediation, path=["response"])
assert cast(Any, response.is_closed) is True
@@ -422,7 +418,7 @@ async def test_method_create(self, async_client: AsyncTogether) -> None:
cluster_id="cluster_id",
mode="REMEDIATION_MODE_VM_ONLY",
)
- assert_matches_type(RemediationCreateResponse, remediation, path=["response"])
+ assert_matches_type(Remediation, remediation, path=["response"])
@parametrize
async def test_method_create_with_all_params(self, async_client: AsyncTogether) -> None:
@@ -433,7 +429,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncTogether)
remediation_id="remediation_id",
reason="reason",
)
- assert_matches_type(RemediationCreateResponse, remediation, path=["response"])
+ assert_matches_type(Remediation, remediation, path=["response"])
@parametrize
async def test_raw_response_create(self, async_client: AsyncTogether) -> None:
@@ -446,7 +442,7 @@ async def test_raw_response_create(self, async_client: AsyncTogether) -> None:
assert response.is_closed is True
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
remediation = await response.parse()
- assert_matches_type(RemediationCreateResponse, remediation, path=["response"])
+ assert_matches_type(Remediation, remediation, path=["response"])
@parametrize
async def test_streaming_response_create(self, async_client: AsyncTogether) -> None:
@@ -459,7 +455,7 @@ async def test_streaming_response_create(self, async_client: AsyncTogether) -> N
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
remediation = await response.parse()
- assert_matches_type(RemediationCreateResponse, remediation, path=["response"])
+ assert_matches_type(Remediation, remediation, path=["response"])
assert cast(Any, response.is_closed) is True
@@ -486,7 +482,7 @@ async def test_method_retrieve(self, async_client: AsyncTogether) -> None:
cluster_id="cluster_id",
instance_id="instance_id",
)
- assert_matches_type(RemediationRetrieveResponse, remediation, path=["response"])
+ assert_matches_type(Remediation, remediation, path=["response"])
@parametrize
async def test_raw_response_retrieve(self, async_client: AsyncTogether) -> None:
@@ -499,7 +495,7 @@ async def test_raw_response_retrieve(self, async_client: AsyncTogether) -> None:
assert response.is_closed is True
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
remediation = await response.parse()
- assert_matches_type(RemediationRetrieveResponse, remediation, path=["response"])
+ assert_matches_type(Remediation, remediation, path=["response"])
@parametrize
async def test_streaming_response_retrieve(self, async_client: AsyncTogether) -> None:
@@ -512,7 +508,7 @@ async def test_streaming_response_retrieve(self, async_client: AsyncTogether) ->
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
remediation = await response.parse()
- assert_matches_type(RemediationRetrieveResponse, remediation, path=["response"])
+ assert_matches_type(Remediation, remediation, path=["response"])
assert cast(Any, response.is_closed) is True
@@ -607,7 +603,7 @@ async def test_method_approve(self, async_client: AsyncTogether) -> None:
cluster_id="cluster_id",
instance_id="instance_id",
)
- assert_matches_type(RemediationApproveResponse, remediation, path=["response"])
+ assert_matches_type(Remediation, remediation, path=["response"])
@parametrize
async def test_method_approve_with_all_params(self, async_client: AsyncTogether) -> None:
@@ -617,7 +613,7 @@ async def test_method_approve_with_all_params(self, async_client: AsyncTogether)
instance_id="instance_id",
comment="comment",
)
- assert_matches_type(RemediationApproveResponse, remediation, path=["response"])
+ assert_matches_type(Remediation, remediation, path=["response"])
@parametrize
async def test_raw_response_approve(self, async_client: AsyncTogether) -> None:
@@ -630,7 +626,7 @@ async def test_raw_response_approve(self, async_client: AsyncTogether) -> None:
assert response.is_closed is True
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
remediation = await response.parse()
- assert_matches_type(RemediationApproveResponse, remediation, path=["response"])
+ assert_matches_type(Remediation, remediation, path=["response"])
@parametrize
async def test_streaming_response_approve(self, async_client: AsyncTogether) -> None:
@@ -643,7 +639,7 @@ async def test_streaming_response_approve(self, async_client: AsyncTogether) ->
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
remediation = await response.parse()
- assert_matches_type(RemediationApproveResponse, remediation, path=["response"])
+ assert_matches_type(Remediation, remediation, path=["response"])
assert cast(Any, response.is_closed) is True
@@ -677,7 +673,7 @@ async def test_method_cancel(self, async_client: AsyncTogether) -> None:
cluster_id="cluster_id",
instance_id="instance_id",
)
- assert_matches_type(RemediationCancelResponse, remediation, path=["response"])
+ assert_matches_type(Remediation, remediation, path=["response"])
@parametrize
async def test_raw_response_cancel(self, async_client: AsyncTogether) -> None:
@@ -690,7 +686,7 @@ async def test_raw_response_cancel(self, async_client: AsyncTogether) -> None:
assert response.is_closed is True
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
remediation = await response.parse()
- assert_matches_type(RemediationCancelResponse, remediation, path=["response"])
+ assert_matches_type(Remediation, remediation, path=["response"])
@parametrize
async def test_streaming_response_cancel(self, async_client: AsyncTogether) -> None:
@@ -703,7 +699,7 @@ async def test_streaming_response_cancel(self, async_client: AsyncTogether) -> N
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
remediation = await response.parse()
- assert_matches_type(RemediationCancelResponse, remediation, path=["response"])
+ assert_matches_type(Remediation, remediation, path=["response"])
assert cast(Any, response.is_closed) is True
@@ -737,7 +733,7 @@ async def test_method_reject(self, async_client: AsyncTogether) -> None:
cluster_id="cluster_id",
instance_id="instance_id",
)
- assert_matches_type(RemediationRejectResponse, remediation, path=["response"])
+ assert_matches_type(Remediation, remediation, path=["response"])
@parametrize
async def test_method_reject_with_all_params(self, async_client: AsyncTogether) -> None:
@@ -747,7 +743,7 @@ async def test_method_reject_with_all_params(self, async_client: AsyncTogether)
instance_id="instance_id",
comment="comment",
)
- assert_matches_type(RemediationRejectResponse, remediation, path=["response"])
+ assert_matches_type(Remediation, remediation, path=["response"])
@parametrize
async def test_raw_response_reject(self, async_client: AsyncTogether) -> None:
@@ -760,7 +756,7 @@ async def test_raw_response_reject(self, async_client: AsyncTogether) -> None:
assert response.is_closed is True
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
remediation = await response.parse()
- assert_matches_type(RemediationRejectResponse, remediation, path=["response"])
+ assert_matches_type(Remediation, remediation, path=["response"])
@parametrize
async def test_streaming_response_reject(self, async_client: AsyncTogether) -> None:
@@ -773,7 +769,7 @@ async def test_streaming_response_reject(self, async_client: AsyncTogether) -> N
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
remediation = await response.parse()
- assert_matches_type(RemediationRejectResponse, remediation, path=["response"])
+ assert_matches_type(Remediation, remediation, path=["response"])
assert cast(Any, response.is_closed) is True
From c5f5a6d90f0d87d9005fada2ff12d246a0ab5aa7 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 15 May 2026 18:29:34 +0000
Subject: [PATCH 11/14] codegen metadata
---
.stats.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.stats.yml b/.stats.yml
index 37b2bba11..cfa550f54 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 81
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/togetherai/togetherai-c8200db997f998a51b2b469b69fcc3a24032e50b45aaaf340c2c176473393adc.yml
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/togetherai/togetherai-3ae47a93fc03893f34b2e442ab8194c1f4224d9d7fe2d02b6342dbe03862f1e7.yml
openapi_spec_hash: 2544f6840cf7ad23eae3fe8bffed4c4b
config_hash: b35d5968fb07cce1c1be735f874898b1
From 5047a5c530460b0e8493b985f1bbff18cb9c2025 Mon Sep 17 00:00:00 2001
From: Cursor Agent
Date: Fri, 15 May 2026 20:08:39 +0000
Subject: [PATCH 12/14] Add beta cluster remediation CLI commands
Co-authored-by: Blaine Kasten
---
src/together/lib/cli/__init__.py | 33 ++++
.../api/beta/clusters/remediations/_util.py | 91 +++++++++++
.../api/beta/clusters/remediations/approve.py | 36 +++++
.../api/beta/clusters/remediations/cancel.py | 30 ++++
.../api/beta/clusters/remediations/create.py | 48 ++++++
.../api/beta/clusters/remediations/list.py | 65 ++++++++
.../api/beta/clusters/remediations/reject.py | 36 +++++
src/together/lib/cli/utils/_help_examples.py | 20 +++
tests/cli/test_beta_clusters.py | 146 ++++++++++++++++++
9 files changed, 505 insertions(+)
create mode 100644 src/together/lib/cli/api/beta/clusters/remediations/_util.py
create mode 100644 src/together/lib/cli/api/beta/clusters/remediations/approve.py
create mode 100644 src/together/lib/cli/api/beta/clusters/remediations/cancel.py
create mode 100644 src/together/lib/cli/api/beta/clusters/remediations/create.py
create mode 100644 src/together/lib/cli/api/beta/clusters/remediations/list.py
create mode 100644 src/together/lib/cli/api/beta/clusters/remediations/reject.py
diff --git a/src/together/lib/cli/__init__.py b/src/together/lib/cli/__init__.py
index 445871f45..9a61f0cf8 100644
--- a/src/together/lib/cli/__init__.py
+++ b/src/together/lib/cli/__init__.py
@@ -61,6 +61,7 @@
FINE_TUNING_DOWNLOAD_HELP_EXAMPLES,
BETA_CLUSTERS_STORAGE_HELP_EXAMPLES,
FILES_RETRIEVE_CONTENT_HELP_EXAMPLES,
+ BETA_CLUSTERS_REMEDIATIONS_HELP_EXAMPLES,
BETA_CLUSTERS_STORAGE_CREATE_HELP_EXAMPLES,
BETA_CLUSTERS_STORAGE_UPDATE_HELP_EXAMPLES,
BETA_CLUSTERS_GET_CREDENTIALS_HELP_EXAMPLES,
@@ -486,6 +487,38 @@ async def run_command() -> None:
)
storage_app.command((f"{_CLI}.beta.clusters.storage.delete:delete"), help="Delete a storage volume", alias="-d")
+### Clusters > Remediations API commands
+remediations_app = clusters_app.command(
+ App(
+ name="remediations",
+ help="Manage node remediations",
+ group="Subcommands",
+ help_epilogue=BETA_CLUSTERS_REMEDIATIONS_HELP_EXAMPLES,
+ )
+)
+remediations_app.command(
+ (f"{_CLI}.beta.clusters.remediations.create:create"),
+ alias="-c",
+ help="Create a node remediation",
+)
+remediations_app.command(
+ (f"{_CLI}.beta.clusters.remediations.list:list"),
+ alias="ls",
+ help="List node remediations",
+)
+remediations_app.command(
+ (f"{_CLI}.beta.clusters.remediations.approve:approve"),
+ help="Approve a pending remediation",
+)
+remediations_app.command(
+ (f"{_CLI}.beta.clusters.remediations.cancel:cancel"),
+ help="Cancel a pending remediation",
+)
+remediations_app.command(
+ (f"{_CLI}.beta.clusters.remediations.reject:reject"),
+ help="Reject a pending remediation",
+)
+
### Jig commands
jig_app = beta_app.command(
App(name="jig", help="Build, deploy, and manage custom containers", help_epilogue=JIG_HELP_EXAMPLES)
diff --git a/src/together/lib/cli/api/beta/clusters/remediations/_util.py b/src/together/lib/cli/api/beta/clusters/remediations/_util.py
new file mode 100644
index 000000000..946cdccda
--- /dev/null
+++ b/src/together/lib/cli/api/beta/clusters/remediations/_util.py
@@ -0,0 +1,91 @@
+from __future__ import annotations
+
+import sys
+from typing import List, Literal, TypeVar, Optional, cast, get_args
+from datetime import datetime
+
+from together import omit
+from together._types import Omit
+from together.lib.cli.utils.config import CLIConfigParameter
+from together.lib.cli.utils._console import console
+from together.lib.cli.components.list import ListTable
+from together.types.beta.clusters.remediation import Remediation
+
+EMPTY_MESSAGE = "No remediations found for this cluster."
+
+T = TypeVar("T")
+RemediationState = Literal[
+ "PENDING_APPROVAL", "PENDING", "RUNNING", "SUCCEEDED", "FAILED", "CANCELLED", "AUTO_RESOLVED"
+]
+_REMEDIATION_STATES = set(get_args(RemediationState))
+
+
+def omit_if_none(value: Optional[T]) -> T | Omit:
+ return omit if value is None else value
+
+
+def parse_states(state: Optional[str]) -> List[RemediationState] | Omit:
+ if state is None:
+ return omit
+ states = [part.strip() for part in state.split(",") if part.strip()]
+ if not states:
+ return omit
+
+ invalid_states = sorted(set(states) - _REMEDIATION_STATES)
+ if invalid_states:
+ console.print(f"[red]Error:[/red] Invalid remediation state: {', '.join(invalid_states)}")
+ sys.exit(1)
+
+ return cast(List[RemediationState], states)
+
+
+def format_time(value: Optional[datetime]) -> str:
+ if value is None:
+ return "-"
+ return value.isoformat()
+
+
+def print_remediations(remediations: List[Remediation]) -> None:
+ table = ListTable(title="Cluster Remediations", empty_message=EMPTY_MESSAGE)
+ table.add_primary_column("ID")
+ table.add_column("Instance")
+ table.add_column("State")
+ table.add_column("Mode")
+ table.add_column("Trigger")
+ table.add_column("Created")
+
+ for remediation in remediations:
+ table.add_row(
+ remediation.id,
+ remediation.instance_id,
+ remediation.state,
+ remediation.mode.replace("REMEDIATION_MODE_", ""),
+ remediation.trigger.replace("REMEDIATION_TRIGGER_", ""),
+ format_time(remediation.create_time),
+ )
+
+ console.print(table)
+
+
+async def resolve_remediation(config: CLIConfigParameter, remediation_id: str) -> Remediation:
+ clusters = await config.client.beta.clusters.list()
+
+ for cluster in clusters.clusters:
+ page_token: str | Omit = omit
+ while True:
+ response = await config.client.beta.clusters.remediations.list(
+ "-",
+ cluster_id=cluster.cluster_id,
+ page_size=100,
+ page_token=page_token,
+ )
+ for remediation in response.remediations:
+ if remediation.id == remediation_id:
+ return remediation
+
+ if not response.has_next or not response.next_page_token:
+ break
+ page_token = response.next_page_token
+
+ console.print(f"[red]Error:[/red] Remediation not found: {remediation_id}")
+ sys.exit(1)
diff --git a/src/together/lib/cli/api/beta/clusters/remediations/approve.py b/src/together/lib/cli/api/beta/clusters/remediations/approve.py
new file mode 100644
index 000000000..46428c161
--- /dev/null
+++ b/src/together/lib/cli/api/beta/clusters/remediations/approve.py
@@ -0,0 +1,36 @@
+from __future__ import annotations
+
+from typing import Optional, Annotated
+
+from cyclopts import Parameter
+
+from together._utils._json import openapi_dumps
+from together.lib.cli.utils.config import CLIConfigParameter
+from together.lib.cli.utils._console import console
+from together.lib.cli.components.loader import show_loading_status
+from together.lib.cli.api.beta.clusters.remediations._util import omit_if_none, resolve_remediation
+
+
+async def approve(
+ remediation_id: str,
+ comment: Annotated[Optional[str], Parameter(help="Comment explaining the approval")] = None,
+ *,
+ config: CLIConfigParameter,
+) -> None:
+ """Approve a pending remediation."""
+ remediation = await show_loading_status("Finding remediation...", resolve_remediation(config, remediation_id))
+ response = await show_loading_status(
+ "Approving remediation...",
+ config.client.beta.clusters.remediations.approve(
+ remediation_id,
+ cluster_id=remediation.cluster_id,
+ instance_id=remediation.instance_id,
+ comment=omit_if_none(comment),
+ ),
+ )
+
+ if config.json:
+ console.print_json(openapi_dumps(response).decode("utf-8"))
+ return
+
+ console.print(f"[blue]Remediation approved.[/blue] ({response.id})")
diff --git a/src/together/lib/cli/api/beta/clusters/remediations/cancel.py b/src/together/lib/cli/api/beta/clusters/remediations/cancel.py
new file mode 100644
index 000000000..fcd34f542
--- /dev/null
+++ b/src/together/lib/cli/api/beta/clusters/remediations/cancel.py
@@ -0,0 +1,30 @@
+from __future__ import annotations
+
+from together._utils._json import openapi_dumps
+from together.lib.cli.utils.config import CLIConfigParameter
+from together.lib.cli.utils._console import console
+from together.lib.cli.components.loader import show_loading_status
+from together.lib.cli.api.beta.clusters.remediations._util import resolve_remediation
+
+
+async def cancel(
+ remediation_id: str,
+ *,
+ config: CLIConfigParameter,
+) -> None:
+ """Cancel a pending remediation."""
+ remediation = await show_loading_status("Finding remediation...", resolve_remediation(config, remediation_id))
+ response = await show_loading_status(
+ "Cancelling remediation...",
+ config.client.beta.clusters.remediations.cancel(
+ remediation_id,
+ cluster_id=remediation.cluster_id,
+ instance_id=remediation.instance_id,
+ ),
+ )
+
+ if config.json:
+ console.print_json(openapi_dumps(response).decode("utf-8"))
+ return
+
+ console.print(f"[blue]Remediation cancelled.[/blue] ({response.id})")
diff --git a/src/together/lib/cli/api/beta/clusters/remediations/create.py b/src/together/lib/cli/api/beta/clusters/remediations/create.py
new file mode 100644
index 000000000..3ef21e7c4
--- /dev/null
+++ b/src/together/lib/cli/api/beta/clusters/remediations/create.py
@@ -0,0 +1,48 @@
+from __future__ import annotations
+
+from typing import Literal, Optional, Annotated
+
+from cyclopts import Parameter
+
+from together._utils._json import openapi_dumps
+from together.lib.cli.utils.config import CLIConfigParameter
+from together.lib.cli.utils._console import console
+from together.lib.cli.components.loader import show_loading_status
+from together.lib.cli.api.beta.clusters.remediations._util import omit_if_none
+
+RemediationModeParameter = Annotated[
+ Literal[
+ "REMEDIATION_MODE_VM_ONLY",
+ "REMEDIATION_MODE_HOST_AWARE",
+ "REMEDIATION_MODE_EVICT_WITHOUT_REPLACEMENT",
+ "REMEDIATION_MODE_REBOOT_VM",
+ ],
+ Parameter(help="How the remediation should be performed"),
+]
+
+
+async def create(
+ cluster_id: str,
+ instance_id: str,
+ *,
+ mode: RemediationModeParameter,
+ remediation_id: Annotated[Optional[str], Parameter(help="Client-specified ID for idempotency")] = None,
+ reason: Annotated[Optional[str], Parameter(help="Reason for the remediation")] = None,
+ config: CLIConfigParameter,
+) -> None:
+ """Create a node remediation for an instance."""
+ request = config.client.beta.clusters.remediations.create(
+ instance_id,
+ cluster_id=cluster_id,
+ mode=mode,
+ remediation_id=omit_if_none(remediation_id),
+ reason=omit_if_none(reason),
+ )
+
+ response = await show_loading_status("Creating remediation...", request)
+ if config.json:
+ console.print_json(openapi_dumps(response).decode("utf-8"))
+ return
+
+ console.print("[blue]Remediation created successfully[/blue]")
+ console.print(f"[primary]Remediation ID:[/primary] {response.id}")
diff --git a/src/together/lib/cli/api/beta/clusters/remediations/list.py b/src/together/lib/cli/api/beta/clusters/remediations/list.py
new file mode 100644
index 000000000..7c0124daf
--- /dev/null
+++ b/src/together/lib/cli/api/beta/clusters/remediations/list.py
@@ -0,0 +1,65 @@
+from __future__ import annotations
+
+from typing import Literal, Optional, Annotated
+
+from cyclopts import Parameter
+
+from together._utils._json import openapi_dumps
+from together.lib.cli.utils.config import CLIConfigParameter
+from together.lib.cli.utils._console import console
+from together.lib.cli.components.loader import show_loading_status
+from together.lib.cli.api.beta.clusters.remediations._util import (
+ omit_if_none,
+ parse_states,
+ print_remediations,
+)
+
+OptionalRemediationModeParameter = Annotated[
+ Optional[
+ Literal[
+ "REMEDIATION_MODE_VM_ONLY",
+ "REMEDIATION_MODE_HOST_AWARE",
+ "REMEDIATION_MODE_EVICT_WITHOUT_REPLACEMENT",
+ "REMEDIATION_MODE_REBOOT_VM",
+ ]
+ ],
+ Parameter(help="Filter by remediation mode"),
+]
+
+
+async def list(
+ cluster_id: str,
+ instance_id: Annotated[Optional[str], Parameter(help="Instance ID to list remediations for")] = None,
+ mode: OptionalRemediationModeParameter = None,
+ state: Annotated[Optional[str], Parameter(help="Comma-separated remediation states to include")] = None,
+ order_by: Annotated[Optional[str], Parameter(help="Order by expression")] = None,
+ page_size: Annotated[Optional[int], Parameter(help="Maximum results to return")] = None,
+ page_token: Annotated[Optional[str], Parameter(help="Pagination token from a previous request")] = None,
+ *,
+ config: CLIConfigParameter,
+) -> None:
+ """List node remediations for a cluster or instance."""
+ response = await show_loading_status(
+ "Loading remediations...",
+ config.client.beta.clusters.remediations.list(
+ instance_id or "-",
+ cluster_id=cluster_id,
+ mode=omit_if_none(mode),
+ state=parse_states(state),
+ order_by=omit_if_none(order_by),
+ page_size=omit_if_none(page_size),
+ page_token=omit_if_none(page_token),
+ ),
+ )
+
+ if config.json:
+ console.print_json(openapi_dumps(response).decode("utf-8"))
+ return
+
+ print_remediations(response.remediations)
+ if response.has_next and response.next_page_token:
+ command = f"tg beta clusters remediations list {cluster_id}"
+ if instance_id:
+ command += f" {instance_id}"
+ console.print("\n[blue dim]To display the next page, run:[/blue dim]")
+ console.print(f" [dim]-[/dim] [white]{command} --page-token {response.next_page_token}[/white]")
diff --git a/src/together/lib/cli/api/beta/clusters/remediations/reject.py b/src/together/lib/cli/api/beta/clusters/remediations/reject.py
new file mode 100644
index 000000000..a5ecb7803
--- /dev/null
+++ b/src/together/lib/cli/api/beta/clusters/remediations/reject.py
@@ -0,0 +1,36 @@
+from __future__ import annotations
+
+from typing import Optional, Annotated
+
+from cyclopts import Parameter
+
+from together._utils._json import openapi_dumps
+from together.lib.cli.utils.config import CLIConfigParameter
+from together.lib.cli.utils._console import console
+from together.lib.cli.components.loader import show_loading_status
+from together.lib.cli.api.beta.clusters.remediations._util import omit_if_none, resolve_remediation
+
+
+async def reject(
+ remediation_id: str,
+ comment: Annotated[Optional[str], Parameter(help="Comment explaining the rejection")] = None,
+ *,
+ config: CLIConfigParameter,
+) -> None:
+ """Reject a pending remediation."""
+ remediation = await show_loading_status("Finding remediation...", resolve_remediation(config, remediation_id))
+ response = await show_loading_status(
+ "Rejecting remediation...",
+ config.client.beta.clusters.remediations.reject(
+ remediation_id,
+ cluster_id=remediation.cluster_id,
+ instance_id=remediation.instance_id,
+ comment=omit_if_none(comment),
+ ),
+ )
+
+ if config.json:
+ console.print_json(openapi_dumps(response).decode("utf-8"))
+ return
+
+ console.print(f"[blue]Remediation rejected.[/blue] ({response.id})")
diff --git a/src/together/lib/cli/utils/_help_examples.py b/src/together/lib/cli/utils/_help_examples.py
index 224eee8a4..c16e05e69 100644
--- a/src/together/lib/cli/utils/_help_examples.py
+++ b/src/together/lib/cli/utils/_help_examples.py
@@ -251,6 +251,10 @@
[dim]-[/dim] Update or delete a cluster:
[primary]tg beta clusters update --num-gpus 16 --cluster-type KUBERNETES[/primary]
[primary]tg beta clusters delete [/primary]
+
+[dim]-[/dim] Manage node remediations:
+ [primary]tg beta clusters remediations list [/primary]
+ [primary]tg beta clusters remediations create --mode REMEDIATION_MODE_VM_ONLY[/primary]
"""
BETA_CLUSTERS_CREATE_HELP_EXAMPLES = """[dim]Examples:[/dim]
@@ -329,6 +333,22 @@
[primary]tg beta clusters storage update --size-tib 4[/primary]
"""
+BETA_CLUSTERS_REMEDIATIONS_HELP_EXAMPLES = """[dim]Examples:[/dim]
+[dim]-[/dim] List all remediations for a cluster:
+ [primary]tg beta clusters remediations list [/primary]
+
+[dim]-[/dim] List remediations for one instance:
+ [primary]tg beta clusters remediations list [/primary]
+
+[dim]-[/dim] Create a remediation:
+ [primary]tg beta clusters remediations create --mode REMEDIATION_MODE_VM_ONLY --reason "node unhealthy"[/primary]
+
+[dim]-[/dim] Review automated remediations:
+ [primary]tg beta clusters remediations approve [/primary]
+ [primary]tg beta clusters remediations reject --comment "already handled"[/primary]
+ [primary]tg beta clusters remediations cancel [/primary]
+"""
+
## Beta > Jig commands
JIG_HELP_EXAMPLES = """[dim]Examples:[/dim]
diff --git a/tests/cli/test_beta_clusters.py b/tests/cli/test_beta_clusters.py
index 5f2bf86ac..ac42ea4c9 100644
--- a/tests/cli/test_beta_clusters.py
+++ b/tests/cli/test_beta_clusters.py
@@ -53,6 +53,28 @@ def _cluster_body(cluster_id: str = "cluster-1", name: str = "my-cluster", **ove
}
+def _remediation_body(remediation_id: str = "rem-1", **overrides: Any) -> dict[str, Any]:
+ body: dict[str, Any] = {
+ "id": remediation_id,
+ "cluster_id": "c1",
+ "instance_id": "i1",
+ "mode": "REMEDIATION_MODE_VM_ONLY",
+ "state": "PENDING_APPROVAL",
+ "trigger": "REMEDIATION_TRIGGER_AUTOMATED",
+ "reason": "health check failed",
+ }
+ body.update(overrides)
+ return body
+
+
+def _remediation_list_body(*remediations: dict[str, Any]) -> dict[str, Any]:
+ return {
+ "has_next": False,
+ "next_page_token": "",
+ "remediations": list(remediations),
+ }
+
+
class TestBetaClustersList:
@pytest.mark.respx(base_url=base_url)
def test_list_table(self, respx_mock: MockRouter, cli_runner: CliRunner) -> None:
@@ -238,3 +260,127 @@ def test_storage_delete_json(self, respx_mock: MockRouter, cli_runner: CliRunner
result = cli_runner.invoke(["beta", "clusters", "storage", "delete", "vol-1", "--json"])
assert json.loads(result.output) == {"success": True}
assert result.exit_code == 0
+
+
+class TestBetaClustersRemediations:
+ @pytest.mark.respx(base_url=base_url)
+ def test_remediations_create_json(self, respx_mock: MockRouter, cli_runner: CliRunner) -> None:
+ route = respx_mock.post("/compute/clusters/c1/instances/i1/remediations").mock(
+ return_value=httpx.Response(200, json=_remediation_body("rem-created", state="PENDING"))
+ )
+ result = cli_runner.invoke(
+ [
+ "beta",
+ "clusters",
+ "remediations",
+ "create",
+ "c1",
+ "i1",
+ "--mode",
+ "REMEDIATION_MODE_VM_ONLY",
+ "--reason",
+ "node unhealthy",
+ "--remediation-id",
+ "rem-created",
+ "--json",
+ ],
+ )
+
+ assert json.loads(result.output)["id"] == "rem-created"
+ request = cast(Call, route.calls[0]).request
+ assert request.url.params["remediation_id"] == "rem-created"
+ assert json.loads(request.content.decode()) == {
+ "mode": "REMEDIATION_MODE_VM_ONLY",
+ "reason": "node unhealthy",
+ }
+ assert result.exit_code == 0
+
+ @pytest.mark.respx(base_url=base_url)
+ def test_remediations_list_uses_wildcard_when_instance_id_omitted(
+ self, respx_mock: MockRouter, cli_runner: CliRunner
+ ) -> None:
+ payload = _remediation_list_body(_remediation_body())
+ route = respx_mock.get("/compute/clusters/c1/instances/-/remediations").mock(
+ return_value=httpx.Response(200, json=payload)
+ )
+ result = cli_runner.invoke(["beta", "clusters", "remediations", "list", "c1", "--json"])
+
+ assert json.loads(result.output) == payload
+ assert cast(Call, route.calls[0]).request.url.path == "/compute/clusters/c1/instances/-/remediations"
+ assert result.exit_code == 0
+
+ @pytest.mark.respx(base_url=base_url)
+ def test_remediations_list_accepts_instance_id(self, respx_mock: MockRouter, cli_runner: CliRunner) -> None:
+ payload = _remediation_list_body(_remediation_body())
+ route = respx_mock.get("/compute/clusters/c1/instances/i1/remediations").mock(
+ return_value=httpx.Response(200, json=payload)
+ )
+ result = cli_runner.invoke(["beta", "clusters", "remediations", "list", "c1", "i1", "--json"])
+
+ assert json.loads(result.output) == payload
+ assert cast(Call, route.calls[0]).request.url.path == "/compute/clusters/c1/instances/i1/remediations"
+ assert result.exit_code == 0
+
+ @pytest.mark.respx(base_url=base_url)
+ def test_remediations_approve_resolves_cluster_and_instance(
+ self, respx_mock: MockRouter, cli_runner: CliRunner
+ ) -> None:
+ respx_mock.get("/compute/clusters").mock(
+ return_value=httpx.Response(200, json={"clusters": [_cluster_body("c1")]})
+ )
+ respx_mock.get("/compute/clusters/c1/instances/-/remediations").mock(
+ return_value=httpx.Response(200, json=_remediation_list_body(_remediation_body("rem-approve")))
+ )
+ route = respx_mock.post("/compute/clusters/c1/instances/i1/remediations/rem-approve/approve").mock(
+ return_value=httpx.Response(200, json=_remediation_body("rem-approve", state="PENDING"))
+ )
+
+ result = cli_runner.invoke(
+ ["beta", "clusters", "remediations", "approve", "rem-approve", "--comment", "go", "--json"]
+ )
+
+ assert json.loads(result.output)["state"] == "PENDING"
+ assert json.loads(cast(Call, route.calls[0]).request.content.decode()) == {"comment": "go"}
+ assert result.exit_code == 0
+
+ @pytest.mark.respx(base_url=base_url)
+ def test_remediations_cancel_resolves_cluster_and_instance(
+ self, respx_mock: MockRouter, cli_runner: CliRunner
+ ) -> None:
+ respx_mock.get("/compute/clusters").mock(
+ return_value=httpx.Response(200, json={"clusters": [_cluster_body("c1")]})
+ )
+ respx_mock.get("/compute/clusters/c1/instances/-/remediations").mock(
+ return_value=httpx.Response(200, json=_remediation_list_body(_remediation_body("rem-cancel")))
+ )
+ route = respx_mock.post("/compute/clusters/c1/instances/i1/remediations/rem-cancel/cancel").mock(
+ return_value=httpx.Response(200, json=_remediation_body("rem-cancel", state="CANCELLED"))
+ )
+
+ result = cli_runner.invoke(["beta", "clusters", "remediations", "cancel", "rem-cancel", "--json"])
+
+ assert json.loads(result.output)["state"] == "CANCELLED"
+ assert route.calls
+ assert result.exit_code == 0
+
+ @pytest.mark.respx(base_url=base_url)
+ def test_remediations_reject_resolves_cluster_and_instance(
+ self, respx_mock: MockRouter, cli_runner: CliRunner
+ ) -> None:
+ respx_mock.get("/compute/clusters").mock(
+ return_value=httpx.Response(200, json={"clusters": [_cluster_body("c1")]})
+ )
+ respx_mock.get("/compute/clusters/c1/instances/-/remediations").mock(
+ return_value=httpx.Response(200, json=_remediation_list_body(_remediation_body("rem-reject")))
+ )
+ route = respx_mock.post("/compute/clusters/c1/instances/i1/remediations/rem-reject/reject").mock(
+ return_value=httpx.Response(200, json=_remediation_body("rem-reject", state="CANCELLED"))
+ )
+
+ result = cli_runner.invoke(
+ ["beta", "clusters", "remediations", "reject", "rem-reject", "--comment", "skip", "--json"]
+ )
+
+ assert json.loads(result.output)["state"] == "CANCELLED"
+ assert json.loads(cast(Call, route.calls[0]).request.content.decode()) == {"comment": "skip"}
+ assert result.exit_code == 0
From 362807ab3551e214f7255aa31c2816a300bff47a Mon Sep 17 00:00:00 2001
From: Blaine Kasten
Date: Mon, 18 May 2026 13:07:33 -0500
Subject: [PATCH 13/14] add remediation retrieve command and some fixes
---
src/together/lib/cli/__init__.py | 7 ++
.../remediations/_resolve_remediation.py | 33 +++++++
.../api/beta/clusters/remediations/_util.py | 91 -------------------
.../api/beta/clusters/remediations/approve.py | 5 +-
.../api/beta/clusters/remediations/cancel.py | 2 +-
.../api/beta/clusters/remediations/create.py | 50 ++++++----
.../api/beta/clusters/remediations/list.py | 70 +++++++-------
.../api/beta/clusters/remediations/reject.py | 5 +-
.../beta/clusters/remediations/retrieve.py | 31 +++++++
src/together/lib/cli/components/model_dump.py | 19 +++-
src/together/lib/cli/utils/_help_examples.py | 25 ++++-
.../lib/cli/utils/_preparse_tokens.py | 1 +
tests/cli/test_beta_clusters.py | 23 ++++-
uv.lock | 2 +-
14 files changed, 208 insertions(+), 156 deletions(-)
create mode 100644 src/together/lib/cli/api/beta/clusters/remediations/_resolve_remediation.py
delete mode 100644 src/together/lib/cli/api/beta/clusters/remediations/_util.py
create mode 100644 src/together/lib/cli/api/beta/clusters/remediations/retrieve.py
diff --git a/src/together/lib/cli/__init__.py b/src/together/lib/cli/__init__.py
index 9a61f0cf8..ffac00a2b 100644
--- a/src/together/lib/cli/__init__.py
+++ b/src/together/lib/cli/__init__.py
@@ -65,6 +65,7 @@
BETA_CLUSTERS_STORAGE_CREATE_HELP_EXAMPLES,
BETA_CLUSTERS_STORAGE_UPDATE_HELP_EXAMPLES,
BETA_CLUSTERS_GET_CREDENTIALS_HELP_EXAMPLES,
+ BETA_CLUSTERS_REMEDIATIONS_CREATE_HELP_EXAMPLES,
)
from together.lib.cli.utils._help_formatter import help_formatter
from together.lib.cli.utils._preparse_tokens import preparse_tokens
@@ -500,12 +501,18 @@ async def run_command() -> None:
(f"{_CLI}.beta.clusters.remediations.create:create"),
alias="-c",
help="Create a node remediation",
+ help_epilogue=BETA_CLUSTERS_REMEDIATIONS_CREATE_HELP_EXAMPLES,
)
remediations_app.command(
(f"{_CLI}.beta.clusters.remediations.list:list"),
alias="ls",
help="List node remediations",
)
+remediations_app.command(
+ (f"{_CLI}.beta.clusters.remediations.retrieve:retrieve"),
+ alias="get",
+ help="Get remediation details",
+)
remediations_app.command(
(f"{_CLI}.beta.clusters.remediations.approve:approve"),
help="Approve a pending remediation",
diff --git a/src/together/lib/cli/api/beta/clusters/remediations/_resolve_remediation.py b/src/together/lib/cli/api/beta/clusters/remediations/_resolve_remediation.py
new file mode 100644
index 000000000..55a49e7ba
--- /dev/null
+++ b/src/together/lib/cli/api/beta/clusters/remediations/_resolve_remediation.py
@@ -0,0 +1,33 @@
+from __future__ import annotations
+
+import sys
+
+from together import omit
+from together._types import Omit
+from together.lib.cli.utils.config import CLIConfigParameter
+from together.lib.cli.utils._console import console
+from together.types.beta.clusters.remediation import Remediation
+
+
+async def resolve_remediation(config: CLIConfigParameter, remediation_id: str) -> Remediation:
+ clusters = await config.client.beta.clusters.list()
+
+ for cluster in clusters.clusters:
+ page_token: str | Omit = omit
+ while True:
+ response = await config.client.beta.clusters.remediations.list(
+ "-",
+ cluster_id=cluster.cluster_id,
+ page_size=100,
+ page_token=page_token,
+ )
+ for remediation in response.remediations:
+ if remediation.id == remediation_id:
+ return remediation
+
+ if not response.has_next or not response.next_page_token:
+ break
+ page_token = response.next_page_token
+
+ console.print(f"[red]Error:[/red] Remediation not found: {remediation_id}")
+ sys.exit(1)
diff --git a/src/together/lib/cli/api/beta/clusters/remediations/_util.py b/src/together/lib/cli/api/beta/clusters/remediations/_util.py
deleted file mode 100644
index 946cdccda..000000000
--- a/src/together/lib/cli/api/beta/clusters/remediations/_util.py
+++ /dev/null
@@ -1,91 +0,0 @@
-from __future__ import annotations
-
-import sys
-from typing import List, Literal, TypeVar, Optional, cast, get_args
-from datetime import datetime
-
-from together import omit
-from together._types import Omit
-from together.lib.cli.utils.config import CLIConfigParameter
-from together.lib.cli.utils._console import console
-from together.lib.cli.components.list import ListTable
-from together.types.beta.clusters.remediation import Remediation
-
-EMPTY_MESSAGE = "No remediations found for this cluster."
-
-T = TypeVar("T")
-RemediationState = Literal[
- "PENDING_APPROVAL", "PENDING", "RUNNING", "SUCCEEDED", "FAILED", "CANCELLED", "AUTO_RESOLVED"
-]
-_REMEDIATION_STATES = set(get_args(RemediationState))
-
-
-def omit_if_none(value: Optional[T]) -> T | Omit:
- return omit if value is None else value
-
-
-def parse_states(state: Optional[str]) -> List[RemediationState] | Omit:
- if state is None:
- return omit
- states = [part.strip() for part in state.split(",") if part.strip()]
- if not states:
- return omit
-
- invalid_states = sorted(set(states) - _REMEDIATION_STATES)
- if invalid_states:
- console.print(f"[red]Error:[/red] Invalid remediation state: {', '.join(invalid_states)}")
- sys.exit(1)
-
- return cast(List[RemediationState], states)
-
-
-def format_time(value: Optional[datetime]) -> str:
- if value is None:
- return "-"
- return value.isoformat()
-
-
-def print_remediations(remediations: List[Remediation]) -> None:
- table = ListTable(title="Cluster Remediations", empty_message=EMPTY_MESSAGE)
- table.add_primary_column("ID")
- table.add_column("Instance")
- table.add_column("State")
- table.add_column("Mode")
- table.add_column("Trigger")
- table.add_column("Created")
-
- for remediation in remediations:
- table.add_row(
- remediation.id,
- remediation.instance_id,
- remediation.state,
- remediation.mode.replace("REMEDIATION_MODE_", ""),
- remediation.trigger.replace("REMEDIATION_TRIGGER_", ""),
- format_time(remediation.create_time),
- )
-
- console.print(table)
-
-
-async def resolve_remediation(config: CLIConfigParameter, remediation_id: str) -> Remediation:
- clusters = await config.client.beta.clusters.list()
-
- for cluster in clusters.clusters:
- page_token: str | Omit = omit
- while True:
- response = await config.client.beta.clusters.remediations.list(
- "-",
- cluster_id=cluster.cluster_id,
- page_size=100,
- page_token=page_token,
- )
- for remediation in response.remediations:
- if remediation.id == remediation_id:
- return remediation
-
- if not response.has_next or not response.next_page_token:
- break
- page_token = response.next_page_token
-
- console.print(f"[red]Error:[/red] Remediation not found: {remediation_id}")
- sys.exit(1)
diff --git a/src/together/lib/cli/api/beta/clusters/remediations/approve.py b/src/together/lib/cli/api/beta/clusters/remediations/approve.py
index 46428c161..eb81a5346 100644
--- a/src/together/lib/cli/api/beta/clusters/remediations/approve.py
+++ b/src/together/lib/cli/api/beta/clusters/remediations/approve.py
@@ -4,11 +4,12 @@
from cyclopts import Parameter
+from together import omit
from together._utils._json import openapi_dumps
from together.lib.cli.utils.config import CLIConfigParameter
from together.lib.cli.utils._console import console
from together.lib.cli.components.loader import show_loading_status
-from together.lib.cli.api.beta.clusters.remediations._util import omit_if_none, resolve_remediation
+from together.lib.cli.api.beta.clusters.remediations._resolve_remediation import resolve_remediation
async def approve(
@@ -25,7 +26,7 @@ async def approve(
remediation_id,
cluster_id=remediation.cluster_id,
instance_id=remediation.instance_id,
- comment=omit_if_none(comment),
+ comment=comment or omit,
),
)
diff --git a/src/together/lib/cli/api/beta/clusters/remediations/cancel.py b/src/together/lib/cli/api/beta/clusters/remediations/cancel.py
index fcd34f542..99adc6bff 100644
--- a/src/together/lib/cli/api/beta/clusters/remediations/cancel.py
+++ b/src/together/lib/cli/api/beta/clusters/remediations/cancel.py
@@ -4,7 +4,7 @@
from together.lib.cli.utils.config import CLIConfigParameter
from together.lib.cli.utils._console import console
from together.lib.cli.components.loader import show_loading_status
-from together.lib.cli.api.beta.clusters.remediations._util import resolve_remediation
+from together.lib.cli.api.beta.clusters.remediations._resolve_remediation import resolve_remediation
async def cancel(
diff --git a/src/together/lib/cli/api/beta/clusters/remediations/create.py b/src/together/lib/cli/api/beta/clusters/remediations/create.py
index 3ef21e7c4..8a6f2db49 100644
--- a/src/together/lib/cli/api/beta/clusters/remediations/create.py
+++ b/src/together/lib/cli/api/beta/clusters/remediations/create.py
@@ -1,29 +1,29 @@
from __future__ import annotations
-from typing import Literal, Optional, Annotated
+from typing import Literal, Optional, Annotated, cast
from cyclopts import Parameter
+from together import omit
from together._utils._json import openapi_dumps
from together.lib.cli.utils.config import CLIConfigParameter
from together.lib.cli.utils._console import console
from together.lib.cli.components.loader import show_loading_status
-from together.lib.cli.api.beta.clusters.remediations._util import omit_if_none
RemediationModeParameter = Annotated[
Literal[
- "REMEDIATION_MODE_VM_ONLY",
- "REMEDIATION_MODE_HOST_AWARE",
- "REMEDIATION_MODE_EVICT_WITHOUT_REPLACEMENT",
- "REMEDIATION_MODE_REBOOT_VM",
+ "VM_ONLY",
+ "HOST_AWARE",
+ "EVICT_WITHOUT_REPLACEMENT",
+ "REBOOT_VM",
],
- Parameter(help="How the remediation should be performed"),
+ Parameter(help="The type of remediation to perform"),
]
async def create(
- cluster_id: str,
- instance_id: str,
+ cluster_id: Annotated[str, Parameter(help="The ID of the cluster")],
+ instance_id: Annotated[str, Parameter(help="The ID of the node within the cluster to remediate")],
*,
mode: RemediationModeParameter,
remediation_id: Annotated[Optional[str], Parameter(help="Client-specified ID for idempotency")] = None,
@@ -31,18 +31,32 @@ async def create(
config: CLIConfigParameter,
) -> None:
"""Create a node remediation for an instance."""
- request = config.client.beta.clusters.remediations.create(
- instance_id,
- cluster_id=cluster_id,
- mode=mode,
- remediation_id=omit_if_none(remediation_id),
- reason=omit_if_none(reason),
+ safe_mode = cast(
+ Literal[
+ "REMEDIATION_MODE_VM_ONLY",
+ "REMEDIATION_MODE_HOST_AWARE",
+ "REMEDIATION_MODE_EVICT_WITHOUT_REPLACEMENT",
+ "REMEDIATION_MODE_REBOOT_VM",
+ ],
+ f"REMEDIATION_MODE_{mode}",
+ )
+
+ response = await show_loading_status(
+ "Creating remediation...",
+ config.client.beta.clusters.remediations.create(
+ instance_id,
+ cluster_id=cluster_id,
+ mode=safe_mode,
+ remediation_id=remediation_id or omit,
+ reason=reason or omit,
+ ),
)
- response = await show_loading_status("Creating remediation...", request)
if config.json:
console.print_json(openapi_dumps(response).decode("utf-8"))
return
- console.print("[blue]Remediation created successfully[/blue]")
- console.print(f"[primary]Remediation ID:[/primary] {response.id}")
+ console.print(f"[green]√ Remediation created[/green] [dim]({response.id})[/dim]")
+ console.print(f" Remediations may take some time to complete.\n")
+ console.print(f" To retrieve the status:")
+ console.print(f" [dim]-[/dim] [primary]tg beta clusters remediations {response.id}[/primary]")
diff --git a/src/together/lib/cli/api/beta/clusters/remediations/list.py b/src/together/lib/cli/api/beta/clusters/remediations/list.py
index 7c0124daf..22c70231d 100644
--- a/src/together/lib/cli/api/beta/clusters/remediations/list.py
+++ b/src/together/lib/cli/api/beta/clusters/remediations/list.py
@@ -1,40 +1,22 @@
from __future__ import annotations
-from typing import Literal, Optional, Annotated
+from typing import Optional, Annotated
from cyclopts import Parameter
+from together import omit
from together._utils._json import openapi_dumps
+from together.lib.utils.tools import format_datetime
from together.lib.cli.utils.config import CLIConfigParameter
from together.lib.cli.utils._console import console
+from together.lib.cli.components.list import ListTable
from together.lib.cli.components.loader import show_loading_status
-from together.lib.cli.api.beta.clusters.remediations._util import (
- omit_if_none,
- parse_states,
- print_remediations,
-)
-
-OptionalRemediationModeParameter = Annotated[
- Optional[
- Literal[
- "REMEDIATION_MODE_VM_ONLY",
- "REMEDIATION_MODE_HOST_AWARE",
- "REMEDIATION_MODE_EVICT_WITHOUT_REPLACEMENT",
- "REMEDIATION_MODE_REBOOT_VM",
- ]
- ],
- Parameter(help="Filter by remediation mode"),
-]
async def list(
cluster_id: str,
instance_id: Annotated[Optional[str], Parameter(help="Instance ID to list remediations for")] = None,
- mode: OptionalRemediationModeParameter = None,
- state: Annotated[Optional[str], Parameter(help="Comma-separated remediation states to include")] = None,
- order_by: Annotated[Optional[str], Parameter(help="Order by expression")] = None,
- page_size: Annotated[Optional[int], Parameter(help="Maximum results to return")] = None,
- page_token: Annotated[Optional[str], Parameter(help="Pagination token from a previous request")] = None,
+ after: Annotated[Optional[str], Parameter(help="Pagination token from a previous request")] = None,
*,
config: CLIConfigParameter,
) -> None:
@@ -44,11 +26,7 @@ async def list(
config.client.beta.clusters.remediations.list(
instance_id or "-",
cluster_id=cluster_id,
- mode=omit_if_none(mode),
- state=parse_states(state),
- order_by=omit_if_none(order_by),
- page_size=omit_if_none(page_size),
- page_token=omit_if_none(page_token),
+ page_token=after or omit,
),
)
@@ -56,10 +34,40 @@ async def list(
console.print_json(openapi_dumps(response).decode("utf-8"))
return
- print_remediations(response.remediations)
+ table = ListTable(title="Cluster Remediations", empty_message="No remediations found for this cluster.")
+ table.add_column("Created")
+ table.add_primary_column("Instance", ratio=3)
+ table.add_column("Mode")
+ table.add_column("State")
+ table.add_column("Remediation ID", ratio=3)
+
+ for remediation in response.remediations:
+ table.add_row(
+ format_datetime(remediation.create_time) if remediation.create_time else "-",
+ remediation.instance_id,
+ remediation.mode.replace("REMEDIATION_MODE_", ""),
+ _colorize(remediation.state),
+ remediation.id,
+ )
+
+ console.print(table)
if response.has_next and response.next_page_token:
- command = f"tg beta clusters remediations list {cluster_id}"
+ command = f"tg beta clusters remediations ls {cluster_id}"
if instance_id:
command += f" {instance_id}"
console.print("\n[blue dim]To display the next page, run:[/blue dim]")
- console.print(f" [dim]-[/dim] [white]{command} --page-token {response.next_page_token}[/white]")
+ console.print(f" [dim]-[/dim] [white]{command} --after {response.next_page_token}[/white]")
+
+
+def _colorize(state: str) -> str:
+ state_colors = {
+ "PENDING_APPROVAL": "yellow",
+ "PENDING": "yellow",
+ "RUNNING": "yellow",
+ "SUCCEEDED": "green",
+ "FAILED": "red",
+ "CANCELLED": "dim",
+ "AUTO_RESOLVED": "green",
+ }
+ color = state_colors[state] if state in state_colors else "white"
+ return f"[{color}]{state}[/{color}]"
diff --git a/src/together/lib/cli/api/beta/clusters/remediations/reject.py b/src/together/lib/cli/api/beta/clusters/remediations/reject.py
index a5ecb7803..6fab890c4 100644
--- a/src/together/lib/cli/api/beta/clusters/remediations/reject.py
+++ b/src/together/lib/cli/api/beta/clusters/remediations/reject.py
@@ -4,11 +4,12 @@
from cyclopts import Parameter
+from together import omit
from together._utils._json import openapi_dumps
from together.lib.cli.utils.config import CLIConfigParameter
from together.lib.cli.utils._console import console
from together.lib.cli.components.loader import show_loading_status
-from together.lib.cli.api.beta.clusters.remediations._util import omit_if_none, resolve_remediation
+from together.lib.cli.api.beta.clusters.remediations._resolve_remediation import resolve_remediation
async def reject(
@@ -25,7 +26,7 @@ async def reject(
remediation_id,
cluster_id=remediation.cluster_id,
instance_id=remediation.instance_id,
- comment=omit_if_none(comment),
+ comment=comment or omit,
),
)
diff --git a/src/together/lib/cli/api/beta/clusters/remediations/retrieve.py b/src/together/lib/cli/api/beta/clusters/remediations/retrieve.py
new file mode 100644
index 000000000..ffb458b52
--- /dev/null
+++ b/src/together/lib/cli/api/beta/clusters/remediations/retrieve.py
@@ -0,0 +1,31 @@
+from __future__ import annotations
+
+from together._utils._json import openapi_dumps
+from together.lib.cli.utils.config import CLIConfigParameter
+from together.lib.cli.utils._console import console
+from together.lib.cli.components.loader import show_loading_status
+from together.lib.cli.components.model_dump import print_model_dump
+from together.lib.cli.api.beta.clusters.remediations._resolve_remediation import resolve_remediation
+
+
+async def retrieve(
+ remediation_id: str,
+ *,
+ config: CLIConfigParameter,
+) -> None:
+ """Retrieve remediation details."""
+ remediation = await show_loading_status("Finding remediation...", resolve_remediation(config, remediation_id))
+ response = await show_loading_status(
+ "Retrieving remediation...",
+ config.client.beta.clusters.remediations.retrieve(
+ remediation_id,
+ cluster_id=remediation.cluster_id,
+ instance_id=remediation.instance_id,
+ ),
+ )
+
+ if config.json:
+ console.print_json(openapi_dumps(response).decode("utf-8"))
+ return
+
+ print_model_dump(response, show_nulls=False)
diff --git a/src/together/lib/cli/components/model_dump.py b/src/together/lib/cli/components/model_dump.py
index fe2e2d196..c16db6b33 100644
--- a/src/together/lib/cli/components/model_dump.py
+++ b/src/together/lib/cli/components/model_dump.py
@@ -37,6 +37,8 @@ def _pretty_print_results(
table.add_row("-", _pretty_print_results(item))
elif isinstance(results, BaseModel):
table.add_row("", _pretty_print_results(results.model_dump(), show_nulls=show_nulls))
+ elif isinstance(results, datetime):
+ table.add_row("", _colorize_value(format_datetime(results)))
else:
table.add_row("", _colorize_value(results))
return table
@@ -70,18 +72,25 @@ def _dump_sorted_model(model: BaseModel) -> dict[str, Any]:
- Lists last
"""
+ model_dump = model.model_dump()
+
+ # Filter out keys that are not in the model
+ model_dump = {k: v for k, v in model_dump.items() if k in model.model_fields_set}
+
def _sort_items(key: str, value: Any) -> int:
- # Returns a sort key: 0 for ID fields, 1 for primitives, 2 for dicts/objects, 3 for lists
+ # Returns a sort key: 0 for ID fields, 1 for dates, 2 for primitives, 3 for dicts/objects, 4 for lists
if key.endswith("_id"):
return 0
+ elif isinstance(value, datetime):
+ return 1
elif isinstance(value, dict) or isinstance(value, BaseModel):
- return 2
- elif isinstance(value, list):
return 3
+ elif isinstance(value, list):
+ return 4
else:
- return 1
+ return 2
- return dict(sorted(model.model_dump().items(), key=lambda kv: _sort_items(kv[0], kv[1])))
+ return dict(sorted(model_dump.items(), key=lambda kv: _sort_items(kv[0], kv[1])))
console.print(
_pretty_print_results(_dump_sorted_model(model), show_nulls=show_nulls, expand=expand, padding=padding)
diff --git a/src/together/lib/cli/utils/_help_examples.py b/src/together/lib/cli/utils/_help_examples.py
index c16e05e69..447b584b6 100644
--- a/src/together/lib/cli/utils/_help_examples.py
+++ b/src/together/lib/cli/utils/_help_examples.py
@@ -253,7 +253,7 @@
[primary]tg beta clusters delete [/primary]
[dim]-[/dim] Manage node remediations:
- [primary]tg beta clusters remediations list [/primary]
+ [primary]tg beta clusters remediations ls [/primary]
[primary]tg beta clusters remediations create --mode REMEDIATION_MODE_VM_ONLY[/primary]
"""
@@ -335,13 +335,16 @@
BETA_CLUSTERS_REMEDIATIONS_HELP_EXAMPLES = """[dim]Examples:[/dim]
[dim]-[/dim] List all remediations for a cluster:
- [primary]tg beta clusters remediations list [/primary]
+ [primary]tg beta clusters remediations ls [/primary]
[dim]-[/dim] List remediations for one instance:
- [primary]tg beta clusters remediations list [/primary]
+ [primary]tg beta clusters remediations ls [/primary]
[dim]-[/dim] Create a remediation:
- [primary]tg beta clusters remediations create --mode REMEDIATION_MODE_VM_ONLY --reason "node unhealthy"[/primary]
+ [primary]tg beta clusters remediations create --mode VM_ONLY --reason "node unhealthy"[/primary]
+
+[dim]-[/dim] Get remediation details:
+ [primary]tg beta clusters remediations [/primary]
[dim]-[/dim] Review automated remediations:
[primary]tg beta clusters remediations approve [/primary]
@@ -349,6 +352,20 @@
[primary]tg beta clusters remediations cancel [/primary]
"""
+BETA_CLUSTERS_REMEDIATIONS_CREATE_HELP_EXAMPLES = """[dim]Examples:[/dim]
+[dim]-[/dim] Create a VM-only remediation:
+ [primary]tg beta clusters remediations create --mode VM_ONLY[/primary]
+
+[dim]-[/dim] Create a host-aware remediation:
+ [primary]tg beta clusters remediations create --mode HOST_AWARE[/primary]
+
+[dim]-[/dim] Create a eviction-without-replacement remediation:
+ [primary]tg beta clusters remediations create --mode EVICT_WITHOUT_REPLACEMENT[/primary]
+
+[dim]-[/dim] Create a reboot-vm remediation:
+ [primary]tg beta clusters remediations create --mode REBOOT_VM[/primary]
+"""
+
## Beta > Jig commands
JIG_HELP_EXAMPLES = """[dim]Examples:[/dim]
diff --git a/src/together/lib/cli/utils/_preparse_tokens.py b/src/together/lib/cli/utils/_preparse_tokens.py
index cfc7ba9a7..2b302303a 100644
--- a/src/together/lib/cli/utils/_preparse_tokens.py
+++ b/src/together/lib/cli/utils/_preparse_tokens.py
@@ -15,6 +15,7 @@
"endpoints": re.compile(r"^endpoint-"),
"beta clusters": _UUID_RE,
"beta clusters storage": _UUID_RE,
+ "beta clusters remediations": _UUID_RE,
"beta jig volumes": _UUID_RE,
}
diff --git a/tests/cli/test_beta_clusters.py b/tests/cli/test_beta_clusters.py
index ac42ea4c9..1e204c49f 100644
--- a/tests/cli/test_beta_clusters.py
+++ b/tests/cli/test_beta_clusters.py
@@ -277,7 +277,7 @@ def test_remediations_create_json(self, respx_mock: MockRouter, cli_runner: CliR
"c1",
"i1",
"--mode",
- "REMEDIATION_MODE_VM_ONLY",
+ "VM_ONLY",
"--reason",
"node unhealthy",
"--remediation-id",
@@ -321,6 +321,27 @@ def test_remediations_list_accepts_instance_id(self, respx_mock: MockRouter, cli
assert cast(Call, route.calls[0]).request.url.path == "/compute/clusters/c1/instances/i1/remediations"
assert result.exit_code == 0
+ @pytest.mark.respx(base_url=base_url)
+ def test_remediations_retrieve_resolves_cluster_and_instance(
+ self, respx_mock: MockRouter, cli_runner: CliRunner
+ ) -> None:
+ body = _remediation_body("rem-get", state="RUNNING")
+ respx_mock.get("/compute/clusters").mock(
+ return_value=httpx.Response(200, json={"clusters": [_cluster_body("c1")]})
+ )
+ respx_mock.get("/compute/clusters/c1/instances/-/remediations").mock(
+ return_value=httpx.Response(200, json=_remediation_list_body(_remediation_body("rem-get")))
+ )
+ route = respx_mock.get("/compute/clusters/c1/instances/i1/remediations/rem-get").mock(
+ return_value=httpx.Response(200, json=body)
+ )
+
+ result = cli_runner.invoke(["beta", "clusters", "remediations", "get", "rem-get", "--json"])
+
+ assert json.loads(result.output) == body
+ assert cast(Call, route.calls[0]).request.url.path == "/compute/clusters/c1/instances/i1/remediations/rem-get"
+ assert result.exit_code == 0
+
@pytest.mark.respx(base_url=base_url)
def test_remediations_approve_resolves_cluster_and_instance(
self, respx_mock: MockRouter, cli_runner: CliRunner
diff --git a/uv.lock b/uv.lock
index a10f2d912..abe93dbc1 100644
--- a/uv.lock
+++ b/uv.lock
@@ -1559,7 +1559,7 @@ wheels = [
[[package]]
name = "together"
-version = "2.12.0"
+version = "2.14.0"
source = { editable = "." }
dependencies = [
{ name = "anyio" },
From fe8f4d1775c8a73843987793d5c575cfe0bf5328 Mon Sep 17 00:00:00 2001
From: Blaine Kasten
Date: Tue, 19 May 2026 14:12:03 -0500
Subject: [PATCH 14/14] address feedback
---
.../beta/clusters/remediations/retrieve.py | 2 +-
src/together/lib/cli/components/model_dump.py | 24 +++++++++++++++----
src/together/lib/cli/utils/_help_examples.py | 2 +-
3 files changed, 22 insertions(+), 6 deletions(-)
diff --git a/src/together/lib/cli/api/beta/clusters/remediations/retrieve.py b/src/together/lib/cli/api/beta/clusters/remediations/retrieve.py
index ffb458b52..8143dc450 100644
--- a/src/together/lib/cli/api/beta/clusters/remediations/retrieve.py
+++ b/src/together/lib/cli/api/beta/clusters/remediations/retrieve.py
@@ -28,4 +28,4 @@ async def retrieve(
console.print_json(openapi_dumps(response).decode("utf-8"))
return
- print_model_dump(response, show_nulls=False)
+ print_model_dump(response, show_nulls=False, only_set_fields=True)
diff --git a/src/together/lib/cli/components/model_dump.py b/src/together/lib/cli/components/model_dump.py
index c16db6b33..4bef66807 100644
--- a/src/together/lib/cli/components/model_dump.py
+++ b/src/together/lib/cli/components/model_dump.py
@@ -12,9 +12,25 @@
def print_model_dump(
- model: BaseModel, show_nulls: bool = True, expand: bool = True, padding: PaddingDimensions = (0, 1, 0, 0)
+ model: BaseModel,
+ show_nulls: bool = True,
+ expand: bool = True,
+ padding: PaddingDimensions = (0, 1, 0, 0),
+ *,
+ only_set_fields: bool = False,
) -> None:
- """Print an entire model with __decent__ formatting."""
+ """Print an entire model with __decent__ formatting.
+
+ Args:
+ model: The response model to render.
+ show_nulls: When True, include fields whose value is None or empty, displayed as
+ "n/a". When False, omit those fields entirely.
+ expand: Passed to the Rich table; when True, the table stretches to the terminal width.
+ padding: Rich table cell padding as (top, right, bottom, left).
+ only_set_fields: When True, only include fields present in the API response
+ (model.model_fields_set). Use this to avoid showing optional fields that were never
+ sent and still carry a default value. When False, all model fields are shown.
+ """
def _pretty_print_results(
results: Any, show_nulls: bool = True, expand: bool = False, padding: PaddingDimensions = (0, 1, 0, 0)
@@ -74,8 +90,8 @@ def _dump_sorted_model(model: BaseModel) -> dict[str, Any]:
model_dump = model.model_dump()
- # Filter out keys that are not in the model
- model_dump = {k: v for k, v in model_dump.items() if k in model.model_fields_set}
+ if only_set_fields:
+ model_dump = {k: v for k, v in model_dump.items() if k in model.model_fields_set}
def _sort_items(key: str, value: Any) -> int:
# Returns a sort key: 0 for ID fields, 1 for dates, 2 for primitives, 3 for dicts/objects, 4 for lists
diff --git a/src/together/lib/cli/utils/_help_examples.py b/src/together/lib/cli/utils/_help_examples.py
index 447b584b6..75ff7cb2c 100644
--- a/src/together/lib/cli/utils/_help_examples.py
+++ b/src/together/lib/cli/utils/_help_examples.py
@@ -254,7 +254,7 @@
[dim]-[/dim] Manage node remediations:
[primary]tg beta clusters remediations ls [/primary]
- [primary]tg beta clusters remediations create --mode REMEDIATION_MODE_VM_ONLY[/primary]
+ [primary]tg beta clusters remediations create --mode VM_ONLY[/primary]
"""
BETA_CLUSTERS_CREATE_HELP_EXAMPLES = """[dim]Examples:[/dim]