diff --git a/routes/shell_routes.py b/routes/shell_routes.py index cd9a5d9..8565319 100644 --- a/routes/shell_routes.py +++ b/routes/shell_routes.py @@ -118,6 +118,7 @@ def _running_in_container(dockerenv_path="/.dockerenv", cgroup_path="/proc/1/cgr DockerRowStatus = namedtuple("DockerRowStatus", ["applicable", "install_hint"]) +PackageUpdateStatus = namedtuple("PackageUpdateStatus", ["available", "note"]) def _docker_row_status(*, on_remote, in_container, installed, default_hint): @@ -180,7 +181,10 @@ def _package_status_note(name: str, probe: dict) -> str: locations = module.get("locations") or [] if name == "vllm": if binaries.get("vllm"): - return f"vLLM CLI: {binaries['vllm']}" + parts = [f"vLLM CLI: {binaries['vllm']}"] + if dists.get("vllm"): + parts.append(f"python package: vllm {dists['vllm']}") + return "; ".join(parts) 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." @@ -201,6 +205,35 @@ def _package_status_note(name: str, probe: dict) -> str: return "" +def _package_pip_update_status(pkg: dict, probe: dict | None = None) -> PackageUpdateStatus: + """Return whether the Dependencies UI should offer a generic pip update. + + "Installed" means Cookbook can use the dependency. It does not always mean + the dependency is a Python package that Cookbook should update with pip: + native llama-server can come from a package manager/source build, and a CLI + may be on PATH without matching Python package metadata. + """ + if pkg.get("kind") == "system" or not pkg.get("pip"): + return PackageUpdateStatus(False, "Update this system dependency outside Odysseus.") + + name = pkg.get("name") + binaries = probe.get("binaries") if isinstance(probe, dict) and isinstance(probe.get("binaries"), dict) else {} + dists = probe.get("dists") if isinstance(probe, dict) and isinstance(probe.get("dists"), dict) else {} + + if name == "llama_cpp" and binaries.get("llama-server"): + return PackageUpdateStatus( + False, + "Using native llama-server on PATH; update it with its package manager or source checkout.", + ) + if name == "vllm" and binaries.get("vllm") and not dists.get("vllm"): + return PackageUpdateStatus( + False, + "Using a vLLM CLI on PATH without Python package metadata; update it outside Odysseus.", + ) + + return PackageUpdateStatus(True, "Update uses pip in the selected Python environment.") + + def _prepend_user_install_bins_to_path() -> None: """Make pip --user console scripts visible to dependency probes. @@ -944,6 +977,7 @@ def setup_shell_routes() -> APIRouter: for pkg in packages: on_remote = bool(host and pkg.get("target") == "remote") + probe = None if on_remote: pkg["installed"] = bool(remote_status.get(pkg["name"], False)) probe = remote_details.get(pkg["name"]) @@ -957,19 +991,36 @@ def setup_shell_routes() -> APIRouter: elif pkg["name"] == "llama_cpp" and shutil.which("llama-server"): pkg["installed"] = True pkg["status_note"] = f"native llama-server: {shutil.which('llama-server')}" + probe = {"binaries": {"llama-server": shutil.which("llama-server")}, "dists": {}} + elif pkg["name"] == "vllm": + _vllm_cli = shutil.which("vllm") + pkg["installed"] = _vllm_cli is not None + if pkg["installed"]: + try: + _vllm_version = importlib_metadata.version(_pip_dist_name(pkg)) + except importlib_metadata.PackageNotFoundError: + _vllm_version = None + probe = { + "binaries": {"vllm": _vllm_cli}, + "dists": {"vllm": _vllm_version} if _vllm_version else {}, + } + pkg["status_note"] = _package_status_note("vllm", probe) else: try: importlib.import_module(pkg["name"]) - if pkg["name"] == "vllm": - pkg["installed"] = shutil.which("vllm") is not None - else: - importlib_metadata.version(_pip_dist_name(pkg)) - pkg["installed"] = True + importlib_metadata.version(_pip_dist_name(pkg)) + pkg["installed"] = True except ImportError: pkg["installed"] = False except importlib_metadata.PackageNotFoundError: pkg["installed"] = False + if pkg.get("installed"): + update_status = _package_pip_update_status(pkg, probe) + pkg["pip_update_available"] = update_status.available + if update_status.note: + pkg["update_note"] = update_status.note + if pkg["name"] == "docker": status = _docker_row_status( on_remote=on_remote, diff --git a/static/js/cookbook.js b/static/js/cookbook.js index 33e2e10..97f974b 100644 --- a/static/js/cookbook.js +++ b/static/js/cookbook.js @@ -618,6 +618,10 @@ async function _fetchDependencies() { const _statusTag = (pkg, isLocal, isSystemDep, winBlocked) => { if (winBlocked) return `N/A`; if (pkg.installed && isSystemDep) return `Installed`; + if (pkg.installed && pkg.pip_update_available === false) { + const tip = esc(pkg.update_note || pkg.status_note || 'Found externally; update outside Odysseus.'); + return `Installed`; + } if (pkg.installed) return ``; if (isSystemDep) { const depTip = esc(pkg.install_hint || 'Install this OS package on the selected server.'); @@ -632,11 +636,13 @@ async function _fetchDependencies() { const isSystemDep = pkg.kind === 'system'; const winBlocked = !isLocal && _isWindows() && _winUnsupported.has(pkg.name); const note = pkg.status_note ? `
${esc(pkg.status_note)}
` : ''; + const updateNote = pkg.installed && pkg.pip_update_available === false && pkg.update_note ? `
${esc(pkg.update_note)}
` : ''; return `
` + `
` + `
${esc(pkg.name)}
` + `
${esc(pkg.desc)}
` + note + + updateNote + `
` + `${esc(pkg.category)}` + _statusTag(pkg, isLocal, isSystemDep, winBlocked) diff --git a/tests/test_shell_routes.py b/tests/test_shell_routes.py index c6ffb3c..afeb8c9 100644 --- a/tests/test_shell_routes.py +++ b/tests/test_shell_routes.py @@ -15,6 +15,7 @@ from routes.shell_routes import ( _running_in_container, _docker_row_status, _package_installed_from_probe, + _package_pip_update_status, _package_probe_script, _package_status_note, _prepend_user_install_bins_to_path, @@ -224,6 +225,21 @@ class TestPackageProbeStatus: } assert _package_installed_from_probe("vllm", probe) is True + assert "python package: vllm 0.8.5" in _package_status_note("vllm", probe) + assert _package_pip_update_status({"name": "vllm", "pip": "vllm"}, probe).available is True + + def test_vllm_cli_without_dist_is_external_for_update(self): + probe = { + "modules": {"vllm": {"found": False, "real_module": False}}, + "dists": {}, + "binaries": {"vllm": "/opt/vllm/bin/vllm"}, + } + + status = _package_pip_update_status({"name": "vllm", "pip": "vllm"}, probe) + + assert _package_installed_from_probe("vllm", probe) is True + assert status.available is False + assert "outside Odysseus" in status.note def test_llama_cpp_is_installed_when_native_llama_server_exists(self): probe = { @@ -234,6 +250,9 @@ class TestPackageProbeStatus: assert _package_installed_from_probe("llama_cpp", probe) is True assert "native llama-server" in _package_status_note("llama_cpp", probe) + status = _package_pip_update_status({"name": "llama_cpp", "pip": "llama-cpp-python[server]"}, probe) + assert status.available is False + assert "package manager or source checkout" in status.note def test_diffusers_requires_torch_too(self): missing_torch = {