diff --git a/routers/cookbook.py b/routers/cookbook.py index 1522bb1..bae70b5 100644 --- a/routers/cookbook.py +++ b/routers/cookbook.py @@ -15,7 +15,7 @@ from hw_math import evaluate_fit, max_ctx_for from config import MODELS_DIR, CMD_TEMPLATE, DEFAULT_TTL from llamaswap import read_config, write_config from jobengine import start_job, JOBS -from recipes import RECIPES +from recipes import RECIPES, UPGRADES router = APIRouter(prefix="/api/cookbook", dependencies=[Depends(auth)]) @@ -34,6 +34,13 @@ class InstallRecipeReq(BaseModel): recipe_id: str hf_token: str | None = None +class InstallModelReq(BaseModel): + repo: str + role: str + params_b: float + quant: str = "Q4_K_M" + hf_token: str | None = None + def extract_params_b(repo_id: str) -> float: """Extrahiert die Parametergröße (in Milliarden) aus dem Repo-Namen.""" # z.B. Qwen2.5-Coder-32B -> 32 @@ -177,3 +184,54 @@ def _pick_gguf(repo: str, quant: str = "Q4_K_M") -> str | None: nosplit = [g for g in ggufs if "-of-" not in g] return (pref or nosplit or ggufs)[0] + +@router.get("/upgrades") +def upgrades(): + """Schlägt für installierte 'alte' Modelle ein aktuelleres vor (UPGRADES-Map).""" + ram_gb = psutil.virtual_memory().total / (1024 ** 3) + installed = (read_config().get("models") or {}) + out, seen = [], set() + for up in UPGRADES: + new_base = up["repo"].split("/")[-1].lower() + if any(new_base in str(s.get("cmd", "")).lower() for s in installed.values()): + continue # neueres Modell schon installiert + old = None + for alias, spec in installed.items(): + hay = (alias + " " + str(spec.get("cmd", ""))).lower() + if any(k in hay for k in up["match"]): + old = alias + break + if not old or up["repo"] in seen: + continue + seen.add(up["repo"]) + out.append({ + "name": up["name"], "repo": up["repo"], "params_b": up["params_b"], "quant": up["quant"], + "why": up["why"], "old": old, "role": old, + "fit": evaluate_fit(up["params_b"], up["quant"], 8192, ram_gb), + "optimal_ctx": max_ctx_for(up["params_b"], up["quant"], ram_gb), + }) + return {"upgrades": out} + + +@router.post("/install-model") +def install_model(req: InstallModelReq): + """Ein einzelnes Modell installieren (Download + Einpflegen unter 'role', optimaler ctx).""" + file = _pick_gguf(req.repo, req.quant) + if not file: + raise HTTPException(404, "Keine GGUF-Datei im Repo gefunden.") + ram_gb = psutil.virtual_memory().total / (1024 ** 3) + env = {"HF_XET_HIGH_PERFORMANCE": "1"} + if req.hf_token: + env["HF_TOKEN"] = req.hf_token + target = MODELS_DIR / req.repo.split("/")[-1] + target.mkdir(parents=True, exist_ok=True) + jid = start_job(["hf", "download", req.repo, file, "--local-dir", str(target)], + f"download {req.repo.split('/')[-1]}", env=env) + JOBS[jid]["result_path"] = str(target / file) + cfg = read_config() + ctx = min(max_ctx_for(req.params_b, req.quant, ram_gb), 32768) + cmd = CMD_TEMPLATE.replace("{model}", str(target / file)).replace("{ctx}", str(ctx)) + cfg["models"][req.role] = {"cmd": LiteralScalarString(cmd + "\n"), "ttl": DEFAULT_TTL} + write_config(cfg) + return {"job_id": jid} + diff --git a/routers/news.py b/routers/news.py index 6e64341..cc3e677 100644 --- a/routers/news.py +++ b/routers/news.py @@ -18,10 +18,11 @@ router = APIRouter(prefix="/api", dependencies=[Depends(auth)]) FEEDS = [ ("HuggingFace", "https://huggingface.co/blog/feed.xml"), - ("llama.cpp", "https://github.com/ggml-org/llama.cpp/releases.atom"), - ("Ollama", "https://github.com/ollama/ollama/releases.atom"), ("r/LocalLLaMA", "https://www.reddit.com/r/LocalLLaMA/.rss"), ("Simon Willison", "https://simonwillison.net/atom/everything/"), + ("Latent Space", "https://www.latent.space/feed"), + ("Ahead of AI", "https://magazine.sebastianraschka.com/feed"), + ("The Batch", "https://www.deeplearning.ai/the-batch/rss.xml"), ] _UA = {"User-Agent": "Mozilla/5.0 (MissionControl NewsBot)"} _TTL = 1800 # 30 min diff --git a/static/js/panels/news.js b/static/js/panels/news.js index 67a4057..930fd50 100644 --- a/static/js/panels/news.js +++ b/static/js/panels/news.js @@ -1,22 +1,66 @@ -// news.js — News-Board (v4): kuratierte Neuigkeiten rund um lokale LLMs. +// news.js — News-Board (v5): kuratierte News + „Für dich: Upgrades" (Modell-Tausch-Vorschläge). -import { api } from "../core/api.js"; -import { $, esc, ago } from "../core/ui.js"; +import { api, getHfToken } from "../core/api.js"; +import { $, esc, ago, toast } from "../core/ui.js"; + +let UPS = []; +const fitCls = l => l === "perfect" ? "ok" : l === "marginal" ? "warn" : "bad"; function mount() { $(".view[data-view='news']").innerHTML = `

News

-
Neuigkeiten rund um lokale LLMs — automatisch aus kuratierten Quellen (HuggingFace, llama.cpp, Ollama, r/LocalLLaMA …).
+
Neuigkeiten rund um lokale LLMs — aus kuratierten Quellen (HuggingFace, r/LocalLLaMA, Latent Space, Ahead of AI …).
+
Lade News…
`; - $("#news-refresh").addEventListener("click", load); + $("#news-refresh").addEventListener("click", () => { load(); loadUpgrades(); }); load(); + loadUpgrades(); } +// ---- Upgrade-Vorschläge ---- +function upRow(u, i) { + return `
+
+
+ ${esc(u.name)} + ${esc(u.fit.text)}
+
Ersetzt dein „${esc(u.old)}". ${esc(u.why)}
+
+ +
`; +} + +async function loadUpgrades() { + const el = $("#upgrades"); if (!el) return; + try { + const d = await api("/api/cookbook/upgrades"); + UPS = d.upgrades || []; + if (!UPS.length) { el.innerHTML = ""; return; } + el.innerHTML = `
+

Für dich: Upgrades

${UPS.length}
+
Neuere Modelle, die ein installiertes ersetzen können — Begründung inklusive.
+ ${UPS.map(upRow).join("")}
`; + el.querySelectorAll("[data-up]").forEach(b => b.addEventListener("click", () => installUpgrade(+b.getAttribute("data-up"), b))); + } catch { el.innerHTML = ""; } +} + +async function installUpgrade(i, btn) { + const u = UPS[i]; if (!u) return; + btn.disabled = true; btn.textContent = "Starte…"; + try { + await api("/api/cookbook/install-model", { method: "POST", body: JSON.stringify({ + repo: u.repo, role: u.role, params_b: u.params_b, quant: u.quant, hf_token: getHfToken() }) }); + toast(`„${u.name}" wird installiert — ersetzt „${u.old}". Siehe Aktivität.`); + document.querySelector(".nav-item[data-view='activity']")?.click(); + } catch (e) { toast("Fehler: " + e.message, true); btn.disabled = false; btn.textContent = "Installieren"; } +} + +// ---- News ---- function card(n) { const date = n.ts ? ago(n.ts) : ""; const rel = n.relevant ? ' · für deine Hardware' : "";