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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: laya-cli
3
- Version: 0.2.1
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "laya-cli"
3
- version = "0.2.1"
3
+ version = "0.2.2"
4
4
  description = "Ergonomic CLI for Laya — typed decisions for humans and AI agents (predict, batch, presets, shortlist, router)"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -1,3 +1,3 @@
1
1
  """laya-cli — streaming JSONL classifier over Laya."""
2
2
 
3
- __version__ = "0.2.1"
3
+ __version__ = "0.2.2"
@@ -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
- raw = "|".join([model or "", subfolder or "", device or "", "1" if router else "0", lang or ""])
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
@@ -499,7 +499,7 @@ wheels = [
499
499
 
500
500
  [[package]]
501
501
  name = "laya-cli"
502
- version = "0.2.1"
502
+ version = "0.2.2"
503
503
  source = { editable = "." }
504
504
  dependencies = [
505
505
  { name = "huggingface-hub" },
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