diff --git a/routers/cookbook.py b/routers/cookbook.py index e02b963..5500a3d 100644 --- a/routers/cookbook.py +++ b/routers/cookbook.py @@ -273,9 +273,14 @@ def install_recipe(req: InstallRecipeReq): file = _pick_gguf(m["repo"], m.get("quant", "Q4_K_M")) if not file: continue # kein GGUF im Repo gefunden -> Modell ueberspringen (Rest installiert trotzdem) + # Vision-Modelle: mmproj-Projektor mitholen (nötig für --mmproj in llama.cpp). + mmproj = _pick_mmproj(m["repo"]) target = MODELS_DIR / m["repo"].split("/")[-1] target.mkdir(parents=True, exist_ok=True) - args = [hf_bin(), "download", m["repo"], file, "--local-dir", str(target)] + args = [hf_bin(), "download", m["repo"], file] + if mmproj: + args.append(mmproj) # hf-cli unterstützt mehrere Dateien in einem Aufruf + args += ["--local-dir", str(target)] jid = start_job(args, f"download {m['name']}", env=env) JOBS[jid]["result_path"] = str(target / file) attach_download_progress(jid, str(target), hf_file_size(m["repo"], file)) @@ -284,6 +289,8 @@ def install_recipe(req: InstallRecipeReq): ctx = max_ctx_for(m["params_b"], m["quant"], ram_gb) path = str(target / file) cmd = CMD_TEMPLATE.replace("{model}", path).replace("{ctx}", str(ctx)) + if mmproj: + cmd = cmd.rstrip() + f" --mmproj {target / mmproj} --jinja" # Schluessel = sprechender Modellname; Rolle (aus dem Rezept) als Alias. mid = model_id_from_path(path) cfg["models"][mid] = {"cmd": LiteralScalarString(cmd + "\n"), "ttl": DEFAULT_TTL} @@ -321,6 +328,26 @@ def _pick_gguf(repo: str, quant: str = "Q4_K_M") -> str | None: return (pref or nosplit or ggufs)[0] +def _pick_mmproj(repo: str) -> str | None: + """Sucht den mmproj-Projektor (Vision-Modelle) im Repo — F16/BF16 bevorzugt. + Gibt None zurueck, wenn kein Projektor vorhanden (= kein Vision-Modell).""" + try: + with httpx.Client(timeout=10.0) as c: + tree = c.get(f"https://huggingface.co/api/models/{repo}/tree/main").json() + except Exception: # noqa: BLE001 + return None + candidates = [ + f["path"] for f in tree + if isinstance(f, dict) and "mmproj" in str(f.get("path", "")).lower() + and str(f.get("path", "")).endswith(".gguf") + ] + if not candidates: + return None + # F16/BF16 hat die beste Projektor-Qualität; nimm das erste wenn keins passt. + pref = [c for c in candidates if "f16" in c.lower() or "bf16" in c.lower()] + return (pref or candidates)[0] + + # --------------------------------------------------------------------------- # Automatische Modell-Entdeckung ("aktuell beste Modelle", live von HF + Cache). # --------------------------------------------------------------------------- @@ -353,7 +380,7 @@ def _model_requirements(role: str, quant: str, fit: dict) -> list[str]: """Ehrliche Voraussetzungen/Hinweise pro Modell (Klartext fuer Anfaenger).""" reqs = [] if role == "vision": - reqs.append("Bild-Modell: Projektor (mmproj) + --jinja werden beim Einpflegen automatisch ergänzt.") + reqs.append("Bild-Modell: Projektor (mmproj-F16) wird automatisch mitgeladen und --jinja gesetzt.") if quant.upper() in ("FP16", "BF16", "F16", "F32"): reqs.append("Unquantisiert — sehr groß. Eine Q4/Q5-Variante ist meist die bessere Wahl.") if fit["level"] == "too_tight": @@ -508,20 +535,26 @@ def install_model(req: InstallModelReq): file = _pick_gguf(req.repo, req.quant) if not file: raise HTTPException(404, "Keine GGUF-Datei im Repo gefunden.") + mmproj = _pick_mmproj(req.repo) ram_gb = psutil.virtual_memory().total / (1024 ** 3) env = dict(HF_DOWNLOAD_ENV) if req.hf_token: env["HF_TOKEN"] = req.hf_token target = MODELS_DIR / req.repo.split("/")[-1] target.mkdir(parents=True, exist_ok=True) - jid = start_job([hf_bin(), "download", req.repo, file, "--local-dir", str(target)], - f"download {req.repo.split('/')[-1]}", env=env) + args = [hf_bin(), "download", req.repo, file] + if mmproj: + args.append(mmproj) + args += ["--local-dir", str(target)] + jid = start_job(args, f"download {req.repo.split('/')[-1]}", env=env) JOBS[jid]["result_path"] = str(target / file) attach_download_progress(jid, str(target), hf_file_size(req.repo, file)) cfg = read_config() ctx = max_ctx_for(req.params_b, req.quant, ram_gb) path = str(target / file) cmd = CMD_TEMPLATE.replace("{model}", path).replace("{ctx}", str(ctx)) + if mmproj: + cmd = cmd.rstrip() + f" --mmproj {target / mmproj} --jinja" mid = model_id_from_path(path) cfg["models"][mid] = {"cmd": LiteralScalarString(cmd + "\n"), "ttl": DEFAULT_TTL} set_role_alias(cfg, mid, req.role)