feat: leere Rollen ehrlich anzeigen + Vision-cmd automatisch (mmproj/jinja)
- status(): Flag "incomplete" fuer Rollen ohne Modell (-m), z.B. provision.sh- Platzhalter. Wurden bisher irrefuehrend als "bereit / ? . 8K . ?" gezeigt. - Modelle-Tab: leere Rollen klar als "kein Modell hinterlegt / leer" + Aktion "Modell zuweisen" (-> Cookbook); nicht im Schnelltest-Dropdown waehlbar - register(): erkennt einen mmproj-Projektor im selben Ordner und ergaenzt --mmproj + --jinja automatisch (Vision braucht das in llama.cpp) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -98,11 +98,15 @@ def status():
|
|||||||
if "--mmproj" in cmd:
|
if "--mmproj" in cmd:
|
||||||
caps.append("Bild")
|
caps.append("Bild")
|
||||||
|
|
||||||
|
# "incomplete" = Rolle existiert in der config, hat aber kein Modell (-m) hinterlegt
|
||||||
|
# (z.B. von provision.sh angelegter Platzhalter). Ehrlich kennzeichnen statt "bereit".
|
||||||
|
incomplete = not (m_path and m_path.group(1))
|
||||||
configured[name] = {
|
configured[name] = {
|
||||||
"name": name,
|
"name": name,
|
||||||
"ttl": spec.get("ttl", cfg.get("globalTTL", 0)),
|
"ttl": spec.get("ttl", cfg.get("globalTTL", 0)),
|
||||||
"cmd": cmd,
|
"cmd": cmd,
|
||||||
"state": "idle",
|
"state": "idle",
|
||||||
|
"incomplete": incomplete,
|
||||||
"port": None,
|
"port": None,
|
||||||
"meta": {
|
"meta": {
|
||||||
"ctx": ctx,
|
"ctx": ctx,
|
||||||
@@ -157,12 +161,29 @@ def download(req: DownloadReq):
|
|||||||
return {"job_id": job_id, "expected_path": str(target / req.file)}
|
return {"job_id": job_id, "expected_path": str(target / req.file)}
|
||||||
|
|
||||||
|
|
||||||
|
def _augment_vision(cmd: str, model_path: str) -> str:
|
||||||
|
"""Liegt im selben Ordner ein mmproj-Projektor, --mmproj + --jinja ergaenzen —
|
||||||
|
Vision-Modelle brauchen das in llama.cpp (siehe CLAUDE.md). No-op, wenn schon gesetzt
|
||||||
|
oder kein Projektor da."""
|
||||||
|
if "--mmproj" in cmd:
|
||||||
|
return cmd
|
||||||
|
try:
|
||||||
|
d = os.path.dirname(model_path)
|
||||||
|
for f in sorted(os.listdir(d)):
|
||||||
|
if "mmproj" in f.lower() and f.lower().endswith(".gguf"):
|
||||||
|
return cmd.rstrip() + f" --mmproj {os.path.join(d, f)} --jinja"
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
return cmd
|
||||||
|
|
||||||
|
|
||||||
@router.post("/register")
|
@router.post("/register")
|
||||||
def register(req: RegisterReq):
|
def register(req: RegisterReq):
|
||||||
# Bewusst KEIN exists()-Check: beim frischen Download läuft der hf-Job noch, die Datei kommt
|
# Bewusst KEIN exists()-Check: beim frischen Download läuft der hf-Job noch, die Datei kommt
|
||||||
# erst gleich. Eintrag jetzt schon schreiben → llama-swap (-watch-config) lädt, sobald sie da ist.
|
# erst gleich. Eintrag jetzt schon schreiben → llama-swap (-watch-config) lädt, sobald sie da ist.
|
||||||
cfg = read_config()
|
cfg = read_config()
|
||||||
cmd = CMD_TEMPLATE.replace("{model}", req.model_path).replace("{ctx}", str(req.ctx))
|
cmd = CMD_TEMPLATE.replace("{model}", req.model_path).replace("{ctx}", str(req.ctx))
|
||||||
|
cmd = _augment_vision(cmd, req.model_path)
|
||||||
cfg["models"][req.alias] = {
|
cfg["models"][req.alias] = {
|
||||||
"cmd": LiteralScalarString(cmd + "\n"),
|
"cmd": LiteralScalarString(cmd + "\n"),
|
||||||
"ttl": req.ttl if req.ttl is not None else DEFAULT_TTL,
|
"ttl": req.ttl if req.ttl is not None else DEFAULT_TTL,
|
||||||
|
|||||||
@@ -81,6 +81,19 @@ function onStatus(s) {
|
|||||||
|
|
||||||
const sel = $("#chat-model"), cur = sel.value; sel.innerHTML = "";
|
const sel = $("#chat-model"), cur = sel.value; sel.innerHTML = "";
|
||||||
tb.innerHTML = ALL.map(m => {
|
tb.innerHTML = ALL.map(m => {
|
||||||
|
if (m.incomplete) {
|
||||||
|
// Leere Rolle (Platzhalter ohne Modell-Datei) — ehrlich kennzeichnen statt „bereit".
|
||||||
|
return `<tr style="opacity:.9">
|
||||||
|
<td class="mid" style="font-weight:500">${esc(m.name)}<div class="li-sub mono-sm" style="color:var(--warn)">kein Modell hinterlegt</div></td>
|
||||||
|
<td>—</td>
|
||||||
|
<td><span class="mono-sm text-mut">leere Rolle</span></td>
|
||||||
|
<td><span class="badge">leer</span></td>
|
||||||
|
<td class="port">—</td>
|
||||||
|
<td style="text-align:right;white-space:nowrap">
|
||||||
|
<button class="ghost" data-assign="${esc(m.name)}">Modell zuweisen</button>
|
||||||
|
<button class="ghost del" data-del="${esc(m.name)}">Löschen</button>
|
||||||
|
</td></tr>`;
|
||||||
|
}
|
||||||
const fn = m.meta?.filename ? `<div class="li-sub mono-sm">${esc(m.meta.filename)}</div>` : "";
|
const fn = m.meta?.filename ? `<div class="li-sub mono-sm">${esc(m.meta.filename)}</div>` : "";
|
||||||
return `<tr>
|
return `<tr>
|
||||||
<td class="mid" style="font-weight:500">${esc(m.name)}${fn}</td>
|
<td class="mid" style="font-weight:500">${esc(m.name)}${fn}</td>
|
||||||
@@ -94,12 +107,16 @@ function onStatus(s) {
|
|||||||
<button class="ghost del" data-del="${esc(m.name)}">Löschen</button>
|
<button class="ghost del" data-del="${esc(m.name)}">Löschen</button>
|
||||||
</td></tr>`;
|
</td></tr>`;
|
||||||
}).join("");
|
}).join("");
|
||||||
for (const m of ALL) sel.insertAdjacentHTML("beforeend", `<option>${esc(m.name)}</option>`);
|
for (const m of ALL) if (!m.incomplete) sel.insertAdjacentHTML("beforeend", `<option>${esc(m.name)}</option>`);
|
||||||
if (cur) sel.value = cur;
|
if (cur) sel.value = cur;
|
||||||
|
|
||||||
tb.querySelectorAll("[data-unload]").forEach(b => b.addEventListener("click", () => unloadOne(b.getAttribute("data-unload"))));
|
tb.querySelectorAll("[data-unload]").forEach(b => b.addEventListener("click", () => unloadOne(b.getAttribute("data-unload"))));
|
||||||
tb.querySelectorAll("[data-cfg]").forEach(b => b.addEventListener("click", () => openConfig(b.getAttribute("data-cfg"))));
|
tb.querySelectorAll("[data-cfg]").forEach(b => b.addEventListener("click", () => openConfig(b.getAttribute("data-cfg"))));
|
||||||
tb.querySelectorAll("[data-del]").forEach(b => b.addEventListener("click", () => deleteModel(b.getAttribute("data-del"))));
|
tb.querySelectorAll("[data-del]").forEach(b => b.addEventListener("click", () => deleteModel(b.getAttribute("data-del"))));
|
||||||
|
tb.querySelectorAll("[data-assign]").forEach(b => b.addEventListener("click", () => {
|
||||||
|
toast("Lade im Cookbook ein Modell und setze den Alias auf: " + b.getAttribute("data-assign"));
|
||||||
|
document.querySelector(".nav-item[data-view='cookbook']")?.click();
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteModel(m) {
|
async function deleteModel(m) {
|
||||||
|
|||||||
Reference in New Issue
Block a user