lightning-sdk 2025.7.10__py3-none-any.whl → 2025.7.22__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- lightning_sdk/__init__.py +3 -2
- lightning_sdk/api/cloud_account_api.py +154 -0
- lightning_sdk/api/deployment_api.py +11 -0
- lightning_sdk/api/job_api.py +9 -0
- lightning_sdk/api/llm_api.py +11 -6
- lightning_sdk/api/mmt_api.py +9 -0
- lightning_sdk/api/pipeline_api.py +4 -3
- lightning_sdk/api/studio_api.py +19 -5
- lightning_sdk/cli/clusters_menu.py +3 -3
- lightning_sdk/cli/create.py +22 -10
- lightning_sdk/cli/deploy/_auth.py +19 -3
- lightning_sdk/cli/deploy/serve.py +18 -4
- lightning_sdk/cli/entrypoint.py +1 -1
- lightning_sdk/cli/start.py +37 -7
- lightning_sdk/deployment/deployment.py +8 -0
- lightning_sdk/job/base.py +37 -5
- lightning_sdk/job/job.py +28 -4
- lightning_sdk/job/v1.py +10 -1
- lightning_sdk/job/v2.py +15 -1
- lightning_sdk/lightning_cloud/openapi/__init__.py +15 -1
- lightning_sdk/lightning_cloud/openapi/api/assistants_service_api.py +335 -0
- lightning_sdk/lightning_cloud/openapi/api/billing_service_api.py +214 -0
- lightning_sdk/lightning_cloud/openapi/api/cluster_service_api.py +5 -1
- lightning_sdk/lightning_cloud/openapi/api/user_service_api.py +11 -11
- lightning_sdk/lightning_cloud/openapi/models/__init__.py +15 -1
- lightning_sdk/lightning_cloud/openapi/models/assistant_id_conversations_body.py +29 -3
- lightning_sdk/lightning_cloud/openapi/models/blogposts_id_body.py +53 -1
- lightning_sdk/lightning_cloud/openapi/models/{v1_list_new_features_for_user_response.py → conversations_id_body1.py} +23 -23
- lightning_sdk/lightning_cloud/openapi/models/messages_id_body.py +123 -0
- lightning_sdk/lightning_cloud/openapi/models/project_id_schedules_body.py +29 -3
- lightning_sdk/lightning_cloud/openapi/models/project_id_storage_body.py +1 -27
- lightning_sdk/lightning_cloud/openapi/models/protobuf_null_value.py +102 -0
- lightning_sdk/lightning_cloud/openapi/models/schedules_id_body.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/storage_complete_body.py +1 -27
- lightning_sdk/lightning_cloud/openapi/models/uploads_upload_id_body1.py +3 -55
- lightning_sdk/lightning_cloud/openapi/models/user_id_upgradetrigger_body.py +175 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_ai_pod_v1.py +123 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_artifact.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_assistant_session_daily_aggregated.py +357 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_blog_post.py +53 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_cloud_provider.py +1 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_cluster_spec.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_cluster_type.py +1 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_complete_upload.py +3 -55
- lightning_sdk/lightning_cloud/openapi/models/v1_conversation.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_create_billing_upgrade_trigger_record_response.py +97 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_create_blog_post_request.py +53 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_create_checkout_session_request.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_create_subscription_checkout_session_request.py +29 -3
- lightning_sdk/lightning_cloud/openapi/models/v1_external_cluster_spec.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_function_tool.py +175 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_get_artifacts_page_response.py +29 -3
- lightning_sdk/lightning_cloud/openapi/models/v1_get_assistant_session_daily_aggregated_response.py +201 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_kubernetes_direct_v1.py +79 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_lightningapp_instance_artifact.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_like_status.py +104 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_list_notification_dialogs_response.py +149 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_list_published_managed_endpoint_models_response.py +123 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_message.py +53 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_presigned_url.py +1 -53
- lightning_sdk/lightning_cloud/openapi/models/v1_project.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_quote_subscription_response.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_schedule.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_tool.py +149 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_update_conversation_like_response.py +149 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_update_conversation_message_like_response.py +149 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_user_features.py +105 -261
- lightning_sdk/lightning_cloud/openapi/models/v1_volume.py +27 -1
- lightning_sdk/llm/llm.py +32 -5
- lightning_sdk/llm/public_assistants.json +3 -1
- lightning_sdk/machine.py +24 -1
- lightning_sdk/mmt/base.py +20 -2
- lightning_sdk/mmt/mmt.py +25 -3
- lightning_sdk/mmt/v1.py +7 -1
- lightning_sdk/mmt/v2.py +21 -2
- lightning_sdk/organization.py +4 -0
- lightning_sdk/pipeline/pipeline.py +16 -5
- lightning_sdk/pipeline/printer.py +5 -3
- lightning_sdk/pipeline/schedule.py +844 -1
- lightning_sdk/pipeline/steps.py +19 -4
- lightning_sdk/sandbox.py +4 -1
- lightning_sdk/serve.py +2 -0
- lightning_sdk/studio.py +79 -39
- lightning_sdk/teamspace.py +14 -8
- lightning_sdk/utils/resolve.py +29 -2
- {lightning_sdk-2025.7.10.dist-info → lightning_sdk-2025.7.22.dist-info}/METADATA +1 -1
- {lightning_sdk-2025.7.10.dist-info → lightning_sdk-2025.7.22.dist-info}/RECORD +92 -78
- lightning_sdk/api/cluster_api.py +0 -119
- /lightning_sdk/cli/{inspect.py → inspection.py} +0 -0
- {lightning_sdk-2025.7.10.dist-info → lightning_sdk-2025.7.22.dist-info}/LICENSE +0 -0
- {lightning_sdk-2025.7.10.dist-info → lightning_sdk-2025.7.22.dist-info}/WHEEL +0 -0
- {lightning_sdk-2025.7.10.dist-info → lightning_sdk-2025.7.22.dist-info}/entry_points.txt +0 -0
- {lightning_sdk-2025.7.10.dist-info → lightning_sdk-2025.7.22.dist-info}/top_level.txt +0 -0
lightning_sdk/__init__.py
CHANGED
|
@@ -4,7 +4,7 @@ from lightning_sdk.constants import __GLOBAL_LIGHTNING_UNIQUE_IDS_STORE__ # noq
|
|
|
4
4
|
from lightning_sdk.deployment import Deployment
|
|
5
5
|
from lightning_sdk.helpers import _check_version_and_prompt_upgrade, _set_tqdm_envvars_noninteractive
|
|
6
6
|
from lightning_sdk.job import Job
|
|
7
|
-
from lightning_sdk.machine import Machine
|
|
7
|
+
from lightning_sdk.machine import CloudProvider, Machine
|
|
8
8
|
from lightning_sdk.mmt import MMT
|
|
9
9
|
from lightning_sdk.organization import Organization
|
|
10
10
|
from lightning_sdk.plugin import JobsPlugin, MultiMachineTrainingPlugin, Plugin, SlurmJobsPlugin
|
|
@@ -16,6 +16,7 @@ from lightning_sdk.user import User
|
|
|
16
16
|
__all__ = [
|
|
17
17
|
"AIHub",
|
|
18
18
|
"Agent",
|
|
19
|
+
"CloudProvider",
|
|
19
20
|
"Deployment",
|
|
20
21
|
"Job",
|
|
21
22
|
"JobsPlugin",
|
|
@@ -31,6 +32,6 @@ __all__ = [
|
|
|
31
32
|
"User",
|
|
32
33
|
]
|
|
33
34
|
|
|
34
|
-
__version__ = "2025.07.
|
|
35
|
+
__version__ = "2025.07.22"
|
|
35
36
|
_check_version_and_prompt_upgrade(__version__)
|
|
36
37
|
_set_tqdm_envvars_noninteractive()
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
from typing import TYPE_CHECKING, Dict, List, Optional, Union
|
|
2
|
+
|
|
3
|
+
from lightning_sdk.lightning_cloud.openapi import (
|
|
4
|
+
Externalv1Cluster,
|
|
5
|
+
V1CloudProvider,
|
|
6
|
+
V1ClusterType,
|
|
7
|
+
V1ExternalCluster,
|
|
8
|
+
V1ListClusterAcceleratorsResponse,
|
|
9
|
+
)
|
|
10
|
+
from lightning_sdk.lightning_cloud.rest_client import LightningClient
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from lightning_sdk.machine import CloudProvider
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class CloudAccountApi:
|
|
17
|
+
"""Internal API client for API requests to cluster endpoints."""
|
|
18
|
+
|
|
19
|
+
def __init__(self) -> None:
|
|
20
|
+
self._client = LightningClient(max_tries=7)
|
|
21
|
+
|
|
22
|
+
def get_cloud_account(self, cloud_account_id: str, teamspace_id: str, org_id: str) -> Externalv1Cluster:
|
|
23
|
+
"""Gets the cluster from given params cluster_id, project_id and owner.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
cloud_account_id: the cloud account to get
|
|
27
|
+
teamspace_id: the teamspace the cloud_account is supposed to be associated with
|
|
28
|
+
org_id: The owning org of this teamspace
|
|
29
|
+
|
|
30
|
+
"""
|
|
31
|
+
res = self._client.cluster_service_get_cluster(id=cloud_account_id, org_id=org_id, project_id=teamspace_id)
|
|
32
|
+
if not res:
|
|
33
|
+
raise ValueError(f"CloudAccount {cloud_account_id} does not exist")
|
|
34
|
+
return res
|
|
35
|
+
|
|
36
|
+
def list_cloud_accounts(self, teamspace_id: str) -> List[V1ExternalCluster]:
|
|
37
|
+
"""Lists the cloud accounts for a given teamspace.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
teamspace_id: The teamspace to list cloud accounts for
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
A list of cloud accounts
|
|
44
|
+
"""
|
|
45
|
+
res = self._client.cluster_service_list_project_clusters(
|
|
46
|
+
project_id=teamspace_id,
|
|
47
|
+
)
|
|
48
|
+
return res.clusters
|
|
49
|
+
|
|
50
|
+
def get_cloud_account_non_org(self, teamspace_id: str, cloud_account_id: str) -> Optional[V1ExternalCluster]:
|
|
51
|
+
for cluster in self.list_cloud_accounts(teamspace_id=teamspace_id):
|
|
52
|
+
if cluster.id == cloud_account_id:
|
|
53
|
+
return cluster
|
|
54
|
+
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
def list_cloud_account_accelerators(self, cloud_account_id: str, org_id: str) -> V1ListClusterAcceleratorsResponse:
|
|
58
|
+
"""Lists the accelerators for a given cloud account.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
cloud_account_id: cluster ID to list accelerators for
|
|
62
|
+
org_id: The owning org of this project
|
|
63
|
+
"""
|
|
64
|
+
res = self._client.cluster_service_list_cluster_accelerators(
|
|
65
|
+
id=cloud_account_id,
|
|
66
|
+
org_id=org_id,
|
|
67
|
+
)
|
|
68
|
+
if not res:
|
|
69
|
+
raise ValueError(f"CloudAccount {cloud_account_id} does not exist")
|
|
70
|
+
return res
|
|
71
|
+
|
|
72
|
+
def list_global_cloud_accounts(self, teamspace_id: str) -> List[V1ExternalCluster]:
|
|
73
|
+
"""Lists the accelerators for a given teamspace.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
teamspace_id: id of the teamspace to get the associated cloud_accounts for
|
|
77
|
+
"""
|
|
78
|
+
cloud_accounts = self.list_cloud_accounts(teamspace_id=teamspace_id)
|
|
79
|
+
if not cloud_accounts:
|
|
80
|
+
raise ValueError(f"Teamspace {teamspace_id} does not exist")
|
|
81
|
+
filtered_cloud_accounts = filter(lambda x: x.spec.cluster_type == V1ClusterType.GLOBAL, cloud_accounts)
|
|
82
|
+
return list(filtered_cloud_accounts)
|
|
83
|
+
|
|
84
|
+
def get_cloud_account_provider_mapping(self, teamspace_id: str) -> Dict["CloudProvider", str]:
|
|
85
|
+
"""Gets the cloud account <-> provider mapping."""
|
|
86
|
+
res = self.list_global_cloud_accounts(teamspace_id=teamspace_id)
|
|
87
|
+
return {self._get_cloud_account_provider(cloud_account): cloud_account.id for cloud_account in res}
|
|
88
|
+
|
|
89
|
+
@staticmethod
|
|
90
|
+
def _get_cloud_account_provider(cloud_account: Optional[V1ExternalCluster]) -> "CloudProvider":
|
|
91
|
+
"""Determines the cloud provider based on the cloud_account configuration.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
cloud_account: An optional Externalv1Cluster object containing cluster specifications
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
CloudProvider: The determined cloud provider, defaults to AWS if no match is found
|
|
98
|
+
"""
|
|
99
|
+
from lightning_sdk.machine import CloudProvider
|
|
100
|
+
|
|
101
|
+
if not cloud_account:
|
|
102
|
+
return CloudProvider.AWS
|
|
103
|
+
|
|
104
|
+
if cloud_account.spec and cloud_account.spec.driver:
|
|
105
|
+
if cloud_account.spec.driver == V1CloudProvider.LIGHTNING:
|
|
106
|
+
return CloudProvider.LIGHTNING
|
|
107
|
+
|
|
108
|
+
if cloud_account.spec.driver == V1CloudProvider.DGX:
|
|
109
|
+
return CloudProvider.DGX
|
|
110
|
+
|
|
111
|
+
if cloud_account.spec:
|
|
112
|
+
if cloud_account.spec.aws_v1:
|
|
113
|
+
return CloudProvider.AWS
|
|
114
|
+
if cloud_account.spec.google_cloud_v1:
|
|
115
|
+
return CloudProvider.GCP
|
|
116
|
+
if cloud_account.spec.lambda_labs_v1:
|
|
117
|
+
return CloudProvider.LAMBDA_LABS
|
|
118
|
+
if cloud_account.spec.vultr_v1:
|
|
119
|
+
return CloudProvider.VULTR
|
|
120
|
+
if cloud_account.spec.voltage_park_v1:
|
|
121
|
+
return CloudProvider.VOLTAGE_PARK
|
|
122
|
+
if cloud_account.spec.nebius_v1:
|
|
123
|
+
return CloudProvider.NEBIUS
|
|
124
|
+
|
|
125
|
+
return CloudProvider.AWS
|
|
126
|
+
|
|
127
|
+
def resolve_cloud_account(
|
|
128
|
+
self,
|
|
129
|
+
teamspace_id: str,
|
|
130
|
+
cloud_account: Optional[str],
|
|
131
|
+
cloud_provider: Optional[Union["CloudProvider", str]],
|
|
132
|
+
default_cloud_account: Optional[str],
|
|
133
|
+
) -> Optional[str]:
|
|
134
|
+
if cloud_account:
|
|
135
|
+
if cloud_provider:
|
|
136
|
+
cloud_account_resp = self.get_cloud_account_non_org(teamspace_id, cloud_account)
|
|
137
|
+
cloud_provider_resp = self._get_cloud_account_provider(cloud_account_resp)
|
|
138
|
+
if cloud_provider_resp != cloud_provider:
|
|
139
|
+
raise RuntimeError(
|
|
140
|
+
f"Specified both cloud_provider ({cloud_provider}) and "
|
|
141
|
+
"cloud_account ({cloud_account} has cloud provider {cloud_provider_resp}) which don't match!"
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
return cloud_account
|
|
145
|
+
|
|
146
|
+
if cloud_provider:
|
|
147
|
+
cloud_account_mapping = self.get_cloud_account_provider_mapping(teamspace_id=teamspace_id)
|
|
148
|
+
if cloud_provider and cloud_provider in cloud_account_mapping:
|
|
149
|
+
return cloud_account_mapping[cloud_provider]
|
|
150
|
+
|
|
151
|
+
if default_cloud_account:
|
|
152
|
+
return default_cloud_account
|
|
153
|
+
|
|
154
|
+
return None
|
|
@@ -264,6 +264,7 @@ class DeploymentApi:
|
|
|
264
264
|
custom_domain: Optional[str] = None,
|
|
265
265
|
quantity: Optional[int] = None,
|
|
266
266
|
include_credentials: Optional[bool] = None,
|
|
267
|
+
max_runtime: Optional[int] = None,
|
|
267
268
|
) -> V1Deployment:
|
|
268
269
|
# Update the deployment in place
|
|
269
270
|
|
|
@@ -291,6 +292,9 @@ class DeploymentApi:
|
|
|
291
292
|
requires_release |= apply_change(deployment.spec, "spot", spot)
|
|
292
293
|
requires_release |= apply_change(deployment.spec, "quantity", quantity)
|
|
293
294
|
requires_release |= apply_change(deployment.spec, "include_credentials", include_credentials)
|
|
295
|
+
requires_release |= apply_change(
|
|
296
|
+
deployment.spec, "requested_run_duration_seconds", str(max_runtime) if max_runtime is not None else None
|
|
297
|
+
)
|
|
294
298
|
|
|
295
299
|
if requires_release:
|
|
296
300
|
if deployment.strategy is None:
|
|
@@ -569,6 +573,7 @@ def to_spec(
|
|
|
569
573
|
quantity: Optional[int] = None,
|
|
570
574
|
include_credentials: Optional[bool] = None,
|
|
571
575
|
cloudspace_id: Optional[None] = None,
|
|
576
|
+
max_runtime: Optional[int] = None,
|
|
572
577
|
) -> V1JobSpec:
|
|
573
578
|
if cloud_account is None:
|
|
574
579
|
raise ValueError("The cloud account should be defined.")
|
|
@@ -585,6 +590,11 @@ def to_spec(
|
|
|
585
590
|
if command is None and cloudspace_id is not None:
|
|
586
591
|
raise ValueError("The command should be defined.")
|
|
587
592
|
|
|
593
|
+
# need to go via kwargs for typing compatibility since autogenerated apis accept None but aren't typed with None
|
|
594
|
+
optional_spec_kwargs = {}
|
|
595
|
+
if max_runtime:
|
|
596
|
+
optional_spec_kwargs["requested_run_duration_seconds"] = str(max_runtime)
|
|
597
|
+
|
|
588
598
|
return V1JobSpec(
|
|
589
599
|
cluster_id=cloud_account,
|
|
590
600
|
command=command,
|
|
@@ -597,6 +607,7 @@ def to_spec(
|
|
|
597
607
|
quantity=quantity,
|
|
598
608
|
include_credentials=include_credentials,
|
|
599
609
|
cloudspace_id=cloudspace_id,
|
|
610
|
+
**optional_spec_kwargs,
|
|
600
611
|
)
|
|
601
612
|
|
|
602
613
|
|
lightning_sdk/api/job_api.py
CHANGED
|
@@ -215,6 +215,7 @@ class JobApiV2:
|
|
|
215
215
|
path_mappings: Optional[Dict[str, str]],
|
|
216
216
|
artifacts_local: Optional[str], # deprecated in favor of path_mappings
|
|
217
217
|
artifacts_remote: Optional[str], # deprecated in favor of path_mappings
|
|
218
|
+
max_runtime: Optional[str] = None,
|
|
218
219
|
) -> V1Job:
|
|
219
220
|
body = self._create_job_body(
|
|
220
221
|
name=name,
|
|
@@ -231,6 +232,7 @@ class JobApiV2:
|
|
|
231
232
|
path_mappings=path_mappings,
|
|
232
233
|
artifacts_local=artifacts_local,
|
|
233
234
|
artifacts_remote=artifacts_remote,
|
|
235
|
+
max_runtime=max_runtime,
|
|
234
236
|
)
|
|
235
237
|
|
|
236
238
|
job: V1Job = self._client.jobs_service_create_job(project_id=teamspace_id, body=body)
|
|
@@ -252,6 +254,7 @@ class JobApiV2:
|
|
|
252
254
|
path_mappings: Optional[Dict[str, str]],
|
|
253
255
|
artifacts_local: Optional[str], # deprecated in favor of path_mappings
|
|
254
256
|
artifacts_remote: Optional[str], # deprecated in favor of path_mappings)
|
|
257
|
+
max_runtime: Optional[int] = None,
|
|
255
258
|
) -> ProjectIdJobsBody:
|
|
256
259
|
env_vars = []
|
|
257
260
|
if env is not None:
|
|
@@ -268,6 +271,11 @@ class JobApiV2:
|
|
|
268
271
|
artifacts_remote=artifacts_remote,
|
|
269
272
|
)
|
|
270
273
|
|
|
274
|
+
# need to go via kwargs for typing compatibility since autogenerated apis accept None but aren't typed with None
|
|
275
|
+
optional_spec_kwargs = {}
|
|
276
|
+
if max_runtime:
|
|
277
|
+
optional_spec_kwargs["requested_run_duration_seconds"] = str(max_runtime)
|
|
278
|
+
|
|
271
279
|
spec = V1JobSpec(
|
|
272
280
|
cloudspace_id=studio_id or "",
|
|
273
281
|
cluster_id=cloud_account or "",
|
|
@@ -281,6 +289,7 @@ class JobApiV2:
|
|
|
281
289
|
image_cluster_credentials=cloud_account_auth,
|
|
282
290
|
image_secret_ref=image_credentials or "",
|
|
283
291
|
path_mappings=path_mappings_list,
|
|
292
|
+
**optional_spec_kwargs,
|
|
284
293
|
)
|
|
285
294
|
return ProjectIdJobsBody(name=name, spec=spec)
|
|
286
295
|
|
lightning_sdk/api/llm_api.py
CHANGED
|
@@ -7,11 +7,12 @@ import threading
|
|
|
7
7
|
import warnings
|
|
8
8
|
from typing import Any, AsyncGenerator, Dict, Generator, List, Optional, Union
|
|
9
9
|
|
|
10
|
-
from
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
from lightning_sdk.lightning_cloud.openapi.models import (
|
|
11
|
+
StreamResultOfV1ConversationResponseChunk,
|
|
12
|
+
V1ConversationResponseChunk,
|
|
13
|
+
V1ResponseChoice,
|
|
14
|
+
V1ResponseChoiceDelta,
|
|
15
|
+
)
|
|
15
16
|
from lightning_sdk.lightning_cloud.rest_client import LightningClient
|
|
16
17
|
|
|
17
18
|
|
|
@@ -54,7 +55,9 @@ class LLMApi:
|
|
|
54
55
|
warnings.warn("Error decoding JSON:", decoded_line)
|
|
55
56
|
return None
|
|
56
57
|
|
|
57
|
-
def _stream_chat_response(
|
|
58
|
+
def _stream_chat_response(
|
|
59
|
+
self, result: StreamResultOfV1ConversationResponseChunk
|
|
60
|
+
) -> Generator[V1ConversationResponseChunk, None, None]:
|
|
58
61
|
for line in result.stream():
|
|
59
62
|
decoded_lines = line.decode("utf-8").strip()
|
|
60
63
|
for decoded_line in decoded_lines.splitlines():
|
|
@@ -80,6 +83,7 @@ class LLMApi:
|
|
|
80
83
|
name: Optional[str] = None,
|
|
81
84
|
metadata: Optional[Dict[str, str]] = None,
|
|
82
85
|
stream: bool = False,
|
|
86
|
+
tools: Optional[List[Dict[str, Any]]] = None,
|
|
83
87
|
**kwargs: Any,
|
|
84
88
|
) -> Union[V1ConversationResponseChunk, Generator[V1ConversationResponseChunk, None, None]]:
|
|
85
89
|
is_internal_conversation = os.getenv("LIGHTNING_INTERNAL_CONVERSATION", "false").lower() == "true"
|
|
@@ -105,6 +109,7 @@ class LLMApi:
|
|
|
105
109
|
"ephemeral": ephemeral,
|
|
106
110
|
"parent_conversation_id": kwargs.get("parent_conversation_id", ""),
|
|
107
111
|
"parent_message_id": kwargs.get("parent_message_id", ""),
|
|
112
|
+
"tools": tools,
|
|
108
113
|
}
|
|
109
114
|
if images:
|
|
110
115
|
for image in images:
|
lightning_sdk/api/mmt_api.py
CHANGED
|
@@ -87,6 +87,7 @@ class MMTApiV2:
|
|
|
87
87
|
path_mappings: Optional[Dict[str, str]],
|
|
88
88
|
artifacts_local: Optional[str], # deprecated in favor of path_mappings
|
|
89
89
|
artifacts_remote: Optional[str], # deprecated in favor of path_mappings
|
|
90
|
+
max_runtime: Optional[int],
|
|
90
91
|
) -> V1MultiMachineJob:
|
|
91
92
|
body = self._create_mmt_body(
|
|
92
93
|
name=name,
|
|
@@ -104,6 +105,7 @@ class MMTApiV2:
|
|
|
104
105
|
path_mappings=path_mappings,
|
|
105
106
|
artifacts_local=artifacts_local, # deprecated in favor of path_mappings
|
|
106
107
|
artifacts_remote=artifacts_remote, # deprecated in favor of path_mappings
|
|
108
|
+
max_runtime=max_runtime,
|
|
107
109
|
)
|
|
108
110
|
|
|
109
111
|
job: V1MultiMachineJob = self._client.jobs_service_create_multi_machine_job(project_id=teamspace_id, body=body)
|
|
@@ -126,6 +128,7 @@ class MMTApiV2:
|
|
|
126
128
|
path_mappings: Optional[Dict[str, str]],
|
|
127
129
|
artifacts_local: Optional[str], # deprecated in favor of path_mappings
|
|
128
130
|
artifacts_remote: Optional[str], # deprecated in favor of path_mappings
|
|
131
|
+
max_runtime: Optional[int] = None,
|
|
129
132
|
) -> ProjectIdMultimachinejobsBody:
|
|
130
133
|
env_vars = []
|
|
131
134
|
if env is not None:
|
|
@@ -142,6 +145,11 @@ class MMTApiV2:
|
|
|
142
145
|
artifacts_remote=artifacts_remote,
|
|
143
146
|
)
|
|
144
147
|
|
|
148
|
+
# need to go via kwargs for typing compatibility since autogenerated apis accept None but aren't typed with None
|
|
149
|
+
optional_spec_kwargs = {}
|
|
150
|
+
if max_runtime:
|
|
151
|
+
optional_spec_kwargs["requested_run_duration_seconds"] = str(max_runtime)
|
|
152
|
+
|
|
145
153
|
spec = V1JobSpec(
|
|
146
154
|
cloudspace_id=studio_id or "",
|
|
147
155
|
cluster_id=cloud_account or "",
|
|
@@ -155,6 +163,7 @@ class MMTApiV2:
|
|
|
155
163
|
image_cluster_credentials=cloud_account_auth,
|
|
156
164
|
image_secret_ref=image_credentials or "",
|
|
157
165
|
path_mappings=path_mappings_list,
|
|
166
|
+
**optional_spec_kwargs,
|
|
158
167
|
)
|
|
159
168
|
return ProjectIdMultimachinejobsBody(
|
|
160
169
|
name=name, spec=spec, cluster_id=cloud_account or "", machines=num_machines
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from typing import TYPE_CHECKING, List, Optional, Union
|
|
2
2
|
|
|
3
|
-
from lightning_sdk.api.
|
|
3
|
+
from lightning_sdk.api.cloud_account_api import CloudAccountApi
|
|
4
4
|
from lightning_sdk.lightning_cloud.openapi.models import (
|
|
5
5
|
ProjectIdPipelinesBody,
|
|
6
6
|
ProjectIdSchedulesBody,
|
|
@@ -23,7 +23,7 @@ class PipelineApi:
|
|
|
23
23
|
|
|
24
24
|
def __init__(self) -> None:
|
|
25
25
|
self._client = LightningClient(max_tries=0, retry=False)
|
|
26
|
-
self.
|
|
26
|
+
self._cloud_account_api = CloudAccountApi()
|
|
27
27
|
|
|
28
28
|
def get_pipeline_by_id(self, project_id: str, pipeline_id_or_name: str) -> Optional[V1Pipeline]:
|
|
29
29
|
if pipeline_id_or_name.startswith("pip_"):
|
|
@@ -75,6 +75,7 @@ class PipelineApi:
|
|
|
75
75
|
resource_id=pipeline.id,
|
|
76
76
|
parent_resource_id=parent_pipeline_id or "",
|
|
77
77
|
resource_type=V1ScheduleResourceType.PIPELINE,
|
|
78
|
+
timezone=schedule.timezone,
|
|
78
79
|
)
|
|
79
80
|
|
|
80
81
|
self._client.schedules_service_create_schedule(body, teamspace.id)
|
|
@@ -97,7 +98,7 @@ class PipelineApi:
|
|
|
97
98
|
|
|
98
99
|
from lightning_sdk.pipeline.utils import _get_cloud_account
|
|
99
100
|
|
|
100
|
-
clusters = self.
|
|
101
|
+
clusters = self._cloud_account_api.list_cloud_accounts(teamspace_id=teamspace.id)
|
|
101
102
|
|
|
102
103
|
selected_cluster = None
|
|
103
104
|
selected_cluster_id = _get_cloud_account(steps)
|
lightning_sdk/api/studio_api.py
CHANGED
|
@@ -121,7 +121,7 @@ class StudioApi:
|
|
|
121
121
|
name: str,
|
|
122
122
|
teamspace_id: str,
|
|
123
123
|
cloud_account: Optional[str] = None,
|
|
124
|
-
source: Optional[V1CloudSpaceSourceType] = None,
|
|
124
|
+
source: Optional[Union[V1CloudSpaceSourceType, str]] = None,
|
|
125
125
|
disable_secrets: bool = False,
|
|
126
126
|
sandbox: bool = False,
|
|
127
127
|
cloud_space_environment_template_id: Optional[str] = None,
|
|
@@ -168,12 +168,26 @@ class StudioApi:
|
|
|
168
168
|
return startup_status and startup_status.top_up_restore_finished
|
|
169
169
|
|
|
170
170
|
def start_studio(
|
|
171
|
-
self,
|
|
171
|
+
self,
|
|
172
|
+
studio_id: str,
|
|
173
|
+
teamspace_id: str,
|
|
174
|
+
machine: Union[Machine, str],
|
|
175
|
+
interruptible: bool = False,
|
|
176
|
+
max_runtime: Optional[int] = None,
|
|
172
177
|
) -> None:
|
|
173
178
|
"""Start an existing Studio."""
|
|
179
|
+
# need to go via kwargs for typing compatibility since autogenerated apis accept None but aren't typed with None
|
|
180
|
+
optional_kwargs_compute_body = {}
|
|
181
|
+
|
|
182
|
+
if max_runtime is not None:
|
|
183
|
+
optional_kwargs_compute_body["requested_run_duration_seconds"] = str(max_runtime)
|
|
174
184
|
self._client.cloud_space_service_start_cloud_space_instance(
|
|
175
185
|
IdStartBody(
|
|
176
|
-
compute_config=V1UserRequestedComputeConfig(
|
|
186
|
+
compute_config=V1UserRequestedComputeConfig(
|
|
187
|
+
name=_machine_to_compute_name(machine),
|
|
188
|
+
spot=interruptible,
|
|
189
|
+
**optional_kwargs_compute_body,
|
|
190
|
+
)
|
|
177
191
|
),
|
|
178
192
|
teamspace_id,
|
|
179
193
|
studio_id,
|
|
@@ -398,7 +412,7 @@ class StudioApi:
|
|
|
398
412
|
body=body,
|
|
399
413
|
)
|
|
400
414
|
|
|
401
|
-
def duplicate_studio(self, studio_id: str, teamspace_id: str, target_teamspace_id: str) -> Dict[str,
|
|
415
|
+
def duplicate_studio(self, studio_id: str, teamspace_id: str, target_teamspace_id: str) -> Dict[str, Any]:
|
|
402
416
|
"""Duplicates the given Studio from a given Teamspace into a given target Teamspace."""
|
|
403
417
|
target_teamspace = self._client.projects_service_get_project(target_teamspace_id)
|
|
404
418
|
init_kwargs = {}
|
|
@@ -421,7 +435,7 @@ class StudioApi:
|
|
|
421
435
|
init_kwargs["name"] = new_cloudspace.name
|
|
422
436
|
init_kwargs["teamspace"] = target_teamspace.name
|
|
423
437
|
|
|
424
|
-
self.start_studio(new_cloudspace.id, target_teamspace_id, Machine.CPU, False)
|
|
438
|
+
self.start_studio(new_cloudspace.id, target_teamspace_id, Machine.CPU, False, None)
|
|
425
439
|
return init_kwargs
|
|
426
440
|
|
|
427
441
|
def delete_studio(self, studio_id: str, teamspace_id: str) -> None:
|
|
@@ -5,7 +5,7 @@ from rich.console import Console
|
|
|
5
5
|
from simple_term_menu import TerminalMenu
|
|
6
6
|
|
|
7
7
|
from lightning_sdk import Teamspace
|
|
8
|
-
from lightning_sdk.api.
|
|
8
|
+
from lightning_sdk.api.cloud_account_api import CloudAccountApi
|
|
9
9
|
from lightning_sdk.lightning_cloud.openapi import Externalv1Cluster, V1ProjectClusterBinding
|
|
10
10
|
|
|
11
11
|
|
|
@@ -29,9 +29,9 @@ class _ClustersMenu:
|
|
|
29
29
|
selected_cluster_id = self._get_cluster_from_interactive_menu(
|
|
30
30
|
possible_clusters=teamspace.cloud_account_objs
|
|
31
31
|
)
|
|
32
|
-
|
|
32
|
+
cloud_account_api = CloudAccountApi()
|
|
33
33
|
|
|
34
|
-
return
|
|
34
|
+
return cloud_account_api.get_cloud_account(
|
|
35
35
|
cluster_id=selected_cluster_id, org_id=teamspace.owner.id, project_id=teamspace.id
|
|
36
36
|
)
|
|
37
37
|
except KeyboardInterrupt:
|
lightning_sdk/cli/create.py
CHANGED
|
@@ -7,12 +7,13 @@ import click
|
|
|
7
7
|
from rich.console import Console
|
|
8
8
|
|
|
9
9
|
from lightning_sdk import Machine, Studio
|
|
10
|
-
from lightning_sdk.api.
|
|
10
|
+
from lightning_sdk.api.cloud_account_api import CloudAccountApi
|
|
11
11
|
from lightning_sdk.cli.teamspace_menu import _TeamspacesMenu
|
|
12
|
-
from lightning_sdk.
|
|
12
|
+
from lightning_sdk.machine import CloudProvider
|
|
13
|
+
from lightning_sdk.utils.resolve import _resolve_deprecated_provider
|
|
13
14
|
|
|
14
15
|
_MACHINE_VALUES = tuple([machine.name for machine in Machine.__dict__.values() if isinstance(machine, Machine)])
|
|
15
|
-
_PROVIDER_VALUES = tuple([provider.value for provider in
|
|
16
|
+
_PROVIDER_VALUES = tuple([provider.value for provider in CloudProvider])
|
|
16
17
|
|
|
17
18
|
|
|
18
19
|
@click.group("create")
|
|
@@ -48,16 +49,26 @@ def create() -> None:
|
|
|
48
49
|
),
|
|
49
50
|
)
|
|
50
51
|
@click.option(
|
|
51
|
-
"--provider",
|
|
52
|
+
"--cloud-provider",
|
|
52
53
|
default=None,
|
|
53
54
|
type=click.Choice(_PROVIDER_VALUES),
|
|
54
55
|
help="The provider to create the studio on. If --cloud-account is specified, this option is prioritized.",
|
|
55
56
|
)
|
|
57
|
+
@click.option(
|
|
58
|
+
"--provider",
|
|
59
|
+
default=None,
|
|
60
|
+
type=click.Choice(_PROVIDER_VALUES),
|
|
61
|
+
help=(
|
|
62
|
+
"Deprecated. Use --cloud-provider instead. The provider to create the studio on. "
|
|
63
|
+
"If --cloud-account is specified, this option is prioritized."
|
|
64
|
+
),
|
|
65
|
+
)
|
|
56
66
|
def studio(
|
|
57
67
|
name: str,
|
|
58
68
|
teamspace: Optional[str] = None,
|
|
59
69
|
start: Optional[str] = None,
|
|
60
70
|
cloud_account: Optional[str] = None,
|
|
71
|
+
cloud_provider: Optional[str] = None,
|
|
61
72
|
provider: Optional[str] = None,
|
|
62
73
|
) -> None:
|
|
63
74
|
"""Create a new studio on the Lightning AI platform.
|
|
@@ -70,12 +81,13 @@ def studio(
|
|
|
70
81
|
menu = _TeamspacesMenu()
|
|
71
82
|
teamspace_resolved = menu._resolve_teamspace(teamspace)
|
|
72
83
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
84
|
+
cloud_provider = str(_resolve_deprecated_provider(cloud_provider, provider))
|
|
85
|
+
|
|
86
|
+
if cloud_provider is not None:
|
|
87
|
+
cloud_account_api = CloudAccountApi()
|
|
88
|
+
cloud_account = cloud_account_api.resolve_cloud_account(
|
|
89
|
+
teamspace_resolved.id, cloud_account, cloud_provider, teamspace_resolved.default_cloud_account
|
|
90
|
+
)
|
|
79
91
|
|
|
80
92
|
# default cloud account to current studios cloud account if run from studio
|
|
81
93
|
# else it will fall back to teamspace default in the backend
|
|
@@ -2,7 +2,8 @@ import os
|
|
|
2
2
|
import time
|
|
3
3
|
from datetime import datetime
|
|
4
4
|
from enum import Enum
|
|
5
|
-
from typing import List, Optional, TypedDict
|
|
5
|
+
from typing import Any, List, Optional, TypedDict
|
|
6
|
+
from urllib.parse import urlencode
|
|
6
7
|
|
|
7
8
|
from rich.console import Console
|
|
8
9
|
from rich.prompt import Confirm
|
|
@@ -10,6 +11,7 @@ from rich.prompt import Confirm
|
|
|
10
11
|
from lightning_sdk import Teamspace
|
|
11
12
|
from lightning_sdk.api import UserApi
|
|
12
13
|
from lightning_sdk.cli.teamspace_menu import _TeamspacesMenu
|
|
14
|
+
from lightning_sdk.lightning_cloud import env
|
|
13
15
|
from lightning_sdk.lightning_cloud.login import Auth, AuthServer
|
|
14
16
|
from lightning_sdk.lightning_cloud.openapi import V1CloudSpace
|
|
15
17
|
from lightning_sdk.lightning_cloud.rest_client import LightningClient
|
|
@@ -24,6 +26,17 @@ class _AuthMode(Enum):
|
|
|
24
26
|
DEPLOY = "deploy"
|
|
25
27
|
|
|
26
28
|
|
|
29
|
+
class _AuthServer(AuthServer):
|
|
30
|
+
def __init__(self, mode: _AuthMode, *args: Any, **kwargs: Any) -> None:
|
|
31
|
+
self._mode = mode
|
|
32
|
+
super().__init__(*args, **kwargs)
|
|
33
|
+
|
|
34
|
+
def get_auth_url(self, port: int) -> str:
|
|
35
|
+
redirect_uri = f"http://localhost:{port}/login-complete"
|
|
36
|
+
params = urlencode({"redirectTo": redirect_uri, "mode": self._mode.value, "okbhrt": LITSERVE_CODE})
|
|
37
|
+
return f"{env.LIGHTNING_CLOUD_URL}/sign-in?{params}"
|
|
38
|
+
|
|
39
|
+
|
|
27
40
|
class _AuthLitServe(Auth):
|
|
28
41
|
def __init__(self, mode: _AuthMode, shall_confirm: bool = False) -> None:
|
|
29
42
|
super().__init__()
|
|
@@ -33,7 +46,10 @@ class _AuthLitServe(Auth):
|
|
|
33
46
|
def _run_server(self) -> None:
|
|
34
47
|
if self._shall_confirm:
|
|
35
48
|
proceed = Confirm.ask(
|
|
36
|
-
"
|
|
49
|
+
"[bold yellow]LitServe needs to authenticate with Lightning AI to deploy your server.[/bold yellow]\n"
|
|
50
|
+
"This will open a browser window for login.\n"
|
|
51
|
+
"Do you want to continue?",
|
|
52
|
+
default=True,
|
|
37
53
|
)
|
|
38
54
|
if not proceed:
|
|
39
55
|
raise RuntimeError(
|
|
@@ -42,7 +58,7 @@ class _AuthLitServe(Auth):
|
|
|
42
58
|
print("Opening browser for authentication...")
|
|
43
59
|
print("Please come back to the terminal after logging in.")
|
|
44
60
|
time.sleep(3)
|
|
45
|
-
|
|
61
|
+
_AuthServer(self._mode).login_with_browser(self)
|
|
46
62
|
|
|
47
63
|
|
|
48
64
|
def authenticate(mode: _AuthMode, shall_confirm: bool = True) -> None:
|
|
@@ -317,12 +317,20 @@ def _handle_cloud(
|
|
|
317
317
|
ls_deployer = _LitServeDeployer(name=deployment_name, teamspace=None)
|
|
318
318
|
path = ls_deployer.dockerize_api(script_path, port=port, gpu=not machine.is_cpu(), tag=tag, print_success=False)
|
|
319
319
|
|
|
320
|
-
console.print(f"\
|
|
321
|
-
|
|
320
|
+
console.print(f"\n[bold]LitServe generated a Dockerfile at:[/bold]\n[u]{path}[/u]\n")
|
|
321
|
+
console.print("Please check that it matches your server setup.")
|
|
322
|
+
correct_dockerfile = (
|
|
323
|
+
True
|
|
324
|
+
if non_interactive
|
|
325
|
+
else Confirm.ask("Have you reviewed the Dockerfile and confirmed it's correct?", default=True)
|
|
326
|
+
)
|
|
322
327
|
if not correct_dockerfile:
|
|
323
|
-
console.print("Please
|
|
328
|
+
console.print("[red]Dockerfile review canceled. Please update the Dockerfile and try again.[/red]")
|
|
324
329
|
return
|
|
325
330
|
|
|
331
|
+
console.print(
|
|
332
|
+
"Building your container image now.\n[cyan bold]Make sure Docker is installed and running.[/cyan bold]\n"
|
|
333
|
+
)
|
|
326
334
|
with Progress(
|
|
327
335
|
SpinnerColumn(),
|
|
328
336
|
TextColumn("[progress.description]{task.description}"),
|
|
@@ -340,7 +348,12 @@ def _handle_cloud(
|
|
|
340
348
|
progress.remove_task(build_task)
|
|
341
349
|
|
|
342
350
|
except Exception as e:
|
|
343
|
-
console.print(
|
|
351
|
+
console.print(
|
|
352
|
+
"❌ Failed to build a container for your server.\n"
|
|
353
|
+
"Make sure Docker is installed and running, then try again.",
|
|
354
|
+
f"\n\nError details: {e}",
|
|
355
|
+
style="red",
|
|
356
|
+
)
|
|
344
357
|
return
|
|
345
358
|
|
|
346
359
|
# Push the container to the registry
|
|
@@ -373,6 +386,7 @@ def _handle_cloud(
|
|
|
373
386
|
f"{resolved_teamspace.owner.name}/{resolved_teamspace.name}/{container_basename}"
|
|
374
387
|
)
|
|
375
388
|
|
|
389
|
+
cloud_account = cloud_account or resolved_teamspace.default_cloud_account
|
|
376
390
|
if from_onboarding:
|
|
377
391
|
thread = Thread(
|
|
378
392
|
target=ls_deployer.run_on_cloud,
|
lightning_sdk/cli/entrypoint.py
CHANGED
|
@@ -21,7 +21,7 @@ from lightning_sdk.cli.deploy.serve import deploy
|
|
|
21
21
|
from lightning_sdk.cli.docker_cli import dockerize
|
|
22
22
|
from lightning_sdk.cli.download import download
|
|
23
23
|
from lightning_sdk.cli.generate import generate
|
|
24
|
-
from lightning_sdk.cli.
|
|
24
|
+
from lightning_sdk.cli.inspection import inspect
|
|
25
25
|
from lightning_sdk.cli.list import list_cli
|
|
26
26
|
from lightning_sdk.cli.open import open
|
|
27
27
|
from lightning_sdk.cli.run import run
|