Skip to content
Closed
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
9 changes: 8 additions & 1 deletion backend/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,11 +194,17 @@ 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.resolve_backend_tool_executable(
APP_CONTEXT, cfg.get("backend"), tool
)


def get_runtime_files():
cfg = load_config()
if llama_manager_service.is_system_backend(cfg.get("backend")):
# System tools live across PATH directories; there is no single
# repository bin directory to enumerate.
return []
search_dir = llama_manager_service.get_backend_bin_dir(APP_CONTEXT, cfg.get("backend"))
if not search_dir.exists():
return []
Expand Down Expand Up @@ -973,6 +979,7 @@ def do_DELETE(self):
.add("POST", "/api/install", install_routes.start_install)
.add("POST", "/api/update", install_routes.start_update)
.add("POST", "/api/activate-custom", install_routes.activate_custom)
.add("POST", "/api/activate-system", install_routes.activate_system)
.add("POST", "/api/launch/preflight", process_routes.preflight_launch)
.add("POST", "/api/presets/fingerprint", process_routes.fingerprint_preset)
.add("POST", "/api/launch", process_routes.launch)
Expand Down
2 changes: 1 addition & 1 deletion backend/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ class BackendServices:
binary_suffix: str = ""
current_arch: str = "unknown"
current_platform: str = "unknown"
find_tool_executable: Callable[[str], Path] = _missing_service
find_tool_executable: Callable[[str], Optional[Path]] = _missing_service
get_local_llama_metrics: Callable[[str, str, str], Tuple[Optional[str], str]] = _missing_service
get_local_llama_props: Callable[[str, str, str], Tuple[Optional[str], str]] = _missing_service
get_local_llama_slots: Callable[[str, str, str], Tuple[Optional[str], str]] = _missing_service
Expand Down
44 changes: 43 additions & 1 deletion backend/routes/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ def get_releases(request, response, ctx):
spec = None
query = urllib.parse.parse_qs(request.query or "")
backend = (query.get("backend") or [""])[0].strip()
if llama_manager.is_custom_backend(backend):
if llama_manager.is_custom_backend(backend) or llama_manager.is_system_backend(backend):
response.json([])
return
if backend:
Expand Down Expand Up @@ -89,6 +89,15 @@ def start_install(request, response, ctx):
if llama_manager.is_custom_backend(backend):
response.error("Use /api/activate-custom to set up the custom backend", 400)
return
if llama_manager.is_system_backend(backend):
if activate_existing:
response.error("Use /api/activate-system to activate System (PATH) tools", 400)
else:
response.error(
"System (PATH) tools are installed by the OS package manager; "
"use /api/activate-system to use them", 400
)
return
if backend not in backend_specs:
response.error(f"Unsupported backend: {backend}", 400)
return
Expand Down Expand Up @@ -142,6 +151,11 @@ def start_update(request, response, ctx):
if llama_manager.is_custom_backend(backend):
response.error("Cannot auto-update a custom backend installation", 400)
return
if llama_manager.is_system_backend(backend):
response.error(
"System (PATH) tools are updated by the OS package manager", 400
)
return
if backend not in backend_specs:
response.error(f"Unsupported configured backend: {backend}", 400)
return
Expand Down Expand Up @@ -216,6 +230,34 @@ def _update(latest_tag, backend_name):
response.json({"status": "started", "from": tag, "to": latest})


def activate_system(request, response, ctx):
body = request.body or {}
if not isinstance(body, dict):
response.error("Invalid request body", 400)
return
for key in ("path", "executable", "executable_path", "bin_dir", "directory"):
if key in body:
response.error(
"System activation uses the PATH inherited by Llama GUI; "
"client-supplied paths are not accepted",
400,
)
return
try:
claim_error = _claim_install_slot(ctx)
if claim_error is not None:
response.error(*claim_error)
return
try:
result = llama_manager.activate_system_backend(ctx)
response.json(result)
finally:
process_manager.release_install_slot(ctx)
except Exception as e:
print(f"[install] activate system backend failed: {e}", file=sys.stderr)
response.error(sanitize_error(e, 500), 500)


def activate_custom(request, response, ctx):
backend = (request.body or {}).get("backend", "custom")
if not llama_manager.is_custom_backend(backend):
Expand Down
21 changes: 18 additions & 3 deletions backend/routes/lifecycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,24 @@ def post_open_folder(request, response, ctx):
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)
if llama_manager.is_system_backend(backend):
# Read-only policy: reveal the discovered server's directory
# without creating anything. A missing server reports PATH
# guidance instead of falling back to an application folder.
try:
server_exe = ctx.services.find_tool_executable("llama-server")
except Exception:
server_exe = None
if server_exe is None or not server_exe.is_file():
response.error(llama_manager.SYSTEM_PATH_GUIDANCE, 409)
return
target = server_exe.parent
elif llama_manager.is_custom_backend(backend):
target = llama_manager.get_backend_bin_dir(ctx, backend).parent
target.mkdir(parents=True, exist_ok=True)
else:
target = 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
58 changes: 53 additions & 5 deletions backend/routes/status.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,25 @@ def get_status(request, response, ctx):
for tool in services.llama_tools:
name = services.get_tool_filename(tool)
executable = services.find_tool_executable(tool)
exes[name] = executable.is_file() and (
exes[name] = executable is not None and executable.is_file() and (
services.current_platform == "win32" or os.access(executable, os.X_OK)
)

runtime_files = services.get_runtime_files()
runtime_health = dict(services.validate_runtime_dependencies())
has_config = bool(cfg.get("tag"))
core_tools_present = all(
exes.get(services.get_tool_filename(tool), False)
for tool in ("llama-cli", "llama-server")
)
is_system = llama_manager.is_system_backend(cfg.get("backend"))
if is_system:
# System readiness needs the required server entry point, not the
# optional CLI. A missing estimator or benchmark tool never blocks it.
core_tools_present = bool(
exes.get(services.get_tool_filename("llama-server"), False)
)
else:
core_tools_present = all(
exes.get(services.get_tool_filename(tool), False)
for tool in ("llama-cli", "llama-server")
)
installed = has_config and core_tools_present and runtime_health.get("ok", True)
config_stale = has_config and not installed
process_status = process_manager.get_process_status_snapshot(ctx)
Expand All @@ -43,6 +51,42 @@ def get_status(request, response, ctx):
print(f"[status] failed to read llama API target: {exc}", file=sys.stderr)
api_target = {"host": LLAMA_HOST, "port": LLAMA_PORT}

# PATH discovery for display: per-tool entry points, availability, and
# probe state. Paths resolve regardless of the active backend so the
# GUI can preview System before activation; build/probe details come
# from runtime health and are only present while System is active.
system_probes = runtime_health.get("system_probes") or {}
system_build_tags = runtime_health.get("build_tags") or {}
system_tools = {}
for tool in services.llama_tools:
try:
discovered = services.find_tool_executable(tool)
except Exception:
discovered = None
probe = system_probes.get(tool) or {}
system_tools[tool] = {
"path": str(discovered) if discovered is not None else None,
"available": bool(
exes.get(services.get_tool_filename(tool), False)
),
"build_tag": system_build_tags.get(tool),
"probe_ok": probe.get("ok"),
"probe_error": probe.get("error"),
}

system_server_error = None
if is_system:
server_probe = system_probes.get("llama-server") or {}
if not system_tools.get("llama-server", {}).get("available"):
system_server_error = llama_manager.SYSTEM_PATH_GUIDANCE
elif server_probe.get("ok") is False:
detail = server_probe.get("error") or "could not be started"
server_name = services.get_tool_filename("llama-server")
system_server_error = (
f"{server_name} {detail}. "
f"Check that it runs with `{server_name} --version`."
)

response.json(
{
"installed": installed,
Expand All @@ -55,6 +99,8 @@ def get_status(request, response, ctx):
"runtime_files_label": "Runtime libraries",
"runtime_health": runtime_health,
"missing_runtime_files": runtime_health.get("missing_runtime_files", []),
"system_tools": system_tools,
"system_server_error": system_server_error,
**model_dir_info,
"running": process_status["running"],
"active_process_tool": process_status["active_process_tool"],
Expand All @@ -73,6 +119,8 @@ def get_status(request, response, ctx):
"id": key, "label": spec["label"],
**({"custom": True, "bin_dir": llama_manager.custom_backend_bin_label(key)}
if llama_manager.is_custom_backend(key) else {}),
**({"system_path": True}
if llama_manager.is_system_backend(key) else {}),
}
for key, spec in backend_specs.items()
],
Expand Down
Loading
Loading