dstack 0.19.25__py3-none-any.whl → 0.19.26__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.

Potentially problematic release.


This version of dstack might be problematic. Click here for more details.

Files changed (128) hide show
  1. dstack/_internal/cli/commands/__init__.py +2 -2
  2. dstack/_internal/cli/commands/apply.py +3 -61
  3. dstack/_internal/cli/commands/attach.py +1 -1
  4. dstack/_internal/cli/commands/completion.py +1 -1
  5. dstack/_internal/cli/commands/delete.py +2 -2
  6. dstack/_internal/cli/commands/fleet.py +1 -1
  7. dstack/_internal/cli/commands/gateway.py +2 -2
  8. dstack/_internal/cli/commands/init.py +56 -24
  9. dstack/_internal/cli/commands/logs.py +1 -1
  10. dstack/_internal/cli/commands/metrics.py +1 -1
  11. dstack/_internal/cli/commands/offer.py +45 -7
  12. dstack/_internal/cli/commands/project.py +2 -2
  13. dstack/_internal/cli/commands/secrets.py +2 -2
  14. dstack/_internal/cli/commands/server.py +1 -1
  15. dstack/_internal/cli/commands/stop.py +1 -1
  16. dstack/_internal/cli/commands/volume.py +1 -1
  17. dstack/_internal/cli/main.py +2 -2
  18. dstack/_internal/cli/services/completion.py +2 -2
  19. dstack/_internal/cli/services/configurators/__init__.py +6 -2
  20. dstack/_internal/cli/services/configurators/base.py +6 -7
  21. dstack/_internal/cli/services/configurators/fleet.py +1 -3
  22. dstack/_internal/cli/services/configurators/gateway.py +2 -4
  23. dstack/_internal/cli/services/configurators/run.py +195 -55
  24. dstack/_internal/cli/services/configurators/volume.py +2 -4
  25. dstack/_internal/cli/services/profile.py +1 -1
  26. dstack/_internal/cli/services/repos.py +51 -47
  27. dstack/_internal/core/backends/aws/configurator.py +11 -7
  28. dstack/_internal/core/backends/azure/configurator.py +11 -7
  29. dstack/_internal/core/backends/base/configurator.py +25 -13
  30. dstack/_internal/core/backends/cloudrift/configurator.py +13 -7
  31. dstack/_internal/core/backends/cudo/configurator.py +11 -7
  32. dstack/_internal/core/backends/datacrunch/compute.py +5 -1
  33. dstack/_internal/core/backends/datacrunch/configurator.py +13 -7
  34. dstack/_internal/core/backends/gcp/configurator.py +11 -7
  35. dstack/_internal/core/backends/hotaisle/configurator.py +13 -7
  36. dstack/_internal/core/backends/kubernetes/configurator.py +13 -7
  37. dstack/_internal/core/backends/lambdalabs/configurator.py +11 -7
  38. dstack/_internal/core/backends/nebius/compute.py +1 -1
  39. dstack/_internal/core/backends/nebius/configurator.py +11 -7
  40. dstack/_internal/core/backends/nebius/resources.py +21 -11
  41. dstack/_internal/core/backends/oci/configurator.py +11 -7
  42. dstack/_internal/core/backends/runpod/configurator.py +11 -7
  43. dstack/_internal/core/backends/template/configurator.py.jinja +11 -7
  44. dstack/_internal/core/backends/tensordock/configurator.py +13 -7
  45. dstack/_internal/core/backends/vastai/configurator.py +11 -7
  46. dstack/_internal/core/backends/vultr/configurator.py +11 -4
  47. dstack/_internal/core/compatibility/gpus.py +13 -0
  48. dstack/_internal/core/compatibility/runs.py +1 -0
  49. dstack/_internal/core/models/common.py +3 -3
  50. dstack/_internal/core/models/configurations.py +172 -27
  51. dstack/_internal/core/models/files.py +1 -1
  52. dstack/_internal/core/models/fleets.py +5 -1
  53. dstack/_internal/core/models/profiles.py +41 -11
  54. dstack/_internal/core/models/resources.py +46 -42
  55. dstack/_internal/core/models/runs.py +4 -0
  56. dstack/_internal/core/services/configs/__init__.py +2 -2
  57. dstack/_internal/core/services/profiles.py +2 -2
  58. dstack/_internal/core/services/repos.py +5 -3
  59. dstack/_internal/core/services/ssh/ports.py +1 -1
  60. dstack/_internal/proxy/lib/deps.py +6 -2
  61. dstack/_internal/server/app.py +22 -17
  62. dstack/_internal/server/background/tasks/process_gateways.py +4 -1
  63. dstack/_internal/server/background/tasks/process_instances.py +10 -2
  64. dstack/_internal/server/background/tasks/process_probes.py +1 -1
  65. dstack/_internal/server/background/tasks/process_running_jobs.py +10 -4
  66. dstack/_internal/server/background/tasks/process_runs.py +1 -1
  67. dstack/_internal/server/background/tasks/process_submitted_jobs.py +54 -43
  68. dstack/_internal/server/background/tasks/process_terminating_jobs.py +2 -2
  69. dstack/_internal/server/background/tasks/process_volumes.py +1 -1
  70. dstack/_internal/server/db.py +8 -4
  71. dstack/_internal/server/models.py +1 -0
  72. dstack/_internal/server/routers/gpus.py +1 -6
  73. dstack/_internal/server/schemas/runner.py +10 -0
  74. dstack/_internal/server/services/backends/__init__.py +14 -8
  75. dstack/_internal/server/services/backends/handlers.py +6 -1
  76. dstack/_internal/server/services/docker.py +5 -5
  77. dstack/_internal/server/services/fleets.py +14 -13
  78. dstack/_internal/server/services/gateways/__init__.py +2 -0
  79. dstack/_internal/server/services/gateways/client.py +5 -2
  80. dstack/_internal/server/services/gateways/connection.py +1 -1
  81. dstack/_internal/server/services/gpus.py +50 -49
  82. dstack/_internal/server/services/instances.py +41 -1
  83. dstack/_internal/server/services/jobs/__init__.py +15 -4
  84. dstack/_internal/server/services/jobs/configurators/base.py +7 -11
  85. dstack/_internal/server/services/jobs/configurators/dev.py +5 -0
  86. dstack/_internal/server/services/jobs/configurators/extensions/cursor.py +3 -3
  87. dstack/_internal/server/services/jobs/configurators/extensions/vscode.py +3 -3
  88. dstack/_internal/server/services/jobs/configurators/service.py +1 -0
  89. dstack/_internal/server/services/jobs/configurators/task.py +3 -0
  90. dstack/_internal/server/services/locking.py +5 -5
  91. dstack/_internal/server/services/logging.py +10 -2
  92. dstack/_internal/server/services/logs/__init__.py +8 -6
  93. dstack/_internal/server/services/logs/aws.py +330 -327
  94. dstack/_internal/server/services/logs/filelog.py +7 -6
  95. dstack/_internal/server/services/logs/gcp.py +141 -139
  96. dstack/_internal/server/services/plugins.py +1 -1
  97. dstack/_internal/server/services/projects.py +2 -5
  98. dstack/_internal/server/services/proxy/repo.py +5 -1
  99. dstack/_internal/server/services/requirements/__init__.py +0 -0
  100. dstack/_internal/server/services/requirements/combine.py +259 -0
  101. dstack/_internal/server/services/runner/client.py +7 -0
  102. dstack/_internal/server/services/runs.py +1 -1
  103. dstack/_internal/server/services/services/__init__.py +8 -2
  104. dstack/_internal/server/services/services/autoscalers.py +2 -0
  105. dstack/_internal/server/services/ssh.py +2 -1
  106. dstack/_internal/server/services/storage/__init__.py +5 -6
  107. dstack/_internal/server/services/storage/gcs.py +49 -49
  108. dstack/_internal/server/services/storage/s3.py +52 -52
  109. dstack/_internal/server/statics/index.html +1 -1
  110. dstack/_internal/server/testing/common.py +1 -1
  111. dstack/_internal/server/utils/logging.py +3 -3
  112. dstack/_internal/server/utils/provisioning.py +3 -3
  113. dstack/_internal/utils/json_schema.py +3 -1
  114. dstack/_internal/utils/typing.py +14 -0
  115. dstack/api/_public/repos.py +21 -2
  116. dstack/api/_public/runs.py +5 -7
  117. dstack/api/server/__init__.py +17 -19
  118. dstack/api/server/_gpus.py +2 -1
  119. dstack/api/server/_group.py +4 -3
  120. dstack/api/server/_repos.py +20 -3
  121. dstack/plugins/builtin/rest_plugin/_plugin.py +1 -0
  122. dstack/version.py +1 -1
  123. {dstack-0.19.25.dist-info → dstack-0.19.26.dist-info}/METADATA +1 -1
  124. {dstack-0.19.25.dist-info → dstack-0.19.26.dist-info}/RECORD +127 -124
  125. dstack/api/huggingface/__init__.py +0 -73
  126. {dstack-0.19.25.dist-info → dstack-0.19.26.dist-info}/WHEEL +0 -0
  127. {dstack-0.19.25.dist-info → dstack-0.19.26.dist-info}/entry_points.txt +0 -0
  128. {dstack-0.19.25.dist-info → dstack-0.19.26.dist-info}/licenses/LICENSE.md +0 -0
@@ -8,7 +8,6 @@ from dstack._internal.core.backends.base.configurator import (
8
8
  from dstack._internal.core.backends.tensordock import api_client
9
9
  from dstack._internal.core.backends.tensordock.backend import TensorDockBackend
10
10
  from dstack._internal.core.backends.tensordock.models import (
11
- AnyTensorDockBackendConfig,
12
11
  TensorDockBackendConfig,
13
12
  TensorDockBackendConfigWithCreds,
14
13
  TensorDockConfig,
@@ -23,7 +22,12 @@ from dstack._internal.core.models.backends.base import (
23
22
  REGIONS = []
24
23
 
25
24
 
26
- class TensorDockConfigurator(Configurator):
25
+ class TensorDockConfigurator(
26
+ Configurator[
27
+ TensorDockBackendConfig,
28
+ TensorDockBackendConfigWithCreds,
29
+ ]
30
+ ):
27
31
  TYPE = BackendType.TENSORDOCK
28
32
  BACKEND_CLASS = TensorDockBackend
29
33
 
@@ -44,12 +48,14 @@ class TensorDockConfigurator(Configurator):
44
48
  auth=TensorDockCreds.parse_obj(config.creds).json(),
45
49
  )
46
50
 
47
- def get_backend_config(
48
- self, record: BackendRecord, include_creds: bool
49
- ) -> AnyTensorDockBackendConfig:
51
+ def get_backend_config_with_creds(
52
+ self, record: BackendRecord
53
+ ) -> TensorDockBackendConfigWithCreds:
54
+ config = self._get_config(record)
55
+ return TensorDockBackendConfigWithCreds.__response__.parse_obj(config)
56
+
57
+ def get_backend_config_without_creds(self, record: BackendRecord) -> TensorDockBackendConfig:
50
58
  config = self._get_config(record)
51
- if include_creds:
52
- return TensorDockBackendConfigWithCreds.__response__.parse_obj(config)
53
59
  return TensorDockBackendConfig.__response__.parse_obj(config)
54
60
 
55
61
  def get_backend(self, record: BackendRecord) -> TensorDockBackend:
@@ -8,7 +8,6 @@ from dstack._internal.core.backends.base.configurator import (
8
8
  from dstack._internal.core.backends.vastai import api_client
9
9
  from dstack._internal.core.backends.vastai.backend import VastAIBackend
10
10
  from dstack._internal.core.backends.vastai.models import (
11
- AnyVastAIBackendConfig,
12
11
  VastAIBackendConfig,
13
12
  VastAIBackendConfigWithCreds,
14
13
  VastAIConfig,
@@ -23,7 +22,12 @@ from dstack._internal.core.models.backends.base import (
23
22
  REGIONS = []
24
23
 
25
24
 
26
- class VastAIConfigurator(Configurator):
25
+ class VastAIConfigurator(
26
+ Configurator[
27
+ VastAIBackendConfig,
28
+ VastAIBackendConfigWithCreds,
29
+ ]
30
+ ):
27
31
  TYPE = BackendType.VASTAI
28
32
  BACKEND_CLASS = VastAIBackend
29
33
 
@@ -42,12 +46,12 @@ class VastAIConfigurator(Configurator):
42
46
  auth=VastAICreds.parse_obj(config.creds).json(),
43
47
  )
44
48
 
45
- def get_backend_config(
46
- self, record: BackendRecord, include_creds: bool
47
- ) -> AnyVastAIBackendConfig:
49
+ def get_backend_config_with_creds(self, record: BackendRecord) -> VastAIBackendConfigWithCreds:
50
+ config = self._get_config(record)
51
+ return VastAIBackendConfigWithCreds.__response__.parse_obj(config)
52
+
53
+ def get_backend_config_without_creds(self, record: BackendRecord) -> VastAIBackendConfig:
48
54
  config = self._get_config(record)
49
- if include_creds:
50
- return VastAIBackendConfigWithCreds.__response__.parse_obj(config)
51
55
  return VastAIBackendConfig.__response__.parse_obj(config)
52
56
 
53
57
  def get_backend(self, record: BackendRecord) -> VastAIBackend:
@@ -23,7 +23,12 @@ from dstack._internal.core.models.backends.base import (
23
23
  REGIONS = []
24
24
 
25
25
 
26
- class VultrConfigurator(Configurator):
26
+ class VultrConfigurator(
27
+ Configurator[
28
+ VultrBackendConfig,
29
+ VultrBackendConfigWithCreds,
30
+ ]
31
+ ):
27
32
  TYPE = BackendType.VULTR
28
33
  BACKEND_CLASS = VultrBackend
29
34
 
@@ -42,10 +47,12 @@ class VultrConfigurator(Configurator):
42
47
  auth=VultrCreds.parse_obj(config.creds).json(),
43
48
  )
44
49
 
45
- def get_backend_config(self, record: BackendRecord, include_creds: bool) -> VultrBackendConfig:
50
+ def get_backend_config_with_creds(self, record: BackendRecord) -> VultrBackendConfigWithCreds:
51
+ config = self._get_config(record)
52
+ return VultrBackendConfigWithCreds.__response__.parse_obj(config)
53
+
54
+ def get_backend_config_without_creds(self, record: BackendRecord) -> VultrBackendConfig:
46
55
  config = self._get_config(record)
47
- if include_creds:
48
- return VultrBackendConfigWithCreds.__response__.parse_obj(config)
49
56
  return VultrBackendConfig.__response__.parse_obj(config)
50
57
 
51
58
  def get_backend(self, record: BackendRecord) -> VultrBackend:
@@ -0,0 +1,13 @@
1
+ from typing import Optional
2
+
3
+ from dstack._internal.core.compatibility.runs import get_run_spec_excludes
4
+ from dstack._internal.core.models.common import IncludeExcludeDictType
5
+ from dstack._internal.server.schemas.gpus import ListGpusRequest
6
+
7
+
8
+ def get_list_gpus_excludes(request: ListGpusRequest) -> Optional[IncludeExcludeDictType]:
9
+ list_gpus_excludes: IncludeExcludeDictType = {}
10
+ run_spec_excludes = get_run_spec_excludes(request.run_spec)
11
+ if run_spec_excludes is not None:
12
+ list_gpus_excludes["run_spec"] = run_spec_excludes
13
+ return list_gpus_excludes
@@ -136,6 +136,7 @@ def get_run_spec_excludes(run_spec: RunSpec) -> IncludeExcludeDictType:
136
136
  configuration_excludes["schedule"] = True
137
137
  if profile is not None and profile.schedule is None:
138
138
  profile_excludes.add("schedule")
139
+ configuration_excludes["repos"] = True
139
140
 
140
141
  if configuration_excludes:
141
142
  spec_excludes["configuration"] = configuration_excludes
@@ -102,12 +102,12 @@ class RegistryAuth(CoreModel):
102
102
  password (str): The password or access token
103
103
  """
104
104
 
105
- class Config(CoreModel.Config):
106
- frozen = True
107
-
108
105
  username: Annotated[str, Field(description="The username")]
109
106
  password: Annotated[str, Field(description="The password or access token")]
110
107
 
108
+ class Config(CoreModel.Config):
109
+ frozen = True
110
+
111
111
 
112
112
  class ApplyAction(str, Enum):
113
113
  CREATE = "create" # resource is to be created or overridden
@@ -1,12 +1,13 @@
1
1
  import re
2
+ import string
2
3
  from collections import Counter
3
4
  from enum import Enum
4
5
  from pathlib import PurePosixPath
5
- from typing import Any, Dict, List, Optional, Union
6
+ from typing import Annotated, Any, Dict, List, Literal, Optional, Union
6
7
 
7
8
  import orjson
8
9
  from pydantic import Field, ValidationError, conint, constr, root_validator, validator
9
- from typing_extensions import Annotated, Literal
10
+ from typing_extensions import Self
10
11
 
11
12
  from dstack._internal.core.errors import ConfigurationError
12
13
  from dstack._internal.core.models.common import CoreModel, Duration, RegistryAuth
@@ -83,6 +84,72 @@ class PortMapping(CoreModel):
83
84
  return PortMapping(local_port=local_port, container_port=int(container_port))
84
85
 
85
86
 
87
+ class RepoSpec(CoreModel):
88
+ local_path: Annotated[
89
+ Optional[str],
90
+ Field(
91
+ description=(
92
+ "The path to the Git repo on the user's machine. Relative paths are resolved"
93
+ " relative to the parent directory of the the configuration file."
94
+ " Mutually exclusive with `url`"
95
+ )
96
+ ),
97
+ ] = None
98
+ url: Annotated[
99
+ Optional[str],
100
+ Field(description="The Git repo URL. Mutually exclusive with `local_path`"),
101
+ ] = None
102
+ branch: Annotated[
103
+ Optional[str],
104
+ Field(
105
+ description=(
106
+ "The repo branch. Defaults to the active branch for local paths"
107
+ " and the default branch for URLs"
108
+ )
109
+ ),
110
+ ] = None
111
+ hash: Annotated[
112
+ Optional[str],
113
+ Field(description="The commit hash"),
114
+ ] = None
115
+ # Not implemented, has no effect, hidden in the docs
116
+ path: str = DEFAULT_REPO_DIR
117
+
118
+ @classmethod
119
+ def parse(cls, v: str) -> Self:
120
+ is_url = False
121
+ parts = v.split(":")
122
+ if len(parts) > 1:
123
+ # Git repo, git@github.com:dstackai/dstack.git or https://github.com/dstackai/dstack
124
+ if "@" in parts[0] or parts[1].startswith("//"):
125
+ parts = [f"{parts[0]}:{parts[1]}", *parts[2:]]
126
+ is_url = True
127
+ # Windows path, e.g., `C:\path\to`, 'c:/path/to'
128
+ elif (
129
+ len(parts[0]) == 1
130
+ and parts[0] in string.ascii_letters
131
+ and parts[1][:1] in ["\\", "/"]
132
+ ):
133
+ parts = [f"{parts[0]}:{parts[1]}", *parts[2:]]
134
+ if len(parts) == 1:
135
+ if is_url:
136
+ return cls(url=parts[0])
137
+ return cls(local_path=parts[0])
138
+ if len(parts) == 2:
139
+ if is_url:
140
+ return cls(url=parts[0], path=parts[1])
141
+ return cls(local_path=parts[0], path=parts[1])
142
+ raise ValueError(f"Invalid repo: {v}")
143
+
144
+ @root_validator
145
+ def validate_local_path_or_url(cls, values):
146
+ if values["local_path"] and values["url"]:
147
+ raise ValueError("`local_path` and `url` are mutually exclusive")
148
+ if not values["local_path"] and not values["url"]:
149
+ raise ValueError("Either `local_path` or `url` must be specified")
150
+ return values
151
+
152
+
86
153
  class ScalingSpec(CoreModel):
87
154
  metric: Annotated[
88
155
  Literal["rps"],
@@ -221,7 +288,7 @@ class ProbeConfig(CoreModel):
221
288
  ),
222
289
  ] = None
223
290
  timeout: Annotated[
224
- Optional[Union[int, str]],
291
+ Optional[int],
225
292
  Field(
226
293
  description=(
227
294
  f"Maximum amount of time the HTTP request is allowed to take. Defaults to `{DEFAULT_PROBE_TIMEOUT}s`"
@@ -229,7 +296,7 @@ class ProbeConfig(CoreModel):
229
296
  ),
230
297
  ] = None
231
298
  interval: Annotated[
232
- Optional[Union[int, str]],
299
+ Optional[int],
233
300
  Field(
234
301
  description=(
235
302
  "Minimum amount of time between the end of one probe execution"
@@ -249,7 +316,19 @@ class ProbeConfig(CoreModel):
249
316
  ),
250
317
  ] = None
251
318
 
252
- @validator("timeout")
319
+ class Config(CoreModel.Config):
320
+ @staticmethod
321
+ def schema_extra(schema: Dict[str, Any]):
322
+ add_extra_schema_types(
323
+ schema["properties"]["timeout"],
324
+ extra_types=[{"type": "string"}],
325
+ )
326
+ add_extra_schema_types(
327
+ schema["properties"]["interval"],
328
+ extra_types=[{"type": "string"}],
329
+ )
330
+
331
+ @validator("timeout", pre=True)
253
332
  def parse_timeout(cls, v: Optional[Union[int, str]]) -> Optional[int]:
254
333
  if v is None:
255
334
  return v
@@ -258,7 +337,7 @@ class ProbeConfig(CoreModel):
258
337
  raise ValueError(f"Probe timeout cannot be shorter than {MIN_PROBE_TIMEOUT}s")
259
338
  return parsed
260
339
 
261
- @validator("interval")
340
+ @validator("interval", pre=True)
262
341
  def parse_interval(cls, v: Optional[Union[int, str]]) -> Optional[int]:
263
342
  if v is None:
264
343
  return v
@@ -373,22 +452,36 @@ class BaseRunConfiguration(CoreModel):
373
452
  ),
374
453
  ),
375
454
  ] = None
376
- volumes: Annotated[
377
- List[Union[MountPoint, str]], Field(description="The volumes mount points")
378
- ] = []
455
+ volumes: Annotated[List[MountPoint], Field(description="The volumes mount points")] = []
379
456
  docker: Annotated[
380
457
  Optional[bool],
381
458
  Field(
382
459
  description="Use Docker inside the container. Mutually exclusive with `image`, `python`, and `nvcc`. Overrides `privileged`"
383
460
  ),
384
461
  ] = None
462
+ repos: Annotated[
463
+ list[RepoSpec],
464
+ Field(description="The list of Git repos"),
465
+ ] = []
385
466
  files: Annotated[
386
- list[Union[FilePathMapping, str]],
467
+ list[FilePathMapping],
387
468
  Field(description="The local to container file path mappings"),
388
469
  ] = []
389
470
  # deprecated since 0.18.31; task, service -- no effect; dev-environment -- executed right before `init`
390
471
  setup: CommandsList = []
391
472
 
473
+ class Config(CoreModel.Config):
474
+ @staticmethod
475
+ def schema_extra(schema: Dict[str, Any]):
476
+ add_extra_schema_types(
477
+ schema["properties"]["volumes"]["items"],
478
+ extra_types=[{"type": "string"}],
479
+ )
480
+ add_extra_schema_types(
481
+ schema["properties"]["files"]["items"],
482
+ extra_types=[{"type": "string"}],
483
+ )
484
+
392
485
  @validator("python", pre=True, always=True)
393
486
  def convert_python(cls, v, values) -> Optional[PythonVersion]:
394
487
  if v is not None and values.get("image"):
@@ -413,18 +506,30 @@ class BaseRunConfiguration(CoreModel):
413
506
  # but it's not possible to do so without breaking backwards compatibility.
414
507
  return v
415
508
 
416
- @validator("volumes", each_item=True)
417
- def convert_volumes(cls, v) -> MountPoint:
509
+ @validator("volumes", each_item=True, pre=True)
510
+ def convert_volumes(cls, v: Union[MountPoint, str]) -> MountPoint:
418
511
  if isinstance(v, str):
419
512
  return parse_mount_point(v)
420
513
  return v
421
514
 
422
- @validator("files", each_item=True)
423
- def convert_files(cls, v) -> FilePathMapping:
515
+ @validator("files", each_item=True, pre=True)
516
+ def convert_files(cls, v: Union[FilePathMapping, str]) -> FilePathMapping:
424
517
  if isinstance(v, str):
425
518
  return FilePathMapping.parse(v)
426
519
  return v
427
520
 
521
+ @validator("repos", pre=True, each_item=True)
522
+ def convert_repos(cls, v: Union[RepoSpec, str]) -> RepoSpec:
523
+ if isinstance(v, str):
524
+ return RepoSpec.parse(v)
525
+ return v
526
+
527
+ @validator("repos")
528
+ def validate_repos(cls, v) -> RepoSpec:
529
+ if len(v) > 1:
530
+ raise ValueError("A maximum of one repo is currently supported")
531
+ return v
532
+
428
533
  @validator("user")
429
534
  def validate_user(cls, v) -> Optional[str]:
430
535
  if v is None:
@@ -444,7 +549,7 @@ class BaseRunConfiguration(CoreModel):
444
549
  raise ValueError("The value must be `sh`, `bash`, or an absolute path")
445
550
 
446
551
 
447
- class BaseRunConfigurationWithPorts(BaseRunConfiguration):
552
+ class ConfigurationWithPortsParams(CoreModel):
448
553
  ports: Annotated[
449
554
  List[Union[ValidPort, constr(regex=r"^(?:[0-9]+|\*):[0-9]+$"), PortMapping]],
450
555
  Field(description="Port numbers/mapping to expose"),
@@ -459,7 +564,7 @@ class BaseRunConfigurationWithPorts(BaseRunConfiguration):
459
564
  return v
460
565
 
461
566
 
462
- class BaseRunConfigurationWithCommands(BaseRunConfiguration):
567
+ class ConfigurationWithCommandsParams(CoreModel):
463
568
  commands: Annotated[CommandsList, Field(description="The shell commands to run")] = []
464
569
 
465
570
  @root_validator
@@ -503,10 +608,25 @@ class DevEnvironmentConfigurationParams(CoreModel):
503
608
 
504
609
 
505
610
  class DevEnvironmentConfiguration(
506
- ProfileParams, BaseRunConfigurationWithPorts, DevEnvironmentConfigurationParams
611
+ ProfileParams,
612
+ BaseRunConfiguration,
613
+ ConfigurationWithPortsParams,
614
+ DevEnvironmentConfigurationParams,
507
615
  ):
508
616
  type: Literal["dev-environment"] = "dev-environment"
509
617
 
618
+ class Config(ProfileParams.Config, BaseRunConfiguration.Config):
619
+ @staticmethod
620
+ def schema_extra(schema: Dict[str, Any]):
621
+ ProfileParams.Config.schema_extra(schema)
622
+ BaseRunConfiguration.Config.schema_extra(schema)
623
+
624
+ @validator("entrypoint")
625
+ def validate_entrypoint(cls, v: Optional[str]) -> Optional[str]:
626
+ if v is not None:
627
+ raise ValueError("entrypoint is not supported for dev-environment")
628
+ return v
629
+
510
630
 
511
631
  class TaskConfigurationParams(CoreModel):
512
632
  nodes: Annotated[int, Field(description="Number of nodes", ge=1)] = 1
@@ -514,12 +634,19 @@ class TaskConfigurationParams(CoreModel):
514
634
 
515
635
  class TaskConfiguration(
516
636
  ProfileParams,
517
- BaseRunConfigurationWithCommands,
518
- BaseRunConfigurationWithPorts,
637
+ BaseRunConfiguration,
638
+ ConfigurationWithCommandsParams,
639
+ ConfigurationWithPortsParams,
519
640
  TaskConfigurationParams,
520
641
  ):
521
642
  type: Literal["task"] = "task"
522
643
 
644
+ class Config(ProfileParams.Config, BaseRunConfiguration.Config):
645
+ @staticmethod
646
+ def schema_extra(schema: Dict[str, Any]):
647
+ ProfileParams.Config.schema_extra(schema)
648
+ BaseRunConfiguration.Config.schema_extra(schema)
649
+
523
650
 
524
651
  class ServiceConfigurationParams(CoreModel):
525
652
  port: Annotated[
@@ -547,7 +674,7 @@ class ServiceConfigurationParams(CoreModel):
547
674
  ),
548
675
  ] = STRIP_PREFIX_DEFAULT
549
676
  model: Annotated[
550
- Optional[Union[AnyModel, str]],
677
+ Optional[AnyModel],
551
678
  Field(
552
679
  description=(
553
680
  "Mapping of the model for the OpenAI-compatible endpoint provided by `dstack`."
@@ -578,6 +705,18 @@ class ServiceConfigurationParams(CoreModel):
578
705
  Field(description="List of probes used to determine job health"),
579
706
  ] = []
580
707
 
708
+ class Config(CoreModel.Config):
709
+ @staticmethod
710
+ def schema_extra(schema: Dict[str, Any]):
711
+ add_extra_schema_types(
712
+ schema["properties"]["replicas"],
713
+ extra_types=[{"type": "integer"}, {"type": "string"}],
714
+ )
715
+ add_extra_schema_types(
716
+ schema["properties"]["model"],
717
+ extra_types=[{"type": "string"}],
718
+ )
719
+
581
720
  @validator("port")
582
721
  def convert_port(cls, v) -> PortMapping:
583
722
  if isinstance(v, int):
@@ -586,7 +725,7 @@ class ServiceConfigurationParams(CoreModel):
586
725
  return PortMapping.parse(v)
587
726
  return v
588
727
 
589
- @validator("model")
728
+ @validator("model", pre=True)
590
729
  def convert_model(cls, v: Optional[Union[AnyModel, str]]) -> Optional[AnyModel]:
591
730
  if isinstance(v, str):
592
731
  return OpenAIChatModel(type="chat", name=v, format="openai")
@@ -645,17 +784,23 @@ class ServiceConfigurationParams(CoreModel):
645
784
 
646
785
 
647
786
  class ServiceConfiguration(
648
- ProfileParams, BaseRunConfigurationWithCommands, ServiceConfigurationParams
787
+ ProfileParams,
788
+ BaseRunConfiguration,
789
+ ConfigurationWithCommandsParams,
790
+ ServiceConfigurationParams,
649
791
  ):
650
792
  type: Literal["service"] = "service"
651
793
 
652
- class Config(CoreModel.Config):
794
+ class Config(
795
+ ProfileParams.Config,
796
+ BaseRunConfiguration.Config,
797
+ ServiceConfigurationParams.Config,
798
+ ):
653
799
  @staticmethod
654
800
  def schema_extra(schema: Dict[str, Any]):
655
- add_extra_schema_types(
656
- schema["properties"]["replicas"],
657
- extra_types=[{"type": "integer"}, {"type": "string"}],
658
- )
801
+ ProfileParams.Config.schema_extra(schema)
802
+ BaseRunConfiguration.Config.schema_extra(schema)
803
+ ServiceConfigurationParams.Config.schema_extra(schema)
659
804
 
660
805
 
661
806
  AnyRunConfiguration = Union[DevEnvironmentConfiguration, TaskConfiguration, ServiceConfiguration]
@@ -28,7 +28,7 @@ class FilePathMapping(CoreModel):
28
28
  Field(
29
29
  description=(
30
30
  "The path in the container. Relative paths are resolved relative to"
31
- " the repo directory (`/workflow`)"
31
+ " the repo directory"
32
32
  )
33
33
  ),
34
34
  ]
@@ -224,7 +224,7 @@ class InstanceGroupParams(CoreModel):
224
224
  Field(description="The maximum instance price per hour, in dollars", gt=0.0),
225
225
  ] = None
226
226
  idle_duration: Annotated[
227
- Optional[Union[Literal["off"], str, int]],
227
+ Optional[int],
228
228
  Field(
229
229
  description="Time to wait before terminating idle instances. Defaults to `5m` for runs and `3d` for fleets. Use `off` for unlimited duration"
230
230
  ),
@@ -243,6 +243,10 @@ class InstanceGroupParams(CoreModel):
243
243
  schema["properties"]["nodes"],
244
244
  extra_types=[{"type": "integer"}, {"type": "string"}],
245
245
  )
246
+ add_extra_schema_types(
247
+ schema["properties"]["idle_duration"],
248
+ extra_types=[{"type": "string"}],
249
+ )
246
250
 
247
251
  _validate_idle_duration = validator("idle_duration", pre=True, allow_reuse=True)(
248
252
  parse_idle_duration
@@ -9,6 +9,7 @@ from dstack._internal.core.models.backends.base import BackendType
9
9
  from dstack._internal.core.models.common import CoreModel, Duration
10
10
  from dstack._internal.utils.common import list_enum_values_for_annotation
11
11
  from dstack._internal.utils.cron import validate_cron
12
+ from dstack._internal.utils.json_schema import add_extra_schema_types
12
13
  from dstack._internal.utils.json_utils import pydantic_orjson_dumps_with_indent
13
14
  from dstack._internal.utils.tags import tags_validator
14
15
 
@@ -61,15 +62,17 @@ def parse_duration(v: Optional[Union[int, str]]) -> Optional[int]:
61
62
  return Duration.parse(v)
62
63
 
63
64
 
64
- def parse_max_duration(v: Optional[Union[int, str, bool]]) -> Optional[Union[str, int]]:
65
+ def parse_max_duration(v: Optional[Union[int, str, bool]]) -> Optional[Union[Literal["off"], int]]:
65
66
  return parse_off_duration(v)
66
67
 
67
68
 
68
- def parse_stop_duration(v: Optional[Union[int, str, bool]]) -> Optional[Union[str, int]]:
69
+ def parse_stop_duration(
70
+ v: Optional[Union[int, str, bool]],
71
+ ) -> Optional[Union[Literal["off"], int]]:
69
72
  return parse_off_duration(v)
70
73
 
71
74
 
72
- def parse_off_duration(v: Optional[Union[int, str, bool]]) -> Optional[Union[str, int]]:
75
+ def parse_off_duration(v: Optional[Union[int, str, bool]]) -> Optional[Union[Literal["off"], int]]:
73
76
  if v == "off" or v is False:
74
77
  return "off"
75
78
  if v is True:
@@ -77,7 +80,7 @@ def parse_off_duration(v: Optional[Union[int, str, bool]]) -> Optional[Union[str
77
80
  return parse_duration(v)
78
81
 
79
82
 
80
- def parse_idle_duration(v: Optional[Union[int, str]]) -> Optional[Union[str, int]]:
83
+ def parse_idle_duration(v: Optional[Union[int, str]]) -> Optional[int]:
81
84
  if v == "off" or v == -1:
82
85
  return -1
83
86
  return parse_duration(v)
@@ -121,10 +124,18 @@ class ProfileRetry(CoreModel):
121
124
  ),
122
125
  ] = None
123
126
  duration: Annotated[
124
- Optional[Union[int, str]],
127
+ Optional[int],
125
128
  Field(description="The maximum period of retrying the run, e.g., `4h` or `1d`"),
126
129
  ] = None
127
130
 
131
+ class Config(CoreModel.Config):
132
+ @staticmethod
133
+ def schema_extra(schema: Dict[str, Any]):
134
+ add_extra_schema_types(
135
+ schema["properties"]["duration"],
136
+ extra_types=[{"type": "string"}],
137
+ )
138
+
128
139
  _validate_duration = validator("duration", pre=True, allow_reuse=True)(parse_duration)
129
140
 
130
141
  @root_validator
@@ -151,7 +162,7 @@ class UtilizationPolicy(CoreModel):
151
162
  ),
152
163
  ]
153
164
  time_window: Annotated[
154
- Union[int, str],
165
+ int,
155
166
  Field(
156
167
  description=(
157
168
  "The time window of metric samples taking into account to measure utilization"
@@ -160,6 +171,14 @@ class UtilizationPolicy(CoreModel):
160
171
  ),
161
172
  ]
162
173
 
174
+ class Config(CoreModel.Config):
175
+ @staticmethod
176
+ def schema_extra(schema: Dict[str, Any]):
177
+ add_extra_schema_types(
178
+ schema["properties"]["time_window"],
179
+ extra_types=[{"type": "string"}],
180
+ )
181
+
163
182
  @validator("time_window", pre=True)
164
183
  def validate_time_window(cls, v: Union[int, str]) -> int:
165
184
  v = parse_duration(v)
@@ -247,7 +266,7 @@ class ProfileParams(CoreModel):
247
266
  Field(description="The policy for resubmitting the run. Defaults to `false`"),
248
267
  ] = None
249
268
  max_duration: Annotated[
250
- Optional[Union[Literal["off"], str, int, bool]],
269
+ Optional[Union[Literal["off"], int]],
251
270
  Field(
252
271
  description=(
253
272
  "The maximum duration of a run (e.g., `2h`, `1d`, etc)."
@@ -257,7 +276,7 @@ class ProfileParams(CoreModel):
257
276
  ),
258
277
  ] = None
259
278
  stop_duration: Annotated[
260
- Optional[Union[Literal["off"], str, int, bool]],
279
+ Optional[Union[Literal["off"], int]],
261
280
  Field(
262
281
  description=(
263
282
  "The maximum duration of a run graceful stopping."
@@ -282,7 +301,7 @@ class ProfileParams(CoreModel):
282
301
  ),
283
302
  ] = None
284
303
  idle_duration: Annotated[
285
- Optional[Union[Literal["off"], str, int]],
304
+ Optional[int],
286
305
  Field(
287
306
  description=(
288
307
  "Time to wait before terminating idle instances."
@@ -347,6 +366,18 @@ class ProfileParams(CoreModel):
347
366
  del schema["properties"]["retry_policy"]
348
367
  del schema["properties"]["termination_policy"]
349
368
  del schema["properties"]["termination_idle_time"]
369
+ add_extra_schema_types(
370
+ schema["properties"]["max_duration"],
371
+ extra_types=[{"type": "boolean"}, {"type": "string"}],
372
+ )
373
+ add_extra_schema_types(
374
+ schema["properties"]["stop_duration"],
375
+ extra_types=[{"type": "boolean"}, {"type": "string"}],
376
+ )
377
+ add_extra_schema_types(
378
+ schema["properties"]["idle_duration"],
379
+ extra_types=[{"type": "string"}],
380
+ )
350
381
 
351
382
  _validate_max_duration = validator("max_duration", pre=True, allow_reuse=True)(
352
383
  parse_max_duration
@@ -366,7 +397,7 @@ class ProfileProps(CoreModel):
366
397
  Field(
367
398
  description="The name of the profile that can be passed as `--profile` to `dstack apply`"
368
399
  ),
369
- ]
400
+ ] = ""
370
401
  default: Annotated[
371
402
  bool, Field(description="If set to true, `dstack apply` will use this profile by default.")
372
403
  ] = False
@@ -382,7 +413,6 @@ class ProfilesConfig(CoreModel):
382
413
  class Config(CoreModel.Config):
383
414
  json_loads = orjson.loads
384
415
  json_dumps = pydantic_orjson_dumps_with_indent
385
-
386
416
  schema_extra = {"$schema": "http://json-schema.org/draft-07/schema#"}
387
417
 
388
418
  def default(self) -> Optional[Profile]: