feat(diagnostic): Bestenlisten — Ranglisten + HF-Trends direkt in den Katalog
Modell-Katalog → "🏆 Bestenlisten": Fenster mit laufend gepflegten Ranglisten nach Kategorie (Allgemein, Programmieren, Bilder erzeugen, Lokal/Community — Arena, Artificial Analysis, LiveBench, Aider, SWE-bench, Vellum, r/LocalLLaMA, HF), darunter "Gerade im Trend auf HuggingFace" pro Kategorie (Allgemein / Programmieren / Bilder verstehen): nur Repos mit ladbarem GGUF, echte Groesse + Quantisierung, "passt / zu gross" gegen die groesste LLM-Karte, standardmaessig nur bekannte Anbieter (Trend != gut). "In Katalog" uebernimmt das Repo; geladen wird wie gewohnt im Katalog. Server: GET /api/llm-discover (HF trendingScore, 30 min Cache), POST /api/llm-catalog/add. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -554,6 +554,61 @@ async function refreshLlmCatalogFromHF() {
|
||||
return { models: merged, added };
|
||||
}
|
||||
|
||||
// ── Gerade im Trend: ladbare GGUF-Modelle von HuggingFace ───────────
|
||||
// Fuer das Fenster "Bestenlisten" im Modell-Katalog. "Trend" ist nicht
|
||||
// "das Beste" — viele Community-Mischungen. Darum standardmaessig nur
|
||||
// bekannte Anbieter. Ergebnis 30 min gecacht (HF nicht dauernd fragen).
|
||||
const TRUSTED_GGUF_AUTHORS = new Set(["unsloth", "bartowski", "lmstudio-community", "ggml-org", "qwen",
|
||||
"google", "mistralai", "microsoft", "meta-llama", "deepseek-ai", "ibm-granite", "nvidia", "openai",
|
||||
"allenai", "huggingfacetb", "thudm", "zai-org", "moonshotai", "minimaxai", "tencent", "baidu"]);
|
||||
const DISCOVER_CATS = {
|
||||
chat: { label: "Allgemein / Chat", q: "pipeline_tag=text-generation" },
|
||||
code: { label: "Programmieren", q: "search=coder" },
|
||||
vision: { label: "Bilder verstehen", q: "pipeline_tag=image-text-to-text" },
|
||||
};
|
||||
const _discoverCache = new Map(); // key → {ts, items}
|
||||
async function discoverGguf(cat, trusted) {
|
||||
const c = DISCOVER_CATS[cat] || DISCOVER_CATS.chat;
|
||||
const key = `${cat}|${trusted ? 1 : 0}`;
|
||||
const hit = _discoverCache.get(key);
|
||||
if (hit && Date.now() - hit.ts < 30 * 60 * 1000) return hit.items;
|
||||
const r = await fetch(`https://huggingface.co/api/models?library=gguf&sort=trendingScore&direction=-1&limit=100&${c.q}`,
|
||||
{ headers: { "User-Agent": "aria-diagnostic" } });
|
||||
if (!r.ok) throw new Error(`HF API ${r.status}`);
|
||||
let list = (await r.json()).filter(m => m && m.id);
|
||||
if (trusted) list = list.filter(m => TRUSTED_GGUF_AUTHORS.has(String(m.id).split("/")[0].toLowerCase()));
|
||||
list = list.slice(0, 18);
|
||||
const items = [];
|
||||
for (let i = 0; i < list.length; i += 6) {
|
||||
await Promise.all(list.slice(i, i + 6).map(async m => {
|
||||
const q = await hfRepoQuant(m.id, "Q4_K_M");
|
||||
if (!q) return; // kein ladbares GGUF im Repo
|
||||
items.push({ repo: m.id, author: String(m.id).split("/")[0], downloads: m.downloads || 0,
|
||||
likes: m.likes || 0, trending: m.trendingScore || 0,
|
||||
lastModified: m.lastModified || m.createdAt || "", quant: q.quant, sizeGB: q.sizeGB });
|
||||
}));
|
||||
}
|
||||
items.sort((a, b) => b.trending - a.trending);
|
||||
_discoverCache.set(key, { ts: Date.now(), items });
|
||||
return items;
|
||||
}
|
||||
|
||||
// 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");
|
||||
const q = await hfRepoQuant(repo, "Q4_K_M");
|
||||
if (!q) throw new Error("kein ladbares GGUF-Modell in diesem Repo");
|
||||
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" });
|
||||
saveLlmCatalog(cat);
|
||||
return { models: cat, id, existed: false };
|
||||
}
|
||||
|
||||
// ── File-Project-Manifest ───────────────────────────────────────────
|
||||
// Jeder Eintrag map[absoluter_pfad] = project_id (leer = Hauptchat).
|
||||
// Wird vom files-list-Endpoint + files-set-project gepflegt.
|
||||
@@ -2333,6 +2388,35 @@ const server = http.createServer((req, res) => {
|
||||
res.writeHead(200, { "Content-Type": "application/json" });
|
||||
res.end(JSON.stringify({ ok: false, error: err.message, models: loadLlmCatalog() }));
|
||||
});
|
||||
} else if (req.url.startsWith("/api/llm-discover") && req.method === "GET") {
|
||||
const u = new URL(req.url, "http://x");
|
||||
discoverGguf(u.searchParams.get("cat") || "chat", u.searchParams.get("trusted") !== "0")
|
||||
.then(items => {
|
||||
const inCat = new Set(loadLlmCatalog().map(m => m.hfRepo));
|
||||
res.writeHead(200, { "Content-Type": "application/json" });
|
||||
res.end(JSON.stringify({ ok: true, items: items.map(i => ({ ...i, inCatalog: inCat.has(i.repo) })) }));
|
||||
})
|
||||
.catch(err => {
|
||||
res.writeHead(200, { "Content-Type": "application/json" });
|
||||
res.end(JSON.stringify({ ok: false, error: err.message, items: [] }));
|
||||
});
|
||||
} else if (req.url === "/api/llm-catalog/add" && req.method === "POST") {
|
||||
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)
|
||||
.then(r => {
|
||||
res.writeHead(200, { "Content-Type": "application/json" });
|
||||
res.end(JSON.stringify({ ok: true, ...r }));
|
||||
if (!r.existed) log("info", "llm", `Katalog: ${repo} aus den HF-Trends übernommen (${r.id})`);
|
||||
})
|
||||
.catch(err => {
|
||||
res.writeHead(200, { "Content-Type": "application/json" });
|
||||
res.end(JSON.stringify({ ok: false, error: err.message }));
|
||||
});
|
||||
});
|
||||
} else if (req.url === "/api/local-llm-config" && req.method === "GET") {
|
||||
res.writeHead(200, { "Content-Type": "application/json" });
|
||||
res.end(JSON.stringify(readLocalLlmConfig()));
|
||||
|
||||
Reference in New Issue
Block a user