feat(diagnostic): Modell per HuggingFace-Link in den Katalog — mit GGUF-Vorschlaegen
Bisher kamen Modelle nur ueber "Von HuggingFace aktualisieren" und die Trends in den Katalog. Jetzt: Feld "HuggingFace-Link oder org/name". Hat das Repo kein GGUF (z.B. nur safetensors wie Virtue-AI-HUB/VulnLLM-R-7B oder fdtn-ai/Foundation-Sec-8B), sucht der Server GGUF-Fassungen desselben Modells (HF-Suche, filter=gguf, nach Downloads) und bietet sie mit "Diese nehmen" an. Hinzugefuegte stehen danach gefiltert in der Liste. Getestet gegen HuggingFace: beide Beispiele ohne GGUF erkannt, je 5 ladbare GGUF-Fassungen gefunden (Q4_K_M ~4,7–4,9 GB). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -960,6 +960,12 @@
|
||||
den Fortschritt. Danach ist es im Modell-Dropdown oben waehlbar.
|
||||
<span style="color:#FFD60A;">Achte auf den VRAM der Box (Spalte VRAM).</span>
|
||||
</p>
|
||||
<div style="display:flex;gap:6px;flex-wrap:wrap;margin:0 0 8px;">
|
||||
<input id="llm-catalog-add" type="text" placeholder="HuggingFace-Link oder org/name hinzufügen …" onkeydown="if(event.key==='Enter')addCatalogByLink()"
|
||||
style="flex:1;min-width:240px;background:#1E1E2E;border:1px solid #333;border-radius:4px;padding:6px 8px;color:#E0E0F0;font-size:12px;">
|
||||
<button class="btn secondary" onclick="addCatalogByLink()" style="padding:4px 10px;font-size:11px;">Hinzufügen</button>
|
||||
</div>
|
||||
<div id="llm-catalog-add-result" style="font-size:12px;margin:0 0 8px;"></div>
|
||||
<input id="llm-catalog-search" type="search" placeholder="🔍 Modell suchen … (z.B. coder, qwen3, 30b)" oninput="filterLlmCatalog()"
|
||||
style="display:none;width:100%;max-width:360px;box-sizing:border-box;margin:0 0 8px;background:#1E1E2E;border:1px solid #333;border-radius:4px;padding:6px 8px;color:#E0E0F0;font-size:12px;">
|
||||
<div id="llm-catalog-list" style="font-size:12px;color:#8888AA;overflow-x:auto;">(lade Katalog…)</div>
|
||||
@@ -10206,6 +10212,35 @@
|
||||
'<td style="' + td + 'text-align:right;white-space:nowrap;">' + act + '</td></tr>';
|
||||
}).join('') + '</table></div>';
|
||||
}
|
||||
// Modell per Link/Repo in den Katalog. Ohne GGUF: GGUF-Fassungen vorschlagen.
|
||||
async function addCatalogByLink(repo) {
|
||||
const inp = document.getElementById('llm-catalog-add');
|
||||
const out = document.getElementById('llm-catalog-add-result');
|
||||
const val = repo || (inp.value || '').trim();
|
||||
if (!val) return;
|
||||
out.innerHTML = '<span style="color:#8888AA;">Prüfe ' + escapeHtml(val) + ' auf HuggingFace …</span>';
|
||||
try {
|
||||
const j = await (await fetch('/api/llm-catalog/add', { method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ hfRepo: val, manual: true }) })).json();
|
||||
if (j.ok) {
|
||||
_llmCatalog = j.models || _llmCatalog;
|
||||
renderLlmCatalog();
|
||||
inp.value = '';
|
||||
out.innerHTML = '<span style="color:#34C759;">✓ ' + escapeHtml(j.id) + (j.existed ? ' war schon im Katalog' : ' ist jetzt im Katalog') + ' — unten Ziel-Karte wählen und laden.</span>';
|
||||
const s = document.getElementById('llm-catalog-search');
|
||||
if (s) { s.value = j.id; filterLlmCatalog(); }
|
||||
return;
|
||||
}
|
||||
const alts = j.alternatives || [];
|
||||
out.innerHTML = '<span style="color:#FF9F0A;">⚠ ' + escapeHtml(j.error || 'Fehler') + '</span>'
|
||||
+ (alts.length ? '<div style="color:#9a9ab5;margin-top:4px;">GGUF-Fassungen desselben Modells:</div>' + alts.map(a =>
|
||||
`<div style="display:flex;gap:8px;align-items:center;margin-top:3px;">
|
||||
<a href="https://huggingface.co/${escapeHtml(a.repo)}" target="_blank" rel="noopener noreferrer" style="color:#E0E0F0;">${escapeHtml(a.repo)}</a>
|
||||
<span style="color:#555570;">↓ ${Number(a.downloads).toLocaleString('de')}</span>
|
||||
<button class="btn secondary" onclick="addCatalogByLink('${escapeHtmlAttr(a.repo)}')" style="padding:1px 8px;font-size:11px;">Diese nehmen</button></div>`).join('')
|
||||
: '');
|
||||
} catch (e) { out.innerHTML = '<span style="color:#FF6B6B;">✗ ' + escapeHtml(e.message) + '</span>'; }
|
||||
}
|
||||
async function addTrendingToCatalog(btn, repo) {
|
||||
btn.disabled = true; btn.textContent = '…';
|
||||
try {
|
||||
|
||||
+33
-8
@@ -640,17 +640,42 @@ async function discoverGguf(cat, trusted) {
|
||||
}
|
||||
|
||||
// Ein HF-Repo in den Katalog uebernehmen (danach dort wie gewohnt "Laden")
|
||||
async function addHfRepoToCatalog(repo) {
|
||||
if (!/^[\w.-]+\/[\w.-]+$/.test(String(repo || ""))) throw new Error("ungueltiges Repo");
|
||||
// Link oder "org/name" → "org/name" (huggingface.co/org/name/tree/main … geht auch)
|
||||
function hfRepoFromInput(input) {
|
||||
const t = String(input || "").trim().replace(/^https?:\/\/(www\.)?huggingface\.co\//i, "").replace(/[?#].*$/, "");
|
||||
const m = t.match(/^([\w.-]+)\/([\w.-]+)/);
|
||||
return m ? `${m[1]}/${m[2]}` : "";
|
||||
}
|
||||
// Repo ohne GGUF (z.B. nur safetensors): GGUF-Fassungen desselben Modells
|
||||
// auf HuggingFace suchen — meist von mradermacher, bartowski, unsloth & Co.
|
||||
async function hfGgufAlternatives(repo) {
|
||||
const name = String(repo).split("/")[1] || "";
|
||||
if (!name) return [];
|
||||
try {
|
||||
const r = await fetch(`https://huggingface.co/api/models?search=${encodeURIComponent(name)}&filter=gguf&sort=downloads&direction=-1&limit=12`,
|
||||
{ headers: { "User-Agent": "aria-diagnostic" } });
|
||||
if (!r.ok) return [];
|
||||
return (await r.json()).filter(m => m.id && m.id !== repo)
|
||||
.map(m => ({ repo: m.id, downloads: m.downloads || 0, likes: m.likes || 0 })).slice(0, 8);
|
||||
} catch { return []; }
|
||||
}
|
||||
|
||||
async function addHfRepoToCatalog(repo, description = "") {
|
||||
repo = hfRepoFromInput(repo);
|
||||
if (!/^[\w.-]+\/[\w.-]+$/.test(String(repo || ""))) throw new Error("ungueltiges Repo — Link oder org/name");
|
||||
const q = await hfRepoQuant(repo, "Q4_K_M");
|
||||
if (!q) throw new Error("kein ladbares GGUF-Modell in diesem Repo");
|
||||
if (!q) {
|
||||
const err = new Error("In diesem Repo gibt es kein GGUF — das braucht die Box (llama.cpp).");
|
||||
err.alternatives = await hfGgufAlternatives(repo);
|
||||
throw err;
|
||||
}
|
||||
const cat = loadLlmCatalog();
|
||||
let id = slugModelId(repo);
|
||||
const same = cat.find(m => m.hfRepo === repo);
|
||||
if (same) return { models: cat, id: same.id, existed: true };
|
||||
while (cat.some(m => m.id === id)) id += "-2";
|
||||
cat.push({ id, hfRepo: repo, quant: q.quant, ctx: 8192, sizeGB: q.sizeGB, quants: q.quants,
|
||||
description: "Aus den HuggingFace-Trends übernommen", source: "hf" });
|
||||
description: description || "Aus den HuggingFace-Trends übernommen", source: "hf" });
|
||||
saveLlmCatalog(cat);
|
||||
return { models: cat, id, existed: false };
|
||||
}
|
||||
@@ -2483,9 +2508,9 @@ const server = http.createServer((req, res) => {
|
||||
let body = "";
|
||||
req.on("data", c => { body += c; if (body.length > 10000) req.destroy(); });
|
||||
req.on("end", () => {
|
||||
let repo = "";
|
||||
try { repo = JSON.parse(body || "{}").hfRepo; } catch {}
|
||||
addHfRepoToCatalog(repo)
|
||||
let repo = "", manual = false;
|
||||
try { const b = JSON.parse(body || "{}"); repo = b.hfRepo; manual = !!b.manual; } catch {}
|
||||
addHfRepoToCatalog(repo, manual ? "Von Hand hinzugefügt" : "")
|
||||
.then(r => {
|
||||
res.writeHead(200, { "Content-Type": "application/json" });
|
||||
res.end(JSON.stringify({ ok: true, ...r }));
|
||||
@@ -2493,7 +2518,7 @@ const server = http.createServer((req, res) => {
|
||||
})
|
||||
.catch(err => {
|
||||
res.writeHead(200, { "Content-Type": "application/json" });
|
||||
res.end(JSON.stringify({ ok: false, error: err.message }));
|
||||
res.end(JSON.stringify({ ok: false, error: err.message, alternatives: err.alternatives || [] }));
|
||||
});
|
||||
});
|
||||
} else if (req.url === "/api/local-llm-config" && req.method === "GET") {
|
||||
|
||||
Reference in New Issue
Block a user