wartung: Groesse je Quantisierung, GitHub-Antworten 15 min gemerkt, Hermes-Version auch bei haengendem Abruf
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
0e961f112b
commit
ee2bb9d03c
+13
-6
@@ -17,10 +17,12 @@ def normalize_repo(s: str) -> str:
|
|||||||
return s.strip("/")
|
return s.strip("/")
|
||||||
|
|
||||||
|
|
||||||
def list_quants(repo: str) -> list[str]:
|
def list_quants(repo: str) -> list[dict]:
|
||||||
"""Verfügbare Quant-Stufen eines Repos (aus den GGUF-Dateinamen, ohne mmproj)."""
|
"""Verfügbare Quant-Stufen eines Repos mit Downloadgröße (alle Teile + mmproj), ohne mmproj
|
||||||
|
als eigene Stufe. Ein Aufruf der Hugging-Face-API für alle Stufen."""
|
||||||
|
baum = _tree(repo)
|
||||||
quants: set[str] = set()
|
quants: set[str] = set()
|
||||||
for e in _tree(repo):
|
for e in baum:
|
||||||
p = str(e.get("path", ""))
|
p = str(e.get("path", ""))
|
||||||
if p.lower().endswith(".gguf") and "mmproj" not in p.lower():
|
if p.lower().endswith(".gguf") and "mmproj" not in p.lower():
|
||||||
m = re.search(r"(I?Q\d[\w]*|F16|BF16|FP16|F32)", p, re.IGNORECASE)
|
m = re.search(r"(I?Q\d[\w]*|F16|BF16|FP16|F32)", p, re.IGNORECASE)
|
||||||
@@ -28,7 +30,8 @@ def list_quants(repo: str) -> list[str]:
|
|||||||
quants.add(m.group(1).upper())
|
quants.add(m.group(1).upper())
|
||||||
# gängige Reihenfolge zuerst
|
# gängige Reihenfolge zuerst
|
||||||
order = {"Q4_K_M": 0, "Q4_K_S": 1, "Q5_K_M": 2, "Q6_K": 3, "Q8_0": 4, "Q3_K_M": 5, "Q2_K": 6}
|
order = {"Q4_K_M": 0, "Q4_K_S": 1, "Q5_K_M": 2, "Q6_K": 3, "Q8_0": 4, "Q3_K_M": 5, "Q2_K": 6}
|
||||||
return sorted(quants, key=lambda q: (order.get(q, 99), q))
|
return [{"quant": q, "total_bytes": auswahl(baum, q)["total_bytes"]}
|
||||||
|
for q in sorted(quants, key=lambda q: (order.get(q, 99), q))]
|
||||||
|
|
||||||
|
|
||||||
def search(q: str = "", limit: int = 24) -> list[dict]:
|
def search(q: str = "", limit: int = 24) -> list[dict]:
|
||||||
@@ -68,12 +71,16 @@ def _size(entry: dict) -> int:
|
|||||||
|
|
||||||
|
|
||||||
def resolve_gguf(repo: str, quant: str = "Q4_K_M") -> dict:
|
def resolve_gguf(repo: str, quant: str = "Q4_K_M") -> dict:
|
||||||
"""Beste GGUF-Auswahl eines Repos für einen Quant. Behandelt Split-GGUFs
|
"""Beste GGUF-Auswahl eines Repos für einen Quant (siehe auswahl)."""
|
||||||
|
return auswahl(_tree(repo), quant)
|
||||||
|
|
||||||
|
|
||||||
|
def auswahl(tree: list[dict], quant: str = "Q4_K_M") -> dict:
|
||||||
|
"""GGUF-Auswahl aus dem Dateibaum eines Repos für einen Quant. Behandelt Split-GGUFs
|
||||||
(-00001-of-000NN) als Gruppe. Liefert die Datei-/Pattern-Infos für den Download.
|
(-00001-of-000NN) als Gruppe. Liefert die Datei-/Pattern-Infos für den Download.
|
||||||
|
|
||||||
Rückgabe: {files:[paths], first:path, total_bytes:int, mmproj:path|None, split:bool}
|
Rückgabe: {files:[paths], first:path, total_bytes:int, mmproj:path|None, split:bool}
|
||||||
"""
|
"""
|
||||||
tree = _tree(repo)
|
|
||||||
ggufs = [e for e in tree if str(e.get("path", "")).lower().endswith(".gguf")]
|
ggufs = [e for e in tree if str(e.get("path", "")).lower().endswith(".gguf")]
|
||||||
q = quant.lower()
|
q = quant.lower()
|
||||||
# mmproj separat (Vision-Projektor)
|
# mmproj separat (Vision-Projektor)
|
||||||
|
|||||||
@@ -50,10 +50,26 @@ _engine_cache = {"ts": 0.0, "avail": False}
|
|||||||
_ENGINE_ASSET_RX = re.compile(r"ubuntu-vulkan-x64\.tar\.gz$", re.IGNORECASE)
|
_ENGINE_ASSET_RX = re.compile(r"ubuntu-vulkan-x64\.tar\.gz$", re.IGNORECASE)
|
||||||
|
|
||||||
|
|
||||||
|
# GitHub erlaubt ohne Anmeldung 60 Anfragen pro Stunde. Jeder Blick auf die Update-Details fragte
|
||||||
|
# bisher neu; jetzt gilt eine Antwort 15 Minuten. Fehler (auch das Anfragelimit) werden nicht gemerkt.
|
||||||
|
GITHUB_CACHE_S = int(os.environ.get("MC_GITHUB_CACHE_S", "900"))
|
||||||
|
_github_cache: dict[str, tuple[float, object]] = {}
|
||||||
|
|
||||||
|
|
||||||
|
def _github_json(pfad: str, timeout: float = 8) -> object:
|
||||||
|
jetzt = time.time()
|
||||||
|
if (eintrag := _github_cache.get(pfad)) and jetzt - eintrag[0] < GITHUB_CACHE_S:
|
||||||
|
return eintrag[1]
|
||||||
|
r = httpx.get(f"https://api.github.com/{pfad}", timeout=timeout, headers={"User-Agent": "MissionControl2"})
|
||||||
|
r.raise_for_status()
|
||||||
|
daten = r.json()
|
||||||
|
_github_cache[pfad] = (jetzt, daten)
|
||||||
|
return daten
|
||||||
|
|
||||||
|
|
||||||
def _latest_engine_asset_release() -> dict | None:
|
def _latest_engine_asset_release() -> dict | None:
|
||||||
try:
|
try:
|
||||||
rels = httpx.get(f"https://api.github.com/repos/{ENGINE_REPO}/releases?per_page=15",
|
rels = _github_json(f"repos/{ENGINE_REPO}/releases?per_page=15")
|
||||||
timeout=8, headers={"User-Agent": "MissionControl2"}).json()
|
|
||||||
for rel in (rels if isinstance(rels, list) else []):
|
for rel in (rels if isinstance(rels, list) else []):
|
||||||
if any(_ENGINE_ASSET_RX.search(a.get("name", "")) for a in (rel.get("assets") or [])):
|
if any(_ENGINE_ASSET_RX.search(a.get("name", "")) for a in (rel.get("assets") or [])):
|
||||||
return rel
|
return rel
|
||||||
@@ -160,9 +176,8 @@ def _swap_update_available() -> bool:
|
|||||||
avail = False
|
avail = False
|
||||||
fehler = None
|
fehler = None
|
||||||
try:
|
try:
|
||||||
rel = httpx.get(f"https://api.github.com/repos/{SWAP_REPO}/releases/latest",
|
rel = _github_json(f"repos/{SWAP_REPO}/releases/latest", timeout=6)
|
||||||
timeout=6, headers={"User-Agent": "MissionControl2"}).json()
|
tag = str(rel.get("tag_name", "")) if isinstance(rel, dict) else ""
|
||||||
tag = str(rel.get("tag_name", ""))
|
|
||||||
latest = int(m.group(1)) if (m := re.search(r"(\d{2,})", tag)) else None
|
latest = int(m.group(1)) if (m := re.search(r"(\d{2,})", tag)) else None
|
||||||
installed = _installed_swap_version()
|
installed = _installed_swap_version()
|
||||||
if latest is not None and installed is not None:
|
if latest is not None and installed is not None:
|
||||||
@@ -346,8 +361,7 @@ def swap_update_details() -> dict:
|
|||||||
"latest_build": None, "latest_tag": None, "name": None,
|
"latest_build": None, "latest_tag": None, "name": None,
|
||||||
"url": None, "body": None}
|
"url": None, "body": None}
|
||||||
try:
|
try:
|
||||||
rel = httpx.get(f"https://api.github.com/repos/{SWAP_REPO}/releases/latest",
|
rel = _github_json(f"repos/{SWAP_REPO}/releases/latest")
|
||||||
timeout=8, headers={"User-Agent": "MissionControl2"}).json()
|
|
||||||
tag = str(rel.get("tag_name", ""))
|
tag = str(rel.get("tag_name", ""))
|
||||||
info["latest_tag"] = tag
|
info["latest_tag"] = tag
|
||||||
info["latest_build"] = int(m.group(1)) if (m := re.search(r"(\d{2,})", tag)) else None
|
info["latest_build"] = int(m.group(1)) if (m := re.search(r"(\d{2,})", tag)) else None
|
||||||
@@ -446,8 +460,8 @@ def _summarize_release(kind: str, key: str, context: str, changes: str) -> dict:
|
|||||||
def _summarize_hermes_commits(commits: list[dict]) -> dict:
|
def _summarize_hermes_commits(commits: list[dict]) -> dict:
|
||||||
subjects = "\n".join(f"- {c['subject']}" for c in commits[:100])
|
subjects = "\n".join(f"- {c['subject']}" for c in commits[:100])
|
||||||
context = ("Es geht um ein Update des Hermes-Agenten (das Gehirn/Werkzeug-System der Box auf "
|
context = ("Es geht um ein Update des Hermes-Agenten (das Gehirn/Werkzeug-System der Box auf "
|
||||||
"Strix Halo; genutzt werden: api_server/Gateway, memory-provider-Plugin 'mc2-memory', "
|
"Strix Halo; genutzt werden: api_server/Gateway, Telegram, eigenes Gedächtnis, "
|
||||||
"terminal-/web-Tools, approvals, cron).")
|
"terminal-/web-Tools, Plugins, cron).")
|
||||||
return _summarize_release("hermes", commits[0]["hash"] if commits else "", context, subjects)
|
return _summarize_release("hermes", commits[0]["hash"] if commits else "", context, subjects)
|
||||||
|
|
||||||
|
|
||||||
@@ -469,6 +483,8 @@ def hermes_update_details() -> dict:
|
|||||||
info["error"] = "Hermes-Agent-Repo nicht gefunden."
|
info["error"] = "Hermes-Agent-Repo nicht gefunden."
|
||||||
return info
|
return info
|
||||||
path = git["path"]
|
path = git["path"]
|
||||||
|
# Vor dem Abruf: scheitert der (Zeitlimit, Netz), zeigt die Seite trotzdem die laufende Version.
|
||||||
|
info["installed_version"] = _hermes_version(path)
|
||||||
try:
|
try:
|
||||||
branch = (subprocess.run(["git", "-C", path, "rev-parse", "--abbrev-ref", "HEAD"],
|
branch = (subprocess.run(["git", "-C", path, "rev-parse", "--abbrev-ref", "HEAD"],
|
||||||
capture_output=True, text=True, timeout=8).stdout.strip() or "main")
|
capture_output=True, text=True, timeout=8).stdout.strip() or "main")
|
||||||
@@ -484,7 +500,6 @@ def hermes_update_details() -> dict:
|
|||||||
commits.append({"hash": parts[0], "subject": parts[1], "when": parts[2]})
|
commits.append({"hash": parts[0], "subject": parts[1], "when": parts[2]})
|
||||||
info["commits"] = commits
|
info["commits"] = commits
|
||||||
info["behind"] = len(commits)
|
info["behind"] = len(commits)
|
||||||
info["installed_version"] = _hermes_version(path)
|
|
||||||
if commits:
|
if commits:
|
||||||
info.update(_summarize_hermes_commits(commits))
|
info.update(_summarize_hermes_commits(commits))
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
"""Kleinigkeiten der Update-Seite (24.09.2026): Größe je Quantisierung, GitHub-Zwischenspeicher,
|
||||||
|
Hermes-Version auch dann, wenn der Abruf scheitert."""
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
import pytest
|
||||||
|
from services import hf, maintenance
|
||||||
|
|
||||||
|
|
||||||
|
def test_quants_mit_groesse(monkeypatch):
|
||||||
|
baum = [
|
||||||
|
{"path": "Q8_0/M-Q8_0-00001-of-00002.gguf", "size": 50},
|
||||||
|
{"path": "Q8_0/M-Q8_0-00002-of-00002.gguf", "size": 50},
|
||||||
|
{"path": "M-Q4_K_M.gguf", "lfs": {"size": 30}},
|
||||||
|
{"path": "mmproj-F16.gguf", "size": 5},
|
||||||
|
{"path": "README.md", "size": 1},
|
||||||
|
]
|
||||||
|
aufrufe = []
|
||||||
|
monkeypatch.setattr(hf, "_tree", lambda repo: aufrufe.append(repo) or baum)
|
||||||
|
assert hf.list_quants("x/y") == [{"quant": "Q4_K_M", "total_bytes": 35}, {"quant": "Q8_0", "total_bytes": 105}]
|
||||||
|
assert aufrufe == ["x/y"] # ein Abruf für alle Stufen
|
||||||
|
|
||||||
|
|
||||||
|
class _Antwort:
|
||||||
|
def __init__(self, status: int, daten: object):
|
||||||
|
self.status_code = status
|
||||||
|
self._daten = daten
|
||||||
|
|
||||||
|
def raise_for_status(self):
|
||||||
|
if self.status_code >= 400:
|
||||||
|
raise httpx.HTTPStatusError("fehler", request=httpx.Request("GET", "https://x"),
|
||||||
|
response=httpx.Response(self.status_code))
|
||||||
|
|
||||||
|
def json(self):
|
||||||
|
return self._daten
|
||||||
|
|
||||||
|
|
||||||
|
def test_github_antworten_werden_gemerkt_fehler_nicht(monkeypatch):
|
||||||
|
monkeypatch.setattr(maintenance, "_github_cache", {})
|
||||||
|
antworten = [_Antwort(403, {"message": "API rate limit exceeded"}), _Antwort(200, {"tag_name": "v258"})]
|
||||||
|
aufrufe = []
|
||||||
|
monkeypatch.setattr(maintenance.httpx, "get", lambda url, **kw: aufrufe.append(url) or antworten.pop(0))
|
||||||
|
with pytest.raises(httpx.HTTPStatusError):
|
||||||
|
maintenance._github_json("repos/a/b/releases/latest")
|
||||||
|
assert maintenance._github_json("repos/a/b/releases/latest") == {"tag_name": "v258"}
|
||||||
|
assert maintenance._github_json("repos/a/b/releases/latest") == {"tag_name": "v258"}
|
||||||
|
assert len(aufrufe) == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_hermes_version_auch_wenn_der_abruf_haengt(monkeypatch, tmp_path):
|
||||||
|
(tmp_path / "pyproject.toml").write_text('[project]\nversion = "0.21.4"\n', encoding="utf-8")
|
||||||
|
monkeypatch.setattr(maintenance.system, "find_hermes_agent_git", lambda: {"path": str(tmp_path)})
|
||||||
|
|
||||||
|
def run(befehl, **kw):
|
||||||
|
if "fetch" in befehl:
|
||||||
|
raise subprocess.TimeoutExpired(befehl, 25)
|
||||||
|
return subprocess.CompletedProcess(befehl, 0, stdout="main\n", stderr="")
|
||||||
|
|
||||||
|
monkeypatch.setattr(maintenance.subprocess, "run", run)
|
||||||
|
info = maintenance.hermes_update_details()
|
||||||
|
assert info["installed_version"] == "0.21.4"
|
||||||
|
assert "error" in info
|
||||||
Reference in New Issue
Block a user