dstack 0.19.25rc1__py3-none-any.whl → 0.19.27__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 (161) 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 +293 -58
  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 +35 -48
  27. dstack/_internal/core/backends/amddevcloud/__init__.py +1 -0
  28. dstack/_internal/core/backends/amddevcloud/backend.py +16 -0
  29. dstack/_internal/core/backends/amddevcloud/compute.py +5 -0
  30. dstack/_internal/core/backends/amddevcloud/configurator.py +29 -0
  31. dstack/_internal/core/backends/aws/compute.py +6 -1
  32. dstack/_internal/core/backends/aws/configurator.py +11 -7
  33. dstack/_internal/core/backends/azure/configurator.py +11 -7
  34. dstack/_internal/core/backends/base/compute.py +33 -5
  35. dstack/_internal/core/backends/base/configurator.py +25 -13
  36. dstack/_internal/core/backends/base/offers.py +2 -0
  37. dstack/_internal/core/backends/cloudrift/configurator.py +13 -7
  38. dstack/_internal/core/backends/configurators.py +15 -0
  39. dstack/_internal/core/backends/cudo/configurator.py +11 -7
  40. dstack/_internal/core/backends/datacrunch/compute.py +5 -1
  41. dstack/_internal/core/backends/datacrunch/configurator.py +13 -7
  42. dstack/_internal/core/backends/digitalocean/__init__.py +1 -0
  43. dstack/_internal/core/backends/digitalocean/backend.py +16 -0
  44. dstack/_internal/core/backends/digitalocean/compute.py +5 -0
  45. dstack/_internal/core/backends/digitalocean/configurator.py +31 -0
  46. dstack/_internal/core/backends/digitalocean_base/__init__.py +1 -0
  47. dstack/_internal/core/backends/digitalocean_base/api_client.py +104 -0
  48. dstack/_internal/core/backends/digitalocean_base/backend.py +5 -0
  49. dstack/_internal/core/backends/digitalocean_base/compute.py +173 -0
  50. dstack/_internal/core/backends/digitalocean_base/configurator.py +57 -0
  51. dstack/_internal/core/backends/digitalocean_base/models.py +43 -0
  52. dstack/_internal/core/backends/gcp/compute.py +32 -8
  53. dstack/_internal/core/backends/gcp/configurator.py +11 -7
  54. dstack/_internal/core/backends/hotaisle/api_client.py +25 -33
  55. dstack/_internal/core/backends/hotaisle/compute.py +1 -6
  56. dstack/_internal/core/backends/hotaisle/configurator.py +13 -7
  57. dstack/_internal/core/backends/kubernetes/configurator.py +13 -7
  58. dstack/_internal/core/backends/lambdalabs/configurator.py +11 -7
  59. dstack/_internal/core/backends/models.py +7 -0
  60. dstack/_internal/core/backends/nebius/compute.py +1 -8
  61. dstack/_internal/core/backends/nebius/configurator.py +11 -7
  62. dstack/_internal/core/backends/nebius/resources.py +21 -11
  63. dstack/_internal/core/backends/oci/compute.py +4 -5
  64. dstack/_internal/core/backends/oci/configurator.py +11 -7
  65. dstack/_internal/core/backends/runpod/configurator.py +11 -7
  66. dstack/_internal/core/backends/template/configurator.py.jinja +11 -7
  67. dstack/_internal/core/backends/tensordock/configurator.py +13 -7
  68. dstack/_internal/core/backends/vastai/configurator.py +11 -7
  69. dstack/_internal/core/backends/vultr/compute.py +1 -5
  70. dstack/_internal/core/backends/vultr/configurator.py +11 -4
  71. dstack/_internal/core/compatibility/fleets.py +5 -0
  72. dstack/_internal/core/compatibility/gpus.py +13 -0
  73. dstack/_internal/core/compatibility/runs.py +9 -1
  74. dstack/_internal/core/models/backends/base.py +5 -1
  75. dstack/_internal/core/models/common.py +3 -3
  76. dstack/_internal/core/models/configurations.py +191 -32
  77. dstack/_internal/core/models/files.py +1 -1
  78. dstack/_internal/core/models/fleets.py +80 -3
  79. dstack/_internal/core/models/profiles.py +41 -11
  80. dstack/_internal/core/models/resources.py +46 -42
  81. dstack/_internal/core/models/runs.py +28 -5
  82. dstack/_internal/core/services/configs/__init__.py +6 -3
  83. dstack/_internal/core/services/profiles.py +2 -2
  84. dstack/_internal/core/services/repos.py +86 -79
  85. dstack/_internal/core/services/ssh/ports.py +1 -1
  86. dstack/_internal/proxy/lib/deps.py +6 -2
  87. dstack/_internal/server/app.py +22 -17
  88. dstack/_internal/server/background/tasks/process_fleets.py +109 -13
  89. dstack/_internal/server/background/tasks/process_gateways.py +4 -1
  90. dstack/_internal/server/background/tasks/process_instances.py +22 -73
  91. dstack/_internal/server/background/tasks/process_probes.py +1 -1
  92. dstack/_internal/server/background/tasks/process_running_jobs.py +12 -4
  93. dstack/_internal/server/background/tasks/process_runs.py +3 -1
  94. dstack/_internal/server/background/tasks/process_submitted_jobs.py +67 -44
  95. dstack/_internal/server/background/tasks/process_terminating_jobs.py +2 -2
  96. dstack/_internal/server/background/tasks/process_volumes.py +1 -1
  97. dstack/_internal/server/db.py +8 -4
  98. dstack/_internal/server/migrations/versions/2498ab323443_add_fleetmodel_consolidation_attempt_.py +44 -0
  99. dstack/_internal/server/models.py +6 -2
  100. dstack/_internal/server/routers/gpus.py +1 -6
  101. dstack/_internal/server/schemas/runner.py +11 -0
  102. dstack/_internal/server/services/backends/__init__.py +14 -8
  103. dstack/_internal/server/services/backends/handlers.py +6 -1
  104. dstack/_internal/server/services/docker.py +5 -5
  105. dstack/_internal/server/services/fleets.py +37 -38
  106. dstack/_internal/server/services/gateways/__init__.py +2 -0
  107. dstack/_internal/server/services/gateways/client.py +5 -2
  108. dstack/_internal/server/services/gateways/connection.py +1 -1
  109. dstack/_internal/server/services/gpus.py +50 -49
  110. dstack/_internal/server/services/instances.py +44 -4
  111. dstack/_internal/server/services/jobs/__init__.py +15 -4
  112. dstack/_internal/server/services/jobs/configurators/base.py +53 -17
  113. dstack/_internal/server/services/jobs/configurators/dev.py +9 -4
  114. dstack/_internal/server/services/jobs/configurators/extensions/cursor.py +6 -8
  115. dstack/_internal/server/services/jobs/configurators/extensions/vscode.py +7 -9
  116. dstack/_internal/server/services/jobs/configurators/service.py +1 -3
  117. dstack/_internal/server/services/jobs/configurators/task.py +3 -3
  118. dstack/_internal/server/services/locking.py +5 -5
  119. dstack/_internal/server/services/logging.py +10 -2
  120. dstack/_internal/server/services/logs/__init__.py +8 -6
  121. dstack/_internal/server/services/logs/aws.py +330 -327
  122. dstack/_internal/server/services/logs/filelog.py +7 -6
  123. dstack/_internal/server/services/logs/gcp.py +141 -139
  124. dstack/_internal/server/services/plugins.py +1 -1
  125. dstack/_internal/server/services/projects.py +2 -5
  126. dstack/_internal/server/services/proxy/repo.py +5 -1
  127. dstack/_internal/server/services/requirements/__init__.py +0 -0
  128. dstack/_internal/server/services/requirements/combine.py +259 -0
  129. dstack/_internal/server/services/runner/client.py +7 -0
  130. dstack/_internal/server/services/runs.py +17 -1
  131. dstack/_internal/server/services/services/__init__.py +8 -2
  132. dstack/_internal/server/services/services/autoscalers.py +2 -0
  133. dstack/_internal/server/services/ssh.py +2 -1
  134. dstack/_internal/server/services/storage/__init__.py +5 -6
  135. dstack/_internal/server/services/storage/gcs.py +49 -49
  136. dstack/_internal/server/services/storage/s3.py +52 -52
  137. dstack/_internal/server/statics/index.html +1 -1
  138. dstack/_internal/server/statics/{main-d151b300fcac3933213d.js → main-4eecc75fbe64067eb1bc.js} +1146 -899
  139. dstack/_internal/server/statics/{main-d151b300fcac3933213d.js.map → main-4eecc75fbe64067eb1bc.js.map} +1 -1
  140. dstack/_internal/server/statics/{main-aec4762350e34d6fbff9.css → main-56191c63d516fd0041c4.css} +1 -1
  141. dstack/_internal/server/testing/common.py +7 -4
  142. dstack/_internal/server/utils/logging.py +3 -3
  143. dstack/_internal/server/utils/provisioning.py +3 -3
  144. dstack/_internal/utils/json_schema.py +3 -1
  145. dstack/_internal/utils/path.py +8 -1
  146. dstack/_internal/utils/ssh.py +7 -0
  147. dstack/_internal/utils/typing.py +14 -0
  148. dstack/api/_public/repos.py +62 -8
  149. dstack/api/_public/runs.py +19 -8
  150. dstack/api/server/__init__.py +17 -19
  151. dstack/api/server/_gpus.py +2 -1
  152. dstack/api/server/_group.py +4 -3
  153. dstack/api/server/_repos.py +20 -3
  154. dstack/plugins/builtin/rest_plugin/_plugin.py +1 -0
  155. dstack/version.py +1 -1
  156. {dstack-0.19.25rc1.dist-info → dstack-0.19.27.dist-info}/METADATA +2 -2
  157. {dstack-0.19.25rc1.dist-info → dstack-0.19.27.dist-info}/RECORD +160 -142
  158. dstack/api/huggingface/__init__.py +0 -73
  159. {dstack-0.19.25rc1.dist-info → dstack-0.19.27.dist-info}/WHEEL +0 -0
  160. {dstack-0.19.25rc1.dist-info → dstack-0.19.27.dist-info}/entry_points.txt +0 -0
  161. {dstack-0.19.25rc1.dist-info → dstack-0.19.27.dist-info}/licenses/LICENSE.md +0 -0
@@ -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
@@ -33,7 +34,7 @@ STRIP_PREFIX_DEFAULT = True
33
34
  RUN_PRIOTIRY_MIN = 0
34
35
  RUN_PRIOTIRY_MAX = 100
35
36
  RUN_PRIORITY_DEFAULT = 0
36
- DEFAULT_REPO_DIR = "/workflow"
37
+ LEGACY_REPO_DIR = "/workflow"
37
38
  MIN_PROBE_TIMEOUT = 1
38
39
  MIN_PROBE_INTERVAL = 1
39
40
  DEFAULT_PROBE_URL = "/"
@@ -83,6 +84,87 @@ 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
+ path: Annotated[
116
+ Optional[str],
117
+ Field(
118
+ description=(
119
+ "The repo path inside the run container. Relative paths are resolved"
120
+ f" relative to the working directory. Defaults to `{LEGACY_REPO_DIR}`"
121
+ )
122
+ ),
123
+ ] = None
124
+
125
+ @classmethod
126
+ def parse(cls, v: str) -> Self:
127
+ is_url = False
128
+ parts = v.split(":")
129
+ if len(parts) > 1:
130
+ # Git repo, git@github.com:dstackai/dstack.git or https://github.com/dstackai/dstack
131
+ if "@" in parts[0] or parts[1].startswith("//"):
132
+ parts = [f"{parts[0]}:{parts[1]}", *parts[2:]]
133
+ is_url = True
134
+ # Windows path, e.g., `C:\path\to`, 'c:/path/to'
135
+ elif (
136
+ len(parts[0]) == 1
137
+ and parts[0] in string.ascii_letters
138
+ and parts[1][:1] in ["\\", "/"]
139
+ ):
140
+ parts = [f"{parts[0]}:{parts[1]}", *parts[2:]]
141
+ if len(parts) == 1:
142
+ if is_url:
143
+ return cls(url=parts[0])
144
+ return cls(local_path=parts[0])
145
+ if len(parts) == 2:
146
+ if is_url:
147
+ return cls(url=parts[0], path=parts[1])
148
+ return cls(local_path=parts[0], path=parts[1])
149
+ raise ValueError(f"Invalid repo: {v}")
150
+
151
+ @root_validator
152
+ def validate_local_path_or_url(cls, values):
153
+ if values["local_path"] and values["url"]:
154
+ raise ValueError("`local_path` and `url` are mutually exclusive")
155
+ if not values["local_path"] and not values["url"]:
156
+ raise ValueError("Either `local_path` or `url` must be specified")
157
+ return values
158
+
159
+ @validator("path")
160
+ def validate_path(cls, v: Optional[str]) -> Optional[str]:
161
+ if v is None:
162
+ return v
163
+ if v.startswith("~") and PurePosixPath(v).parts[0] != "~":
164
+ raise ValueError("`~username` syntax is not supported")
165
+ return v
166
+
167
+
86
168
  class ScalingSpec(CoreModel):
87
169
  metric: Annotated[
88
170
  Literal["rps"],
@@ -221,7 +303,7 @@ class ProbeConfig(CoreModel):
221
303
  ),
222
304
  ] = None
223
305
  timeout: Annotated[
224
- Optional[Union[int, str]],
306
+ Optional[int],
225
307
  Field(
226
308
  description=(
227
309
  f"Maximum amount of time the HTTP request is allowed to take. Defaults to `{DEFAULT_PROBE_TIMEOUT}s`"
@@ -229,7 +311,7 @@ class ProbeConfig(CoreModel):
229
311
  ),
230
312
  ] = None
231
313
  interval: Annotated[
232
- Optional[Union[int, str]],
314
+ Optional[int],
233
315
  Field(
234
316
  description=(
235
317
  "Minimum amount of time between the end of one probe execution"
@@ -249,7 +331,19 @@ class ProbeConfig(CoreModel):
249
331
  ),
250
332
  ] = None
251
333
 
252
- @validator("timeout")
334
+ class Config(CoreModel.Config):
335
+ @staticmethod
336
+ def schema_extra(schema: Dict[str, Any]):
337
+ add_extra_schema_types(
338
+ schema["properties"]["timeout"],
339
+ extra_types=[{"type": "string"}],
340
+ )
341
+ add_extra_schema_types(
342
+ schema["properties"]["interval"],
343
+ extra_types=[{"type": "string"}],
344
+ )
345
+
346
+ @validator("timeout", pre=True)
253
347
  def parse_timeout(cls, v: Optional[Union[int, str]]) -> Optional[int]:
254
348
  if v is None:
255
349
  return v
@@ -258,7 +352,7 @@ class ProbeConfig(CoreModel):
258
352
  raise ValueError(f"Probe timeout cannot be shorter than {MIN_PROBE_TIMEOUT}s")
259
353
  return parsed
260
354
 
261
- @validator("interval")
355
+ @validator("interval", pre=True)
262
356
  def parse_interval(cls, v: Optional[Union[int, str]]) -> Optional[int]:
263
357
  if v is None:
264
358
  return v
@@ -301,7 +395,7 @@ class BaseRunConfiguration(CoreModel):
301
395
  Field(
302
396
  description=(
303
397
  "The user inside the container, `user_name_or_id[:group_name_or_id]`"
304
- " (e.g., `ubuntu`, `1000:1000`). Defaults to the default `image` user"
398
+ " (e.g., `ubuntu`, `1000:1000`). Defaults to the default user from the `image`"
305
399
  )
306
400
  ),
307
401
  ] = None
@@ -311,9 +405,8 @@ class BaseRunConfiguration(CoreModel):
311
405
  Optional[str],
312
406
  Field(
313
407
  description=(
314
- "The path to the working directory inside the container."
315
- f" It's specified relative to the repository directory (`{DEFAULT_REPO_DIR}`) and should be inside it."
316
- ' Defaults to `"."` '
408
+ "The absolute path to the working directory inside the container."
409
+ f" Defaults to `{LEGACY_REPO_DIR}`"
317
410
  )
318
411
  ),
319
412
  ] = None
@@ -373,22 +466,36 @@ class BaseRunConfiguration(CoreModel):
373
466
  ),
374
467
  ),
375
468
  ] = None
376
- volumes: Annotated[
377
- List[Union[MountPoint, str]], Field(description="The volumes mount points")
378
- ] = []
469
+ volumes: Annotated[List[MountPoint], Field(description="The volumes mount points")] = []
379
470
  docker: Annotated[
380
471
  Optional[bool],
381
472
  Field(
382
473
  description="Use Docker inside the container. Mutually exclusive with `image`, `python`, and `nvcc`. Overrides `privileged`"
383
474
  ),
384
475
  ] = None
476
+ repos: Annotated[
477
+ list[RepoSpec],
478
+ Field(description="The list of Git repos"),
479
+ ] = []
385
480
  files: Annotated[
386
- list[Union[FilePathMapping, str]],
481
+ list[FilePathMapping],
387
482
  Field(description="The local to container file path mappings"),
388
483
  ] = []
389
484
  # deprecated since 0.18.31; task, service -- no effect; dev-environment -- executed right before `init`
390
485
  setup: CommandsList = []
391
486
 
487
+ class Config(CoreModel.Config):
488
+ @staticmethod
489
+ def schema_extra(schema: Dict[str, Any]):
490
+ add_extra_schema_types(
491
+ schema["properties"]["volumes"]["items"],
492
+ extra_types=[{"type": "string"}],
493
+ )
494
+ add_extra_schema_types(
495
+ schema["properties"]["files"]["items"],
496
+ extra_types=[{"type": "string"}],
497
+ )
498
+
392
499
  @validator("python", pre=True, always=True)
393
500
  def convert_python(cls, v, values) -> Optional[PythonVersion]:
394
501
  if v is not None and values.get("image"):
@@ -413,18 +520,30 @@ class BaseRunConfiguration(CoreModel):
413
520
  # but it's not possible to do so without breaking backwards compatibility.
414
521
  return v
415
522
 
416
- @validator("volumes", each_item=True)
417
- def convert_volumes(cls, v) -> MountPoint:
523
+ @validator("volumes", each_item=True, pre=True)
524
+ def convert_volumes(cls, v: Union[MountPoint, str]) -> MountPoint:
418
525
  if isinstance(v, str):
419
526
  return parse_mount_point(v)
420
527
  return v
421
528
 
422
- @validator("files", each_item=True)
423
- def convert_files(cls, v) -> FilePathMapping:
529
+ @validator("files", each_item=True, pre=True)
530
+ def convert_files(cls, v: Union[FilePathMapping, str]) -> FilePathMapping:
424
531
  if isinstance(v, str):
425
532
  return FilePathMapping.parse(v)
426
533
  return v
427
534
 
535
+ @validator("repos", pre=True, each_item=True)
536
+ def convert_repos(cls, v: Union[RepoSpec, str]) -> RepoSpec:
537
+ if isinstance(v, str):
538
+ return RepoSpec.parse(v)
539
+ return v
540
+
541
+ @validator("repos")
542
+ def validate_repos(cls, v) -> RepoSpec:
543
+ if len(v) > 1:
544
+ raise ValueError("A maximum of one repo is currently supported")
545
+ return v
546
+
428
547
  @validator("user")
429
548
  def validate_user(cls, v) -> Optional[str]:
430
549
  if v is None:
@@ -444,7 +563,7 @@ class BaseRunConfiguration(CoreModel):
444
563
  raise ValueError("The value must be `sh`, `bash`, or an absolute path")
445
564
 
446
565
 
447
- class BaseRunConfigurationWithPorts(BaseRunConfiguration):
566
+ class ConfigurationWithPortsParams(CoreModel):
448
567
  ports: Annotated[
449
568
  List[Union[ValidPort, constr(regex=r"^(?:[0-9]+|\*):[0-9]+$"), PortMapping]],
450
569
  Field(description="Port numbers/mapping to expose"),
@@ -459,7 +578,7 @@ class BaseRunConfigurationWithPorts(BaseRunConfiguration):
459
578
  return v
460
579
 
461
580
 
462
- class BaseRunConfigurationWithCommands(BaseRunConfiguration):
581
+ class ConfigurationWithCommandsParams(CoreModel):
463
582
  commands: Annotated[CommandsList, Field(description="The shell commands to run")] = []
464
583
 
465
584
  @root_validator
@@ -503,10 +622,25 @@ class DevEnvironmentConfigurationParams(CoreModel):
503
622
 
504
623
 
505
624
  class DevEnvironmentConfiguration(
506
- ProfileParams, BaseRunConfigurationWithPorts, DevEnvironmentConfigurationParams
625
+ ProfileParams,
626
+ BaseRunConfiguration,
627
+ ConfigurationWithPortsParams,
628
+ DevEnvironmentConfigurationParams,
507
629
  ):
508
630
  type: Literal["dev-environment"] = "dev-environment"
509
631
 
632
+ class Config(ProfileParams.Config, BaseRunConfiguration.Config):
633
+ @staticmethod
634
+ def schema_extra(schema: Dict[str, Any]):
635
+ ProfileParams.Config.schema_extra(schema)
636
+ BaseRunConfiguration.Config.schema_extra(schema)
637
+
638
+ @validator("entrypoint")
639
+ def validate_entrypoint(cls, v: Optional[str]) -> Optional[str]:
640
+ if v is not None:
641
+ raise ValueError("entrypoint is not supported for dev-environment")
642
+ return v
643
+
510
644
 
511
645
  class TaskConfigurationParams(CoreModel):
512
646
  nodes: Annotated[int, Field(description="Number of nodes", ge=1)] = 1
@@ -514,12 +648,19 @@ class TaskConfigurationParams(CoreModel):
514
648
 
515
649
  class TaskConfiguration(
516
650
  ProfileParams,
517
- BaseRunConfigurationWithCommands,
518
- BaseRunConfigurationWithPorts,
651
+ BaseRunConfiguration,
652
+ ConfigurationWithCommandsParams,
653
+ ConfigurationWithPortsParams,
519
654
  TaskConfigurationParams,
520
655
  ):
521
656
  type: Literal["task"] = "task"
522
657
 
658
+ class Config(ProfileParams.Config, BaseRunConfiguration.Config):
659
+ @staticmethod
660
+ def schema_extra(schema: Dict[str, Any]):
661
+ ProfileParams.Config.schema_extra(schema)
662
+ BaseRunConfiguration.Config.schema_extra(schema)
663
+
523
664
 
524
665
  class ServiceConfigurationParams(CoreModel):
525
666
  port: Annotated[
@@ -547,7 +688,7 @@ class ServiceConfigurationParams(CoreModel):
547
688
  ),
548
689
  ] = STRIP_PREFIX_DEFAULT
549
690
  model: Annotated[
550
- Optional[Union[AnyModel, str]],
691
+ Optional[AnyModel],
551
692
  Field(
552
693
  description=(
553
694
  "Mapping of the model for the OpenAI-compatible endpoint provided by `dstack`."
@@ -578,6 +719,18 @@ class ServiceConfigurationParams(CoreModel):
578
719
  Field(description="List of probes used to determine job health"),
579
720
  ] = []
580
721
 
722
+ class Config(CoreModel.Config):
723
+ @staticmethod
724
+ def schema_extra(schema: Dict[str, Any]):
725
+ add_extra_schema_types(
726
+ schema["properties"]["replicas"],
727
+ extra_types=[{"type": "integer"}, {"type": "string"}],
728
+ )
729
+ add_extra_schema_types(
730
+ schema["properties"]["model"],
731
+ extra_types=[{"type": "string"}],
732
+ )
733
+
581
734
  @validator("port")
582
735
  def convert_port(cls, v) -> PortMapping:
583
736
  if isinstance(v, int):
@@ -586,7 +739,7 @@ class ServiceConfigurationParams(CoreModel):
586
739
  return PortMapping.parse(v)
587
740
  return v
588
741
 
589
- @validator("model")
742
+ @validator("model", pre=True)
590
743
  def convert_model(cls, v: Optional[Union[AnyModel, str]]) -> Optional[AnyModel]:
591
744
  if isinstance(v, str):
592
745
  return OpenAIChatModel(type="chat", name=v, format="openai")
@@ -645,17 +798,23 @@ class ServiceConfigurationParams(CoreModel):
645
798
 
646
799
 
647
800
  class ServiceConfiguration(
648
- ProfileParams, BaseRunConfigurationWithCommands, ServiceConfigurationParams
801
+ ProfileParams,
802
+ BaseRunConfiguration,
803
+ ConfigurationWithCommandsParams,
804
+ ServiceConfigurationParams,
649
805
  ):
650
806
  type: Literal["service"] = "service"
651
807
 
652
- class Config(CoreModel.Config):
808
+ class Config(
809
+ ProfileParams.Config,
810
+ BaseRunConfiguration.Config,
811
+ ServiceConfigurationParams.Config,
812
+ ):
653
813
  @staticmethod
654
814
  def schema_extra(schema: Dict[str, Any]):
655
- add_extra_schema_types(
656
- schema["properties"]["replicas"],
657
- extra_types=[{"type": "integer"}, {"type": "string"}],
658
- )
815
+ ProfileParams.Config.schema_extra(schema)
816
+ BaseRunConfiguration.Config.schema_extra(schema)
817
+ ServiceConfigurationParams.Config.schema_extra(schema)
659
818
 
660
819
 
661
820
  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 working directory"
32
32
  )
33
33
  ),
34
34
  ]
@@ -19,7 +19,7 @@ from dstack._internal.core.models.profiles import (
19
19
  TerminationPolicy,
20
20
  parse_idle_duration,
21
21
  )
22
- from dstack._internal.core.models.resources import Range, ResourcesSpec
22
+ from dstack._internal.core.models.resources import ResourcesSpec
23
23
  from dstack._internal.utils.common import list_enum_values_for_annotation
24
24
  from dstack._internal.utils.json_schema import add_extra_schema_types
25
25
  from dstack._internal.utils.tags import tags_validator
@@ -141,6 +141,67 @@ class SSHParams(CoreModel):
141
141
  return value
142
142
 
143
143
 
144
+ class FleetNodesSpec(CoreModel):
145
+ min: Annotated[
146
+ int, Field(description=("The minimum number of instances to maintain in the fleet"))
147
+ ]
148
+ target: Annotated[
149
+ int,
150
+ Field(
151
+ description=(
152
+ "The number of instances to provision on fleet apply. `min` <= `target` <= `max`"
153
+ " Defaults to `min`"
154
+ )
155
+ ),
156
+ ]
157
+ max: Annotated[
158
+ Optional[int],
159
+ Field(
160
+ description=(
161
+ "The maximum number of instances allowed in the fleet. Unlimited if not specified"
162
+ )
163
+ ),
164
+ ] = None
165
+
166
+ def dict(self, *args, **kwargs) -> Dict:
167
+ # super() does not work with pydantic-duality
168
+ res = CoreModel.dict(self, *args, **kwargs)
169
+ # For backward compatibility with old clients
170
+ # that do not ignore extra fields due to https://github.com/dstackai/dstack/issues/3066
171
+ if "target" in res and res["target"] == res["min"]:
172
+ del res["target"]
173
+ return res
174
+
175
+ @root_validator(pre=True)
176
+ def set_min_and_target_defaults(cls, values):
177
+ min_ = values.get("min")
178
+ target = values.get("target")
179
+ if min_ is None:
180
+ values["min"] = 0
181
+ if target is None:
182
+ values["target"] = values["min"]
183
+ return values
184
+
185
+ @validator("min")
186
+ def validate_min(cls, v: int) -> int:
187
+ if v < 0:
188
+ raise ValueError("min cannot be negative")
189
+ return v
190
+
191
+ @root_validator(skip_on_failure=True)
192
+ def _post_validate_ranges(cls, values):
193
+ min_ = values["min"]
194
+ target = values["target"]
195
+ max_ = values.get("max")
196
+ if target < min_:
197
+ raise ValueError("target must not be be less than min")
198
+ if max_ is not None and max_ < min_:
199
+ raise ValueError("max must not be less than min")
200
+ if max_ is not None and max_ < target:
201
+ raise ValueError("max must not be less than target")
202
+ return values
203
+
204
+
144
205
  class InstanceGroupParams(CoreModel):
145
206
  env: Annotated[
146
207
  Env,
@@ -151,7 +212,9 @@ class InstanceGroupParams(CoreModel):
151
212
  Field(description="The parameters for adding instances via SSH"),
152
213
  ] = None
153
214
 
154
- nodes: Annotated[Optional[Range[int]], Field(description="The number of instances")] = None
215
+ nodes: Annotated[
216
+ Optional[FleetNodesSpec], Field(description="The number of instances in cloud fleet")
217
+ ] = None
155
218
  placement: Annotated[
156
219
  Optional[InstanceGroupPlacement],
157
220
  Field(description="The placement of instances: `any` or `cluster`"),
@@ -224,7 +287,7 @@ class InstanceGroupParams(CoreModel):
224
287
  Field(description="The maximum instance price per hour, in dollars", gt=0.0),
225
288
  ] = None
226
289
  idle_duration: Annotated[
227
- Optional[Union[Literal["off"], str, int]],
290
+ Optional[int],
228
291
  Field(
229
292
  description="Time to wait before terminating idle instances. Defaults to `5m` for runs and `3d` for fleets. Use `off` for unlimited duration"
230
293
  ),
@@ -243,6 +306,20 @@ class InstanceGroupParams(CoreModel):
243
306
  schema["properties"]["nodes"],
244
307
  extra_types=[{"type": "integer"}, {"type": "string"}],
245
308
  )
309
+ add_extra_schema_types(
310
+ schema["properties"]["idle_duration"],
311
+ extra_types=[{"type": "string"}],
312
+ )
313
+
314
+ @validator("nodes", pre=True)
315
+ def parse_nodes(cls, v: Optional[Union[dict, str]]) -> Optional[dict]:
316
+ if isinstance(v, str) and ".." in v:
317
+ v = v.replace(" ", "")
318
+ min, max = v.split("..")
319
+ return dict(min=min or None, max=max or None)
320
+ elif isinstance(v, str) or isinstance(v, int):
321
+ return dict(min=v, max=v)
322
+ return v
246
323
 
247
324
  _validate_idle_duration = validator("idle_duration", pre=True, allow_reuse=True)(
248
325
  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]: