6.9 KiB
How it works
whichllm has one main job: start with the user's hardware, collect candidate models, estimate what can run, and rank the results.
The implementation is intentionally split into small packages:
src/whichllm/
├── cli.py
├── constants.py
├── data/
├── hardware/
├── models/
├── engine/
└── output/
Request flow
The default whichllm command follows this path:
- Validate CLI flags.
- Detect hardware.
- Load model cache or fetch from HuggingFace.
- Load benchmark cache or fetch benchmark sources.
- Group related model repos into families.
- Flatten families back into rankable candidates.
- Rank every candidate variant.
- Backfill missing published dates for top results.
- Print a Rich table or JSON.
The plan, upgrade, run, snippet, and hardware subcommands reuse parts
of the same pipeline.
Hardware detection
hardware/detector.py orchestrates detection. Each detector is fail-safe and
returns an empty result on failure.
| Module | Role |
|---|---|
hardware/nvidia.py |
Uses nvidia-ml-py; falls back to nvidia-smi, including optional memory-clock data |
hardware/amd.py |
Uses rocm-smi; falls back to lspci and /sys/class/drm |
hardware/intel.py |
Detects Linux Intel iGPUs through lspci or sysfs |
hardware/windows.py |
Detects Windows AMD and Intel fallback GPUs through WMI and registry memory fields |
hardware/apple.py |
Uses system_profiler on macOS |
hardware/cpu.py |
Reads CPU name, physical cores, AVX2, and AVX-512 |
hardware/memory.py |
Reads RAM and disk free space |
hardware/gpu_simulator.py |
Builds synthetic GPUs for --gpu |
The result is a HardwareInfo dataclass. GPUs are represented by GPUInfo.
Model fetching
models/fetcher.py reads the HuggingFace API and turns responses into
ModelInfo objects.
The fetcher combines several queries:
- Popular
text-generationmodels sorted by downloads. - Popular GGUF repos.
- Recently modified GGUF repos.
- Trending text-generation repos, with and without the GGUF filter.
- A curated list of frontier and hard-to-find model IDs.
- Vision candidates from
image-text-to-textwhen the active profile needs them.
For each repository, the parser extracts:
- repo ID and display name
- parameter count
- active parameter count for known or config-detected MoE models
- architecture and context length
- license, downloads, likes, published date
- GGUF variants and file sizes
cardData.base_model- conservative HuggingFace
evalResultsvalues
Parameter counts can come from safetensors metadata, GGUF metadata, config estimation, name hints, or a curated fallback table for important models with missing metadata.
Caches
Both caches normally live under ~/.cache/whichllm/. If XDG_CACHE_HOME is
set to an absolute path, whichllm uses $XDG_CACHE_HOME/whichllm/ instead.
| File | TTL | Contents |
|---|---|---|
models.json |
6 hours | Serialized ModelInfo data |
benchmark.json |
24 hours | Combined benchmark score map |
--refresh bypasses the relevant cache and writes a new one after fetching.
Benchmark sources
models/benchmark.py builds one score map from multiple sources. Scores are
normalized to a 0-100 scale before ranking.
whichllm separates sources into two tiers:
| Tier | Sources | Treatment |
|---|---|---|
| Current | LiveBench, Artificial Analysis, Aider, Vision | Overrides frozen scores for the same model |
| Frozen | Open LLM Leaderboard v2, Chatbot Arena ELO | Kept for older coverage, capped and recency-demoted |
Current sources can use live scrapes when reachable and curated snapshots when the upstream page shape changes. The snapshot month is printed below rankings.
Frozen-only scores are demoted by model lineage. This prevents an older model with a stale leaderboard score from outranking a newer generation simply because the newer model was never added to that frozen leaderboard.
Benchmark evidence
A model can receive benchmark evidence through several paths:
| Evidence | Meaning |
|---|---|
direct |
Exact independent benchmark match |
variant |
Suffix-stripped or -Instruct variant match |
base_model |
Match through HuggingFace cardData.base_model |
line_interp |
Size-aware interpolation within the same model line |
self_reported |
Uploader-provided evalResults only |
none |
No usable evidence |
Inheritance is rejected when the actual model size differs too much from the reference. This catches small draft heads, MTP heads, and unrelated forks that would otherwise borrow a larger base model's score.
Family grouping
models/grouper.py groups related repos by:
cardData.base_model, when available.- Normalized repository names.
The normalizer removes common suffixes such as -GGUF, -AWQ, -GPTQ,
-Instruct, -Chat, -FP16, and date suffixes. It also handles versioned
model lines such as Qwen, Llama, Mistral, and DeepSeek.
Within a family, the ranker evaluates all members and variants but keeps only the best result for the final table.
Candidate variants
For each ModelInfo, engine/ranker.py builds candidate variants:
- Existing GGUF files are evaluated by quantization.
- Extreme low-bit GGUF variants are skipped unless explicitly requested with
--quant. - Official safetensors-only repos can receive synthetic GGUF estimates for
common community conversions such as
Q4_K_M,Q5_K_M,Q6_K, andQ8_0. - Pre-quantized repos such as AWQ, GPTQ, FP8, and BF16 are not given synthetic GGUF variants.
Apple Silicon and CPU-only rankings are restricted to GGUF candidates for runtime stability. Linux with NVIDIA can also rank non-GGUF AWQ/GPTQ/FP16/BF16 repos.
Ranking
For each candidate variant:
- Estimate memory.
- Check whether it can run.
- Estimate tok/s and attach speed confidence/range metadata.
- Resolve benchmark evidence.
- Compute a quality score.
- Keep the best variant for the model family.
The final sorting key stays close to the displayed quality score, with a small direct-benchmark bonus and a CPU-only penalty. Full-GPU candidates are already favored inside the score through the runtime-fit and speed adjustments, so the sort key does not add a second full-GPU bonus.
See Scoring for the score details.
Output
Output is split by surface:
output/ranking.pyrenders hardware panels and recommendation tables.output/json_output.pyrenders ranking,plan, andupgradeJSON.output/plan.pyrendersplantables.output/upgrade.pyrenders upgrade comparison tables.output/display.pyre-exports those functions for older imports.
Normal ranking tables show memory required, estimated generation speed, fit
type, and published date. --details switches to download-oriented metadata.
Speed color is based on absolute usability, while ~ marks estimates with a
range and ? marks low-confidence, backend-sensitive estimates.