Coverage for src/lilbee/providers/fleet/binary.py: 100%

107 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-11-06 08:17 +0000

1"""Resolve the bundled engine binaries (llama-server, llama-swap, gguf-parser).""" 

2 

3from __future__ import annotations 

4 

5import functools 

6import hashlib 

7import shutil 

8import subprocess 

9from enum import StrEnum 

10from importlib.metadata import version as _pkg_version 

11from pathlib import Path 

12 

13from lilbee.providers.base import ProviderError, ProviderErrorKind 

14from lilbee.providers.fleet.engine_install import engine_install_hint 

15 

16# --help exits from arg parsing, before any backend or model load, so this is 

17# generous; a binary that cannot even print usage in this time is not one the 

18# fleet should silently trust either way. 

19_HELP_PROBE_TIMEOUT_S = 15.0 

20 

21 

22class EngineTool(StrEnum): 

23 """A bundled engine executable resolved from the ``lilbee-engine`` wheel.""" 

24 

25 LLAMA_SERVER = "llama-server" 

26 LLAMA_SWAP = "llama-swap" 

27 GGUF_PARSER = "gguf-parser" 

28 

29 

30_BUNDLED_ACCESSORS = { 

31 EngineTool.LLAMA_SERVER: "get_llama_server_path", 

32 EngineTool.LLAMA_SWAP: "get_llama_swap_path", 

33 EngineTool.GGUF_PARSER: "get_gguf_parser_path", 

34} 

35 

36 

37def _bundled_tool(tool: EngineTool) -> Path | None: 

38 """Path to *tool* from the ``lilbee-engine`` wheel, or ``None`` if absent.""" 

39 try: 

40 import lilbee_engine 

41 except ImportError: 

42 return None 

43 accessor = getattr(lilbee_engine, _BUNDLED_ACCESSORS[tool], None) 

44 if accessor is None: # a wheel that predates this tool lacks the accessor 

45 return None 

46 path = Path(accessor()) 

47 return path if path.is_file() else None 

48 

49 

50def engine_pin() -> str: 

51 """Identity of the engine this lilbee would spawn; sharing keys on it. 

52 

53 Two dimensions must match for two processes to share one engine: the engine 

54 BUILD (a configured ``LILBEE_LLAMA_SERVER_PATH`` is its own identity so a 

55 bring-your-own engine never silently shares with a bundled one) and the 

56 revision of the llama-swap config the engine runs under. A process whose pin 

57 differs has ``contract_matches`` refuse the bind and overflows to its own 

58 engine. The settings a launch bakes in are matched per launch instead 

59 (``InstanceLaunch.settings``). Total: never raises, because it runs on every 

60 state write. 

61 """ 

62 # circular: swap_config -> binary via readback.engine_help 

63 from lilbee.providers.fleet.swap_config import SWAP_CONFIG_REVISION 

64 

65 return f"{engine_build_id()}|swap-config={SWAP_CONFIG_REVISION}" 

66 

67 

68def engine_build_id() -> str: 

69 """The engine build's identity: configured path, wheel pin, PATH, or unpinned. 

70 

71 A BYO (``custom:``) or PATH-resolved (``path:``) binary is identified by its 

72 location AND a cheap build fingerprint (size + mtime), so replacing the binary 

73 in place (a brew upgrade, a re-download) changes the pin and never binds a new 

74 process to an engine spawned from the old build. The bundled wheel needs no 

75 fingerprint: its pin already encodes the build. 

76 """ 

77 from lilbee.core.config import cfg 

78 

79 if cfg.llama_server_path: 

80 return f"custom:{cfg.llama_server_path}@{_binary_signature(Path(cfg.llama_server_path))}" 

81 try: 

82 import lilbee_engine 

83 except ImportError: 

84 lilbee_engine = None 

85 if lilbee_engine is not None: 

86 try: 

87 return str(lilbee_engine.get_engine_pin()) 

88 except AttributeError: # pre-pin wheels lack the accessor 

89 return f"wheel:{_engine_wheel_version()}" 

90 found = shutil.which(EngineTool.LLAMA_SERVER.value) 

91 if found is not None: 

92 return f"path:{found}@{_binary_signature(Path(found))}" 

93 return "unpinned" 

94 

95 

96def _engine_wheel_version() -> str: 

97 """The engine wheel's version, or a marker when it has no distribution metadata. 

98 

99 ``lilbee_engine`` can be importable with nothing to look up: an extracted 

100 wheel on sys.path, a vendored copy, or a distribution registered under a name 

101 that does not normalize to ``lilbee-engine``. Since this feeds the pin, and 

102 the pin is computed on every state write, a missing version degrades to a 

103 marker rather than raising out of ``engine_pin``. 

104 """ 

105 from importlib.metadata import PackageNotFoundError 

106 

107 try: 

108 return _pkg_version("lilbee-engine") 

109 except PackageNotFoundError: 

110 return "unknown" 

111 

112 

113def _binary_signature(path: Path) -> str: 

114 """A cheap build fingerprint of the binary at *path*: size and mtime. 

115 

116 An in-place replacement changes both, so the pin stops matching the old build. 

117 Best-effort and total (engine_pin runs on every state write): an unstatable 

118 path degrades to a fixed marker rather than raising. 

119 """ 

120 try: 

121 st = path.stat() 

122 except OSError: 

123 return "unstatable" 

124 return f"{st.st_size}-{st.st_mtime_ns}" 

125 

126 

127def engine_binary_identity(binary: Path) -> str: 

128 """Identity of the engine file at *binary*: its location and a digest of its bytes. 

129 

130 The file rather than the wheel, because a stub wheel can carry the version of 

131 the real one; only the bytes that answered a probe identify what answered it. 

132 The bytes rather than the stat fields ``engine_pin`` matches on: an upgrade 

133 rewrites the file in place, the rewrite keeps the inode, and a build of the 

134 same size written inside one mtime tick moves no stat field at all, so 

135 metadata can repeat across a real engine change. ``llama-server`` is a 

136 launcher of a few tens of kilobytes that loads the backend libraries at run 

137 time, and this is read once per planning pass, so the digest costs far less 

138 than the device probe it decides to re-run. 

139 """ 

140 try: 

141 with binary.open("rb") as handle: 

142 return f"{binary}@{hashlib.file_digest(handle, 'sha256').hexdigest()}" 

143 except OSError: 

144 return f"{binary}@unreadable" 

145 

146 

147def resolve_engine_tool(tool: EngineTool) -> Path: 

148 """Resolve *tool*: configured llama-server path, then bundled wheel, then PATH. 

149 

150 Never downloads anything; the binaries arrive via the bundled ``lilbee-engine`` 

151 wheel or bring-your-own. Only llama-server honors ``LILBEE_LLAMA_SERVER_PATH`` 

152 (an explicit setting beats the bundled wheel); the other tools resolve from the 

153 wheel, then ``PATH``. 

154 """ 

155 if tool is EngineTool.LLAMA_SERVER: 

156 from lilbee.core.config import cfg 

157 

158 if cfg.llama_server_path: 

159 configured = Path(cfg.llama_server_path) 

160 if not configured.is_file(): 

161 raise ProviderError(f"LILBEE_LLAMA_SERVER_PATH is not a file: {configured}") 

162 return configured 

163 

164 bundled = _bundled_tool(tool) 

165 if bundled is not None: 

166 return bundled 

167 

168 found = shutil.which(tool.value) 

169 if found is not None: 

170 return Path(found) 

171 

172 # Only llama-server carries NOT_FOUND: it marks the engine-less host that 

173 # legitimately serves nothing. A missing sibling tool (gguf-parser) must not 

174 # take that kind, or the sizing fallback would misreport it as a model that 

175 # isn't installed. 

176 kind = ( 

177 ProviderErrorKind.NOT_FOUND 

178 if tool is EngineTool.LLAMA_SERVER 

179 else ProviderErrorKind.UNKNOWN 

180 ) 

181 raise ProviderError(f"{tool.value} binary not found. {engine_install_hint()}", kind=kind) 

182 

183 

184def resolve_llama_server() -> Path: 

185 """Resolve the ``llama-server`` executable.""" 

186 return resolve_engine_tool(EngineTool.LLAMA_SERVER) 

187 

188 

189def resolve_llama_swap() -> Path: 

190 """Resolve the ``llama-swap`` executable.""" 

191 return resolve_engine_tool(EngineTool.LLAMA_SWAP) 

192 

193 

194def resolve_gguf_parser() -> Path: 

195 """Resolve the ``gguf-parser`` executable.""" 

196 return resolve_engine_tool(EngineTool.GGUF_PARSER) 

197 

198 

199def llama_server_runtime_env() -> dict[str, str]: 

200 """Extra environment for a spawned ``llama-server``. 

201 

202 The bundled wheel ships its own ggml/llama/mtmd next to the binary with a baked 

203 rpath (``@loader_path`` on macOS, ``$ORIGIN`` on Linux), but a CUDA build also 

204 links the CUDA 12 runtime, which driver-only GPU images omit. On Linux this 

205 adds any installed CUDA-runtime wheel libs to ``LD_LIBRARY_PATH``; elsewhere it 

206 is empty. 

207 """ 

208 from lilbee.providers.fleet.cuda_runtime import cuda_runtime_env 

209 

210 return cuda_runtime_env() 

211 

212 

213@functools.lru_cache(maxsize=8) 

214def engine_help(binary: Path) -> str: 

215 """The usage text *binary* prints for ``--help``, stdout then stderr, read once per path. 

216 

217 Empty when the binary cannot run or answer in time: every caller reads an 

218 absent flag as unsupported, so a launch never carries a flag it did not see. 

219 """ 

220 from lilbee.providers.fleet.gpu_env import engine_environ 

221 

222 try: 

223 done = subprocess.run( # noqa: S603 - argv is the resolved engine binary and a literal 

224 [str(binary), "--help"], 

225 env=engine_environ(), 

226 capture_output=True, 

227 encoding="utf-8", 

228 errors="replace", 

229 timeout=_HELP_PROBE_TIMEOUT_S, 

230 check=False, 

231 ) 

232 except (OSError, subprocess.SubprocessError): 

233 return "" 

234 return f"{done.stdout}\n{done.stderr}" 

235 

236 

237def help_lists_option(help_text: str, flag: str) -> bool: 

238 """Whether an option line of *help_text* names *flag*. 

239 

240 An option line starts with ``-`` and names its flags up to the first word 

241 that is not one, so a flag quoted in another option's description does not count. 

242 """ 

243 for line in help_text.splitlines(): 

244 if not line.startswith("-"): 

245 continue 

246 for token in line.split(): 

247 if not token.startswith("-"): 

248 break 

249 if token.rstrip(",") == flag: 

250 return True 

251 return False