9e8f1bbeed
Dashboard / frontend (push) Failing after 0s
Dashboard / api (push) Failing after 0s
Lint PowerShell / powershell-lint (ubuntu-latest) (push) Failing after 1s
Python Lint / Lint Python with Ruff (push) Failing after 1s
ShellCheck / Lint shell scripts (push) Failing after 1s
Matrix Smoke / linux-smoke (push) Failing after 1s
Matrix Smoke / distro: cachyos (push) Failing after 15s
Matrix Smoke / distro: linux-mint-21.3 (push) Failing after 15s
Matrix Smoke / distro: debian-12 (push) Failing after 5m21s
Matrix Smoke / distro: fedora-41 (push) Failing after 4m56s
Matrix Smoke / distro: ubuntu-24.04 (push) Failing after 2m13s
Matrix Smoke / distro: rocky-9 (push) Failing after 10m39s
Matrix Smoke / distro: manjaro (push) Failing after 12m11s
Matrix Smoke / distro: opensuse-tw (push) Failing after 11m53s
Matrix Smoke / distro: archlinux (push) Failing after 20m3s
Matrix Smoke / distro: ubuntu-22.04 (push) Failing after 13m49s
Validate .env Schema / tier-1-env-validation (push) Successful in 52s
Validate .env Schema / tier-2-env-validation (push) Successful in 44s
Validate .env Schema / tier-3-env-validation (push) Successful in 52s
Validate .env Schema / tier-4-env-validation (push) Successful in 51s
Validate Extensions Catalog / Check catalog is up-to-date (push) Failing after 9m47s
Secret Scan / Scan for secrets (push) Failing after 21m4s
Validate Docker Compose / Validate Docker Compose files (push) Has been cancelled
Python Type Check / Type check with mypy (push) Has been cancelled
Validate .env Schema / tier-0-env-validation (push) Has been cancelled
Test Linux / integration-smoke (push) Has been cancelled
Lint PowerShell / powershell-lint (windows-latest) (push) Has been cancelled
Matrix Smoke / macos-smoke (push) Has been cancelled
1064 lines
45 KiB
Python
1064 lines
45 KiB
Python
"""Evidence-first performance metadata for the dashboard model library.
|
|
|
|
The oracle is deliberately conservative:
|
|
1. measured local throughput from this machine;
|
|
2. exact published benchmark match;
|
|
3. calibrated prediction from another local measurement on this machine;
|
|
4. benchmark required.
|
|
|
|
It never returns catalog tok/s estimates as observed performance.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import platform
|
|
import re
|
|
from pathlib import Path
|
|
from typing import Any, Optional
|
|
|
|
from gguf_inspector import inspect_gguf
|
|
from helpers import get_model_performance_samples, get_recorded_model_performance
|
|
from models import GPUInfo
|
|
|
|
|
|
_EVIDENCE_PATH = Path(__file__).with_name("performance_evidence.json")
|
|
_DEFAULT_RECOMMENDATION_POLICY = "catalog-fit-pre-download"
|
|
_VRAM_FIT_TOLERANCE_GB = 0.25
|
|
_MODEL_SELECTOR_POLICY = "context-aware-largest-capable-general-v1"
|
|
|
|
|
|
def normalize_key(value: Any) -> str:
|
|
return re.sub(r"[^a-z0-9]+", "-", str(value or "").lower()).strip("-")
|
|
|
|
|
|
def local_model_id(value: Any) -> str:
|
|
name = re.sub(r"[^A-Za-z0-9._-]+", "-", str(value or "")).strip("-._")
|
|
return name or "local-gguf"
|
|
|
|
|
|
def _list_value(value: Any) -> list[str]:
|
|
if value is None:
|
|
return []
|
|
if isinstance(value, list):
|
|
return [str(item) for item in value]
|
|
return [str(value)]
|
|
|
|
|
|
def _normalize_host_arch(value: Any) -> str:
|
|
key = normalize_key(value)
|
|
if key in {"aarch64", "arm64"}:
|
|
return "arm64"
|
|
if key in {"x86-64", "x86_64", "amd64", "x64"}:
|
|
return "amd64"
|
|
return key or "unknown"
|
|
|
|
|
|
def _system_ram_gb() -> int:
|
|
try:
|
|
if os.name == "nt":
|
|
import ctypes
|
|
|
|
class MEMORYSTATUSEX(ctypes.Structure):
|
|
_fields_ = [
|
|
("dwLength", ctypes.c_ulong),
|
|
("dwMemoryLoad", ctypes.c_ulong),
|
|
("ullTotalPhys", ctypes.c_ulonglong),
|
|
("ullAvailPhys", ctypes.c_ulonglong),
|
|
("ullTotalPageFile", ctypes.c_ulonglong),
|
|
("ullAvailPageFile", ctypes.c_ulonglong),
|
|
("ullTotalVirtual", ctypes.c_ulonglong),
|
|
("ullAvailVirtual", ctypes.c_ulonglong),
|
|
("sullAvailExtendedVirtual", ctypes.c_ulonglong),
|
|
]
|
|
|
|
stat = MEMORYSTATUSEX()
|
|
stat.dwLength = ctypes.sizeof(MEMORYSTATUSEX)
|
|
if ctypes.windll.kernel32.GlobalMemoryStatusEx(ctypes.byref(stat)):
|
|
return int(round(stat.ullTotalPhys / (1024**3)))
|
|
pages = os.sysconf("SC_PHYS_PAGES")
|
|
page_size = os.sysconf("SC_PAGE_SIZE")
|
|
return int(round((pages * page_size) / (1024**3)))
|
|
except (AttributeError, OSError, ValueError):
|
|
return 0
|
|
|
|
|
|
def read_env_value(key: str, install_dir: str | Path) -> str:
|
|
value = os.environ.get(key, "")
|
|
if value:
|
|
return value.strip().strip("\"'")
|
|
return read_env_file_value(key, install_dir)
|
|
|
|
|
|
def read_env_file_value(key: str, install_dir: str | Path) -> str:
|
|
env_path = Path(install_dir) / ".env"
|
|
try:
|
|
for line in env_path.read_text(encoding="utf-8").splitlines():
|
|
if line.startswith(f"{key}="):
|
|
return line.split("=", 1)[1].strip().strip("\"'")
|
|
except OSError:
|
|
pass
|
|
return ""
|
|
|
|
|
|
def model_files_dir(data_dir: str | Path) -> Path:
|
|
return Path(data_dir) / "models"
|
|
|
|
|
|
def _model_aliases(model: dict[str, Any]) -> set[str]:
|
|
aliases = {
|
|
str(model.get("id") or ""),
|
|
str(model.get("name") or ""),
|
|
str(model.get("gguf") or ""),
|
|
str(model.get("gguf_file") or ""),
|
|
str(model.get("llm_model_name") or ""),
|
|
}
|
|
aliases.update(str(alias or "") for alias in model.get("aliases", []))
|
|
for part in model.get("gguf_parts", []) or []:
|
|
if isinstance(part, dict):
|
|
aliases.add(str(part.get("file") or ""))
|
|
return {alias for alias in aliases if alias}
|
|
|
|
|
|
def normalize_catalog_entry(raw: dict[str, Any]) -> dict[str, Any] | None:
|
|
"""Convert config/model-library.json entries to the oracle shape."""
|
|
if not isinstance(raw, dict):
|
|
return None
|
|
gguf_parts = raw.get("gguf_parts") if isinstance(raw.get("gguf_parts"), list) else []
|
|
gguf = raw.get("gguf") or raw.get("gguf_file")
|
|
if not gguf and gguf_parts and isinstance(gguf_parts[0], dict):
|
|
gguf = gguf_parts[0].get("file")
|
|
|
|
model_id = raw.get("id") or raw.get("llm_model_name") or raw.get("name") or gguf
|
|
if not model_id or not gguf:
|
|
return None
|
|
|
|
try:
|
|
size_mb = float(raw.get("size_mb") or (float(raw.get("sizeGb", 0)) * 1024))
|
|
except (TypeError, ValueError):
|
|
size_mb = 0.0
|
|
try:
|
|
vram_required = float(raw.get("vram_required_gb") or raw.get("vramRequired") or 0)
|
|
except (TypeError, ValueError):
|
|
vram_required = 0.0
|
|
try:
|
|
context_length = int(raw.get("context_length") or raw.get("contextLength") or 0)
|
|
except (TypeError, ValueError):
|
|
context_length = 0
|
|
|
|
aliases = set(_model_aliases(raw))
|
|
if raw.get("llm_model_name"):
|
|
aliases.add(str(raw["llm_model_name"]))
|
|
|
|
model = {
|
|
"id": str(model_id),
|
|
"name": raw.get("name") or str(model_id),
|
|
"family": raw.get("family"),
|
|
"gguf": str(gguf),
|
|
"gguf_file": str(gguf),
|
|
"gguf_url": raw.get("gguf_url", ""),
|
|
"gguf_sha256": raw.get("gguf_sha256", ""),
|
|
"gguf_parts": gguf_parts,
|
|
"llm_model_name": raw.get("llm_model_name"),
|
|
"llama_server_image": raw.get("llama_server_image"),
|
|
"size_mb": size_mb,
|
|
"vram_required_gb": vram_required,
|
|
"context_length": context_length,
|
|
"specialty": raw.get("specialty", "General"),
|
|
"description": raw.get("description", ""),
|
|
"quantization": raw.get("quantization"),
|
|
"architecture": raw.get("architecture", "dense"),
|
|
"active_params_b": raw.get("active_params_b"),
|
|
"tokens_per_sec_estimate": raw.get("tokens_per_sec_estimate"),
|
|
"runtime_profiles": raw.get("runtime_profiles") if isinstance(raw.get("runtime_profiles"), list) else [],
|
|
"aliases": sorted(aliases),
|
|
}
|
|
if raw.get("decode_read_mb"):
|
|
model["decode_read_mb"] = raw["decode_read_mb"]
|
|
return model
|
|
|
|
|
|
def load_model_catalog(install_dir: str | Path) -> list[dict[str, Any]]:
|
|
path = Path(install_dir) / "config" / "model-library.json"
|
|
try:
|
|
data = json.loads(path.read_text(encoding="utf-8"))
|
|
except (OSError, json.JSONDecodeError):
|
|
return []
|
|
|
|
raw_models = data.get("models", [])
|
|
if not isinstance(raw_models, list):
|
|
return []
|
|
|
|
return [
|
|
model
|
|
for model in (normalize_catalog_entry(raw) for raw in raw_models)
|
|
if model is not None
|
|
]
|
|
|
|
|
|
def collect_runtime_flags(install_dir: str | Path) -> dict[str, str]:
|
|
mapping = {
|
|
"LLAMA_ARG_N_CPU_MOE": "n_cpu_moe",
|
|
"LLAMA_N_CPU_MOE": "n_cpu_moe",
|
|
"LLAMA_ARG_CACHE_TYPE_K": "cache_type_k",
|
|
"LLAMA_CACHE_TYPE_K": "cache_type_k",
|
|
"LLAMA_ARG_CACHE_TYPE_V": "cache_type_v",
|
|
"LLAMA_CACHE_TYPE_V": "cache_type_v",
|
|
"LLAMA_ARG_FLASH_ATTN": "flash_attn",
|
|
"LLAMA_FLASH_ATTN": "flash_attn",
|
|
"LLAMA_ARG_CHECKPOINT_EVERY_N_TOKENS": "checkpoint_every_n_tokens",
|
|
"LLAMA_CHECKPOINT_EVERY_N_TOKENS": "checkpoint_every_n_tokens",
|
|
"LLAMA_ARG_NO_CACHE_PROMPT": "no_cache_prompt",
|
|
"LLAMA_NO_CACHE_PROMPT": "no_cache_prompt",
|
|
}
|
|
flags: dict[str, str] = {}
|
|
for env_name, canonical in mapping.items():
|
|
value = read_env_value(env_name, install_dir)
|
|
if value and canonical not in flags:
|
|
flags[canonical] = value
|
|
return flags
|
|
|
|
|
|
def load_evidence(path: Path = _EVIDENCE_PATH) -> list[dict[str, Any]]:
|
|
try:
|
|
data = json.loads(path.read_text(encoding="utf-8"))
|
|
except (OSError, json.JSONDecodeError):
|
|
return []
|
|
entries = data.get("entries", [])
|
|
return entries if isinstance(entries, list) else []
|
|
|
|
|
|
def format_size(size_mb: int | float) -> str:
|
|
gb = float(size_mb) / 1024
|
|
return f"{gb:.1f} GB" if gb >= 1 else f"{int(size_mb)} MB"
|
|
|
|
|
|
def current_model_matches(model: dict[str, Any], current_model: str | None, current_gguf: str | None = None) -> bool:
|
|
haystack = normalize_key(" ".join([current_model or "", current_gguf or ""]))
|
|
if not haystack:
|
|
return False
|
|
for alias in _model_aliases(model):
|
|
key = normalize_key(alias)
|
|
if key and (key == haystack or key in haystack or haystack in key):
|
|
return True
|
|
return False
|
|
|
|
|
|
def find_catalog_model(catalog: list[dict[str, Any]], model_name: str | None, gguf: str | None = None) -> dict[str, Any] | None:
|
|
if not model_name and not gguf:
|
|
return None
|
|
return next((model for model in catalog if current_model_matches(model, model_name, gguf)), None)
|
|
|
|
|
|
def _hardware_match(gpu_info: Optional[GPUInfo], context_length: Optional[int],
|
|
quantization: str | None, runtime: str | None = None) -> dict[str, Any]:
|
|
if not gpu_info:
|
|
return {
|
|
"backend": "unknown",
|
|
"gpu": None,
|
|
"vramGb": None,
|
|
"contextLength": context_length,
|
|
"quantization": quantization,
|
|
"runtime": runtime,
|
|
}
|
|
return {
|
|
"backend": gpu_info.gpu_backend,
|
|
"gpu": gpu_info.name,
|
|
"vramGb": round(gpu_info.memory_total_mb / 1024, 1),
|
|
"contextLength": context_length,
|
|
"quantization": quantization,
|
|
"runtime": runtime,
|
|
}
|
|
|
|
|
|
def _default_performance(source: str, label: str, hardware_match: dict[str, Any]) -> dict[str, Any]:
|
|
return {
|
|
"source": source,
|
|
"label": label,
|
|
"tokensPerSec": None,
|
|
"low": None,
|
|
"high": None,
|
|
"confidence": "none",
|
|
"sampleCount": 0,
|
|
"sourceUrl": None,
|
|
"hardwareMatch": hardware_match,
|
|
}
|
|
|
|
|
|
def _fits_declared_vram(required_gb: float, capacity_gb: float) -> bool:
|
|
"""Compare catalog VRAM requirements against detected memory.
|
|
|
|
GPU vendors report memory in MiB and marketing specs in rounded GB. A card
|
|
sold as 8GB commonly reports slightly below 8.0GiB, so keep a small fixed
|
|
tolerance to avoid marking exact-tier models as incompatible.
|
|
"""
|
|
return required_gb <= capacity_gb + _VRAM_FIT_TOLERANCE_GB
|
|
|
|
|
|
def _estimated_param_billions(model: dict[str, Any]) -> float:
|
|
"""Best-effort model scale from explicit metadata, name, then file size.
|
|
|
|
The selector needs this before a GGUF exists locally. Catalog file size is
|
|
still authoritative for disk/download size; this estimate is only for
|
|
context/KV memory pressure.
|
|
"""
|
|
for key in ("total_params_b", "params_b"):
|
|
try:
|
|
value = float(model.get(key) or 0)
|
|
if value > 0:
|
|
return value
|
|
except (TypeError, ValueError):
|
|
pass
|
|
|
|
numbers = []
|
|
for text in (model.get("id"), model.get("name"), model.get("llm_model_name"), model.get("gguf")):
|
|
numbers.extend(float(match) for match in re.findall(r"(\d+(?:\.\d+)?)\s*b", str(text or ""), re.I))
|
|
if numbers:
|
|
return max(numbers)
|
|
|
|
size_mb = float(model.get("size_mb") or 0)
|
|
if size_mb > 0:
|
|
# Q4_K_M GGUFs are roughly 0.55-0.65 GiB per billion params. Use the
|
|
# middle so unknown compact models still get a realistic KV estimate.
|
|
return max(size_mb / 600.0, 1.0)
|
|
return 4.0
|
|
|
|
|
|
def _estimated_context_kv_gb(model: dict[str, Any]) -> float:
|
|
context = max(int(model.get("context_length") or 0), 8192)
|
|
params_b = _estimated_param_billions(model)
|
|
# KV cache is architecture-dependent, but the catalog does not always carry
|
|
# hidden size/layer metadata before download. This intentionally estimates
|
|
# standard llama.cpp KV pressure for the requested context and lets published
|
|
# runtime-specific evidence (TurboQuant/DFlash/etc.) override performance,
|
|
# not baseline install compatibility.
|
|
kv_per_32k_gb = min(max(params_b * 0.12, 0.35), 3.5)
|
|
return round(kv_per_32k_gb * (context / 32768.0), 2)
|
|
|
|
|
|
def _selector_required_memory_gb(model: dict[str, Any]) -> float:
|
|
declared = float(model.get("vram_required_gb") or 0)
|
|
size_gb = float(model.get("size_mb") or 0) / 1024.0
|
|
context_kv_gb = _estimated_context_kv_gb(model)
|
|
if size_gb <= 0:
|
|
return round(declared, 2)
|
|
return round(max(declared, size_gb + context_kv_gb), 2)
|
|
|
|
|
|
def _matching_runtime_profile(model: dict[str, Any], gpu_info: Optional[GPUInfo],
|
|
system_ram_gb: int | None = None) -> dict[str, Any] | None:
|
|
if not gpu_info:
|
|
return None
|
|
backend = normalize_key(gpu_info.gpu_backend)
|
|
memory_type = (
|
|
"unified"
|
|
if backend == "apple"
|
|
or normalize_key(getattr(gpu_info, "memory_type", "")) == "unified"
|
|
or "strix-halo" in normalize_key(gpu_info.name)
|
|
else "discrete"
|
|
)
|
|
host_arch = _normalize_host_arch(platform.machine())
|
|
vram_gb = float(gpu_info.memory_total_mb or 0) / 1024.0
|
|
ram_gb = system_ram_gb if system_ram_gb is not None else _system_ram_gb()
|
|
for profile in model.get("runtime_profiles", []) or []:
|
|
if not isinstance(profile, dict):
|
|
continue
|
|
if normalize_key(profile.get("backend")) not in {"", backend}:
|
|
continue
|
|
allowed_arches = {_normalize_host_arch(item) for item in _list_value(profile.get("host_arch"))}
|
|
if allowed_arches and host_arch not in allowed_arches:
|
|
continue
|
|
required_memory_type = normalize_key(profile.get("memory_type"))
|
|
if required_memory_type and required_memory_type != memory_type:
|
|
continue
|
|
try:
|
|
if profile.get("vram_min_gb") is not None and vram_gb < float(profile["vram_min_gb"]):
|
|
continue
|
|
if profile.get("vram_max_gb") is not None and vram_gb > float(profile["vram_max_gb"]):
|
|
continue
|
|
if profile.get("system_ram_min_gb") is not None and float(ram_gb or 0) < float(profile["system_ram_min_gb"]):
|
|
continue
|
|
except (TypeError, ValueError):
|
|
continue
|
|
return profile
|
|
return None
|
|
|
|
|
|
def _effective_context_length(model: dict[str, Any], runtime_profile: dict[str, Any] | None = None) -> int:
|
|
if runtime_profile and runtime_profile.get("context_length"):
|
|
return int(runtime_profile["context_length"])
|
|
return int(model.get("context_length") or 0)
|
|
|
|
|
|
def _effective_required_memory_gb(model: dict[str, Any],
|
|
runtime_profile: dict[str, Any] | None = None) -> float:
|
|
if runtime_profile and runtime_profile.get("estimated_required_gb") is not None:
|
|
return round(float(runtime_profile["estimated_required_gb"]), 2)
|
|
if runtime_profile and runtime_profile.get("context_length"):
|
|
model = {**model, "context_length": int(runtime_profile["context_length"])}
|
|
return _selector_required_memory_gb(model)
|
|
|
|
|
|
def _usable_model_memory_gb(gpu_info: Optional[GPUInfo]) -> float:
|
|
if not gpu_info:
|
|
return 0.0
|
|
total_gb = gpu_info.memory_total_mb / 1024
|
|
backend = normalize_key(gpu_info.gpu_backend)
|
|
if backend == "apple" or "strix-halo" in normalize_key(gpu_info.name):
|
|
return max(total_gb * 0.55, 2.0)
|
|
return total_gb
|
|
|
|
|
|
def _tokens_performance(source: str, label: str, tokens_per_second: float, confidence: str,
|
|
hardware_match: dict[str, Any], sample_count: int = 1,
|
|
source_url: str | None = None, spread: float = 0.1) -> dict[str, Any]:
|
|
tps = round(float(tokens_per_second), 1)
|
|
low = round(tps * (1 - spread), 1)
|
|
high = round(tps * (1 + spread), 1)
|
|
return {
|
|
"source": source,
|
|
"label": label.format(tps=tps, low=low, high=high),
|
|
"tokensPerSec": tps,
|
|
"low": low,
|
|
"high": high,
|
|
"confidence": confidence,
|
|
"sampleCount": int(sample_count or 0),
|
|
"sourceUrl": source_url,
|
|
"hardwareMatch": hardware_match,
|
|
}
|
|
|
|
|
|
def _sample_tps(sample: Optional[dict[str, Any]]) -> Optional[float]:
|
|
if not sample:
|
|
return None
|
|
try:
|
|
tps = float(sample.get("tokens_per_second") or sample.get("tokensPerSecond") or 0)
|
|
except (TypeError, ValueError):
|
|
return None
|
|
return tps if tps > 0 else None
|
|
|
|
|
|
def _exact_sample(model: dict[str, Any], gpu_info: Optional[GPUInfo], context_length: Optional[int]) -> Optional[dict[str, Any]]:
|
|
if not gpu_info:
|
|
return None
|
|
for alias in _model_aliases(model):
|
|
sample = get_recorded_model_performance(
|
|
alias,
|
|
gpu_info.name,
|
|
gpu_info.gpu_backend,
|
|
context_length=context_length,
|
|
gguf=model.get("gguf"),
|
|
vram_total_mb=gpu_info.memory_total_mb,
|
|
)
|
|
if sample:
|
|
return sample
|
|
return None
|
|
|
|
|
|
def _published_exact(model: dict[str, Any], gpu_info: Optional[GPUInfo], context_length: Optional[int],
|
|
quantization: str | None, flags: dict[str, str],
|
|
runtime: str | None,
|
|
evidence: list[dict[str, Any]]) -> Optional[dict[str, Any]]:
|
|
if not gpu_info or not context_length:
|
|
return None
|
|
model_keys = {normalize_key(alias) for alias in _model_aliases(model)}
|
|
for entry in evidence:
|
|
entry_names = {normalize_key(entry.get("model_id"))}
|
|
entry_names.update(normalize_key(v) for v in entry.get("model_names", []))
|
|
if not model_keys.intersection(entry_names):
|
|
continue
|
|
if normalize_key(entry.get("backend")) != normalize_key(gpu_info.gpu_backend):
|
|
continue
|
|
if normalize_key(entry.get("gpu_name")) != normalize_key(gpu_info.name):
|
|
continue
|
|
if int(round(float(entry.get("vram_gb", 0)))) != int(round(gpu_info.memory_total_mb / 1024)):
|
|
continue
|
|
if int(entry.get("context_length", 0)) != int(context_length):
|
|
continue
|
|
if normalize_key(entry.get("quantization")) != normalize_key(quantization):
|
|
continue
|
|
entry_runtime = normalize_key(entry.get("runtime"))
|
|
current_runtime = normalize_key(runtime)
|
|
if entry_runtime and current_runtime and current_runtime not in entry_runtime and entry_runtime not in current_runtime:
|
|
continue
|
|
required_flags = entry.get("flags") or {}
|
|
if any(normalize_key(flags.get(k)) != normalize_key(v) for k, v in required_flags.items()):
|
|
continue
|
|
return entry
|
|
return None
|
|
|
|
|
|
def _best_calibration_sample(gpu_info: Optional[GPUInfo]) -> Optional[dict[str, Any]]:
|
|
if not gpu_info:
|
|
return None
|
|
best = None
|
|
for sample in get_model_performance_samples():
|
|
if normalize_key(sample.get("backend")) != normalize_key(gpu_info.gpu_backend):
|
|
continue
|
|
if normalize_key(sample.get("gpu")) != normalize_key(gpu_info.name):
|
|
continue
|
|
if sample.get("vram_total_mb") and abs(float(sample["vram_total_mb"]) - gpu_info.memory_total_mb) > 1024:
|
|
continue
|
|
if _sample_tps(sample) is None:
|
|
continue
|
|
if best is None or int(sample.get("sample_count", 0)) > int(best.get("sample_count", 0)):
|
|
best = sample
|
|
return best
|
|
|
|
|
|
def _model_decode_mb(model: dict[str, Any], metadata: dict[str, Any]) -> float:
|
|
if model.get("decode_read_mb"):
|
|
return float(model["decode_read_mb"])
|
|
if metadata.get("size_bytes"):
|
|
return max(float(metadata["size_bytes"]) / (1024 * 1024), 1.0)
|
|
if model.get("sizeGb"):
|
|
return max(float(model["sizeGb"]) * 1024, 1.0)
|
|
return max(float(model.get("size_mb") or 1), 1.0)
|
|
|
|
|
|
def _predicted_from_calibration(model: dict[str, Any], metadata: dict[str, Any],
|
|
calibration: Optional[dict[str, Any]],
|
|
hardware_match: dict[str, Any]) -> Optional[dict[str, Any]]:
|
|
base_tps = _sample_tps(calibration)
|
|
if not calibration or not base_tps:
|
|
return None
|
|
calibration_mb = float(calibration.get("decode_read_mb") or calibration.get("model_size_mb") or 0)
|
|
if calibration_mb <= 0:
|
|
return None
|
|
target_mb = _model_decode_mb(model, metadata)
|
|
predicted = max(base_tps * (calibration_mb / target_mb), 1.0)
|
|
count = int(calibration.get("sample_count", 1))
|
|
confidence = "medium" if count >= 3 else "low"
|
|
spread = 0.2 if confidence == "medium" else 0.35
|
|
return _tokens_performance(
|
|
"predicted_calibrated",
|
|
"{low}-{high} tok/s calibrated",
|
|
predicted,
|
|
confidence,
|
|
hardware_match,
|
|
sample_count=count,
|
|
spread=spread,
|
|
)
|
|
|
|
|
|
def evaluate_performance(model: dict[str, Any], gpu_info: Optional[GPUInfo], metadata: dict[str, Any],
|
|
is_loaded: bool, live_tps: float, context_length: Optional[int],
|
|
flags: dict[str, str], evidence: list[dict[str, Any]],
|
|
fits_total: bool, runtime: str | None = None) -> dict[str, Any]:
|
|
quantization = metadata.get("quantization")
|
|
if not quantization or quantization == "unknown":
|
|
quantization = model.get("quantization")
|
|
hardware_match = _hardware_match(gpu_info, context_length, quantization, runtime)
|
|
|
|
if gpu_info and not fits_total and not is_loaded:
|
|
return _default_performance("incompatible", "does not fit this GPU", hardware_match)
|
|
|
|
if is_loaded and live_tps > 0:
|
|
return _tokens_performance(
|
|
"measured_local",
|
|
"{tps} tok/s measured locally",
|
|
live_tps,
|
|
"high",
|
|
hardware_match,
|
|
sample_count=1,
|
|
)
|
|
|
|
sample = _exact_sample(model, gpu_info, context_length)
|
|
tps = _sample_tps(sample)
|
|
if tps is not None:
|
|
return _tokens_performance(
|
|
"measured_local",
|
|
"{tps} tok/s measured locally",
|
|
tps,
|
|
"high",
|
|
hardware_match,
|
|
sample_count=int(sample.get("sample_count", 1)),
|
|
)
|
|
|
|
published = _published_exact(model, gpu_info, context_length, quantization, flags, runtime, evidence)
|
|
if published:
|
|
return _tokens_performance(
|
|
"published_exact",
|
|
"{tps} tok/s published exact",
|
|
float(published["tokens_per_second"]),
|
|
"medium",
|
|
hardware_match,
|
|
sample_count=1,
|
|
source_url=published.get("source_url"),
|
|
spread=0.05,
|
|
)
|
|
|
|
prediction = _predicted_from_calibration(model, metadata, _best_calibration_sample(gpu_info), hardware_match)
|
|
if prediction:
|
|
return prediction
|
|
|
|
return _default_performance("benchmark_required", "benchmark required", hardware_match)
|
|
|
|
|
|
def build_sample_signature(model: dict[str, Any], gpu_info: Optional[GPUInfo],
|
|
context_length: Optional[int], install_dir: str | Path,
|
|
gguf_path: Path | None = None) -> dict[str, Any]:
|
|
metadata = inspect_gguf(gguf_path) if gguf_path else {}
|
|
return {
|
|
"model_id": model.get("id"),
|
|
"gguf": model.get("gguf"),
|
|
"quantization": metadata.get("quantization") if metadata.get("quantization") != "unknown" else model.get("quantization"),
|
|
"architecture": metadata.get("architecture") or model.get("architecture", "unknown"),
|
|
"context_length": context_length or model.get("context_length"),
|
|
"decode_read_mb": _model_decode_mb(model, metadata),
|
|
"backend": gpu_info.gpu_backend if gpu_info else "unknown",
|
|
"gpu": gpu_info.name if gpu_info else None,
|
|
"vram_total_mb": gpu_info.memory_total_mb if gpu_info else None,
|
|
"os": platform.platform(),
|
|
"flags": collect_runtime_flags(install_dir),
|
|
}
|
|
|
|
|
|
def _recommendation_from_env(install_dir: str | Path) -> dict[str, Any]:
|
|
recommended_context = read_env_value("MODEL_RECOMMENDED_CONTEXT", install_dir)
|
|
return {
|
|
"source": read_env_value("MODEL_RECOMMENDATION_SOURCE", install_dir) or "installer_configured",
|
|
"confidence": read_env_value("MODEL_RECOMMENDATION_CONFIDENCE", install_dir) or "medium",
|
|
"reason": read_env_value("MODEL_RECOMMENDATION_REASON", install_dir) or "",
|
|
"performanceSource": read_env_value("MODEL_PERFORMANCE_SOURCE", install_dir) or "benchmark_required",
|
|
"performanceLabel": read_env_value("MODEL_PERFORMANCE_LABEL", install_dir) or "Benchmark after first launch",
|
|
"model": read_env_value("MODEL_RECOMMENDED_MODEL", install_dir) or read_env_value("LLM_MODEL", install_dir) or None,
|
|
"gguf": read_env_value("MODEL_RECOMMENDED_GGUF", install_dir) or read_env_value("GGUF_FILE", install_dir) or None,
|
|
"contextLength": int(recommended_context) if str(recommended_context).isdigit() else None,
|
|
"selectionPolicy": read_env_value("MODEL_RECOMMENDATION_POLICY", install_dir) or _DEFAULT_RECOMMENDATION_POLICY,
|
|
}
|
|
|
|
|
|
def _host_amd_runtime_gpu_from_env(install_dir: str | Path, system_ram_gb: int) -> Optional[GPUInfo]:
|
|
"""Build a conservative GPU surrogate for Windows AMD native runtimes.
|
|
|
|
On Windows AMD installs, dashboard-api runs inside Docker while Lemonade or
|
|
llama-server runs on the host. The container often cannot inspect the host
|
|
APU/GPU directly, so get_gpu_info() can be None even though the host runtime
|
|
can load a downloaded model. Use the installer-written runtime contract
|
|
instead of falling back to an artificial 4GB compatibility ceiling.
|
|
"""
|
|
def runtime_value(key: str) -> str:
|
|
return read_env_file_value(key, install_dir) or os.environ.get(key, "")
|
|
|
|
gpu_backend = normalize_key(runtime_value("GPU_BACKEND"))
|
|
if gpu_backend != "amd" or system_ram_gb <= 0:
|
|
return None
|
|
|
|
location = normalize_key(runtime_value("AMD_INFERENCE_LOCATION"))
|
|
runtime = normalize_key(runtime_value("AMD_INFERENCE_RUNTIME"))
|
|
llm_backend = normalize_key(runtime_value("LLM_BACKEND"))
|
|
managed = normalize_key(runtime_value("AMD_INFERENCE_MANAGED"))
|
|
if location not in {"host", "local", ""}:
|
|
return None
|
|
if runtime not in {"lemonade", "llama-server", ""} and llm_backend != "lemonade":
|
|
return None
|
|
if managed in {"false", "no", "off"} and not runtime and llm_backend != "lemonade":
|
|
return None
|
|
profile_text = normalize_key(" ".join([
|
|
runtime_value("MODEL_RUNTIME_PROFILE"),
|
|
runtime_value("MODEL_RUNTIME_PROFILE_LABEL"),
|
|
runtime_value("MODEL_RECOMMENDATION_POLICY"),
|
|
runtime_value("MODEL_RECOMMENDATION_REASON"),
|
|
]))
|
|
if not any(marker in profile_text for marker in ("strix", "unified-memory", "unified")):
|
|
return None
|
|
|
|
total_mb = int(system_ram_gb * 1024)
|
|
return GPUInfo(
|
|
name="AMD Strix Halo host runtime",
|
|
memory_used_mb=0,
|
|
memory_total_mb=total_mb,
|
|
memory_percent=0,
|
|
utilization_percent=0,
|
|
temperature_c=0,
|
|
memory_type="unified",
|
|
gpu_backend="amd",
|
|
)
|
|
|
|
|
|
def _catalog_fit_reason(model: dict[str, Any], gpu_info: Optional[GPUInfo], configured: bool) -> str:
|
|
runtime_profile = model.get("_runtime_profile") if isinstance(model.get("_runtime_profile"), dict) else None
|
|
context_k = int(_effective_context_length(model, runtime_profile) / 1024) if _effective_context_length(model, runtime_profile) else 0
|
|
required = _effective_required_memory_gb(model, runtime_profile)
|
|
if runtime_profile:
|
|
prefix = "Selected by the installer" if configured else "Recommended from catalog runtime profile"
|
|
label = runtime_profile.get("label") or runtime_profile.get("id") or "advanced runtime profile"
|
|
return (
|
|
f"{prefix}: {model['name']} uses {label}, needs about {required:g}GB GPU headroom "
|
|
f"plus {runtime_profile.get('system_ram_min_gb', 'documented')}GB system RAM, "
|
|
f"and provides {context_k}K context. Benchmark locally after first launch."
|
|
)
|
|
if gpu_info:
|
|
detected = round(gpu_info.memory_total_mb / 1024, 1)
|
|
usable = round(_usable_model_memory_gb(gpu_info), 1)
|
|
if usable < detected:
|
|
basis = f"{usable}GB usable {gpu_info.gpu_backend.upper()} memory ({detected}GB detected)"
|
|
else:
|
|
basis = f"{detected}GB {gpu_info.gpu_backend.upper()} memory"
|
|
else:
|
|
basis = "detected local hardware"
|
|
prefix = "Selected by the installer" if configured else "Recommended from catalog fit"
|
|
return (
|
|
f"{prefix}: {model['name']} needs about {required:g}GB including context/KV, "
|
|
f"fits {basis}, and provides {context_k}K context. Benchmark locally after first launch."
|
|
)
|
|
|
|
|
|
def _model_profile(install_dir: str | Path | None = None, explicit_profile: str | None = None) -> str:
|
|
profile = explicit_profile or (read_env_value("MODEL_PROFILE", install_dir) if install_dir else "") or "qwen"
|
|
normalized = normalize_key(profile)
|
|
if normalized in {"gemma", "gemma4", "gemma-4"}:
|
|
return "gemma4"
|
|
if normalized == "auto":
|
|
return "auto"
|
|
return "qwen"
|
|
|
|
|
|
def _family_allowed_for_profile(model: dict[str, Any], profile: str) -> bool:
|
|
family = normalize_key(model.get("family"))
|
|
if profile == "gemma4":
|
|
# Keep the tiny Qwen bootstrap fallback available for the minimum tier,
|
|
# but otherwise honor the Gemma profile choice.
|
|
return family == "gemma4" or model.get("id") == "qwen3.5-2b-q4"
|
|
# The default profile is the broad open-model lane. It keeps Gemma out so a
|
|
# user who explicitly wants Gemma does not get a Qwen-family recommendation
|
|
# and vice versa, while still allowing Phi/DeepSeek entries in the catalog.
|
|
return family != "gemma4"
|
|
|
|
|
|
def _recommendation_score(model: dict[str, Any], capacity_gb: float, profile: str) -> float:
|
|
runtime_profile = model.get("_runtime_profile") if isinstance(model.get("_runtime_profile"), dict) else None
|
|
required = _effective_required_memory_gb(model, runtime_profile)
|
|
size_mb = max(float(model.get("size_mb") or 1), 1.0)
|
|
context = max(_effective_context_length(model, runtime_profile), 8192)
|
|
specialty = str(model.get("specialty") or "General")
|
|
family = normalize_key(model.get("family"))
|
|
|
|
specialty_weight = {
|
|
"Code": 4.4,
|
|
"Quality": 4.1,
|
|
"General": 3.8,
|
|
"Balanced": 3.5,
|
|
"Reasoning": 3.3,
|
|
"Fast": 2.0,
|
|
"Bootstrap": 1.0,
|
|
}.get(specialty, 2.5)
|
|
family_bonus = 0.35 if (profile == "gemma4" and family == "gemma4") else 0.0
|
|
family_bonus += 0.25 if (profile in {"qwen", "auto"} and family == "qwen") else 0.0
|
|
context_bonus = min(context / 32768, 4.0) * 0.18
|
|
capability = min(size_mb / 1024, 48.0) * 0.24
|
|
|
|
# Exact-tier cards are valid, but prefer a little headroom when two models
|
|
# are otherwise similar. This prevents a 4GB card from picking a 4GB model
|
|
# over a nearly-equivalent 3GB option while still allowing 8GB-class picks.
|
|
fit_ratio = required / max(capacity_gb, 1.0)
|
|
headroom_penalty = 0.0
|
|
if fit_ratio > 0.98:
|
|
headroom_penalty = 0.35
|
|
elif fit_ratio > 0.92:
|
|
headroom_penalty = 0.15
|
|
|
|
return specialty_weight + family_bonus + context_bonus + capability - headroom_penalty
|
|
|
|
|
|
def rank_pre_download_models(catalog: list[dict[str, Any]], gpu_info: Optional[GPUInfo],
|
|
profile: str = "qwen", installable_only: bool = False,
|
|
limit: int = 3, system_ram_gb: int | None = None) -> list[dict[str, Any]]:
|
|
"""Rank catalog entries before any model is installed.
|
|
|
|
The ranker uses only compatibility metadata from model-library.json and the
|
|
detected hardware envelope. It intentionally does not turn catalog tok/s
|
|
estimates into displayed performance.
|
|
"""
|
|
if not catalog:
|
|
return []
|
|
|
|
normalized_profile = _model_profile(explicit_profile=profile)
|
|
capacity_gb = _usable_model_memory_gb(gpu_info) if gpu_info else 4.0
|
|
|
|
candidates = []
|
|
for model in catalog:
|
|
if installable_only and not model.get("gguf_url"):
|
|
continue
|
|
if not _family_allowed_for_profile(model, normalized_profile):
|
|
continue
|
|
runtime_profile = _matching_runtime_profile(model, gpu_info, system_ram_gb)
|
|
candidate_model = {**model, "_runtime_profile": runtime_profile} if runtime_profile else model
|
|
required = _effective_required_memory_gb(candidate_model, runtime_profile)
|
|
fits = _fits_declared_vram(required, capacity_gb)
|
|
if not fits:
|
|
continue
|
|
candidates.append({
|
|
"model": candidate_model,
|
|
"score": _recommendation_score(candidate_model, capacity_gb or max(required, 1.0), normalized_profile),
|
|
})
|
|
|
|
if not candidates:
|
|
fallback_pool = [
|
|
model for model in catalog
|
|
if (not installable_only or model.get("gguf_url")) and _family_allowed_for_profile(model, normalized_profile)
|
|
] or catalog
|
|
fallback = min(fallback_pool, key=lambda m: float(m.get("vram_required_gb") or 999))
|
|
candidates = [{"model": fallback, "score": -1.0}]
|
|
|
|
ranked = sorted(
|
|
candidates,
|
|
key=lambda item: (
|
|
item["score"],
|
|
_effective_required_memory_gb(item["model"], item["model"].get("_runtime_profile")),
|
|
_effective_context_length(item["model"], item["model"].get("_runtime_profile")),
|
|
),
|
|
reverse=True,
|
|
)
|
|
return [item["model"] for item in ranked[:max(limit, 1)]]
|
|
|
|
|
|
def select_pre_download_model(catalog: list[dict[str, Any]], gpu_info: Optional[GPUInfo]) -> dict[str, Any] | None:
|
|
"""Select the best catalog candidate when no installer choice exists.
|
|
|
|
This uses the project-maintained `vram_required_gb` compatibility field,
|
|
then prefers larger capable models and longer context. It is a fit
|
|
recommendation, not a performance estimate.
|
|
"""
|
|
ranked = rank_pre_download_models(catalog, gpu_info, profile="qwen", limit=1)
|
|
return ranked[0] if ranked else None
|
|
|
|
|
|
def _recommendation_alternative(model: dict[str, Any], gpu_info: Optional[GPUInfo]) -> dict[str, Any]:
|
|
runtime_profile = model.get("_runtime_profile") if isinstance(model.get("_runtime_profile"), dict) else _matching_runtime_profile(model, gpu_info)
|
|
context = _effective_context_length(model, runtime_profile)
|
|
vram_required = float(model.get("vram_required_gb") or 0)
|
|
selector_required = _effective_required_memory_gb(model, runtime_profile)
|
|
return {
|
|
"id": model.get("id"),
|
|
"name": model.get("name"),
|
|
"model": model.get("llm_model_name") or model.get("id"),
|
|
"gguf": model.get("gguf"),
|
|
"vramRequired": vram_required,
|
|
"estimatedRequired": selector_required,
|
|
"contextLength": context,
|
|
"specialty": model.get("specialty"),
|
|
"runtimeProfile": runtime_profile.get("id") if runtime_profile else None,
|
|
"fitsVram": _fits_declared_vram(selector_required, _usable_model_memory_gb(gpu_info) if gpu_info else 4.0),
|
|
"reason": _catalog_fit_reason({**model, "_runtime_profile": runtime_profile} if runtime_profile else model, gpu_info, configured=False),
|
|
}
|
|
|
|
|
|
def _downloaded_catalog_path(model: dict[str, Any], downloaded_files: dict[str, Path]) -> tuple[bool, Path | None, set[str]]:
|
|
parts = model.get("gguf_parts") or []
|
|
if parts:
|
|
part_paths = [
|
|
downloaded_files.get(str(part.get("file", "")).lower())
|
|
for part in parts
|
|
if isinstance(part, dict)
|
|
]
|
|
downloaded = bool(part_paths) and all(part_paths)
|
|
seen = {str(part.get("file", "")).lower() for part in parts if isinstance(part, dict)}
|
|
return downloaded, part_paths[0] if downloaded else None, seen if downloaded else set()
|
|
|
|
gguf = str(model["gguf"]).lower()
|
|
path = downloaded_files.get(gguf)
|
|
return bool(path), path, {gguf} if path else set()
|
|
|
|
|
|
def build_models_payload(gpu_info: Optional[GPUInfo], loaded_model: Optional[str], live_tps: float,
|
|
install_dir: str | Path, data_dir: str | Path | None = None,
|
|
context_length: Optional[int] = None,
|
|
catalog: list[dict[str, Any]] | None = None,
|
|
evidence: list[dict[str, Any]] | None = None,
|
|
downloaded_files_override: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
catalog = [
|
|
model
|
|
for model in (normalize_catalog_entry(raw) for raw in (catalog or load_model_catalog(install_dir)))
|
|
if model is not None
|
|
]
|
|
evidence = load_evidence() if evidence is None else evidence
|
|
recommendation = _recommendation_from_env(install_dir)
|
|
configured_model = recommendation.get("model") or read_env_value("LLM_MODEL", install_dir)
|
|
configured_gguf = recommendation.get("gguf") or read_env_value("GGUF_FILE", install_dir)
|
|
configured_entry = find_catalog_model(catalog, configured_model, configured_gguf)
|
|
profile = _model_profile(install_dir)
|
|
try:
|
|
install_ram_gb = int(read_env_file_value("SYSTEM_RAM_GB", install_dir) or read_env_value("SYSTEM_RAM_GB", install_dir) or 0)
|
|
except ValueError:
|
|
install_ram_gb = 0
|
|
if gpu_info is None:
|
|
gpu_info = _host_amd_runtime_gpu_from_env(install_dir, install_ram_gb)
|
|
ranked_recommendations = rank_pre_download_models(catalog, gpu_info, profile=profile, limit=3, system_ram_gb=install_ram_gb or None)
|
|
recommended_entry = configured_entry or (ranked_recommendations[0] if ranked_recommendations else None)
|
|
flags = collect_runtime_flags(install_dir)
|
|
runtime = read_env_value("LLM_BACKEND", install_dir) or os.environ.get("LLM_BACKEND") or "llama-server"
|
|
runtime_profile_text = " ".join([
|
|
read_env_value("MODEL_RUNTIME_PROFILE", install_dir),
|
|
read_env_value("MODEL_RUNTIME_PROFILE_LABEL", install_dir),
|
|
read_env_value("LLAMA_SERVER_IMAGE", install_dir),
|
|
])
|
|
if "turboquant" in normalize_key(runtime_profile_text):
|
|
runtime = "turboquant"
|
|
data_root = Path(data_dir) if data_dir is not None else Path(install_dir) / "data"
|
|
models_dir = model_files_dir(data_root)
|
|
if downloaded_files_override is not None:
|
|
downloaded_files = {
|
|
str(name).lower(): value if isinstance(value, Path) else models_dir / str(name)
|
|
for name, value in downloaded_files_override.items()
|
|
}
|
|
else:
|
|
downloaded_files = {}
|
|
if models_dir.exists():
|
|
for p in models_dir.iterdir():
|
|
try:
|
|
if p.is_file() and p.name.lower().endswith(".gguf") and p.stat().st_size > 0:
|
|
downloaded_files[p.name.lower()] = p
|
|
except OSError:
|
|
continue
|
|
|
|
gpu_data = None
|
|
free_gb = 0.0
|
|
if gpu_info:
|
|
vram_total = round(gpu_info.memory_total_mb / 1024, 1)
|
|
vram_used = round(gpu_info.memory_used_mb / 1024, 1)
|
|
free_gb = max(vram_total - vram_used, 0.0)
|
|
gpu_data = {
|
|
"vramTotal": vram_total,
|
|
"vramUsed": vram_used,
|
|
"vramFree": round(free_gb, 1),
|
|
"name": gpu_info.name,
|
|
"backend": gpu_info.gpu_backend or "unknown",
|
|
}
|
|
|
|
response_models = []
|
|
seen_files: set[str] = set()
|
|
current_model_id = None
|
|
configured_model_id = configured_entry["id"] if configured_entry else configured_model or configured_gguf or None
|
|
|
|
def append_model(model: dict[str, Any], path: Path | None, status_if_not_loaded: str) -> None:
|
|
nonlocal current_model_id
|
|
is_loaded = bool(loaded_model and current_model_matches(model, loaded_model, loaded_model))
|
|
is_configured = configured_entry is not None and model["id"] == configured_entry["id"]
|
|
is_recommended = recommended_entry is not None and model["id"] == recommended_entry["id"]
|
|
if is_loaded:
|
|
current_model_id = model["id"]
|
|
metadata = inspect_gguf(path) if path else {"exists": False, "readable": False, "quantization": model.get("quantization", "unknown")}
|
|
runtime_profile = _matching_runtime_profile(model, gpu_info, install_ram_gb or None)
|
|
profile_context = _effective_context_length(model, runtime_profile)
|
|
recommended_context = recommendation.get("contextLength") if is_configured else None
|
|
actual_context = context_length if is_loaded and context_length else recommended_context or profile_context or model.get("context_length")
|
|
vram_required = float(model["vram_required_gb"])
|
|
selector_required = _effective_required_memory_gb({**model, "context_length": actual_context}, runtime_profile)
|
|
if gpu_info:
|
|
capacity_gb = _usable_model_memory_gb(gpu_info)
|
|
fits_total = bool(_fits_declared_vram(selector_required, capacity_gb) or is_loaded)
|
|
fits_current = bool(_fits_declared_vram(selector_required, free_gb) or is_loaded)
|
|
else:
|
|
fits_total = bool(_fits_declared_vram(selector_required, 4.0) or is_loaded)
|
|
fits_current = False
|
|
perf = evaluate_performance(model, gpu_info, metadata, is_loaded, live_tps, actual_context, flags, evidence, fits_total, runtime)
|
|
reason = recommendation.get("reason") if is_configured else ""
|
|
if is_recommended and not is_loaded and perf["source"] == "benchmark_required":
|
|
perf = {
|
|
**perf,
|
|
"label": recommendation["performanceLabel"],
|
|
"hardwareMatch": {
|
|
**perf["hardwareMatch"],
|
|
"recommendationSource": recommendation["source"] if is_configured else "catalog_fit_pre_download",
|
|
"recommendationConfidence": recommendation["confidence"] if is_configured else "medium",
|
|
},
|
|
}
|
|
quantization = metadata.get("quantization")
|
|
if not quantization or quantization == "unknown":
|
|
quantization = model.get("quantization")
|
|
model_recommendation = None
|
|
if is_recommended:
|
|
model_recommendation = {
|
|
**recommendation,
|
|
"source": recommendation["source"] if is_configured else "catalog_fit_pre_download",
|
|
"confidence": recommendation["confidence"] if is_configured else "medium",
|
|
"reason": reason or _catalog_fit_reason({**model, "_runtime_profile": runtime_profile}, gpu_info, is_configured),
|
|
"model": model.get("llm_model_name") or model["id"],
|
|
"gguf": model.get("gguf"),
|
|
"contextLength": actual_context,
|
|
}
|
|
response_models.append({
|
|
"id": model["id"],
|
|
"name": model["name"],
|
|
"gguf": model.get("gguf"),
|
|
"ggufParts": model.get("gguf_parts") or None,
|
|
"downloadUrl": model.get("gguf_url") or None,
|
|
"downloadSha256": model.get("gguf_sha256") or None,
|
|
"llmModelName": model.get("llm_model_name") or None,
|
|
"size": format_size(model["size_mb"]),
|
|
"sizeGb": round(float(model["size_mb"]) / 1024, 1),
|
|
"vramRequired": vram_required,
|
|
"estimatedRequired": selector_required,
|
|
"contextLength": actual_context,
|
|
"specialty": model["specialty"],
|
|
"description": model["description"],
|
|
"tokensPerSecEstimate": model.get("tokens_per_sec_estimate"),
|
|
"tokensPerSec": perf["tokensPerSec"],
|
|
"quantization": quantization,
|
|
"architecture": metadata.get("architecture") if metadata.get("architecture") != "unknown" else model.get("architecture", "dense"),
|
|
"activeParamsB": model.get("active_params_b"),
|
|
"metadata": {
|
|
"source": "gguf" if metadata.get("readable") else "catalog",
|
|
"readable": bool(metadata.get("readable")),
|
|
"blockCount": metadata.get("block_count"),
|
|
"expertCount": metadata.get("expert_count"),
|
|
"expertUsedCount": metadata.get("expert_used_count"),
|
|
},
|
|
"status": "loaded" if is_loaded else status_if_not_loaded,
|
|
"recommended": is_recommended,
|
|
"configured": is_configured,
|
|
"recommendation": model_recommendation,
|
|
"fitsVram": fits_total,
|
|
"fitsCurrentVram": fits_current,
|
|
"fitLabel": runtime_profile.get("fit_label") if runtime_profile else ("Fits GPU" if fits_total else "Too large"),
|
|
"runtimeProfile": {
|
|
"id": runtime_profile.get("id"),
|
|
"label": runtime_profile.get("label"),
|
|
"runtime": runtime_profile.get("runtime"),
|
|
"sourceUrl": runtime_profile.get("source_url"),
|
|
} if runtime_profile else None,
|
|
"performance": perf,
|
|
"performanceLabel": perf["label"],
|
|
})
|
|
|
|
for model in catalog:
|
|
downloaded, path, seen = _downloaded_catalog_path(model, downloaded_files)
|
|
seen_files.update(seen)
|
|
append_model(model, path, "downloaded" if downloaded else "available")
|
|
|
|
for path in downloaded_files.values():
|
|
if path.name.lower() in seen_files:
|
|
continue
|
|
if not path.exists():
|
|
continue
|
|
size_mb = path.stat().st_size / (1024 * 1024)
|
|
fallback = {
|
|
"id": local_model_id(path.stem),
|
|
"name": path.stem,
|
|
"gguf": path.name,
|
|
"size_mb": size_mb,
|
|
"vram_required_gb": round((size_mb / 1024) + 1.5, 1),
|
|
"context_length": int(read_env_value("MAX_CONTEXT", install_dir) or read_env_value("CTX_SIZE", install_dir) or 32768),
|
|
"specialty": "Local",
|
|
"description": "Locally installed GGUF model.",
|
|
"quantization": "GGUF",
|
|
"decode_read_mb": size_mb,
|
|
}
|
|
append_model(fallback, path, "downloaded")
|
|
|
|
return {
|
|
"models": response_models,
|
|
"gpu": gpu_data,
|
|
"currentModel": current_model_id,
|
|
"loadedModel": loaded_model,
|
|
"configuredModel": configured_model_id,
|
|
"recommendationPolicy": recommendation.get("selectionPolicy") or _DEFAULT_RECOMMENDATION_POLICY,
|
|
"recommendationAlternatives": [
|
|
_recommendation_alternative(model, gpu_info)
|
|
for model in ranked_recommendations
|
|
],
|
|
}
|