fix: report serve dependency readiness (#412)
This commit is contained in:
@@ -92,6 +92,115 @@ def _docker_row_status(*, on_remote, in_container, installed, default_hint):
|
||||
return DockerRowStatus(applicable=True, install_hint=default_hint)
|
||||
|
||||
|
||||
def _package_installed_from_probe(name: str, probe: dict) -> bool:
|
||||
"""Return whether an optional dependency is usable by Cookbook.
|
||||
|
||||
A Python import alone is not enough: namespace packages can be created by a
|
||||
same-named directory, and vLLM serving needs the CLI on PATH. Keep this
|
||||
aligned with the actual serve command each backend launches.
|
||||
"""
|
||||
binaries = probe.get("binaries") if isinstance(probe.get("binaries"), dict) else {}
|
||||
dists = probe.get("dists") if isinstance(probe.get("dists"), dict) else {}
|
||||
modules = probe.get("modules") if isinstance(probe.get("modules"), dict) else {}
|
||||
|
||||
if name == "vllm":
|
||||
return bool(binaries.get("vllm"))
|
||||
if name == "llama_cpp":
|
||||
return bool(binaries.get("llama-server") or dists.get("llama-cpp-python"))
|
||||
if name == "sglang":
|
||||
return bool(dists.get("sglang") or modules.get("sglang", {}).get("real_module"))
|
||||
if name == "diffusers":
|
||||
return bool(
|
||||
(dists.get("diffusers") or modules.get("diffusers", {}).get("real_module"))
|
||||
and (dists.get("torch") or modules.get("torch", {}).get("real_module"))
|
||||
)
|
||||
if name == "hf_transfer":
|
||||
return bool(dists.get("hf-transfer") or modules.get("hf_transfer", {}).get("real_module"))
|
||||
return bool(dists.get(name) or modules.get(name, {}).get("real_module"))
|
||||
|
||||
|
||||
def _package_status_note(name: str, probe: dict) -> str:
|
||||
binaries = probe.get("binaries") if isinstance(probe.get("binaries"), dict) else {}
|
||||
modules = probe.get("modules") if isinstance(probe.get("modules"), dict) else {}
|
||||
dists = probe.get("dists") if isinstance(probe.get("dists"), dict) else {}
|
||||
module = modules.get(name) if isinstance(modules.get(name), dict) else {}
|
||||
locations = module.get("locations") or []
|
||||
if name == "vllm":
|
||||
if binaries.get("vllm"):
|
||||
return f"vLLM CLI: {binaries['vllm']}"
|
||||
if module.get("found") and not dists.get("vllm"):
|
||||
loc = locations[0] if locations else module.get("origin") or "unknown path"
|
||||
return f"Python sees a vllm namespace at {loc}, but no vLLM CLI is on PATH."
|
||||
return "vLLM CLI not found on PATH."
|
||||
if name == "llama_cpp":
|
||||
parts = []
|
||||
if binaries.get("llama-server"):
|
||||
parts.append(f"native llama-server: {binaries['llama-server']}")
|
||||
if dists.get("llama-cpp-python"):
|
||||
parts.append(f"python package: llama-cpp-python {dists['llama-cpp-python']}")
|
||||
return "; ".join(parts) if parts else "No native llama-server or llama-cpp-python server package found."
|
||||
if name == "diffusers":
|
||||
if _package_installed_from_probe(name, probe):
|
||||
return f"diffusers {dists.get('diffusers', 'available')} with torch {dists.get('torch', 'available')}"
|
||||
return "Diffusers serving needs both diffusers and torch."
|
||||
if name in dists:
|
||||
return f"{name} {dists[name]}"
|
||||
return ""
|
||||
|
||||
|
||||
def _package_probe_script(names: list[str]) -> str:
|
||||
names_lit = ",".join(repr(n) for n in names)
|
||||
return f"""
|
||||
import importlib.util
|
||||
import importlib.metadata as md
|
||||
import json
|
||||
import shutil
|
||||
|
||||
names=[{names_lit}]
|
||||
dist_names={{
|
||||
'vllm':['vllm'],
|
||||
'llama_cpp':['llama-cpp-python'],
|
||||
'sglang':['sglang'],
|
||||
'diffusers':['diffusers','torch'],
|
||||
'hf_transfer':['hf-transfer','hf_transfer'],
|
||||
}}
|
||||
bin_names={{
|
||||
'vllm':['vllm'],
|
||||
'llama_cpp':['llama-server'],
|
||||
}}
|
||||
|
||||
def mod_status(n):
|
||||
spec = importlib.util.find_spec(n)
|
||||
loader = getattr(spec, 'loader', None) if spec else None
|
||||
return {{
|
||||
'found': bool(spec),
|
||||
'origin': getattr(spec, 'origin', None) if spec else None,
|
||||
'loader': type(loader).__name__ if loader else None,
|
||||
'locations': list(getattr(spec, 'submodule_search_locations', []) or []),
|
||||
'real_module': bool(spec and loader),
|
||||
}}
|
||||
|
||||
def dist_status(ds):
|
||||
out = {{}}
|
||||
for d in ds:
|
||||
try:
|
||||
out[d] = md.version(d)
|
||||
except Exception:
|
||||
pass
|
||||
return out
|
||||
|
||||
def probe(n):
|
||||
mods = {{n: mod_status(n)}}
|
||||
if n == 'diffusers':
|
||||
mods['torch'] = mod_status('torch')
|
||||
dists = dist_status(dist_names.get(n, [n]))
|
||||
bins = {{b: shutil.which(b) for b in bin_names.get(n, [])}}
|
||||
return {{'modules': mods, 'dists': dists, 'binaries': bins}}
|
||||
|
||||
print(json.dumps({{n: probe(n) for n in names}}))
|
||||
"""
|
||||
|
||||
|
||||
def _find_line_break(buf):
|
||||
"""Find next line terminator in buffer. Returns (index, separator_length) or (-1, 0)."""
|
||||
ni = buf.find(b"\n")
|
||||
@@ -646,7 +755,7 @@ def setup_shell_routes() -> APIRouter:
|
||||
never reflected because the check only ever looked at the local host.
|
||||
"""
|
||||
_require_admin(request)
|
||||
import importlib, shlex, json as _json
|
||||
import importlib, importlib.metadata as importlib_metadata, shlex, json as _json
|
||||
port_arg = ""
|
||||
if ssh_port and str(ssh_port).strip() not in ("", "22"):
|
||||
_port = str(ssh_port).strip()
|
||||
@@ -672,18 +781,12 @@ def setup_shell_routes() -> APIRouter:
|
||||
# Remote check: for remote-target packages, probe the selected server's
|
||||
# venv over SSH so a remote `pip install` actually reflects here.
|
||||
remote_status: dict = {}
|
||||
remote_details: dict = {}
|
||||
remote_names = [p["name"] for p in packages if p.get("target") == "remote" and p.get("kind") != "system"]
|
||||
remote_system_names = [p["name"] for p in packages if p.get("target") == "remote" and p.get("kind") == "system"]
|
||||
if host and remote_names:
|
||||
try:
|
||||
names_lit = ",".join(repr(n) for n in remote_names)
|
||||
py = (
|
||||
"import importlib.util,json,shutil;"
|
||||
f"names=[{names_lit}];"
|
||||
"status={n:(importlib.util.find_spec(n) is not None) for n in names};"
|
||||
"status['llama_cpp']=status.get('llama_cpp',False) or shutil.which('llama-server') is not None;"
|
||||
"print(json.dumps(status))"
|
||||
)
|
||||
py = _package_probe_script(remote_names)
|
||||
src = ""
|
||||
if venv:
|
||||
act = venv if venv.endswith("/bin/activate") else venv.rstrip("/") + "/bin/activate"
|
||||
@@ -705,7 +808,12 @@ def setup_shell_routes() -> APIRouter:
|
||||
for line in reversed(txt.splitlines()):
|
||||
line = line.strip()
|
||||
if line.startswith("{"):
|
||||
remote_status = _json.loads(line)
|
||||
remote_details = _json.loads(line)
|
||||
remote_status = {
|
||||
name: _package_installed_from_probe(name, probe)
|
||||
for name, probe in remote_details.items()
|
||||
if isinstance(probe, dict)
|
||||
}
|
||||
break
|
||||
except Exception:
|
||||
remote_status = {}
|
||||
@@ -736,16 +844,29 @@ def setup_shell_routes() -> APIRouter:
|
||||
on_remote = bool(host and pkg.get("target") == "remote")
|
||||
if on_remote:
|
||||
pkg["installed"] = bool(remote_status.get(pkg["name"], False))
|
||||
probe = remote_details.get(pkg["name"])
|
||||
if isinstance(probe, dict):
|
||||
pkg["details"] = probe
|
||||
note = _package_status_note(pkg["name"], probe)
|
||||
if note:
|
||||
pkg["status_note"] = note
|
||||
elif pkg.get("kind") == "system":
|
||||
pkg["installed"] = shutil.which(pkg["name"]) is not None
|
||||
elif pkg["name"] == "llama_cpp" and shutil.which("llama-server"):
|
||||
pkg["installed"] = True
|
||||
pkg["status_note"] = f"native llama-server: {shutil.which('llama-server')}"
|
||||
else:
|
||||
try:
|
||||
importlib.import_module(pkg["name"])
|
||||
pkg["installed"] = True
|
||||
if pkg["name"] == "vllm":
|
||||
pkg["installed"] = shutil.which("vllm") is not None
|
||||
else:
|
||||
importlib_metadata.version(pkg["name"].replace("_", "-"))
|
||||
pkg["installed"] = True
|
||||
except ImportError:
|
||||
pkg["installed"] = False
|
||||
except importlib_metadata.PackageNotFoundError:
|
||||
pkg["installed"] = False
|
||||
|
||||
if pkg["name"] == "docker":
|
||||
status = _docker_row_status(
|
||||
|
||||
@@ -554,10 +554,12 @@ async function _fetchDependencies() {
|
||||
const isLocal = pkg.target === 'local';
|
||||
const isSystemDep = pkg.kind === 'system';
|
||||
const winBlocked = !isLocal && _isWindows() && _winUnsupported.has(pkg.name);
|
||||
const note = pkg.status_note ? `<div class="memory-item-meta" style="font-size:10px;opacity:0.65;margin-top:3px;">${esc(pkg.status_note)}</div>` : '';
|
||||
return `<div class="cookbook-dep-row${winBlocked ? ' cookbook-dep-blocked' : ''}" data-pkg-name="${esc(pkg.name)}" data-dep-pip="${esc(pkg.pip || '')}" data-dep-target="${isLocal ? 'local' : 'remote'}" data-dep-kind="${esc(pkg.kind || 'python')}">`
|
||||
+ `<div class="cookbook-dep-info">`
|
||||
+ `<div class="memory-item-title">${esc(pkg.name)}</div>`
|
||||
+ `<div class="memory-item-meta" style="font-size:10px;opacity:0.5;margin-top:2px;">${esc(pkg.desc)}</div>`
|
||||
+ note
|
||||
+ `</div>`
|
||||
+ `<span class="cookbook-dep-tag cookbook-dep-cat">${esc(pkg.category)}</span>`
|
||||
+ _statusTag(pkg, isLocal, isSystemDep, winBlocked)
|
||||
|
||||
@@ -11,6 +11,8 @@ from routes.shell_routes import (
|
||||
_find_line_break,
|
||||
_running_in_container,
|
||||
_docker_row_status,
|
||||
_package_installed_from_probe,
|
||||
_package_status_note,
|
||||
DOCKER_IN_CONTAINER_HINT,
|
||||
)
|
||||
|
||||
@@ -182,3 +184,60 @@ class TestDockerRowStatus:
|
||||
assert "remote" in lowered
|
||||
assert "socket" in lowered
|
||||
assert "host-root" in lowered or "host root" in lowered
|
||||
|
||||
|
||||
class TestPackageProbeStatus:
|
||||
"""Dependency rows should reflect serve readiness, not import coincidences."""
|
||||
|
||||
def test_vllm_namespace_without_cli_is_not_installed(self):
|
||||
probe = {
|
||||
"modules": {
|
||||
"vllm": {
|
||||
"found": True,
|
||||
"origin": None,
|
||||
"loader": None,
|
||||
"locations": ["/root/vllm"],
|
||||
"real_module": False,
|
||||
}
|
||||
},
|
||||
"dists": {},
|
||||
"binaries": {"vllm": None},
|
||||
}
|
||||
|
||||
assert _package_installed_from_probe("vllm", probe) is False
|
||||
assert "namespace" in _package_status_note("vllm", probe)
|
||||
assert "no vLLM CLI" in _package_status_note("vllm", probe)
|
||||
|
||||
def test_vllm_requires_cli_for_current_serve_command(self):
|
||||
probe = {
|
||||
"modules": {"vllm": {"found": True, "real_module": True}},
|
||||
"dists": {"vllm": "0.8.5"},
|
||||
"binaries": {"vllm": "/home/user/venv/bin/vllm"},
|
||||
}
|
||||
|
||||
assert _package_installed_from_probe("vllm", probe) is True
|
||||
|
||||
def test_llama_cpp_is_installed_when_native_llama_server_exists(self):
|
||||
probe = {
|
||||
"modules": {"llama_cpp": {"found": False, "real_module": False}},
|
||||
"dists": {},
|
||||
"binaries": {"llama-server": "/usr/local/bin/llama-server"},
|
||||
}
|
||||
|
||||
assert _package_installed_from_probe("llama_cpp", probe) is True
|
||||
assert "native llama-server" in _package_status_note("llama_cpp", probe)
|
||||
|
||||
def test_diffusers_requires_torch_too(self):
|
||||
missing_torch = {
|
||||
"modules": {"diffusers": {"found": True, "real_module": True}, "torch": {"found": False}},
|
||||
"dists": {"diffusers": "0.37.0"},
|
||||
"binaries": {},
|
||||
}
|
||||
ready = {
|
||||
"modules": {"diffusers": {"found": True, "real_module": True}, "torch": {"found": True, "real_module": True}},
|
||||
"dists": {"diffusers": "0.37.0", "torch": "2.10.0"},
|
||||
"binaries": {},
|
||||
}
|
||||
|
||||
assert _package_installed_from_probe("diffusers", missing_torch) is False
|
||||
assert _package_installed_from_probe("diffusers", ready) is True
|
||||
|
||||
Reference in New Issue
Block a user