feat(diagnostic): Katalog zeigt, ob ein Modell Werkzeuge kann (🔧) + Filter
Erkennung: Chat-Vorlage des GGUF (HF expand[]=gguf) kennt ein Werkzeug- Format, oder Schlagworte wie function-calling/tool-use. Zeigt, OB das Format da ist — nicht, wie gut das Modell damit umgeht. - Neue und hinzugefuegte Eintraege bekommen tools: true/false/null, bestehende beim naechsten "Von HuggingFace aktualisieren". - Liste: 🔧-Abzeichen; Haken "nur Modelle mit Werkzeugen" (Standard an) blendet Modelle ohne Werkzeug-Format aus (unbekannt bleibt sichtbar). - ℹ: Zeile "Werkzeuge: ja / nein / unbekannt". Getestet gegen HuggingFace (7 Modelle, u.a. Hermes 3 ueber das Schlagwort, weil dessen offizielle Vorlage minimal ist). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+10
-2
@@ -966,6 +966,8 @@
|
||||
<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>
|
||||
<label style="display:inline-flex;gap:5px;align-items:center;font-size:11px;color:#9a9ab5;margin:0 0 6px;cursor:pointer;" title="Modelle ohne Werkzeug-Format ausblenden (z.B. für Übersetzen/Zusammenfassen reichen auch die)">
|
||||
<input type="checkbox" id="llm-catalog-tools" checked onchange="filterLlmCatalog()"> nur Modelle mit Werkzeugen 🔧</label>
|
||||
<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>
|
||||
@@ -9737,7 +9739,11 @@
|
||||
const q = (document.getElementById('llm-catalog-search') || {}).value || '';
|
||||
const rows = document.querySelectorAll('#llm-catalog-list tbody tr[data-search]');
|
||||
let shown = 0;
|
||||
rows.forEach(tr => { const ok = _llmMatches(tr.dataset.search, q); tr.style.display = ok ? '' : 'none'; if (ok) shown++; });
|
||||
const onlyTools = (document.getElementById('llm-catalog-tools') || {}).checked;
|
||||
rows.forEach(tr => {
|
||||
const ok = _llmMatches(tr.dataset.search, q) && !(onlyTools && tr.dataset.tools === 'nein');
|
||||
tr.style.display = ok ? '' : 'none'; if (ok) shown++;
|
||||
});
|
||||
const none = document.getElementById('llm-catalog-none');
|
||||
if (none) none.style.display = rows.length && !shown ? '' : 'none';
|
||||
}
|
||||
@@ -9822,8 +9828,9 @@
|
||||
? targets.map(t => `<option value="${escapeHtml(t.value)}"${t.value === pre ? ' selected' : ''}>${escapeHtml(t.label)}</option>`).join('')
|
||||
: '<option value="">(keine Box online)</option>';
|
||||
const searchText = [m.id, m.description, m.hfRepo, m.quant].filter(Boolean).join(' ');
|
||||
return '<tr data-search="' + escapeHtml(searchText) + '" style="border-bottom:1px solid #1E1E2E;">' +
|
||||
return '<tr data-search="' + escapeHtml(searchText) + '" data-tools="' + (m.tools === false ? 'nein' : 'ja') + '" style="border-bottom:1px solid #1E1E2E;">' +
|
||||
'<td ' + td + 'white-space:nowrap;"><b>' + escapeHtml(m.id) + '</b>'
|
||||
+ (m.tools ? ' <span title="Kann Werkzeuge aufrufen (laut Chat-Vorlage) — z.B. Websuche, wenn ARIA es lokal nutzt" style="font-size:10px;color:#34C759;border:1px solid #34C759;border-radius:8px;padding:0 5px;">🔧</span>' : '')
|
||||
+ (m.uncensored ? ' <span title="Unzensierte Fassung — lehnt kaum etwas ab" style="font-size:10px;color:#FFB020;border:1px solid #FFB020;border-radius:8px;padding:0 5px;">unzensiert</span>' : '')
|
||||
+ (m.added_at && (Date.now() - new Date(m.added_at).getTime()) < 7 * 864e5 ? ' <span title="In den letzten 7 Tagen in den Katalog gekommen" style="font-size:10px;color:#4DB8FF;border:1px solid #4DB8FF;border-radius:8px;padding:0 5px;">neu</span>' : '')
|
||||
+ (m.hfRepo ? ' <button class="btn secondary" onclick="openModelInfo(' + i + ')" title="Was kann das Modell? Zensiert oder nicht?" style="padding:0 6px;font-size:11px;margin-left:4px;">ℹ</button>' : '')
|
||||
@@ -10272,6 +10279,7 @@
|
||||
<div id="model-info-summary" style="margin-bottom:10px;color:#8888AA;">Kurzbeschreibung wird vom lokalen Modell aus der Modellkarte erstellt …</div>
|
||||
${fam}
|
||||
<table style="font-size:12px;margin-top:8px;">
|
||||
${row('Werkzeuge', j.tools === true ? '🔧 ja — die Chat-Vorlage kennt Werkzeug-Aufrufe' : j.tools === false ? 'nein — eher für Übersetzen, Zusammenfassen, Erklären' : 'unbekannt (keine Chat-Vorlage)')}
|
||||
${row('Größe', m.sizeGB ? '~' + m.sizeGB + ' GB · ' + escapeHtml(m.quant || '') : '')}
|
||||
${row('Grundmodell', j.base ? escapeHtml(j.base) : '')}
|
||||
${row('Von', escapeHtml(j.author))}
|
||||
|
||||
+23
-1
@@ -573,6 +573,11 @@ async function refreshLlmCatalogFromHF() {
|
||||
e.quant = q.quant; e.sizeGB = q.sizeGB; e.quants = q.quants; delete e.unusable;
|
||||
}));
|
||||
}
|
||||
// Werkzeug-Faehigkeit nachtragen (alle Eintraege, die noch keine Angabe haben)
|
||||
const noTools = [...byId.values()].filter(e => e.hfRepo && e.tools === undefined);
|
||||
for (let i = 0; i < noTools.length; i += 6) {
|
||||
await Promise.all(noTools.slice(i, i + 6).map(async e => { e.tools = await hfToolSupport(e.hfRepo); }));
|
||||
}
|
||||
let added = 0;
|
||||
for (const e of byId.values()) { if (e._new) { added++; delete e._new; } }
|
||||
// Aufraeumen: von HF uebernommen, vor > LLM_CATALOG_KEEP_DAYS Tagen, auf
|
||||
@@ -821,12 +826,27 @@ async function llmModelInfo(repo) {
|
||||
created: meta.createdAt || "", updated: meta.lastModified || "",
|
||||
tags: tags.slice(0, 20), families,
|
||||
uncensored: !!unc,
|
||||
tools: await hfToolSupport(repo),
|
||||
uncensoredWhy: unc ? `erkannt an „${unc[0]}“ in Name/Schlagworten` : "",
|
||||
intro: await hfReadmeIntro(base || repo) || await hfReadmeIntro(repo),
|
||||
introFrom: base || repo,
|
||||
};
|
||||
}
|
||||
|
||||
// Kann das Modell Werkzeuge? Kennzeichen: die Chat-Vorlage im GGUF (HF liefert
|
||||
// sie unter expand[]=gguf) kennt ein Werkzeug-Format, oder Schlagworte wie
|
||||
// function-calling/tool-use. Zeigt nur, OB das Format da ist, nicht wie gut.
|
||||
const TOOLS_TEMPLATE_RE = /\btools\b|tool_call|<tool|function_call/;
|
||||
const TOOLS_TAG_RE = /function.?call|tool.?use|tool.?calling|agent/i;
|
||||
async function hfToolSupport(repo) {
|
||||
const d = await hfJson(`https://huggingface.co/api/models/${repo}?expand[]=gguf&expand[]=tags`);
|
||||
if (!d) return null;
|
||||
const tpl = String(((d.gguf || {}).chat_template) || "");
|
||||
if (TOOLS_TEMPLATE_RE.test(tpl)) return true;
|
||||
if ((d.tags || []).some(t => TOOLS_TAG_RE.test(t))) return true;
|
||||
return tpl ? false : null; // keine Vorlage = unbekannt
|
||||
}
|
||||
|
||||
async function addHfRepoToCatalog(repo, description = "") {
|
||||
repo = hfRepoFromInput(repo);
|
||||
if (!/^[\w.-]+\/[\w.-]+$/.test(String(repo || ""))) throw new Error("ungueltiges Repo — Link oder org/name");
|
||||
@@ -842,7 +862,9 @@ async function addHfRepoToCatalog(repo, description = "") {
|
||||
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: description || "Aus den HuggingFace-Trends übernommen", source: "hf" });
|
||||
description: description || "Aus den HuggingFace-Trends übernommen", source: "hf",
|
||||
tools: await hfToolSupport(repo), uncensored: UNCENSORED_RE.test(repo),
|
||||
added_at: new Date().toISOString().slice(0, 10) });
|
||||
saveLlmCatalog(cat);
|
||||
return { models: cat, id, existed: false };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user