diff --git a/routes/cookbook_helpers.py b/routes/cookbook_helpers.py index c746a52..b2401d5 100644 --- a/routes/cookbook_helpers.py +++ b/routes/cookbook_helpers.py @@ -225,6 +225,15 @@ def _append_serve_preflight_exit_lines(runner_lines: list[str], *, keep_shell_op runner_lines.append('fi') +def _append_serve_exit_code_lines(runner_lines: list[str], *, keep_shell_open: bool) -> None: + """Append serve-runner lines that preserve and report the command exit code.""" + runner_lines.append('ODYSSEUS_CMD_EXIT=$?') + if keep_shell_open: + runner_lines.append('echo ""; echo "=== Process exited with code $ODYSSEUS_CMD_EXIT ==="; exec "${SHELL:-/bin/bash}"') + else: + runner_lines.append('echo ""; echo "=== Process exited with code $ODYSSEUS_CMD_EXIT ==="') + + 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 37b4617..3c6bf5b 100644 --- a/routes/cookbook_routes.py +++ b/routes/cookbook_routes.py @@ -37,6 +37,7 @@ from routes.cookbook_helpers import ( _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, _append_serve_preflight_exit_lines, + _append_serve_exit_code_lines, ModelDownloadRequest, ServeRequest, ) @@ -1086,10 +1087,10 @@ def setup_cookbook_routes() -> APIRouter: if local_windows: # Detached background process — no interactive shell to keep open. # Print the exit marker the status poller looks for, then stop. - runner_lines.append('echo ""; echo "=== Process exited with code $? ==="') + _append_serve_exit_code_lines(runner_lines, keep_shell_open=False) else: # Keep shell open after exit so user can see errors - runner_lines.append('echo ""; echo "=== Process exited with code $? ==="; exec "${SHELL:-/bin/bash}"') + _append_serve_exit_code_lines(runner_lines, keep_shell_open=True) runner_path = TMUX_LOG_DIR / f"{session_id}_run.sh" runner_path.write_text("\n".join(runner_lines) + "\n", encoding="utf-8") diff --git a/tests/test_cookbook_helpers.py b/tests/test_cookbook_helpers.py index bdf6c2b..566b99f 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_exit_code_lines, _append_serve_preflight_exit_lines, _local_tooling_path_export, _safe_env_prefix, @@ -80,3 +81,14 @@ def test_serve_preflight_failure_keeps_tmux_pane_visible(): assert 'echo "=== Process exited with code $ODYSSEUS_PREFLIGHT_EXIT ==="' in script assert 'exec "${SHELL:-/bin/bash}"' in script assert "exit 127" not in script + + +def test_serve_runner_preserves_command_exit_code(): + """The serve wrapper must capture `$?` before any echo resets it.""" + runner_lines = ["vllm serve Qwen/Qwen3.6-35B-A3B-NVFP4 --host 0.0.0.0 --port 8000"] + _append_serve_exit_code_lines(runner_lines, keep_shell_open=True) + script = "\n".join(runner_lines) + + assert "ODYSSEUS_CMD_EXIT=$?" in script + assert 'echo "=== Process exited with code $ODYSSEUS_CMD_EXIT ==="' in script + assert 'echo "=== Process exited with code $? ==="' not in script