Skip to content

Commit 779777b

Browse files
peterschmidt85Andrey Cheptsovclaude
authored
[Presets] Typed models and stored format overhaul (#4150)
Co-authored-by: Andrey Cheptsov <andrey.cheptsov@github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 7767e22 commit 779777b

30 files changed

Lines changed: 2519 additions & 1758 deletions

src/dstack/_internal/cli/commands/preset.py

Lines changed: 42 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import argparse
2+
import io
23
import os
4+
import sys
35
import time
4-
from contextlib import suppress
6+
from contextlib import redirect_stderr, suppress
57
from pathlib import Path
68

79
from argcomplete import FilesCompleter # type: ignore[attr-defined]
@@ -14,6 +16,7 @@
1416
PresetListOutput,
1517
)
1618
from dstack._internal.cli.services.completion import ProjectNameCompleter
19+
from dstack._internal.cli.services.configurators import APPLY_STDIN_NAME
1720
from dstack._internal.cli.services.presets.apply import apply_preset
1821
from dstack._internal.cli.services.presets.create import (
1922
CreationStopped,
@@ -28,13 +31,14 @@
2831
)
2932
from dstack._internal.cli.services.presets.output import get_presets_table, print_presets
3033
from dstack._internal.cli.services.presets.session import (
31-
list_agent_sessions,
32-
load_resumable_agent_session,
34+
list_preset_sessions,
35+
load_resumable_session,
3336
resolve_session_ref,
3437
)
3538
from dstack._internal.cli.services.presets.store import (
3639
PresetStore,
3740
load_preset_configuration,
41+
parse_preset_configuration,
3842
resolve_preset_prompt,
3943
)
4044
from dstack._internal.cli.services.profile import (
@@ -251,9 +255,13 @@ def _list(self, args: argparse.Namespace) -> None:
251255
limit=args.limit,
252256
)
253257
return
258+
# The store warns about unreadable presets on stderr once per read;
259+
# inside Live that would tear the render on every refresh. The first
260+
# read happens before Live starts so warnings print once, above the
261+
# table; refreshes read with stderr suppressed.
262+
presets, sessions = self._list_presets_and_sessions(base=base, repo=repo)
254263
with Live(console=console, refresh_per_second=LIVE_TABLE_REFRESH_RATE_PER_SEC) as live:
255264
while True:
256-
presets, sessions = self._list_presets_and_sessions(base=base, repo=repo)
257265
live.update(
258266
get_presets_table(
259267
presets,
@@ -264,13 +272,15 @@ def _list(self, args: argparse.Namespace) -> None:
264272
)
265273
)
266274
time.sleep(LIVE_TABLE_PROVISION_INTERVAL_SECS)
275+
with redirect_stderr(io.StringIO()):
276+
presets, sessions = self._list_presets_and_sessions(base=base, repo=repo)
267277

268278
def _list_presets_and_sessions(
269279
self, *, base: str | None, repo: str | None
270280
) -> tuple[list[Preset], list[dict]]:
271281
self._reconcile()
272282
presets = PresetStore().list()
273-
sessions = list_agent_sessions()
283+
sessions = list_preset_sessions()
274284
if base or repo:
275285
repo_to_base = {preset.model: preset.base for preset in presets}
276286
presets = _filter_presets(presets, base=base, repo=repo)
@@ -282,13 +292,14 @@ def _list_presets_and_sessions(
282292
return presets, sessions
283293

284294
def _create(self, args: argparse.Namespace) -> None:
285-
configuration_path, configuration = load_preset_configuration(args.configuration_file)
295+
_check_stdin_configuration_confirmable(args)
296+
_, configuration = _read_configuration_arg(args.configuration_file)
286297
configuration = _get_effective_configuration(configuration, args, require_name=False)
287-
user_prompt = resolve_preset_prompt(configuration, configuration_path)
298+
user_prompt = resolve_preset_prompt(configuration, _prompt_base(args.configuration_file))
288299
store = PresetStore()
289300
resume_session = None
290301
if getattr(args, "resume", None):
291-
resume_session = load_resumable_agent_session(args.resume)
302+
resume_session = load_resumable_session(args.resume)
292303
if getattr(args, "trials", None) is not None:
293304
console.print(
294305
"[warning]--trials is ignored when resuming: "
@@ -368,7 +379,7 @@ def _get(self, args: argparse.Namespace) -> None:
368379

369380
def _apply(self, args: argparse.Namespace) -> None:
370381
self._reconcile()
371-
configuration_path, configuration = load_preset_configuration(args.configuration_file)
382+
configuration_path, configuration = _read_configuration_arg(args.configuration_file)
372383
configuration = _get_effective_configuration(configuration, args)
373384
apply_preset(
374385
api=Client.from_config(project_name=args.project),
@@ -532,6 +543,28 @@ def _confirm_preset_creation(store: PresetStore, name: str | None, *, assume_yes
532543
return True
533544

534545

546+
def _read_configuration_arg(configuration_file: str) -> tuple[str, PresetConfiguration]:
547+
"""`-f <path>`, or `-f -` for stdin — the same convention as `dstack apply`."""
548+
if configuration_file == APPLY_STDIN_NAME:
549+
return APPLY_STDIN_NAME, parse_preset_configuration(sys.stdin)
550+
path = Path(configuration_file)
551+
return str(path.resolve()), load_preset_configuration(path)
552+
553+
554+
def _prompt_base(configuration_file: str) -> Path:
555+
"""Prompt files resolve relative to the configuration file; cwd for stdin."""
556+
if configuration_file == APPLY_STDIN_NAME:
557+
return Path.cwd()
558+
return Path(configuration_file).resolve().parent
559+
560+
561+
def _check_stdin_configuration_confirmable(args: argparse.Namespace) -> None:
562+
# Same rule as `dstack apply`: the confirmation prompt cannot read from a
563+
# stdin that is the configuration itself.
564+
if not args.yes and args.configuration_file == APPLY_STDIN_NAME:
565+
raise CLIError("Cannot read configuration from stdin if -y/--yes is not specified")
566+
567+
535568
def _get_effective_configuration(
536569
configuration: PresetConfiguration,
537570
args: argparse.Namespace,

src/dstack/_internal/cli/models/configurations.py

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -135,14 +135,15 @@ class PresetConfiguration(
135135
),
136136
] = None
137137
min_context_length: Annotated[
138-
Optional[PositiveInt], Field(description="The minimum required context length")
138+
Optional[PositiveInt],
139+
Field(description="The minimum required context length. Required for creation"),
139140
] = None
140141
max_ttft: Annotated[
141142
Optional[PositiveInt],
142143
Field(
143144
description=(
144145
"The maximum p50 time to first token, in milliseconds, that any benchmark"
145-
" may report"
146+
" may report. Required for creation"
146147
)
147148
),
148149
] = None
@@ -151,7 +152,7 @@ class PresetConfiguration(
151152
Field(
152153
description=(
153154
"The number of benchmarked trials during preset creation"
154-
" before the best one is promoted"
155+
" before the best one is promoted. Required for creation"
155156
)
156157
),
157158
] = None
@@ -168,7 +169,8 @@ class PresetConfiguration(
168169
Optional[PositiveInt],
169170
Field(
170171
description=(
171-
"The number of simultaneous requests used for benchmarks during preset creation"
172+
"The number of simultaneous requests used for benchmarks during preset"
173+
" creation. Required for creation"
172174
)
173175
),
174176
] = None
@@ -182,7 +184,7 @@ class PresetConfiguration(
182184
),
183185
] = None
184186
output_tokens: Annotated[
185-
Optional[PositiveInt],
187+
Optional[Annotated[int, Field(ge=2)]],
186188
Field(
187189
description=(
188190
"The number of output tokens per request used for benchmarks during"
@@ -191,7 +193,7 @@ class PresetConfiguration(
191193
),
192194
] = None
193195
shared_prefix_tokens: Annotated[
194-
Optional[PositiveInt],
196+
Optional[Annotated[int, Field(ge=0)]],
195197
Field(
196198
description=(
197199
"How many of `input_tokens` are a prefix identical in every benchmark request,"
@@ -338,13 +340,23 @@ class PresetConstraints(CoreModel):
338340
max_ttft: PositiveInt
339341
trials_num: PositiveInt
340342
concurrency: PositiveInt
341-
input_tokens: Optional[PositiveInt] = None
342-
output_tokens: Optional[PositiveInt] = None
343-
shared_prefix_tokens: Optional[int] = None
344-
dataset: Optional[str] = None
345-
baseline: bool = False
346-
fleets: list[str] = Field(min_length=1)
347-
env: list[str] = []
343+
baseline: bool
344+
fleets: Annotated[list[str], Field(min_length=1)]
345+
env: list[str]
346+
347+
348+
class PresetRandomConstraints(PresetConstraints):
349+
"""Constraints for the synthetic `random` dataset, which the request shape defines."""
350+
351+
input_tokens: PositiveInt
352+
output_tokens: Annotated[int, Field(ge=2)]
353+
shared_prefix_tokens: Annotated[int, Field(ge=0)]
354+
355+
356+
class PresetDatasetConstraints(PresetConstraints):
357+
"""Constraints for a named dataset, which defines its own request shape."""
358+
359+
dataset: str
348360

349361

350362
def _validate_model(value: Any, *, field: str) -> str:

0 commit comments

Comments
 (0)