mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-07-08 11:56:59 +00:00
375 lines
12 KiB
Python
375 lines
12 KiB
Python
import json
|
|
import os
|
|
import re
|
|
import time
|
|
import urllib.parse
|
|
import urllib.request
|
|
from email.utils import parsedate_to_datetime
|
|
from pathlib import Path
|
|
|
|
from src.constants import DATA_DIR
|
|
|
|
|
|
HF_COLLECTIONS_URL = "https://huggingface.co/api/collections"
|
|
HW_FIT_CACHE_DIR = Path(DATA_DIR) / "hwfit"
|
|
MLX_COMMUNITY_CACHE = HW_FIT_CACHE_DIR / "mlx_community_models.json"
|
|
HF_COLLECTION_MODELS_CACHE = HW_FIT_CACHE_DIR / "hf_collection_models.json"
|
|
HF_COLLECTION_TTL_SECONDS = 24 * 3600
|
|
|
|
|
|
HF_COLLECTION_SOURCES = (
|
|
{
|
|
"key": "mlx_community",
|
|
"owner": "mlx-community",
|
|
"provider": "mlx-community",
|
|
"repo_prefix": "mlx-community/",
|
|
"mlx_only": True,
|
|
},
|
|
{
|
|
"key": "zai_org",
|
|
"owner": "zai-org",
|
|
"provider": "zai-org",
|
|
},
|
|
{
|
|
"key": "deepseek_ai",
|
|
"owner": "deepseek-ai",
|
|
"provider": "deepseek-ai",
|
|
},
|
|
{
|
|
"key": "minimax_ai",
|
|
"owner": "MiniMaxAI",
|
|
"provider": "MiniMaxAI",
|
|
},
|
|
{
|
|
"key": "qwen",
|
|
"owner": "Qwen",
|
|
"provider": "Qwen",
|
|
},
|
|
{
|
|
"key": "stepfun_ai",
|
|
"owner": "stepfun-ai",
|
|
"provider": "stepfun-ai",
|
|
},
|
|
{
|
|
"key": "google",
|
|
"owner": "google",
|
|
"provider": "google",
|
|
},
|
|
{
|
|
"key": "openai",
|
|
"owner": "openai",
|
|
"provider": "openai",
|
|
},
|
|
{
|
|
"key": "mistralai",
|
|
"owner": "mistralai",
|
|
"provider": "mistralai",
|
|
},
|
|
{
|
|
"key": "meta_llama",
|
|
"owner": "meta-llama",
|
|
"provider": "meta-llama",
|
|
},
|
|
{
|
|
"key": "nousresearch",
|
|
"owner": "NousResearch",
|
|
"provider": "NousResearch",
|
|
},
|
|
{
|
|
"key": "moonshotai",
|
|
"owner": "moonshotai",
|
|
"provider": "moonshotai",
|
|
},
|
|
{
|
|
"key": "mllama",
|
|
"owner": "mllama",
|
|
"provider": "mllama",
|
|
},
|
|
)
|
|
|
|
|
|
def _format_params(raw):
|
|
try:
|
|
n = int(raw or 0)
|
|
except (TypeError, ValueError):
|
|
n = 0
|
|
if n <= 0:
|
|
return "", 0
|
|
if n >= 1_000_000_000_000:
|
|
return f"{n / 1_000_000_000_000:.3g}T", n
|
|
if n >= 1_000_000_000:
|
|
return f"{n / 1_000_000_000:.4g}B", n
|
|
if n >= 1_000_000:
|
|
return f"{n / 1_000_000:.4g}M", n
|
|
if n >= 1_000:
|
|
return f"{n / 1_000:.4g}K", n
|
|
return str(n), n
|
|
|
|
|
|
def _parse_params_from_name(repo_id):
|
|
name = (repo_id or "").rsplit("/", 1)[-1]
|
|
active = None
|
|
m_active = re.search(r"[-_][Aa](\d+(?:\.\d+)?)[Bb](?![a-zA-Z])", name)
|
|
if m_active:
|
|
active = int(float(m_active.group(1)) * 1_000_000_000)
|
|
name = name[: m_active.start()] + name[m_active.end() :]
|
|
total = None
|
|
for m in re.finditer(r"(\d+(?:\.\d+)?)[Bb](?![a-zA-Z])", name):
|
|
total = int(float(m.group(1)) * 1_000_000_000)
|
|
break
|
|
if total is None:
|
|
for m in re.finditer(r"(\d+(?:\.\d+)?)[Mm](?![a-zA-Z])", name):
|
|
total = int(float(m.group(1)) * 1_000_000)
|
|
break
|
|
return total or 0, active
|
|
|
|
|
|
def _infer_quant(repo_id, source):
|
|
name = (repo_id or "").rsplit("/", 1)[-1].lower()
|
|
if source.get("mlx_only"):
|
|
if "8bit" in name or "8-bit" in name:
|
|
return "mlx-8bit"
|
|
if "6bit" in name or "6-bit" in name:
|
|
return "mlx-6bit"
|
|
if "5bit" in name or "5-bit" in name:
|
|
return "mlx-5bit"
|
|
if "3bit" in name or "3-bit" in name:
|
|
return "mlx-3bit"
|
|
if re.search(r"(^|[-_/])bf16($|[-_/])", name):
|
|
return "BF16"
|
|
return "mlx-4bit"
|
|
if "awq" in name and ("8bit" in name or "8-bit" in name or "int8" in name):
|
|
return "AWQ-8bit"
|
|
if "awq" in name or "4bit" in name or "4-bit" in name:
|
|
return "AWQ-4bit"
|
|
if "gptq" in name and ("8bit" in name or "8-bit" in name or "int8" in name):
|
|
return "GPTQ-Int8"
|
|
if "gptq" in name:
|
|
return "GPTQ-Int4"
|
|
if "mxfp4" in name or "nvfp4" in name or re.search(r"(^|[-_/])fp4($|[-_/])", name):
|
|
return "FP4-MoE-Mixed"
|
|
if "mxfp8" in name or re.search(r"(^|[-_/])fp8($|[-_/])", name):
|
|
return "FP8-Mixed"
|
|
if "gguf" in name or "q4_k" in name or "q4-k" in name:
|
|
return "Q4_K_M"
|
|
if re.search(r"(^|[-_/])bf16($|[-_/])", name):
|
|
return "BF16"
|
|
return "BF16"
|
|
|
|
|
|
def _quant_bytes_per_param(quant):
|
|
return {
|
|
"BF16": 2.2,
|
|
"FP8": 1.15,
|
|
"FP8-Mixed": 1.15,
|
|
"FP4-MoE-Mixed": 0.62,
|
|
"AWQ-4bit": 0.62,
|
|
"AWQ-8bit": 1.15,
|
|
"GPTQ-Int4": 0.62,
|
|
"GPTQ-Int8": 1.15,
|
|
"Q4_K_M": 0.62,
|
|
"mlx-8bit": 1.25,
|
|
"mlx-6bit": 0.95,
|
|
"mlx-5bit": 0.82,
|
|
"mlx-4bit": 0.70,
|
|
"mlx-3bit": 0.55,
|
|
}.get(quant, 2.2)
|
|
|
|
|
|
def _infer_context(repo_id, pipeline_tag):
|
|
text = f"{repo_id or ''} {pipeline_tag or ''}".lower()
|
|
if any(k in text for k in ("whisper", "asr", "speech-recognition", "tts", "audio", "image", "video", "diffusion")):
|
|
return 4096
|
|
if any(k in text for k in ("glm-5.2", "deepseek-v4", "minimax-m3")):
|
|
return 1_000_000
|
|
if any(k in text for k in ("qwen3", "glm", "deepseek", "minimax")):
|
|
return 32768
|
|
return 32768
|
|
|
|
|
|
def _infer_use_case(repo_id, pipeline_tag):
|
|
text = f"{repo_id or ''} {pipeline_tag or ''}".lower()
|
|
if any(k in text for k in ("whisper", "asr", "speech-recognition", "transcrib")):
|
|
return "stt"
|
|
if any(k in text for k in ("tts", "text-to-speech", "kokoro", "audio")):
|
|
return "tts"
|
|
if any(k in text for k in ("image-text", "vision", "vlm", "vl-", "ocr", "multimodal")):
|
|
return "multimodal"
|
|
if any(k in text for k in ("code", "coder")):
|
|
return "coding"
|
|
if any(k in text for k in ("reason", "thinking", "thinker", "r1")):
|
|
return "reasoning"
|
|
return "general"
|
|
|
|
|
|
def _entry_from_collection_item(collection, item, source):
|
|
repo_id = item.get("id") or ""
|
|
if item.get("type") != "model" or not repo_id:
|
|
return None
|
|
repo_prefix = source.get("repo_prefix")
|
|
if repo_prefix and not repo_id.startswith(repo_prefix):
|
|
return None
|
|
raw_params = item.get("numParameters") or 0
|
|
active = None
|
|
if not raw_params:
|
|
raw_params, active = _parse_params_from_name(repo_id)
|
|
param_label, raw_params = _format_params(raw_params)
|
|
if not raw_params:
|
|
return None
|
|
|
|
quant = _infer_quant(repo_id, source)
|
|
pipeline_tag = item.get("pipeline_tag") or ""
|
|
min_ram = round((raw_params / 1_000_000_000) * _quant_bytes_per_param(quant) + 0.8, 1)
|
|
last_modified = item.get("lastModified") or collection.get("lastUpdated") or ""
|
|
release_date = ""
|
|
if last_modified:
|
|
try:
|
|
release_date = parsedate_to_datetime(last_modified).date().isoformat()
|
|
except Exception:
|
|
release_date = str(last_modified)[:10]
|
|
|
|
entry = {
|
|
"name": repo_id,
|
|
"provider": source.get("provider") or repo_id.split("/", 1)[0],
|
|
"parameter_count": param_label,
|
|
"parameters_raw": raw_params,
|
|
"min_ram_gb": min_ram,
|
|
"recommended_ram_gb": round(min_ram * 1.3 + 0.5, 1),
|
|
"min_vram_gb": 0.0 if source.get("mlx_only") else min_ram,
|
|
"quantization": quant,
|
|
"context_length": _infer_context(repo_id, pipeline_tag),
|
|
"use_case": _infer_use_case(repo_id, pipeline_tag),
|
|
"capabilities": ["mlx"] if source.get("mlx_only") else ["vllm", "sglang"],
|
|
"pipeline_tag": pipeline_tag,
|
|
"architecture": "",
|
|
"hf_downloads": int(item.get("downloads") or 0),
|
|
"hf_likes": int(item.get("likes") or 0),
|
|
"release_date": release_date,
|
|
"format": "mlx" if source.get("mlx_only") else "safetensors",
|
|
"collection": collection.get("title") or "",
|
|
"description": collection.get("description") or "",
|
|
"_discovered": True,
|
|
"_source": "hf_collections",
|
|
"_source_owner": source.get("owner") or "",
|
|
}
|
|
if source.get("mlx_only"):
|
|
entry["mlx_only"] = True
|
|
if quant == "Q4_K_M":
|
|
entry["is_gguf"] = True
|
|
entry["format"] = "gguf"
|
|
entry["capabilities"] = ["llama.cpp"]
|
|
if active:
|
|
entry["is_moe"] = True
|
|
entry["active_parameters"] = active
|
|
return entry
|
|
|
|
|
|
def _next_link(header):
|
|
if not header:
|
|
return None
|
|
m = re.search(r'<([^>]+)>;\s*rel="next"', header)
|
|
return m.group(1) if m else None
|
|
|
|
|
|
def fetch_collection_models(source, timeout=20, max_pages=20):
|
|
params = urllib.parse.urlencode({
|
|
"owner": source["owner"],
|
|
"limit": "100",
|
|
"expand": "true",
|
|
})
|
|
url = f"{HF_COLLECTIONS_URL}?{params}"
|
|
models = {}
|
|
pages = 0
|
|
while url and pages < max_pages:
|
|
req = urllib.request.Request(url, headers={"User-Agent": "odysseus-hwfit/1.0"})
|
|
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
|
payload = json.load(resp)
|
|
url = _next_link(resp.headers.get("Link"))
|
|
pages += 1
|
|
if not isinstance(payload, list):
|
|
break
|
|
for collection in payload:
|
|
if not isinstance(collection, dict):
|
|
continue
|
|
for item in collection.get("items") or []:
|
|
if not isinstance(item, dict):
|
|
continue
|
|
entry = _entry_from_collection_item(collection, item, source)
|
|
if entry and entry["name"] not in models:
|
|
models[entry["name"]] = entry
|
|
rows = list(models.values())
|
|
rows.sort(key=lambda x: (x.get("hf_downloads") or 0, x.get("release_date") or ""), reverse=True)
|
|
return rows
|
|
|
|
|
|
def _load_cache(path):
|
|
try:
|
|
with path.open(encoding="utf-8") as f:
|
|
data = json.load(f)
|
|
rows = data.get("models") if isinstance(data, dict) else data
|
|
return rows if isinstance(rows, list) else []
|
|
except (OSError, ValueError):
|
|
return []
|
|
|
|
|
|
def _write_cache(path, source, rows):
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
payload = {
|
|
"source": source,
|
|
"fetched_at": int(time.time()),
|
|
"count": len(rows),
|
|
"models": rows,
|
|
}
|
|
tmp = path.with_suffix(".json.tmp")
|
|
tmp.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
os.replace(tmp, path)
|
|
|
|
|
|
def load_cached_mlx_community_models():
|
|
return _load_cache(MLX_COMMUNITY_CACHE)
|
|
|
|
|
|
def load_cached_hf_collection_models():
|
|
return _load_cache(HF_COLLECTION_MODELS_CACHE)
|
|
|
|
|
|
def _cache_fresh(path):
|
|
try:
|
|
return (time.time() - path.stat().st_mtime) < HF_COLLECTION_TTL_SECONDS
|
|
except OSError:
|
|
return False
|
|
|
|
|
|
def refresh_mlx_community_cache(force=False):
|
|
if not force and _cache_fresh(MLX_COMMUNITY_CACHE):
|
|
return load_cached_mlx_community_models()
|
|
source = next(s for s in HF_COLLECTION_SOURCES if s["key"] == "mlx_community")
|
|
rows = fetch_collection_models(source)
|
|
_write_cache(MLX_COMMUNITY_CACHE, "https://huggingface.co/mlx-community/collections", rows)
|
|
return rows
|
|
|
|
|
|
def refresh_hf_collection_models_cache(force=False):
|
|
if not force and _cache_fresh(HF_COLLECTION_MODELS_CACHE):
|
|
return load_cached_hf_collection_models()
|
|
rows_by_name = {}
|
|
for source in HF_COLLECTION_SOURCES:
|
|
if source["key"] == "mlx_community":
|
|
continue
|
|
try:
|
|
for row in fetch_collection_models(source):
|
|
rows_by_name.setdefault(row["name"], row)
|
|
except Exception:
|
|
# Keep partial refreshes useful. A temporary DNS/provider issue for
|
|
# one brand should not invalidate the other cached collection rows.
|
|
continue
|
|
rows = sorted(
|
|
rows_by_name.values(),
|
|
key=lambda x: (x.get("hf_downloads") or 0, x.get("release_date") or ""),
|
|
reverse=True,
|
|
)
|
|
if rows:
|
|
_write_cache(HF_COLLECTION_MODELS_CACHE, "https://huggingface.co/collections", rows)
|
|
return rows
|
|
return load_cached_hf_collection_models()
|