laya-cli 0.2.1__tar.gz → 0.2.2__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.
- {laya_cli-0.2.1 → laya_cli-0.2.2}/PKG-INFO +1 -1
- {laya_cli-0.2.1 → laya_cli-0.2.2}/pyproject.toml +1 -1
- {laya_cli-0.2.1 → laya_cli-0.2.2}/src/laya_cli/__init__.py +1 -1
- {laya_cli-0.2.1 → laya_cli-0.2.2}/src/laya_cli/daemon.py +51 -1
- {laya_cli-0.2.1 → laya_cli-0.2.2}/tests/test_daemon.py +86 -0
- {laya_cli-0.2.1 → laya_cli-0.2.2}/uv.lock +1 -1
- {laya_cli-0.2.1 → laya_cli-0.2.2}/.github/workflows/ci.yml +0 -0
- {laya_cli-0.2.1 → laya_cli-0.2.2}/.github/workflows/publish.yml +0 -0
- {laya_cli-0.2.1 → laya_cli-0.2.2}/.gitignore +0 -0
- {laya_cli-0.2.1 → laya_cli-0.2.2}/.python-version +0 -0
- {laya_cli-0.2.1 → laya_cli-0.2.2}/LICENSE +0 -0
- {laya_cli-0.2.1 → laya_cli-0.2.2}/README.md +0 -0
- {laya_cli-0.2.1 → laya_cli-0.2.2}/TASK.md +0 -0
- {laya_cli-0.2.1 → laya_cli-0.2.2}/src/laya_cli/cli.py +0 -0
- {laya_cli-0.2.1 → laya_cli-0.2.2}/tests/__init__.py +0 -0
- {laya_cli-0.2.1 → laya_cli-0.2.2}/tests/conftest.py +0 -0
- {laya_cli-0.2.1 → laya_cli-0.2.2}/tests/test_classify.py +0 -0
- {laya_cli-0.2.1 → laya_cli-0.2.2}/tests/test_cli.py +0 -0
- {laya_cli-0.2.1 → laya_cli-0.2.2}/tests/test_evaluate.py +0 -0
- {laya_cli-0.2.1 → laya_cli-0.2.2}/tests/test_filter.py +0 -0
- {laya_cli-0.2.1 → laya_cli-0.2.2}/tests/test_predict.py +0 -0
- {laya_cli-0.2.1 → laya_cli-0.2.2}/tests/test_questions.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: laya-cli
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Ergonomic CLI for Laya — typed decisions for humans and AI agents (predict, batch, presets, shortlist, router)
|
|
5
5
|
Project-URL: Homepage, https://github.com/MIt9/laya-cli
|
|
6
6
|
Project-URL: Repository, https://github.com/MIt9/laya-cli.git
|
|
@@ -25,8 +25,58 @@ from typing import Any
|
|
|
25
25
|
CACHE_DIR = Path.home() / ".cache" / "laya-cli" / "daemons"
|
|
26
26
|
|
|
27
27
|
|
|
28
|
+
def _resolve_device(device: str | None) -> str:
|
|
29
|
+
"""Resolve effective device: auto (cuda>mps>cpu) or explicit with fallback to cpu if unavailable."""
|
|
30
|
+
# explicit
|
|
31
|
+
if device is not None and str(device).strip() != "":
|
|
32
|
+
dev = str(device).strip().lower()
|
|
33
|
+
# normalize aliases
|
|
34
|
+
if dev in ("cuda", "cuda:0", "gpu"):
|
|
35
|
+
dev = "cuda"
|
|
36
|
+
elif dev == "mps":
|
|
37
|
+
dev = "mps"
|
|
38
|
+
elif dev == "cpu":
|
|
39
|
+
dev = "cpu"
|
|
40
|
+
# check availability, fallback to cpu
|
|
41
|
+
try:
|
|
42
|
+
import torch
|
|
43
|
+
|
|
44
|
+
if dev == "cuda" and not torch.cuda.is_available():
|
|
45
|
+
return "cpu"
|
|
46
|
+
if dev == "mps" and not (hasattr(torch.backends, "mps") and torch.backends.mps.is_available()):
|
|
47
|
+
return "cpu"
|
|
48
|
+
except Exception:
|
|
49
|
+
pass
|
|
50
|
+
return dev
|
|
51
|
+
# auto
|
|
52
|
+
try:
|
|
53
|
+
import torch
|
|
54
|
+
|
|
55
|
+
if torch.cuda.is_available():
|
|
56
|
+
return "cuda"
|
|
57
|
+
if hasattr(torch.backends, "mps") and torch.backends.mps.is_available():
|
|
58
|
+
return "mps"
|
|
59
|
+
return "cpu"
|
|
60
|
+
except Exception:
|
|
61
|
+
return "cpu"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _normalize_config(
|
|
65
|
+
model: str, subfolder: str | None, device: str | None, router: bool, lang: str | None
|
|
66
|
+
) -> tuple[str, str, str, str, str]:
|
|
67
|
+
"""Single place for config normalization before hashing — used by both serve and client."""
|
|
68
|
+
norm_model = (model or "convaiinnovations/laya").strip()
|
|
69
|
+
norm_subfolder = (subfolder or "").strip()
|
|
70
|
+
# treat "" and None as same (bundle root)
|
|
71
|
+
norm_device = _resolve_device(device)
|
|
72
|
+
norm_router = "1" if router else "0"
|
|
73
|
+
norm_lang = (lang or "").strip().lower()
|
|
74
|
+
return (norm_model, norm_subfolder, norm_device, norm_router, norm_lang)
|
|
75
|
+
|
|
76
|
+
|
|
28
77
|
def config_hash(model: str, subfolder: str | None, device: str | None, router: bool, lang: str | None) -> str:
|
|
29
|
-
|
|
78
|
+
norm = _normalize_config(model, subfolder, device, router, lang)
|
|
79
|
+
raw = "|".join(norm)
|
|
30
80
|
return hashlib.sha256(raw.encode()).hexdigest()[:12]
|
|
31
81
|
|
|
32
82
|
|
|
@@ -241,3 +241,89 @@ def test_daemon_only_localhost(fake_laya):
|
|
|
241
241
|
time.sleep(0.3)
|
|
242
242
|
if daemon_file.exists():
|
|
243
243
|
daemon_file.unlink()
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def test_daemon_config_hash_device_normalization(fake_laya):
|
|
247
|
+
"""Regression for TASK2 BUG: hash must be after device resolve, not raw --device.
|
|
248
|
+
|
|
249
|
+
On MPS Mac, `serve` without --device (auto=mps) and `serve --device mps` must share file,
|
|
250
|
+
and `predict` without --device must find daemon started with --device mps.
|
|
251
|
+
"""
|
|
252
|
+
from laya_cli.daemon import daemon_file_for, daemon_port_for_args
|
|
253
|
+
|
|
254
|
+
# Same effective device -> same file (auto resolves to mps on this Mac, so None and "mps" coincide)
|
|
255
|
+
f_auto = daemon_file_for("convaiinnovations/laya", None, None, False, None)
|
|
256
|
+
f_mps = daemon_file_for("convaiinnovations/laya", None, "mps", False, None)
|
|
257
|
+
assert f_auto == f_mps, "hash after resolve: auto=None and explicit mps must be same file on MPS host"
|
|
258
|
+
|
|
259
|
+
# Start daemon with explicit --device mps, then client without --device should find it
|
|
260
|
+
model = "convaiinnovations/laya-test-hash-mps"
|
|
261
|
+
daemon_file_mps = daemon_file_for(model, None, "mps", False, None)
|
|
262
|
+
daemon_file_auto = daemon_file_for(model, None, None, False, None)
|
|
263
|
+
# They should be same file
|
|
264
|
+
assert daemon_file_mps == daemon_file_auto
|
|
265
|
+
|
|
266
|
+
# Also subfolder "" vs None and lang case
|
|
267
|
+
assert daemon_file_for("m", None, None, False, None) == daemon_file_for("m", "", None, False, None)
|
|
268
|
+
assert daemon_file_for("m", None, None, False, "EN") == daemon_file_for("m", None, None, False, "en")
|
|
269
|
+
|
|
270
|
+
# Now test live daemon discovery across device flag mismatch
|
|
271
|
+
if daemon_file_mps.exists():
|
|
272
|
+
daemon_file_mps.unlink()
|
|
273
|
+
t = threading.Thread(
|
|
274
|
+
target=run_server,
|
|
275
|
+
kwargs=dict(
|
|
276
|
+
model=model,
|
|
277
|
+
subfolder=None,
|
|
278
|
+
device="mps",
|
|
279
|
+
router=False,
|
|
280
|
+
lang=None,
|
|
281
|
+
port=0,
|
|
282
|
+
idle_timeout=10,
|
|
283
|
+
daemon_file=daemon_file_mps,
|
|
284
|
+
),
|
|
285
|
+
daemon=True,
|
|
286
|
+
)
|
|
287
|
+
t.start()
|
|
288
|
+
for _ in range(30):
|
|
289
|
+
time.sleep(0.2)
|
|
290
|
+
alive, _, _ = is_daemon_alive(daemon_file_mps)
|
|
291
|
+
if alive:
|
|
292
|
+
break
|
|
293
|
+
assert alive, "daemon with --device mps did not start"
|
|
294
|
+
|
|
295
|
+
# Client without --device (auto) should find it via daemon_port_for_args
|
|
296
|
+
import types
|
|
297
|
+
|
|
298
|
+
fake_args = types.SimpleNamespace(
|
|
299
|
+
model=model, subfolder=None, device=None, router=False, lang=None, no_daemon=False
|
|
300
|
+
)
|
|
301
|
+
port = daemon_port_for_args(fake_args) # type: ignore
|
|
302
|
+
assert port is not None, (
|
|
303
|
+
"predict without --device should find daemon started with --device mps (hash after resolve)"
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
# Also via CLI: predict without --device should auto-use daemon
|
|
307
|
+
q = {"q1": {"type": "noul", "instructions": "Is it good?"}}
|
|
308
|
+
out, err = _run_cli(
|
|
309
|
+
["predict", "--text", "forest good", "--questions-inline", json.dumps(q), "--model", model, "--format", "json"]
|
|
310
|
+
)
|
|
311
|
+
assert "using daemon" in err, "CLI predict without --device should have used daemon started with --device mps"
|
|
312
|
+
|
|
313
|
+
# Cleanup
|
|
314
|
+
from laya_cli.daemon import _http_post_shutdown
|
|
315
|
+
|
|
316
|
+
alive, status, port = is_daemon_alive(daemon_file_mps)
|
|
317
|
+
if alive and port:
|
|
318
|
+
_http_post_shutdown(port)
|
|
319
|
+
time.sleep(0.3)
|
|
320
|
+
if daemon_file_mps.exists():
|
|
321
|
+
try:
|
|
322
|
+
daemon_file_mps.unlink()
|
|
323
|
+
except Exception:
|
|
324
|
+
pass
|
|
325
|
+
if daemon_file_auto.exists() and daemon_file_auto != daemon_file_mps:
|
|
326
|
+
try:
|
|
327
|
+
daemon_file_auto.unlink()
|
|
328
|
+
except Exception:
|
|
329
|
+
pass
|
|
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
|
|
File without changes
|