envloop-cli 1.4.0__py3-none-any.whl → 1.7.0__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.
- envloop/cli.py +35 -12
- envloop/cli_errors.py +5 -0
- envloop/client.py +6 -33
- envloop/inference.py +35 -0
- envloop/resources.py +0 -4
- envloop/train.py +25 -2
- envloop/train_runner_cli.py +64 -0
- envloop/training/__init__.py +3 -122
- envloop_cli-1.7.0.dist-info/METADATA +195 -0
- {envloop_cli-1.4.0.dist-info → envloop_cli-1.7.0.dist-info}/RECORD +12 -13
- envloop/training/credentials.py +0 -107
- envloop/training/sampling_client.py +0 -58
- envloop/training/service_client.py +0 -165
- envloop_cli-1.4.0.dist-info/METADATA +0 -122
- {envloop_cli-1.4.0.dist-info → envloop_cli-1.7.0.dist-info}/WHEEL +0 -0
- {envloop_cli-1.4.0.dist-info → envloop_cli-1.7.0.dist-info}/entry_points.txt +0 -0
envloop/cli.py
CHANGED
|
@@ -133,11 +133,11 @@ def log_page_size(value):
|
|
|
133
133
|
|
|
134
134
|
def runner_arguments(submit):
|
|
135
135
|
submit.add_argument("--runner", metavar="NAME[@REVISION]",
|
|
136
|
-
help="Agent:
|
|
136
|
+
help="Agent/Train: namespace/name@REVISION; Sandbox: registered Runner name (for example base)")
|
|
137
137
|
submit.add_argument("--job-id", help="caller-selected Job ID")
|
|
138
138
|
submit.add_argument("--tag", help="run label")
|
|
139
139
|
submit.add_argument("--priority", type=int, help="waiting priority; larger values run first (default: 0)")
|
|
140
|
-
submit.add_argument("--command", help="shell
|
|
140
|
+
submit.add_argument("--command", help="Agent/Sandbox: shell override; Train: declared run_command parameter passed to the published Runner entrypoint; preserves multiline text")
|
|
141
141
|
|
|
142
142
|
|
|
143
143
|
def parser():
|
|
@@ -153,10 +153,20 @@ def parser():
|
|
|
153
153
|
from .auth_cli import add_auth_commands
|
|
154
154
|
|
|
155
155
|
jobs = groups.add_parser("job", help="Submit and manage Agent, Sandbox, Train and Inference Jobs").add_subparsers(dest="action", required=True)
|
|
156
|
-
job_submit = jobs.add_parser("submit", help="Submit an Agent, Sandbox, Train or Inference Job")
|
|
156
|
+
job_submit = jobs.add_parser("submit", help="Submit an Agent, Sandbox, Train or Inference Job", allow_abbrev=False)
|
|
157
157
|
job_submit.add_argument("--type", choices=("agent", "sandbox", "train", "inference"), default="agent", help="Job type (default: agent)")
|
|
158
|
-
job_submit.add_argument("--image", metavar="IMAGE", help="Sandbox
|
|
159
|
-
|
|
158
|
+
job_submit.add_argument("--image", metavar="IMAGE", help="Sandbox image override (server default: python:3.11); Train Runner: public spelling of --docker")
|
|
159
|
+
from .train_runner_cli import memory_mebibytes, positive_integer
|
|
160
|
+
|
|
161
|
+
job_submit.add_argument("--docker", metavar="IMAGE", help="Train Runner Docker image override; omitted uses the Runner declaration")
|
|
162
|
+
job_submit.add_argument("--trails", type=positive_integer, metavar="COUNT", help="Train Runner: 1–16 independent Trials (default: 1)")
|
|
163
|
+
job_submit.add_argument("--gput-type", metavar="MODEL", help="Train Runner: GPU model for each Trial (for example H100)")
|
|
164
|
+
job_submit.add_argument("--gpu", type=positive_integer, metavar="COUNT", help="Train Runner: positive GPU count per Trial")
|
|
165
|
+
job_submit.add_argument("--cpu", type=positive_integer, metavar="COUNT", help="Train Runner: positive CPU count per Trial")
|
|
166
|
+
for option in ('memory', 'disk', 'shm-size'):
|
|
167
|
+
job_submit.add_argument('--' + option, type=memory_mebibytes, metavar="SIZE",
|
|
168
|
+
help="Train Runner: per-Trial size in Mi/Gi/Ti (for example 64Gi)")
|
|
169
|
+
job_submit.add_argument("--file", type=Path, help="Managed Train or Inference Job JSON (spec or Train trials array; interactive training is unsupported)")
|
|
160
170
|
job_submit.add_argument("--source", type=Path, help="two-file directory for a single-task Job")
|
|
161
171
|
job_submit.add_argument("--task-id", help="select one task within the dataset")
|
|
162
172
|
job_submit.add_argument(
|
|
@@ -175,10 +185,11 @@ def parser():
|
|
|
175
185
|
help="named runtime Profile; omitted version requires an explicit default")
|
|
176
186
|
job_submit.add_argument("--seed", type=int)
|
|
177
187
|
job_submit.add_argument("--timeout-sec", type=wait_seconds,
|
|
178
|
-
help="Agent/Sandbox execution timeout (whole seconds); Train wait timeout (default: 600)")
|
|
188
|
+
help="Agent/Sandbox or Train Runner execution timeout (whole seconds); Train --file wait timeout (default: 600)")
|
|
179
189
|
job_submit.add_argument("--delete-delay", type=nonnegative_seconds, metavar="SECONDS",
|
|
180
190
|
help="keep each sandbox (E2B or Fly) after completion for SECONDS (default: 0); cancellation/timeout delete immediately")
|
|
181
191
|
job_submit.add_argument("--wait", action="store_true")
|
|
192
|
+
job_submit.add_argument("--json", action="store_true", help="Output the complete response as JSON (the default for submit; accepted for consistency with list commands)")
|
|
182
193
|
for action in ("get", "stop", "result", "pause", "resume", "wake", "event"):
|
|
183
194
|
command = jobs.add_parser(action)
|
|
184
195
|
command.add_argument("job_id")
|
|
@@ -394,16 +405,27 @@ def main(argv=None):
|
|
|
394
405
|
print(format_error(exc, args=args, program=cli.prog), file=sys.stderr)
|
|
395
406
|
return 1
|
|
396
407
|
if args.group == "job" and args.action == "submit":
|
|
408
|
+
from .train_runner_cli import SHORTCUT_FIELDS, runner_request
|
|
409
|
+
|
|
410
|
+
runner_mode = args.type == 'train' and args.file is None
|
|
411
|
+
if not runner_mode and any(getattr(args, key) is not None for key in SHORTCUT_FIELDS):
|
|
412
|
+
cli.error('Train Runner shortcuts require --type train --runner and cannot be combined with --file')
|
|
397
413
|
if args.type in {"train", "inference"}:
|
|
398
|
-
if args.file is None:
|
|
414
|
+
if args.file is None and not runner_mode:
|
|
399
415
|
cli.error(f"--type {args.type} requires --file REQUEST.json")
|
|
400
416
|
if args.type == 'inference' and (args.wait or args.timeout_sec is not None):
|
|
401
417
|
cli.error('Inference is a continuous service; --wait and --timeout-sec do not apply')
|
|
402
418
|
agent_fields = ("dataset", "dataset_option", "source", "task_id", "config", "split", "revision",
|
|
403
|
-
"
|
|
404
|
-
|
|
419
|
+
"job_id", "tag", "priority", "attempts", "profile", "seed", "delete_delay")
|
|
420
|
+
if not runner_mode:
|
|
421
|
+
agent_fields += ('runner', 'command', 'request_id', 'image')
|
|
405
422
|
if any(getattr(args, key) is not None for key in agent_fields):
|
|
406
|
-
cli.error(f"--type {args.type}
|
|
423
|
+
cli.error(f"--type {args.type} cannot be combined with Agent submission options or mixed --file/Runner input")
|
|
424
|
+
if runner_mode:
|
|
425
|
+
try:
|
|
426
|
+
args.train_request = runner_request(args)
|
|
427
|
+
except ValueError as exc:
|
|
428
|
+
cli.error(str(exc))
|
|
407
429
|
elif args.type == 'sandbox':
|
|
408
430
|
from .sandbox import sandbox_runner_selector, validate_image
|
|
409
431
|
|
|
@@ -506,14 +528,15 @@ def main(argv=None):
|
|
|
506
528
|
if args.type == 'inference':
|
|
507
529
|
result = client.jobs.submit_inference(json.loads(args.file.read_text(encoding='utf-8')))
|
|
508
530
|
elif args.type == "train":
|
|
509
|
-
|
|
531
|
+
request = args.train_request if args.file is None else json.loads(args.file.read_text(encoding="utf-8"))
|
|
532
|
+
result = client.training_jobs.submit(request)
|
|
510
533
|
else:
|
|
511
534
|
result = client.jobs.submit(args.dataset, type=args.type, source=args.source, task_id=args.task_id,
|
|
512
535
|
attempts=args.attempts if args.attempts is not None else 1,
|
|
513
536
|
request_id=args.request_id, config=args.config, split=args.split,
|
|
514
537
|
revision=args.revision, **submission_options(args))
|
|
515
538
|
if args.wait:
|
|
516
|
-
wait_options = {"timeout": args.timeout_sec} if args.type == "train" and args.timeout_sec is not None else {}
|
|
539
|
+
wait_options = {"timeout": args.timeout_sec} if args.type == "train" and args.file is not None and args.timeout_sec is not None else {}
|
|
517
540
|
result = client.jobs.wait(result["job_id"], **wait_options)
|
|
518
541
|
elif args.action == 'event':
|
|
519
542
|
result = client.jobs.events(args.job_id)
|
envloop/cli_errors.py
CHANGED
|
@@ -90,6 +90,11 @@ _register("RSI_CONTROL_UNCONFIGURED RSI_CONTROL_UNAVAILABLE RSI_CONTROL_TIMEOUT"
|
|
|
90
90
|
_register("JOB_ACTION_UNSUPPORTED", "This Job type does not support the requested action.", "Check the Job type with '{program} job get JOB_ID' and use its supported actions.")
|
|
91
91
|
_register("INFERENCE_ROUTE_RETIRED", "The global inference route has been retired.", "Use the owning Job's base URL: API_BASE/v1/job/JOB_ID/Inference, and a model deployed by that Job.")
|
|
92
92
|
|
|
93
|
+
_register("CHECKPOINT_UNAVAILABLE CHECKPOINT_NOT_DURABLE CHECKPOINT_FILES_MISSING", "The fixed inference input is unavailable or not durable.", "Inspect checkpoint publication, expiry, deletion and permissions. Do not retry with different weights under the same request ID.")
|
|
94
|
+
_register("CHECKPOINT_INTEGRITY_FAILED CHECKPOINT_FORMAT_UNSUPPORTED CHECKPOINT_BASE_MISMATCH", "The fixed input failed integrity or compatibility validation.", "Use a verified durable checkpoint matching the serving profile. A different input requires a new Job and request ID.")
|
|
95
|
+
_register("INFERENCE_AUTHORIZATION_DENIED INFERENCE_AUTHORIZATION_UNAVAILABLE", "The platform could not authorize a new inference worker.", "Check current project membership and Runner/input access before waking or resuming the Job.")
|
|
96
|
+
_register("INFERENCE_PROFILE_UNSUPPORTED INFERENCE_RUNNER_INVALID INFERENCE_MODEL_UNPINNED INFERENCE_RUNNER_IMAGE_MISMATCH INFERENCE_RUNNER_IMAGE_UNAVAILABLE INFERENCE_RUNTIME_UNAVAILABLE", "The selected Runner or frozen inference configuration is unavailable or unsupported.", "Discover a published compatible Runner and serving profile. Submit a new Job for changes to weights, base model or Runner.")
|
|
97
|
+
|
|
93
98
|
# Public service identifiers verified against EnvPlatform. Keep unexpected codes
|
|
94
99
|
# private even when they look uppercase: a server can echo a submitted secret in
|
|
95
100
|
# that field. HTTP status still supplies useful recovery for newer service codes.
|
envloop/client.py
CHANGED
|
@@ -548,10 +548,8 @@ class Jobs:
|
|
|
548
548
|
|
|
549
549
|
def submit_inference(self, body: dict):
|
|
550
550
|
"""Submit a continuous inference deployment, identified by its Job ID."""
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
if not isinstance(body.get('spec'), dict):
|
|
554
|
-
raise ValueError('inference submission requires a spec object')
|
|
551
|
+
from .inference import validate_submission
|
|
552
|
+
validate_submission(body)
|
|
555
553
|
return self._server.submit_job({'request_id': str(uuid4()), **body, 'type': 'inference'})
|
|
556
554
|
|
|
557
555
|
def pause(self, job_id):
|
|
@@ -646,36 +644,11 @@ class Client:
|
|
|
646
644
|
|
|
647
645
|
return TemporaryResources(self, **options)
|
|
648
646
|
|
|
649
|
-
def training_service(self,
|
|
650
|
-
"""
|
|
647
|
+
def training_service(self, *args, **options):
|
|
648
|
+
"""Removed: raise a pointer to managed Jobs."""
|
|
649
|
+
from .train import INTERACTIVE_TRAINING_REMOVED
|
|
651
650
|
|
|
652
|
-
|
|
653
|
-
token. Upstream token refresh follows the same platform URL and the
|
|
654
|
-
gateway rechecks the underlying subject and credential on every request.
|
|
655
|
-
"""
|
|
656
|
-
from .training import ServiceClient
|
|
657
|
-
from .training.credentials import TrainingHttpClient
|
|
658
|
-
|
|
659
|
-
if 'http_client' in options:
|
|
660
|
-
raise ValueError('Client.training_service manages its authenticated HTTP client')
|
|
661
|
-
if any(name in options for name in ('base_url', 'api_key', 'default_headers')):
|
|
662
|
-
raise ValueError('Client.training_service manages its connection and credential headers')
|
|
663
|
-
overrides = options.get('default_headers_overrides') or {}
|
|
664
|
-
protected_headers = {'authorization', 'x-api-key', 'cookie', 'x-workspace-id',
|
|
665
|
-
'x-project-id', 'x-envloop-temporary-scope'}
|
|
666
|
-
if any(name.lower() in protected_headers for name in overrides):
|
|
667
|
-
raise ValueError('Client.training_service manages authentication, project and temporary scope headers')
|
|
668
|
-
transport = self._transport
|
|
669
|
-
project_id = transport.headers.get("X-Project-ID", "")
|
|
670
|
-
if 'project_id' in options and options.pop('project_id') != project_id:
|
|
671
|
-
raise ValueError('Select the training project through Client.remote(project_id=...)')
|
|
672
|
-
token = transport.request("POST", "/v1/job/tinker/api/v1/auth/token", {})["jwt"]
|
|
673
|
-
headers = {name: value for name, value in transport.headers.items() if name in {"X-Workspace-ID", "X-Project-ID"}}
|
|
674
|
-
if temporary_scope_id is not None:
|
|
675
|
-
headers["X-EnvLoop-Temporary-Scope"] = temporary_scope_id
|
|
676
|
-
base_url = transport.base + "/v1/job/tinker"
|
|
677
|
-
return ServiceClient(base_url=base_url, api_key=token, project_id=project_id, default_headers=headers,
|
|
678
|
-
http_client=TrainingHttpClient(base_url, transport.headers["Authorization"]), **options)
|
|
651
|
+
raise RuntimeError(INTERACTIVE_TRAINING_REMOVED)
|
|
679
652
|
|
|
680
653
|
@classmethod
|
|
681
654
|
def remote(cls, api_url=None, *, timeout=30, api_key=None, workspace_id=None, project_id=None, config=True):
|
envloop/inference.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Public inference intent validation; resolution and admission remain server owned."""
|
|
2
|
+
import re
|
|
3
|
+
|
|
4
|
+
_FIELDS = {'name', 'runner', 'inputs', 'serving_profile', 'hardware', 'weights_format',
|
|
5
|
+
'scaling', 'temporary_scope_id'}
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def validate_submission(body):
|
|
9
|
+
if (not isinstance(body, dict) or set(body) - {'type', 'request_id', 'spec'}
|
|
10
|
+
or body.get('type', 'inference') != 'inference'):
|
|
11
|
+
raise ValueError('inference submission requires spec and request_id')
|
|
12
|
+
spec = body.get('spec')
|
|
13
|
+
if not isinstance(spec, dict):
|
|
14
|
+
raise ValueError('inference submission requires a spec object')
|
|
15
|
+
if 'request_id' in body and (not isinstance(body['request_id'], str)
|
|
16
|
+
or not body['request_id'].strip() or len(body['request_id']) > 128):
|
|
17
|
+
raise ValueError('request_id must be a non-empty string of at most 128 characters')
|
|
18
|
+
if any(key in spec for key in ('provider', 'executor', 'resolved_runner', 'runtime')):
|
|
19
|
+
raise ValueError('execution configuration and Runner resolution are owned by the server')
|
|
20
|
+
if 'runner' not in spec:
|
|
21
|
+
return # Explicit legacy compatibility: existing adapter specs still reach the API.
|
|
22
|
+
if set(spec) - _FIELDS:
|
|
23
|
+
raise ValueError('fixed-input inference cannot mix legacy or runtime fields')
|
|
24
|
+
runner = spec['runner']
|
|
25
|
+
if (not isinstance(runner, str)
|
|
26
|
+
or not re.fullmatch(r'[A-Za-z0-9][A-Za-z0-9._-]*/[A-Za-z0-9][A-Za-z0-9._-]*@[^\s@]+', runner)):
|
|
27
|
+
raise ValueError('runner requires namespace/name@revision')
|
|
28
|
+
if any(not isinstance(spec.get(key), str) or not spec[key].strip()
|
|
29
|
+
for key in ('name', 'serving_profile')):
|
|
30
|
+
raise ValueError('fixed-input inference requires name and serving_profile')
|
|
31
|
+
inputs = spec.get('inputs', {})
|
|
32
|
+
if (not isinstance(inputs, dict) or set(inputs) - {'checkpoint'}
|
|
33
|
+
or 'checkpoint' in inputs and (not isinstance(inputs['checkpoint'], str)
|
|
34
|
+
or not inputs['checkpoint'].startswith('envloop://') or not inputs['checkpoint'][10:])):
|
|
35
|
+
raise ValueError('inputs accepts one Checkpoint ANS URI')
|
envloop/resources.py
CHANGED
|
@@ -183,10 +183,6 @@ class TemporaryResources:
|
|
|
183
183
|
def get(self):
|
|
184
184
|
return self.client._transport.request("GET", self.path)
|
|
185
185
|
|
|
186
|
-
def training_service(self, **options):
|
|
187
|
-
self.path # Fail before any authentication/session if the scope was not entered.
|
|
188
|
-
return self.client.training_service(temporary_scope_id=self.scope_id, **options)
|
|
189
|
-
|
|
190
186
|
def retain(self, resources):
|
|
191
187
|
return self.client._transport.request("POST", self.path + "/retain", {"resources": resources})
|
|
192
188
|
|
envloop/train.py
CHANGED
|
@@ -3,19 +3,42 @@ import math
|
|
|
3
3
|
import time
|
|
4
4
|
from urllib.parse import quote, urlencode
|
|
5
5
|
|
|
6
|
+
from .http import RemoteError
|
|
7
|
+
|
|
8
|
+
INTERACTIVE_TRAINING_REMOVED = (
|
|
9
|
+
"Interactive training (envloop.training, Client.training_service) was removed; training is "
|
|
10
|
+
"managed Jobs only. Submit with `el job submit --type train --file REQUEST.json` "
|
|
11
|
+
"(or client.training_jobs.submit(request)) and discover recipes with "
|
|
12
|
+
"client.training_jobs.models(). See docs/training.md."
|
|
13
|
+
)
|
|
14
|
+
|
|
6
15
|
|
|
7
16
|
class TrainJobs:
|
|
8
17
|
def __init__(self, transport):
|
|
9
18
|
self.transport = transport
|
|
10
19
|
|
|
20
|
+
def models(self) -> dict:
|
|
21
|
+
"""Discover configured managed recipes; admission and GPU readiness remain separate."""
|
|
22
|
+
try:
|
|
23
|
+
return self.transport.request('GET', '/v1/job/training-models')
|
|
24
|
+
except RemoteError as error:
|
|
25
|
+
if error.status_code == 404:
|
|
26
|
+
raise RuntimeError(
|
|
27
|
+
"This platform does not serve /v1/job/training-models; upgrade the platform to a "
|
|
28
|
+
"version that matches this client (managed training discovery is unavailable).") from error
|
|
29
|
+
raise
|
|
30
|
+
|
|
11
31
|
@staticmethod
|
|
12
32
|
def _view(result):
|
|
13
33
|
return {**(result.get('training') or {}), **result}
|
|
14
34
|
|
|
15
35
|
def submit(self, request: dict) -> dict:
|
|
16
|
-
"""Submit one
|
|
36
|
+
"""Submit one managed spec or ``{request_id, trials: [{name?, spec}, ...]}``.
|
|
17
37
|
|
|
18
|
-
Each Trial spec is independent
|
|
38
|
+
Each Trial spec is an independent managed recipe or published Runner
|
|
39
|
+
(``{runner: "namespace/name@REVISION", resources?: {...}, ...}``).
|
|
40
|
+
Runner ``command`` is the declared run_command parameter, never an
|
|
41
|
+
entrypoint override. Validation, defaults and execution selection belong
|
|
19
42
|
to the platform. The training view retains the server's complete response.
|
|
20
43
|
"""
|
|
21
44
|
if not isinstance(request, dict) or request.get("type", "train") != "train":
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Translate Runner Train CLI shortcuts into the public Job request schema."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from uuid import uuid4
|
|
5
|
+
|
|
6
|
+
SHORTCUT_FIELDS = ('docker', 'trails', 'gput_type', 'gpu', 'cpu', 'memory', 'disk', 'shm_size')
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def positive_integer(value):
|
|
10
|
+
count = int(value)
|
|
11
|
+
if count <= 0:
|
|
12
|
+
raise ValueError('expected a positive integer')
|
|
13
|
+
return count
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def memory_mebibytes(value):
|
|
17
|
+
"""CLI binary units map exactly to the platform's historical *_mb fields."""
|
|
18
|
+
match = re.fullmatch(r'([1-9][0-9]*)(Mi|Gi|Ti)(?:B)?', value)
|
|
19
|
+
if not match:
|
|
20
|
+
raise ValueError('expected a positive integer with Mi, Gi or Ti units (for example 64Gi)')
|
|
21
|
+
return int(match[1]) * {'Mi': 1, 'Gi': 1024, 'Ti': 1024 * 1024}[match[2]]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def runner_request(args):
|
|
25
|
+
from .client import runner_selector
|
|
26
|
+
from .sandbox import validate_image
|
|
27
|
+
|
|
28
|
+
if not args.runner or not re.fullmatch(r'[A-Za-z0-9][A-Za-z0-9._-]*/[A-Za-z0-9][A-Za-z0-9._-]*@[^\s@]+', args.runner):
|
|
29
|
+
raise ValueError('Train Runner requires --runner namespace/name@REVISION')
|
|
30
|
+
runner = runner_selector(args.runner)
|
|
31
|
+
if args.docker is not None and args.image is not None:
|
|
32
|
+
raise ValueError('--docker and --image cannot be combined')
|
|
33
|
+
count = args.trails if args.trails is not None else 1
|
|
34
|
+
if not 1 <= count <= 16:
|
|
35
|
+
raise ValueError('--trails must be from 1 to 16')
|
|
36
|
+
spec = {'runner': runner}
|
|
37
|
+
image = args.docker if args.docker is not None else args.image
|
|
38
|
+
if image is not None:
|
|
39
|
+
try:
|
|
40
|
+
spec['image'] = validate_image(image)
|
|
41
|
+
except ValueError:
|
|
42
|
+
raise ValueError('Train image must be a Docker image name, optionally with a tag or sha256 digest') from None
|
|
43
|
+
if args.command is not None:
|
|
44
|
+
try:
|
|
45
|
+
valid_command = bool(args.command.strip()) and '\0' not in args.command and len(args.command.encode('utf-8')) <= 65536
|
|
46
|
+
except UnicodeEncodeError:
|
|
47
|
+
valid_command = False
|
|
48
|
+
if not valid_command:
|
|
49
|
+
raise ValueError('--command must be non-empty UTF-8, without NUL, at most 65536 bytes')
|
|
50
|
+
spec['command'] = args.command
|
|
51
|
+
resources = {field: getattr(args, option) for option, field in (
|
|
52
|
+
('gput_type', 'gpu_type'), ('gpu', 'gpus'), ('cpu', 'cpus'),
|
|
53
|
+
('memory', 'memory_mb'), ('disk', 'storage_mb'), ('shm_size', 'shm_mb'))
|
|
54
|
+
if getattr(args, option) is not None}
|
|
55
|
+
if args.gput_type is not None and not args.gput_type.strip():
|
|
56
|
+
raise ValueError('--gput-type must be non-empty')
|
|
57
|
+
if resources:
|
|
58
|
+
spec['resources'] = resources
|
|
59
|
+
if args.timeout_sec is not None:
|
|
60
|
+
if args.timeout_sec <= 0 or not args.timeout_sec.is_integer():
|
|
61
|
+
raise ValueError('Train Runner --timeout-sec must be positive whole seconds')
|
|
62
|
+
spec['timeout_sec'] = int(args.timeout_sec)
|
|
63
|
+
return {'type': 'train', 'request_id': args.request_id if args.request_id is not None else str(uuid4()),
|
|
64
|
+
'trials': [{'name': f'trial-{index + 1}', 'spec': dict(spec)} for index in range(count)]}
|
envloop/training/__init__.py
CHANGED
|
@@ -1,123 +1,4 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Removed: the public interactive training SDK. Training is managed Jobs only."""
|
|
2
|
+
from envloop.train import INTERACTIVE_TRAINING_REMOVED
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
TrainingRun is the upstream data type, not an EnvLoop lifecycle API.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
from tinker import (
|
|
8
|
-
AdamOptimizerConfig as AdamOptimizerConfig,
|
|
9
|
-
AdamParams as AdamParams,
|
|
10
|
-
APIConnectionError as APIConnectionError,
|
|
11
|
-
APIError as APIError,
|
|
12
|
-
APIFuture as APIFuture,
|
|
13
|
-
APIResponseValidationError as APIResponseValidationError,
|
|
14
|
-
APIStatusError as APIStatusError,
|
|
15
|
-
APITimeoutError as APITimeoutError,
|
|
16
|
-
AuthenticationError as AuthenticationError,
|
|
17
|
-
BadRequestError as BadRequestError,
|
|
18
|
-
BillingError as BillingError,
|
|
19
|
-
Checkpoint as Checkpoint,
|
|
20
|
-
CheckpointType as CheckpointType,
|
|
21
|
-
ConflictError as ConflictError,
|
|
22
|
-
Datum as Datum,
|
|
23
|
-
DimuonOptimizerConfig as DimuonOptimizerConfig,
|
|
24
|
-
DimuonParams as DimuonParams,
|
|
25
|
-
DmelChunk as DmelChunk,
|
|
26
|
-
EncodedTextChunk as EncodedTextChunk,
|
|
27
|
-
ForwardBackwardOutput as ForwardBackwardOutput,
|
|
28
|
-
InternalServerError as InternalServerError,
|
|
29
|
-
LoraConfig as LoraConfig,
|
|
30
|
-
ModelID as ModelID,
|
|
31
|
-
ModelInput as ModelInput,
|
|
32
|
-
ModelInputChunk as ModelInputChunk,
|
|
33
|
-
NotFoundError as NotFoundError,
|
|
34
|
-
OptimParams as OptimParams,
|
|
35
|
-
OptimStepRequest as OptimStepRequest,
|
|
36
|
-
OptimStepResponse as OptimStepResponse,
|
|
37
|
-
ParsedCheckpointTinkerPath as ParsedCheckpointTinkerPath,
|
|
38
|
-
PermissionDeniedError as PermissionDeniedError,
|
|
39
|
-
PromptProvenanceSpan as PromptProvenanceSpan,
|
|
40
|
-
RateLimitError as RateLimitError,
|
|
41
|
-
RequestFailedError as RequestFailedError,
|
|
42
|
-
RequestOptions as RequestOptions,
|
|
43
|
-
SampledProvenanceSpan as SampledProvenanceSpan,
|
|
44
|
-
SampledSequence as SampledSequence,
|
|
45
|
-
SampleRequest as SampleRequest,
|
|
46
|
-
SampleResponse as SampleResponse,
|
|
47
|
-
SamplingParams as SamplingParams,
|
|
48
|
-
StopReason as StopReason,
|
|
49
|
-
TensorData as TensorData,
|
|
50
|
-
TensorDtype as TensorDtype,
|
|
51
|
-
Timeout as Timeout,
|
|
52
|
-
TinkerError as TinkerError,
|
|
53
|
-
TrainingClient as TrainingClient,
|
|
54
|
-
TrainingRun as TrainingRun,
|
|
55
|
-
UnprocessableEntityError as UnprocessableEntityError,
|
|
56
|
-
__title__ as __title__,
|
|
57
|
-
__version__ as __version__,
|
|
58
|
-
auth as auth,
|
|
59
|
-
types as types,
|
|
60
|
-
)
|
|
61
|
-
from tinker.types import ImageChunk as ImageChunk
|
|
62
|
-
|
|
63
|
-
from .sampling_client import SamplingClient as SamplingClient
|
|
64
|
-
from .service_client import RestClient as RestClient, ServiceClient as ServiceClient
|
|
65
|
-
|
|
66
|
-
__all__ = [
|
|
67
|
-
"TrainingClient",
|
|
68
|
-
"ServiceClient",
|
|
69
|
-
"RestClient",
|
|
70
|
-
"SamplingClient",
|
|
71
|
-
"APIFuture",
|
|
72
|
-
"auth",
|
|
73
|
-
"AdamOptimizerConfig",
|
|
74
|
-
"AdamParams",
|
|
75
|
-
"DimuonOptimizerConfig",
|
|
76
|
-
"DimuonParams",
|
|
77
|
-
"OptimParams",
|
|
78
|
-
"Checkpoint",
|
|
79
|
-
"CheckpointType",
|
|
80
|
-
"Datum",
|
|
81
|
-
"DmelChunk",
|
|
82
|
-
"EncodedTextChunk",
|
|
83
|
-
"ImageChunk",
|
|
84
|
-
"ForwardBackwardOutput",
|
|
85
|
-
"SampledProvenanceSpan",
|
|
86
|
-
"LoraConfig",
|
|
87
|
-
"ModelID",
|
|
88
|
-
"ModelInput",
|
|
89
|
-
"ModelInputChunk",
|
|
90
|
-
"OptimStepRequest",
|
|
91
|
-
"OptimStepResponse",
|
|
92
|
-
"ParsedCheckpointTinkerPath",
|
|
93
|
-
"PromptProvenanceSpan",
|
|
94
|
-
"SampledSequence",
|
|
95
|
-
"SampleRequest",
|
|
96
|
-
"SampleResponse",
|
|
97
|
-
"SamplingParams",
|
|
98
|
-
"StopReason",
|
|
99
|
-
"TensorData",
|
|
100
|
-
"TensorDtype",
|
|
101
|
-
"TrainingRun",
|
|
102
|
-
"Timeout",
|
|
103
|
-
"RequestOptions",
|
|
104
|
-
"TinkerError",
|
|
105
|
-
"APIError",
|
|
106
|
-
"APIStatusError",
|
|
107
|
-
"APITimeoutError",
|
|
108
|
-
"APIConnectionError",
|
|
109
|
-
"APIResponseValidationError",
|
|
110
|
-
"RequestFailedError",
|
|
111
|
-
"BadRequestError",
|
|
112
|
-
"AuthenticationError",
|
|
113
|
-
"BillingError",
|
|
114
|
-
"PermissionDeniedError",
|
|
115
|
-
"NotFoundError",
|
|
116
|
-
"ConflictError",
|
|
117
|
-
"UnprocessableEntityError",
|
|
118
|
-
"RateLimitError",
|
|
119
|
-
"InternalServerError",
|
|
120
|
-
"types",
|
|
121
|
-
"__version__",
|
|
122
|
-
"__title__",
|
|
123
|
-
]
|
|
4
|
+
raise ModuleNotFoundError(INTERACTIVE_TRAINING_REMOVED, name="envloop.training")
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: envloop-cli
|
|
3
|
+
Version: 1.7.0
|
|
4
|
+
Summary: EnvLoop CLI and Python SDK for jobs, training, checkpoints and inference
|
|
5
|
+
Project-URL: Homepage, https://envloop.ai
|
|
6
|
+
Project-URL: Documentation, https://docs.envloop.ai
|
|
7
|
+
Project-URL: Repository, https://github.com/EnvLoop/envloop-cli
|
|
8
|
+
Project-URL: Issues, https://github.com/EnvLoop/envloop-cli/issues
|
|
9
|
+
Project-URL: Agent instructions, https://docs.envloop.ai/skill.md
|
|
10
|
+
Requires-Python: >=3.11
|
|
11
|
+
Requires-Dist: tinker==0.34.0
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# envloop-cli: EnvLoop CLI and Python SDK
|
|
15
|
+
|
|
16
|
+
Install the public client to run agent Jobs, inspect Trials, manage training and
|
|
17
|
+
checkpoints, and deploy or call inference endpoints. The `el` and `envloop`
|
|
18
|
+
commands are equivalent. The Python import package is `envloop`.
|
|
19
|
+
|
|
20
|
+
## Version 1.7
|
|
21
|
+
|
|
22
|
+
- Use published Runner resource shortcuts for Train Jobs and fixed Checkpoint inputs for Inference Jobs.
|
|
23
|
+
- Pass `--json` explicitly to `job submit`; failures retain safe error codes and recovery guidance.
|
|
24
|
+
- Submit training through Train Jobs. The retired interactive `envloop.training` interface is removed.
|
|
25
|
+
|
|
26
|
+
After upgrading, check `el --version` and `el job submit --help`. Execution also
|
|
27
|
+
requires a matching platform, project permissions and published Runners; package
|
|
28
|
+
installation alone does not establish those prerequisites. See the
|
|
29
|
+
[PyPI release history](https://pypi.org/project/envloop-cli/#history) for published versions.
|
|
30
|
+
|
|
31
|
+
## Install
|
|
32
|
+
|
|
33
|
+
Use a Python virtual environment:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
python -m pip install --upgrade envloop-cli
|
|
37
|
+
el --help
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The standard installation includes the client dependencies for managed training.
|
|
41
|
+
Check that the SDK can be imported:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
python -c 'from envloop import Client; print("EnvLoop SDK ready")'
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
When you see `EnvLoop SDK ready`, continue to Connect. This check imports the
|
|
48
|
+
client without creating a Job. Submit training through `client.training_jobs`;
|
|
49
|
+
follow the [managed training guide](https://docs.envloop.ai/training/cli-hello-world).
|
|
50
|
+
|
|
51
|
+
Install the client in your Python environment; the platform provides the execution resources.
|
|
52
|
+
|
|
53
|
+
## Connect
|
|
54
|
+
|
|
55
|
+
Use an authorized EnvLoop account and project. Browser authorization:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
el auth login
|
|
59
|
+
el auth status --check
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
For an existing project API key, use the masked configuration wizard:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
el auth config
|
|
66
|
+
el auth status --check
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
CI can read credentials from `ENVLOOP_API_KEY` and connection settings from
|
|
70
|
+
`ENVLOOP_BASE_URL`, `ENVLOOP_WORKSPACE_ID` and `ENVLOOP_PROJECT_ID`. Keep keys in
|
|
71
|
+
your secret manager. Public API access is scoped to the authorized project.
|
|
72
|
+
|
|
73
|
+
Credentials and settings live in `~/.envloop` (override with an absolute
|
|
74
|
+
`ENVLOOP_HOME`). On POSIX the directory must be private and owned by you. Use the
|
|
75
|
+
default path when the optional environment variable is unset:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
mkdir -p "${ENVLOOP_HOME:-$HOME/.envloop}"
|
|
79
|
+
chmod 700 "${ENVLOOP_HOME:-$HOME/.envloop}"
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
An insecure directory causes `el` to fail with `CONFIG_INSECURE_PATH`.
|
|
83
|
+
|
|
84
|
+
## Explore your project
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
el job list --limit 5
|
|
88
|
+
el job list --type train --limit 5
|
|
89
|
+
el trail list --limit 5
|
|
90
|
+
el trail list --type train --job-id JOB_ID
|
|
91
|
+
el checkpoint list --limit 5
|
|
92
|
+
el job list --type inference
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Query a returned Job ID with `el job get JOB_ID` and `el job result JOB_ID`.
|
|
96
|
+
Trial commands use the spelling `trail`, for example
|
|
97
|
+
`el trail artifact list TRIAL_ID`. Use each command's `--help` for options.
|
|
98
|
+
|
|
99
|
+
For the Python SDK:
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
from envloop import Client
|
|
103
|
+
|
|
104
|
+
with Client.remote() as client:
|
|
105
|
+
print(client.jobs.list(limit=5))
|
|
106
|
+
print(client.trials.list(type="train", limit=5))
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Submitting work, training a model or starting inference can allocate billable
|
|
110
|
+
resources. Follow the task-specific guide and retain the outputs you need before
|
|
111
|
+
cleaning up temporary resources.
|
|
112
|
+
|
|
113
|
+
## Sandbox jobs
|
|
114
|
+
|
|
115
|
+
With a matching platform and a published `envhub/base` ANS Runner, submit a
|
|
116
|
+
Sandbox Job without a Dataset:
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
el job submit --type sandbox --runner base --command 'python --version'
|
|
120
|
+
el job submit --type sandbox --runner base --image python:3.11 --wait
|
|
121
|
+
el job list --type sandbox
|
|
122
|
+
el job log JOB_ID --follow
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
The platform resolves `base` to the registered `envhub/base@main` source ref and
|
|
126
|
+
freezes its immutable Revision. The Sandbox image defaults to `python:3.11`;
|
|
127
|
+
`--image` accepts an image name, tag or digest. The execution provider is configured
|
|
128
|
+
by the server. Job logs retain the actual captured output and collection metadata.
|
|
129
|
+
Failures include a code, readable explanation, HTTP status when available, and
|
|
130
|
+
recovery guidance without echoing arbitrary response bodies or credentials.
|
|
131
|
+
|
|
132
|
+
## Existing installations
|
|
133
|
+
|
|
134
|
+
This distribution provides both the CLI and the `envloop` import package. Use a
|
|
135
|
+
fresh environment if you previously installed the separate source distribution
|
|
136
|
+
named `envloop`, so two distributions do not own the same Python module files.
|
|
137
|
+
Existing client configuration is reused; installing a package does not change
|
|
138
|
+
your account permissions or move remote resources. Check `el --help` when
|
|
139
|
+
upgrading because command groups may differ from your earlier installation.
|
|
140
|
+
|
|
141
|
+
## Documentation
|
|
142
|
+
|
|
143
|
+
- [English documentation](https://docs.envloop.ai/)
|
|
144
|
+
- [中文文档](https://docs.envloop.ai/zh-cn/)
|
|
145
|
+
- [Installation](https://docs.envloop.ai/cli-install)
|
|
146
|
+
- [CLI reference](https://docs.envloop.ai/reference/cli)
|
|
147
|
+
- [Training and deployment](https://docs.envloop.ai/training/train-deploy)
|
|
148
|
+
- [Agent entry: skill.md](https://docs.envloop.ai/skill.md)
|
|
149
|
+
|
|
150
|
+
## Submit managed training
|
|
151
|
+
|
|
152
|
+
Prepare a recipe-supported `REQUEST.json` using the [SFT quickstart](https://docs.envloop.ai/training/quickstart).
|
|
153
|
+
Training uses billable resources. Check your project permissions, balance and data format, then submit once:
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
el job submit --type train --file REQUEST.json
|
|
157
|
+
el job get JOB_ID
|
|
158
|
+
el job log JOB_ID --follow
|
|
159
|
+
el job result JOB_ID
|
|
160
|
+
el checkpoint list --job-id JOB_ID
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Replace `JOB_ID` with the receipt's value. Read the training status and logs, then find the registered checkpoint after success.
|
|
164
|
+
Ctrl-C ends log reading; to stop training, use `el job stop JOB_ID` and check its state and resource cleanup.
|
|
165
|
+
See [Checkpoints](https://docs.envloop.ai/training/checkpoints) to download or delete saved weights.
|
|
166
|
+
|
|
167
|
+
The built-in SFT recipe does not support custom loss, RL or optimizer-state resume.
|
|
168
|
+
For an older interactive training script, follow the [managed training migration](https://docs.envloop.ai/training/sdk-compatibility).
|
|
169
|
+
|
|
170
|
+
## Run a GPU Runner
|
|
171
|
+
|
|
172
|
+
Choose a published Runner readable in your project. Save this request as `gpu-run.json`, replacing `FULL_COMMIT` with the full Revision from `el runner list --json`:
|
|
173
|
+
|
|
174
|
+
```json
|
|
175
|
+
{
|
|
176
|
+
"request_id": "gpu-run-001",
|
|
177
|
+
"trials": [
|
|
178
|
+
{"name": "first", "spec": {"runner": "envhub/bash-runner@FULL_COMMIT", "resources": {"gpu_type": "H100", "gpus": 4}, "timeout_sec": 600}},
|
|
179
|
+
{"name": "second", "spec": {"runner": "envhub/bash-runner@FULL_COMMIT", "resources": {"gpu_type": "H100", "gpus": 4}, "timeout_sec": 600}}
|
|
180
|
+
]
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
el job submit --type train --file gpu-run.json
|
|
186
|
+
el job get JOB_ID
|
|
187
|
+
el job log JOB_ID --follow
|
|
188
|
+
el job result JOB_ID
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Replace `JOB_ID` with the returned value. The example has two independent Trials, each requesting four H100 GPUs: concurrent execution needs eight GPUs. Capacity and budget determine actual concurrency. Startup and execution are billable; check the Runner requirements, project `training.run` permission and [training prices](https://docs.envloop.ai/pricing) first. Reduce the Trial or GPU count when your program supports a smaller first run.
|
|
192
|
+
|
|
193
|
+
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
|
+
|
|
195
|
+
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.
|
|
@@ -3,30 +3,29 @@ envloop/agent_runs.py,sha256=ohBuiv6G2DcyItfPcMh68N9NpAmHwDkaZkyEqKwVQM8,10178
|
|
|
3
3
|
envloop/auth_cli.py,sha256=yTzNaqFFBBZ-rEp7XUW7Yn2K9Uz6IGe3i3Ye61_vccc,14826
|
|
4
4
|
envloop/auth_login.py,sha256=wURCu4HEhRlW3EGWn0MJvgPfe1wXhdplMmeQybNIWwQ,6527
|
|
5
5
|
envloop/auth_workspace.py,sha256=CC1EToAOcKW69Mvb4xZRkVltmXY4OpApfZaUc2ChB-c,14377
|
|
6
|
-
envloop/cli.py,sha256=
|
|
7
|
-
envloop/cli_errors.py,sha256=
|
|
6
|
+
envloop/cli.py,sha256=2FuP70kr5JpAzKP-4rJ_KKFu070wb6-XGyLkICfrGJk,35123
|
|
7
|
+
envloop/cli_errors.py,sha256=cruj0vydFrLOcy7vsOgxFE8kZDxVf5FxU-eQnvEEu50,25032
|
|
8
8
|
envloop/cli_output.py,sha256=bW2J7aCo9WqPqzIl2WHwVgpuQ7nGaaA2GEz1reMJczs,11151
|
|
9
|
-
envloop/client.py,sha256=
|
|
9
|
+
envloop/client.py,sha256=qznO0YaELy727uM0ogXl4KMG05LbIKUHBOwqpZxUH9k,32038
|
|
10
10
|
envloop/config.py,sha256=aSTztNusGD4JOvwMmTy6GpjlDs4eZrwnntnvzbjO1f8,19699
|
|
11
11
|
envloop/http.py,sha256=8pmCZRc3-Z70dLdE4nmgxbcFICKhxDO1XbfbcU-Bytc,15061
|
|
12
|
+
envloop/inference.py,sha256=L_2sMyM9y3k9d_dl8LXIu2vpMnEvua_a-77G0YZFViM,2095
|
|
12
13
|
envloop/job_logs.py,sha256=n1rOAg4XvRlqhuGl5Z1AazxhjwzgnDT0FGrtJFCI1AY,892
|
|
13
14
|
envloop/lifecycle_cli.py,sha256=Di7zzslJI09CBWJXgi2PrAqzCvvo8UfDh3fcFew7Fng,2966
|
|
14
15
|
envloop/navigation_resources.py,sha256=sTXBdPvSvekW2eQeQHp6nvf5kKqNfE0ZE7mZOkuJacg,15553
|
|
15
16
|
envloop/profile_secrets.py,sha256=jDDmBTO5D8hAUc74zkq7u44QWFTz-J0-brYBrk7zgqU,1107
|
|
16
17
|
envloop/profile_updates.py,sha256=YUQky5SSU7PZrjGFvVSVEcxm8CnayX4hsQljTGyc7zU,2975
|
|
17
18
|
envloop/resource_cli.py,sha256=YHDzA476SSqgarAIi5C61hGYWX0KJ9PWLPhbWmCDNtA,9846
|
|
18
|
-
envloop/resources.py,sha256=
|
|
19
|
+
envloop/resources.py,sha256=THh7uMdi8vxFHQklZXZlw4NmdLlInN980R2K96pe5cM,9303
|
|
19
20
|
envloop/rsi_cli.py,sha256=YQIGKqADS1cDIlsBlMTIfMXImOKiN8KOJ9yZRtMtLis,1831
|
|
20
21
|
envloop/sandbox.py,sha256=hqKq3MST89p47PuAtk1lm3lpk3f796I_5T6P_yV9uW4,1208
|
|
21
|
-
envloop/train.py,sha256=
|
|
22
|
+
envloop/train.py,sha256=sW-dzU1QRggW_Gvz3vnBtGwcRRKQBXPye63ROd5zpic,4515
|
|
22
23
|
envloop/train_log_cli.py,sha256=eqHYLLu1ohk4hYeio0qVhanrqBRpGRMRUcqkI-LWIuA,4201
|
|
23
|
-
envloop/
|
|
24
|
-
envloop/training/
|
|
25
|
-
envloop/training/sampling_client.py,sha256=195ze2IcbV0kcTIXmGiV8r6MJ5tdKyd-g73WJzxRQsY,3226
|
|
26
|
-
envloop/training/service_client.py,sha256=UF0nxQdSCrfas5mPsGKWXyNF5uYzNJGdTfUynDpk2sA,8277
|
|
24
|
+
envloop/train_runner_cli.py,sha256=hwfY0hskgiUMEE9AM3Onh1JC0WTdaiBVuoWVsltO8qI,3014
|
|
25
|
+
envloop/training/__init__.py,sha256=bg491cs_deETTEZ9ddjbLmxUvH3CiGJC7_438IRWto4,220
|
|
27
26
|
envloop/utils/__init__.py,sha256=huB3qPQuKNr3JxfH3Ys09YWFtHiDamCHFsBa4epANgQ,25
|
|
28
27
|
envloop/utils/logging.py,sha256=rXN0qyxhfGEq4vWXxjwfaczPPZf_7atnOkkBTgNNRPc,1017
|
|
29
|
-
envloop_cli-1.
|
|
30
|
-
envloop_cli-1.
|
|
31
|
-
envloop_cli-1.
|
|
32
|
-
envloop_cli-1.
|
|
28
|
+
envloop_cli-1.7.0.dist-info/METADATA,sha256=QQ_GerUhFYvb4O8TOvjW5kPlxXqEGnQj6-0_a1DlFPU,8314
|
|
29
|
+
envloop_cli-1.7.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
30
|
+
envloop_cli-1.7.0.dist-info/entry_points.txt,sha256=8aXOpgtVk4GndhChA4XNecShkb1w0xGHBHrKla78ofI,67
|
|
31
|
+
envloop_cli-1.7.0.dist-info/RECORD,,
|
envloop/training/credentials.py
DELETED
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
"""Per-service HTTP auth boundary for the pinned Tinker client's JWT renewals."""
|
|
2
|
-
|
|
3
|
-
import dataclasses
|
|
4
|
-
import re
|
|
5
|
-
|
|
6
|
-
import httpx
|
|
7
|
-
from tinker._base_client import DefaultAsyncHttpxClient
|
|
8
|
-
from tinker.lib.internal_client_holder import InternalClientHolder, _ShadowHolderConfig
|
|
9
|
-
|
|
10
|
-
_SCOPE_HEADERS = {'x-workspace-id', 'x-project-id', 'x-envloop-temporary-scope'}
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
class _ProcessLocalShadowConfig(_ShadowHolderConfig):
|
|
14
|
-
"""Prevent native SamplingClient pickle from exporting the bootstrap JWT.
|
|
15
|
-
|
|
16
|
-
Upstream serializes shadow_config rather than the holder itself. Its handle
|
|
17
|
-
export first calls without_credentials(), which returns the official plain
|
|
18
|
-
config shape without this process boundary or any authentication headers.
|
|
19
|
-
"""
|
|
20
|
-
|
|
21
|
-
def __reduce__(self):
|
|
22
|
-
raise TypeError('EnvLoop training services are process-local; export a sampler handle instead')
|
|
23
|
-
|
|
24
|
-
def without_credentials(self):
|
|
25
|
-
fields = dataclasses.asdict(self)
|
|
26
|
-
fields['api_key'] = None
|
|
27
|
-
fields['default_headers_overrides'] = {
|
|
28
|
-
name: value for name, value in self.default_headers_overrides.items()
|
|
29
|
-
if name.lower() in _SCOPE_HEADERS
|
|
30
|
-
}
|
|
31
|
-
return _ShadowHolderConfig(**fields)
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
class TrainingClientHolder(InternalClientHolder):
|
|
35
|
-
"""Instance-local transport options for the pinned Tinker 0.34 holder.
|
|
36
|
-
|
|
37
|
-
The high-level upstream constructor no longer forwards transport options.
|
|
38
|
-
Inject them before its first config request, so config, refresh, session and
|
|
39
|
-
REST pools all retain the same authenticated transport and timeout policy.
|
|
40
|
-
"""
|
|
41
|
-
|
|
42
|
-
def __init__(self, *, transport_options, **kwargs):
|
|
43
|
-
self._envloop_transport_options = transport_options
|
|
44
|
-
super().__init__(**kwargs)
|
|
45
|
-
|
|
46
|
-
async def _fetch_client_config(self, auth):
|
|
47
|
-
self._constructor_kwargs.update(self._envloop_transport_options)
|
|
48
|
-
return await super()._fetch_client_config(auth)
|
|
49
|
-
|
|
50
|
-
def __reduce__(self):
|
|
51
|
-
raise TypeError('EnvLoop training services are process-local; create a new training_service in each process')
|
|
52
|
-
|
|
53
|
-
@property
|
|
54
|
-
def shadow_config(self):
|
|
55
|
-
return _ProcessLocalShadowConfig(**dataclasses.asdict(super().shadow_config))
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
class _PlatformCredential(httpx.Auth):
|
|
59
|
-
def __init__(self, authorization):
|
|
60
|
-
self._authorization = authorization
|
|
61
|
-
|
|
62
|
-
def auth_flow(self, request):
|
|
63
|
-
# Never combine the original credential with Tinker's bootstrap JWT.
|
|
64
|
-
request.headers.pop('X-API-Key', None)
|
|
65
|
-
request.headers['Authorization'] = self._authorization
|
|
66
|
-
yield request
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
class TrainingHttpClient(DefaultAsyncHttpxClient):
|
|
70
|
-
"""Keep upstream JWT refresh, but authenticate each exchange with its source.
|
|
71
|
-
|
|
72
|
-
Only the exact configured config/exchange URLs receive the original key.
|
|
73
|
-
TrainingClientHolder forwards this through the pinned low-level client's
|
|
74
|
-
http_client option without global auth/environment hooks.
|
|
75
|
-
"""
|
|
76
|
-
|
|
77
|
-
def __init__(self, base_url, authorization):
|
|
78
|
-
super().__init__(follow_redirects=False)
|
|
79
|
-
self._credential_urls = {httpx.URL(base_url + path) for path in
|
|
80
|
-
('/api/v1/client/config', '/api/v1/auth/token')}
|
|
81
|
-
self._platform_credential = _PlatformCredential(authorization)
|
|
82
|
-
|
|
83
|
-
async def send(self, request, **kwargs):
|
|
84
|
-
# X-API-Key is not stripped by HTTPX on cross-origin redirects. Both
|
|
85
|
-
# bootstrap exchanges and ordinary JWT requests must stay on their URL.
|
|
86
|
-
kwargs['follow_redirects'] = False
|
|
87
|
-
exchange = request.method == 'POST' and request.url in self._credential_urls
|
|
88
|
-
if exchange:
|
|
89
|
-
kwargs['auth'] = self._platform_credential
|
|
90
|
-
response = await super().send(request, **kwargs)
|
|
91
|
-
if exchange and not 200 <= response.status_code < 300:
|
|
92
|
-
# Upstream logs refresh failures. Retain a bounded code/status, never
|
|
93
|
-
# a provider's arbitrary response body or potentially secret detail.
|
|
94
|
-
await response.aread()
|
|
95
|
-
try:
|
|
96
|
-
code = response.json().get('code')
|
|
97
|
-
except (ValueError, AttributeError):
|
|
98
|
-
code = None
|
|
99
|
-
if not isinstance(code, str) or not re.fullmatch(r'[A-Z][A-Z0-9_]{0,127}', code):
|
|
100
|
-
code = 'TRAINING_AUTH_FAILED'
|
|
101
|
-
await response.aclose()
|
|
102
|
-
return httpx.Response(response.status_code, request=response.request,
|
|
103
|
-
json={'code': code, 'error': 'EnvLoop training authorization failed'})
|
|
104
|
-
return response
|
|
105
|
-
|
|
106
|
-
def __reduce__(self):
|
|
107
|
-
raise TypeError('EnvLoop training services are process-local; create a new training_service in each process')
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
"""Restore official sampler handles using an explicit EnvLoop credential."""
|
|
2
|
-
|
|
3
|
-
import base64
|
|
4
|
-
import json
|
|
5
|
-
from urllib.parse import urlsplit
|
|
6
|
-
|
|
7
|
-
from tinker import SamplingClient as TinkerSamplingClient
|
|
8
|
-
from tinker.lib.internal_client_holder import _ShadowHolderConfig
|
|
9
|
-
from tinker.lib.public_interfaces.sampling_client import _SamplingClientPickleState
|
|
10
|
-
|
|
11
|
-
from .credentials import TrainingClientHolder
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
class SamplingClient(TinkerSamplingClient):
|
|
15
|
-
@staticmethod
|
|
16
|
-
def from_sampler_handle(handle, *, api_key=None):
|
|
17
|
-
fields = json.loads(base64.urlsafe_b64decode(handle))
|
|
18
|
-
config = _ShadowHolderConfig(**fields.pop('holder'))
|
|
19
|
-
state = _SamplingClientPickleState(**fields, holder=config)
|
|
20
|
-
url = urlsplit(config.base_url)
|
|
21
|
-
if not url.path.rstrip('/').endswith('/v1/job/tinker'):
|
|
22
|
-
return TinkerSamplingClient.from_sampler_handle(handle, api_key=api_key)
|
|
23
|
-
if not isinstance(api_key, str) or not api_key.strip():
|
|
24
|
-
raise ValueError('EnvLoop sampler handles require an explicit original EnvLoop api_key')
|
|
25
|
-
if url.scheme not in {'http', 'https'} or not url.hostname or url.username or url.password or url.query or url.fragment:
|
|
26
|
-
raise ValueError('invalid EnvLoop sampler API URL')
|
|
27
|
-
if config.api_key is not None:
|
|
28
|
-
raise ValueError('EnvLoop sampler handles must not contain credentials')
|
|
29
|
-
headers = {name.lower(): value for name, value in config.default_headers_overrides.items()}
|
|
30
|
-
if set(headers) - {'x-workspace-id', 'x-project-id', 'x-envloop-temporary-scope'}:
|
|
31
|
-
raise ValueError('EnvLoop sampler handles may contain only workspace, project and temporary scope headers')
|
|
32
|
-
|
|
33
|
-
from envloop import Client
|
|
34
|
-
from envloop.http import HttpService
|
|
35
|
-
|
|
36
|
-
base_url = config.base_url.rstrip('/')
|
|
37
|
-
# A missing scope means key-selected scope, never this worker's ambient
|
|
38
|
-
# EnvLoop configuration or environment.
|
|
39
|
-
client = Client(HttpService(base_url.removesuffix('/v1/job/tinker'), api_key=api_key,
|
|
40
|
-
workspace_id=headers.get('x-workspace-id'),
|
|
41
|
-
project_id=headers.get('x-project-id'), config=False, environment=False))
|
|
42
|
-
try:
|
|
43
|
-
service = client.training_service(temporary_scope_id=headers.get('x-envloop-temporary-scope'))
|
|
44
|
-
# Fetch current server flags with the explicit original key. Never
|
|
45
|
-
# trust cached auth flags from a portable handle or create a new job.
|
|
46
|
-
holder = TrainingClientHolder(
|
|
47
|
-
session_id=state.session_id, base_url=service._base_url, api_key=service._api_key,
|
|
48
|
-
default_headers_overrides=service._default_headers_overrides,
|
|
49
|
-
transport_options=service._envloop_transport_options,
|
|
50
|
-
)
|
|
51
|
-
return SamplingClient(holder, sampling_session_id=state.sampling_session_id,
|
|
52
|
-
shadow=True, record_stability_info=state.record_stability_info)
|
|
53
|
-
finally:
|
|
54
|
-
client.close()
|
|
55
|
-
|
|
56
|
-
def close(self):
|
|
57
|
-
"""Release this restored client's local resources without finishing its job."""
|
|
58
|
-
return self.holder.run_coroutine_threadsafe(self.holder.aclose())
|
|
@@ -1,165 +0,0 @@
|
|
|
1
|
-
"""EnvLoop checkpoint path adaptation for the pinned Tinker SDK."""
|
|
2
|
-
|
|
3
|
-
import re
|
|
4
|
-
import threading
|
|
5
|
-
|
|
6
|
-
from tinker import ServiceClient as TinkerServiceClient
|
|
7
|
-
from tinker.lib.public_interfaces.rest_client import RestClient as TinkerRestClient
|
|
8
|
-
|
|
9
|
-
_ENVLOOP_PATH = re.compile(
|
|
10
|
-
r"envloop://(?P<session>tts-[0-9a-f]{12}|job_[0-7][0-9A-HJKMNP-TV-Z]{25})/(?P<checkpoint>[A-Za-z0-9_-][A-Za-z0-9_.-]*)"
|
|
11
|
-
)
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def _checked(path):
|
|
15
|
-
match = _ENVLOOP_PATH.fullmatch(path)
|
|
16
|
-
if match is None:
|
|
17
|
-
raise ValueError("invalid envloop checkpoint path")
|
|
18
|
-
return match
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
def tinker_path(path):
|
|
22
|
-
"""Map ``envloop://<session>/<checkpoint>`` onto Tinker's checkpoint path form.
|
|
23
|
-
|
|
24
|
-
The platform resolves ``tinker://<session>/weights/<checkpoint>`` to the same stored
|
|
25
|
-
checkpoint whatever its type; ``tinker://`` paths (what save_state returns) pass through.
|
|
26
|
-
"""
|
|
27
|
-
if not isinstance(path, str) or not path.startswith("envloop://"):
|
|
28
|
-
return path
|
|
29
|
-
match = _checked(path)
|
|
30
|
-
return f"tinker://{match['session']}/weights/{match['checkpoint']}"
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
class RestClient(TinkerRestClient):
|
|
34
|
-
"""Tinker RestClient whose ``*_from_tinker_path`` helpers also accept envloop:// paths."""
|
|
35
|
-
|
|
36
|
-
def get_training_run_by_tinker_path(self, tinker_path_, *args, **kwargs):
|
|
37
|
-
return super().get_training_run_by_tinker_path(tinker_path(tinker_path_), *args, **kwargs)
|
|
38
|
-
|
|
39
|
-
async def get_training_run_by_tinker_path_async(self, tinker_path_, *args, **kwargs):
|
|
40
|
-
return await super().get_training_run_by_tinker_path_async(tinker_path(tinker_path_), *args, **kwargs)
|
|
41
|
-
|
|
42
|
-
def get_weights_info_by_tinker_path(self, tinker_path_):
|
|
43
|
-
return super().get_weights_info_by_tinker_path(tinker_path(tinker_path_))
|
|
44
|
-
|
|
45
|
-
def delete_checkpoint_from_tinker_path(self, tinker_path_):
|
|
46
|
-
return super().delete_checkpoint_from_tinker_path(tinker_path(tinker_path_))
|
|
47
|
-
|
|
48
|
-
async def delete_checkpoint_from_tinker_path_async(self, tinker_path_):
|
|
49
|
-
return await super().delete_checkpoint_from_tinker_path_async(tinker_path(tinker_path_))
|
|
50
|
-
|
|
51
|
-
def get_checkpoint_archive_url_from_tinker_path(self, tinker_path_):
|
|
52
|
-
return super().get_checkpoint_archive_url_from_tinker_path(tinker_path(tinker_path_))
|
|
53
|
-
|
|
54
|
-
async def get_checkpoint_archive_url_from_tinker_path_async(self, tinker_path_):
|
|
55
|
-
return await super().get_checkpoint_archive_url_from_tinker_path_async(tinker_path(tinker_path_))
|
|
56
|
-
|
|
57
|
-
def publish_checkpoint_from_tinker_path(self, tinker_path_):
|
|
58
|
-
return super().publish_checkpoint_from_tinker_path(tinker_path(tinker_path_))
|
|
59
|
-
|
|
60
|
-
async def publish_checkpoint_from_tinker_path_async(self, tinker_path_):
|
|
61
|
-
return await super().publish_checkpoint_from_tinker_path_async(tinker_path(tinker_path_))
|
|
62
|
-
|
|
63
|
-
def unpublish_checkpoint_from_tinker_path(self, tinker_path_):
|
|
64
|
-
return super().unpublish_checkpoint_from_tinker_path(tinker_path(tinker_path_))
|
|
65
|
-
|
|
66
|
-
async def unpublish_checkpoint_from_tinker_path_async(self, tinker_path_):
|
|
67
|
-
return await super().unpublish_checkpoint_from_tinker_path_async(tinker_path(tinker_path_))
|
|
68
|
-
|
|
69
|
-
def set_checkpoint_ttl_from_tinker_path(self, tinker_path_, ttl_seconds):
|
|
70
|
-
return super().set_checkpoint_ttl_from_tinker_path(tinker_path(tinker_path_), ttl_seconds)
|
|
71
|
-
|
|
72
|
-
async def set_checkpoint_ttl_from_tinker_path_async(self, tinker_path_, ttl_seconds):
|
|
73
|
-
return await super().set_checkpoint_ttl_from_tinker_path_async(tinker_path(tinker_path_), ttl_seconds)
|
|
74
|
-
|
|
75
|
-
def copy_weights(self, path, destination_project_id=None, *, ttl_seconds=None, weights_access_token=None):
|
|
76
|
-
return super().copy_weights(tinker_path(path), destination_project_id,
|
|
77
|
-
ttl_seconds=ttl_seconds, weights_access_token=weights_access_token)
|
|
78
|
-
|
|
79
|
-
async def copy_weights_async(self, path, destination_project_id=None, *, ttl_seconds=None, weights_access_token=None):
|
|
80
|
-
return await super().copy_weights_async(tinker_path(path), destination_project_id,
|
|
81
|
-
ttl_seconds=ttl_seconds, weights_access_token=weights_access_token)
|
|
82
|
-
|
|
83
|
-
def get_external_weights_urls(self, path):
|
|
84
|
-
return super().get_external_weights_urls(self._external_path(path))
|
|
85
|
-
|
|
86
|
-
async def get_external_weights_urls_async(self, path):
|
|
87
|
-
return await super().get_external_weights_urls_async(self._external_path(path))
|
|
88
|
-
|
|
89
|
-
@staticmethod
|
|
90
|
-
def _external_path(path):
|
|
91
|
-
if isinstance(path, str) and path.startswith("envloop://"):
|
|
92
|
-
match = _checked(path)
|
|
93
|
-
return f"tinker://{match['session']}/external_weights/{match['checkpoint']}"
|
|
94
|
-
return path
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
class ServiceClient(TinkerServiceClient):
|
|
98
|
-
"""Tinker ServiceClient accepting EnvLoop checkpoint paths.
|
|
99
|
-
|
|
100
|
-
Tinker validates sampling paths in its shared session holder, and its
|
|
101
|
-
RestClient parses paths client-side, before sending HTTP. Adapt those boundaries
|
|
102
|
-
so ServiceClient, its TrainingClients and RestClients accept envloop:// without
|
|
103
|
-
replacing native futures, training clients, authentication, or globally patching
|
|
104
|
-
the upstream SDK. Platform responses already use Tinker's tinker:// form.
|
|
105
|
-
"""
|
|
106
|
-
|
|
107
|
-
def __init__(self, *args, **kwargs):
|
|
108
|
-
# 0.34's public constructor silently ignores these previously supported
|
|
109
|
-
# options. Keep EnvLoop's per-service auth/timeout boundary explicit.
|
|
110
|
-
self._envloop_transport_options = {name: kwargs.pop(name) for name in
|
|
111
|
-
('http_client', 'timeout', 'max_retries') if name in kwargs}
|
|
112
|
-
headers = kwargs.pop('default_headers', None)
|
|
113
|
-
if headers is not None:
|
|
114
|
-
kwargs['default_headers_overrides'] = dict(headers) | dict(kwargs.get('default_headers_overrides') or {})
|
|
115
|
-
super().__init__(*args, **kwargs)
|
|
116
|
-
self._checkpoint_adapter_lock = threading.Lock()
|
|
117
|
-
self._checkpoint_adapter_holder = None
|
|
118
|
-
|
|
119
|
-
def create_rest_client(self) -> RestClient:
|
|
120
|
-
return RestClient(self._get_rest_holder())
|
|
121
|
-
|
|
122
|
-
def _get_session_holder(self):
|
|
123
|
-
holder = self._envloop_holder(session=True) if self._envloop_transport_options else super()._get_session_holder()
|
|
124
|
-
with self._checkpoint_adapter_lock:
|
|
125
|
-
if self._checkpoint_adapter_holder is not holder:
|
|
126
|
-
create = holder._create_sampling_session
|
|
127
|
-
|
|
128
|
-
async def create_sampling_session(model_path=None, base_model=None):
|
|
129
|
-
if isinstance(model_path, str) and model_path.startswith("envloop://"):
|
|
130
|
-
_checked(model_path)
|
|
131
|
-
model_path = "tinker://" + model_path.removeprefix("envloop://")
|
|
132
|
-
return await create(model_path=model_path, base_model=base_model)
|
|
133
|
-
|
|
134
|
-
holder._create_sampling_session = create_sampling_session
|
|
135
|
-
self._checkpoint_adapter_holder = holder
|
|
136
|
-
return holder
|
|
137
|
-
|
|
138
|
-
def _get_rest_holder(self):
|
|
139
|
-
return self._envloop_holder(session=False) if self._envloop_transport_options else super()._get_rest_holder()
|
|
140
|
-
|
|
141
|
-
def _envloop_holder(self, *, session):
|
|
142
|
-
from .credentials import TrainingClientHolder
|
|
143
|
-
|
|
144
|
-
attribute = '_session_holder' if session else '_rest_holder'
|
|
145
|
-
lock = self._session_holder_lock if session else self._rest_holder_lock
|
|
146
|
-
with self._lifecycle_lock:
|
|
147
|
-
if self._closed:
|
|
148
|
-
raise RuntimeError('ServiceClient is closed')
|
|
149
|
-
holder = getattr(self, attribute)
|
|
150
|
-
if holder is not None:
|
|
151
|
-
return holder
|
|
152
|
-
with lock:
|
|
153
|
-
with self._lifecycle_lock:
|
|
154
|
-
if self._closed:
|
|
155
|
-
raise RuntimeError('ServiceClient is closed')
|
|
156
|
-
holder = getattr(self, attribute)
|
|
157
|
-
if holder is None:
|
|
158
|
-
options = dict(user_metadata=self._user_metadata, project_id=self._project_id,
|
|
159
|
-
display_name=self._display_name) if session else {'_skip_session': True}
|
|
160
|
-
holder = TrainingClientHolder(
|
|
161
|
-
base_url=self._base_url, api_key=self._api_key,
|
|
162
|
-
default_headers_overrides=self._default_headers_overrides,
|
|
163
|
-
transport_options=self._envloop_transport_options, **options)
|
|
164
|
-
setattr(self, attribute, holder)
|
|
165
|
-
return holder
|
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.5
|
|
2
|
-
Name: envloop-cli
|
|
3
|
-
Version: 1.4.0
|
|
4
|
-
Summary: EnvLoop CLI and Python SDK for jobs, training, checkpoints and inference
|
|
5
|
-
Project-URL: Homepage, https://envloop.ai
|
|
6
|
-
Project-URL: Documentation, https://docs.envloop.ai
|
|
7
|
-
Project-URL: Repository, https://github.com/EnvLoop/envloop-cli
|
|
8
|
-
Project-URL: Issues, https://github.com/EnvLoop/envloop-cli/issues
|
|
9
|
-
Project-URL: Agent instructions, https://docs.envloop.ai/skill.md
|
|
10
|
-
Requires-Python: >=3.11
|
|
11
|
-
Requires-Dist: tinker==0.34.0
|
|
12
|
-
Description-Content-Type: text/markdown
|
|
13
|
-
|
|
14
|
-
# envloop-cli: EnvLoop CLI and Python SDK
|
|
15
|
-
|
|
16
|
-
Install the public client to run agent Jobs, inspect Trials, manage training and
|
|
17
|
-
checkpoints, and deploy or call inference endpoints. The `el` and `envloop`
|
|
18
|
-
commands are equivalent. The Python import package is `envloop`.
|
|
19
|
-
|
|
20
|
-
## Install
|
|
21
|
-
|
|
22
|
-
Use a Python virtual environment:
|
|
23
|
-
|
|
24
|
-
```bash
|
|
25
|
-
python -m pip install --upgrade envloop-cli
|
|
26
|
-
el --help
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
The standard installation includes training support:
|
|
30
|
-
|
|
31
|
-
```bash
|
|
32
|
-
python -c 'from envloop import Client, training; print("EnvLoop SDK ready")'
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
The package installs the training protocol client by default. You do not need a source checkout, GitHub account,
|
|
36
|
-
local execution backend, Docker, or a GPU to install the client.
|
|
37
|
-
|
|
38
|
-
## Connect
|
|
39
|
-
|
|
40
|
-
Use an authorized EnvLoop account and project. Browser authorization:
|
|
41
|
-
|
|
42
|
-
```bash
|
|
43
|
-
el auth login
|
|
44
|
-
el auth status --check
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
For an existing project API key, use the masked configuration wizard:
|
|
48
|
-
|
|
49
|
-
```bash
|
|
50
|
-
el auth config
|
|
51
|
-
el auth status --check
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
CI can read credentials from `ENVLOOP_API_KEY` and connection settings from
|
|
55
|
-
`ENVLOOP_BASE_URL`, `ENVLOOP_WORKSPACE_ID` and `ENVLOOP_PROJECT_ID`. Keep keys in
|
|
56
|
-
your secret manager. Public API access is scoped to the authorized project.
|
|
57
|
-
|
|
58
|
-
## Explore your project
|
|
59
|
-
|
|
60
|
-
```bash
|
|
61
|
-
el job list --limit 5
|
|
62
|
-
el job list --type train --limit 5
|
|
63
|
-
el trail list --limit 5
|
|
64
|
-
el trail list --type train --job-id JOB_ID
|
|
65
|
-
el checkpoint list --limit 5
|
|
66
|
-
el job list --type inference
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
Query a returned Job ID with `el job get JOB_ID` and `el job result JOB_ID`.
|
|
70
|
-
Trial commands use the spelling `trail`, for example
|
|
71
|
-
`el trail artifact list TRIAL_ID`. Use each command's `--help` for options.
|
|
72
|
-
|
|
73
|
-
For the Python SDK:
|
|
74
|
-
|
|
75
|
-
```python
|
|
76
|
-
from envloop import Client
|
|
77
|
-
|
|
78
|
-
with Client.remote() as client:
|
|
79
|
-
print(client.jobs.list(limit=5))
|
|
80
|
-
print(client.trials.list(type="train", limit=5))
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
Submitting work, training a model or starting inference can allocate billable
|
|
84
|
-
resources. Follow the task-specific guide and retain the outputs you need before
|
|
85
|
-
cleaning up temporary resources.
|
|
86
|
-
|
|
87
|
-
## Sandbox jobs
|
|
88
|
-
|
|
89
|
-
With a matching platform and a published `envhub/base` ANS Runner, submit a
|
|
90
|
-
Sandbox Job without a Dataset:
|
|
91
|
-
|
|
92
|
-
```bash
|
|
93
|
-
el job submit --type sandbox --runner base --command 'python --version'
|
|
94
|
-
el job submit --type sandbox --runner base --image python:3.11 --wait
|
|
95
|
-
el job list --type sandbox
|
|
96
|
-
el job log JOB_ID --follow
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
The platform resolves `base` to the registered `envhub/base@main` source ref and
|
|
100
|
-
freezes its immutable Revision. The Sandbox image defaults to `python:3.11`;
|
|
101
|
-
`--image` accepts an image name, tag or digest. The execution provider is configured
|
|
102
|
-
by the server. Job logs retain the actual captured output and collection metadata.
|
|
103
|
-
Failures include a code, readable explanation, HTTP status when available, and
|
|
104
|
-
recovery guidance without echoing arbitrary response bodies or credentials.
|
|
105
|
-
|
|
106
|
-
## Existing installations
|
|
107
|
-
|
|
108
|
-
This distribution provides both the CLI and the `envloop` import package. Use a
|
|
109
|
-
fresh environment if you previously installed the separate source distribution
|
|
110
|
-
named `envloop`, so two distributions do not own the same Python module files.
|
|
111
|
-
Existing client configuration is reused; installing a package does not change
|
|
112
|
-
your account permissions or move remote resources. Check `el --help` when
|
|
113
|
-
upgrading because command groups may differ from your earlier installation.
|
|
114
|
-
|
|
115
|
-
## Documentation
|
|
116
|
-
|
|
117
|
-
- [English documentation](https://docs.envloop.ai/)
|
|
118
|
-
- [中文文档](https://docs.envloop.ai/zh-cn/)
|
|
119
|
-
- [Installation](https://docs.envloop.ai/cli-install)
|
|
120
|
-
- [CLI reference](https://docs.envloop.ai/reference/cli)
|
|
121
|
-
- [Training and deployment](https://docs.envloop.ai/training/train-deploy)
|
|
122
|
-
- [Agent entry: skill.md](https://docs.envloop.ai/skill.md)
|
|
File without changes
|
|
File without changes
|