Feat: kuratierter Modell-Katalog (Cookbook) - korrekte Metadaten statt Namens-Raterei
Inspiriert von Odysseus' Cookbook: backend/models_catalog.json mit echten Metadaten (total/active params, moe, generation) je Rolle. services/catalog.py: Laden, Name-Match, MoE-bewusstes Scoring (Wissen + Tempo via tps -> MoE-first auf der bandbreiten-Box), Fit. - discover.py: Empfehlung jetzt KATALOG-FIRST (kuratiert, korrekt), HF-Dynamik als Ergaenzung/Fallback. - maintenance.model_upgrades: Metadaten aus Katalog -> praezise Familie/Generation/Groesse + MoE-first (dense ersetzt MoE nur bei grossem Wissens-Sprung). Behebt Coder-Next=7B-Fehlschaetzung, Qwen2.5-VL-Generations-Downgrade, falsches dense-scout-Upgrade. - fit.py: MXFP4/FP8/AWQ in der Quant-Tabelle. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,60 @@
|
|||||||
|
{
|
||||||
|
"_comment": "Kuratierter Modell-Katalog (Cookbook) für Strix Halo / Ryzen AI MAX+ 395 — 128GB unified, bandbreiten-limitiert (256 GB/s). MoE-first. EINE Quelle der Wahrheit für KORREKTE Metadaten (total/active params, moe, generation) → präzise Empfehlungen ohne Namens-Raterei. Inspiriert vom Odysseus-Cookbook (statischer, validierter Katalog statt Live-Scraping). Erweiterbar: neue Modelle hier eintragen. Felder: name (Match-Identifier), repo (HF org/name für Install), family (+Subtyp), generation (numerisch, für Upgrade-Vergleich), total_params_b, active_params_b (=total bei dense), moe, quant, ctx (empfohlen), tools, vision.",
|
||||||
|
"version": "2026-06-27",
|
||||||
|
"models": [
|
||||||
|
{
|
||||||
|
"role": "fast", "name": "Qwen3.6-35B-A3B", "repo": "Qwen/Qwen3.6-35B-A3B-GGUF",
|
||||||
|
"family": "qwen", "generation": 3.6, "total_params_b": 35, "active_params_b": 3,
|
||||||
|
"moe": true, "quant": "Q4_K_M", "ctx": 32768, "tools": true, "vision": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "fast", "name": "Qwen3-30B-A3B-Instruct", "repo": "unsloth/Qwen3-30B-A3B-Instruct-2507-GGUF",
|
||||||
|
"family": "qwen", "generation": 3.0, "total_params_b": 30, "active_params_b": 3,
|
||||||
|
"moe": true, "quant": "Q4_K_M", "ctx": 32768, "tools": true, "vision": false
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"role": "heavy", "name": "Qwen3.5-122B-A10B", "repo": "Qwen/Qwen3.5-122B-A10B-GGUF",
|
||||||
|
"family": "qwen", "generation": 3.5, "total_params_b": 122, "active_params_b": 10,
|
||||||
|
"moe": true, "quant": "Q4_K_M", "ctx": 32768, "tools": true, "vision": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "heavy", "name": "gpt-oss-120b", "repo": "ggml-org/gpt-oss-120b-GGUF",
|
||||||
|
"family": "gpt-oss", "generation": 1.0, "total_params_b": 120, "active_params_b": 5,
|
||||||
|
"moe": true, "quant": "MXFP4", "ctx": 32768, "tools": true, "vision": false
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"role": "coder", "name": "Qwen3-Coder-Next", "repo": "Qwen/Qwen3-Coder-Next-GGUF",
|
||||||
|
"family": "qwen-coder", "generation": 3.0, "total_params_b": 84, "active_params_b": 3,
|
||||||
|
"moe": true, "quant": "Q4_K_M", "ctx": 65536, "tools": true, "vision": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "coder", "name": "Qwen3-Coder-30B-A3B-Instruct", "repo": "unsloth/Qwen3-Coder-30B-A3B-Instruct-GGUF",
|
||||||
|
"family": "qwen-coder", "generation": 3.0, "total_params_b": 30, "active_params_b": 3,
|
||||||
|
"moe": true, "quant": "Q4_K_M", "ctx": 65536, "tools": true, "vision": false
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"role": "vision", "name": "Qwen3-VL-8B-Instruct", "repo": "Qwen/Qwen3-VL-8B-Instruct-GGUF",
|
||||||
|
"family": "qwen-vl", "generation": 3.0, "total_params_b": 8, "active_params_b": 8,
|
||||||
|
"moe": false, "quant": "Q4_K_M", "ctx": 32768, "tools": false, "vision": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "vision", "name": "Qwen3-VL-2B-Instruct", "repo": "Qwen/Qwen3-VL-2B-Instruct-GGUF",
|
||||||
|
"family": "qwen-vl", "generation": 3.0, "total_params_b": 2, "active_params_b": 2,
|
||||||
|
"moe": false, "quant": "Q4_K_M", "ctx": 32768, "tools": false, "vision": true
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"role": "scout", "name": "gemma-4-26B-A4B-it", "repo": "google/gemma-4-26B-A4B-it-GGUF",
|
||||||
|
"family": "gemma", "generation": 4.0, "total_params_b": 26, "active_params_b": 4,
|
||||||
|
"moe": true, "quant": "Q4_K_M", "ctx": 32768, "tools": false, "vision": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "scout", "name": "gemma-4-31B-it", "repo": "google/gemma-4-31B-it-GGUF",
|
||||||
|
"family": "gemma", "generation": 4.0, "total_params_b": 31, "active_params_b": 31,
|
||||||
|
"moe": false, "quant": "Q4_K_M", "ctx": 32768, "tools": false, "vision": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
"""
|
||||||
|
Kuratierter Modell-Katalog ("Cookbook", inspiriert von Odysseus): EINE Quelle der
|
||||||
|
Wahrheit für KORREKTE Metadaten (total/active params, moe, generation) statt
|
||||||
|
Namens-Raterei. Macht Empfehlung + Upgrade-Erkennung präzise und MoE-bewusst
|
||||||
|
für die bandbreiten-limitierte Strix-Halo-Box.
|
||||||
|
|
||||||
|
Daten: backend/models_catalog.json. Fällt sanft aus (leerer Katalog), wenn die
|
||||||
|
Datei fehlt → discover nutzt dann nur die HF-Dynamik.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import math
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
|
from services.fit import estimate_memory_gb, estimate_speed
|
||||||
|
|
||||||
|
_CATALOG_PATH = os.path.join(os.path.dirname(__file__), "..", "models_catalog.json")
|
||||||
|
_cache: dict = {"mtime": 0.0, "models": []}
|
||||||
|
|
||||||
|
|
||||||
|
def _load() -> list[dict]:
|
||||||
|
try:
|
||||||
|
mt = os.path.getmtime(_CATALOG_PATH)
|
||||||
|
if mt != _cache["mtime"]:
|
||||||
|
with open(_CATALOG_PATH, encoding="utf-8") as f:
|
||||||
|
data = json.load(f) or {}
|
||||||
|
_cache.update(mtime=mt, models=[m for m in data.get("models", []) if m.get("name")])
|
||||||
|
except (OSError, ValueError):
|
||||||
|
_cache.update(mtime=0.0, models=[])
|
||||||
|
return _cache["models"]
|
||||||
|
|
||||||
|
|
||||||
|
def _norm(name: str) -> str:
|
||||||
|
"""Vergleichs-Stamm: kleingeschrieben, Org-Prefix/Quant/GGUF/Split entfernt."""
|
||||||
|
s = (name or "").lower().split("/")[-1]
|
||||||
|
s = re.sub(r"\.gguf$", "", s)
|
||||||
|
s = re.sub(r"-\d+-of-\d+$", "", s)
|
||||||
|
s = re.sub(r"[-_](ud-)?(i?q\d[\w]*|f16|bf16|fp16|f32|mxfp4)$", "", s)
|
||||||
|
return s.strip("-_ ")
|
||||||
|
|
||||||
|
|
||||||
|
def entries() -> list[dict]:
|
||||||
|
return list(_load())
|
||||||
|
|
||||||
|
|
||||||
|
def entries_for_role(role: str) -> list[dict]:
|
||||||
|
return [e for e in _load() if e.get("role") == role]
|
||||||
|
|
||||||
|
|
||||||
|
def meta_for_name(name: str) -> dict | None:
|
||||||
|
"""Katalog-Metadaten zu einem Modell(namen) — matcht lokalen Namen ODER HF-Repo."""
|
||||||
|
n = _norm(name)
|
||||||
|
if not n:
|
||||||
|
return None
|
||||||
|
for e in _load():
|
||||||
|
cand = {_norm(e.get("name", "")), _norm(e.get("repo", ""))}
|
||||||
|
if n in cand or any(c and (c in n or n in c) for c in cand):
|
||||||
|
return e
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def fit_of(e: dict, ram_gb: float) -> dict:
|
||||||
|
"""Hardware-Fit eines Katalog-Eintrags (MoE-bewusst über active_params_b)."""
|
||||||
|
total = float(e.get("total_params_b") or 7)
|
||||||
|
active = float(e.get("active_params_b") or total)
|
||||||
|
quant = e.get("quant") or "Q4_K_M"
|
||||||
|
ctx = int(e.get("ctx") or 32768)
|
||||||
|
req_gb = estimate_memory_gb(total, quant, ctx)
|
||||||
|
tps = estimate_speed(req_gb, ram_gb, (active / total) if total else 1.0)
|
||||||
|
usable = max(ram_gb - 4.0, 0)
|
||||||
|
if req_gb > usable:
|
||||||
|
level, text = "too_tight", "Zu groß (OOM)"
|
||||||
|
elif req_gb > usable * 0.8:
|
||||||
|
level, text = "marginal", "Könnte knapp werden"
|
||||||
|
else:
|
||||||
|
level, text = "perfect", "Passt perfekt"
|
||||||
|
return {"level": level, "text": text, "req_gb": round(req_gb, 1), "tps": round(tps, 0)}
|
||||||
|
|
||||||
|
|
||||||
|
def stack_score(e: dict, ram_gb: float) -> float:
|
||||||
|
"""Score für DIESE Hardware: muss passen, dann Wissen (total params) + Tempo
|
||||||
|
(tps — belohnt MoE durch niedrige aktive Params automatisch). Bandbreiten-Box
|
||||||
|
→ MoE gewinnt bei vergleichbarem Wissen gegen dense."""
|
||||||
|
fit = fit_of(e, ram_gb)
|
||||||
|
if fit["level"] == "too_tight":
|
||||||
|
return -100.0
|
||||||
|
total = float(e.get("total_params_b") or 7)
|
||||||
|
fit_bonus = 3.0 if fit["level"] == "perfect" else 1.0
|
||||||
|
knowledge = math.log2(total + 1) / 8.0 # ~0..1 (bis ~256B)
|
||||||
|
speed = min((fit["tps"] or 0) / 80.0, 1.0) # normalisiert; MoE = hohe tps
|
||||||
|
return fit_bonus + 1.2 * knowledge + 1.0 * speed
|
||||||
|
|
||||||
|
|
||||||
|
def to_model_dict(e: dict, ram_gb: float) -> dict:
|
||||||
|
"""Katalog-Eintrag → discover-kompatibles Modell-Dict (echte Metadaten)."""
|
||||||
|
total = float(e.get("total_params_b") or 7)
|
||||||
|
active = e.get("active_params_b")
|
||||||
|
repo = e.get("repo") or e.get("name")
|
||||||
|
role = e.get("role")
|
||||||
|
caps = {
|
||||||
|
"moe": bool(e.get("moe")), "active_b": active,
|
||||||
|
"tools": "yes" if e.get("tools") else "no",
|
||||||
|
"vision": bool(e.get("vision")), "coder": role == "coder",
|
||||||
|
"reasoning": role == "heavy", "embedding": False,
|
||||||
|
"ctx": e.get("ctx"), "params_b": total, "arch": e.get("family"),
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
"name": e.get("name"), "author": repo.split("/")[0] if "/" in repo else "catalog",
|
||||||
|
"repo": repo, "role": role, "params_b": total, "active_b": active,
|
||||||
|
"moe": bool(e.get("moe")), "generation": e.get("generation"),
|
||||||
|
"family": e.get("family"), "quant": e.get("quant") or "Q4_K_M",
|
||||||
|
"tags": ["catalog"], "downloads": 0, "fit": fit_of(e, ram_gb),
|
||||||
|
"optimal_ctx": int(e.get("ctx") or 32768), "caps": caps, "curated": True,
|
||||||
|
}
|
||||||
@@ -19,6 +19,7 @@ import httpx
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from config import DISCOVER_CACHE_PATH, DISCOVER_TTL
|
from config import DISCOVER_CACHE_PATH, DISCOVER_TTL
|
||||||
|
from services import catalog
|
||||||
from services.caps import capabilities
|
from services.caps import capabilities
|
||||||
from services.fit import evaluate_fit, extract_params_b, max_ctx_for
|
from services.fit import evaluate_fit, extract_params_b, max_ctx_for
|
||||||
from services.sources import CATEGORIES, SKIP_TOKENS, TRUSTED_AUTHORS
|
from services.sources import CATEGORIES, SKIP_TOKENS, TRUSTED_AUTHORS
|
||||||
@@ -100,7 +101,7 @@ def refresh_discover(ram_gb: float) -> dict:
|
|||||||
continue
|
continue
|
||||||
seen.add(rid)
|
seen.add(rid)
|
||||||
raw.append(m)
|
raw.append(m)
|
||||||
if ok == 0:
|
if ok == 0 and not catalog.entries():
|
||||||
raise RuntimeError("Keine Quelle erreichbar.")
|
raise RuntimeError("Keine Quelle erreichbar.")
|
||||||
|
|
||||||
by_cat: dict[str, list] = {c["role"]: [] for c in CATEGORIES}
|
by_cat: dict[str, list] = {c["role"]: [] for c in CATEGORIES}
|
||||||
@@ -125,15 +126,25 @@ def refresh_discover(ram_gb: float) -> dict:
|
|||||||
|
|
||||||
cats = []
|
cats = []
|
||||||
for c in CATEGORIES:
|
for c in CATEGORIES:
|
||||||
items = by_cat[c["role"]]
|
role = c["role"]
|
||||||
ranked = rank_runnable(items)
|
# 1) KATALOG zuerst (kuratierte, korrekte Metadaten, MoE-bewusst gerankt) —
|
||||||
# Top 4 je Kategorie = die best-gerankten (fähigstes-was-passt zuerst).
|
# macht die Empfehlung präzise statt Namens-Raterei.
|
||||||
top = ranked[:4]
|
cat_entries = sorted(catalog.entries_for_role(role),
|
||||||
if top:
|
key=lambda e: -catalog.stack_score(e, ram_gb))
|
||||||
|
cat_models = [m for m in (catalog.to_model_dict(e, ram_gb) for e in cat_entries)
|
||||||
|
if m["fit"]["level"] != "too_tight"]
|
||||||
|
# 2) HF-Dynamik als Ergänzung (nicht-kuratierte Funde), dedupliziert.
|
||||||
|
hf_ranked = rank_runnable(by_cat[role])
|
||||||
|
seen = {catalog._norm(m["repo"]) for m in cat_models}
|
||||||
|
extra = [h for h in hf_ranked if catalog._norm(h["repo"]) not in seen]
|
||||||
|
combined = cat_models + extra
|
||||||
|
if combined:
|
||||||
cats.append({
|
cats.append({
|
||||||
"role": c["role"], "title": c["title"], "icon": c["icon"],
|
"role": role, "title": c["title"], "icon": c["icon"],
|
||||||
"models": top,
|
"models": combined[:6],
|
||||||
"recommended": ranked[0]["repo"] if ranked else None,
|
# Empfehlung = bester KURATIERTER Eintrag, sonst beste HF-Fundstelle.
|
||||||
|
"recommended": (cat_models[0]["repo"] if cat_models
|
||||||
|
else (hf_ranked[0]["repo"] if hf_ranked else None)),
|
||||||
})
|
})
|
||||||
|
|
||||||
data = {"updated": time.time(), "categories": cats}
|
data = {"updated": time.time(), "categories": cats}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ QUANT_BYTES_PER_PARAM = {
|
|||||||
"Q4_0": 0.50, "Q4_1": 0.55, "Q4_K_S": 0.50, "Q4_K_M": 0.55,
|
"Q4_0": 0.50, "Q4_1": 0.55, "Q4_K_S": 0.50, "Q4_K_M": 0.55,
|
||||||
"Q5_0": 0.62, "Q5_1": 0.68, "Q5_K_S": 0.62, "Q5_K_M": 0.65,
|
"Q5_0": 0.62, "Q5_1": 0.68, "Q5_K_S": 0.62, "Q5_K_M": 0.65,
|
||||||
"Q6_K": 0.75, "Q8_0": 1.00, "F16": 2.00, "BF16": 2.00,
|
"Q6_K": 0.75, "Q8_0": 1.00, "F16": 2.00, "BF16": 2.00,
|
||||||
|
"MXFP4": 0.55, "FP8": 1.05, "AWQ": 0.55,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import httpx
|
|||||||
import psutil
|
import psutil
|
||||||
|
|
||||||
from config import ANYTHINGLLM_REPO, ANYTHINGLLM_URL, HERMES_AGENT_REPO
|
from config import ANYTHINGLLM_REPO, ANYTHINGLLM_URL, HERMES_AGENT_REPO
|
||||||
from services import discover, jobengine, llamaswap, system
|
from services import catalog, discover, jobengine, llamaswap, system
|
||||||
|
|
||||||
# System-Dienste (root, via sudo -n NOPASSWD) vs. User-Dienste (systemctl --user).
|
# System-Dienste (root, via sudo -n NOPASSWD) vs. User-Dienste (systemctl --user).
|
||||||
SYSTEM_SERVICES = {"llama-swap"}
|
SYSTEM_SERVICES = {"llama-swap"}
|
||||||
@@ -185,11 +185,27 @@ def _gen_key(name: str):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _meta(name: str, model_dict: dict | None = None, im: dict | None = None) -> dict:
|
||||||
|
"""Metadaten (family, gen, total, active, moe) — bevorzugt den kuratierten Katalog,
|
||||||
|
sonst die Felder eines Discover-/Modell-Dicts, sonst Namens-/Größen-Heuristik."""
|
||||||
|
cm = catalog.meta_for_name(name)
|
||||||
|
if cm:
|
||||||
|
return {"family": cm.get("family"), "gen": cm.get("generation"),
|
||||||
|
"total": float(cm.get("total_params_b") or 0),
|
||||||
|
"active": cm.get("active_params_b"), "moe": bool(cm.get("moe"))}
|
||||||
|
d = model_dict or {}
|
||||||
|
g = _gen_key(name)
|
||||||
|
total = float(d.get("params_b") or 0) or (_params_of(im) if im else 0.0)
|
||||||
|
return {"family": (d.get("family") or (g[0] if g else None)),
|
||||||
|
"gen": (d.get("generation") if d.get("generation") is not None else (g[1] if g else None)),
|
||||||
|
"total": total, "active": d.get("active_b"), "moe": bool(d.get("moe"))}
|
||||||
|
|
||||||
|
|
||||||
def model_upgrades() -> list[dict]:
|
def model_upgrades() -> list[dict]:
|
||||||
"""Je Rolle ein ECHTES Upgrade-Vorschlag — nur wenn die Empfehlung wirklich besser ist:
|
"""Je Rolle ein ECHTES Upgrade — nur wenn die Empfehlung wirklich besser ist:
|
||||||
gleiche Familie UND (neuere Generation ODER größer in gleicher Generation). Verhindert
|
gleiche Familie UND (neuere Generation ODER deutlich größer) UND kein Tempo-Downgrade
|
||||||
Generations-Downgrades (Qwen2.5-VL über Qwen3-VL), Größen-Downgrades (Coder-Next → 30B)
|
(MoE-first für die bandbreiten-limitierte Box: dense ersetzt MoE nur bei großem Wissens-
|
||||||
und Fremd-Familien-Swaps (gpt-oss als „Upgrade" von Qwen)."""
|
Sprung). Metadaten kommen aus dem kuratierten Katalog → keine Namens-Raterei."""
|
||||||
disc = discover.safe_discover(_ram_gb())
|
disc = discover.safe_discover(_ram_gb())
|
||||||
if not disc:
|
if not disc:
|
||||||
return []
|
return []
|
||||||
@@ -203,23 +219,31 @@ def model_upgrades() -> list[dict]:
|
|||||||
for c in disc.get("categories", []):
|
for c in disc.get("categories", []):
|
||||||
role = c["role"]
|
role = c["role"]
|
||||||
im = inst_by_role.get(role)
|
im = inst_by_role.get(role)
|
||||||
if im is None: # nur Rollen mit bereits installiertem Modell
|
if im is None:
|
||||||
continue
|
continue
|
||||||
rec = c.get("recommended")
|
rec = c.get("recommended")
|
||||||
if not rec:
|
if not rec:
|
||||||
continue
|
continue
|
||||||
rec_model = next((x for x in c.get("models", []) if x.get("repo") == rec), None)
|
rec_model = next((x for x in c.get("models", []) if x.get("repo") == rec), None)
|
||||||
rec_params = float((rec_model or {}).get("params_b") or 0.0)
|
|
||||||
inst_params = _params_of(im)
|
|
||||||
|
|
||||||
ig, rg = _gen_key(im["name"]), _gen_key(rec)
|
i = _meta(im["name"], im=im)
|
||||||
# Upgrade nur bei GLEICHER, erkennbarer Familie:
|
r = _meta(rec, model_dict=rec_model)
|
||||||
if not (ig and rg) or ig[0] != rg[0]:
|
|
||||||
continue # Fremd-Familie / unbekannt → kein „Upgrade"
|
if not i["family"] or not r["family"] or i["family"] != r["family"]:
|
||||||
if rg[1] < ig[1] - 1e-6:
|
continue # andere/unbekannte Familie → kein Upgrade
|
||||||
|
if r["gen"] is not None and i["gen"] is not None and r["gen"] < i["gen"] - 1e-6:
|
||||||
continue # ältere Generation → niemals
|
continue # ältere Generation → niemals
|
||||||
if abs(rg[1] - ig[1]) < 1e-6 and rec_params and rec_params < inst_params * 1.05:
|
same_gen = (r["gen"] is None or i["gen"] is None or abs(r["gen"] - i["gen"]) < 1e-6)
|
||||||
continue # gleiche Gen, nicht (deutlich) größer
|
if same_gen:
|
||||||
|
if r["total"] and i["total"] and r["total"] < i["total"] * 1.05:
|
||||||
|
continue # gleiche Gen, nicht größer → kein Upgrade
|
||||||
|
# MoE-first: ein MoE durch dense ersetzen nur bei deutlichem Wissens-Sprung
|
||||||
|
if i["moe"] and not r["moe"] and r["total"] < i["total"] * 1.5:
|
||||||
|
continue
|
||||||
|
# Tempo nicht verschlechtern (aktive Params), außer großer Wissens-Gewinn
|
||||||
|
ia, ra = (i["active"] or i["total"]), (r["active"] or r["total"])
|
||||||
|
if ia and ra > ia * 1.3 and r["total"] < i["total"] * 1.3:
|
||||||
|
continue
|
||||||
|
|
||||||
base = rec.split("/")[-1].lower()
|
base = rec.split("/")[-1].lower()
|
||||||
stem = base[:-5] if base.endswith("-gguf") else base
|
stem = base[:-5] if base.endswith("-gguf") else base
|
||||||
|
|||||||
Reference in New Issue
Block a user