feat(llm): Modell-Download + HuggingFace-Katalog (Stage D)
Neue lokale GGUF-Modelle per Knopf auf eine Box laden — ohne Image-Rebuild. - llm-adapter besitzt jetzt llama-swaps Config: generiert /models/llama-swap.config.yaml aus Basis-Template (xtts/llama-swap/config.yaml) + persistenter Registry /models/aria_models.json. Neue RVS-Handler llm_provision_model / llm_remove_model (targetInstance-gefiltert): Registry+ Config schreiben, llama-swap-Reload anstossen, neu announcen, Warmup (zieht das GGUF via -hf, Fortschritt via service_status loading→ready). pyyaml ergaenzt. - compose: llama-swap liest --config /models/llama-swap.config.yaml; llm-adapter mountet ./models (rw) + ./llama-swap (ro Template). - diagnostic/server.js: /shared/config/llm_catalog.json (kuratierte GGUF-Liste) + GET /api/llm-catalog + POST /api/llm-catalog/refresh (HuggingFace-API-Merge); Actions llm_provision_model / llm_remove_model / llm_test; llm_provision_result an Browser durchgereicht. - diagnostic/index.html: "Modell-Katalog"-Card (HF-Refresh, Ziel-Box waehlen, Laden, Verfuegbarkeit) + Test-Chat-Zeile ans lokale LLM (Antwort + Latenz). Download nutzt llama-swaps vorhandenen -hf-Pfad (kein neuer Download-Code). Reload ist der einzige Deploy-Verify-Punkt (llama-swap-Image); Fallback Box-up. Deploy: diagnostic neu bauen (VM) + llm-Boxen neu bauen. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+119
-1
@@ -609,9 +609,37 @@
|
||||
<!-- LLM-Flotte: welche Box faehrt welche Modelle (live via RVS) -->
|
||||
<div id="local-llm-fleet" style="font-size:11px;color:#8888AA;margin:6px 0 0 0;"></div>
|
||||
|
||||
<!-- Test-Chat: kurze Nachricht direkt ans lokale LLM -->
|
||||
<div style="margin:12px 0 0 0;padding-top:10px;border-top:1px solid #2a2a3a;">
|
||||
<div style="font-size:12px;color:#E0E0F0;margin-bottom:4px;"><strong>Test-Chat</strong> (aktuelles Modell, direkt ans lokale LLM):</div>
|
||||
<div style="display:flex;gap:6px;">
|
||||
<input id="llm-test-input" type="text" placeholder="z. B. Sag kurz Hallo." style="flex:1;background:#1E1E2E;border:1px solid #333;border-radius:4px;padding:6px 8px;color:#E0E0F0;font-family:inherit;font-size:12px;" onkeydown="if(event.key==='Enter')runLlmTest();">
|
||||
<button class="btn secondary" onclick="runLlmTest()" style="padding:4px 12px;font-size:11px;">Senden</button>
|
||||
</div>
|
||||
<div id="llm-test-result" style="font-size:11px;color:#8888AA;margin-top:6px;white-space:pre-wrap;"></div>
|
||||
</div>
|
||||
|
||||
<div id="local-llm-status" style="font-size:11px;color:#6a6a88;margin-top:8px;padding-top:8px;border-top:1px solid #2a2a3a;min-height:14px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modell-Katalog: GGUF-Modelle herunterladen/aktivieren (Stage D) -->
|
||||
<div class="settings-section">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;">
|
||||
<h2 style="margin:0;">Modell-Katalog</h2>
|
||||
<button class="btn secondary" onclick="refreshLlmCatalog()" style="padding:4px 10px;font-size:11px;">Von HuggingFace aktualisieren</button>
|
||||
</div>
|
||||
<div class="card" style="max-width:720px;">
|
||||
<p style="color:#8888AA;font-size:12px;margin:0 0 8px;">
|
||||
Gaengige lokale GGUF-Modelle. „Laden" schickt das Modell an die gewaehlte
|
||||
LLM-Box — llama-swap zieht das GGUF beim ersten Mal (mehrere GB) und meldet
|
||||
den Fortschritt. Danach ist es im Modell-Dropdown oben waehlbar.
|
||||
<span style="color:#FFD60A;">Achte auf den VRAM der Box (Groesse je Modell).</span>
|
||||
</p>
|
||||
<div id="llm-catalog-list" style="font-size:12px;color:#8888AA;">(lade Katalog…)</div>
|
||||
<div id="llm-catalog-status" style="font-size:11px;color:#6a6a88;margin-top:6px;min-height:14px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Externe KI-Anbieter (Platzhalter) -->
|
||||
<div class="settings-section">
|
||||
<h2>Externe Anbieter (OpenRouter & Co)</h2>
|
||||
@@ -2010,7 +2038,27 @@
|
||||
}
|
||||
|
||||
if (msg.type === 'sat_update') { satellites = msg.satellites || []; renderSatellites(); return; }
|
||||
if (msg.type === 'worker_update') { workers = msg.workers || []; renderWorkers(); if (typeof refreshLocalLlmModelChoices === 'function') refreshLocalLlmModelChoices(); return; }
|
||||
if (msg.type === 'worker_update') { workers = msg.workers || []; renderWorkers(); if (typeof refreshLocalLlmModelChoices === 'function') refreshLocalLlmModelChoices(); if (typeof renderLlmCatalog === 'function') renderLlmCatalog(); return; }
|
||||
if (msg.type === 'llm_response') {
|
||||
const p = msg.payload || {};
|
||||
const out = document.getElementById('llm-test-result');
|
||||
if (out) {
|
||||
const ms = window._llmTestStart ? (Date.now() - window._llmTestStart) : (p.elapsedMs || 0);
|
||||
if (p.ok) { out.textContent = (p.content || '(leer)') + '\n— ' + (p.model || '') + ' · ' + ms + ' ms'; out.style.color = '#E0E0F0'; }
|
||||
else { out.textContent = '✗ ' + (p.error || 'Fehler'); out.style.color = '#FF6B6B'; }
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (msg.type === 'llm_provision_result') {
|
||||
const p = msg.payload || {};
|
||||
const st = document.getElementById('llm-catalog-status');
|
||||
if (st) {
|
||||
if (p.removed) { st.textContent = `✓ '${p.key}' entfernt`; st.style.color = '#8888AA'; }
|
||||
else if (p.ok) { st.textContent = `✓ '${p.key}' geladen/aktiv auf ${p.instanceId || '?'}`; st.style.color = '#3FFF3F'; }
|
||||
else { st.textContent = `✗ '${p.key}': ${p.error || 'Fehler'}`; st.style.color = '#FF6B6B'; }
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (msg.type === 'sat_devices') {
|
||||
if (msg.satellite) { satDevices[msg.satellite] = { devices: msg.devices || [], location: msg.location, ts: Date.now() }; }
|
||||
satScanning = null;
|
||||
@@ -2026,6 +2074,7 @@
|
||||
alert('Es war kein Fingerprint vorhanden.');
|
||||
}
|
||||
refreshVoiceIdStatus();
|
||||
loadLlmCatalog();
|
||||
switchSettingsTab(localStorage.getItem('diag_settings_subtab') || 'models');
|
||||
return;
|
||||
}
|
||||
@@ -6928,6 +6977,75 @@
|
||||
} catch (e) { /* still */ }
|
||||
}
|
||||
|
||||
// ── Modell-Katalog (Stage D) ────────────────────────────
|
||||
let _llmCatalog = [];
|
||||
async function loadLlmCatalog() {
|
||||
try {
|
||||
const r = await fetch('/api/llm-catalog');
|
||||
const j = await r.json();
|
||||
_llmCatalog = (j && j.models) || [];
|
||||
} catch (e) { _llmCatalog = []; }
|
||||
renderLlmCatalog();
|
||||
}
|
||||
async function refreshLlmCatalog() {
|
||||
const st = document.getElementById('llm-catalog-status');
|
||||
if (st) { st.textContent = 'Aktualisiere von HuggingFace…'; st.style.color = '#8888AA'; }
|
||||
try {
|
||||
const r = await fetch('/api/llm-catalog/refresh', { method: 'POST' });
|
||||
const j = await r.json();
|
||||
_llmCatalog = (j && j.models) || _llmCatalog;
|
||||
renderLlmCatalog();
|
||||
if (st) { st.textContent = j.ok ? `✓ ${j.added || 0} neue Modelle von HuggingFace` : `✗ ${j.error || 'Fehler'}`; st.style.color = j.ok ? '#3FFF3F' : '#FF6B6B'; }
|
||||
} catch (e) { if (st) { st.textContent = '✗ ' + e.message; st.style.color = '#FF6B6B'; } }
|
||||
}
|
||||
// online LLM-Boxen (fuer die Ziel-Auswahl + Verfuegbarkeit)
|
||||
function onlineLlmBoxes() { return workers.filter(w => w.service === 'llm' && w.online); }
|
||||
function boxesServing(id) { return onlineLlmBoxes().filter(w => (w.models || []).includes(id) || w.model === id); }
|
||||
function renderLlmCatalog() {
|
||||
const box = document.getElementById('llm-catalog-list');
|
||||
if (!box) return;
|
||||
if (!_llmCatalog.length) { box.innerHTML = '<span style="color:#6a6a88;">Katalog leer.</span>'; return; }
|
||||
const boxes = onlineLlmBoxes();
|
||||
box.innerHTML = _llmCatalog.map((m, i) => {
|
||||
const have = boxesServing(m.id);
|
||||
const haveTxt = have.length ? `<span style="color:#3FFF3F;">✓ auf ${have.map(b => escapeHtml(b.node)).join(', ')}</span>` : '<span style="color:#6a6a88;">nicht geladen</span>';
|
||||
const sizeTxt = m.sizeGB ? ` · ~${m.sizeGB} GB` : '';
|
||||
const opts = boxes.length
|
||||
? boxes.map(b => `<option value="${escapeHtml(b.instanceId)}">${escapeHtml(b.node)}</option>`).join('')
|
||||
: '<option value="">(keine Box online)</option>';
|
||||
return '<div style="display:flex;align-items:center;gap:8px;padding:5px 0;border-bottom:1px solid #1E1E2E;flex-wrap:wrap;">' +
|
||||
'<span style="min-width:150px;"><b>' + escapeHtml(m.id) + '</b>' + sizeTxt + '</span>' +
|
||||
'<span style="color:#8888AA;flex:1;min-width:160px;">' + escapeHtml(m.description || m.hfRepo || '') + '</span>' +
|
||||
haveTxt +
|
||||
'<select id="llm-cat-box-' + i + '" style="background:#1E1E2E;border:1px solid #333;border-radius:4px;padding:3px 6px;color:#E0E0F0;font-size:11px;">' + opts + '</select>' +
|
||||
'<button class="btn secondary" ' + (boxes.length ? '' : 'disabled') + ' onclick="provisionModel(' + i + ')" style="padding:3px 10px;font-size:11px;">Laden</button>' +
|
||||
'</div>';
|
||||
}).join('');
|
||||
}
|
||||
function provisionModel(i) {
|
||||
const m = _llmCatalog[i];
|
||||
if (!m) return;
|
||||
const sel = document.getElementById('llm-cat-box-' + i);
|
||||
const target = sel && sel.value;
|
||||
if (!target) return;
|
||||
const st = document.getElementById('llm-catalog-status');
|
||||
if (st) { st.textContent = `Lade '${m.id}' auf ${target}… (GGUF-Download kann dauern)`; st.style.color = '#FFD60A'; }
|
||||
send({ action: 'llm_provision_model', targetInstance: target, key: m.id, hfRepo: m.hfRepo, quant: m.quant, ctx: m.ctx });
|
||||
}
|
||||
function runLlmTest() {
|
||||
const inp = document.getElementById('llm-test-input');
|
||||
const out = document.getElementById('llm-test-result');
|
||||
const text = (inp && inp.value || '').trim();
|
||||
if (!text) return;
|
||||
const sel = document.getElementById('local-llm-model');
|
||||
const model = (sel && sel.value) || _currentLocalModel || '';
|
||||
const serving = boxesServing(model);
|
||||
const target = serving.length ? serving[0].instanceId : '';
|
||||
if (out) { out.textContent = '… sende an ' + (target || '(Broadcast)') + ' (' + model + ')'; out.style.color = '#8888AA'; }
|
||||
window._llmTestStart = Date.now();
|
||||
send({ action: 'llm_test', text, model, targetInstance: target });
|
||||
}
|
||||
|
||||
// ── Einstellungen: OpenClaw Config ──────────────────────
|
||||
|
||||
// loadOpenClawConfig entfernt — aria-core ist raus.
|
||||
|
||||
Reference in New Issue
Block a user