From 15822e91ff2451a8bac71db3d65df4ccf0e8611c Mon Sep 17 00:00:00 2001 From: spooky Date: Mon, 1 Jun 2026 23:40:06 +1000 Subject: [PATCH] fix: keep serve preflight errors visible (#398) --- routes/cookbook_helpers.py | 11 +++++++++++ routes/cookbook_routes.py | 17 +++++++++++------ tests/test_cookbook_helpers.py | 22 ++++++++++++++++++++++ 3 files changed, 44 insertions(+), 6 deletions(-) diff --git a/routes/cookbook_helpers.py b/routes/cookbook_helpers.py index a8412d5..c746a52 100644 --- a/routes/cookbook_helpers.py +++ b/routes/cookbook_helpers.py @@ -214,6 +214,17 @@ def _validate_serve_cmd(v: str | None) -> str | None: return v +def _append_serve_preflight_exit_lines(runner_lines: list[str], *, keep_shell_open: bool) -> None: + """Append serve-runner lines that surface preflight failures before exit.""" + runner_lines.append('if [ -n "$ODYSSEUS_PREFLIGHT_EXIT" ]; then') + runner_lines.append(' echo ""; echo "=== Process exited with code $ODYSSEUS_PREFLIGHT_EXIT ==="') + if keep_shell_open: + runner_lines.append(' exec "${SHELL:-/bin/bash}"') + else: + runner_lines.append(' exit "$ODYSSEUS_PREFLIGHT_EXIT"') + runner_lines.append('fi') + + class ModelDownloadRequest(BaseModel): repo_id: str include: str | None = None # glob pattern e.g. "*Q4_K_M*" diff --git a/routes/cookbook_routes.py b/routes/cookbook_routes.py index 909cc6d..37b4617 100644 --- a/routes/cookbook_routes.py +++ b/routes/cookbook_routes.py @@ -36,7 +36,7 @@ from routes.cookbook_helpers import ( _validate_repo_id, _validate_include, _validate_remote_host, _validate_token, _validate_local_dir, _validate_ssh_port, _validate_gpus, _shell_path, _ps_squote, _bash_squote, _validate_serve_cmd, _parse_serve_phase, - _safe_env_prefix, _local_tooling_path_export, + _safe_env_prefix, _local_tooling_path_export, _append_serve_preflight_exit_lines, ModelDownloadRequest, ServeRequest, ) @@ -950,6 +950,7 @@ def setup_cookbook_routes() -> APIRouter: # ── Linux/Termux: bash + tmux (existing flow) ── runner_lines = ["#!/bin/bash"] runner_lines.extend(_user_shell_path_bootstrap()) + runner_lines.append('ODYSSEUS_PREFLIGHT_EXIT=""') # Put Odysseus's own venv bin on PATH (local runs only) so the serve # shell resolves the bundled python3/hf, mirroring the download flow. if not remote: @@ -1044,7 +1045,7 @@ def setup_cookbook_routes() -> APIRouter: # command (the natural serving engine on Apple Silicon / Metal). runner_lines.append('if ! command -v ollama &>/dev/null; then') runner_lines.append(' echo "ERROR: Ollama not found. Install it (macOS: brew install ollama, or https://ollama.com/download), then launch again."') - runner_lines.append(' exit 127') + runner_lines.append(' ODYSSEUS_PREFLIGHT_EXIT=127') runner_lines.append('fi') runner_lines.append('if ! curl -sf http://localhost:11434/api/tags >/dev/null 2>&1; then') runner_lines.append(' echo "Starting ollama server..."; (ollama serve >/dev/null 2>&1 &)') @@ -1054,7 +1055,7 @@ def setup_cookbook_routes() -> APIRouter: # vLLM is CUDA/ROCm-only and does not run on macOS at all. runner_lines.append('if [ "$(uname -s)" = "Darwin" ]; then') runner_lines.append(' echo "ERROR: vLLM does not run on macOS. Use Ollama or llama.cpp (Metal) instead."') - runner_lines.append(' exit 1') + runner_lines.append(' ODYSSEUS_PREFLIGHT_EXIT=1') runner_lines.append('fi') # Put ~/.local/bin on PATH first — without a venv, vllm installs # there via --user and the non-login serve shell otherwise can't @@ -1062,21 +1063,25 @@ def setup_cookbook_routes() -> APIRouter: runner_lines.append('export PATH="$HOME/.local/bin:$PATH"') runner_lines.append('if ! command -v vllm &>/dev/null; then') runner_lines.append(' echo "ERROR: vLLM is not installed. Open Cookbook -> Dependencies and install vllm on this server, then launch again."') - runner_lines.append(' exit 127') + runner_lines.append(' ODYSSEUS_PREFLIGHT_EXIT=127') runner_lines.append('fi') elif "sglang.launch_server" in req.cmd: runner_lines.append('export PATH="$HOME/.local/bin:$PATH"') runner_lines.append('if ! python3 -c "import sglang" 2>/dev/null; then') runner_lines.append(' echo "ERROR: SGLang is not installed. Open Cookbook -> Dependencies and install sglang on this server, then launch again."') - runner_lines.append(' exit 127') + runner_lines.append(' ODYSSEUS_PREFLIGHT_EXIT=127') runner_lines.append('fi') elif "scripts/diffusion_server.py" in req.cmd or ".diffusion_server.py" in req.cmd: runner_lines.append('export PATH="$HOME/.local/bin:$PATH"') runner_lines.append('if ! python3 -c "import torch, diffusers" 2>/dev/null; then') runner_lines.append(' echo "ERROR: Diffusion serving requires PyTorch + diffusers. Open Cookbook -> Dependencies and install diffusers on this server, then launch again."') - runner_lines.append(' exit 127') + runner_lines.append(' ODYSSEUS_PREFLIGHT_EXIT=127') runner_lines.append('fi') + _append_serve_preflight_exit_lines( + runner_lines, + keep_shell_open=not local_windows, + ) runner_lines.append(req.cmd) if local_windows: # Detached background process — no interactive shell to keep open. diff --git a/tests/test_cookbook_helpers.py b/tests/test_cookbook_helpers.py index 9f15e59..bdf6c2b 100644 --- a/tests/test_cookbook_helpers.py +++ b/tests/test_cookbook_helpers.py @@ -2,6 +2,7 @@ import pytest from fastapi import HTTPException from routes.cookbook_helpers import ( + _append_serve_preflight_exit_lines, _local_tooling_path_export, _safe_env_prefix, _validate_gpus, @@ -58,3 +59,24 @@ def test_local_tooling_path_export_preserves_spaces_and_expands_path(): line = _local_tooling_path_export("/Users/John Smith/.venv/bin/python3") assert line == 'export PATH="/Users/John Smith/.venv/bin:$PATH"' assert line.endswith(':$PATH"') # $PATH stays expandable in double quotes + + +def test_serve_preflight_failure_keeps_tmux_pane_visible(): + """Dependency preflight failures should remain visible in tmux output. + + A bare `exit 127` kills the tmux pane before the browser/status poller can + capture the helpful error, leaving users with a blank "crashed" card. + """ + runner_lines = [ + 'ODYSSEUS_PREFLIGHT_EXIT=""', + 'echo "ERROR: vLLM is not installed. Open Cookbook -> Dependencies and install vllm on this server, then launch again."', + 'ODYSSEUS_PREFLIGHT_EXIT=127', + ] + _append_serve_preflight_exit_lines(runner_lines, keep_shell_open=True) + script = "\n".join(runner_lines) + + assert "ERROR: vLLM is not installed" in script + assert 'ODYSSEUS_PREFLIGHT_EXIT=127' in script + assert 'echo "=== Process exited with code $ODYSSEUS_PREFLIGHT_EXIT ==="' in script + assert 'exec "${SHELL:-/bin/bash}"' in script + assert "exit 127" not in script