envloop-cli 1.7.0__tar.gz → 1.12.0__tar.gz
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.
- {envloop_cli-1.7.0 → envloop_cli-1.12.0}/PKG-INFO +26 -3
- {envloop_cli-1.7.0 → envloop_cli-1.12.0}/README.pypi.md +25 -2
- {envloop_cli-1.7.0 → envloop_cli-1.12.0}/envloop/agent_runs.py +7 -3
- {envloop_cli-1.7.0 → envloop_cli-1.12.0}/envloop/auth_cli.py +1 -1
- {envloop_cli-1.7.0 → envloop_cli-1.12.0}/envloop/cli.py +87 -32
- {envloop_cli-1.7.0 → envloop_cli-1.12.0}/envloop/cli_errors.py +152 -104
- {envloop_cli-1.7.0 → envloop_cli-1.12.0}/envloop/cli_output.py +15 -16
- {envloop_cli-1.7.0 → envloop_cli-1.12.0}/envloop/client.py +16 -3
- {envloop_cli-1.7.0 → envloop_cli-1.12.0}/envloop/http.py +78 -25
- {envloop_cli-1.7.0 → envloop_cli-1.12.0}/envloop/job_logs.py +6 -2
- envloop_cli-1.12.0/envloop/job_spec_cli.py +138 -0
- {envloop_cli-1.7.0 → envloop_cli-1.12.0}/envloop/rsi_cli.py +16 -5
- envloop_cli-1.12.0/envloop/terminal.py +77 -0
- {envloop_cli-1.7.0 → envloop_cli-1.12.0}/envloop/train.py +9 -4
- envloop_cli-1.12.0/envloop/train_log_cli.py +128 -0
- envloop_cli-1.12.0/envloop/train_progress.py +51 -0
- {envloop_cli-1.7.0 → envloop_cli-1.12.0}/pyproject.toml +1 -1
- envloop_cli-1.7.0/envloop/train_log_cli.py +0 -97
- {envloop_cli-1.7.0 → envloop_cli-1.12.0}/.gitignore +0 -0
- {envloop_cli-1.7.0 → envloop_cli-1.12.0}/envloop/__init__.py +0 -0
- {envloop_cli-1.7.0 → envloop_cli-1.12.0}/envloop/auth_login.py +0 -0
- {envloop_cli-1.7.0 → envloop_cli-1.12.0}/envloop/auth_workspace.py +0 -0
- {envloop_cli-1.7.0 → envloop_cli-1.12.0}/envloop/config.py +0 -0
- {envloop_cli-1.7.0 → envloop_cli-1.12.0}/envloop/inference.py +0 -0
- {envloop_cli-1.7.0 → envloop_cli-1.12.0}/envloop/lifecycle_cli.py +0 -0
- {envloop_cli-1.7.0 → envloop_cli-1.12.0}/envloop/navigation_resources.py +0 -0
- {envloop_cli-1.7.0 → envloop_cli-1.12.0}/envloop/profile_secrets.py +0 -0
- {envloop_cli-1.7.0 → envloop_cli-1.12.0}/envloop/profile_updates.py +0 -0
- {envloop_cli-1.7.0 → envloop_cli-1.12.0}/envloop/resource_cli.py +0 -0
- {envloop_cli-1.7.0 → envloop_cli-1.12.0}/envloop/resources.py +0 -0
- {envloop_cli-1.7.0 → envloop_cli-1.12.0}/envloop/sandbox.py +0 -0
- {envloop_cli-1.7.0 → envloop_cli-1.12.0}/envloop/train_runner_cli.py +0 -0
- {envloop_cli-1.7.0 → envloop_cli-1.12.0}/envloop/training/__init__.py +0 -0
- {envloop_cli-1.7.0 → envloop_cli-1.12.0}/envloop/utils/__init__.py +0 -0
- {envloop_cli-1.7.0 → envloop_cli-1.12.0}/envloop/utils/logging.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: envloop-cli
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.12.0
|
|
4
4
|
Summary: EnvLoop CLI and Python SDK for jobs, training, checkpoints and inference
|
|
5
5
|
Project-URL: Homepage, https://envloop.ai
|
|
6
6
|
Project-URL: Documentation, https://docs.envloop.ai
|
|
@@ -81,6 +81,19 @@ chmod 700 "${ENVLOOP_HOME:-$HOME/.envloop}"
|
|
|
81
81
|
|
|
82
82
|
An insecure directory causes `el` to fail with `CONFIG_INSECURE_PATH`.
|
|
83
83
|
|
|
84
|
+
## Errors
|
|
85
|
+
|
|
86
|
+
Errors go to stderr as an error code, a short explanation and the applicable
|
|
87
|
+
HTTP status, without a Hint. Interactive terminals use a red heading and a dim
|
|
88
|
+
HTTP status. Redirected output, pipes, `TERM=dumb`, or a nonempty `NO_COLOR`
|
|
89
|
+
disable color. `--json` stdout remains JSON.
|
|
90
|
+
|
|
91
|
+
```text
|
|
92
|
+
Error: PROFILE_NOT_FOUND
|
|
93
|
+
The Profile was not found or is not readable in the current scope.
|
|
94
|
+
HTTP 404
|
|
95
|
+
```
|
|
96
|
+
|
|
84
97
|
## Explore your project
|
|
85
98
|
|
|
86
99
|
```bash
|
|
@@ -153,13 +166,16 @@ Prepare a recipe-supported `REQUEST.json` using the [SFT quickstart](https://doc
|
|
|
153
166
|
Training uses billable resources. Check your project permissions, balance and data format, then submit once:
|
|
154
167
|
|
|
155
168
|
```bash
|
|
156
|
-
el job submit --type train --
|
|
169
|
+
el job submit --type train --spec REQUEST.json
|
|
157
170
|
el job get JOB_ID
|
|
158
171
|
el job log JOB_ID --follow
|
|
159
172
|
el job result JOB_ID
|
|
160
173
|
el checkpoint list --job-id JOB_ID
|
|
161
174
|
```
|
|
162
175
|
|
|
176
|
+
`job submit --spec` reads the complete Job specification JSON file for Train or Inference.
|
|
177
|
+
The former `job submit --file` option is removed; the JSON payload is unchanged.
|
|
178
|
+
|
|
163
179
|
Replace `JOB_ID` with the receipt's value. Read the training status and logs, then find the registered checkpoint after success.
|
|
164
180
|
Ctrl-C ends log reading; to stop training, use `el job stop JOB_ID` and check its state and resource cleanup.
|
|
165
181
|
See [Checkpoints](https://docs.envloop.ai/training/checkpoints) to download or delete saved weights.
|
|
@@ -182,7 +198,7 @@ Choose a published Runner readable in your project. Save this request as `gpu-ru
|
|
|
182
198
|
```
|
|
183
199
|
|
|
184
200
|
```bash
|
|
185
|
-
el job submit --type train --
|
|
201
|
+
el job submit --type train --spec gpu-run.json
|
|
186
202
|
el job get JOB_ID
|
|
187
203
|
el job log JOB_ID --follow
|
|
188
204
|
el job result JOB_ID
|
|
@@ -193,3 +209,10 @@ Replace `JOB_ID` with the returned value. The example has two independent Trials
|
|
|
193
209
|
The request file does not depend on GPU shortcut flags. If `el job submit --help` lists both `--trails` and `--gput-type`, you can instead use the shortcuts in [GPU Runner training](https://docs.envloop.ai/training/runner). Choose one submission method to avoid duplicate runs. The SDK accepts the same dictionary through `client.training_jobs.submit(request)`.
|
|
194
210
|
|
|
195
211
|
The platform starts the published Revision’s `main.sh` / `main.py` using the Runner image; it does not upload local code. An optional command can only supply a declared `run_command` parameter. If this mode or GPU type is unavailable, contact your project administrator. Use `el job stop JOB_ID` to stop every Trial, then inspect each Trial’s `cleanup_confirmed` and download the uploaded artifacts you need.
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
CLI output uses compact timestamps and highlights statuses and log levels by default on terminals.
|
|
215
|
+
Use `el --no-color job list` or append `--no-color` to a command to disable highlighting, including errors.
|
|
216
|
+
`NO_COLOR=1`, `TERM=dumb`, pipes, and redirected output also disable colors. JSON preserves original values.
|
|
217
|
+
List times use the local timezone; Job logs retain the recorded timezone, with full precision and offsets available through `--json`.
|
|
218
|
+
`el job run live RUN --follow --json` emits complete response frames as NDJSON; Ctrl-C stops reading without stopping the Run.
|
|
@@ -68,6 +68,19 @@ chmod 700 "${ENVLOOP_HOME:-$HOME/.envloop}"
|
|
|
68
68
|
|
|
69
69
|
An insecure directory causes `el` to fail with `CONFIG_INSECURE_PATH`.
|
|
70
70
|
|
|
71
|
+
## Errors
|
|
72
|
+
|
|
73
|
+
Errors go to stderr as an error code, a short explanation and the applicable
|
|
74
|
+
HTTP status, without a Hint. Interactive terminals use a red heading and a dim
|
|
75
|
+
HTTP status. Redirected output, pipes, `TERM=dumb`, or a nonempty `NO_COLOR`
|
|
76
|
+
disable color. `--json` stdout remains JSON.
|
|
77
|
+
|
|
78
|
+
```text
|
|
79
|
+
Error: PROFILE_NOT_FOUND
|
|
80
|
+
The Profile was not found or is not readable in the current scope.
|
|
81
|
+
HTTP 404
|
|
82
|
+
```
|
|
83
|
+
|
|
71
84
|
## Explore your project
|
|
72
85
|
|
|
73
86
|
```bash
|
|
@@ -140,13 +153,16 @@ Prepare a recipe-supported `REQUEST.json` using the [SFT quickstart](https://doc
|
|
|
140
153
|
Training uses billable resources. Check your project permissions, balance and data format, then submit once:
|
|
141
154
|
|
|
142
155
|
```bash
|
|
143
|
-
el job submit --type train --
|
|
156
|
+
el job submit --type train --spec REQUEST.json
|
|
144
157
|
el job get JOB_ID
|
|
145
158
|
el job log JOB_ID --follow
|
|
146
159
|
el job result JOB_ID
|
|
147
160
|
el checkpoint list --job-id JOB_ID
|
|
148
161
|
```
|
|
149
162
|
|
|
163
|
+
`job submit --spec` reads the complete Job specification JSON file for Train or Inference.
|
|
164
|
+
The former `job submit --file` option is removed; the JSON payload is unchanged.
|
|
165
|
+
|
|
150
166
|
Replace `JOB_ID` with the receipt's value. Read the training status and logs, then find the registered checkpoint after success.
|
|
151
167
|
Ctrl-C ends log reading; to stop training, use `el job stop JOB_ID` and check its state and resource cleanup.
|
|
152
168
|
See [Checkpoints](https://docs.envloop.ai/training/checkpoints) to download or delete saved weights.
|
|
@@ -169,7 +185,7 @@ Choose a published Runner readable in your project. Save this request as `gpu-ru
|
|
|
169
185
|
```
|
|
170
186
|
|
|
171
187
|
```bash
|
|
172
|
-
el job submit --type train --
|
|
188
|
+
el job submit --type train --spec gpu-run.json
|
|
173
189
|
el job get JOB_ID
|
|
174
190
|
el job log JOB_ID --follow
|
|
175
191
|
el job result JOB_ID
|
|
@@ -180,3 +196,10 @@ Replace `JOB_ID` with the returned value. The example has two independent Trials
|
|
|
180
196
|
The request file does not depend on GPU shortcut flags. If `el job submit --help` lists both `--trails` and `--gput-type`, you can instead use the shortcuts in [GPU Runner training](https://docs.envloop.ai/training/runner). Choose one submission method to avoid duplicate runs. The SDK accepts the same dictionary through `client.training_jobs.submit(request)`.
|
|
181
197
|
|
|
182
198
|
The platform starts the published Revision’s `main.sh` / `main.py` using the Runner image; it does not upload local code. An optional command can only supply a declared `run_command` parameter. If this mode or GPU type is unavailable, contact your project administrator. Use `el job stop JOB_ID` to stop every Trial, then inspect each Trial’s `cleanup_confirmed` and download the uploaded artifacts you need.
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
CLI output uses compact timestamps and highlights statuses and log levels by default on terminals.
|
|
202
|
+
Use `el --no-color job list` or append `--no-color` to a command to disable highlighting, including errors.
|
|
203
|
+
`NO_COLOR=1`, `TERM=dumb`, pipes, and redirected output also disable colors. JSON preserves original values.
|
|
204
|
+
List times use the local timezone; Job logs retain the recorded timezone, with full precision and offsets available through `--json`.
|
|
205
|
+
`el job run live RUN --follow --json` emits complete response frames as NDJSON; Ctrl-C stops reading without stopping the Run.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""Agent runs: a read-only view of Train Jobs grouped by their agent's run label.
|
|
2
2
|
|
|
3
3
|
The platform stores no run object. Agents tag every Train Job they create with
|
|
4
|
-
``
|
|
4
|
+
``tags`` (the RSI loop: ``rsi_run``, ``rsi_round``, ``rsi_checkpoint``...);
|
|
5
5
|
this module pages through ``GET /v1/job`` and derives runs from those tags,
|
|
6
6
|
using the same rules as the Console's agent runs page.
|
|
7
7
|
"""
|
|
@@ -34,7 +34,11 @@ class AgentRunError(RuntimeError):
|
|
|
34
34
|
|
|
35
35
|
|
|
36
36
|
def _metadata(job) -> dict:
|
|
37
|
-
metadata = job.get("
|
|
37
|
+
metadata = job.get("tags")
|
|
38
|
+
if not isinstance(metadata, dict):
|
|
39
|
+
metadata = job.get("labels")
|
|
40
|
+
if not isinstance(metadata, dict):
|
|
41
|
+
metadata = job.get("user_metadata")
|
|
38
42
|
return metadata if isinstance(metadata, dict) else {}
|
|
39
43
|
|
|
40
44
|
|
|
@@ -111,7 +115,7 @@ def _job_view(job) -> dict:
|
|
|
111
115
|
"lora_rank": job.get("lora_rank"),
|
|
112
116
|
"model_id": job.get("model_id"),
|
|
113
117
|
"checkpoint": _checkpoint(job),
|
|
114
|
-
"
|
|
118
|
+
"tags": metadata,
|
|
115
119
|
}
|
|
116
120
|
|
|
117
121
|
|
|
@@ -282,7 +282,7 @@ def run_auth(args, parser):
|
|
|
282
282
|
report["check"] = {401: "authentication_rejected", 403: "authorization_denied"}.get(
|
|
283
283
|
getattr(error, "status_code", None), safe_error_code(error))
|
|
284
284
|
_print(report, args.json)
|
|
285
|
-
print(format_error(error
|
|
285
|
+
print(format_error(error), file=sys.stderr)
|
|
286
286
|
return 1
|
|
287
287
|
_print(report, getattr(args, "json", False))
|
|
288
288
|
return 0
|
|
@@ -39,6 +39,8 @@ class HelpArgumentParser(argparse.ArgumentParser):
|
|
|
39
39
|
def __init__(self, *args, **kwargs):
|
|
40
40
|
kwargs.setdefault('formatter_class', CommandHelpFormatter)
|
|
41
41
|
super().__init__(*args, **kwargs)
|
|
42
|
+
self.add_argument("--no-color", action="store_true", default=argparse.SUPPRESS,
|
|
43
|
+
help="Disable terminal highlighting")
|
|
42
44
|
self.set_defaults(_help_parser=self)
|
|
43
45
|
self._help_parser = self
|
|
44
46
|
|
|
@@ -61,9 +63,21 @@ class HelpArgumentParser(argparse.ArgumentParser):
|
|
|
61
63
|
def error(self, message):
|
|
62
64
|
if message.startswith("unrecognized arguments:"):
|
|
63
65
|
message = "unrecognized arguments"
|
|
66
|
+
# argparse includes raw argument values in these diagnostics. Keep the
|
|
67
|
+
# option/type description while excluding values (which can be secrets).
|
|
68
|
+
if "invalid choice:" in message:
|
|
69
|
+
message = message.split("invalid choice:", 1)[0] + "invalid choice"
|
|
70
|
+
elif " value:" in message:
|
|
71
|
+
message = message.split(" value:", 1)[0] + " value"
|
|
72
|
+
elif message.startswith("ambiguous option:"):
|
|
73
|
+
message = "ambiguous option"
|
|
74
|
+
from .cli_errors import error_block
|
|
75
|
+
|
|
64
76
|
command = self._help_parser
|
|
77
|
+
print(error_block("INVALID_ARGUMENT", message), file=sys.stderr)
|
|
78
|
+
print(file=sys.stderr)
|
|
65
79
|
command.print_help(sys.stderr)
|
|
66
|
-
command.exit(2
|
|
80
|
+
command.exit(2)
|
|
67
81
|
|
|
68
82
|
|
|
69
83
|
class CommandHelpFormatter(argparse.HelpFormatter):
|
|
@@ -133,10 +147,10 @@ def log_page_size(value):
|
|
|
133
147
|
|
|
134
148
|
def runner_arguments(submit):
|
|
135
149
|
submit.add_argument("--runner", metavar="NAME[@REVISION]",
|
|
136
|
-
help="Agent/Train: namespace/name@REVISION; Sandbox: registered Runner name (for example base)")
|
|
137
|
-
submit.add_argument("--job-id", help="caller-selected Job ID")
|
|
138
|
-
submit.add_argument("--tag", help="run label")
|
|
139
|
-
submit.add_argument("--priority", type=int, help="waiting priority;
|
|
150
|
+
help="Agent/Train/Inference: namespace/name@REVISION; Sandbox: registered Runner name (for example base)")
|
|
151
|
+
submit.add_argument("--job-id", help="Agent/Sandbox: caller-selected Job ID")
|
|
152
|
+
submit.add_argument("--tag", help="Agent/Sandbox: run label")
|
|
153
|
+
submit.add_argument("--priority", type=int, help="Agent/Sandbox/Train recipe: waiting priority; explicit value overrides spec (server default: 0)")
|
|
140
154
|
submit.add_argument("--command", help="Agent/Sandbox: shell override; Train: declared run_command parameter passed to the published Runner entrypoint; preserves multiline text")
|
|
141
155
|
|
|
142
156
|
|
|
@@ -147,14 +161,14 @@ def parser():
|
|
|
147
161
|
description="Manage Agent, Sandbox, Train and Inference Jobs and worker trials with EnvLoop.\nConfigure your connection with 'auth config'.",
|
|
148
162
|
)
|
|
149
163
|
cli._optionals.title = "Options"
|
|
150
|
-
cli.set_defaults(api_url=None, workspace_id=None)
|
|
164
|
+
cli.set_defaults(api_url=None, workspace_id=None, no_color=False)
|
|
151
165
|
cli.add_argument("-V", "--version", action="version", version=f"%(prog)s {CLI_VERSION}", help="Display CLI version")
|
|
152
166
|
groups = cli.add_subparsers(dest="group", title="Commands", prog=cli.prog, required=True)
|
|
153
167
|
from .auth_cli import add_auth_commands
|
|
154
168
|
|
|
155
169
|
jobs = groups.add_parser("job", help="Submit and manage Agent, Sandbox, Train and Inference Jobs").add_subparsers(dest="action", required=True)
|
|
156
170
|
job_submit = jobs.add_parser("submit", help="Submit an Agent, Sandbox, Train or Inference Job", allow_abbrev=False)
|
|
157
|
-
job_submit.add_argument("--type", choices=("agent", "sandbox", "train", "inference"), default=
|
|
171
|
+
job_submit.add_argument("--type", choices=("agent", "sandbox", "train", "inference"), default=None, help="Job type (default: spec type, otherwise agent)")
|
|
158
172
|
job_submit.add_argument("--image", metavar="IMAGE", help="Sandbox image override (server default: python:3.11); Train Runner: public spelling of --docker")
|
|
159
173
|
from .train_runner_cli import memory_mebibytes, positive_integer
|
|
160
174
|
|
|
@@ -166,7 +180,7 @@ def parser():
|
|
|
166
180
|
for option in ('memory', 'disk', 'shm-size'):
|
|
167
181
|
job_submit.add_argument('--' + option, type=memory_mebibytes, metavar="SIZE",
|
|
168
182
|
help="Train Runner: per-Trial size in Mi/Gi/Ti (for example 64Gi)")
|
|
169
|
-
job_submit.add_argument("--
|
|
183
|
+
job_submit.add_argument("--spec", type=Path, metavar="REQUEST.json", help="Job specification JSON; explicit submission options override file values (except --trails)")
|
|
170
184
|
job_submit.add_argument("--source", type=Path, help="two-file directory for a single-task Job")
|
|
171
185
|
job_submit.add_argument("--task-id", help="select one task within the dataset")
|
|
172
186
|
job_submit.add_argument(
|
|
@@ -175,17 +189,17 @@ def parser():
|
|
|
175
189
|
)
|
|
176
190
|
job_submit.add_argument("--dataset", dest="dataset_option", type=dataset_reference, metavar="NAME[@VERSION]",
|
|
177
191
|
help="dataset name; default selection uses the platform's pinned snapshot")
|
|
178
|
-
job_submit.add_argument("--config", help="dataset subset;
|
|
192
|
+
job_submit.add_argument("--config", help="Agent: dataset subset; Train recipe: named preset; omitted preserves spec or server default")
|
|
179
193
|
job_submit.add_argument("--split", help="one complete split name; omitted selects all splits")
|
|
180
194
|
job_submit.add_argument("--revision", help="Git branch/tag/commit; must match the platform's pinned snapshot")
|
|
181
195
|
runner_arguments(job_submit)
|
|
182
196
|
job_submit.add_argument("--attempts", type=int)
|
|
183
|
-
job_submit.add_argument("--request-id")
|
|
197
|
+
job_submit.add_argument("--request-id", help="idempotency key; overrides request_id in --spec")
|
|
184
198
|
job_submit.add_argument("--profile", type=profile_reference, metavar="NAME[@VERSION]",
|
|
185
|
-
help="named runtime Profile; omitted version
|
|
199
|
+
help="named runtime Profile; omitted version prefers default, then newest unarchived creation")
|
|
186
200
|
job_submit.add_argument("--seed", type=int)
|
|
187
201
|
job_submit.add_argument("--timeout-sec", type=wait_seconds,
|
|
188
|
-
help="Agent/Sandbox or Train Runner execution timeout (whole seconds); Train --
|
|
202
|
+
help="Agent/Sandbox or Train Runner execution timeout (whole seconds); Train recipe --wait timeout (default: 600)")
|
|
189
203
|
job_submit.add_argument("--delete-delay", type=nonnegative_seconds, metavar="SECONDS",
|
|
190
204
|
help="keep each sandbox (E2B or Fly) after completion for SECONDS (default: 0); cancellation/timeout delete immediately")
|
|
191
205
|
job_submit.add_argument("--wait", action="store_true")
|
|
@@ -207,6 +221,8 @@ def parser():
|
|
|
207
221
|
train_log = jobs.add_parser("log", help="Read or follow Agent, Sandbox or Train Job logs")
|
|
208
222
|
train_log.add_argument("job_id")
|
|
209
223
|
train_log.add_argument("--source", choices=("all", "system", "runtime"), default="all")
|
|
224
|
+
train_log.add_argument("--process", metavar="NAME",
|
|
225
|
+
help="Only one recorded process, e.g. trainer (a Train Job's step metrics)")
|
|
210
226
|
position = train_log.add_mutually_exclusive_group()
|
|
211
227
|
position.add_argument("--tail", type=log_page_size, help="Start with the last N records (default: 200)")
|
|
212
228
|
position.add_argument("--cursor", help="Resume from a previously returned log cursor")
|
|
@@ -217,7 +233,7 @@ def parser():
|
|
|
217
233
|
runs_list = runs.add_parser("list", help="List agent runs, most recently updated first")
|
|
218
234
|
runs_list.add_argument("--json", action="store_true", help="Output the complete response as JSON (default: table)")
|
|
219
235
|
runs_get = runs.add_parser("get", help="Show a run's rounds, Train Jobs and checkpoints")
|
|
220
|
-
runs_get.add_argument("run", metavar="RUN", help="run
|
|
236
|
+
runs_get.add_argument("run", metavar="RUN", help="run tag: tags.rsi_run or agent_run")
|
|
221
237
|
runs_get.add_argument("--json", action="store_true", help="Output the complete run as JSON (default: tables)")
|
|
222
238
|
runs_start = runs.add_parser("start", help="Start a new RSI loop run (paid; needs training.run)")
|
|
223
239
|
runs_start.add_argument("run", metavar="RUN", help="new run id: lower-case letters, digits and dashes")
|
|
@@ -233,6 +249,7 @@ def parser():
|
|
|
233
249
|
runs_start.add_argument("--student-model", help="gateway student model of a dry run")
|
|
234
250
|
runs_live = runs.add_parser("live", help="Show what a run does now; --follow streams new events")
|
|
235
251
|
runs_live.add_argument("run", metavar="RUN")
|
|
252
|
+
runs_live.add_argument("--json", action="store_true", help="Output JSON; --follow emits complete frames as NDJSON")
|
|
236
253
|
runs_live.add_argument("--since", type=int, help="only events after this cursor (the previous next_cursor)")
|
|
237
254
|
runs_live.add_argument("--follow", action="store_true", help="poll and print new events until the run ends; Ctrl-C exits without stopping it")
|
|
238
255
|
runs_steer = runs.add_parser("steer", help="Message the running operator now (needs training.run)")
|
|
@@ -273,7 +290,7 @@ def parser():
|
|
|
273
290
|
update = profiles.add_parser("update", help="Edit an existing Profile by publishing a new immutable version")
|
|
274
291
|
update.add_argument("name")
|
|
275
292
|
update.add_argument("version", help="New version; existing versions cannot be overwritten")
|
|
276
|
-
update.add_argument("--from-version", help="Source version; omit to use
|
|
293
|
+
update.add_argument("--from-version", help="Source version; omit to use default, then newest unarchived creation")
|
|
277
294
|
update.add_argument("--file", type=Path, required=True, help="TOML or JSON patch: merge params keys, replace supplied metadata; preserve omitted fields")
|
|
278
295
|
update.add_argument("--runner", metavar="RUNNER", help="Bind a Runner revision; v1 requires a full commit; omit to preserve the source binding")
|
|
279
296
|
update_secret = update.add_mutually_exclusive_group()
|
|
@@ -378,11 +395,24 @@ def resolve_cli_connection(api_url=None, workspace_id=None, project_id=None):
|
|
|
378
395
|
|
|
379
396
|
|
|
380
397
|
def main(argv=None):
|
|
398
|
+
from .terminal import output_options
|
|
399
|
+
|
|
400
|
+
with output_options():
|
|
401
|
+
return _main(argv)
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def _main(argv=None):
|
|
381
405
|
cli = parser()
|
|
382
406
|
argv = sys.argv[1:] if argv is None else argv
|
|
383
407
|
if not argv:
|
|
384
408
|
cli.print_help()
|
|
385
409
|
return 0
|
|
410
|
+
# Apply display options before validation, even if an earlier argument is invalid.
|
|
411
|
+
options = argv[:argv.index("--")] if "--" in argv else argv
|
|
412
|
+
if "--no-color" in options:
|
|
413
|
+
from .terminal import disable_color
|
|
414
|
+
|
|
415
|
+
disable_color()
|
|
386
416
|
if any(value == "--api-key" or value.startswith("--api-key=") for value in argv):
|
|
387
417
|
cli.error("API keys must be entered with auth config or supplied through ENVLOOP_API_KEY")
|
|
388
418
|
args = cli.parse_args(argv)
|
|
@@ -397,22 +427,38 @@ def main(argv=None):
|
|
|
397
427
|
from .cli_errors import format_error
|
|
398
428
|
from .config import ConfigError
|
|
399
429
|
|
|
400
|
-
print(format_error(ConfigError('AUTH_INPUT_CANCELLED')
|
|
430
|
+
print(format_error(ConfigError('AUTH_INPUT_CANCELLED')), file=sys.stderr)
|
|
401
431
|
return 1
|
|
402
432
|
except Exception as exc:
|
|
403
433
|
from .cli_errors import format_error
|
|
404
434
|
|
|
405
|
-
print(format_error(exc
|
|
435
|
+
print(format_error(exc), file=sys.stderr)
|
|
406
436
|
return 1
|
|
407
437
|
if args.group == "job" and args.action == "submit":
|
|
408
438
|
from .train_runner_cli import SHORTCUT_FIELDS, runner_request
|
|
409
439
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
440
|
+
if args.spec is not None:
|
|
441
|
+
from .job_spec_cli import SpecFileError, spec_request
|
|
442
|
+
|
|
443
|
+
try:
|
|
444
|
+
args.job_request = spec_request(args)
|
|
445
|
+
except (OSError, UnicodeError, json.JSONDecodeError, SpecFileError) as exc:
|
|
446
|
+
from .cli_errors import format_error
|
|
447
|
+
|
|
448
|
+
print(format_error(exc), file=sys.stderr)
|
|
449
|
+
return 1
|
|
450
|
+
except (ValueError, TypeError) as exc:
|
|
451
|
+
cli.error(str(exc) if isinstance(exc, ValueError) else 'Unable to merge Job spec')
|
|
452
|
+
else:
|
|
453
|
+
args.type = args.type or 'agent'
|
|
454
|
+
runner_mode = args.type == 'train' and args.spec is None
|
|
455
|
+
if args.spec is None and not runner_mode and any(getattr(args, key) is not None for key in SHORTCUT_FIELDS):
|
|
456
|
+
cli.error('Train Runner shortcuts require --type train --runner and cannot be combined with --spec')
|
|
457
|
+
if args.spec is not None:
|
|
458
|
+
pass # The merged request was validated above; the platform owns final admission.
|
|
459
|
+
elif args.type in {"train", "inference"}:
|
|
460
|
+
if args.spec is None and not runner_mode:
|
|
461
|
+
cli.error(f"--type {args.type} requires --spec REQUEST.json")
|
|
416
462
|
if args.type == 'inference' and (args.wait or args.timeout_sec is not None):
|
|
417
463
|
cli.error('Inference is a continuous service; --wait and --timeout-sec do not apply')
|
|
418
464
|
agent_fields = ("dataset", "dataset_option", "source", "task_id", "config", "split", "revision",
|
|
@@ -420,7 +466,7 @@ def main(argv=None):
|
|
|
420
466
|
if not runner_mode:
|
|
421
467
|
agent_fields += ('runner', 'command', 'request_id', 'image')
|
|
422
468
|
if any(getattr(args, key) is not None for key in agent_fields):
|
|
423
|
-
cli.error(f"--type {args.type} cannot be combined with Agent submission options or mixed --
|
|
469
|
+
cli.error(f"--type {args.type} cannot be combined with Agent submission options or mixed --spec/Runner input")
|
|
424
470
|
if runner_mode:
|
|
425
471
|
try:
|
|
426
472
|
args.train_request = runner_request(args)
|
|
@@ -430,8 +476,8 @@ def main(argv=None):
|
|
|
430
476
|
from .sandbox import sandbox_runner_selector, validate_image
|
|
431
477
|
|
|
432
478
|
if any(getattr(args, key) is not None for key in (
|
|
433
|
-
'
|
|
434
|
-
cli.error('--type sandbox accepts --runner without Dataset, Task,
|
|
479
|
+
'spec', 'dataset', 'dataset_option', 'source', 'task_id', 'config', 'split', 'revision', 'seed')):
|
|
480
|
+
cli.error('--type sandbox accepts --runner without Dataset, Task, Job spec or seed options')
|
|
435
481
|
try:
|
|
436
482
|
args.runner = sandbox_runner_selector(args.runner)
|
|
437
483
|
if args.image is not None:
|
|
@@ -443,8 +489,8 @@ def main(argv=None):
|
|
|
443
489
|
cli.error('Sandbox --timeout-sec must be whole seconds')
|
|
444
490
|
args.timeout_sec = int(args.timeout_sec)
|
|
445
491
|
else:
|
|
446
|
-
if args.
|
|
447
|
-
cli.error("--
|
|
492
|
+
if args.spec is not None:
|
|
493
|
+
cli.error("--spec requires --type train or inference")
|
|
448
494
|
if args.image is not None:
|
|
449
495
|
cli.error('--image requires --type sandbox')
|
|
450
496
|
if args.runner is not None:
|
|
@@ -526,17 +572,19 @@ def main(argv=None):
|
|
|
526
572
|
elif args.group == "job":
|
|
527
573
|
if args.action == "submit":
|
|
528
574
|
if args.type == 'inference':
|
|
529
|
-
result = client.jobs.submit_inference(
|
|
575
|
+
result = client.jobs.submit_inference(args.job_request)
|
|
530
576
|
elif args.type == "train":
|
|
531
|
-
request = args.train_request if args.
|
|
577
|
+
request = args.train_request if args.spec is None else args.job_request
|
|
532
578
|
result = client.training_jobs.submit(request)
|
|
579
|
+
elif args.spec is not None:
|
|
580
|
+
result = client.jobs.submit_spec(args.job_request)
|
|
533
581
|
else:
|
|
534
582
|
result = client.jobs.submit(args.dataset, type=args.type, source=args.source, task_id=args.task_id,
|
|
535
583
|
attempts=args.attempts if args.attempts is not None else 1,
|
|
536
584
|
request_id=args.request_id, config=args.config, split=args.split,
|
|
537
585
|
revision=args.revision, **submission_options(args))
|
|
538
586
|
if args.wait:
|
|
539
|
-
wait_options = {"timeout": args.timeout_sec} if args.type == "train" and args.
|
|
587
|
+
wait_options = {"timeout": args.timeout_sec} if args.type == "train" and args.spec is not None and args.timeout_sec is not None else {}
|
|
540
588
|
result = client.jobs.wait(result["job_id"], **wait_options)
|
|
541
589
|
elif args.action == 'event':
|
|
542
590
|
result = client.jobs.events(args.job_id)
|
|
@@ -563,6 +611,13 @@ def main(argv=None):
|
|
|
563
611
|
result = client.jobs.list(**options)
|
|
564
612
|
else:
|
|
565
613
|
result = getattr(client.jobs, 'status' if args.action == 'get' else args.action)(args.job_id)
|
|
614
|
+
if args.action == 'get':
|
|
615
|
+
from .train_progress import job_progress, progress_line
|
|
616
|
+
|
|
617
|
+
# The JSON on stdout is unchanged; a Train Job's live progress is a readable stderr line.
|
|
618
|
+
line = progress_line(job_progress(result))
|
|
619
|
+
if line:
|
|
620
|
+
print(line, file=sys.stderr)
|
|
566
621
|
elif args.group == "trail":
|
|
567
622
|
if args.action == "artifact":
|
|
568
623
|
if args.artifact_action == "download":
|
|
@@ -606,10 +661,10 @@ def main(argv=None):
|
|
|
606
661
|
|
|
607
662
|
if isinstance(exc, CleanupError):
|
|
608
663
|
print(json.dumps(operation_output(exc.operation), ensure_ascii=False))
|
|
609
|
-
print(format_error(exc
|
|
664
|
+
print(format_error(exc), file=sys.stderr)
|
|
610
665
|
return 1
|
|
611
666
|
# Do not emit arbitrary exception messages, file content or credentials.
|
|
612
|
-
print(format_error(exc
|
|
667
|
+
print(format_error(exc), file=sys.stderr)
|
|
613
668
|
return 1
|
|
614
669
|
|
|
615
670
|
|