Skip to content
Merged
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
4 changes: 2 additions & 2 deletions backend/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,12 +194,12 @@ def get_tool_filename(tool):

def find_tool_executable(tool):
cfg = load_config()
return llama_manager_service.get_backend_bin_dir(APP_CONTEXT, cfg.get("backend")) / get_tool_filename(tool)
return llama_manager_service.get_backend_bin_dir(APP_CONTEXT, cfg.get("backend"), cfg) / get_tool_filename(tool)


def get_runtime_files():
cfg = load_config()
search_dir = llama_manager_service.get_backend_bin_dir(APP_CONTEXT, cfg.get("backend"))
search_dir = llama_manager_service.get_backend_bin_dir(APP_CONTEXT, cfg.get("backend"), cfg)
if not search_dir.exists():
return []
runtime_files = []
Expand Down
20 changes: 16 additions & 4 deletions backend/routes/file_picker.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
"""Routes for native file selection."""

from pathlib import Path
import sys

from backend.http import sanitize_error
from backend.services import file_picker, model_dir
from backend.services import file_picker, llama_manager, model_dir


def select_file(request, response, ctx):
Expand Down Expand Up @@ -35,10 +36,20 @@ def select_file(request, response, ctx):

def select_folder(request, response, ctx):
body = request.body or {}
title = str(body.get("title") or "Select Models Folder").strip() or "Select Models Folder"
default_title = "Select llama.cpp Executable Folder" if body.get("purpose") == "custom-backend" else "Select Models Folder"
title = str(body.get("title") or default_title).strip() or default_title
try:
info = model_dir.get_models_dir_info(ctx)
initial_dir = Path(info["models_dir"]) if info["models_dir_available"] else ctx.paths.models.parent
if body.get("purpose") == "custom-backend":
backend = body.get("backend", "custom")
if not llama_manager.is_custom_backend(backend):
response.error("Unsupported custom backend", 400)
return
initial_dir = llama_manager.get_backend_bin_dir(ctx, backend)
if not initial_dir.is_dir():
initial_dir = ctx.paths.root
else:
info = model_dir.get_models_dir_info(ctx)
initial_dir = Path(info["models_dir"]) if info["models_dir_available"] else ctx.paths.models.parent
selected_path = file_picker.select_folder_in_native_dialog(
title=title,
initial_dir=initial_dir,
Expand All @@ -47,4 +58,5 @@ def select_folder(request, response, ctx):
except file_picker.NativePickerUnavailableError as exc:
response.error(str(exc), 503)
except Exception as exc:
print(f"[file_picker] folder selection failed: {exc}", file=sys.stderr)
response.error(sanitize_error(exc, 500), 500)
4 changes: 3 additions & 1 deletion backend/routes/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,9 +227,11 @@ def activate_custom(request, response, ctx):
response.error(*claim_error)
return
try:
result = llama_manager.activate_custom_backend(ctx, backend)
options = {"bin_dir": request.body["bin_dir"]} if "bin_dir" in (request.body or {}) else {}
result = llama_manager.activate_custom_backend(ctx, backend, **options)
response.json(result)
finally:
process_manager.release_install_slot(ctx)
except Exception as e:
print(f"[install] custom activation failed: {e}", file=sys.stderr)
response.error(sanitize_error(e, 500), 500)
14 changes: 10 additions & 4 deletions backend/routes/lifecycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,16 @@ def post_open_folder(request, response, ctx):
return
try:
if folder == "llama":
backend = ctx.services.load_config().get("backend")
target = (llama_manager.get_backend_bin_dir(ctx, backend).parent
if llama_manager.is_custom_backend(backend) else ctx.paths.llama)
target.mkdir(parents=True, exist_ok=True)
cfg = ctx.services.load_config()
backend = cfg.get("backend")
if llama_manager.is_custom_backend(backend) and backend in cfg.get("custom_backend_dirs", {}):
target = llama_manager.get_backend_bin_dir(ctx, backend, cfg)
if not target.is_dir():
raise ValueError(f"Custom backend folder is unavailable: {target}")
else:
target = (llama_manager.get_default_backend_bin_dir(ctx, backend).parent
if llama_manager.is_custom_backend(backend) else ctx.paths.llama)
target.mkdir(parents=True, exist_ok=True)
else:
target = model_dir.get_models_dir(ctx)
lifecycle_service.open_folder_in_file_manager(target)
Expand Down
2 changes: 1 addition & 1 deletion backend/routes/status.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ def get_status(request, response, ctx):
"available_backends": [
{
"id": key, "label": spec["label"],
**({"custom": True, "bin_dir": llama_manager.custom_backend_bin_label(key)}
**({"custom": True, **llama_manager.get_custom_backend_dir_info(ctx, key, cfg)}
if llama_manager.is_custom_backend(key) else {}),
}
for key, spec in backend_specs.items()
Expand Down
103 changes: 89 additions & 14 deletions backend/services/llama_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,25 +52,47 @@ def is_custom_backend(backend: Any) -> bool:
return isinstance(backend, str) and backend in CUSTOM_BACKEND_SPECS


def get_backend_bin_dir(ctx: AppContext, backend: Any) -> pathlib.Path:
def get_default_backend_bin_dir(ctx: AppContext, backend: Any) -> pathlib.Path:
if backend == "custom":
return ctx.paths.llama_custom_bin
if is_custom_backend(backend):
return ctx.paths.llama / CUSTOM_BACKEND_SPECS[backend]["folder"] / "bin"
return ctx.paths.llama_bin


def get_backend_bin_dir(
ctx: AppContext, backend: Any, cfg: Optional[Mapping[str, Any]] = None
) -> pathlib.Path:
if is_custom_backend(backend):
cfg = ctx.services.load_config() if cfg is None else cfg
folders = cfg.get("custom_backend_dirs", {})
if not isinstance(folders, Mapping):
raise ValueError("Configured custom backend folders are invalid.")
if backend in folders:
value = folders[backend]
if not isinstance(value, str) or not value.strip() or "\x00" in value:
raise ValueError("Configured custom backend folder is invalid.")
path = pathlib.Path(value)
if not path.is_absolute():
raise ValueError("Custom backend folder must be an absolute path.")
return path
return get_default_backend_bin_dir(ctx, backend)


def get_custom_backend_dir_info(ctx: AppContext, backend: str, cfg: Mapping[str, Any]) -> dict[str, Any]:
path = get_backend_bin_dir(ctx, backend, cfg)
default = get_default_backend_bin_dir(ctx, backend)
return {"bin_dir": str(path), "default_bin_dir": str(default), "bin_dir_is_default": path == default}


def get_backend_grammars_dir(ctx: AppContext, backend: Any) -> pathlib.Path:
if backend == "custom":
return ctx.paths.llama_custom_grammars
if is_custom_backend(backend):
return get_backend_bin_dir(ctx, backend).parent / "grammars"
return get_default_backend_bin_dir(ctx, backend).parent / "grammars"
return ctx.paths.llama_grammars


def custom_backend_bin_label(backend: str) -> str:
return f"llama/{CUSTOM_BACKEND_SPECS[backend]['folder']}/bin/"

# (gpu_target, family label) for every target upstream publishes.
LEMONADE_ROCM_TARGETS = [
("gfx103X", "AMD RDNA2"),
Expand Down Expand Up @@ -464,7 +486,7 @@ def validate_runtime_dependencies(
print(f"[llama_manager] load_config failed during runtime validation: {e}", file=sys.stderr)
cfg = {}

cache_key = (cfg.get("backend"), tool_names)
cache_key = (cfg.get("backend"), str(get_backend_bin_dir(ctx, cfg.get("backend"), cfg)), tool_names)
now = time.monotonic()
with ctx.state.runtime_health_lock:
cached = ctx.state.runtime_health_cache.get(cache_key)
Expand All @@ -489,7 +511,7 @@ def _validate_runtime_dependencies_uncached(
current_platform = ctx.services.current_platform or sys.platform
if current_platform == "unknown":
current_platform = sys.platform
runtime_dir = get_backend_bin_dir(ctx, cfg.get("backend"))
runtime_dir = get_backend_bin_dir(ctx, cfg.get("backend"), cfg)

for tool in tool_names:
exe_path = ctx.services.find_tool_executable(tool)
Expand Down Expand Up @@ -543,13 +565,54 @@ def _validate_runtime_dependencies_uncached(
}


def activate_custom_backend(ctx: AppContext, backend: str = "custom") -> dict[str, Any]:
_CUSTOM_DIR_UNSET = object()


def activate_custom_backend(
ctx: AppContext, backend: str = "custom", bin_dir: Any = _CUSTOM_DIR_UNSET
) -> dict[str, Any]:
if not is_custom_backend(backend):
return {"ok": False, "error": "Unsupported custom backend"}
try:
bin_dir = get_backend_bin_dir(ctx, backend)
bin_dir.mkdir(parents=True, exist_ok=True)
get_backend_grammars_dir(ctx, backend).mkdir(parents=True, exist_ok=True)
candidate_cfg = dict(ctx.services.load_config())
folders = candidate_cfg.get("custom_backend_dirs", {})
if not isinstance(folders, Mapping):
raise ValueError("Configured custom backend folders are invalid.")
folders = dict(folders)
if bin_dir is not _CUSTOM_DIR_UNSET:
if bin_dir is None:
folders.pop(backend, None)
else:
if not isinstance(bin_dir, str) or not bin_dir.strip() or "\x00" in bin_dir:
raise ValueError("Custom backend folder must be an absolute path or null.")
candidate = pathlib.Path(bin_dir.strip()).expanduser()
if not candidate.is_absolute():
raise ValueError("Custom backend folder must be an absolute path.")
folders[backend] = str(candidate)
candidate_cfg["custom_backend_dirs"] = folders
bin_dir = get_backend_bin_dir(ctx, backend, candidate_cfg)
# Official installs replace these trees; cleanup also removes the DLL tree.
managed_dirs = [ctx.paths.llama_bin, ctx.paths.llama_grammars, ctx.paths.llama / "dll"]
for directory in (ctx.paths.llama_bin, ctx.paths.llama_grammars):
managed_dirs.extend(directory.with_name(directory.name + suffix) for suffix in (".new", ".old"))
resolved_bin_dir = bin_dir.resolve()
for directory in managed_dirs:
resolved_directory = directory.resolve()
if (resolved_bin_dir == resolved_directory
or resolved_directory in resolved_bin_dir.parents
or resolved_bin_dir in resolved_directory.parents):
raise ValueError(
f"Custom backend folder overlaps an app-managed runtime folder: {directory}. "
"Choose a separate executable folder."
)
if backend in folders:
if not bin_dir.is_dir():
raise ValueError(f"Custom backend folder does not exist or is not a directory: {bin_dir}")
with os.scandir(bin_dir):
pass
else:
bin_dir.mkdir(parents=True, exist_ok=True)
get_backend_grammars_dir(ctx, backend).mkdir(parents=True, exist_ok=True)

required = {
ctx.services.get_tool_filename("llama-cli"),
Expand Down Expand Up @@ -580,7 +643,7 @@ def activate_custom_backend(ctx: AppContext, backend: str = "custom") -> dict[st
"not_executable": sorted(not_executable),
}

runtime_health = _validate_custom_runtime_dependencies(ctx, required, backend)
runtime_health = _validate_custom_runtime_dependencies(ctx, required, backend, bin_dir=bin_dir)
if not runtime_health.get("ok", True):
return {
"ok": False,
Expand All @@ -603,6 +666,15 @@ def activate_custom_backend(ctx: AppContext, backend: str = "custom") -> dict[st
cfg["version"] = "custom"
cfg["backend"] = backend
cfg["tag"] = "custom"
saved_folders = dict(cfg.get("custom_backend_dirs", {}))
if backend in folders:
saved_folders[backend] = folders[backend]
else:
saved_folders.pop(backend, None)
if saved_folders:
cfg["custom_backend_dirs"] = saved_folders
else:
cfg.pop("custom_backend_dirs", None)
ctx.services.save_config(cfg)
ctx.state.clear_runtime_health_cache()
return {
Expand All @@ -613,6 +685,8 @@ def activate_custom_backend(ctx: AppContext, backend: str = "custom") -> dict[st
"not_executable": [],
"runtime_health": runtime_health,
}
except ValueError as e:
return {"ok": False, "error": str(e)}
except Exception as e:
print(f"[llama_manager] activate_custom_backend failed: {e}", file=sys.stderr)
return {"ok": False, "error": sanitize_error(e, 500)}
Expand Down Expand Up @@ -713,9 +787,10 @@ def activate_official_backend(ctx: AppContext, backend: str) -> dict[str, Any]:


def _validate_custom_runtime_dependencies(
ctx: AppContext, executable_names: Iterable[str], backend: str = "custom"
ctx: AppContext, executable_names: Iterable[str], backend: str = "custom",
*, bin_dir: Optional[pathlib.Path] = None,
) -> dict[str, Any]:
bin_dir = get_backend_bin_dir(ctx, backend)
bin_dir = get_backend_bin_dir(ctx, backend) if bin_dir is None else bin_dir
current_platform = ctx.services.current_platform or sys.platform
if current_platform == "unknown":
current_platform = sys.platform
Expand Down
8 changes: 4 additions & 4 deletions backend/services/process_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -493,7 +493,7 @@ def _load_config_safe(ctx: AppContext) -> dict[str, Any]:
def _fit_params_executable(ctx: AppContext) -> Any:
suffix = getattr(ctx.services, "binary_suffix", "") or ""
cfg = _load_config_safe(ctx)
bin_dir = llama_manager.get_backend_bin_dir(ctx, cfg.get("backend"))
bin_dir = llama_manager.get_backend_bin_dir(ctx, cfg.get("backend"), cfg)
return bin_dir / f"llama-fit-params{suffix}"


Expand Down Expand Up @@ -1063,7 +1063,7 @@ def normalize_process_env(value: Any) -> dict[str, str]:
def _build_process_env(ctx: AppContext) -> dict[str, str]:
env = os.environ.copy()
cfg = _load_config_safe(ctx)
bin_dir = llama_manager.get_backend_bin_dir(ctx, cfg.get("backend"))
bin_dir = llama_manager.get_backend_bin_dir(ctx, cfg.get("backend"), cfg)
runtime_paths = [str(bin_dir)]
existing_path = env.get("PATH", "")
env["PATH"] = os.pathsep.join(runtime_paths + ([existing_path] if existing_path else []))
Expand Down Expand Up @@ -1154,7 +1154,7 @@ def _validate_launch_environment(
if current_platform != "win32" and not os.access(exe_path, os.X_OK):
backend = _load_config_safe(ctx).get("backend")
recovery = (
f"Run chmod +x on {llama_manager.custom_backend_bin_label(backend)}{exe_name}."
f"Run chmod +x on {exe_path}."
if llama_manager.is_custom_backend(backend)
else "Use Repair Install to restore executable permissions."
)
Expand All @@ -1169,7 +1169,7 @@ def _validate_launch_environment(
plural = "libraries" if len(missing_runtime_files) != 1 else "library"
backend = _load_config_safe(ctx).get("backend")
recovery = (
f"Add the missing files to {llama_manager.custom_backend_bin_label(backend)}."
f"Add the missing files to {exe_path.parent}."
if llama_manager.is_custom_backend(backend)
else (
"Use Repair Install, then verify the matching Vulkan/ROCm driver runtime is installed."
Expand Down
Loading
Loading