feat(diagnostic): ℹ im Modell-Katalog — was kann das Modell, ist es zensiert?
Neben jedem Katalog-Eintrag ℹ → Fenster mit: - Kurzbeschreibung auf Deutsch, beim ersten Oeffnen vom LOKALEN Modell aus der Modellkarte (Grundmodell + GGUF-Repo) erzeugt und im Katalog gespeichert (summary_de) — so hat jedes neue Modell eine, ohne Pflege. Hinweis "kann Fehler enthalten". Laeuft kein Modell: Meldung. - Feste deutsche Texte je Modellfamilie (Qwen3-Coder, Foundation-Sec, VulnLLM, Dolphin, Hermes, Lexi, Gemma, Qwen, Llama, Mistral, DeepSeek, Phi), erkannt als ganzes Wort in Name/Grundmodell/Schlagworten. - "unzensiert" / "mit Schutzfiltern" (uncensored/abliterated/heretic/ dolphin/lexi … in Name oder Schlagworten). - Fakten von HF: Grundmodell, Autor, Lizenz, Beliebtheit, Stand, Schlagworte; Auszug der Modellkarte (Link-Absaetze uebersprungen). Server: /api/llm-model-info, /api/llm-model-summary, rvsRequest() als Promise-Variante fuer eigene RVS-Anfragen. Getestet: Info-Teil gegen HuggingFace (4 Modelle). Die Zusammenfassung durch das lokale Modell ungetestet (Karte gerade im Gegenlauf belegt). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -660,6 +660,121 @@ async function hfGgufAlternatives(repo) {
|
||||
} catch { return []; }
|
||||
}
|
||||
|
||||
// ── ℹ im Modell-Katalog: was kann das Modell, ist es zensiert? ──────
|
||||
// HuggingFace liefert Grundmodell, Lizenz und Schlagworte, aber keine
|
||||
// verstaendliche Beschreibung. Darum: kurze deutsche Texte je Modellfamilie
|
||||
// (erkannt am Namen) + die Fakten von HF + Anfang der Modellkarte.
|
||||
const MODEL_FAMILIES = [
|
||||
["qwen3-coder", "Qwen3-Coder (Alibaba)", "Aufs Programmieren spezialisiert: Code schreiben, verstehen, umbauen, mit Werkzeugen arbeiten (Agenten). Die 30B-A3B-Fassung ist ein MoE-Modell: pro Wort rechnen nur ~3 Mrd. Parameter — schnell für seine Größe."],
|
||||
["foundation-sec", "Foundation-Sec (Cisco Foundation AI)", "Auf IT-Sicherheit trainiert: Schwachstellen, Angriffstechniken (MITRE ATT&CK), Logs und Warnmeldungen auswerten. Die reine „Foundation-Sec-8B“ ist ein Basismodell (setzt nur Text fort) — zum Chatten die Instruct- oder Reasoning-Fassung nehmen."],
|
||||
["vulnllm", "VulnLLM-R", "Sucht Sicherheitslücken in Quellcode und begründet Schritt für Schritt (Reasoning), warum eine Stelle verwundbar ist."],
|
||||
["dolphin", "Dolphin (Cognitive Computations)", "Bewusst ohne Ablehnungen trainiert: macht, was man verlangt, und lässt sich stark über den Systemprompt steuern. Gut für Aufgaben, die große Anbieter verweigern — man ist aber selbst verantwortlich, was man damit tut."],
|
||||
["hermes", "Hermes (Nous Research)", "Sehr gutes Befolgen von Anweisungen, Rollenspiel und Werkzeug-Aufrufe (Function Calling), wenige Ablehnungen. Hermes 4 kann zusätzlich „denken“ (Reasoning)."],
|
||||
["lexi", "Lexi (Orenguteng)", "Unzensierter Llama-Ableger für allgemeinen Chat, beantwortet auch Fragen, die das Original ablehnt."],
|
||||
["gemma", "Gemma (Google)", "Allround-Modell mit gutem Deutsch; neuere Fassungen verstehen auch Bilder. Eher vorsichtig, lehnt heikle Themen ab."],
|
||||
["qwen", "Qwen (Alibaba)", "Starke Allround-Familie: Wissen, Logik, Mathe, Code, viele Sprachen inkl. Deutsch; neuere Fassungen mit Denkmodus. Standard-Fassungen haben Schutzfilter."],
|
||||
["llama", "Llama (Meta)", "Bewährtes Allround-Modell und Basis für sehr viele Ableger; ordentliches Deutsch, solide Werkzeug-Aufrufe."],
|
||||
["mistral", "Mistral (Mistral AI, Frankreich)", "Gut in europäischen Sprachen, effizient; Mistral Small 24B ist für seine Größe sehr stark."],
|
||||
["deepseek", "DeepSeek", "Stark in Logik, Mathe und Code, oft mit ausführlichem Denkweg (Reasoning)."],
|
||||
["phi", "Phi (Microsoft)", "Klein und schnell, erstaunlich gut in Logik für die Größe; eher Englisch."],
|
||||
];
|
||||
const UNCENSORED_RE = /uncensor|abliterat|heretic|obliterat|dolphin|lexi|unfiltered|nsfw|no.?refus/i;
|
||||
async function hfJson(url) {
|
||||
try {
|
||||
const r = await fetch(url, { headers: { "User-Agent": "aria-diagnostic" } });
|
||||
return r.ok ? await r.json() : null;
|
||||
} catch { return null; }
|
||||
}
|
||||
async function hfReadmeIntro(repo) {
|
||||
try {
|
||||
const r = await fetch(`https://huggingface.co/${repo}/raw/main/README.md`, { headers: { "User-Agent": "aria-diagnostic" } });
|
||||
if (!r.ok) return "";
|
||||
let t = await r.text();
|
||||
t = t.replace(/^---[\s\S]*?\n---\n/, ""); // YAML-Kopf
|
||||
const paras = t.split(/\n\s*\n/).map(p => p.trim())
|
||||
.filter(p => p && !/^(#|!\[|<|\||```|\[!\[|>|-{3,})/.test(p) && p.replace(/[^A-Za-z]/g, "").length > 60)
|
||||
// Absaetze, die fast nur Links sind (Website:, Twitter: …), ueberspringen
|
||||
.filter(p => p.replace(/https?:\/\/\S+/g, "").replace(/[^A-Za-z]/g, "").length > 80);
|
||||
return paras.slice(0, 2).join("\n\n").replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").slice(0, 900);
|
||||
} catch { return ""; }
|
||||
}
|
||||
// Ganze Modellkarte als Text (ohne YAML/Bilder/HTML), fuer die Zusammenfassung
|
||||
async function hfReadmeText(repo, max = 7000) {
|
||||
try {
|
||||
const r = await fetch(`https://huggingface.co/${repo}/raw/main/README.md`, { headers: { "User-Agent": "aria-diagnostic" } });
|
||||
if (!r.ok) return "";
|
||||
return (await r.text()).replace(/^---[\s\S]*?\n---\n/, "").replace(/!\[[^\]]*\]\([^)]*\)/g, "")
|
||||
.replace(/<[^>]+>/g, "").replace(/\n{3,}/g, "\n\n").slice(0, max);
|
||||
} catch { return ""; }
|
||||
}
|
||||
// Deutsche Kurzbeschreibung vom LOKALEN Modell aus der Modellkarte — einmal
|
||||
// erzeugt, dann im Katalog gespeichert (summary_de). So hat jedes neue Modell
|
||||
// eine, ohne dass jemand Texte pflegt.
|
||||
async function llmModelSummary(repo) {
|
||||
repo = hfRepoFromInput(repo);
|
||||
const cat = loadLlmCatalog();
|
||||
const entry = cat.find(m => m.hfRepo === repo);
|
||||
if (entry && entry.summary_de) return { summary: entry.summary_de, by: entry.summary_by || "", cached: true };
|
||||
const meta = await hfJson(`https://huggingface.co/api/models/${repo}`) || {};
|
||||
let base = (meta.cardData || {}).base_model;
|
||||
base = Array.isArray(base) ? base[0] : base || "";
|
||||
const card = [await hfReadmeText(base) , await hfReadmeText(repo, 3000)].filter(Boolean).join("\n\n---\n\n");
|
||||
if (!card.trim()) return { summary: "", error: "Keine Modellkarte auf HuggingFace." };
|
||||
const tags = (meta.tags || []).filter(t => !/^(base_model|license|region|endpoints)/.test(t)).slice(0, 25).join(", ");
|
||||
const res = await rvsRequest("llm_request", {
|
||||
requestId: "modelinfo_" + Date.now(),
|
||||
messages: [
|
||||
{ role: "system", content: "Du fasst Modellkarten von KI-Sprachmodellen fuer einen Laien auf Deutsch zusammen. "
|
||||
+ "Nur was in der Karte steht, nichts erfinden. 3 bis 5 kurze Saetze: wofuer das Modell gedacht ist, worin es "
|
||||
+ "besonders gut ist, ob es Ablehnungen/Schutzfilter entfernt hat (uncensored/abliterated), und Besonderheiten "
|
||||
+ "(Denkmodus, Werkzeuge, Sprachen, Groesse). Keine Ueberschrift, kein Markdown." },
|
||||
{ role: "user", content: `Modell: ${repo}\nGrundmodell: ${base || "-"}\nSchlagworte: ${tags}\n\nModellkarte:\n${card}` },
|
||||
],
|
||||
max_tokens: 400, temperature: 0.2, model: "", targetInstance: "",
|
||||
}, "llm_response", 180000);
|
||||
const text = String(res.content || res.text || "").replace(/<think>[\s\S]*?<\/think>/g, "").trim();
|
||||
if (!res.ok || !text) return { summary: "", error: res.error || "Das lokale Modell hat nicht geantwortet." };
|
||||
const by = res.model || "lokales Modell";
|
||||
if (entry) {
|
||||
entry.summary_de = text; entry.summary_by = by; entry.summary_at = new Date().toISOString();
|
||||
saveLlmCatalog(cat);
|
||||
}
|
||||
return { summary: text, by, cached: false };
|
||||
}
|
||||
|
||||
async function llmModelInfo(repo) {
|
||||
repo = hfRepoFromInput(repo);
|
||||
if (!repo) throw new Error("ungueltiges Repo");
|
||||
const meta = await hfJson(`https://huggingface.co/api/models/${repo}`) || {};
|
||||
const card = meta.cardData || {};
|
||||
let base = card.base_model;
|
||||
base = Array.isArray(base) ? base[0] : base || "";
|
||||
const baseMeta = base ? (await hfJson(`https://huggingface.co/api/models/${base}`) || {}) : {};
|
||||
const tags = [...new Set([...(meta.tags || []), ...((card.tags) || [])])]
|
||||
.filter(t => !/^(base_model|license|region|endpoints|autotrain|text-generation-inference|safetensors|transformers|gguf|conversational|imatrix)/.test(t));
|
||||
const hay = [repo, base, ...tags].join(" ").toLowerCase();
|
||||
const fams = [];
|
||||
for (const [key, name, text] of MODEL_FAMILIES) {
|
||||
// Ganzes Wort am Anfang ("phi" nicht in "dolphin" finden)
|
||||
const re = new RegExp("(^|[^a-z0-9])" + key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
||||
if (name && re.test(hay) && !fams.some(f => f.name === name)) fams.push({ name, text });
|
||||
}
|
||||
// Allgemeine Familien (Qwen/Llama …) nur, wenn keine speziellere passt
|
||||
const special = fams.filter(f => !/^(Qwen|Llama|Mistral|Gemma) \(/.test(f.name));
|
||||
const families = special.length ? [...special, ...fams.filter(f => !special.includes(f)).slice(0, 1)] : fams.slice(0, 1);
|
||||
const unc = UNCENSORED_RE.exec(hay);
|
||||
return {
|
||||
repo, base, author: meta.author || repo.split("/")[0], license: card.license || "",
|
||||
downloads: meta.downloads || 0, likes: meta.likes || 0,
|
||||
created: meta.createdAt || "", updated: meta.lastModified || "",
|
||||
tags: tags.slice(0, 20), families,
|
||||
uncensored: !!unc,
|
||||
uncensoredWhy: unc ? `erkannt an „${unc[0]}“ in Name/Schlagworten` : "",
|
||||
intro: await hfReadmeIntro(base || repo) || await hfReadmeIntro(repo),
|
||||
introFrom: base || repo,
|
||||
};
|
||||
}
|
||||
|
||||
async function addHfRepoToCatalog(repo, description = "") {
|
||||
repo = hfRepoFromInput(repo);
|
||||
if (!/^[\w.-]+\/[\w.-]+$/.test(String(repo || ""))) throw new Error("ungueltiges Repo — Link oder org/name");
|
||||
@@ -1689,6 +1804,26 @@ function sendToRVS_withResponse(sendType, sendPayload, expectType, clientWs, tim
|
||||
freshWs.on("error", () => {});
|
||||
}
|
||||
|
||||
// Wie sendToRVS_withResponse, aber als Promise fuer den Server selbst:
|
||||
// eigene Verbindung, wartet auf expectType mit passender requestId.
|
||||
function rvsRequest(sendType, sendPayload, expectType, timeoutMs = 60000) {
|
||||
return new Promise((resolve) => {
|
||||
if (!RVS_HOST || !RVS_TOKEN) return resolve({ ok: false, error: "RVS nicht konfiguriert" });
|
||||
const proto = RVS_TLS === "true" ? "wss" : "ws";
|
||||
const ws2 = new WebSocket(`${proto}://${RVS_HOST}:${RVS_PORT}?token=${RVS_TOKEN}`);
|
||||
const done = (v) => { clearTimeout(t); try { ws2.close(); } catch (_) {} resolve(v); };
|
||||
const t = setTimeout(() => done({ ok: false, error: "Zeitüberschreitung" }), timeoutMs);
|
||||
ws2.on("open", () => ws2.send(JSON.stringify({ type: sendType, payload: sendPayload, timestamp: Date.now() })));
|
||||
ws2.on("message", (raw) => {
|
||||
try {
|
||||
const r = JSON.parse(raw.toString());
|
||||
if (r.type === expectType && (r.payload || {}).requestId === sendPayload.requestId) done(r.payload || {});
|
||||
} catch {}
|
||||
});
|
||||
ws2.on("error", (e) => done({ ok: false, error: e.message }));
|
||||
});
|
||||
}
|
||||
|
||||
function sendToRVS_raw(msgObj) {
|
||||
if (!RVS_HOST || !RVS_TOKEN) return;
|
||||
const payload = JSON.stringify(msgObj);
|
||||
@@ -2504,6 +2639,16 @@ const server = http.createServer((req, res) => {
|
||||
res.writeHead(200, { "Content-Type": "application/json" });
|
||||
res.end(JSON.stringify({ ok: false, error: err.message, items: [] }));
|
||||
});
|
||||
} else if (req.url.startsWith("/api/llm-model-info") && req.method === "GET") {
|
||||
const repo = new URL(req.url, "http://x").searchParams.get("repo") || "";
|
||||
llmModelInfo(repo)
|
||||
.then(info => { res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify({ ok: true, ...info })); })
|
||||
.catch(err => { res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify({ ok: false, error: err.message })); });
|
||||
} else if (req.url.startsWith("/api/llm-model-summary") && req.method === "GET") {
|
||||
const repo = new URL(req.url, "http://x").searchParams.get("repo") || "";
|
||||
llmModelSummary(repo)
|
||||
.then(r => { res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify({ ok: !!r.summary, ...r })); })
|
||||
.catch(err => { res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify({ ok: false, error: err.message })); });
|
||||
} 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(); });
|
||||
|
||||
Reference in New Issue
Block a user