envloop-cli 1.27.0__py3-none-any.whl → 1.28.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/_optional.py +3 -3
- envloop/auth_cli.py +13 -11
- envloop/lifecycle_cli.py +4 -4
- envloop/resource_cli.py +28 -28
- {envloop_cli-1.27.0.dist-info → envloop_cli-1.28.0.dist-info}/METADATA +4 -4
- {envloop_cli-1.27.0.dist-info → envloop_cli-1.28.0.dist-info}/RECORD +27 -27
- envloop_data/cli.py +14 -14
- envloop_data/cli_output.py +2 -2
- envloop_data/data_cli.py +10 -10
- envloop_data/http.py +2 -2
- envloop_data/navigation_resources.py +9 -9
- envloop_data/resource_cli.py +18 -18
- envloop_experiment/cli.py +16 -16
- envloop_experiment/client.py +11 -11
- envloop_experiment/decision.py +1 -1
- envloop_experiment/http.py +9 -9
- envloop_experiment/job_cost.py +2 -2
- envloop_experiment/job_spec_cli.py +2 -2
- envloop_experiment/resource_cli.py +5 -5
- envloop_experiment/resources.py +2 -2
- envloop_experiment/train.py +10 -10
- envloop_experiment/train_runner_cli.py +1 -1
- envloop_rsi_loop/agent_runs.py +14 -19
- envloop_rsi_loop/cli.py +1 -1
- envloop_rsi_loop/cli_output.py +8 -8
- {envloop_cli-1.27.0.dist-info → envloop_cli-1.28.0.dist-info}/WHEEL +0 -0
- {envloop_cli-1.27.0.dist-info → envloop_cli-1.28.0.dist-info}/entry_points.txt +0 -0
envloop/_optional.py
CHANGED
|
@@ -16,11 +16,11 @@ from typing import Any, override
|
|
|
16
16
|
FEATURES = {'data': 'envloop_data', 'experiment': 'envloop_experiment', 'rsi-loop': 'envloop_rsi_loop'}
|
|
17
17
|
FEATURE_COMMANDS = {
|
|
18
18
|
'data': {
|
|
19
|
-
'dataset': 'Discover
|
|
19
|
+
'dataset': 'Discover Datasets, revisions and tasks',
|
|
20
20
|
'data': 'Manage versioned Data resources and verified file downloads',
|
|
21
21
|
'runner': 'Discover available Runner revisions',
|
|
22
|
-
'checkpoint': 'List and download saved training
|
|
23
|
-
'profile': 'Manage platform runtime
|
|
22
|
+
'checkpoint': 'List and download saved training Checkpoints',
|
|
23
|
+
'profile': 'Manage platform runtime Profiles',
|
|
24
24
|
},
|
|
25
25
|
'experiment': {
|
|
26
26
|
'job': 'Submit and manage Agent, Sandbox, Train and Inference Jobs',
|
envloop/auth_cli.py
CHANGED
|
@@ -8,6 +8,8 @@ from typing import Any, cast
|
|
|
8
8
|
from .config import ConfigError, ConfigStore, normalize_url, validate_key
|
|
9
9
|
|
|
10
10
|
DEFAULT_BASE_URL = "https://api.envloop.ai"
|
|
11
|
+
CONFIG_FIELDS = ["base-url", "api-key", "workspace-id", "project-id"]
|
|
12
|
+
FIELD_HELP = "One of " + ", ".join(CONFIG_FIELDS)
|
|
11
13
|
|
|
12
14
|
|
|
13
15
|
def add_auth_commands(groups: "argparse._SubParsersAction[argparse.ArgumentParser]") -> None:
|
|
@@ -23,29 +25,29 @@ def add_auth_commands(groups: "argparse._SubParsersAction[argparse.ArgumentParse
|
|
|
23
25
|
actions = configure.add_subparsers(dest="config_action")
|
|
24
26
|
init = actions.add_parser("init", help="Configure interactively with masked API key input")
|
|
25
27
|
init.add_argument("--no-check", action="store_true", default=argparse.SUPPRESS, help="Save without API verification (offline)")
|
|
26
|
-
project = actions.add_parser("project", help="Select and verify a
|
|
27
|
-
project.add_argument("project_id", nargs="?", help="Project ID; omit for an interactive list with names and IDs")
|
|
28
|
-
project.add_argument("--stdin", action="store_true", help="Read the selected
|
|
28
|
+
project = actions.add_parser("project", help="Select and verify a Project with its own API key")
|
|
29
|
+
project.add_argument("project_id", metavar="PROJECT_ID", nargs="?", help="Project ID; omit for an interactive list with names and IDs")
|
|
30
|
+
project.add_argument("--stdin", action="store_true", help="Read the selected Project's API key from stdin")
|
|
29
31
|
setting = actions.add_parser("set", help="Save one field (API key is masked or read from stdin)")
|
|
30
|
-
setting.add_argument("field",
|
|
31
|
-
setting.add_argument("value", nargs="?", help=f"Base URL (default: {DEFAULT_BASE_URL}) or
|
|
32
|
+
setting.add_argument("field", metavar="FIELD", choices=CONFIG_FIELDS, help=FIELD_HELP)
|
|
33
|
+
setting.add_argument("value", metavar="VALUE", nargs="?", help=f"Base URL (default: {DEFAULT_BASE_URL}) or Workspace ID; API keys use masked input or --stdin")
|
|
32
34
|
setting.add_argument("--stdin", action="store_true", help="Read a single API key from stdin")
|
|
33
35
|
setting.add_argument("--no-check", action="store_true", default=argparse.SUPPRESS, help="Save API key without verification (offline)")
|
|
34
36
|
getting = actions.add_parser("get", help="Read saved configuration; never export an API key")
|
|
35
|
-
getting.add_argument("field",
|
|
37
|
+
getting.add_argument("field", metavar="FIELD", choices=CONFIG_FIELDS, help=FIELD_HELP)
|
|
36
38
|
actions.add_parser("path", help="Print configuration and credential file paths")
|
|
37
39
|
show = actions.add_parser("show", help="Show saved and effective settings without revealing keys")
|
|
38
40
|
show.add_argument("--json", action="store_true")
|
|
39
41
|
remove = actions.add_parser("unset", help="Delete a saved field; does not revoke the server key")
|
|
40
|
-
remove.add_argument("field",
|
|
42
|
+
remove.add_argument("field", metavar="FIELD", choices=CONFIG_FIELDS, help=FIELD_HELP)
|
|
41
43
|
status = commands.add_parser("status", help="Inspect local credentials; --check verifies read access")
|
|
42
44
|
status.add_argument("--check", action="store_true", help="Make a read-only request to the effective Base URL")
|
|
43
45
|
status.add_argument("--json", action="store_true")
|
|
44
46
|
commands.add_parser("logout", help="Remove the saved key for the stored default Base URL (local only)")
|
|
45
|
-
workspace = commands.add_parser("workspace", help="Select and verify the active
|
|
46
|
-
workspace.add_argument("workspace_id", nargs="?", help="Workspace ID; omit for an interactive list")
|
|
47
|
-
workspace.add_argument("--project-id", help="Project within the
|
|
48
|
-
workspace.add_argument("--stdin", action="store_true", help="Read the selected
|
|
47
|
+
workspace = commands.add_parser("workspace", help="Select and verify the active Workspace")
|
|
48
|
+
workspace.add_argument("workspace_id", metavar="WORKSPACE_ID", nargs="?", help="Workspace ID; omit for an interactive list")
|
|
49
|
+
workspace.add_argument("--project-id", help="Project within the Workspace; its own API key is required")
|
|
50
|
+
workspace.add_argument("--stdin", action="store_true", help="Read the selected Workspace's API key from stdin")
|
|
49
51
|
|
|
50
52
|
|
|
51
53
|
def _key_from_input(stdin: bool = False) -> str:
|
envloop/lifecycle_cli.py
CHANGED
|
@@ -6,7 +6,7 @@ from typing import Any
|
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
def wait_options(parser: argparse.ArgumentParser):
|
|
9
|
-
parser.add_argument('--wait', action='store_true', help='Wait for the persisted cleanup
|
|
9
|
+
parser.add_argument('--wait', action='store_true', help='Wait for the persisted cleanup Operation')
|
|
10
10
|
parser.add_argument('--timeout-seconds', type=float, default=600)
|
|
11
11
|
|
|
12
12
|
|
|
@@ -14,10 +14,10 @@ def add_management_commands(settings: "argparse._SubParsersAction[argparse.Argum
|
|
|
14
14
|
operations = settings.add_parser('operation', help='Inspect or retry persisted cleanup').add_subparsers(dest='operation_action', required=True)
|
|
15
15
|
for action in ('get', 'retry', 'wait'):
|
|
16
16
|
command = operations.add_parser(action)
|
|
17
|
-
command.add_argument('identifier')
|
|
17
|
+
command.add_argument('identifier', metavar='OPERATION_ID')
|
|
18
18
|
if action == 'wait':
|
|
19
19
|
command.add_argument('--timeout-seconds', type=float, default=600)
|
|
20
|
-
cleanup = settings.add_parser('cleanup', help='Clean an explicit set of
|
|
20
|
+
cleanup = settings.add_parser('cleanup', help='Clean an explicit set of Project resource IDs')
|
|
21
21
|
cleanup.add_argument('--file', type=Path, required=True, help='JSON array of resource kind and ID objects')
|
|
22
22
|
cleanup.add_argument('--request-id', required=True)
|
|
23
23
|
wait_options(cleanup)
|
|
@@ -27,7 +27,7 @@ def add_management_commands(settings: "argparse._SubParsersAction[argparse.Argum
|
|
|
27
27
|
create.add_argument('--request-id', required=True)
|
|
28
28
|
for action in ('get', 'cleanup', 'retain'):
|
|
29
29
|
command = scopes.add_parser(action)
|
|
30
|
-
command.add_argument('identifier')
|
|
30
|
+
command.add_argument('identifier', metavar='RESOURCE_SCOPE_ID')
|
|
31
31
|
if action == 'cleanup':
|
|
32
32
|
wait_options(command)
|
|
33
33
|
if action == 'retain':
|
envloop/resource_cli.py
CHANGED
|
@@ -17,10 +17,10 @@ def listing(commands: "argparse._SubParsersAction[argparse.ArgumentParser]", *,
|
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
def add_resource_commands(groups: "argparse._SubParsersAction[argparse.ArgumentParser]") -> None:
|
|
20
|
-
dashboard = groups.add_parser('dashboard', help='Inspect the current
|
|
21
|
-
dashboard.add_parser('get', help='Show
|
|
20
|
+
dashboard = groups.add_parser('dashboard', help='Inspect the current Project overview').add_subparsers(dest='action', required=True)
|
|
21
|
+
dashboard.add_parser('get', help='Show Project details and recent experiment pages')
|
|
22
22
|
|
|
23
|
-
billing = groups.add_parser('billing', help='Inspect
|
|
23
|
+
billing = groups.add_parser('billing', help='Inspect Project billing and recorded usage').add_subparsers(dest='resource', required=True)
|
|
24
24
|
for resource in ('account', 'usage', 'ledger', 'price'):
|
|
25
25
|
commands = billing.add_parser(resource, help=f'Inspect billing {resource}').add_subparsers(dest='action', required=True)
|
|
26
26
|
command = commands.add_parser('get', help='Show the billing account') if resource == 'account' else listing(commands, paged=False)
|
|
@@ -31,27 +31,27 @@ def add_resource_commands(groups: "argparse._SubParsersAction[argparse.ArgumentP
|
|
|
31
31
|
if resource == 'ledger':
|
|
32
32
|
command.add_argument('--limit', type=int, default=25)
|
|
33
33
|
|
|
34
|
-
limits = groups.add_parser('limit', help='Inspect
|
|
34
|
+
limits = groups.add_parser('limit', help='Inspect Project resource limits').add_subparsers(dest='action', required=True)
|
|
35
35
|
limits.add_parser('get', help='Read effective execution limits, compose Runner state and the registry credential '
|
|
36
36
|
'count (read-only)')
|
|
37
|
-
members = groups.add_parser('member', help='Manage
|
|
37
|
+
members = groups.add_parser('member', help='Manage Project members subject to server authorization').add_subparsers(dest='action', required=True)
|
|
38
38
|
listing(members, paged=False)
|
|
39
|
-
candidates = members.add_parser('candidate', help='Find eligible
|
|
40
|
-
listing(candidates, paged=False, help='List eligible
|
|
39
|
+
candidates = members.add_parser('candidate', help='Find eligible Project members').add_subparsers(dest='candidate_action', required=True)
|
|
40
|
+
listing(candidates, paged=False, help='List eligible Project members matching a query').add_argument('--query', required=True)
|
|
41
41
|
for action in ('add', 'update', 'remove'):
|
|
42
|
-
command = members.add_parser(action, help=f'{action.capitalize()} a
|
|
43
|
-
command.add_argument('user_id')
|
|
42
|
+
command = members.add_parser(action, help=f'{action.capitalize()} a Project membership (human-session authorization required)')
|
|
43
|
+
command.add_argument('user_id', metavar='USER_ID')
|
|
44
44
|
if action != 'remove':
|
|
45
45
|
command.add_argument('--role', choices=['viewer', 'developer', 'admin'], default='developer')
|
|
46
46
|
command.add_argument('--expected-version', required=True, type=int, help='Current membership version; use 0 for a new grant')
|
|
47
47
|
|
|
48
|
-
settings = groups.add_parser('setting', help='Inspect and update the current
|
|
49
|
-
settings.add_parser('get', help='Show current
|
|
50
|
-
update = settings.add_parser('update', help='Change
|
|
48
|
+
settings = groups.add_parser('setting', help='Inspect and update the current Project settings').add_subparsers(dest='action', required=True)
|
|
49
|
+
settings.add_parser('get', help='Show current Project settings')
|
|
50
|
+
update = settings.add_parser('update', help='Change Project fields (human-session authorization required)')
|
|
51
51
|
for field in ('name', 'slug', 'region'):
|
|
52
52
|
update.add_argument('--' + field)
|
|
53
53
|
update.add_argument('--expected-version', required=True, type=int)
|
|
54
|
-
archive = settings.add_parser('archive', help='Archive the
|
|
54
|
+
archive = settings.add_parser('archive', help='Archive the Project (human-session authorization required)')
|
|
55
55
|
archive.add_argument('--expected-version', required=True, type=int)
|
|
56
56
|
from .lifecycle_cli import add_management_commands
|
|
57
57
|
add_management_commands(settings)
|
|
@@ -89,40 +89,40 @@ def _revision(value: str) -> str:
|
|
|
89
89
|
|
|
90
90
|
def add_self_service_commands(settings: "argparse._SubParsersAction[argparse.ArgumentParser]") -> None:
|
|
91
91
|
"""Project Admin settings that also work with an API key whose ceiling includes them."""
|
|
92
|
-
registry = settings.add_parser('registry', help='Manage private registry credentials for this
|
|
92
|
+
registry = settings.add_parser('registry', help='Manage private registry credentials for this Project\'s images'
|
|
93
93
|
).add_subparsers(dest='registry_action', required=True)
|
|
94
94
|
listing(registry, paged=False, help='List registry credentials (metadata only; secrets are write-only)')
|
|
95
|
-
add = registry.add_parser('add', help='Register a write-only registry credential (
|
|
95
|
+
add = registry.add_parser('add', help='Register a write-only registry credential (Project Admin)')
|
|
96
96
|
add.add_argument('--registry', required=True, help='Registry host, for example ghcr.io')
|
|
97
97
|
add.add_argument('--repository-prefix', required=True, help='Repository prefix the credential covers, for example my-org')
|
|
98
98
|
add.add_argument('--username', required=True)
|
|
99
99
|
add.add_argument('--password-stdin', action='store_true',
|
|
100
100
|
help='Read the token or password from stdin; without it you are prompted (no echo)')
|
|
101
101
|
add.add_argument('--password', '--token', nargs='?', action=_RejectSecretArgument, help=argparse.SUPPRESS)
|
|
102
|
-
remove = registry.add_parser('remove', help='Remove a credential; later image pulls no longer receive it (
|
|
103
|
-
remove.add_argument('credential_id')
|
|
104
|
-
compose = settings.add_parser('compose', help='Compose Runner (multi-container Task) access for this
|
|
102
|
+
remove = registry.add_parser('remove', help='Remove a credential; later image pulls no longer receive it (Project Admin)')
|
|
103
|
+
remove.add_argument('credential_id', metavar='CREDENTIAL_ID')
|
|
104
|
+
compose = settings.add_parser('compose', help='Compose Runner (multi-container Task) access for this Project'
|
|
105
105
|
).add_subparsers(dest='compose_action', required=True)
|
|
106
106
|
compose.add_parser('get', help='Show the effective state, the reason and the platform bounds')
|
|
107
|
-
compose.add_parser('enable', help='Request compose Runners; the platform policy may need operator approval (
|
|
108
|
-
compose.add_parser('disable', help='Withdraw this
|
|
109
|
-
concurrency = settings.add_parser('concurrency', help='Concurrent Trial limit of this
|
|
107
|
+
compose.add_parser('enable', help='Request compose Runners; the platform policy may need operator approval (Project Admin)')
|
|
108
|
+
compose.add_parser('disable', help='Withdraw this Project\'s compose Runner request (Project Admin)')
|
|
109
|
+
concurrency = settings.add_parser('concurrency', help='Concurrent Trial limit of this Project'
|
|
110
110
|
).add_subparsers(dest='concurrency_action', required=True)
|
|
111
|
-
command = concurrency.add_parser('set', help='Lower the
|
|
112
|
-
'(
|
|
111
|
+
command = concurrency.add_parser('set', help='Lower the Project limit below the operator ceiling, or none to clear it '
|
|
112
|
+
'(Project Admin)')
|
|
113
113
|
command.add_argument('value', type=_concurrency, metavar='VALUE', help='1..10000 or none')
|
|
114
|
-
model = settings.add_parser('model-access', help='Metered model access for published Runners of this
|
|
114
|
+
model = settings.add_parser('model-access', help='Metered model access for published Runners of this Project'
|
|
115
115
|
).add_subparsers(dest='model_access_action', required=True)
|
|
116
116
|
listing(model, paged=False, help='List grants and whether the platform can deliver them now')
|
|
117
|
-
grant = model.add_parser('grant', help='Let one published Runner receive the
|
|
118
|
-
'Trials (
|
|
117
|
+
grant = model.add_parser('grant', help='Let one published Runner receive the Project\'s metered model key on Fly '
|
|
118
|
+
'Trials (Project Admin)')
|
|
119
119
|
grant.add_argument('runner', type=_runner_name, metavar='NAMESPACE/NAME', help='Published Runner name')
|
|
120
120
|
grant.add_argument('--protocol', dest='protocols', action='append', choices=['anthropic', 'openai'], required=True,
|
|
121
121
|
help='Model protocol to grant; repeat for both')
|
|
122
122
|
grant.add_argument('--revision', type=_revision, help='Limit the grant to one full commit Revision')
|
|
123
123
|
revoke = model.add_parser('revoke', help='Revoke a grant; Trials that have not started no longer get the key '
|
|
124
|
-
'(
|
|
125
|
-
revoke.add_argument('grant_id')
|
|
124
|
+
'(Project Admin)')
|
|
125
|
+
revoke.add_argument('grant_id', metavar='GRANT_ID')
|
|
126
126
|
|
|
127
127
|
|
|
128
128
|
def _read_secret(args: argparse.Namespace) -> str:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: envloop-cli
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.28.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
|
|
@@ -226,8 +226,8 @@ Choose a published Runner readable in your project. Save this request as `gpu-ru
|
|
|
226
226
|
{
|
|
227
227
|
"request_id": "gpu-run-001",
|
|
228
228
|
"trials": [
|
|
229
|
-
{"name": "first", "spec": {"runner": "envloop/bash@FULL_COMMIT", "resources": {"gpu_type": "H100", "gpus": 4}, "
|
|
230
|
-
{"name": "second", "spec": {"runner": "envloop/bash@FULL_COMMIT", "resources": {"gpu_type": "H100", "gpus": 4}, "
|
|
229
|
+
{"name": "first", "spec": {"runner": "envloop/bash@FULL_COMMIT", "resources": {"gpu_type": "H100", "gpus": 4}, "timeout_seconds": 600}},
|
|
230
|
+
{"name": "second", "spec": {"runner": "envloop/bash@FULL_COMMIT", "resources": {"gpu_type": "H100", "gpus": 4}, "timeout_seconds": 600}}
|
|
231
231
|
]
|
|
232
232
|
}
|
|
233
233
|
```
|
|
@@ -272,7 +272,7 @@ with Client.remote() as client:
|
|
|
272
272
|
```
|
|
273
273
|
|
|
274
274
|
Use the exact model returned by the Job. The SDK forwards the payload to the
|
|
275
|
-
Job's `/
|
|
275
|
+
Job's `/inference/decision` API; the approved plugin validates its business fields.
|
|
276
276
|
Availability requires matching platform support and approved content in your project.
|
|
277
277
|
|
|
278
278
|
## ANS data and verified downloads
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
envloop/__init__.py,sha256=tX06Gh2EmufRr0J_3YKtjDeNrJ30auR3y3BJ3N3FHJY,421
|
|
2
|
-
envloop/_optional.py,sha256=
|
|
3
|
-
envloop/auth_cli.py,sha256=
|
|
2
|
+
envloop/_optional.py,sha256=r9vtIbeDSneOQWZZRDdKVOCzkWBpjLbkFkKbGOolqqg,8963
|
|
3
|
+
envloop/auth_cli.py,sha256=Ve7G9AHNsO-zvWNrUgcKiVCTgXKvHYRHjnnhoeTstmE,15448
|
|
4
4
|
envloop/auth_login.py,sha256=PBdgIO_GJ6nMVisWdbGBW1NL2gMV73t1Fao4q3aWvLs,6973
|
|
5
5
|
envloop/auth_workspace.py,sha256=g92Dok4eSp6P-Du5qkwzZG4JDeSrdYTXJbn5WCzCFMo,14927
|
|
6
6
|
envloop/cli.py,sha256=Vafivql1JBrlIrfJOD-XAw6AevQKIasmvDHmcazIzqo,15010
|
|
@@ -10,54 +10,54 @@ envloop/client.py,sha256=TdTUhHwtK0FL6xzIz4moaNdIPlC-TkisPvU4zNPnaTA,3270
|
|
|
10
10
|
envloop/config.py,sha256=1Wr2lNdNNZzsQDPNGsk-TsnEUvDqOtocy9TgUpmOZO0,22494
|
|
11
11
|
envloop/downloads.py,sha256=ktL-hz9HWPGNuQX_N5RmLyt16qbooArljW9z80XYmTo,3253
|
|
12
12
|
envloop/http.py,sha256=2mu5yIgQDESC0y2LvdE53d7fPfSzT0oXVjlY25qwGkY,16479
|
|
13
|
-
envloop/lifecycle_cli.py,sha256=
|
|
13
|
+
envloop/lifecycle_cli.py,sha256=eKK0mP1yXBmmuEpzafNPRoSQjyluGIxcSYGPfr07Rr0,3179
|
|
14
14
|
envloop/navigation_resources.py,sha256=3PeZiYtXHXPMwuCLRdw1mUnZDTDJUYthUhsiJkHtI_g,10378
|
|
15
15
|
envloop/profile_secrets.py,sha256=5QpHRV_Gy1MxObR62SX4nNblqk7gWqQZNtzy6wZNV0s,6325
|
|
16
|
-
envloop/resource_cli.py,sha256=
|
|
16
|
+
envloop/resource_cli.py,sha256=JUMpptd3VvCas0D3cdS5p8_iHVCkiaiIqqGLeFdCnws,13371
|
|
17
17
|
envloop/resources.py,sha256=V5nDuM8QgE52V9slSj2ywmhZqwgMKFosm4o5naFaEJo,3025
|
|
18
18
|
envloop/terminal.py,sha256=kuvcOrFUxdgyNO9nEXabZzVJuX9QS0MhWZXhr3mrUWo,2617
|
|
19
19
|
envloop/utils/__init__.py,sha256=huB3qPQuKNr3JxfH3Ys09YWFtHiDamCHFsBa4epANgQ,25
|
|
20
20
|
envloop/utils/logging.py,sha256=rXN0qyxhfGEq4vWXxjwfaczPPZf_7atnOkkBTgNNRPc,1017
|
|
21
21
|
envloop_data/__init__.py,sha256=nKDGmzFNAQv2mq16YijiI3Q-gDL_smNyZD-AQaqoz7Y,68
|
|
22
|
-
envloop_data/cli.py,sha256=
|
|
23
|
-
envloop_data/cli_output.py,sha256=
|
|
22
|
+
envloop_data/cli.py,sha256=5J_pS5jf65gyaRpE2EV4jDfDUZ1oEsK0iHAmhJlk-oU,9023
|
|
23
|
+
envloop_data/cli_output.py,sha256=oPMBa_CQoMF9a6xnqzxlEUvxNs6g_xIDAKbZC2IhkdY,4271
|
|
24
24
|
envloop_data/client.py,sha256=mw2RlyGOWD1EK0qzq0BiBAGcetxBk7LO23tpCZq9SoQ,12515
|
|
25
25
|
envloop_data/data.py,sha256=OxixzVuNwY21GW_KiCWx9qu_HK3QKWyHsF9Jyd8xjNE,27541
|
|
26
|
-
envloop_data/data_cli.py,sha256=
|
|
26
|
+
envloop_data/data_cli.py,sha256=BoJyoCwHyaobxbqtBs8TS0CmsBMexlbEvofMfxjzC9g,13437
|
|
27
27
|
envloop_data/data_publications.py,sha256=CigperPxG5-V8pDJqHc26SerbElrZEgpAqwifEGPxtA,6865
|
|
28
28
|
envloop_data/data_upload.py,sha256=lQdxGbxnO2Zlxe9QWe1-rTyxAyNjc9aSD0BqC4mbiKU,14270
|
|
29
|
-
envloop_data/http.py,sha256=
|
|
30
|
-
envloop_data/navigation_resources.py,sha256=
|
|
29
|
+
envloop_data/http.py,sha256=adbMFCUsS1YYanzdCoqM4HIgIOxjgXoHKOkxSwUuvW4,6726
|
|
30
|
+
envloop_data/navigation_resources.py,sha256=TB505mMv1yoG4V-QAohKHXAYCu7njVe3p-luOvpN1DM,12283
|
|
31
31
|
envloop_data/profile_updates.py,sha256=FB3Huf0AIlqg6CUC0TcrEeWCF5GH1Z6iJN5Wkq8KcYg,3126
|
|
32
32
|
envloop_data/recipes.py,sha256=JH0bvM0LCH_iN4p4ONfTSF53eSHAVLUQNFLueOx07UA,1301
|
|
33
|
-
envloop_data/resource_cli.py,sha256=
|
|
33
|
+
envloop_data/resource_cli.py,sha256=0539WCdc4lGUd3ZlgKyCxF4Nk6jIX5ck3338nCDodcc,7129
|
|
34
34
|
envloop_data/runner_packages.py,sha256=Xi50hZbdoew9DJ8iYQevMo2jv3qgR3iT50wtV04rFfQ,3424
|
|
35
35
|
envloop_experiment/__init__.py,sha256=nKDGmzFNAQv2mq16YijiI3Q-gDL_smNyZD-AQaqoz7Y,68
|
|
36
|
-
envloop_experiment/cli.py,sha256=
|
|
36
|
+
envloop_experiment/cli.py,sha256=0RfMAGanAL_vd2J2kwSVieNeF0FCZfwWYYujFZGJrg0,28655
|
|
37
37
|
envloop_experiment/cli_output.py,sha256=-FojsqOMIIQuOnfhk2AaqMtwUz5skoWvLeNASm8An6Y,2689
|
|
38
|
-
envloop_experiment/client.py,sha256=
|
|
39
|
-
envloop_experiment/decision.py,sha256=
|
|
40
|
-
envloop_experiment/http.py,sha256=
|
|
38
|
+
envloop_experiment/client.py,sha256=VKKqI9IDazxaN7-coP48DvXcz6SqjZ8ScM4O6plBNWI,25122
|
|
39
|
+
envloop_experiment/decision.py,sha256=HJTj-jSJJ5kcxQQzFXwrA9IICWXA6zSNBFL9EbbrxqI,918
|
|
40
|
+
envloop_experiment/http.py,sha256=A_PHWVr7PV4eiP30lXmOITiljXf6GEIYGkJlr1KFz-Q,6110
|
|
41
41
|
envloop_experiment/inference.py,sha256=74MZDs0lqqqNwgdvj58rs-D3emDj4iHv1W-copc5PYA,2134
|
|
42
|
-
envloop_experiment/job_cost.py,sha256
|
|
42
|
+
envloop_experiment/job_cost.py,sha256=KUaFTn0mg1yF8EvqQxlm131G_4YpsnCe1dM_QT4tHjg,1920
|
|
43
43
|
envloop_experiment/job_logs.py,sha256=RLipbj89e5hQdBeXtszsxSYYw7By1hscwSTGFQzQ3Qw,1340
|
|
44
|
-
envloop_experiment/job_spec_cli.py,sha256=
|
|
44
|
+
envloop_experiment/job_spec_cli.py,sha256=0IaQv_nR93WVYYx3Lba-HbqMN-WCLrzseChEKcXdkxA,7981
|
|
45
45
|
envloop_experiment/navigation_resources.py,sha256=xoI4VFmd0dzviKvzndwKAoWdfbaL_f50cnXRxPkfoo4,1832
|
|
46
|
-
envloop_experiment/resource_cli.py,sha256=
|
|
47
|
-
envloop_experiment/resources.py,sha256=
|
|
46
|
+
envloop_experiment/resource_cli.py,sha256=MCFWHl4iCNe1UiGyHcvMM67LOHQ7MZl3joFtftQwqLk,2083
|
|
47
|
+
envloop_experiment/resources.py,sha256=4guXzssv7XhYkn8ESH-79DIMWUyKDIg70uOa-KoNvp0,7236
|
|
48
48
|
envloop_experiment/runtime_params.py,sha256=ITF6hDuhfdChnWo3e8NW27WxEoNoCuOTww-Hz60zqQU,3608
|
|
49
49
|
envloop_experiment/sandbox.py,sha256=sTpAWbcs2jAPfb7gILK4MTJSmcpSeZG3anFkLA5TgdQ,1274
|
|
50
|
-
envloop_experiment/train.py,sha256=
|
|
50
|
+
envloop_experiment/train.py,sha256=fb6Sqdyo15ynq-vKeUbnyRTfkG5anVu3Ewt152g0fK8,5110
|
|
51
51
|
envloop_experiment/train_log_cli.py,sha256=quOgnUqjE647YhsO44C_zp778ndarmJ2Wr1ANCL45Ck,6368
|
|
52
52
|
envloop_experiment/train_progress.py,sha256=Cln8NPWfmSH9Mnlml-78dOXqwZTqWVeJ8ArDWFAQv-A,2536
|
|
53
|
-
envloop_experiment/train_runner_cli.py,sha256=
|
|
53
|
+
envloop_experiment/train_runner_cli.py,sha256=OiHv7Dp2VPoQVmFR0vgJoAd5Ni0uQvtCyNf5Lk4AFfc,3154
|
|
54
54
|
envloop_experiment/training/__init__.py,sha256=IEQWbCHe-zZ2e7JQxOrFOdYlSQxMfOceCZUf_KeszBk,221
|
|
55
55
|
envloop_rsi_loop/__init__.py,sha256=nKDGmzFNAQv2mq16YijiI3Q-gDL_smNyZD-AQaqoz7Y,68
|
|
56
|
-
envloop_rsi_loop/agent_runs.py,sha256=
|
|
57
|
-
envloop_rsi_loop/cli.py,sha256
|
|
58
|
-
envloop_rsi_loop/cli_output.py,sha256=
|
|
56
|
+
envloop_rsi_loop/agent_runs.py,sha256=GrXSfnQ8MchA9i8UI3LWJiTninpI0PeTm0hgz2TJ0No,10729
|
|
57
|
+
envloop_rsi_loop/cli.py,sha256=Kqearg20Mvfyj62r2mibVdWEMo1NHjthgvS-Tl7SC-A,3047
|
|
58
|
+
envloop_rsi_loop/cli_output.py,sha256=GxM4UiL4NzzSyjRrikai6ROW-1WPjFtwrIfpnE8GZ4U,3173
|
|
59
59
|
envloop_rsi_loop/rsi_cli.py,sha256=e9XHeIPwMz1dZ6NhK3UIETCdy7LbIWgFosF1FnKVUQk,2580
|
|
60
|
-
envloop_cli-1.
|
|
61
|
-
envloop_cli-1.
|
|
62
|
-
envloop_cli-1.
|
|
63
|
-
envloop_cli-1.
|
|
60
|
+
envloop_cli-1.28.0.dist-info/METADATA,sha256=ouQWXHdlfU-i6Hgn-D9jnh5nmwkEl7jw5HlDaPt4nVY,15656
|
|
61
|
+
envloop_cli-1.28.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
|
|
62
|
+
envloop_cli-1.28.0.dist-info/entry_points.txt,sha256=8aXOpgtVk4GndhChA4XNecShkb1w0xGHBHrKla78ofI,67
|
|
63
|
+
envloop_cli-1.28.0.dist-info/RECORD,,
|
envloop_data/cli.py
CHANGED
|
@@ -79,10 +79,10 @@ def secret_option(command: argparse.ArgumentParser) -> None:
|
|
|
79
79
|
|
|
80
80
|
|
|
81
81
|
def add_commands(groups: "argparse._SubParsersAction[argparse.ArgumentParser]") -> None:
|
|
82
|
-
profiles = groups.add_parser("profile", help="Manage platform runtime
|
|
82
|
+
profiles = groups.add_parser("profile", help="Manage platform runtime Profiles").add_subparsers(dest="action", required=True)
|
|
83
83
|
put = profiles.add_parser("create", help="Create an immutable Profile version from TOML or JSON")
|
|
84
|
-
put.add_argument("name")
|
|
85
|
-
put.add_argument("version")
|
|
84
|
+
put.add_argument("name", metavar="NAME")
|
|
85
|
+
put.add_argument("version", metavar="VERSION")
|
|
86
86
|
put.add_argument("--file", type=Path, required=True, help="Profile TOML or JSON file")
|
|
87
87
|
put.add_argument("--runner", metavar="RUNNER", help="Bind a Runner revision to this version")
|
|
88
88
|
secret_input = put.add_mutually_exclusive_group()
|
|
@@ -90,8 +90,8 @@ def add_commands(groups: "argparse._SubParsersAction[argparse.ArgumentParser]")
|
|
|
90
90
|
secret_input.add_argument("--model-api-key-stdin", action="store_true", help="Read the model API key from one line of standard input")
|
|
91
91
|
secret_option(put)
|
|
92
92
|
update = profiles.add_parser("update", help="Edit an existing Profile by publishing a new immutable version")
|
|
93
|
-
update.add_argument("name")
|
|
94
|
-
update.add_argument("version", help="New version; existing versions cannot be overwritten")
|
|
93
|
+
update.add_argument("name", metavar="NAME")
|
|
94
|
+
update.add_argument("version", metavar="VERSION", help="New version; existing versions cannot be overwritten")
|
|
95
95
|
update.add_argument("--from-version", help="Source version; omit to use default, then newest unarchived creation")
|
|
96
96
|
update.add_argument("--file", type=Path, required=True, help="TOML or JSON patch: merge params keys, replace supplied metadata; preserve omitted fields")
|
|
97
97
|
update.add_argument("--runner", metavar="RUNNER", help="Bind a Runner revision; v1 requires a full commit; omit to preserve the source binding")
|
|
@@ -100,13 +100,13 @@ def add_commands(groups: "argparse._SubParsersAction[argparse.ArgumentParser]")
|
|
|
100
100
|
update_secret.add_argument("--model-api-key-stdin", action="store_true", help="Read an encrypted model connection key from one line of standard input")
|
|
101
101
|
secret_option(update)
|
|
102
102
|
fork = profiles.add_parser('fork', help='Fork an existing Profile into a new immutable identity')
|
|
103
|
-
fork.add_argument('source_name')
|
|
104
|
-
fork.add_argument('source_version')
|
|
105
|
-
fork.add_argument('name')
|
|
106
|
-
fork.add_argument('version')
|
|
103
|
+
fork.add_argument('source_name', metavar='SOURCE_NAME')
|
|
104
|
+
fork.add_argument('source_version', metavar='SOURCE_VERSION')
|
|
105
|
+
fork.add_argument('name', metavar='NAME')
|
|
106
|
+
fork.add_argument('version', metavar='VERSION')
|
|
107
107
|
secret_option(fork)
|
|
108
108
|
get = profiles.add_parser("get")
|
|
109
|
-
get.add_argument("name")
|
|
109
|
+
get.add_argument("name", metavar="NAME")
|
|
110
110
|
get.add_argument("--version")
|
|
111
111
|
listing = profiles.add_parser("list")
|
|
112
112
|
listing.add_argument("--name")
|
|
@@ -115,13 +115,13 @@ def add_commands(groups: "argparse._SubParsersAction[argparse.ArgumentParser]")
|
|
|
115
115
|
listing.add_argument("--cursor")
|
|
116
116
|
listing.add_argument("--json", action="store_true", help="Output the complete response as JSON (default: table)")
|
|
117
117
|
default = profiles.add_parser("set-default")
|
|
118
|
-
default.add_argument("name")
|
|
119
|
-
default.add_argument("version")
|
|
118
|
+
default.add_argument("name", metavar="NAME")
|
|
119
|
+
default.add_argument("version", metavar="VERSION")
|
|
120
120
|
default.add_argument("--expected-version", help="Omit only when no default exists")
|
|
121
121
|
for command in ("archive", "export"):
|
|
122
122
|
item = profiles.add_parser(command)
|
|
123
|
-
item.add_argument("name")
|
|
124
|
-
item.add_argument("version")
|
|
123
|
+
item.add_argument("name", metavar="NAME")
|
|
124
|
+
item.add_argument("version", metavar="VERSION")
|
|
125
125
|
from envloop_data.resource_cli import add_resource_commands
|
|
126
126
|
add_resource_commands(groups)
|
|
127
127
|
|
envloop_data/cli_output.py
CHANGED
|
@@ -38,9 +38,9 @@ def list_layout(items: list[dict[str, Any]], args: argparse.Namespace) -> tuple[
|
|
|
38
38
|
render = dataset_row
|
|
39
39
|
empty = 'No datasets found.'
|
|
40
40
|
elif args.group == 'checkpoint':
|
|
41
|
-
headers = ('CHECKPOINT ID', 'TYPE', 'MODEL', '
|
|
41
|
+
headers = ('CHECKPOINT ID', 'TYPE', 'MODEL', 'JOB ID', 'URI')
|
|
42
42
|
render = lambda item: (item.get('checkpoint_id'), item.get('checkpoint_type'), item.get('base_model'),
|
|
43
|
-
item.get('job_id')
|
|
43
|
+
item.get('job_id'), item.get('uri'))
|
|
44
44
|
empty = 'No checkpoints found.'
|
|
45
45
|
elif args.group == "profile" and args.name is None:
|
|
46
46
|
headers = ("NAME", "VERSIONS", "DEFAULT", "UPDATED AT")
|
envloop_data/data_cli.py
CHANGED
|
@@ -18,14 +18,14 @@ from envloop_data.data_publications import (
|
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
def add_commands(groups: "argparse._SubParsersAction[argparse.ArgumentParser]", listing: Callable[[Any], argparse.ArgumentParser]):
|
|
21
|
-
data = groups.add_parser('data', help='Manage
|
|
21
|
+
data = groups.add_parser('data', help='Manage Project ANS resources and verified downloads').add_subparsers(dest='action', required=True)
|
|
22
22
|
command = listing(data)
|
|
23
23
|
command.add_argument('--type', required=True, choices=[*KINDS, 'artifact'])
|
|
24
24
|
for option in ('ref', 'job-id', 'trial-id'):
|
|
25
25
|
command.add_argument('--' + option)
|
|
26
26
|
for action in ('get', 'files'):
|
|
27
27
|
command = data.add_parser(action, help='Read a resource and its files' if action == 'get' else 'List one page of files')
|
|
28
|
-
command.add_argument('data_id')
|
|
28
|
+
command.add_argument('data_id', metavar='DATA_ID')
|
|
29
29
|
command.add_argument('--limit', type=int, default=25)
|
|
30
30
|
command.add_argument('--cursor')
|
|
31
31
|
command.add_argument('--json', action='store_true')
|
|
@@ -35,15 +35,15 @@ def add_commands(groups: "argparse._SubParsersAction[argparse.ArgumentParser]",
|
|
|
35
35
|
command = data.add_parser(action, help='Register native content or HF metadata' if action == 'create' else 'Register a new immutable revision')
|
|
36
36
|
command.add_argument('--type', required=action == 'create', choices=KINDS)
|
|
37
37
|
if action == 'update':
|
|
38
|
-
command.add_argument('data_id')
|
|
38
|
+
command.add_argument('data_id', metavar='DATA_ID')
|
|
39
39
|
publication_arguments(command, source=True)
|
|
40
40
|
command.add_argument('--idempotency-key', help='Reuse for a resumable multipart retry of the same input')
|
|
41
|
-
command = data.add_parser('delete', help='Retire a
|
|
42
|
-
command.add_argument('data_id')
|
|
41
|
+
command = data.add_parser('delete', help='Retire a Project registration; retain immutable content')
|
|
42
|
+
command.add_argument('data_id', metavar='DATA_ID')
|
|
43
43
|
command.add_argument('--dry-run', action='store_true', help='Check authorization and preview without revoking')
|
|
44
44
|
command.add_argument('--json', action='store_true')
|
|
45
45
|
command = data.add_parser('download', help='Download and verify an ANS directory or one resource file')
|
|
46
|
-
command.add_argument('data_id')
|
|
46
|
+
command.add_argument('data_id', metavar='DATA_ID')
|
|
47
47
|
command.add_argument('--file', dest='file_id', help='Select one file by its resource file ID')
|
|
48
48
|
command.add_argument('--split', help='Select the complete Dataset split directory')
|
|
49
49
|
command.add_argument('--output', required=True, type=Path)
|
|
@@ -57,7 +57,7 @@ def add_commands(groups: "argparse._SubParsersAction[argparse.ArgumentParser]",
|
|
|
57
57
|
command.add_argument('--parent-id', help='Existing publication when creating a new revision')
|
|
58
58
|
for action in ('get', 'part', 'complete', 'abort'):
|
|
59
59
|
command = uploads.add_parser(action, help=f'{action.capitalize()} a multipart upload')
|
|
60
|
-
command.add_argument('upload_id')
|
|
60
|
+
command.add_argument('upload_id', metavar='UPLOAD_ID')
|
|
61
61
|
command.add_argument('--json', action='store_true')
|
|
62
62
|
if action == 'part':
|
|
63
63
|
command.add_argument('--number', type=int, required=True)
|
|
@@ -65,7 +65,7 @@ def add_commands(groups: "argparse._SubParsersAction[argparse.ArgumentParser]",
|
|
|
65
65
|
|
|
66
66
|
|
|
67
67
|
command = uploads.add_parser('harbor', help='Upload a private Harbor task directory or tar bundle; optionally register it for Jobs')
|
|
68
|
-
command.add_argument('source', type=Path)
|
|
68
|
+
command.add_argument('source', metavar='SOURCE', type=Path)
|
|
69
69
|
command.add_argument('--name', required=True, help='Dataset name as NAMESPACE/NAME')
|
|
70
70
|
command.add_argument('--version', required=True, help='Exact version from dataset.toml')
|
|
71
71
|
command.add_argument('--register', action='store_true', help='Register the uploaded snapshot with the selected Runner')
|
|
@@ -74,12 +74,12 @@ def add_commands(groups: "argparse._SubParsersAction[argparse.ArgumentParser]",
|
|
|
74
74
|
command.add_argument('--idempotency-key', help='Stable upload key; identical input resumes by default')
|
|
75
75
|
command.add_argument('--json', action='store_true')
|
|
76
76
|
command = data.add_parser('register', help='Register an uploaded Harbor Dataset snapshot for Jobs')
|
|
77
|
-
command.add_argument('data_id')
|
|
77
|
+
command.add_argument('data_id', metavar='DATA_ID')
|
|
78
78
|
command.add_argument('--runner', required=True, help='Existing Runner NAME@REVISION')
|
|
79
79
|
command.add_argument('--wait', action='store_true', help='Wait until registration finishes (default: return after it starts)')
|
|
80
80
|
command.add_argument('--json', action='store_true')
|
|
81
81
|
command = data.add_parser('registration', help='Read and advance Harbor Dataset registration')
|
|
82
|
-
command.add_argument('data_id')
|
|
82
|
+
command.add_argument('data_id', metavar='DATA_ID')
|
|
83
83
|
command.add_argument('--json', action='store_true')
|
|
84
84
|
|
|
85
85
|
|
envloop_data/http.py
CHANGED
|
@@ -58,9 +58,9 @@ class Endpoints:
|
|
|
58
58
|
return {"kind": "dataset", **{key: matches[0][key] for key in ("name", "version", "manifest_sha256")}}
|
|
59
59
|
|
|
60
60
|
|
|
61
|
-
def open_checkpoint_archive(self,
|
|
61
|
+
def open_checkpoint_archive(self, job_id: str) -> Download:
|
|
62
62
|
"""Read a local mock checkpoint through its authenticated public API route."""
|
|
63
|
-
path = f"/v1/
|
|
63
|
+
path = f"/v1/jobs/{quote(job_id, safe='')}/checkpoint/download"
|
|
64
64
|
try:
|
|
65
65
|
return Download(self.opener.open(Request(self.base + path, headers=self.headers), timeout=self.timeout))
|
|
66
66
|
except HTTPError as error:
|
|
@@ -61,10 +61,10 @@ class Datasets:
|
|
|
61
61
|
return self.transport.request('GET', query(path, limit=limit, cursor=cursor))
|
|
62
62
|
|
|
63
63
|
|
|
64
|
-
def _checkpoint_matches(
|
|
65
|
-
if
|
|
64
|
+
def _checkpoint_matches(checkpoint: str, item: dict[str, Any]) -> bool:
|
|
65
|
+
if checkpoint in {item.get('checkpoint_id'), item.get('uri'), item.get('model_uri')}:
|
|
66
66
|
return True
|
|
67
|
-
parsed = urlsplit(
|
|
67
|
+
parsed = urlsplit(checkpoint)
|
|
68
68
|
if parsed.scheme not in {'envloop', 'tinker'} or parsed.query or parsed.fragment:
|
|
69
69
|
return False
|
|
70
70
|
name = parsed.path.lstrip('/')
|
|
@@ -128,12 +128,12 @@ class Checkpoints:
|
|
|
128
128
|
def list(self, **filters: Any) -> Any:
|
|
129
129
|
if self.training is not None:
|
|
130
130
|
return self.training.checkpoints(**filters)
|
|
131
|
-
return self.transport.request('GET', '/v1/
|
|
131
|
+
return self.transport.request('GET', '/v1/checkpoints?' + urlencode({key: value for key, value in filters.items() if value is not None}))
|
|
132
132
|
|
|
133
133
|
def delete(self, checkpoint_id: str) -> Any:
|
|
134
|
-
return self.transport.request('DELETE', f"/v1/
|
|
134
|
+
return self.transport.request('DELETE', f"/v1/checkpoints/{quote(checkpoint_id, safe='')}")
|
|
135
135
|
|
|
136
|
-
def download(self,
|
|
136
|
+
def download(self, checkpoint: str, directory: str | Path, *, job_id: str | None = None, run_id: str | None = None) -> dict[str, Any]:
|
|
137
137
|
from pathlib import Path
|
|
138
138
|
|
|
139
139
|
directory = Path(directory)
|
|
@@ -141,7 +141,7 @@ class Checkpoints:
|
|
|
141
141
|
while True:
|
|
142
142
|
page = self.list(job_id=job_id, run_id=run_id, cursor=cursor, limit=100)
|
|
143
143
|
for item in page['items']:
|
|
144
|
-
if _checkpoint_matches(
|
|
144
|
+
if _checkpoint_matches(checkpoint, item):
|
|
145
145
|
matches.append(item)
|
|
146
146
|
cursor = page.get('next_cursor')
|
|
147
147
|
if cursor is None:
|
|
@@ -172,7 +172,7 @@ class Checkpoints:
|
|
|
172
172
|
|
|
173
173
|
def refresh(self) -> None:
|
|
174
174
|
# Keep the original authorized identity and file hashes across refreshes.
|
|
175
|
-
signed = transport.request('GET', query('/v1/
|
|
175
|
+
signed = transport.request('GET', query('/v1/checkpoints/download', uri=checkpoint,
|
|
176
176
|
job_id=item['job_id'], run_id=run_id))
|
|
177
177
|
urls = signed.get('urls', {})
|
|
178
178
|
if (set(names) != set(urls)
|
|
@@ -210,7 +210,7 @@ class Checkpoints:
|
|
|
210
210
|
import tempfile
|
|
211
211
|
import zipfile
|
|
212
212
|
|
|
213
|
-
job_id = item.get('
|
|
213
|
+
job_id = item.get('job_id')
|
|
214
214
|
if not isinstance(job_id, str) or not job_id:
|
|
215
215
|
raise RemoteError('CHECKPOINT_FILES_INVALID')
|
|
216
216
|
# Bound the streamed archive by its declared payload plus ZIP metadata.
|
envloop_data/resource_cli.py
CHANGED
|
@@ -6,21 +6,21 @@ from envloop.resource_cli import listing
|
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
def add_resource_commands(groups: "argparse._SubParsersAction[argparse.ArgumentParser]"):
|
|
9
|
-
datasets = groups.add_parser('dataset', help='Discover
|
|
9
|
+
datasets = groups.add_parser('dataset', help='Discover Datasets, revisions and tasks').add_subparsers(dest='action', required=True)
|
|
10
10
|
command = listing(datasets)
|
|
11
|
-
command.add_argument('--type', choices=['environment', 'trajectory', 'record'], help='Filter by
|
|
11
|
+
command.add_argument('--type', choices=['environment', 'trajectory', 'record'], help='Filter by Dataset content type')
|
|
12
12
|
command.add_argument('--query', help='Search Dataset names')
|
|
13
|
-
datasets.add_parser('get', help='Show a
|
|
13
|
+
datasets.add_parser('get', help='Show a Dataset and its pinned Runner reference').add_argument('name', metavar='NAME')
|
|
14
14
|
for resource in ('revision', 'task'):
|
|
15
|
-
commands = datasets.add_parser(resource, help=f'Inspect
|
|
15
|
+
commands = datasets.add_parser(resource, help=f'Inspect Dataset {resource}').add_subparsers(dest='operation', required=True)
|
|
16
16
|
command = listing(commands)
|
|
17
|
-
command.add_argument('name')
|
|
17
|
+
command.add_argument('name', metavar='NAME')
|
|
18
18
|
if resource == 'task':
|
|
19
|
-
command.add_argument('revision')
|
|
19
|
+
command.add_argument('revision', metavar='REVISION')
|
|
20
20
|
else:
|
|
21
|
-
command = commands.add_parser('get', help='Show an immutable
|
|
22
|
-
command.add_argument('name')
|
|
23
|
-
command.add_argument('revision')
|
|
21
|
+
command = commands.add_parser('get', help='Show an immutable Dataset revision')
|
|
22
|
+
command.add_argument('name', metavar='NAME')
|
|
23
|
+
command.add_argument('revision', metavar='REVISION')
|
|
24
24
|
|
|
25
25
|
from envloop_data.data_cli import add_commands
|
|
26
26
|
add_commands(groups, listing)
|
|
@@ -34,8 +34,8 @@ def add_resource_commands(groups: "argparse._SubParsersAction[argparse.ArgumentP
|
|
|
34
34
|
'SHA-256, creates the Runner on first use and registers the revision; identical retries are idempotent and '
|
|
35
35
|
'changed content for an existing revision is refused. This is el data create --type runner with the native '
|
|
36
36
|
'manifest built for you; use that command to publish an existing manifest. The key needs envloop:data:write '
|
|
37
|
-
'and runners.write, and publish permission on a Runner owned by another
|
|
38
|
-
register.add_argument('directory', type=Path, help='Runner package directory; every file in it is published')
|
|
37
|
+
'and runners.write, and publish permission on a Runner owned by another Project.')
|
|
38
|
+
register.add_argument('directory', metavar='DIRECTORY', type=Path, help='Runner package directory; every file in it is published')
|
|
39
39
|
register.add_argument('--revision', required=True, help='Full 40- or 64-character source commit, for example $(git rev-parse HEAD)')
|
|
40
40
|
register.add_argument('--name', help='NAMESPACE/NAME; must equal [runner] name in runner.toml (default: that name)')
|
|
41
41
|
register.add_argument('--executable', action='append', dest='executables', metavar='PATH',
|
|
@@ -44,18 +44,18 @@ def add_resource_commands(groups: "argparse._SubParsersAction[argparse.ArgumentP
|
|
|
44
44
|
register.add_argument('--source-ref-kind', choices=['branch', 'tag', 'commit'], default='branch', help='Kind of --source-ref (default: branch)')
|
|
45
45
|
register.add_argument('--dry-run', action='store_true', help='Validate the package and authorization without publishing')
|
|
46
46
|
|
|
47
|
-
checkpoints = groups.add_parser('checkpoint', help='List and download saved training
|
|
47
|
+
checkpoints = groups.add_parser('checkpoint', help='List and download saved training Checkpoints').add_subparsers(dest='action', required=True)
|
|
48
48
|
command = listing(checkpoints)
|
|
49
|
-
command.add_argument('--job-id', help='Only
|
|
50
|
-
command.add_argument('--run-id', help='Only
|
|
49
|
+
command.add_argument('--job-id', help='Only Checkpoints of this Train Job')
|
|
50
|
+
command.add_argument('--run-id', help='Only Checkpoints of this training run label')
|
|
51
51
|
download = checkpoints.add_parser('download', help='Download files and verify their sizes and SHA-256 hashes')
|
|
52
|
-
download.add_argument('checkpoint', metavar='CHECKPOINT', help='
|
|
53
|
-
download.add_argument('directory', type=Path)
|
|
52
|
+
download.add_argument('checkpoint', metavar='CHECKPOINT', help='Checkpoint ID or uri from `checkpoint list`')
|
|
53
|
+
download.add_argument('directory', metavar='DIRECTORY', type=Path)
|
|
54
54
|
download.add_argument('--job-id')
|
|
55
55
|
download.add_argument('--run-id')
|
|
56
56
|
from envloop.lifecycle_cli import wait_options
|
|
57
|
-
delete = checkpoints.add_parser('delete', help='Remove an unreferenced
|
|
58
|
-
delete.add_argument('identifier')
|
|
57
|
+
delete = checkpoints.add_parser('delete', help='Remove an unreferenced Checkpoint and stored files')
|
|
58
|
+
delete.add_argument('identifier', metavar='CHECKPOINT_ID')
|
|
59
59
|
wait_options(delete)
|
|
60
60
|
|
|
61
61
|
|
envloop_experiment/cli.py
CHANGED
|
@@ -95,8 +95,8 @@ def submission_options(args: argparse.Namespace):
|
|
|
95
95
|
options["profile"] = {"seed": args.seed}
|
|
96
96
|
else:
|
|
97
97
|
options["seed"] = args.seed
|
|
98
|
-
execution = {key: value for key, value in {"
|
|
99
|
-
"
|
|
98
|
+
execution = {key: value for key, value in {"timeout_seconds": args.timeout_seconds,
|
|
99
|
+
"delete_delay_seconds": args.delete_delay_seconds}.items() if value is not None}
|
|
100
100
|
if execution:
|
|
101
101
|
options["execution"] = execution
|
|
102
102
|
return options
|
|
@@ -119,14 +119,14 @@ def add_commands(groups: "argparse._SubParsersAction[argparse.ArgumentParser]"):
|
|
|
119
119
|
help="Train Runner: per-Trial size in Mi/Gi/Ti (for example 64Gi)")
|
|
120
120
|
job_submit.add_argument("--spec", type=Path, metavar="REQUEST.json", help="Job specification JSON; explicit submission options override file values (except --trials)")
|
|
121
121
|
job_submit.add_argument("--source", type=Path, help="two-file directory for a single-task Job")
|
|
122
|
-
job_submit.add_argument("--task-id", help="select one task within the
|
|
122
|
+
job_submit.add_argument("--task-id", help="select one task within the Dataset")
|
|
123
123
|
job_submit.add_argument(
|
|
124
124
|
"dataset", nargs="?", type=dataset_reference, metavar="NAME[@VERSION]",
|
|
125
|
-
help="
|
|
125
|
+
help="Dataset name; optional author version (legacy positional form)",
|
|
126
126
|
)
|
|
127
127
|
job_submit.add_argument("--dataset", dest="dataset_option", type=dataset_reference, metavar="NAME[@VERSION]",
|
|
128
|
-
help="
|
|
129
|
-
job_submit.add_argument("--config", help="Agent:
|
|
128
|
+
help="Dataset name; default selection uses the platform's pinned snapshot")
|
|
129
|
+
job_submit.add_argument("--config", help="Agent: Dataset subset; Train recipe: named preset; omitted preserves spec or server default")
|
|
130
130
|
job_submit.add_argument("--split", help="one complete split name; omitted selects all splits")
|
|
131
131
|
job_submit.add_argument("--revision", help="Git branch/tag/commit; must match the platform's pinned snapshot")
|
|
132
132
|
runner_arguments(job_submit)
|
|
@@ -141,15 +141,15 @@ def add_commands(groups: "argparse._SubParsersAction[argparse.ArgumentParser]"):
|
|
|
141
141
|
job_submit.add_argument("--timeout-seconds", type=wait_seconds,
|
|
142
142
|
help="Agent/Sandbox or Train Runner execution timeout (whole seconds); Train recipe --wait timeout (default: 600)")
|
|
143
143
|
job_submit.add_argument("--max-cost-usd", type=max_cost_usd, metavar="USD",
|
|
144
|
-
help="Agent/Sandbox: Job spend cap in USD (model calls plus
|
|
144
|
+
help="Agent/Sandbox: Job spend cap in USD (model calls plus Sandbox time); the Job stops "
|
|
145
145
|
"with error BUDGET_EXCEEDED once it is reached")
|
|
146
146
|
job_submit.add_argument("--delete-delay-seconds", type=nonnegative_seconds, metavar="SECONDS",
|
|
147
|
-
help="keep each
|
|
147
|
+
help="keep each Sandbox (E2B or Fly) after completion for SECONDS (default: 0); cancellation/timeout delete immediately")
|
|
148
148
|
job_submit.add_argument("--wait", action="store_true")
|
|
149
149
|
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)")
|
|
150
150
|
for action in ("get", "stop", "result", "pause", "resume", "wake", "event"):
|
|
151
151
|
command = jobs.add_parser(action)
|
|
152
|
-
command.add_argument("job_id")
|
|
152
|
+
command.add_argument("job_id", metavar="JOB_ID")
|
|
153
153
|
if action == 'event':
|
|
154
154
|
command.add_argument('--json', action='store_true')
|
|
155
155
|
command.set_defaults(_list_view=True)
|
|
@@ -174,7 +174,7 @@ def add_commands(groups: "argparse._SubParsersAction[argparse.ArgumentParser]"):
|
|
|
174
174
|
regrade.add_argument("--timeout-seconds", type=wait_seconds, help="--wait timeout in whole seconds (default: 600)")
|
|
175
175
|
regrade.add_argument("--json", action="store_true", help="Output the complete response as JSON (the default)")
|
|
176
176
|
decision = jobs.add_parser("decision", help="Invoke the frozen decision plugin of an Inference Job")
|
|
177
|
-
decision.add_argument("job_id")
|
|
177
|
+
decision.add_argument("job_id", metavar="JOB_ID")
|
|
178
178
|
decision.add_argument("--model", required=True)
|
|
179
179
|
decision.add_argument("--spec", type=Path, required=True, metavar="PAYLOAD.json")
|
|
180
180
|
from envloop_experiment.resource_cli import add_job_resources
|
|
@@ -187,7 +187,7 @@ def add_commands(groups: "argparse._SubParsersAction[argparse.ArgumentParser]"):
|
|
|
187
187
|
listing.add_argument("--json", action="store_true", help="Output the complete response as JSON (default: table)")
|
|
188
188
|
listing.add_argument("--limit", type=int, default=25)
|
|
189
189
|
train_log = jobs.add_parser("log", help="Read or follow Agent, Sandbox or Train Job logs")
|
|
190
|
-
train_log.add_argument("job_id")
|
|
190
|
+
train_log.add_argument("job_id", metavar="JOB_ID")
|
|
191
191
|
train_log.add_argument("--source", choices=("all", "system", "runtime"), default="all")
|
|
192
192
|
train_log.add_argument("--process", metavar="NAME",
|
|
193
193
|
help="Only one recorded process, e.g. trainer (a Train Job's step metrics)")
|
|
@@ -208,20 +208,20 @@ def add_commands(groups: "argparse._SubParsersAction[argparse.ArgumentParser]"):
|
|
|
208
208
|
listing.add_argument("--cursor")
|
|
209
209
|
listing.add_argument("--limit", type=int, default=25)
|
|
210
210
|
snapshot = tasks.add_parser("snapshot")
|
|
211
|
-
snapshot.add_argument("trial_id")
|
|
211
|
+
snapshot.add_argument("trial_id", metavar="TRIAL_ID")
|
|
212
212
|
for action in ("get", "stop", "result"):
|
|
213
|
-
tasks.add_parser(action).add_argument("trial_id")
|
|
213
|
+
tasks.add_parser(action).add_argument("trial_id", metavar="TRIAL_ID")
|
|
214
214
|
listing.add_argument("--json", action="store_true", help="Output the complete response as JSON")
|
|
215
215
|
artifacts = tasks.add_parser("artifact", help="List or download a Trial's artifacts").add_subparsers(dest="artifact_action", required=True)
|
|
216
216
|
artifact_list = artifacts.add_parser("list", help="List one page of artifacts")
|
|
217
217
|
artifact_list.set_defaults(_list_view=True)
|
|
218
|
-
artifact_list.add_argument("trial_id")
|
|
218
|
+
artifact_list.add_argument("trial_id", metavar="TRIAL_ID")
|
|
219
219
|
artifact_list.add_argument("--cursor")
|
|
220
220
|
artifact_list.add_argument("--limit", type=int, default=25)
|
|
221
221
|
artifact_list.add_argument("--json", action="store_true")
|
|
222
222
|
download = artifacts.add_parser("download", help="Download all uploaded artifacts and verify their sizes and hashes")
|
|
223
|
-
download.add_argument("trial_id")
|
|
224
|
-
download.add_argument("directory", type=Path)
|
|
223
|
+
download.add_argument("trial_id", metavar="TRIAL_ID")
|
|
224
|
+
download.add_argument("directory", metavar="DIRECTORY", type=Path)
|
|
225
225
|
|
|
226
226
|
def validate(args: argparse.Namespace, cli: argparse.ArgumentParser):
|
|
227
227
|
if args.group == "job" and args.action == "decision":
|
envloop_experiment/client.py
CHANGED
|
@@ -250,14 +250,14 @@ class Jobs:
|
|
|
250
250
|
runner: str | None = None,
|
|
251
251
|
profile_id: str | None = None, job_id: str | None = None, tag: str | None = None,
|
|
252
252
|
queue_id: str = "default",
|
|
253
|
-
priority: int = 0, command: str | None = None,
|
|
254
|
-
|
|
253
|
+
priority: int = 0, command: str | None = None, source_job_id: str | None = None,
|
|
254
|
+
source_trial_id: str | None = None,
|
|
255
255
|
type: str = 'agent', image: str | None = None, params: Mapping[str, Any] | None = None,
|
|
256
256
|
max_cost_usd: float | None = None, **options: Any):
|
|
257
257
|
"""Submit an Agent Job or a Sandbox Job using a registered Runner.
|
|
258
258
|
|
|
259
|
-
|
|
260
|
-
ownership and project scope
|
|
259
|
+
``source_trial_id`` requires its ``source_job_id``. The API verifies the
|
|
260
|
+
source type, ownership and project scope.
|
|
261
261
|
``max_cost_usd`` caps the Job's model plus sandbox spend; the Job stops with
|
|
262
262
|
``error=BUDGET_EXCEEDED`` when it is reached and its result reports ``cost``.
|
|
263
263
|
"""
|
|
@@ -265,12 +265,12 @@ class Jobs:
|
|
|
265
265
|
raise ValueError('Jobs.submit type must be agent or sandbox; use training_jobs.submit for train')
|
|
266
266
|
if image is not None and type != 'sandbox':
|
|
267
267
|
raise ValueError('image requires a Sandbox Job')
|
|
268
|
-
for field, value in (("
|
|
269
|
-
("
|
|
268
|
+
for field, value in (("source_job_id", source_job_id),
|
|
269
|
+
("source_trial_id", source_trial_id)):
|
|
270
270
|
if value is not None and (not isinstance(value, str) or not value.strip()):
|
|
271
271
|
raise ValueError(f"{field} must be a non-empty string")
|
|
272
|
-
if
|
|
273
|
-
raise ValueError("
|
|
272
|
+
if source_trial_id is not None and source_job_id is None:
|
|
273
|
+
raise ValueError("source_trial_id requires source_job_id")
|
|
274
274
|
if request_id is not None and (not isinstance(request_id, str) or not request_id.strip() or len(request_id) > 128):
|
|
275
275
|
raise ValueError("request_id must be a non-empty string of at most 128 characters")
|
|
276
276
|
if params is not None:
|
|
@@ -286,7 +286,7 @@ class Jobs:
|
|
|
286
286
|
)
|
|
287
287
|
|
|
288
288
|
if any(value is not None for value in (dataset, source, task_ref, task_id, namespace,
|
|
289
|
-
config, split, revision,
|
|
289
|
+
config, split, revision, source_job_id, source_trial_id)):
|
|
290
290
|
raise ValueError('Sandbox Jobs accept a Runner without Dataset, Task or training-source options')
|
|
291
291
|
if any(key in options for key in ('seed', 'profile_name', 'profile_version')):
|
|
292
292
|
raise ValueError('Sandbox profiles must use profile_id with a registered Runner')
|
|
@@ -333,8 +333,8 @@ class Jobs:
|
|
|
333
333
|
else:
|
|
334
334
|
raise ValueError("dataset must be a fixed reference, name or name@version")
|
|
335
335
|
for key, value in dict(runner=runner, profile_id=profile_id,
|
|
336
|
-
job_id=job_id, tag=tag,
|
|
337
|
-
|
|
336
|
+
job_id=job_id, tag=tag, source_job_id=source_job_id,
|
|
337
|
+
source_trial_id=source_trial_id).items():
|
|
338
338
|
if value is not None:
|
|
339
339
|
options[key] = value
|
|
340
340
|
# Omit new defaults on legacy submissions to retain older API compatibility.
|
envloop_experiment/decision.py
CHANGED
|
@@ -16,5 +16,5 @@ class DecisionClient:
|
|
|
16
16
|
|
|
17
17
|
def evaluate(self, job_id: str, *, model: str, payload: dict[str, Any]):
|
|
18
18
|
self.validate(job_id, model, payload)
|
|
19
|
-
return self.transport.request('POST', f'/v1/
|
|
19
|
+
return self.transport.request('POST', f'/v1/jobs/{quote(job_id, safe="")}/inference/decision',
|
|
20
20
|
{'model': model, **payload})
|
envloop_experiment/http.py
CHANGED
|
@@ -29,35 +29,35 @@ class Endpoints:
|
|
|
29
29
|
from envloop_experiment.runtime_params import validate_params
|
|
30
30
|
body = {**body, 'params': validate_params(body['params'])}
|
|
31
31
|
configure_transport(self)
|
|
32
|
-
return self.request("POST", "/v1/
|
|
32
|
+
return self.request("POST", "/v1/jobs", body)
|
|
33
33
|
|
|
34
34
|
|
|
35
35
|
def get_job(self, job_id: str):
|
|
36
|
-
return self.request("GET", f"/v1/
|
|
36
|
+
return self.request("GET", f"/v1/jobs/{quote(job_id, safe='')}")
|
|
37
37
|
|
|
38
38
|
|
|
39
39
|
def get_job_result(self, job_id: str):
|
|
40
|
-
return self.request("GET", f"/v1/
|
|
40
|
+
return self.request("GET", f"/v1/jobs/{quote(job_id, safe='')}/result")
|
|
41
41
|
|
|
42
42
|
|
|
43
43
|
def get_job_logs(self, job_id: str, **kwargs: Any):
|
|
44
|
-
return self.request("GET", f"/v1/
|
|
44
|
+
return self.request("GET", f"/v1/jobs/{quote(job_id, safe='')}/logs?" + urlencode(kwargs))
|
|
45
45
|
|
|
46
46
|
|
|
47
47
|
def list_jobs(self, **kwargs: Any):
|
|
48
|
-
return self.request("GET", "/v1/
|
|
48
|
+
return self.request("GET", "/v1/jobs?" + urlencode({key: value for key, value in kwargs.items() if value is not None}))
|
|
49
49
|
|
|
50
50
|
|
|
51
51
|
def job_action(self, job_id: str, action: str):
|
|
52
|
-
return self.request('POST', f"/v1/
|
|
52
|
+
return self.request('POST', f"/v1/jobs/{quote(job_id, safe='')}/{action}", {})
|
|
53
53
|
|
|
54
54
|
|
|
55
55
|
def job_events(self, job_id: str):
|
|
56
|
-
return self.request('GET', f"/v1/
|
|
56
|
+
return self.request('GET', f"/v1/jobs/{quote(job_id, safe='')}/events")
|
|
57
57
|
|
|
58
58
|
|
|
59
59
|
def stop_job(self, job_id: str):
|
|
60
|
-
return self.request("POST", f"/v1/
|
|
60
|
+
return self.request("POST", f"/v1/jobs/{quote(job_id, safe='')}/stop")
|
|
61
61
|
|
|
62
62
|
|
|
63
63
|
def list_trials(self, **kwargs: Any):
|
|
@@ -129,7 +129,7 @@ def configure_transport(transport: Any):
|
|
|
129
129
|
|
|
130
130
|
def request_policy(self: Any, method: str, path: str, body: Any, headers: Any):
|
|
131
131
|
from envloop import http as base_http
|
|
132
|
-
if method != 'POST' or path != '/v1/
|
|
132
|
+
if method != 'POST' or path != '/v1/jobs':
|
|
133
133
|
return self._send(method, path, body, headers, self.timeout)
|
|
134
134
|
try:
|
|
135
135
|
return self._send(method, path, body, headers, max(self.timeout, getattr(base_http, "SUBMIT_TIMEOUT_SEC", 120)))
|
envloop_experiment/job_cost.py
CHANGED
|
@@ -25,8 +25,8 @@ def cost_line(job: Any) -> str | None:
|
|
|
25
25
|
return None
|
|
26
26
|
line = f"Cost (estimate): {_usd(cost.get('total_usd'))} (model {_usd(cost.get('model_usd'))}, " \
|
|
27
27
|
f"sandbox {_usd(cost.get('sandbox_usd'))})"
|
|
28
|
-
if cost.get("
|
|
29
|
-
line += f" of a {_usd(cost.get('
|
|
28
|
+
if cost.get("max_cost_usd") is not None:
|
|
29
|
+
line += f" of a {_usd(cost.get('max_cost_usd'))} budget"
|
|
30
30
|
if cost.get("unmetered") is True:
|
|
31
31
|
line += "; some usage has no price, so the budget cannot be enforced"
|
|
32
32
|
elif cost.get("budget_exceeded") is True:
|
|
@@ -133,9 +133,9 @@ def spec_request(args: argparse.Namespace):
|
|
|
133
133
|
if args.timeout_seconds is not None:
|
|
134
134
|
if not args.timeout_seconds.is_integer():
|
|
135
135
|
raise ValueError('--timeout-seconds must be whole seconds')
|
|
136
|
-
execution['
|
|
136
|
+
execution['timeout_seconds'] = int(args.timeout_seconds)
|
|
137
137
|
if args.delete_delay_seconds is not None:
|
|
138
|
-
execution['
|
|
138
|
+
execution['delete_delay_seconds'] = args.delete_delay_seconds
|
|
139
139
|
if execution:
|
|
140
140
|
request['execution'] = {**request.get('execution', {}), **execution}
|
|
141
141
|
if args.max_cost_usd is not None:
|
|
@@ -10,15 +10,15 @@ from envloop.resource_cli import listing
|
|
|
10
10
|
def add_job_resources(jobs: "argparse._SubParsersAction[argparse.ArgumentParser]"):
|
|
11
11
|
profiles = jobs.add_parser('serving-profile', help='Discover deployable serving profiles').add_subparsers(dest='resource_action', required=True)
|
|
12
12
|
listing(profiles, paged=False)
|
|
13
|
-
adapters = jobs.add_parser('adapter', help='Manage reusable inference
|
|
13
|
+
adapters = jobs.add_parser('adapter', help='Manage reusable inference Adapters').add_subparsers(dest='resource_action', required=True)
|
|
14
14
|
listing(adapters, paged=False)
|
|
15
|
-
adapters.add_parser('get', help='Show an
|
|
16
|
-
register = adapters.add_parser('register', help='Register an
|
|
15
|
+
adapters.add_parser('get', help='Show an Adapter').add_argument('identifier', metavar='ADAPTER_ID')
|
|
16
|
+
register = adapters.add_parser('register', help='Register an Adapter using a JSON request')
|
|
17
17
|
register.add_argument('--file', type=Path, required=True, metavar='REQUEST.json',
|
|
18
18
|
help='{"serving_profile": "NAME", "source": {"type": "train_checkpoint", '
|
|
19
19
|
'"uri": "envloop://..."}}; request_id is generated when omitted')
|
|
20
|
-
delete = adapters.add_parser('delete', help='Delete an unreferenced
|
|
21
|
-
delete.add_argument('identifier')
|
|
20
|
+
delete = adapters.add_parser('delete', help='Delete an unreferenced Adapter')
|
|
21
|
+
delete.add_argument('identifier', metavar='ADAPTER_ID')
|
|
22
22
|
from envloop.lifecycle_cli import wait_options
|
|
23
23
|
wait_options(delete)
|
|
24
24
|
|
envloop_experiment/resources.py
CHANGED
|
@@ -46,7 +46,7 @@ class Endpoints:
|
|
|
46
46
|
|
|
47
47
|
def create(self, *, name: str, serving_profile: str, adapters: Sequence[dict[str, str]], request_id: str | None = None, **options: Any):
|
|
48
48
|
body = {"request_id": request_id or str(uuid4()), "name": name, "serving_profile": serving_profile,
|
|
49
|
-
"adapters": adapters, "scaling": {"min_replicas": 0, "max_replicas": 1, "
|
|
49
|
+
"adapters": adapters, "scaling": {"min_replicas": 0, "max_replicas": 1, "scale_to_zero_after_seconds": 300},
|
|
50
50
|
**options}
|
|
51
51
|
if self.temporary_scope_id:
|
|
52
52
|
body["temporary_scope_id"] = self.temporary_scope_id
|
|
@@ -86,7 +86,7 @@ class Endpoints:
|
|
|
86
86
|
"""Return the API completion; transient cold-start errors remain observable."""
|
|
87
87
|
if job_id is None:
|
|
88
88
|
raise ValueError('job_id is required; use the Job ID returned by endpoint creation')
|
|
89
|
-
return self.transport.request("POST", f"/v1/
|
|
89
|
+
return self.transport.request("POST", f"/v1/jobs/{_id(job_id)}/inference/chat/completions", {
|
|
90
90
|
"model": model, "messages": [{"role": "user", "content": prompt}], **options,
|
|
91
91
|
})
|
|
92
92
|
|
envloop_experiment/train.py
CHANGED
|
@@ -18,11 +18,11 @@ class TrainJobs:
|
|
|
18
18
|
def models(self) -> dict[str, Any]:
|
|
19
19
|
"""Discover configured managed recipes; admission and GPU readiness remain separate."""
|
|
20
20
|
try:
|
|
21
|
-
return self.transport.request('GET', '/v1/
|
|
21
|
+
return self.transport.request('GET', '/v1/training-models')
|
|
22
22
|
except RemoteError as error:
|
|
23
23
|
if error.status_code == 404:
|
|
24
24
|
raise RuntimeError(
|
|
25
|
-
"This platform does not serve /v1/
|
|
25
|
+
"This platform does not serve /v1/training-models; upgrade the platform to a "
|
|
26
26
|
"version that matches this client (managed training discovery is unavailable).") from error
|
|
27
27
|
raise
|
|
28
28
|
|
|
@@ -43,13 +43,13 @@ class TrainJobs:
|
|
|
43
43
|
if not isinstance(request, dict) or request.get("type", "train") != "train":
|
|
44
44
|
raise ValueError("Train Job request must be a JSON object")
|
|
45
45
|
# Like every other submission, an omitted idempotency key is generated so an unanswered submit can be retried.
|
|
46
|
-
return self._view(self.transport.request("POST", "/v1/
|
|
46
|
+
return self._view(self.transport.request("POST", "/v1/jobs", {"request_id": str(uuid4())} | request | {"type": "train"}))
|
|
47
47
|
|
|
48
48
|
def status(self, job_id: str) -> dict[str, Any]:
|
|
49
|
-
return self._view(self.transport.request("GET", f"/v1/
|
|
49
|
+
return self._view(self.transport.request("GET", f"/v1/jobs/{quote(job_id, safe='')}") )
|
|
50
50
|
|
|
51
51
|
def result(self, job_id: str) -> dict[str, Any]:
|
|
52
|
-
return self._view(self.transport.request('GET', f'/v1/
|
|
52
|
+
return self._view(self.transport.request('GET', f'/v1/jobs/{quote(job_id, safe="")}/result'))
|
|
53
53
|
|
|
54
54
|
def logs(self, job_id: str, *, source: str = "all", cursor: str | None = None, tail: int | None = None, limit: int = 200, process: str | None = None) -> dict[str, Any]:
|
|
55
55
|
"""Read a log page; cursors retain the server's source and capture metadata.
|
|
@@ -60,16 +60,16 @@ class TrainJobs:
|
|
|
60
60
|
from envloop_experiment.job_logs import log_query
|
|
61
61
|
|
|
62
62
|
query = urlencode(log_query(source=source, cursor=cursor, tail=tail, limit=limit, process=process))
|
|
63
|
-
return self.transport.request("GET", f"/v1/
|
|
63
|
+
return self.transport.request("GET", f"/v1/jobs/{quote(job_id, safe='')}/logs?{query}")
|
|
64
64
|
|
|
65
65
|
def stop(self, job_id: str) -> dict[str, Any]:
|
|
66
|
-
return self._view(self.transport.request('POST', f'/v1/
|
|
66
|
+
return self._view(self.transport.request('POST', f'/v1/jobs/{quote(job_id, safe="")}/stop'))
|
|
67
67
|
|
|
68
68
|
def list(self, *, status: str | None = None, cursor: str | None = None, limit: int = 25) -> dict[str, Any]:
|
|
69
69
|
query = urlencode({key: value for key, value in {
|
|
70
70
|
"type": "train", "status": status, "cursor": cursor, "limit": limit,
|
|
71
71
|
}.items() if value is not None})
|
|
72
|
-
page = self.transport.request("GET", "/v1/
|
|
72
|
+
page = self.transport.request("GET", "/v1/jobs?" + query)
|
|
73
73
|
return {**page, "items": [self._view(item) for item in page["items"]]}
|
|
74
74
|
|
|
75
75
|
def checkpoints(self, *, job_id: str | None = None, run_id: str | None = None, cursor: str | None = None, limit: int = 25) -> dict[str, Any]:
|
|
@@ -77,10 +77,10 @@ class TrainJobs:
|
|
|
77
77
|
"job_id": job_id, "run_id": run_id,
|
|
78
78
|
"cursor": cursor, "limit": limit,
|
|
79
79
|
}.items() if value is not None})
|
|
80
|
-
return self.transport.request("GET", "/v1/
|
|
80
|
+
return self.transport.request("GET", "/v1/checkpoints?" + query)
|
|
81
81
|
|
|
82
82
|
def delete_checkpoint(self, checkpoint_id: str) -> dict[str, Any]:
|
|
83
|
-
return self.transport.request("DELETE", f"/v1/
|
|
83
|
+
return self.transport.request("DELETE", f"/v1/checkpoints/{quote(checkpoint_id, safe='')}")
|
|
84
84
|
|
|
85
85
|
def wait(self, job_id: str, *, timeout: float = 600) -> dict[str, Any]:
|
|
86
86
|
if not math.isfinite(timeout) or timeout < 0:
|
|
@@ -61,6 +61,6 @@ def runner_request(args: argparse.Namespace):
|
|
|
61
61
|
if args.timeout_seconds is not None:
|
|
62
62
|
if args.timeout_seconds <= 0 or not args.timeout_seconds.is_integer():
|
|
63
63
|
raise ValueError('Train Runner --timeout-seconds must be positive whole seconds')
|
|
64
|
-
spec['
|
|
64
|
+
spec['timeout_seconds'] = int(args.timeout_seconds)
|
|
65
65
|
return {'type': 'train', 'request_id': args.request_id if args.request_id is not None else str(uuid4()),
|
|
66
66
|
'trials': [{'name': f'trial-{index + 1}', 'spec': dict(spec)} for index in range(count)]}
|
envloop_rsi_loop/agent_runs.py
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
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
|
-
this module pages through ``GET /v1/
|
|
5
|
+
this module pages through ``GET /v1/jobs`` and derives runs from those tags,
|
|
6
6
|
using the same rules as the Console's agent runs page.
|
|
7
7
|
"""
|
|
8
8
|
|
|
@@ -76,10 +76,10 @@ def _round(job: dict[str, Any]) -> int | None:
|
|
|
76
76
|
|
|
77
77
|
def _order(job: dict[str, Any]) -> tuple[str, str]:
|
|
78
78
|
# Creation order, as the platform pages jobs (newest first, reversed here).
|
|
79
|
-
return (str(job.get("created_at") or ""), str(job.get("job_id") or
|
|
79
|
+
return (str(job.get("created_at") or ""), str(job.get("job_id") or ""))
|
|
80
80
|
|
|
81
81
|
|
|
82
|
-
def
|
|
82
|
+
def _duration_seconds(job: dict[str, Any]) -> int | None:
|
|
83
83
|
if job.get("status") not in TERMINAL_STATUSES:
|
|
84
84
|
return None
|
|
85
85
|
try:
|
|
@@ -91,19 +91,19 @@ def _duration_sec(job: dict[str, Any]) -> int | None:
|
|
|
91
91
|
|
|
92
92
|
|
|
93
93
|
def _checkpoint(job: dict[str, Any]) -> dict[str, Any] | None:
|
|
94
|
-
"""The checkpoint a job produced: its agent's version name and platform
|
|
94
|
+
"""The checkpoint a job produced: its agent's version name and platform uri."""
|
|
95
95
|
stored = job.get("checkpoint")
|
|
96
|
-
|
|
96
|
+
uri = stored.get("uri") if isinstance(stored, dict) else None
|
|
97
97
|
name = _field(job, "checkpoint")
|
|
98
|
-
if not isinstance(
|
|
98
|
+
if not isinstance(uri, str) or not uri:
|
|
99
99
|
return None
|
|
100
|
-
return {"name": name, "
|
|
100
|
+
return {"name": name, "uri": uri}
|
|
101
101
|
|
|
102
102
|
|
|
103
103
|
def _job_view(job: dict[str, Any]) -> dict[str, Any]:
|
|
104
104
|
metadata = _metadata(job)
|
|
105
105
|
return {
|
|
106
|
-
"
|
|
106
|
+
"job_id": job.get("job_id"),
|
|
107
107
|
"status": job.get("status"),
|
|
108
108
|
"round": _round(job),
|
|
109
109
|
"purpose": _field(job, "purpose"),
|
|
@@ -111,7 +111,7 @@ def _job_view(job: dict[str, Any]) -> dict[str, Any]:
|
|
|
111
111
|
"transitions": list(job.get("transitions") or []),
|
|
112
112
|
"created_at": job.get("created_at"),
|
|
113
113
|
"updated_at": job.get("updated_at"),
|
|
114
|
-
"
|
|
114
|
+
"duration_seconds": _duration_seconds(job),
|
|
115
115
|
"base_model": job.get("base_model"),
|
|
116
116
|
"lora_rank": job.get("lora_rank"),
|
|
117
117
|
"model_id": job.get("model_id"),
|
|
@@ -131,8 +131,7 @@ def _summary(run: str, jobs: list[dict[str, Any]]) -> dict[str, Any]:
|
|
|
131
131
|
latest_saved = next((job for job in reversed(jobs) if _checkpoint(job) is not None), None)
|
|
132
132
|
latest_checkpoint = _checkpoint(latest_saved) if latest_saved is not None else None
|
|
133
133
|
if latest_checkpoint is not None:
|
|
134
|
-
latest_checkpoint["
|
|
135
|
-
or cast(dict[str, Any], latest_saved).get("train_job_id"))
|
|
134
|
+
latest_checkpoint["job_id"] = cast(dict[str, Any], latest_saved).get("job_id")
|
|
136
135
|
rounds = [number for number in map(_round, jobs) if number is not None]
|
|
137
136
|
activity = [stamp for job in jobs if isinstance(stamp := job.get("updated_at") or job.get("created_at"), str)]
|
|
138
137
|
return {
|
|
@@ -155,7 +154,7 @@ def _detail(run: str, jobs: list[dict[str, Any]]) -> dict[str, Any]:
|
|
|
155
154
|
rounds.setdefault(view["round"], []).append(view)
|
|
156
155
|
ordered = sorted(rounds, key=lambda number: (number is None, number or 0))
|
|
157
156
|
checkpoints = [
|
|
158
|
-
{**view["checkpoint"], "round": view["round"], "
|
|
157
|
+
{**view["checkpoint"], "round": view["round"], "job_id": view["job_id"],
|
|
159
158
|
"created_at": view["created_at"]}
|
|
160
159
|
for view in views if view["checkpoint"] is not None
|
|
161
160
|
]
|
|
@@ -166,10 +165,6 @@ def _detail(run: str, jobs: list[dict[str, Any]]) -> dict[str, Any]:
|
|
|
166
165
|
}
|
|
167
166
|
|
|
168
167
|
|
|
169
|
-
|
|
170
|
-
# SDK limit names and the RSI API's `limits` keys.
|
|
171
|
-
RSI_LIMIT_FIELDS = {"max_model_cost_usd": "model_usd_cap", "max_cost_usd": "usd_cap", "max_model_requests": "model_request_cap"}
|
|
172
|
-
|
|
173
168
|
class AgentRuns:
|
|
174
169
|
"""Read-only agent runs derived from the visible Train Jobs listing."""
|
|
175
170
|
|
|
@@ -183,7 +178,7 @@ class AgentRuns:
|
|
|
183
178
|
if observed_at is None:
|
|
184
179
|
observed_at = page.get("observed_at")
|
|
185
180
|
for job in page.get("items") or []:
|
|
186
|
-
job_id = job.get("job_id")
|
|
181
|
+
job_id = job.get("job_id") if isinstance(job, dict) else None
|
|
187
182
|
if not isinstance(job, dict) or job_id in ids:
|
|
188
183
|
continue
|
|
189
184
|
if job_id is not None:
|
|
@@ -227,10 +222,10 @@ class AgentRuns:
|
|
|
227
222
|
max_model_requests: int | None = None, domain: str | None = None, rounds: int | None = None,
|
|
228
223
|
dry_run: bool = False, student_model: str | None = None) -> dict[str, Any]:
|
|
229
224
|
"""Start a new RSI run; the platform validates every field. ``dry_run`` uses no GPU."""
|
|
230
|
-
limits: dict[str, Any] = {
|
|
225
|
+
limits: dict[str, Any] = {"max_model_cost_usd": max_model_cost_usd}
|
|
231
226
|
for key, value in (("max_cost_usd", max_cost_usd), ("max_model_requests", max_model_requests)):
|
|
232
227
|
if value is not None:
|
|
233
|
-
limits[
|
|
228
|
+
limits[key] = value
|
|
234
229
|
body: dict[str, Any] = {"run": run, "brief": brief, "limits": limits}
|
|
235
230
|
for key, value in (("domain", domain), ("rounds", rounds), ("student_model", student_model)):
|
|
236
231
|
if value is not None:
|
envloop_rsi_loop/cli.py
CHANGED
|
@@ -5,7 +5,7 @@ def add_commands(jobs: "argparse._SubParsersAction[argparse.ArgumentParser]"):
|
|
|
5
5
|
runs = jobs.add_parser("run", help="Inspect runs grouped from their Train Jobs").add_subparsers(dest="run_action", required=True)
|
|
6
6
|
runs_list = runs.add_parser("list", help="List agent runs, most recently updated first")
|
|
7
7
|
runs_list.add_argument("--json", action="store_true", help="Output the complete response as JSON (default: table)")
|
|
8
|
-
runs_get = runs.add_parser("get", help="Show a run's rounds, Train Jobs and
|
|
8
|
+
runs_get = runs.add_parser("get", help="Show a run's rounds, Train Jobs and Checkpoints")
|
|
9
9
|
runs_get.add_argument("run", metavar="RUN", help="run tag: tags.rsi_run or agent_run")
|
|
10
10
|
runs_get.add_argument("--json", action="store_true", help="Output the complete run as JSON (default: tables)")
|
|
11
11
|
runs_start = runs.add_parser("start", help="Start a new RSI loop run (paid; needs training.run)")
|
envloop_rsi_loop/cli_output.py
CHANGED
|
@@ -18,7 +18,7 @@ def trainings(run: dict[str, Any]) -> str:
|
|
|
18
18
|
def checkpoint_name(checkpoint: dict[str, Any] | None) -> Any:
|
|
19
19
|
if not checkpoint:
|
|
20
20
|
return None
|
|
21
|
-
return checkpoint.get("name") or checkpoint.get("
|
|
21
|
+
return checkpoint.get("name") or checkpoint.get("uri")
|
|
22
22
|
|
|
23
23
|
|
|
24
24
|
def run_row(item: dict[str, Any]) -> tuple[Any, ...]:
|
|
@@ -42,8 +42,8 @@ def run_output(run: dict[str, Any]) -> str:
|
|
|
42
42
|
"""A run's summary, one row per Train Job in round order, then its checkpoints."""
|
|
43
43
|
latest = run.get("latest_checkpoint")
|
|
44
44
|
checkpoint = checkpoint_name(latest)
|
|
45
|
-
if latest and latest.get("name") and latest.get("
|
|
46
|
-
checkpoint = f"{display(latest['name'])} {display(latest['
|
|
45
|
+
if latest and latest.get("name") and latest.get("uri"):
|
|
46
|
+
checkpoint = f"{display(latest['name'])} {display(latest['uri'])}"
|
|
47
47
|
fields = (
|
|
48
48
|
("Run", run.get("run")), ("Agent", run.get("agent")), ("Status", run_status(run.get("status"))),
|
|
49
49
|
("Latest round", run.get("latest_round")), ("Trainings", trainings(run) + " succeeded"),
|
|
@@ -53,16 +53,16 @@ def run_output(run: dict[str, Any]) -> str:
|
|
|
53
53
|
summary = "\n".join(f"{label + ':':<19}" + (status_text(display(value)) if label == "Status" else display(value))
|
|
54
54
|
for label, value in fields)
|
|
55
55
|
jobs = table(
|
|
56
|
-
("ROUND", "
|
|
57
|
-
[(round_.get("round"), job.get("job_id")
|
|
58
|
-
job.get("method"), job.get("lora_rank"), duration(job.get("
|
|
56
|
+
("ROUND", "JOB ID", "STATUS", "PURPOSE", "METHOD", "RANK", "DURATION", "CHECKPOINT"),
|
|
57
|
+
[(round_.get("round"), job.get("job_id"), job.get("status"), job.get("purpose"),
|
|
58
|
+
job.get("method"), job.get("lora_rank"), duration(job.get("duration_seconds")),
|
|
59
59
|
checkpoint_name(job.get("checkpoint")))
|
|
60
60
|
for round_ in run.get("rounds") or [] for job in round_.get("jobs") or []],
|
|
61
61
|
)
|
|
62
62
|
checkpoints = run.get("checkpoints") or []
|
|
63
63
|
produced = table(
|
|
64
|
-
("CHECKPOINT", "ROUND", "
|
|
65
|
-
[(item.get("name"), item.get("round"), item.get("
|
|
64
|
+
("CHECKPOINT", "ROUND", "JOB ID", "URI"),
|
|
65
|
+
[(item.get("name"), item.get("round"), item.get("job_id"), item.get("uri")) for item in checkpoints],
|
|
66
66
|
) if checkpoints else "No checkpoints yet."
|
|
67
67
|
return "\n\n".join((summary, jobs, produced))
|
|
68
68
|
|
|
File without changes
|
|
File without changes
|