feat(admin): Modell-Browser (Cloud) mit Tool-Badge, Preisen & $/Antwort
Phase 1 (Cloud) des Modell-Auswahlmenues: - Token je Turn im CostMeter (record_openrouter_cost liest prompt/completion_ tokens) -> globales Turn-Profil (turn_profile, EMA) fuer die $/Antwort-Schaetzung. - app/model_menu.py: gecachter OpenRouter-Katalog -> Chat-Modelle mit tool_supported (supported_parameters), eval_verified (kuratiert), Up/Down-Preisen pro Mio. Token und geschaetzter $/Antwort (Profil x Modellpreise). - GET /api/admin/model-menu; durchsuchbarer Browser im System-Tab, "Verwenden" setzt openrouter_llm_model (Freitext/Preset bleiben). Lokale Modelle (Durchsatz/Strom): Phase 2. Konzept in Ideen/. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
94740daa77
commit
a87dc75a46
5 changed files with 233 additions and 3 deletions
|
|
@ -1785,6 +1785,17 @@ async function loadStatus() {
|
|||
|
||||
${modelCard}
|
||||
|
||||
<!-- Modell-Browser (Cloud/OpenRouter) -->
|
||||
<div class="bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 p-4">
|
||||
<div class="flex items-center justify-between mb-1">
|
||||
<h3 class="font-semibold text-sm">Modell-Browser (Cloud)</h3>
|
||||
<button id="model-menu-load" class="text-xs rounded-lg border border-slate-300 dark:border-slate-600 px-2.5 py-1 hover:bg-slate-50 dark:hover:bg-slate-700/40 transition-colors">Modelle laden</button>
|
||||
</div>
|
||||
<p class="text-xs text-slate-400 mb-2">✅ im Eval bestätigt · 🔧 Tools laut Anbieter · Preise pro Mio. Token (⬆ Eingabe / ⬇ Ausgabe) · ~$/Antwort geschätzt.</p>
|
||||
<input id="model-menu-search" type="text" placeholder="Filtern (Name/ID) …" class="hidden w-full mb-2 rounded-lg border border-slate-300 dark:border-slate-600 bg-transparent px-3 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" />
|
||||
<div id="model-menu-results" class="text-xs"></div>
|
||||
</div>
|
||||
|
||||
<!-- Provider -->
|
||||
<div class="bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 p-4">
|
||||
<h3 class="font-semibold text-sm mb-3">Provider (Standard-Route)</h3>
|
||||
|
|
@ -1817,6 +1828,7 @@ async function loadStatus() {
|
|||
</div>
|
||||
`;
|
||||
wireModelPicker();
|
||||
wireModelBrowser();
|
||||
} catch (e) {
|
||||
console.error("loadStatus error:", e);
|
||||
container.innerHTML = '<p class="text-sm text-red-500">Fehler beim Laden.</p>';
|
||||
|
|
@ -1839,6 +1851,70 @@ function wireModelPicker() {
|
|||
});
|
||||
}
|
||||
|
||||
// Modell-Browser (Cloud): Katalog laden, filtern, ein Modell als openrouter_llm_model setzen.
|
||||
let _modelMenu = [];
|
||||
let _modelCurrent = "";
|
||||
function wireModelBrowser() {
|
||||
const btn = $("#model-menu-load");
|
||||
if (btn) btn.addEventListener("click", loadModelMenu);
|
||||
}
|
||||
|
||||
async function loadModelMenu() {
|
||||
const box = $("#model-menu-results");
|
||||
const search = $("#model-menu-search");
|
||||
if (!box) return;
|
||||
box.innerHTML = '<p class="text-slate-400 py-2">lade …</p>';
|
||||
const data = await adminFetch("/api/admin/model-menu");
|
||||
if (!data) { box.innerHTML = '<p class="text-red-500 py-2">Fehler beim Laden.</p>'; return; }
|
||||
if (!data.available) {
|
||||
box.innerHTML = `<p class="text-red-500 py-2">Katalog nicht verfügbar: ${escHtml(data.error || "unbekannt")}</p>`;
|
||||
return;
|
||||
}
|
||||
_modelMenu = data.models || [];
|
||||
_modelCurrent = data.current || "";
|
||||
const p = data.profile || {};
|
||||
box.dataset.note = `~$/Antwort bei Ø ${p.avg_prompt ?? "?"} Eingabe- + ${p.avg_completion ?? "?"} Ausgabe-Token (${p.measured ? "gemessen" : "Default"}).`;
|
||||
if (search) {
|
||||
search.classList.remove("hidden");
|
||||
search.value = "";
|
||||
search.oninput = () => renderModelMenu(search.value.trim().toLowerCase());
|
||||
}
|
||||
renderModelMenu("");
|
||||
}
|
||||
|
||||
function renderModelMenu(q) {
|
||||
const box = $("#model-menu-results");
|
||||
if (!box) return;
|
||||
const list = q
|
||||
? _modelMenu.filter((m) => (m.id + " " + m.name).toLowerCase().includes(q))
|
||||
: _modelMenu;
|
||||
const fmt = (n) => "$" + (Number(n) || 0);
|
||||
const rows = list.slice(0, 400).map((m) => {
|
||||
const badge = m.eval_verified ? "✅" : (m.tool_supported ? "🔧" : "·");
|
||||
const cur = m.id === _modelCurrent;
|
||||
return `
|
||||
<div class="flex items-center gap-2 py-1 border-b border-slate-100 dark:border-slate-800 ${cur ? "bg-blue-50 dark:bg-blue-900/20" : ""}">
|
||||
<span class="w-4 text-center" title="${m.eval_verified ? "im Eval bestätigt" : m.tool_supported ? "Tools laut Anbieter" : "kein Tool-Support"}">${badge}</span>
|
||||
<span class="flex-1 truncate font-mono" title="${escHtml(m.id)}">${escHtml(m.name)}</span>
|
||||
<span class="w-16 text-right text-slate-500" title="Eingabe / Mio. Token">⬆${fmt(m.price_in_per_mtok)}</span>
|
||||
<span class="w-16 text-right text-slate-500" title="Ausgabe / Mio. Token">⬇${fmt(m.price_out_per_mtok)}</span>
|
||||
<span class="w-20 text-right text-slate-600 dark:text-slate-300" title="geschätzte Kosten pro Antwort">~$${(Number(m.est_cost_per_answer) || 0).toFixed(5)}</span>
|
||||
<button class="btn-use-model text-xs rounded border border-slate-300 dark:border-slate-600 px-2 py-0.5 hover:bg-slate-100 dark:hover:bg-slate-700 ${cur ? "opacity-50" : ""}" data-id="${escHtml(m.id)}">${cur ? "aktiv" : "Verwenden"}</button>
|
||||
</div>`;
|
||||
}).join("");
|
||||
box.innerHTML = `
|
||||
<p class="text-slate-400 mb-1">${escHtml(box.dataset.note || "")} ${list.length} Modelle.</p>
|
||||
<div class="max-h-80 overflow-y-auto">${rows || '<p class="text-slate-400 py-2">keine Treffer</p>'}</div>`;
|
||||
box.querySelectorAll(".btn-use-model").forEach((b) =>
|
||||
b.addEventListener("click", () => useModel(b.dataset.id)));
|
||||
}
|
||||
|
||||
async function useModel(id) {
|
||||
if (!confirm(`Cloud-LLM auf „${id}" setzen?\n(Greift nur, wenn der LLM-Provider „openrouter" ist.)`)) return;
|
||||
const res = await adminFetch("/api/admin/config/openrouter_llm_model", "PUT", { value: id });
|
||||
if (res) { _modelCurrent = id; loadStatus(); }
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════
|
||||
// TAB: WARTUNG (Konfiguration)
|
||||
// ════════════════════════════════════════
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue