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.
|
# Upgrade-Map: hat der Nutzer ein „altes" Modell, schlagen wir das aktuelle vor.
|
||||||
UPGRADES = [
|
UPGRADES = [
|
||||||
{"match": ["qwen2.5-coder", "qwen2_5-coder"], "name": "Qwen3-Coder 30B-A3B",
|
{"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 ruamel.yaml.scalarstring import LiteralScalarString
|
||||||
|
|
||||||
from auth import auth
|
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,
|
from config import (MODELS_DIR, CMD_TEMPLATE, DEFAULT_TTL, HF_DOWNLOAD_ENV, USER_RECIPES_PATH,
|
||||||
DISCOVER_CACHE_PATH, DISCOVER_TTL, hf_bin)
|
DISCOVER_CACHE_PATH, DISCOVER_TTL, hf_bin)
|
||||||
from llamaswap import read_config, write_config, model_id_from_path, set_role_alias
|
from llamaswap import read_config, write_config, model_id_from_path, set_role_alias
|
||||||
from jobengine import start_job, JOBS, attach_download_progress
|
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
|
from sources import TRUSTED_AUTHORS, CATEGORIES, SKIP_TOKENS
|
||||||
|
|
||||||
router = APIRouter(prefix="/api/cookbook", dependencies=[Depends(auth)])
|
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)
|
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:
|
def all_recipes() -> list:
|
||||||
"""Eingebaute + eigene Setups (eigene tragen 'user': True)."""
|
"""Auto-komponierte Setups (live aus discover, Hardware-aware) + eigene Setups.
|
||||||
return list(RECIPES) + load_user_recipes()
|
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):
|
class AnalyzeRequest(BaseModel):
|
||||||
repo_id: str
|
repo_id: str
|
||||||
|
|||||||
Reference in New Issue
Block a user