feat(v9): Phase 11 — selbst-aktualisierende Setups (Rollen-Slots × discover)
Setups sind nicht mehr hartkodiert: RECIPE_TEMPLATES definieren das KONZEPT als Rollen-Slots, build_dynamic_recipes füllt sie live aus discover (Hardware- Fit, MoE bevorzugt, dedupe, leere Slots ausgelassen). all_recipes liefert Auto-Setups + eigene; statische RECIPES nur als Fallback. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+31
@@ -91,6 +91,37 @@ RECIPES = [
|
||||
},
|
||||
]
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Dynamische Setups: KONZEPT bleibt kuratiert (Rollen-Slots), die Modelle werden
|
||||
# zur Laufzeit aus `discover` (live HF + Hardware-Fit) gefuellt → Setups
|
||||
# aktualisieren sich von selbst und passen sich der Hardware an.
|
||||
# slots = Rollen aus sources.CATEGORIES (vision/coder/reasoning/agent/scout).
|
||||
# ---------------------------------------------------------------------------
|
||||
RECIPE_TEMPLATES = [
|
||||
{"id": "auto-elite", "title": "Elite-Lineup (für deine APU)", "icon": "bolt",
|
||||
"desc": "Das Stärkste, das auf deiner Hardware flott läuft — bevorzugt MoE (viel Wissen, "
|
||||
"wenige aktive Parameter). Ein Spitzenmodell je Rolle, automatisch aktuell gehalten.",
|
||||
"slots": ["coder", "reasoning", "agent", "vision", "scout"]},
|
||||
{"id": "auto-coding", "title": "Coden & Programmieren", "icon": "code",
|
||||
"desc": "Bestes Coder-Modell für deine Hardware + ein Reasoning-Modell zum Gegenlesen.",
|
||||
"slots": ["coder", "reasoning"]},
|
||||
{"id": "auto-chat", "title": "Alltag & Chat", "icon": "compass",
|
||||
"desc": "Vielseitiger Allrounder + Bildverständnis für den täglichen Gebrauch.",
|
||||
"slots": ["scout", "vision"]},
|
||||
{"id": "auto-agent", "title": "Agenten & Tools", "icon": "layers",
|
||||
"desc": "Tool-fähige Modelle für autonome Aufgaben, ergänzt um einen Coder.",
|
||||
"slots": ["agent", "coder"]},
|
||||
]
|
||||
|
||||
# Kurzbegruendung je Rolle (ersetzt die handgeschriebenen „why"-Texte bei Auto-Setups).
|
||||
ROLE_WHY = {
|
||||
"coder": "Bestes lokales Coder-Modell für deine Hardware.",
|
||||
"reasoning": "Reasoning-Modell für Mathe, Logik & knifflige Aufgaben.",
|
||||
"agent": "Tool-fähig — plant, ruft Werkzeuge auf, arbeitet mehrschrittig.",
|
||||
"vision": "Versteht Screenshots, Diagramme & Fehlerbilder.",
|
||||
"scout": "Flinker Allrounder für schnelle Fragen & Chat.",
|
||||
}
|
||||
|
||||
# Upgrade-Map: hat der Nutzer ein „altes" Modell, schlagen wir das aktuelle vor.
|
||||
UPGRADES = [
|
||||
{"match": ["qwen2.5-coder", "qwen2_5-coder"], "name": "Qwen3-Coder 30B-A3B",
|
||||
|
||||
+60
-4
@@ -14,12 +14,12 @@ import psutil
|
||||
from ruamel.yaml.scalarstring import LiteralScalarString
|
||||
|
||||
from auth import auth
|
||||
from hw_math import evaluate_fit, max_ctx_for, extract_params_b
|
||||
from hw_math import evaluate_fit, max_ctx_for, extract_params_b, extract_active_params_b
|
||||
from config import (MODELS_DIR, CMD_TEMPLATE, DEFAULT_TTL, HF_DOWNLOAD_ENV, USER_RECIPES_PATH,
|
||||
DISCOVER_CACHE_PATH, DISCOVER_TTL, hf_bin)
|
||||
from llamaswap import read_config, write_config, model_id_from_path, set_role_alias
|
||||
from jobengine import start_job, JOBS, attach_download_progress
|
||||
from recipes import RECIPES, UPGRADES
|
||||
from recipes import RECIPES, UPGRADES, RECIPE_TEMPLATES, ROLE_WHY
|
||||
from sources import TRUSTED_AUTHORS, CATEGORIES, SKIP_TOKENS
|
||||
|
||||
router = APIRouter(prefix="/api/cookbook", dependencies=[Depends(auth)])
|
||||
@@ -46,9 +46,65 @@ def save_user_recipes(items: list) -> None:
|
||||
os.replace(tmp, USER_RECIPES_PATH)
|
||||
|
||||
|
||||
def _is_moe(name: str) -> bool:
|
||||
low = (name or "").lower()
|
||||
return (extract_active_params_b(name) is not None
|
||||
or "mixtral" in low or bool(re.search(r"\d+x\d+\.?\d*b", low)))
|
||||
|
||||
|
||||
def _safe_discover(ram_gb: float) -> dict | None:
|
||||
"""discover-Daten aus Cache (oder live), ohne zu werfen — None wenn nichts da."""
|
||||
cached = _load_discover()
|
||||
if cached and (time.time() - cached.get("updated", 0) < DISCOVER_TTL):
|
||||
return cached
|
||||
try:
|
||||
return refresh_discover(ram_gb)
|
||||
except Exception: # noqa: BLE001
|
||||
return cached
|
||||
|
||||
|
||||
def build_dynamic_recipes(disc: dict) -> list:
|
||||
"""Fuellt die kuratierten Rollen-Slot-Templates LIVE aus discover.
|
||||
Pro Slot das beste passende Modell (perfect>marginal, MoE bevorzugt fuer APU,
|
||||
dann Beliebtheit); dedupe ueber Slots; leere Slots werden still ausgelassen."""
|
||||
cats = {c["role"]: c for c in (disc.get("categories") or [])}
|
||||
out = []
|
||||
for tpl in RECIPE_TEMPLATES:
|
||||
used: set[str] = set()
|
||||
models = []
|
||||
for role in tpl["slots"]:
|
||||
cat = cats.get(role)
|
||||
if not cat:
|
||||
continue
|
||||
cands = [m for m in cat["models"]
|
||||
if m["fit"]["level"] != "too_tight" and m["repo"] not in used]
|
||||
if not cands:
|
||||
continue
|
||||
cands.sort(key=lambda m: (_FIT_ORDER[m["fit"]["level"]],
|
||||
0 if _is_moe(m["repo"]) else 1,
|
||||
-int(m.get("downloads") or 0)))
|
||||
pick = cands[0]
|
||||
used.add(pick["repo"])
|
||||
models.append({
|
||||
"role": role, "name": pick["name"], "repo": pick["repo"],
|
||||
"params_b": pick["params_b"], "quant": pick.get("quant", "Q4_K_M"),
|
||||
"tags": pick.get("tags", []), "why": ROLE_WHY.get(role, ""),
|
||||
})
|
||||
if models:
|
||||
out.append({"id": tpl["id"], "title": tpl["title"], "icon": tpl["icon"],
|
||||
"desc": tpl["desc"], "models": models, "auto": True})
|
||||
return out
|
||||
|
||||
|
||||
def all_recipes() -> list:
|
||||
"""Eingebaute + eigene Setups (eigene tragen 'user': True)."""
|
||||
return list(RECIPES) + load_user_recipes()
|
||||
"""Auto-komponierte Setups (live aus discover, Hardware-aware) + eigene Setups.
|
||||
Fallback auf die statischen RECIPES, wenn discover gerade nichts liefert."""
|
||||
ram_gb = psutil.virtual_memory().total / (1024 ** 3)
|
||||
disc = _safe_discover(ram_gb)
|
||||
base = build_dynamic_recipes(disc) if disc else []
|
||||
if not base:
|
||||
base = list(RECIPES)
|
||||
return base + load_user_recipes()
|
||||
|
||||
class AnalyzeRequest(BaseModel):
|
||||
repo_id: str
|
||||
|
||||
Reference in New Issue
Block a user