Coverage for src/lilbee/providers/fleet/engine_install.py: 100%
47 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-11-06 08:17 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-11-06 08:17 +0000
1"""Install advice for a missing engine, in the form that fits how lilbee was installed."""
3from __future__ import annotations
5import sys
6from enum import StrEnum
7from pathlib import Path
9from lilbee._frozen import is_frozen
12class InstallMethod(StrEnum):
13 """How this lilbee was put on the machine."""
15 UV_TOOL = "uv_tool"
16 UV_ENV = "uv_env"
17 PIPX = "pipx"
18 PIP = "pip"
19 HOMEBREW = "homebrew"
20 STANDALONE = "standalone"
21 UNKNOWN = "unknown"
24# Hardware label and index name, one line each in the advice. One index per
25# hardware because the builds are not interchangeable: cpu has every GPU backend
26# off, metal ships only a macOS arm64 wheel, vulkan only Linux and Windows ones.
27_INDEXES: tuple[tuple[str, str], ...] = (
28 ("NVIDIA (CUDA)", "cu125"),
29 ("AMD (ROCm)", "rocm"),
30 ("Apple silicon", "metal"),
31 ("Other GPUs", "vulkan"),
32 ("No GPU", "cpu"),
33)
34_INDEX_URL = "https://lilbee.sh/{index}/"
35_COMMANDS: dict[InstallMethod, str] = {
36 InstallMethod.PIP: "pip install --pre 'lilbee[engine]' --extra-index-url {url}",
37 InstallMethod.UV_ENV: (
38 "uv pip install --prerelease=allow 'lilbee[engine]' --extra-index-url {url}"
39 ),
40 InstallMethod.UV_TOOL: (
41 "uv tool install --force --prerelease=allow 'lilbee[engine]' --extra-index-url {url}"
42 ),
43 InstallMethod.PIPX: (
44 "pipx install --force --pip-args='--pre --extra-index-url {url}' 'lilbee[engine]'"
45 ),
46}
47_BUNDLED_FORMS = (
48 "Install a self-contained build again; it carries its own engine: "
49 "'brew reinstall lilbee' (Homebrew), 'npm install -g lilbee' (npm launcher), "
50 "or the standalone binary from https://github.com/tobocop2/lilbee/releases/latest."
51)
52_BRING_YOUR_OWN = (
53 "To bring your own engine, set LILBEE_LLAMA_SERVER_PATH to a llama-server binary "
54 "and put llama-swap / gguf-parser on PATH."
55)
58def detect_install_method() -> InstallMethod:
59 """Tell how lilbee was installed from the interpreter's own location."""
60 if is_frozen():
61 exe = Path(sys.executable).as_posix().lower()
62 return (
63 InstallMethod.HOMEBREW
64 if ("/cellar/" in exe or "/homebrew/" in exe)
65 else (InstallMethod.STANDALONE)
66 )
67 prefix = Path(sys.prefix).as_posix().lower()
68 if "/uv/tools/" in prefix:
69 return InstallMethod.UV_TOOL
70 if "/pipx/venvs/" in prefix:
71 return InstallMethod.PIPX
72 if sys.prefix == sys.base_prefix:
73 return InstallMethod.UNKNOWN
74 try:
75 lines = (Path(sys.prefix) / "pyvenv.cfg").read_text(encoding="utf-8").splitlines()
76 except OSError:
77 return InstallMethod.PIP
78 made_by_uv = any(line.partition("=")[0].strip() == "uv" for line in lines)
79 return InstallMethod.UV_ENV if made_by_uv else InstallMethod.PIP
82def _index_lines(method: InstallMethod) -> list[str]:
83 template = _COMMANDS[method]
84 return [
85 f" {label + ':':<15}{template.format(url=_INDEX_URL.format(index=index))}"
86 for label, index in _INDEXES
87 ]
90def engine_install_hint(method: InstallMethod | None = None) -> str:
91 """The advice for installing the engine, for *method* or the detected install."""
92 method = detect_install_method() if method is None else method
93 if method in (InstallMethod.HOMEBREW, InstallMethod.STANDALONE):
94 return f"{_BUNDLED_FORMS} {_BRING_YOUR_OWN}"
95 intro = (
96 "The engine is lilbee's 'engine' extra, published on lilbee.sh rather than "
97 "PyPI, so the index is part of the command:"
98 )
99 if method is InstallMethod.UNKNOWN:
100 blocks = [
101 "With pip:",
102 *_index_lines(InstallMethod.PIP),
103 "With uv:",
104 *_index_lines(InstallMethod.UV_TOOL),
105 ]
106 else:
107 blocks = _index_lines(method)
108 return "\n".join([intro, *blocks, _BUNDLED_FORMS, _BRING_YOUR_OWN])