Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions mkdocs/docs/concepts/presets.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,13 +91,15 @@ To stop a creation and its runs, use `dstack preset stop`.
export DSTACK_AGENT_ANTHROPIC_API_KEY=...
```

By default, the agent uses `claude-opus-4-8`. It doesn't set an effort level, so the `claude` CLI default applies. To override them, set:
By default, the agent sets neither a model nor an effort level, so the `claude` CLI's built-in defaults apply. To override them, set:

```shell
export DSTACK_AGENT_ANTHROPIC_MODEL=claude-opus-5
export DSTACK_AGENT_ANTHROPIC_MODEL=claude-fable-5-1
export DSTACK_AGENT_CLAUDE_EFFORT=max
```

See the [Models overview](https://platform.claude.com/docs/en/models/overview) for the available models and their IDs.

Supported effort levels are `low`, `medium`, `high`, `xhigh`, and `max`.

??? info "Presets directory"
Expand Down
2 changes: 1 addition & 1 deletion mkdocs/docs/reference/cli/dstack/preset.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ Preset creation uses the existing `claude` login unless
| --- | --- |
| `DSTACK_AGENT_ANTHROPIC_API_KEY` | Anthropic API key used by the agent. |
| `DSTACK_AGENT_CLAUDE_PATH` | `claude` executable name or path. Defaults to `claude` from `PATH`. |
| `DSTACK_AGENT_ANTHROPIC_MODEL` | Claude model used by the agent. Defaults to `claude-opus-4-8`. |
| `DSTACK_AGENT_ANTHROPIC_MODEL` | Claude model used by the agent. If unset, the `claude` CLI's built-in default is used. |
| `DSTACK_AGENT_CLAUDE_EFFORT` | Claude effort level: `low`, `medium`, `high`, `xhigh`, or `max`. If unset, the `claude` CLI default is used. |

Agent progress is written to `agent.log` under `~/.dstack/presets/<preset-id>/`,
Expand Down
2 changes: 1 addition & 1 deletion mkdocs/docs/reference/env.md
Original file line number Diff line number Diff line change
Expand Up @@ -219,5 +219,5 @@ $ find ~/.dstack/logs/cli/
- `DSTACK_PROJECT`{ #DSTACK_PROJECT } – Has the same effect as `--project`. Defaults to `None`.
- `DSTACK_AGENT_ANTHROPIC_API_KEY`{ #DSTACK_AGENT_ANTHROPIC_API_KEY } – The Anthropic API key used by the preset agent. If unset, the existing `claude` login is used.
- `DSTACK_AGENT_CLAUDE_PATH`{ #DSTACK_AGENT_CLAUDE_PATH } – The `claude` executable name or path used by the preset agent. Defaults to `claude` from `PATH`.
- `DSTACK_AGENT_ANTHROPIC_MODEL`{ #DSTACK_AGENT_ANTHROPIC_MODEL } – The Claude model used by the preset agent. Defaults to `claude-opus-4-8`.
- `DSTACK_AGENT_ANTHROPIC_MODEL`{ #DSTACK_AGENT_ANTHROPIC_MODEL } – The Claude model used by the preset agent. If unset, the `claude` CLI's built-in default is used.
- `DSTACK_AGENT_CLAUDE_EFFORT`{ #DSTACK_AGENT_CLAUDE_EFFORT } – The Claude effort level used by the preset agent. Can be `low`, `medium`, `high`, `xhigh`, or `max`. If unset, the `claude` CLI default is used.
14 changes: 14 additions & 0 deletions src/dstack/_internal/cli/models/preset_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,18 @@ class PresetSessionState(CoreModel):
run: Optional[PresetSessionRun]


class PresetAgentInfo(CoreModel):
"""`agent.json`: how the claude agent was launched. A debug record."""

executable: str
version: Optional[str]
auth_status: str
# None is the claude CLI's default.
effort: Optional[str]
# Reported by claude on its init line; None until then.
model: Optional[str]


class ClaudeStreamEvent(CoreModel):
"""One line of the claude CLI's `--output-format stream-json`. Not our format:
unknown fields are dropped and omitted fields default."""
Expand All @@ -135,6 +147,8 @@ class ClaudeStreamEvent(CoreModel):
# Identifies the claude conversation, so an interrupted creation can be
# resumed with `claude --resume`. Not every line carries it.
session_id: Optional[str] = None
# The model claude runs with, e.g. "claude-opus-5[1m]". Set on the init line only.
model: Optional[str] = None


class ClaudeResultEvent(ClaudeStreamEvent):
Expand Down
23 changes: 18 additions & 5 deletions src/dstack/_internal/cli/services/presets/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
AnyClaudeStreamEvent,
ClaudeResultEvent,
PresetAgentFailure,
PresetAgentInfo,
PresetAgentSuccess,
PresetSessionProcess,
)
Expand Down Expand Up @@ -90,9 +91,9 @@
class ClaudeAuth:
api_key: Optional[str]
executable: str
# None uses the claude CLI's own default.
# None means the flag is not passed and claude uses its own default.
effort: Optional[ClaudeEffort]
model: str
model: Optional[str]


@dataclass
Expand Down Expand Up @@ -131,6 +132,16 @@ def _get_claude_auth_status(auth: "ClaudeAuth") -> str:
return "unknown"


def get_agent_info(auth: ClaudeAuth) -> PresetAgentInfo:
return PresetAgentInfo(
executable=auth.executable,
version=_get_claude_version(auth),
auth_status=_get_claude_auth_status(auth),
effort=auth.effort,
model=None,
)


def get_claude_auth() -> ClaudeAuth:
api_key = os.getenv("DSTACK_AGENT_ANTHROPIC_API_KEY") or None
configured_path = os.getenv("DSTACK_AGENT_CLAUDE_PATH") or "claude"
Expand All @@ -146,7 +157,7 @@ def get_claude_auth() -> ClaudeAuth:
api_key=api_key,
executable=executable,
effort=effort,
model=os.getenv("DSTACK_AGENT_ANTHROPIC_MODEL", "claude-opus-4-8"),
model=os.getenv("DSTACK_AGENT_ANTHROPIC_MODEL") or None,
)


Expand Down Expand Up @@ -351,8 +362,6 @@ def _build_claude_command(*, auth: ClaudeAuth, resume_session_id: Optional[str])
"Task,NotebookEdit",
"--permission-mode",
"bypassPermissions",
"--model",
auth.model,
"--json-schema",
json.dumps(_get_report_json_schema()),
]
Expand All @@ -362,6 +371,8 @@ def _build_claude_command(*, auth: ClaudeAuth, resume_session_id: Optional[str])
command[2:2] = ["--bare"]
if auth.effort is not None:
command[2:2] = ["--effort", auth.effort]
if auth.model is not None:
command[2:2] = ["--model", auth.model]
if resume_session_id is not None:
command += ["--resume", resume_session_id]
return command
Expand Down Expand Up @@ -559,6 +570,8 @@ async def _read_process_stream(
if output.session_id is None and event.session_id:
output.session_id = event.session_id
session.record_claude_session_id(event.session_id)
if event.model:
session.record_agent_model(event.model)
if event.type == "assistant":
output.made_progress = True
if not isinstance(event, ClaudeResultEvent):
Expand Down
5 changes: 3 additions & 2 deletions src/dstack/_internal/cli/services/presets/create.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
PresetAgentProcessOutput,
attach_preset_agent,
build_preset_agent_env,
get_agent_info,
get_claude_auth,
run_preset_agent,
terminate_agent_process,
Expand Down Expand Up @@ -636,8 +637,8 @@ async def _create_preset(
# while the listing and `--previous` read constraints from the session dir.
session.write_constraints(constraints_text)
session.write_prompt(prompt)
if setup.auth is not None:
session.write_agent_info(setup.auth)
if setup.auth is not None:
session.write_agent_info(get_agent_info(setup.auth))
try:
if mode == "attach":
process_output = await attach_preset_agent(
Expand Down
40 changes: 21 additions & 19 deletions src/dstack/_internal/cli/services/presets/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,15 @@
from dataclasses import dataclass, field
from datetime import datetime, timezone
from pathlib import Path
from typing import TYPE_CHECKING, Any, Iterator, Optional, Sequence
from typing import Any, Iterator, Optional, Sequence

import psutil
import yaml
from pydantic import ValidationError
from rich.text import Text

from dstack._internal.cli.models.preset_agent import (
PresetAgentInfo,
PresetSessionFinalize,
PresetSessionProcess,
PresetSessionRun,
Expand All @@ -28,14 +29,10 @@
from dstack._internal.cli.utils.common import console
from dstack._internal.compat import IS_WINDOWS
from dstack._internal.core.errors import CLIError
from dstack._internal.core.models.common import validate_extra_ignore
from dstack._internal.core.models.common import validate_extra_ignore, validate_json_extra_ignore
from dstack._internal.core.models.configurations import PresetConfiguration
from dstack._internal.utils.common import get_dstack_dir

if TYPE_CHECKING:
from dstack._internal.cli.services.presets.agent import ClaudeAuth


_PROGRESS_FILENAME = "progress.jsonl"
_RUNS_FILENAME = "runs.jsonl"
TRIALS_DIRNAME = "trials"
Expand All @@ -45,6 +42,7 @@
_CONSTRAINTS_FILENAME = "constraints.json"
_FINAL_REPORT_FILENAME = "final_report.json"
_SESSION_FILENAME = "session.json"
_AGENT_INFO_FILENAME = "agent.json"
_USER_PROMPT_FILENAME = "user_prompt.md"


Expand Down Expand Up @@ -114,21 +112,25 @@ def write_constraints(self, constraints_text: str) -> None:
def write_final_report(self, report_text: str) -> None:
_write_private_text(self.path / _FINAL_REPORT_FILENAME, report_text)

def write_agent_info(self, auth: "ClaudeAuth") -> None:
from dstack._internal.cli.services.presets.agent import (
_get_claude_auth_status,
_get_claude_version,
def write_agent_info(self, info: PresetAgentInfo) -> None:
_write_private_text(
self.path / _AGENT_INFO_FILENAME,
info.model_dump_json(indent=2) + "\n",
)

# `agent.json`: a debug document written once and read by nothing, so it
# is a plain dump, not a model.
info = {
"executable": auth.executable,
"version": _get_claude_version(auth),
"model": {"name": auth.model, "effort": auth.effort or "default"},
"auth_status": _get_claude_auth_status(auth),
}
_write_private_text(self.path / "agent.json", json.dumps(info, indent=2) + "\n")
def read_agent_info(self) -> Optional[PresetAgentInfo]:
try:
text = (self.path / _AGENT_INFO_FILENAME).read_text(encoding="utf-8")
return validate_json_extra_ignore(PresetAgentInfo, text)
except (OSError, ValidationError):
return None

def record_agent_model(self, model: str) -> None:
info = self.read_agent_info()
if info is None:
return
info.model = model
self.write_agent_info(info)

def append_log(self, line: str) -> None:
if not self._log_enabled:
Expand Down
Loading
Loading