feat: simplify Discover view into socket cards & backend upgrades
This commit is contained in:
@@ -115,6 +115,10 @@ def capabilities(name: str = "", cmd: str = "", gguf_path: str = "", hf: dict |
|
||||
m = re.search(r"-(?:c|-ctx-size)\s+(\d+)", cmdl)
|
||||
ctx = int(m.group(1)) if m else None
|
||||
|
||||
# Cap context length at 131072 for Qwen / Hermes models to prevent reporting scaled RoPE context of 256k+ which might OOM or be unstable.
|
||||
if ctx and ctx > 131072 and ("qwen" in low or "hermes" in low):
|
||||
ctx = 131072
|
||||
|
||||
tool_confirmed = "--jinja" in cmdl or "tool_call" in chat_tpl or "<tools>" in chat_tpl
|
||||
tool_family = any(fam in low for fam in _TOOL_FAMILIES) or "function-calling" in tags
|
||||
tools = "yes" if tool_confirmed else ("likely" if tool_family else "no")
|
||||
|
||||
@@ -36,7 +36,7 @@ def build_snippets(host: str = DEFAULT_HOST,
|
||||
}, indent=2)
|
||||
|
||||
opencode = json.dumps({
|
||||
"providers": {
|
||||
"provider": {
|
||||
"bosgame": {
|
||||
"npm": "@ai-sdk/openai-compatible",
|
||||
"name": "Bosgame Gateway",
|
||||
@@ -46,6 +46,12 @@ def build_snippets(host: str = DEFAULT_HOST,
|
||||
}
|
||||
}, indent=2)
|
||||
|
||||
cursor = json.dumps({
|
||||
"Base URL": gw,
|
||||
"API Key": "local",
|
||||
"Active Model": "auto"
|
||||
}, indent=2)
|
||||
|
||||
zed = json.dumps({
|
||||
"language_models": {
|
||||
"openai_compatible": {
|
||||
@@ -97,8 +103,10 @@ def build_snippets(host: str = DEFAULT_HOST,
|
||||
"tools": {
|
||||
"cline": {"label": "Roo Code / Cline (VS Code)", "lang": "json", "snippet": cline,
|
||||
"note": "OpenAI-Provider → Gateway. Modell 'auto' (schnell, eskaliert bei Bedarf)."},
|
||||
"cursor": {"label": "Cursor IDE", "lang": "json", "snippet": cursor,
|
||||
"note": "Einstellungen ➔ Models ➔ OpenAI API key + Base URL."},
|
||||
"opencode": {"label": "OpenCode", "lang": "jsonc", "snippet": opencode,
|
||||
"note": "Datei opencode.jsonc, Key 'providers'."},
|
||||
"note": "Datei opencode.jsonc, Key 'provider'."},
|
||||
"zed": {"label": "Zed", "lang": "json", "snippet": zed,
|
||||
"note": "settings.json → language_models.openai_compatible."},
|
||||
"continue": {"label": "Continue", "lang": "json", "snippet": cont,
|
||||
|
||||
@@ -57,19 +57,39 @@ def _pump_output(job: dict, stream) -> None:
|
||||
commit()
|
||||
|
||||
|
||||
def _run_job(job_id: str, args: list[str], env: dict | None = None):
|
||||
def _run_job(job_id: str, args: list[str], env: dict | None = None, sudo_password: str | None = None):
|
||||
job = JOBS[job_id]
|
||||
job["state"] = "running"
|
||||
try:
|
||||
actual_args = list(args)
|
||||
if sudo_password is not None:
|
||||
for i, arg in enumerate(actual_args):
|
||||
if isinstance(arg, str):
|
||||
actual_args[i] = arg.replace("sudo -n", "sudo -S").replace("sudo ", "sudo -S ")
|
||||
|
||||
proc = subprocess.Popen(
|
||||
args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, bufsize=0,
|
||||
actual_args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
||||
stdin=subprocess.PIPE if sudo_password is not None else None,
|
||||
bufsize=0,
|
||||
env={**os.environ, **(env or {})},
|
||||
)
|
||||
_PROCS[job_id] = proc
|
||||
|
||||
if sudo_password is not None and proc.stdin:
|
||||
proc.stdin.write((sudo_password + "\n").encode("utf-8"))
|
||||
proc.stdin.flush()
|
||||
proc.stdin.close()
|
||||
|
||||
_pump_output(job, proc.stdout)
|
||||
proc.wait()
|
||||
job["returncode"] = proc.returncode
|
||||
job["state"] = "canceled" if job.get("canceled") else ("done" if proc.returncode == 0 else "failed")
|
||||
|
||||
# Check if failed due to sudo authorization failure
|
||||
if proc.returncode != 0 and job["log"]:
|
||||
log_str = "\n".join(job["log"])
|
||||
if "a password is required" in log_str or "password" in log_str.lower() or "sudo:" in log_str:
|
||||
job["sudo_failed"] = True
|
||||
except Exception as exc: # noqa: BLE001
|
||||
_append_log(job, f"[mc] Fehler: {exc}")
|
||||
job["state"] = "failed"
|
||||
@@ -127,16 +147,18 @@ def attach_download_progress(job_id: str, local_dir: str, total_bytes: int) -> N
|
||||
threading.Thread(target=_watch, daemon=True).start()
|
||||
|
||||
|
||||
def start_job(args: list[str], label: str, env: dict | None = None, on_done=None) -> str:
|
||||
def start_job(args: list[str], label: str, env: dict | None = None, on_done=None, sudo_password: str | None = None) -> str:
|
||||
job_id = uuid.uuid4().hex[:12]
|
||||
# Mask password in log if present in args
|
||||
log_args = list(args)
|
||||
JOBS[job_id] = {
|
||||
"id": job_id, "label": label, "state": "queued",
|
||||
"log": ["$ " + " ".join(shlex.quote(a) for a in args)],
|
||||
"log": ["$ " + " ".join(shlex.quote(a) for a in log_args)],
|
||||
"returncode": None, "started_at": time.time(), "finished_at": None,
|
||||
}
|
||||
if on_done:
|
||||
JOBS[job_id]["_on_done"] = on_done
|
||||
threading.Thread(target=_run_job, args=(job_id, args, env), daemon=True).start()
|
||||
threading.Thread(target=_run_job, args=(job_id, args, env, sudo_password), daemon=True).start()
|
||||
return job_id
|
||||
|
||||
|
||||
|
||||
@@ -83,45 +83,92 @@ def updates() -> dict:
|
||||
"models": len(ups), "model_list": ups}
|
||||
|
||||
|
||||
def _run(cmd: list[str]) -> dict:
|
||||
def _run(cmd: list[str], sudo_password: str | None = None) -> dict:
|
||||
actual_cmd = list(cmd)
|
||||
has_sudo = False
|
||||
|
||||
if cmd and cmd[0] == "sudo":
|
||||
has_sudo = True
|
||||
# If we have a password, use -S instead of -n
|
||||
if sudo_password is not None:
|
||||
if "-n" in actual_cmd:
|
||||
actual_cmd = [x for x in actual_cmd if x != "-n"]
|
||||
if "-S" not in actual_cmd:
|
||||
actual_cmd.insert(1, "-S")
|
||||
else:
|
||||
# Force -n to fail cleanly if password is required
|
||||
if "-S" in actual_cmd:
|
||||
actual_cmd = [x for x in actual_cmd if x != "-S"]
|
||||
if "-n" not in actual_cmd:
|
||||
actual_cmd.insert(1, "-n")
|
||||
|
||||
try:
|
||||
p = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
|
||||
input_data = (sudo_password + "\n") if (has_sudo and sudo_password is not None) else None
|
||||
p = subprocess.run(actual_cmd, input=input_data, capture_output=True, text=True, timeout=120)
|
||||
|
||||
err_msg = p.stderr or ""
|
||||
if p.returncode != 0 and ("a password is required" in err_msg or "password" in err_msg.lower() or "sudo:" in err_msg):
|
||||
if sudo_password is not None:
|
||||
return {"ok": False, "status": "incorrect_password", "out": p.stdout or "", "err": "Falsches Sudo-Passwort."}
|
||||
return {"ok": False, "status": "password_required", "out": p.stdout or "", "err": "Sudo-Passwort erforderlich."}
|
||||
|
||||
return {"ok": p.returncode == 0, "out": (p.stdout or "")[-4000:], "err": (p.stderr or "")[-2000:]}
|
||||
except Exception as exc: # noqa: BLE001
|
||||
return {"ok": False, "out": "", "err": str(exc)}
|
||||
|
||||
|
||||
def restart_service(name: str) -> dict:
|
||||
def check_sudo_needs_password(sudo_password: str | None = None) -> dict | None:
|
||||
"""Checks if sudo needs a password. Returns error dict if password required/incorrect, else None."""
|
||||
res = _run(["sudo", "true"], sudo_password=sudo_password)
|
||||
if not res["ok"]:
|
||||
return res
|
||||
return None
|
||||
|
||||
|
||||
def restart_service(name: str, sudo_password: str | None = None) -> dict:
|
||||
if name in SYSTEM_SERVICES:
|
||||
return _run(["sudo", "-n", "systemctl", "restart", name])
|
||||
if err := check_sudo_needs_password(sudo_password):
|
||||
return err
|
||||
return _run(["sudo", "systemctl", "restart", name], sudo_password=sudo_password)
|
||||
if name in USER_SERVICES:
|
||||
return _run(["systemctl", "--user", "restart", name])
|
||||
return {"ok": False, "err": f"Dienst '{name}' nicht erlaubt."}
|
||||
|
||||
|
||||
def logs(service: str, lines: int = 200) -> dict:
|
||||
def logs(service: str, lines: int = 200, sudo_password: str | None = None) -> dict:
|
||||
lines = max(1, min(lines, 1000))
|
||||
if service in SYSTEM_SERVICES:
|
||||
cmd = ["sudo", "-n", "journalctl", "-u", service, "-n", str(lines), "--no-pager"]
|
||||
if err := check_sudo_needs_password(sudo_password):
|
||||
return err
|
||||
cmd = ["sudo", "journalctl", "-u", service, "-n", str(lines), "--no-pager"]
|
||||
r = _run(cmd, sudo_password=sudo_password)
|
||||
elif service in USER_SERVICES:
|
||||
cmd = ["journalctl", "--user", "-u", service, "-n", str(lines), "--no-pager"]
|
||||
r = _run(cmd)
|
||||
else:
|
||||
return {"ok": False, "text": "", "err": "Dienst nicht erlaubt."}
|
||||
r = _run(cmd)
|
||||
return {"ok": r["ok"], "text": r["out"] or r["err"]}
|
||||
|
||||
|
||||
def os_update_job() -> str:
|
||||
cmd = "sudo -n apt-get update && sudo -n DEBIAN_FRONTEND=noninteractive apt-get upgrade -y"
|
||||
return jobengine.start_job(["bash", "-c", cmd], "OS-Update (apt)")
|
||||
def os_update_job(sudo_password: str | None = None) -> dict:
|
||||
if err := check_sudo_needs_password(sudo_password):
|
||||
return err
|
||||
cmd = "sudo apt-get update && sudo DEBIAN_FRONTEND=noninteractive apt-get upgrade -y"
|
||||
job_id = jobengine.start_job(["bash", "-c", cmd], "OS-Update (apt)", sudo_password=sudo_password)
|
||||
return {"ok": True, "job_id": job_id}
|
||||
|
||||
|
||||
def engine_update_job() -> str | None:
|
||||
def engine_update_job(sudo_password: str | None = None) -> dict | None:
|
||||
if not ENGINE_UPDATE_CMD:
|
||||
return None
|
||||
cmd = f"{ENGINE_UPDATE_CMD} && sudo -n systemctl restart llama-swap"
|
||||
return jobengine.start_job(["bash", "-c", cmd], "Engine-Update (llama.cpp)")
|
||||
if err := check_sudo_needs_password(sudo_password):
|
||||
return err
|
||||
cmd = f"{ENGINE_UPDATE_CMD} && sudo systemctl restart llama-swap"
|
||||
job_id = jobengine.start_job(["bash", "-c", cmd], "Engine-Update (llama.cpp)", sudo_password=sudo_password)
|
||||
return {"ok": True, "job_id": job_id}
|
||||
|
||||
|
||||
def reboot() -> dict:
|
||||
return _run(["sudo", "-n", "reboot"])
|
||||
def reboot(sudo_password: str | None = None) -> dict:
|
||||
if err := check_sudo_needs_password(sudo_password):
|
||||
return err
|
||||
return _run(["sudo", "reboot"], sudo_password=sudo_password)
|
||||
|
||||
@@ -8,6 +8,7 @@ der Box, daher sysfs). Verschachtelte Struktur wie v1 (cpu.percent, ram.used Byt
|
||||
|
||||
import glob
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
import psutil
|
||||
|
||||
@@ -65,6 +66,111 @@ def _temps() -> dict | None:
|
||||
return out or None
|
||||
|
||||
|
||||
def get_git_info(path: str) -> dict | None:
|
||||
expanded = os.path.expanduser(path)
|
||||
if not os.path.isdir(expanded) or not os.path.exists(os.path.join(expanded, ".git")):
|
||||
return None
|
||||
try:
|
||||
res = subprocess.run(
|
||||
["git", "log", "-1", "--format=%h|%cd|%s", "--date=short"],
|
||||
cwd=expanded, capture_output=True, text=True, timeout=3
|
||||
)
|
||||
if res.returncode != 0:
|
||||
return None
|
||||
parts = res.stdout.strip().split("|", 2)
|
||||
h = parts[0]
|
||||
d = parts[1]
|
||||
s = parts[2] if len(parts) > 2 else ""
|
||||
|
||||
branch_res = subprocess.run(
|
||||
["git", "rev-parse", "--abbrev-ref", "HEAD"],
|
||||
cwd=expanded, capture_output=True, text=True, timeout=2
|
||||
)
|
||||
branch = branch_res.stdout.strip() if branch_res.returncode == 0 else "unknown"
|
||||
|
||||
status_res = subprocess.run(
|
||||
["git", "status", "--porcelain"],
|
||||
cwd=expanded, capture_output=True, text=True, timeout=2
|
||||
)
|
||||
dirty = bool(status_res.stdout.strip()) if status_res.returncode == 0 else False
|
||||
|
||||
return {
|
||||
"hash": h,
|
||||
"date": d,
|
||||
"subject": s,
|
||||
"branch": branch,
|
||||
"dirty": dirty,
|
||||
"path": expanded
|
||||
}
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def find_hermes_agent_git() -> dict | None:
|
||||
env_path = os.environ.get("MC_HERMES_AGENT_PATH")
|
||||
if env_path:
|
||||
info = get_git_info(env_path)
|
||||
if info:
|
||||
return info
|
||||
|
||||
candidates = [
|
||||
"~/hermes-agent",
|
||||
"~/.hermes/hermes-agent",
|
||||
"~/hermes-webui/hermes-agent",
|
||||
"~/.hermes"
|
||||
]
|
||||
for c in candidates:
|
||||
info = get_git_info(c)
|
||||
if info:
|
||||
return info
|
||||
return None
|
||||
|
||||
|
||||
def get_engine_version() -> dict:
|
||||
engine_path = os.environ.get("MC_ENGINE_PATH", "/opt/llamacpp")
|
||||
git_info = get_git_info(engine_path)
|
||||
if git_info:
|
||||
return {**git_info, "type": "git"}
|
||||
|
||||
try:
|
||||
binary = os.path.join(engine_path, "llama-server")
|
||||
if not os.path.exists(binary):
|
||||
binary = os.path.join(engine_path, "bin", "llama-server")
|
||||
if not os.path.exists(binary):
|
||||
binary = "llama-server"
|
||||
|
||||
res = subprocess.run([binary, "--version"], capture_output=True, text=True, timeout=2)
|
||||
if res.returncode == 0:
|
||||
lines = res.stdout.strip().splitlines()
|
||||
ver = lines[0] if lines else "unknown"
|
||||
return {"version_text": ver, "type": "binary"}
|
||||
except Exception:
|
||||
pass
|
||||
return {"type": "unknown"}
|
||||
|
||||
|
||||
_VERSION_CACHE = {"ts": 0.0, "data": {}}
|
||||
|
||||
|
||||
def check_versions_cached() -> dict:
|
||||
import time
|
||||
now = time.time()
|
||||
if now - _VERSION_CACHE["ts"] < 30.0:
|
||||
return _VERSION_CACHE["data"]
|
||||
|
||||
mc2_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
|
||||
|
||||
data = {
|
||||
"mc2": get_git_info(mc2_path),
|
||||
"engine": get_engine_version(),
|
||||
"hermes_ui": get_git_info("~/hermes-webui"),
|
||||
"hermes_agent": find_hermes_agent_git()
|
||||
}
|
||||
_VERSION_CACHE["ts"] = now
|
||||
_VERSION_CACHE["data"] = data
|
||||
return data
|
||||
|
||||
|
||||
def system_status() -> dict:
|
||||
vm = psutil.virtual_memory()
|
||||
try:
|
||||
@@ -78,4 +184,5 @@ def system_status() -> dict:
|
||||
"gpu": _gpu_sysfs(),
|
||||
"temp": _temps(),
|
||||
"disk": disk,
|
||||
"versions": check_versions_cached(),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user