diff --git a/routes/cookbook_helpers.py b/routes/cookbook_helpers.py index 4589c5a..30f99e7 100644 --- a/routes/cookbook_helpers.py +++ b/routes/cookbook_helpers.py @@ -202,6 +202,34 @@ def _pip_install_fallback_chain(package: str, *, python_cmd: str = "python3 -m p return f"{base} || {{ ! {venv_check} && {user}; }}" +def _venv_safe_local_pip_install_cmd(cmd: str, *, local: bool, in_venv: bool) -> str: + """Drop pip user-install flags that are invalid for local venv installs. + + Cookbook dependency installs run through the model-serve task path so users + can watch progress in the same log UI. For local POSIX runs, that task + prepends Odysseus' own interpreter directory to PATH. If Odysseus itself is + running from a venv, `python3` resolves to the venv Python and pip rejects + `--user` with "User site-packages are not visible in this virtualenv". + + Keep remote and non-venv installs unchanged: remotes may intentionally use + system Python, and Docker/non-venv installs still need user-site fallback. + """ + if not local or not in_venv: + return cmd + if "pip install" not in (cmd or ""): + return cmd + try: + parts = shlex.split(cmd) + except ValueError: + return cmd + stripped = [ + part + for part in parts + if part not in {"--user", "--break-system-packages"} + ] + return shlex.join(stripped) + + def _cached_model_scan_script(model_dirs: list[str] | None = None) -> str: """Build the standalone Python scanner used by /api/model/cached.""" lines = [ diff --git a/routes/cookbook_routes.py b/routes/cookbook_routes.py index abc5927..63f4b46 100644 --- a/routes/cookbook_routes.py +++ b/routes/cookbook_routes.py @@ -38,7 +38,8 @@ from routes.cookbook_helpers import ( _ps_squote, _bash_squote, _validate_serve_cmd, _parse_serve_phase, _safe_env_prefix, _local_tooling_path_export, _append_serve_preflight_exit_lines, _append_serve_exit_code_lines, _append_llama_cpp_linux_accel_build_lines, _cached_model_scan_script, - _ollama_bind_from_cmd, _pip_install_fallback_chain, ModelDownloadRequest, ServeRequest, + _ollama_bind_from_cmd, _pip_install_fallback_chain, _venv_safe_local_pip_install_cmd, + ModelDownloadRequest, ServeRequest, ) _HF_TOKEN_STATUS_SNIPPET = ( @@ -819,6 +820,11 @@ def setup_cookbook_routes() -> APIRouter: # many downstream `"engine" in req.cmd` membership checks can't hit # `TypeError: argument of type 'NoneType'` (a 500 instead of a clean 400). req.cmd = _validate_serve_cmd(req.cmd) or "" + req.cmd = _venv_safe_local_pip_install_cmd( + req.cmd, + local=not bool(req.remote_host), + in_venv=sys.prefix != sys.base_prefix, + ) is_pip_install = bool(req.cmd and "pip install" in req.cmd) if is_pip_install: # PEP-508-style package spec — letters, digits, `.-_` for the diff --git a/tests/test_cookbook_helpers.py b/tests/test_cookbook_helpers.py index d19c36e..b52fb00 100644 --- a/tests/test_cookbook_helpers.py +++ b/tests/test_cookbook_helpers.py @@ -15,6 +15,7 @@ from routes.cookbook_helpers import ( _pip_install_fallback_chain, _ollama_bind_from_cmd, _safe_env_prefix, + _venv_safe_local_pip_install_cmd, _validate_gpus, _validate_repo_id, _validate_serve_cmd, @@ -157,6 +158,16 @@ def test_pip_install_fallback_chain_tries_user_outside_venv(): assert "user_attempt" in result.stdout, "Chain should try --user when not in venv and base fails" +def test_venv_safe_local_pip_install_strips_user_flags_only_for_local_venv(): + cmd = 'python3 -m pip install -U --user --break-system-packages "vllm"' + + cleaned = _venv_safe_local_pip_install_cmd(cmd, local=True, in_venv=True) + + assert cleaned == "python3 -m pip install -U vllm" + assert _venv_safe_local_pip_install_cmd(cmd, local=False, in_venv=True) == cmd + assert _venv_safe_local_pip_install_cmd(cmd, local=True, in_venv=False) == cmd + + def test_pip_install_attempt_wraps_in_status_preserving_subshell(): """Each pip attempt must be a bash -c subshell that captures output, prints tail, cleans up, and exits with pip's real status — not tail's."""