euler-train 2.8.0__tar.gz → 2.9.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {euler_train-2.8.0 → euler_train-2.9.0}/PKG-INFO +1 -1
- {euler_train-2.8.0 → euler_train-2.9.0}/README.md +9 -11
- {euler_train-2.8.0 → euler_train-2.9.0}/euler_train/stream.py +21 -18
- {euler_train-2.8.0 → euler_train-2.9.0}/euler_train/stream_check_cli.py +8 -1
- {euler_train-2.8.0 → euler_train-2.9.0}/euler_train.egg-info/PKG-INFO +1 -1
- {euler_train-2.8.0 → euler_train-2.9.0}/pyproject.toml +1 -1
- {euler_train-2.8.0 → euler_train-2.9.0}/tests/test_runlog.py +77 -10
- {euler_train-2.8.0 → euler_train-2.9.0}/euler_train/__init__.py +0 -0
- {euler_train-2.8.0 → euler_train-2.9.0}/euler_train/_dataset_contract.py +0 -0
- {euler_train-2.8.0 → euler_train-2.9.0}/euler_train/architecture.py +0 -0
- {euler_train-2.8.0 → euler_train-2.9.0}/euler_train/environment.py +0 -0
- {euler_train-2.8.0 → euler_train-2.9.0}/euler_train/git_info.py +0 -0
- {euler_train-2.8.0 → euler_train-2.9.0}/euler_train/outputs.py +0 -0
- {euler_train-2.8.0 → euler_train-2.9.0}/euler_train/run.py +0 -0
- {euler_train-2.8.0 → euler_train-2.9.0}/euler_train/serialization.py +0 -0
- {euler_train-2.8.0 → euler_train-2.9.0}/euler_train/slurm.py +0 -0
- {euler_train-2.8.0 → euler_train-2.9.0}/euler_train.egg-info/SOURCES.txt +0 -0
- {euler_train-2.8.0 → euler_train-2.9.0}/euler_train.egg-info/dependency_links.txt +0 -0
- {euler_train-2.8.0 → euler_train-2.9.0}/euler_train.egg-info/entry_points.txt +0 -0
- {euler_train-2.8.0 → euler_train-2.9.0}/euler_train.egg-info/requires.txt +0 -0
- {euler_train-2.8.0 → euler_train-2.9.0}/euler_train.egg-info/top_level.txt +0 -0
- {euler_train-2.8.0 → euler_train-2.9.0}/setup.cfg +0 -0
|
@@ -64,9 +64,9 @@ run = euler_train.init(
|
|
|
64
64
|
config=cfg,
|
|
65
65
|
stream={
|
|
66
66
|
"base_url": os.environ["EULER_VIEW_BASE_URL"],
|
|
67
|
-
"model_id": 42,
|
|
68
67
|
"api_token": os.environ["EULER_VIEW_API_TOKEN"],
|
|
69
68
|
"stream_attach_token": os.environ.get("EULER_VIEW_STREAM_ATTACH_TOKEN"),
|
|
69
|
+
"model_id": 42, # only needed for the SLURM fallback path
|
|
70
70
|
},
|
|
71
71
|
)
|
|
72
72
|
```
|
|
@@ -74,7 +74,7 @@ run = euler_train.init(
|
|
|
74
74
|
What each field is for:
|
|
75
75
|
|
|
76
76
|
- `base_url`: Euler View server base URL, for example `https://view.example.com`.
|
|
77
|
-
- `model_id`: Euler View model that should receive this run.
|
|
77
|
+
- `model_id`: Euler View model that should receive this run. This is optional when `stream_attach_token` is provided, and required for the SLURM fallback path.
|
|
78
78
|
- `api_token` or `access_token`: credential used to open a stream session. This is required whenever the package needs to negotiate its own short-lived ingest token, even if `stream_attach_token` is also provided.
|
|
79
79
|
- `stream_attach_token`: optional opaque launch-attachment token. When present, the producer sends it during the session handshake and Euler View attaches the stream to that launch directly. This is the preferred path for interactive or otherwise non-SLURM launches. Internally Euler View currently maps this token to `model_launches.id`, but the package treats it as an opaque token.
|
|
80
80
|
- `stream_token`: optional pre-issued ingest token. If you pass this, the package skips the session handshake entirely and posts events directly to the ingest endpoint.
|
|
@@ -85,9 +85,9 @@ What each field is for:
|
|
|
85
85
|
How the handshake works:
|
|
86
86
|
|
|
87
87
|
1. `euler_train` creates the local run directory and writes `meta.json`, `config.json`, and the other standard files as usual.
|
|
88
|
-
2. When the first stream flush happens, the package requests a session from Euler View at
|
|
88
|
+
2. When the first stream flush happens, the package requests a session from Euler View at `POST /api/model-run-stream/session`.
|
|
89
89
|
3. If `stream_attach_token` is configured, it is sent as `streamAttachToken` and used as the explicit launch attachment key.
|
|
90
|
-
4. If no `stream_attach_token` is configured, the package falls back to the SLURM metadata already captured in `meta.json["slurm"]["job_id"]`.
|
|
90
|
+
4. If no `stream_attach_token` is configured, the package falls back to the SLURM metadata already captured in `meta.json["slurm"]["job_id"]`. In that fallback mode, `model_id` must be configured so Euler View knows which model namespace to match against.
|
|
91
91
|
5. Euler View returns a short-lived ingest token plus the canonical `streamAttachToken`; the package caches that returned token and reuses it on later session refreshes.
|
|
92
92
|
6. Subsequent events are sent to `/api/model-run-stream/ingest`.
|
|
93
93
|
|
|
@@ -100,27 +100,25 @@ If you need to verify from a cluster node whether Euler View is reachable and wh
|
|
|
100
100
|
```bash
|
|
101
101
|
euler-train.stream.check \
|
|
102
102
|
--api-url https://view.example.com \
|
|
103
|
-
--model-id 42 \
|
|
104
103
|
--api-key "$EULER_VIEW_API_TOKEN" \
|
|
105
104
|
--stream-attach-token "$EULER_VIEW_STREAM_ATTACH_TOKEN"
|
|
106
105
|
```
|
|
107
106
|
|
|
108
|
-
This command calls
|
|
109
|
-
|
|
110
|
-
- `POST /api/models/{model_id}/runs/{run_id}/stream/check`
|
|
107
|
+
This command calls `POST /api/model-run-stream/check`.
|
|
111
108
|
|
|
112
109
|
It validates:
|
|
113
110
|
|
|
114
111
|
- network reachability to Euler View
|
|
115
112
|
- API token validity and scope
|
|
116
|
-
- model existence
|
|
117
113
|
- stream attachment resolution via `stream_attach_token`, when provided
|
|
114
|
+
- model existence, when the request uses the `model_id` + `--slurm-job-id` fallback path
|
|
118
115
|
- SLURM fallback matching via `--slurm-job-id`, when no attach token is provided
|
|
119
116
|
|
|
120
117
|
Useful flags:
|
|
121
118
|
|
|
122
119
|
- `--run-id`: override the dry-run run ID. If omitted, the CLI generates a temporary `stream-check-*` ID.
|
|
123
120
|
- `--stream-attach-token`: test the explicit launch attachment path.
|
|
121
|
+
- `--model-id`: required for the fallback SLURM matching path, optional when `--stream-attach-token` is used.
|
|
124
122
|
- `--slurm-job-id`: test the fallback SLURM matching path.
|
|
125
123
|
- `--datasource-id`: include the datasource hint that a real stream session would send.
|
|
126
124
|
- `--euler-train-dir` and `--run-dir`: include path hints in the dry-run payload.
|
|
@@ -131,8 +129,8 @@ The dry-run check does not start a run and does not mint an ingest token. It onl
|
|
|
131
129
|
What has to be configured where:
|
|
132
130
|
|
|
133
131
|
- In the training script or job wrapper: pass `stream={...}` to `euler_train.init(...)`.
|
|
134
|
-
- In Euler View / the launcher: if the run should attach to a specific launch regardless of SLURM availability, provide a `stream_attach_token` to the runtime and forward it into the `stream` config.
|
|
135
|
-
- Under plain SLURM launches: no extra attachment config is required as long as Euler View can uniquely match the run by `slurm.job_id
|
|
132
|
+
- In Euler View / the launcher: if the run should attach to a specific launch regardless of SLURM availability, provide a `stream_attach_token` to the runtime and forward it into the `stream` config. In that mode the producer does not need to know `model_id`.
|
|
133
|
+
- Under plain SLURM launches: no extra attachment config is required as long as Euler View can uniquely match the run by `slurm.job_id`, but `model_id` still has to be configured for the fallback lookup.
|
|
136
134
|
- If neither `stream_attach_token` nor a unique SLURM match is available, run streaming still works, but launch-to-run linking in Euler View may remain unresolved.
|
|
137
135
|
|
|
138
136
|
For new integrations, prefer `stream_attach_token`. The older `launch_id` / `launchId` config keys are still accepted as compatibility aliases, but new code should treat the attachment value as an opaque stream token rather than a launch ID.
|
|
@@ -11,7 +11,6 @@ from datetime import datetime
|
|
|
11
11
|
from pathlib import Path
|
|
12
12
|
from typing import Any, Protocol
|
|
13
13
|
from urllib.error import HTTPError, URLError
|
|
14
|
-
from urllib.parse import quote
|
|
15
14
|
from urllib.request import Request, urlopen
|
|
16
15
|
|
|
17
16
|
from .serialization import _json_default
|
|
@@ -146,9 +145,9 @@ class EulerViewStreamConfig:
|
|
|
146
145
|
)
|
|
147
146
|
|
|
148
147
|
if self.stream_token is None:
|
|
149
|
-
if self.model_id is None:
|
|
148
|
+
if self.model_id is None and self.stream_attach_token is None:
|
|
150
149
|
raise ValueError(
|
|
151
|
-
"stream.model_id is required when stream.stream_token is
|
|
150
|
+
"stream.model_id is required when neither stream.stream_token nor stream.stream_attach_token is provided",
|
|
152
151
|
)
|
|
153
152
|
if self.api_token is None and self.access_token is None:
|
|
154
153
|
raise ValueError(
|
|
@@ -312,11 +311,10 @@ class EulerViewStreamConsumer:
|
|
|
312
311
|
if not force and now < self._next_retry_at:
|
|
313
312
|
return False
|
|
314
313
|
|
|
315
|
-
session_url =
|
|
316
|
-
f"{self.config.base_url}/api/models/{self.config.model_id}"
|
|
317
|
-
f"/runs/{quote(self._context.run_id, safe='')}/stream/session"
|
|
318
|
-
)
|
|
314
|
+
session_url = f"{self.config.base_url}/api/model-run-stream/session"
|
|
319
315
|
session_body = _build_stream_session_body(
|
|
316
|
+
run_id=self._context.run_id,
|
|
317
|
+
model_id=self.config.model_id,
|
|
320
318
|
run_dir=self.config.run_dir or str(self._context.run_dir),
|
|
321
319
|
euler_train_dir=self.config.euler_train_dir or str(self._context.runs_dir),
|
|
322
320
|
stream_attach_token=(
|
|
@@ -447,9 +445,6 @@ def check_stream_handshake(
|
|
|
447
445
|
"stream.stream_token bypasses the session handshake; a dry-run handshake requires api_token or access_token instead",
|
|
448
446
|
)
|
|
449
447
|
|
|
450
|
-
if resolved_config.model_id is None:
|
|
451
|
-
raise ValueError("stream.model_id is required for handshake checks")
|
|
452
|
-
|
|
453
448
|
normalized_run_id = (
|
|
454
449
|
_normalize_optional_text(run_id, field_name="run_id")
|
|
455
450
|
if run_id is not None
|
|
@@ -473,6 +468,13 @@ def check_stream_handshake(
|
|
|
473
468
|
raise ValueError(
|
|
474
469
|
"stream_attach_token argument does not match stream.stream_attach_token",
|
|
475
470
|
)
|
|
471
|
+
resolved_stream_attach_token = (
|
|
472
|
+
normalized_stream_attach_token or resolved_config.stream_attach_token
|
|
473
|
+
)
|
|
474
|
+
if resolved_config.model_id is None and resolved_stream_attach_token is None:
|
|
475
|
+
raise ValueError(
|
|
476
|
+
"stream.model_id is required for handshake checks when stream_attach_token is not provided",
|
|
477
|
+
)
|
|
476
478
|
|
|
477
479
|
resolved_datasource_id = (
|
|
478
480
|
_require_positive_int(
|
|
@@ -506,16 +508,13 @@ def check_stream_handshake(
|
|
|
506
508
|
else resolved_config.run_dir
|
|
507
509
|
)
|
|
508
510
|
|
|
509
|
-
url =
|
|
510
|
-
f"{resolved_config.base_url}/api/models/{resolved_config.model_id}"
|
|
511
|
-
f"/runs/{quote(normalized_run_id, safe='')}/stream/check"
|
|
512
|
-
)
|
|
511
|
+
url = f"{resolved_config.base_url}/api/model-run-stream/check"
|
|
513
512
|
payload = _build_stream_session_body(
|
|
513
|
+
run_id=normalized_run_id,
|
|
514
|
+
model_id=resolved_config.model_id,
|
|
514
515
|
run_dir=resolved_run_dir,
|
|
515
516
|
euler_train_dir=resolved_euler_train_dir,
|
|
516
|
-
stream_attach_token=
|
|
517
|
-
normalized_stream_attach_token or resolved_config.stream_attach_token
|
|
518
|
-
),
|
|
517
|
+
stream_attach_token=resolved_stream_attach_token,
|
|
519
518
|
slurm_job_id=resolved_slurm_job_id,
|
|
520
519
|
datasource_id=resolved_datasource_id,
|
|
521
520
|
session_expires_in_sec=resolved_config.session_expires_in_sec,
|
|
@@ -633,6 +632,8 @@ def _extract_session_stream_attach_token(payload: Mapping[str, Any]) -> str | No
|
|
|
633
632
|
|
|
634
633
|
def _build_stream_session_body(
|
|
635
634
|
*,
|
|
635
|
+
run_id: str,
|
|
636
|
+
model_id: int | None,
|
|
636
637
|
run_dir: str | None,
|
|
637
638
|
euler_train_dir: str | None,
|
|
638
639
|
stream_attach_token: str | None,
|
|
@@ -640,7 +641,9 @@ def _build_stream_session_body(
|
|
|
640
641
|
datasource_id: int | None,
|
|
641
642
|
session_expires_in_sec: int | None,
|
|
642
643
|
) -> dict[str, Any]:
|
|
643
|
-
body: dict[str, Any] = {}
|
|
644
|
+
body: dict[str, Any] = {"runId": run_id}
|
|
645
|
+
if model_id is not None:
|
|
646
|
+
body["modelId"] = model_id
|
|
644
647
|
if run_dir is not None:
|
|
645
648
|
body["runDir"] = run_dir
|
|
646
649
|
if euler_train_dir is not None:
|
|
@@ -14,7 +14,11 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
14
14
|
description="Dry-run the Euler View stream handshake",
|
|
15
15
|
)
|
|
16
16
|
parser.add_argument("--api-url", required=True, help="Euler View base URL")
|
|
17
|
-
parser.add_argument(
|
|
17
|
+
parser.add_argument(
|
|
18
|
+
"--model-id",
|
|
19
|
+
type=int,
|
|
20
|
+
help="Euler View model ID. Required when using the SLURM fallback path instead of --stream-attach-token.",
|
|
21
|
+
)
|
|
18
22
|
parser.add_argument(
|
|
19
23
|
"--api-key",
|
|
20
24
|
required=True,
|
|
@@ -64,6 +68,9 @@ def main(argv: Sequence[str] | None = None) -> int:
|
|
|
64
68
|
parser = build_parser()
|
|
65
69
|
args = parser.parse_args(argv)
|
|
66
70
|
|
|
71
|
+
if args.model_id is None and not args.stream_attach_token:
|
|
72
|
+
parser.error("--model-id is required unless --stream-attach-token is provided")
|
|
73
|
+
|
|
67
74
|
try:
|
|
68
75
|
result = check_stream_handshake(
|
|
69
76
|
{
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "euler-train"
|
|
7
|
-
version = "2.
|
|
7
|
+
version = "2.9.0"
|
|
8
8
|
description = "Lightweight file-based experiment logger for PyTorch"
|
|
9
9
|
requires-python = ">=3.9"
|
|
10
10
|
dependencies = ["numpy", "euler-dataset-contract"]
|
|
@@ -1963,7 +1963,7 @@ class TestStreaming:
|
|
|
1963
1963
|
"timeout": timeout,
|
|
1964
1964
|
},
|
|
1965
1965
|
)
|
|
1966
|
-
if request.full_url.
|
|
1966
|
+
if request.full_url == "https://sync.example/api/model-run-stream/session":
|
|
1967
1967
|
return _FakeHttpResponse(
|
|
1968
1968
|
{
|
|
1969
1969
|
"token": "stream-token",
|
|
@@ -1997,12 +1997,12 @@ class TestStreaming:
|
|
|
1997
1997
|
run.log({"loss": 0.25}, step=3, epoch=1)
|
|
1998
1998
|
run.finish()
|
|
1999
1999
|
|
|
2000
|
-
assert calls[0]["url"] ==
|
|
2001
|
-
f"https://sync.example/api/models/42/runs/{run.run_id}/stream/session"
|
|
2002
|
-
)
|
|
2000
|
+
assert calls[0]["url"] == "https://sync.example/api/model-run-stream/session"
|
|
2003
2001
|
assert calls[0]["method"] == "POST"
|
|
2004
2002
|
assert calls[0]["headers"]["authorization"] == "Bearer user-token"
|
|
2005
2003
|
assert calls[0]["body"] == {
|
|
2004
|
+
"runId": run.run_id,
|
|
2005
|
+
"modelId": 42,
|
|
2006
2006
|
"runDir": str(run.dir),
|
|
2007
2007
|
"eulerTrainDir": str(run.dir.parent),
|
|
2008
2008
|
"datasourceId": 7,
|
|
@@ -2028,6 +2028,73 @@ class TestStreaming:
|
|
|
2028
2028
|
finish_event = ingest_calls[2]["body"]["events"][0]
|
|
2029
2029
|
assert finish_event["status"] == "completed"
|
|
2030
2030
|
|
|
2031
|
+
def test_euler_view_http_consumer_can_handshake_without_model_id_when_attach_token_is_present(
|
|
2032
|
+
self,
|
|
2033
|
+
tmp_path,
|
|
2034
|
+
monkeypatch,
|
|
2035
|
+
):
|
|
2036
|
+
calls: list[dict] = []
|
|
2037
|
+
|
|
2038
|
+
def fake_urlopen(request, timeout=0):
|
|
2039
|
+
headers = {k.lower(): v for k, v in request.header_items()}
|
|
2040
|
+
body = json.loads(request.data.decode("utf-8"))
|
|
2041
|
+
calls.append(
|
|
2042
|
+
{
|
|
2043
|
+
"url": request.full_url,
|
|
2044
|
+
"method": request.get_method(),
|
|
2045
|
+
"headers": headers,
|
|
2046
|
+
"body": body,
|
|
2047
|
+
"timeout": timeout,
|
|
2048
|
+
},
|
|
2049
|
+
)
|
|
2050
|
+
if request.full_url.endswith("/api/model-run-stream/session"):
|
|
2051
|
+
return _FakeHttpResponse(
|
|
2052
|
+
{
|
|
2053
|
+
"token": "stream-token",
|
|
2054
|
+
"expiresAt": "2099-01-01T00:00:00+00:00",
|
|
2055
|
+
"ingestUrl": "https://sync.example/api/model-run-stream/ingest",
|
|
2056
|
+
"run": {
|
|
2057
|
+
"modelId": 42,
|
|
2058
|
+
"runId": "placeholder",
|
|
2059
|
+
"streamAttachToken": "attach-123",
|
|
2060
|
+
},
|
|
2061
|
+
},
|
|
2062
|
+
)
|
|
2063
|
+
if request.full_url.endswith("/api/model-run-stream/ingest"):
|
|
2064
|
+
return _FakeHttpResponse(
|
|
2065
|
+
{
|
|
2066
|
+
"success": True,
|
|
2067
|
+
"events": [],
|
|
2068
|
+
"latestCursor": len(calls),
|
|
2069
|
+
},
|
|
2070
|
+
)
|
|
2071
|
+
raise AssertionError(f"Unexpected URL {request.full_url}")
|
|
2072
|
+
|
|
2073
|
+
monkeypatch.setattr(stream_mod, "urlopen", fake_urlopen)
|
|
2074
|
+
|
|
2075
|
+
run = euler_train.init(
|
|
2076
|
+
dir=str(tmp_path / "r"),
|
|
2077
|
+
config={"lr": 1e-3},
|
|
2078
|
+
stream={
|
|
2079
|
+
"base_url": "https://sync.example",
|
|
2080
|
+
"api_token": "user-token",
|
|
2081
|
+
"stream_attach_token": "attach-123",
|
|
2082
|
+
"batch_size": 1,
|
|
2083
|
+
},
|
|
2084
|
+
)
|
|
2085
|
+
run.log({"loss": 0.25}, step=3, epoch=1)
|
|
2086
|
+
run.finish()
|
|
2087
|
+
|
|
2088
|
+
assert calls[0]["url"] == "https://sync.example/api/model-run-stream/session"
|
|
2089
|
+
assert calls[0]["method"] == "POST"
|
|
2090
|
+
assert calls[0]["headers"]["authorization"] == "Bearer user-token"
|
|
2091
|
+
assert calls[0]["body"] == {
|
|
2092
|
+
"runId": run.run_id,
|
|
2093
|
+
"runDir": str(run.dir),
|
|
2094
|
+
"eulerTrainDir": str(run.dir.parent),
|
|
2095
|
+
"streamAttachToken": "attach-123",
|
|
2096
|
+
}
|
|
2097
|
+
|
|
2031
2098
|
def test_stream_network_failures_do_not_break_training(
|
|
2032
2099
|
self,
|
|
2033
2100
|
tmp_path,
|
|
@@ -2072,7 +2139,7 @@ class TestStreaming:
|
|
|
2072
2139
|
"timeout": timeout,
|
|
2073
2140
|
},
|
|
2074
2141
|
)
|
|
2075
|
-
if request.full_url.
|
|
2142
|
+
if request.full_url == "https://sync.example/api/model-run-stream/check":
|
|
2076
2143
|
return _FakeHttpResponse(
|
|
2077
2144
|
{
|
|
2078
2145
|
"success": True,
|
|
@@ -2096,7 +2163,6 @@ class TestStreaming:
|
|
|
2096
2163
|
payload = stream_mod.check_stream_handshake(
|
|
2097
2164
|
{
|
|
2098
2165
|
"base_url": "https://sync.example",
|
|
2099
|
-
"model_id": 42,
|
|
2100
2166
|
"api_token": "user-token",
|
|
2101
2167
|
"stream_attach_token": "attach-123",
|
|
2102
2168
|
"datasource_id": 7,
|
|
@@ -2107,7 +2173,7 @@ class TestStreaming:
|
|
|
2107
2173
|
assert payload["success"] is True
|
|
2108
2174
|
assert calls == [
|
|
2109
2175
|
{
|
|
2110
|
-
"url": "https://sync.example/api/
|
|
2176
|
+
"url": "https://sync.example/api/model-run-stream/check",
|
|
2111
2177
|
"method": "POST",
|
|
2112
2178
|
"headers": {
|
|
2113
2179
|
"accept": "application/json",
|
|
@@ -2115,6 +2181,7 @@ class TestStreaming:
|
|
|
2115
2181
|
"content-type": "application/json",
|
|
2116
2182
|
},
|
|
2117
2183
|
"body": {
|
|
2184
|
+
"runId": "dry-run-1",
|
|
2118
2185
|
"streamAttachToken": "attach-123",
|
|
2119
2186
|
"datasourceId": 7,
|
|
2120
2187
|
},
|
|
@@ -2128,7 +2195,7 @@ class TestStreaming:
|
|
|
2128
2195
|
capsys,
|
|
2129
2196
|
):
|
|
2130
2197
|
def fake_urlopen(request, timeout=0):
|
|
2131
|
-
if
|
|
2198
|
+
if request.full_url != "https://sync.example/api/model-run-stream/check":
|
|
2132
2199
|
raise AssertionError(f"Unexpected URL {request.full_url}")
|
|
2133
2200
|
return _FakeHttpResponse(
|
|
2134
2201
|
{
|
|
@@ -2153,12 +2220,12 @@ class TestStreaming:
|
|
|
2153
2220
|
[
|
|
2154
2221
|
"--api-url",
|
|
2155
2222
|
"https://sync.example",
|
|
2156
|
-
"--model-id",
|
|
2157
|
-
"42",
|
|
2158
2223
|
"--api-key",
|
|
2159
2224
|
"user-token",
|
|
2160
2225
|
"--run-id",
|
|
2161
2226
|
"dry-run-2",
|
|
2227
|
+
"--stream-attach-token",
|
|
2228
|
+
"attach-123",
|
|
2162
2229
|
"--json",
|
|
2163
2230
|
],
|
|
2164
2231
|
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|