Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions routes/cookbook_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,33 @@ def _user_shell_path_bootstrap() -> list[str]:
]


def _parse_model_dirs(model_dir: str | None) -> list[str]:
"""Split the comma-separated ``model_dir`` query param for /api/model/cached
into individual, validated directory paths.

Bare paths users paste without a leading slash (``home/...``, ``mnt/...``,
etc.) are prefixed with ``/`` first, matching the convention the generated
scanner's own ``normalize_model_dir`` uses. Each resulting segment is then
run through :func:`_validate_local_dir` — the same validator the sibling
``/api/model/download`` endpoint applies to ``local_dir`` — so an invalid
path (shell metacharacters, a segment starting with ``-``) is rejected
with a 400 instead of silently reaching the generated scan script.
"""
if not model_dir:
return []
result = []
for segment in model_dir.split(","):
segment = segment.strip()
if not segment:
continue
if segment.startswith(("home/", "mnt/", "media/", "data/", "opt/", "srv/", "var/")):
segment = "/" + segment
validated = _validate_local_dir(segment)
if validated:
result.append(validated)
return result


def _cached_model_scan_script(model_dirs: list[str] | None = None, add_hf_cache: str | None = None) -> str:
"""Build the standalone Python scanner used by /api/model/cached.
Allows for an additional HuggingFace cache path to be scanned (i.e. Windows HF cache for local WSL envs.)
Expand Down
10 changes: 2 additions & 8 deletions routes/cookbook_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
_ps_squote, _bash_squote, _validate_serve_cmd, _parse_serve_phase, OLLAMA_MISSING_HINT,
_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,
_parse_model_dirs,
load_stored_hf_token,
_append_vllm_linux_preflight_lines, _ollama_bind_from_cmd, _pip_install_fallback_chain,
_pip_install_no_cache, _user_shell_path_bootstrap, _venv_safe_local_pip_install_cmd,
Expand Down Expand Up @@ -1353,14 +1354,7 @@ async def model_cached(request: Request, host: str | None = None, model_dir: str
ssh_port = validate_ssh_port(ssh_port)
TMUX_LOG_DIR.mkdir(parents=True, exist_ok=True)

model_dirs = []
if model_dir:
for d in model_dir.split(','):
d = d.strip()
if d:
if d.startswith(("home/", "mnt/", "media/", "data/", "opt/", "srv/", "var/")):
d = "/" + d
model_dirs.append(d)
model_dirs = _parse_model_dirs(model_dir)
paths_code = _cached_model_scan_script(model_dirs)

scan_py = TMUX_LOG_DIR / "scan_cache.py"
Expand Down
30 changes: 30 additions & 0 deletions tests/test_cookbook_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
_llama_cpp_rebuild_cmd,
_append_vllm_linux_preflight_lines,
_local_tooling_path_export,
_parse_model_dirs,
_pip_install_attempt,
_pip_install_fallback_chain,
_ollama_bind_from_cmd,
Expand Down Expand Up @@ -190,6 +191,35 @@ def test_validate_local_dir_rejects_leading_dash_segments():
_validate_local_dir(path)


def test_parse_model_dirs_returns_empty_for_none_or_blank():
assert _parse_model_dirs(None) == []
assert _parse_model_dirs("") == []
assert _parse_model_dirs(" , ,") == []


def test_parse_model_dirs_splits_and_validates_each_segment():
assert _parse_model_dirs("/models/a, /models/b") == ["/models/a", "/models/b"]


def test_parse_model_dirs_prefixes_bare_unix_style_paths():
# Users often paste Linux absolute paths without the leading slash.
assert _parse_model_dirs("home/user/models") == ["/home/user/models"]
assert _parse_model_dirs("mnt/data/models,srv/models") == ["/mnt/data/models", "/srv/models"]


def test_parse_model_dirs_rejects_shell_metacharacters_like_download_local_dir():
# Same validator the sibling /api/model/download endpoint applies to
# local_dir — a segment with shell metacharacters must 400, not reach
# the generated scan script.
with pytest.raises(HTTPException):
_parse_model_dirs("/models/a; touch /tmp/pwned")


def test_parse_model_dirs_rejects_leading_dash_segments():
with pytest.raises(HTTPException):
_parse_model_dirs("/models/-rf")


def test_validate_gpus_accepts_indexes_only():
assert _validate_gpus("0,1,2") == "0,1,2"
with pytest.raises(HTTPException):
Expand Down
Loading