feat(llm): Download-Fortschritt beim Modell-Laden — und "Laden" kommt endlich an

Eigentlicher Fehler zuerst: llm_provision_model/_result und llm_remove_model
standen nie in der RVS-Allow-List. Der RVS verwirft unbekannte Typen mit einem
stillen return — jeder Klick auf "Laden" im Modell-Katalog ist seit Stage D
(19.09.) an dieser Liste gestorben, die Box hat nie davon erfahren. Laden
liessen sich nur die Modelle aus der Basis-Config der Box.

Gefunden, weil grep rvs/server.js fuer eine Binaerdatei hielt und still
uebersprang: in Zeile 164 stand seit a7998bcd ein ECHTES Nullbyte im
Template-String statt eines Escapes. Node stoert das nicht, aber git zeigte
fuer jede Aenderung am RVS nur noch "Binary files differ". Jetzt \u0000 —
zur Laufzeit derselbe String.

Fortschritt: llama.cpp zieht das GGUF per -hf selbst und meldet nichts. Der
llm-adapter fragt vorab bei Hugging Face, welche Dateien (Groesse, sha256)
kommen, und misst genau diese im HF-Cache-Layout, das llama.cpp verwendet:
  /models/models--org--repo/blobs/<sha256>.downloadInProgress  (waechst)
  /models/models--org--repo/blobs/<sha256>                     (fertig)
Layout gegen den echten llama.cpp-Server ausprobiert, nicht angenommen. Geht
auch bei gesplitteten Modellen und zwei Downloads gleichzeitig; Fallback fuer
aeltere Layouts ueber alle *.downloadInProgress. Phasen: queued → start →
download (%, MB/s, Restzeit) → load ("laedt in die Karte") → ready ("in Karte
N geladen", Name + VRAM aus nvidia-smi) bzw. error.

Mitgefixt: der Warmup wartete LLM_TIMEOUT_SEC (Default 60 s). Ein Modell mit
mehreren GB meldete deshalb "Fehler", waehrend llama-swap weiterlud. Jetzt
LLM_WARMUP_TIMEOUT_S (1900 s, ueber llama-swaps healthCheckTimeout).

Diagnostic: Knopf "⬇ Downloads" neben "Von HuggingFace aktualisieren" mit
Zaehler fuer laufende; "Laden" oeffnet das Fenster direkt. Der Server merkt
sich den Stand pro (Box, Modell), ein spaeter geoeffneter Tab sieht laufende
Downloads. Antwortet die Box nicht, steht nach 30 s eine Warnung da statt
nichts.

Compose (KI-Box): llm-adapter bekommt LLM_GPU (Karte fuer die Meldung) und
HF_TOKEN (Dateiliste gesperrter Repos).

Test: Fortschritt gegen einen echten llama.cpp-Download (Qwen2.5-0.5B Q8,
676 MB): Prozent steigend 0 → 91.6, dann load; zweiter Lauf aus dem Cache
direkt load mit cached=true.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-18 15:33:54 +02:00
co-authored by Claude Opus 5.5
parent d684bbb5f0
commit 864237ff2c
6 changed files with 342 additions and 7 deletions
+43 -1
View File
@@ -1372,7 +1372,17 @@ function connectRVS(forcePlain) {
broadcast({ type: "service_status", payload: msg.payload });
} else if (msg.type === "llm_provision_result") {
// Ergebnis eines Modell-Downloads/Aktivierens → an Browser (Katalog-Status).
broadcast({ type: "llm_provision_result", payload: msg.payload || {} });
const p = msg.payload || {};
if (p.key && p.instanceId && !p.removed) {
const job = llmJobUpdate(p.instanceId, p.key, p.ok ? {} : { phase: "error", error: p.error || "Fehler" });
if (job) broadcast({ type: "llm_provision_progress", payload: job });
}
broadcast({ type: "llm_provision_result", payload: p });
} else if (msg.type === "llm_provision_progress") {
// Download-Fortschritt einer Box (Prozent, Tempo, Restzeit, dann
// "laedt in die Karte" und "geladen"). Merken fuer spaeter geoeffnete Tabs.
const job = llmJobUpdate((msg.payload || {}).instanceId, (msg.payload || {}).key, msg.payload || {});
if (job) broadcast({ type: "llm_provision_progress", payload: job });
} else if (msg.type === "node_stats" || msg.type === "node_stats_history" || msg.type === "node_stats_reset_done") {
// Auslastungs-Monitor (Stage E): Box-Antworten an die Browser durchreichen.
broadcast({ type: msg.type, payload: msg.payload || {} });
@@ -2074,6 +2084,27 @@ setInterval(checkDiskSpace, 30000);
// ── HTTP Server + WebSocket fuer Browser ────────────────
// ── Modell-Downloads auf LLM-Boxen ──────────────────────────────────
// Letzter bekannter Stand pro (Box, Modell). Im Speicher reicht: das Fenster
// soll zeigen, was gerade laeuft und was eben fertig wurde, keine Historie.
const llmJobs = new Map();
function llmJobUpdate(instanceId, key, patch) {
if (!instanceId || !key) return null;
const id = `${instanceId}\u0000${key}`;
const job = { ...(llmJobs.get(id) || { instanceId, key, startedAt: Date.now() }), ...patch,
instanceId, key, updatedAt: Date.now() };
// Ein neuer Auftrag fuer dasselbe Modell faengt frisch an.
if (patch.phase === "queued") { delete job.error; delete job.percent; delete job.bytes; job.startedAt = Date.now(); }
llmJobs.set(id, job);
// Alte Abgeschlossene nicht ewig mitschleppen.
if (llmJobs.size > 30) {
const done = [...llmJobs.entries()].filter(([, j]) => j.phase === "ready" || j.phase === "error")
.sort((a, b) => a[1].updatedAt - b[1].updatedAt);
while (llmJobs.size > 30 && done.length) llmJobs.delete(done.shift()[0]);
}
return job;
}
const htmlPath = path.join(__dirname, "index.html");
const mailHtmlPath = path.join(__dirname, "email.html");
@@ -3320,8 +3351,19 @@ wss.on("connection", (ws) => {
// Sessions- und Brain-File-Viewer entfernt — Sessions sind raus, Memory
// laeuft jetzt komplett ueber die Vector-DB im aria-brain (siehe Gehirn-Tab).
// restart_session kommt weiter rein, weil der Watchdog ihn manchmal triggert.
} else if (msg.action === "get_llm_jobs") {
ws.send(JSON.stringify({ type: "llm_jobs", payload: { jobs: [...llmJobs.values()] } }));
} else if (msg.action === "clear_llm_jobs") {
// Nur Abgeschlossene wegraeumen — laufende Downloads bleiben sichtbar.
for (const [k, j] of llmJobs) if (j.phase === "ready" || j.phase === "error") llmJobs.delete(k);
broadcast({ type: "llm_jobs", payload: { jobs: [...llmJobs.values()] } });
} else if (msg.action === "llm_provision_model") {
// Modell auf eine bestimmte LLM-Box laden/aktivieren (Stage D).
// Sofort als "angefragt" fuehren: kommt danach nie etwas von der Box,
// sieht man das im Download-Fenster, statt dass einfach nichts passiert.
const queued = llmJobUpdate(msg.targetInstance || "", msg.key || "",
{ phase: "queued", requestedAt: Date.now() });
if (queued) broadcast({ type: "llm_provision_progress", payload: queued });
sendToRVS_raw({ type: "llm_provision_model", payload: {
targetInstance: msg.targetInstance || "",
key: msg.key, hfRepo: msg.hfRepo, quant: msg.quant, ctx: msg.ctx, ngl: msg.ngl,