feat(llm): llama.cpp/llama-swap aus der Diagnostic aktualisieren
Modell-Katalog → ⚙ llama.cpp zeigt je LLM-Box die installierte Version, sucht nach Updates (Pruefsumme des Image-Tags in der Registry + letztes llama-swap-Release) und aktualisiert auf Knopfdruck: altes Image sichern, neues ziehen, Container per Compose neu anlegen, Selbsttest (llama.cpp sieht die Karten, vorgeladene Modelle wieder bereit) — sonst automatisch zurueck. Zuruecksetzen auf die Vorversion per Knopf. Docker-Zugriff hat nur der neue llm-updater (feste Aktionen fuer aria-llama-swap, kein Port nach aussen, Token in /models). Der llm-adapter reicht llm_runtime weiter und meldet den Fortschritt. RVS-Allow-List um llm_runtime/_result/_progress ergaenzt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -952,6 +952,7 @@
|
||||
<button class="btn secondary" onclick="refreshLlmCatalog()" style="padding:4px 10px;font-size:11px;">Von HuggingFace aktualisieren</button>
|
||||
<button class="btn secondary" id="llm-dl-btn" onclick="openLlmDownloads()" title="Fortschritt der Modell-Downloads auf die LLM-Boxen" style="padding:4px 10px;font-size:11px;">⬇ Downloads <span id="llm-dl-badge" style="display:none;margin-left:4px;padding:0 6px;border-radius:8px;background:#FFD60A;color:#000;font-weight:700;"></span></button>
|
||||
<button class="btn secondary" onclick="openModelBoards()" title="Ranglisten der besten Modelle + was gerade auf HuggingFace im Trend ist (direkt ladbar)" style="padding:4px 10px;font-size:11px;">🏆 Bestenlisten</button>
|
||||
<button class="btn secondary" onclick="openLlmRuntime()" title="Installierte llama.cpp-/llama-swap-Version der LLM-Boxen, nach Updates suchen, aktualisieren" style="padding:4px 10px;font-size:11px;">⚙ llama.cpp</button>
|
||||
</div>
|
||||
<div class="card" style="max-width:1100px;">
|
||||
<p style="color:#8888AA;font-size:12px;margin:0 0 8px;">
|
||||
@@ -2691,6 +2692,8 @@
|
||||
if (msg.type === 'rooms_info') { rooms = (msg.payload && msg.payload.rooms) || []; renderRooms(); return; }
|
||||
if (msg.type === 'sat_result') { onSatResult(msg.payload || {}); return; }
|
||||
if (msg.type === 'llm_openai_config_result') { onOpenAiConfigResult(msg.payload || {}); return; }
|
||||
if (msg.type === 'llm_runtime_result') { onLlmRuntimeResult(msg.payload || {}); return; }
|
||||
if (msg.type === 'llm_runtime_progress') { onLlmRuntimeProgress(msg.payload || {}); return; }
|
||||
if (msg.type === 'agent_history_list') {
|
||||
if (agentHistoryHost && msg.payload && msg.payload.host === agentHistoryHost) {
|
||||
renderAgentHistory(msg.payload.events || []);
|
||||
@@ -9972,6 +9975,152 @@
|
||||
function closeLlmDownloads() {
|
||||
document.getElementById('llm-dl-modal').classList.remove('open');
|
||||
}
|
||||
|
||||
// ── llama.cpp/llama-swap der LLM-Boxen (Modell-Katalog → ⚙ llama.cpp) ──
|
||||
// Pruefen/Aktualisieren/Zuruecksetzen macht auf der Box der llm-updater
|
||||
// (einziger Dienst mit Docker-Zugriff). Ob es etwas Neues gibt, entscheidet
|
||||
// die Pruefsumme des Image-Tags in der Registry, nicht die Versionsnummer.
|
||||
const _llmRt = {}; // instanceId -> {status, checked, busy, log, result, error}
|
||||
function openLlmRuntime() {
|
||||
let bg = document.getElementById('llmrt-modal');
|
||||
if (!bg) {
|
||||
bg = document.createElement('div');
|
||||
bg.id = 'llmrt-modal';
|
||||
bg.style.cssText = 'position:fixed;inset:0;z-index:10000;background:rgba(0,0,0,.6);display:flex;align-items:center;justify-content:center;padding:16px;';
|
||||
bg.innerHTML = '<div style="background:#12121E;border:1px solid #444466;border-radius:8px;padding:16px 18px;max-width:640px;width:100%;max-height:85vh;overflow-y:auto;color:#E0E0F0;font-size:13px;line-height:1.5;">' +
|
||||
'<div style="display:flex;align-items:center;margin-bottom:10px;"><b style="font-size:15px;">⚙ llama.cpp auf den LLM-Boxen</b>' +
|
||||
'<button class="btn secondary" onclick="closeLlmRuntime()" style="margin-left:auto;padding:2px 10px;font-size:11px;">Schließen</button></div>' +
|
||||
'<div id="llmrt-body"></div></div>';
|
||||
bg.addEventListener('click', e => { if (e.target === bg) closeLlmRuntime(); });
|
||||
document.body.appendChild(bg);
|
||||
}
|
||||
bg.style.display = 'flex';
|
||||
for (const b of onlineLlmBoxes()) {
|
||||
const st = _llmRt[b.instanceId] || (_llmRt[b.instanceId] = { log: [] });
|
||||
if (!st.busy) llmRtSend(b.instanceId, 'status');
|
||||
}
|
||||
renderLlmRuntime();
|
||||
}
|
||||
function closeLlmRuntime() {
|
||||
const bg = document.getElementById('llmrt-modal');
|
||||
if (bg) bg.style.display = 'none';
|
||||
}
|
||||
function llmRtSend(instanceId, op) {
|
||||
send({ action: 'llm_runtime', instanceId, op, requestId: 'rt_' + Date.now() });
|
||||
}
|
||||
function llmRtCheck(id) {
|
||||
const st = _llmRt[id] || (_llmRt[id] = { log: [] });
|
||||
st.checking = true; st.error = '';
|
||||
llmRtSend(id, 'check');
|
||||
renderLlmRuntime();
|
||||
}
|
||||
async function llmRtUpdate(id) {
|
||||
const st = _llmRt[id] || {};
|
||||
const rel = (st.status && st.status.release) || {};
|
||||
if (!await uiConfirm('llama.cpp jetzt aktualisieren?\n\n' +
|
||||
'Die Box zieht das neue llama-swap-Image' + (rel.tag ? ' (' + rel.tag + ')' : '') + ' (mehrere GB) und legt den Container neu an. ' +
|
||||
'Die geladenen Modelle fliegen dabei kurz von den Karten und laden danach neu — laufende Anfragen (z.B. die Schmiede) brechen ab.\n\n' +
|
||||
'Vorher wird die alte Version gesichert. Sieht llama.cpp danach keine Grafikkarte oder lädt das Modell nicht, wird automatisch zurückgesetzt.')) return;
|
||||
Object.assign(st, { busy: 'update', log: [], result: null, error: '' });
|
||||
_llmRt[id] = st;
|
||||
llmRtSend(id, 'update');
|
||||
renderLlmRuntime();
|
||||
}
|
||||
async function llmRtRollback(id) {
|
||||
const st = _llmRt[id] || {};
|
||||
const bk = ((st.status && st.status.backups) || [])[0];
|
||||
if (!bk) return;
|
||||
if (!await uiConfirm('Auf die gesicherte Version zurücksetzen?\n\n' +
|
||||
'Setzt ' + bk.tag + ' wieder ein und legt den Container neu an. Die Modelle laden danach neu, laufende Anfragen brechen ab.')) return;
|
||||
Object.assign(st, { busy: 'rollback', log: [], result: null, error: '' });
|
||||
llmRtSend(id, 'rollback');
|
||||
renderLlmRuntime();
|
||||
}
|
||||
function onLlmRuntimeProgress(p) {
|
||||
const st = _llmRt[p.instanceId] || (_llmRt[p.instanceId] = { log: [] });
|
||||
st.busy = st.busy || p.op;
|
||||
st.log = (st.log || []).concat(p.lines || []).slice(-40);
|
||||
renderLlmRuntime();
|
||||
}
|
||||
function onLlmRuntimeResult(p) {
|
||||
const st = _llmRt[p.instanceId] || (_llmRt[p.instanceId] = { log: [] });
|
||||
if (p.op === 'status' || p.op === 'check') {
|
||||
st.checking = false;
|
||||
if (p.ok === false) { st.error = p.error || 'fehlgeschlagen'; }
|
||||
else {
|
||||
st.status = Object.assign({}, st.status || {}, p);
|
||||
if (p.op === 'check') st.checked = true;
|
||||
st.error = '';
|
||||
if (p.job && p.job.state === 'running' && !st.busy) st.busy = p.job.op;
|
||||
}
|
||||
} else {
|
||||
st.busy = '';
|
||||
st.result = p;
|
||||
llmRtSend(p.instanceId, 'check'); // neuen Stand anzeigen
|
||||
}
|
||||
renderLlmRuntime();
|
||||
}
|
||||
function _rtDate(s) {
|
||||
if (!s) return '';
|
||||
const d = new Date(s);
|
||||
return isNaN(d) ? String(s) : d.toLocaleDateString('de-DE') + ' ' + d.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' });
|
||||
}
|
||||
function renderLlmRuntime() {
|
||||
const body = document.getElementById('llmrt-body');
|
||||
if (!body) return;
|
||||
const boxes = onlineLlmBoxes();
|
||||
if (!boxes.length) { body.innerHTML = '<span style="color:#6a6a88;">Keine LLM-Box online.</span>'; return; }
|
||||
body.innerHTML = boxes.map(b => {
|
||||
const id = b.instanceId, st = _llmRt[id] || {}, s = st.status || {}, inst = s.installed || {};
|
||||
const q = "'" + escapeHtml(id) + "'";
|
||||
let h = '<div style="background:#0D0D1A;border-radius:6px;padding:10px 12px;margin-bottom:10px;">' +
|
||||
'<div style="font-weight:600;color:#AAB;margin-bottom:6px;">🖥️ ' + escapeHtml(b.node || id) + '</div>';
|
||||
if (st.error) h += '<div style="color:#FF6E6E;margin-bottom:6px;">✗ ' + escapeHtml(st.error) + '</div>';
|
||||
if (!s.image && !st.error) h += '<div style="color:#8888AA;">frage Version ab …</div>';
|
||||
if (s.image) {
|
||||
h += '<div style="font-size:12px;"><b>Installiert:</b> llama-swap ' + escapeHtml(inst.llamaSwap || '?') +
|
||||
' · llama.cpp ' + escapeHtml(inst.llamaCpp || '?') + '</div>' +
|
||||
'<div style="font-size:11px;color:#8888AA;">Image gebaut ' + escapeHtml(_rtDate(inst.created)) +
|
||||
' · <code>' + escapeHtml(s.image) + '</code></div>';
|
||||
if (st.checked) {
|
||||
const rel = s.release || {};
|
||||
const relTxt = rel.tag ? 'llama-swap <b>' + escapeHtml(rel.tag) + '</b> vom ' + escapeHtml(_rtDate(rel.published)) +
|
||||
(rel.url ? ' · <a href="' + escapeHtml(rel.url) + '" target="_blank" style="color:#0A84FF;">Änderungen</a>' : '') : '';
|
||||
if (s.latestError) h += '<div style="color:#FF9500;margin-top:6px;">⚠ ' + escapeHtml(s.latestError) + '</div>';
|
||||
else if (s.updateAvailable) h += '<div style="color:#FFD60A;margin-top:6px;">⬆ Update verfügbar' + (relTxt ? ': ' + relTxt : '') + '</div>';
|
||||
else h += '<div style="color:#34C759;margin-top:6px;">✓ Aktuell' + (relTxt ? ' (neuestes Release: ' + relTxt + ')' : '') + '</div>';
|
||||
}
|
||||
const bk = (s.backups || [])[0];
|
||||
if (bk) h += '<div style="font-size:11px;color:#8888AA;margin-top:4px;">Gesicherte Vorversion: <code>' + escapeHtml(bk.tag) + '</code></div>';
|
||||
if (!st.busy) {
|
||||
h += '<div style="display:flex;gap:6px;flex-wrap:wrap;margin-top:8px;">' +
|
||||
'<button class="btn secondary" onclick="llmRtCheck(' + q + ')" style="padding:3px 10px;font-size:11px;"' + (st.checking ? ' disabled' : '') + '>' +
|
||||
(st.checking ? 'suche …' : 'Nach Updates suchen') + '</button>' +
|
||||
(st.checked && s.updateAvailable ? '<button class="btn" onclick="llmRtUpdate(' + q + ')" style="padding:3px 10px;font-size:11px;">Jetzt aktualisieren</button>' : '') +
|
||||
(bk ? '<button class="btn secondary" onclick="llmRtRollback(' + q + ')" style="padding:3px 10px;font-size:11px;">Zurück auf Vorversion</button>' : '') +
|
||||
'</div>';
|
||||
}
|
||||
}
|
||||
if (st.busy) h += '<div style="color:#FFD60A;margin-top:8px;">⏳ ' + (st.busy === 'update' ? 'Aktualisiere' : 'Setze zurück') +
|
||||
' … das dauert ein paar Minuten (Download + Modell neu laden).</div>';
|
||||
if ((st.log || []).length && (st.busy || st.result)) {
|
||||
h += '<pre style="background:#08080F;color:#9a9ab5;font-size:10.5px;padding:6px 8px;border-radius:4px;max-height:160px;overflow-y:auto;white-space:pre-wrap;margin:6px 0 0;">' +
|
||||
escapeHtml(st.log.join('\n')) + '</pre>';
|
||||
}
|
||||
const r = st.result;
|
||||
if (r && !st.busy) {
|
||||
if (r.ok && r.changed === false) h += '<div style="color:#34C759;margin-top:6px;">✓ ' + escapeHtml(r.message || 'nichts geändert') + '</div>';
|
||||
else if (r.ok) h += '<div style="color:#34C759;margin-top:6px;">✓ Fertig. ' + escapeHtml(r.selftest || '') +
|
||||
(r.before && r.after ? '<br>llama-swap ' + escapeHtml(r.before.llamaSwap || '?') + ' → ' + escapeHtml(r.after.llamaSwap || '?') +
|
||||
' · llama.cpp ' + escapeHtml(r.before.llamaCpp || '?') + ' → ' + escapeHtml(r.after.llamaCpp || '?') : '') + '</div>';
|
||||
else h += '<div style="color:#FF6E6E;margin-top:6px;">✗ ' + escapeHtml(r.error || 'fehlgeschlagen') +
|
||||
(r.rolledBack ? '<br><span style="color:#FF9500;">Automatisch zurückgesetzt: ' + escapeHtml(r.rollbackSelftest || '') + '</span>' : '') + '</div>';
|
||||
}
|
||||
return h + '</div>';
|
||||
}).join('') +
|
||||
'<div style="font-size:11px;color:#6a6a88;">Ob es etwas Neues gibt, zeigt die Prüfsumme des Images in der Registry — der Tag <code>unified-cuda</code> wandert mit. ' +
|
||||
'Das Image enthält llama-swap und llama.cpp zusammen.</div>';
|
||||
}
|
||||
// "Box antwortet nicht" braucht ein Ticken, auch wenn keine Meldung kommt.
|
||||
setInterval(() => {
|
||||
if (Object.values(_llmJobs).some(j => j.phase === 'queued')) renderLlmDownloads();
|
||||
|
||||
Reference in New Issue
Block a user