v5 Phase 3: News-Quellen aufgeraeumt + Upgrade-Vorschlaege
- news.py: Git-Release-Feeds raus (b9745-Rauschen), Qualitaetsquellen rein (HuggingFace, r/LocalLLaMA, Simon Willison, Latent Space, Ahead of AI, The Batch). - cookbook.py: /upgrades (matcht installierte alte Modelle gegen UPGRADES-Map) + /install-model (einzelnes Modell tauschen, GGUF dynamisch, optimaler ctx). - news.js: 'Fuer dich: Upgrades'-Sektion oben (Modell X ersetzt dein Y, weil ... [Installieren]). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+59
-1
@@ -15,7 +15,7 @@ from hw_math import evaluate_fit, max_ctx_for
|
|||||||
from config import MODELS_DIR, CMD_TEMPLATE, DEFAULT_TTL
|
from config import MODELS_DIR, CMD_TEMPLATE, DEFAULT_TTL
|
||||||
from llamaswap import read_config, write_config
|
from llamaswap import read_config, write_config
|
||||||
from jobengine import start_job, JOBS
|
from jobengine import start_job, JOBS
|
||||||
from recipes import RECIPES
|
from recipes import RECIPES, UPGRADES
|
||||||
|
|
||||||
router = APIRouter(prefix="/api/cookbook", dependencies=[Depends(auth)])
|
router = APIRouter(prefix="/api/cookbook", dependencies=[Depends(auth)])
|
||||||
|
|
||||||
@@ -34,6 +34,13 @@ class InstallRecipeReq(BaseModel):
|
|||||||
recipe_id: str
|
recipe_id: str
|
||||||
hf_token: str | None = None
|
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:
|
def extract_params_b(repo_id: str) -> float:
|
||||||
"""Extrahiert die Parametergröße (in Milliarden) aus dem Repo-Namen."""
|
"""Extrahiert die Parametergröße (in Milliarden) aus dem Repo-Namen."""
|
||||||
# z.B. Qwen2.5-Coder-32B -> 32
|
# 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]
|
nosplit = [g for g in ggufs if "-of-" not in g]
|
||||||
return (pref or nosplit or ggufs)[0]
|
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}
|
||||||
|
|
||||||
|
|||||||
+3
-2
@@ -18,10 +18,11 @@ router = APIRouter(prefix="/api", dependencies=[Depends(auth)])
|
|||||||
|
|
||||||
FEEDS = [
|
FEEDS = [
|
||||||
("HuggingFace", "https://huggingface.co/blog/feed.xml"),
|
("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"),
|
("r/LocalLLaMA", "https://www.reddit.com/r/LocalLLaMA/.rss"),
|
||||||
("Simon Willison", "https://simonwillison.net/atom/everything/"),
|
("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)"}
|
_UA = {"User-Agent": "Mozilla/5.0 (MissionControl NewsBot)"}
|
||||||
_TTL = 1800 # 30 min
|
_TTL = 1800 # 30 min
|
||||||
|
|||||||
@@ -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 { api, getHfToken } from "../core/api.js";
|
||||||
import { $, esc, ago } from "../core/ui.js";
|
import { $, esc, ago, toast } from "../core/ui.js";
|
||||||
|
|
||||||
|
let UPS = [];
|
||||||
|
const fitCls = l => l === "perfect" ? "ok" : l === "marginal" ? "warn" : "bad";
|
||||||
|
|
||||||
function mount() {
|
function mount() {
|
||||||
$(".view[data-view='news']").innerHTML = `
|
$(".view[data-view='news']").innerHTML = `
|
||||||
<div class="pagehead">
|
<div class="pagehead">
|
||||||
<div><h1>News</h1>
|
<div><h1>News</h1>
|
||||||
<div class="sub">Neuigkeiten rund um lokale LLMs — automatisch aus kuratierten Quellen (HuggingFace, llama.cpp, Ollama, r/LocalLLaMA …).</div></div>
|
<div class="sub">Neuigkeiten rund um lokale LLMs — aus kuratierten Quellen (HuggingFace, r/LocalLLaMA, Latent Space, Ahead of AI …).</div></div>
|
||||||
<button class="ghost" id="news-refresh">Aktualisieren</button>
|
<button class="ghost" id="news-refresh">Aktualisieren</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="upgrades"></div>
|
||||||
<div class="card"><div id="news-list">
|
<div class="card"><div id="news-list">
|
||||||
<div class="empty" style="text-align:center;padding:40px">Lade News…</div>
|
<div class="empty" style="text-align:center;padding:40px">Lade News…</div>
|
||||||
</div></div>`;
|
</div></div>`;
|
||||||
$("#news-refresh").addEventListener("click", load);
|
$("#news-refresh").addEventListener("click", () => { load(); loadUpgrades(); });
|
||||||
load();
|
load();
|
||||||
|
loadUpgrades();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- Upgrade-Vorschläge ----
|
||||||
|
function upRow(u, i) {
|
||||||
|
return `<div class="li" style="align-items:flex-start">
|
||||||
|
<div class="li-main">
|
||||||
|
<div class="flex items-center gap-2" style="flex-wrap:wrap">
|
||||||
|
<span class="li-id" style="font-family:var(--sans);font-weight:500">${esc(u.name)}</span>
|
||||||
|
<span class="fit-badge ${fitCls(u.fit.level)}">${esc(u.fit.text)}</span></div>
|
||||||
|
<div class="li-sub">Ersetzt dein „${esc(u.old)}". ${esc(u.why)}</div>
|
||||||
|
</div>
|
||||||
|
<button class="primary" data-up="${i}">Installieren</button>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = `<div class="card" style="margin-bottom:16px">
|
||||||
|
<div class="card-h"><h3>Für dich: Upgrades</h3><span class="meta">${UPS.length}</span></div>
|
||||||
|
<div class="card-sub">Neuere Modelle, die ein installiertes ersetzen können — Begründung inklusive.</div>
|
||||||
|
${UPS.map(upRow).join("")}</div>`;
|
||||||
|
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) {
|
function card(n) {
|
||||||
const date = n.ts ? ago(n.ts) : "";
|
const date = n.ts ? ago(n.ts) : "";
|
||||||
const rel = n.relevant ? ' · <span class="text-accent">für deine Hardware</span>' : "";
|
const rel = n.relevant ? ' · <span class="text-accent">für deine Hardware</span>' : "";
|
||||||
|
|||||||
Reference in New Issue
Block a user