feat(diagnostic): pro LLM-Karte ein Dropdown — und Kontextfenster an Clients melden
Einstellungen → Modelle & KI → "LLM-Karten": fuer jede Karte, die auf einer Box fuers LLM vorgesehen ist, ein Dropdown mit den Katalog-Modellen und "— leer —" (entlaedt die Karte). Nur echte LLM-Karten erscheinen. Modelle, die auf derselben Box schon auf einer anderen Karte liegen, werden nicht angeboten; zu grosse (Katalog-Groesse > VRAM der Karte) sind als "passt nicht" markiert. Waehrend eines Ladevorgangs ist das Dropdown gesperrt und zeigt den Fortschritt. Box meldet dafuer pro Karte den VRAM (nvidia-smi memory.total) und das Kontextfenster, mit dem llama-server das Modell startet (-c). Kontext an Clients: llm_models enthaelt jetzt "ctx". Anlass: die Software- Schmiede rechnete mit 32768 Tokens, die Box startet die Modelle mit 8192 — lange Auftraege waeren gescheitert oder falsch gekuerzt worden, und der Client konnte es nicht wissen. Doku entsprechend ergaenzt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+97
-1
@@ -839,6 +839,25 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- LLM-Karten: pro Karte ein Dropdown — welches Modell dort liegt -->
|
||||
<div class="settings-section">
|
||||
<div style="display:flex;align-items:center;gap:14px;margin-bottom:8px;">
|
||||
<h2 style="margin:0;">LLM-Karten</h2>
|
||||
<button class="btn secondary" onclick="openLlmDownloads()" style="padding:4px 10px;font-size:11px;">⬇ Downloads</button>
|
||||
</div>
|
||||
<div class="card" style="max-width:1100px;">
|
||||
<p style="color:#8888AA;font-size:12px;margin:0 0 10px;line-height:1.5;">
|
||||
Jede Grafikkarte, die auf einer KI-Box fürs LLM vorgesehen ist (<code>LLM_GPUS</code> in deren
|
||||
<code>.env</code>), hält genau ein Modell — dauerhaft geladen. Hier legst du fest, welches.
|
||||
ARIA und angebundene Programme wählen nur unter diesen Modellen.
|
||||
„— leer —" entlädt die Karte. Beim Wechsel starten die anderen Karten derselben Box kurz neu.
|
||||
</p>
|
||||
<div id="llm-card-settings" style="display:flex;flex-direction:column;gap:8px;">
|
||||
<span style="color:#6a6a88;">Keine LLM-Box online.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modell-Katalog: GGUF-Modelle herunterladen/aktivieren (Stage D) -->
|
||||
<div class="settings-section">
|
||||
<div style="display:flex;align-items:center;gap:14px;margin-bottom:8px;">
|
||||
@@ -2587,7 +2606,7 @@
|
||||
if (p.ok) setTimeout(closeDeviceCreds, 700);
|
||||
return;
|
||||
}
|
||||
if (msg.type === 'worker_update') { workers = msg.workers || []; renderWorkers(); if (typeof refreshLocalLlmModelChoices === 'function') refreshLocalLlmModelChoices(); if (typeof renderLlmCatalog === 'function') renderLlmCatalog(); return; }
|
||||
if (msg.type === 'worker_update') { workers = msg.workers || []; renderWorkers(); renderLlmCardSettings(); 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');
|
||||
@@ -8972,6 +8991,7 @@
|
||||
function renderLlmCatalog() {
|
||||
const box = document.getElementById('llm-catalog-list');
|
||||
if (!box) return;
|
||||
renderLlmCardSettings();
|
||||
if (!_llmCatalog.length) { box.innerHTML = '<span style="color:#6a6a88;">Katalog leer.</span>'; return; }
|
||||
const boxes = onlineLlmBoxes();
|
||||
const th = 'style="padding:6px 8px;text-align:left;position:sticky;top:0;background:#12122A;"';
|
||||
@@ -9035,6 +9055,7 @@
|
||||
if (!job || !job.key) return;
|
||||
_llmJobs[_llmJobId(job)] = job;
|
||||
renderLlmDownloads();
|
||||
renderLlmCardSettings();
|
||||
// Fertig geladen → Katalog-Status ("✓ ki-box") nachziehen.
|
||||
if (job.phase === 'ready' && typeof requestWorkers === 'function') requestWorkers();
|
||||
}
|
||||
@@ -9110,6 +9131,81 @@
|
||||
if (Object.values(_llmJobs).some(j => j.phase === 'queued')) renderLlmDownloads();
|
||||
}, 5000);
|
||||
|
||||
// ── LLM-Karten: Dropdown pro Karte (Einstellungen → Modelle & KI) ──
|
||||
// Optionen = Modell-Katalog + was gerade geladen ist (falls nicht im
|
||||
// Katalog). Zu gross fuer die Karte → markiert, aber waehlbar (Groesse ist
|
||||
// nur eine Schaetzung, der Kontext braucht zusaetzlich Platz).
|
||||
function renderLlmCardSettings() {
|
||||
const box = document.getElementById('llm-card-settings');
|
||||
if (!box) return;
|
||||
if (!_llmCatalog.length && !window._llmCatalogRequested) {
|
||||
window._llmCatalogRequested = true;
|
||||
loadLlmCatalog(); // ruft renderLlmCatalog → renderLlmCardSettings
|
||||
}
|
||||
const boxes = onlineLlmBoxes().filter(b => Array.isArray(b.slots));
|
||||
if (!boxes.length) {
|
||||
const old = onlineLlmBoxes().length;
|
||||
box.innerHTML = '<span style="color:#6a6a88;">' + (old
|
||||
? 'Die LLM-Box meldet noch keine Karten — llm-adapter auf der Box aktualisieren.'
|
||||
: 'Keine LLM-Box online.') + '</span>';
|
||||
return;
|
||||
}
|
||||
const rows = [];
|
||||
for (const b of boxes) {
|
||||
for (const sl of b.slots) {
|
||||
const vramGB = sl.vramMB ? sl.vramMB / 1024 : 0;
|
||||
const ids = new Set(_llmCatalog.map(m => m.id));
|
||||
const opts = ['<option value="">— leer —</option>'];
|
||||
const list = _llmCatalog.filter(m => !m.unusable);
|
||||
if (sl.model && !ids.has(sl.model)) list.unshift({ id: sl.model });
|
||||
for (const m of list) {
|
||||
// Liegt das Modell schon auf einer ANDEREN Karte dieser Box? → nicht anbieten.
|
||||
const elsewhere = (b.slots || []).find(x => x.model === m.id && x.gpu !== sl.gpu);
|
||||
if (elsewhere) continue;
|
||||
const tooBig = vramGB && m.sizeGB && m.sizeGB > vramGB * 0.92;
|
||||
const label = m.id + (m.sizeGB ? ` · ~${m.sizeGB} GB` : '') + (m.quant ? ` · ${m.quant}` : '')
|
||||
+ (tooBig ? ' · passt nicht' : '');
|
||||
opts.push(`<option value="${escapeHtml(m.id)}"${m.id === sl.model ? ' selected' : ''}${tooBig ? ' style="color:#FF9F0A;"' : ''}>${escapeHtml(label)}</option>`);
|
||||
}
|
||||
const [c, st] = sl.model ? (SLOT_STATE[sl.state] || ['#8888AA', escapeHtml(sl.state || '?')]) : ['#555570', 'leer'];
|
||||
const job = Object.values(_llmJobs || {}).find(j => j.instanceId === b.instanceId && String(j.gpu) === String(sl.gpu)
|
||||
&& !['ready', 'error'].includes(j.phase));
|
||||
const jobTxt = job ? (job.phase === 'download' && job.percent != null ? `lädt ${job.percent.toFixed(0)} %` : 'lädt …') : '';
|
||||
rows.push(`<div style="display:grid;grid-template-columns:minmax(220px,1fr) minmax(260px,2fr) 160px;gap:10px;align-items:center;">
|
||||
<div><b style="color:#E0E0F0;">${escapeHtml(b.node)}</b> · Karte ${escapeHtml(sl.gpu)}
|
||||
<div style="color:#555570;font-size:11px;">${escapeHtml(sl.gpuName || '')}${vramGB ? ' · ' + vramGB.toFixed(0) + ' GB' : ''}${sl.ctx ? ' · Kontext ' + sl.ctx : ''}</div></div>
|
||||
<select data-box="${escapeHtml(b.instanceId)}" data-gpu="${escapeHtml(sl.gpu)}" data-current="${escapeHtml(sl.model || '')}"
|
||||
onchange="onLlmCardSelect(this)" ${job ? 'disabled' : ''}
|
||||
style="background:#1E1E2E;border:1px solid #333;border-radius:4px;padding:5px 8px;color:#E0E0F0;font-size:12px;">${opts.join('')}</select>
|
||||
<span style="color:${job ? '#FFD60A' : c};font-size:12px;">${jobTxt || st}</span>
|
||||
</div>`);
|
||||
}
|
||||
}
|
||||
box.innerHTML = rows.join('');
|
||||
}
|
||||
|
||||
function onLlmCardSelect(sel) {
|
||||
const target = sel.dataset.box, gpu = sel.dataset.gpu, current = sel.dataset.current, next = sel.value;
|
||||
const node = _boxName(target);
|
||||
if (next === current) return;
|
||||
if (!next) {
|
||||
if (!confirm(`Karte ${gpu} auf ${node} leeren?\n\n${current} wird entladen; ARIA und Programme können es dann nicht mehr wählen. `
|
||||
+ `Die Datei bleibt auf der Box.\n\nDie anderen Karten dieser Box starten dabei kurz neu.`)) { sel.value = current; return; }
|
||||
send({ action: 'llm_unload_model', targetInstance: target, gpu });
|
||||
return;
|
||||
}
|
||||
const m = _llmCatalog.find(x => x.id === next);
|
||||
if (!m || !m.hfRepo) { alert(`${next} steht nicht im Modell-Katalog — Herkunft unbekannt.`); sel.value = current; return; }
|
||||
const sl = (workers.find(w => w.instanceId === target)?.slots || []).find(x => String(x.gpu) === String(gpu)) || {};
|
||||
const vramGB = sl.vramMB ? sl.vramMB / 1024 : 0;
|
||||
const warn = (vramGB && m.sizeGB && m.sizeGB > vramGB * 0.92)
|
||||
? `\n\n⚠ ${next} ist etwa ${m.sizeGB} GB groß, die Karte hat ${vramGB.toFixed(0)} GB — wahrscheinlich passt es nicht.` : '';
|
||||
const repl = current ? `\n\nErsetzt ${current} — Programme, die es nutzen, verlieren es.` : '';
|
||||
if (!confirm(`${next} auf ${node} · Karte ${gpu} laden?${repl}${warn}\n\nDie anderen Karten dieser Box starten dabei kurz neu.`)) { sel.value = current; return; }
|
||||
send({ action: 'llm_provision_model', targetInstance: target, gpu, key: m.id, hfRepo: m.hfRepo, quant: m.quant, ctx: m.ctx });
|
||||
openLlmDownloads();
|
||||
}
|
||||
|
||||
// ── LLM-Karten ────────────────────────────────────────────────────
|
||||
const SLOT_STATE = {
|
||||
ready: ['#3FFF3F', '● geladen'], starting: ['#FFD60A', '◐ startet …'],
|
||||
|
||||
Reference in New Issue
Block a user