How whichllm Groups and Deduplicates LLM Model Variants: A Deep Dive into the Source Code
whichllm groups and deduplicates LLM model variants by normalizing model identifiers into stable family keys, grouping first by base_model references and then by normalized names, and finally collapsing each group into a single ModelFamily object where benchmarks are merged and duplicates are hidden from CLI output.
The whichllm open-source project, as implemented in Andyyyy64/whichllm, tackles registry noise by ensuring that quantized files, instruction-tuned checkpoints, and re-uploaded prefixes of the same underlying LLM appear as one logical entry. To group and deduplicate LLM model variants, it runs a deterministic multi-pass pipeline implemented in src/whichllm/models/grouper.py with data structures defined in src/whichllm/models/types.py. The result is a concise list of ModelFamily instances that downstream code, including the CLI in src/whichllm/cli.py, renders for end users.
How Model Normalization Creates a Stable Family Key
At the heart of deduplication is the _normalize_name function inside src/whichllm/models/grouper.py. This method transforms a raw model ID into a deterministic key so that any two identifiers mapping to the same normalized name are treated as one family.
The function performs several cleaning steps:
- Strips prefixes – Organization slugs like
org/…and common meta-prefixes such asqwen_,meta-llama_, orgoogle_are removed. - Removes stacked suffixes – Tokens that denote quantization formats, chat/instruction variants, or date stamps are stripped in up to three passes to handle stacked suffixes such as
-gguf-q4or-instruct-2401. - Collapses versions and sizes – Patterns like
mistral-small-3.2-24bare rewritten tomistral-small-24b, and minor version numbers such as3.5are collapsed to3. - Returns a stable key – The final string, for example
qwen3-30b-a3bormeta-llama-3.1-8b, becomes the canonical identifier used for comparison.
Because the key is deterministic, two otherwise distinct model entries that differ only by quantizer tag or upload prefix will converge to the same family signature.
The Two-Pass Grouping Strategy
The group_models function in src/whichllm/models/grouper.py organizes records through two sequential grouping passes. This design ensures that explicit parent-child relationships are honored before falling back to name-based clustering.
Pass 1: Group by base_model Field
Every ModelInfo object may carry a base_model field that points to the upstream checkpoint it was fine-tuned from. In this phase:
- All models with a non-empty
base_modelare collected intobase_model_groupskeyed by the lower-casedbase_modelvalue. - Models that lack a
base_modelreference are placed into an ungrouped bucket for the next pass.
This captures direct derivation relationships, such as a GGUF re-upload declaring the original Transformers checkpoint as its parent.
Pass 2: Group by Normalised Name
After the first pass, the remaining ungrouped models are normalized:
- Each entry in
ungroupedis run through_normalize_nameand placed into name_groups. - The existing
base_model_groupsare also merged into a normalized view called merged_base, so groups that share the same normalized key are combined. - If a normalized name appears in both
merged_baseandname_groups, the two lists are merged into a single group.
This guarantees that a model is never counted twice, even when it could be matched both by explicit base_model lineage and by identifier similarity.
Family Construction and Deduplication Logic
For every resulting group—whether it originated from merged_base or name_groups—the algorithm constructs a final ModelFamily as defined in src/whichllm/models/types.py.
Base Model Selection
The algorithm chooses one representative from the group using the following priority:
- Referenced upstream checkpoint – Prefer a model that is explicitly referenced by another member’s
base_modelfield, since it is the true parent. - Clean, non-derived model – If no upstream exists, prefer a model that has no GGUF or quantization suffixes and does not itself declare a
base_model. - Fallback – If neither condition is met, pick any member.
- Tie-breaker – When multiple candidates satisfy the same rule, the model with the highest
downloadscount wins.
The chosen model’s normalized name becomes the family_id, and the same family_id is written back to every member via model.family_id = family_id.
Variant and Benchmark Aggregation
Once the base is selected:
- All other members are stored as variants inside the
ModelFamily. - Benchmark scores are merged across the entire family, keeping the highest value per metric in the
best_benchmarkdictionary.
The resulting ModelFamily carries:
family_id– the stable key derived from the base model.display_name– the human-readable name of the selected base.base_model– the fullModelInfoof the chosen representative.variants– the remainingModelInfoobjects belonging to the same family.best_benchmark– the best metric scores found across all variants.
Downstream code can now deduplicate the variants and present a single concise view to the user.
Code Example: Grouping Models Programmatically
You can reproduce the grouping behavior directly by importing group_models and passing a list of ModelInfo objects:
from whichllm.models.grouper import group_models
from whichllm.models.types import ModelInfo
# A few mock ModelInfo objects simulating registry variants
models = [
ModelInfo(
id="meta-llama/Meta-Llama-3.1-8B",
family_id="",
name="Meta Llama 3.1 8B",
parameter_count=8_000_000_000,
downloads=120_000,
base_model=None,
),
ModelInfo(
id="meta-llama/Meta-Llama-3.1-8B-gguf",
family_id="",
name="Meta Llama 3.1 8B (GGUF)",
parameter_count=8_000_000_000,
downloads=30_000,
gguf_variants=[],
base_model="meta-llama/Meta-Llama-3.1-8B",
),
ModelInfo(
id="someorg/Meta-Llama-3.1-8B-Instruct",
family_id="",
name="Meta Llama 3.1 8B Instruct",
parameter_count=8_000_000_000,
downloads=15_000,
base_model=None,
),
]
families = group_models(models)
for fam in families:
print(f"Family: {fam.family_id}")
print(f" Base: {fam.base_model.name} ({fam.base_model.id})")
print(f" Variants:")
for v in fam.variants:
print(f" - {v.name} ({v.id})")
Running this script demonstrates how the GGUF re-upload and the Instruct fork are folded under one family anchored by the original checkpoint.
How Deduplication Appears in the CLI
When you run whichllm list, the tool fetches model metadata, calls group_models, and renders only the display_name of each ModelFamily. The individual variants remain accessible in the data layer but are hidden from the default view.
$ whichllm list
Meta Llama 3.1 8B ← family (base + GGUF + Instruct variants)
Qwen 2.5 7B ← another family
...
Because every member shares the same family_id, the CLI can collapse duplicates without losing information about the representative model or the aggregated benchmarks.
Summary
- Normalization first –
_normalize_nameinsrc/whichllm/models/grouper.pystrips prefixes, removes stacked suffixes, and collapses version tokens to produce a stable family key. - Two-pass grouping – Models are first bucketed by explicit
base_modelreferences, then remaining entries are clustered by normalized name and merged safely. - Smart base selection – The algorithm prefers the true upstream checkpoint, falls back to a clean non-quantized model, and breaks ties by download count.
- Unified representation – Each family receives a shared
family_id, a list ofvariants, and abest_benchmarkscore map, enabling downstream consumers likesrc/whichllm/cli.pyto deduplicate output cleanly.
Frequently Asked Questions
What file contains the core deduplication logic in whichllm?
The core logic lives in src/whichllm/models/grouper.py. This file implements both the _normalize_name helper and the group_models entry point that orchestrates the two-pass clustering and family construction pipeline.
How does whichllm choose the base model for a family?
It applies a priority hierarchy: the preferred candidate is the upstream checkpoint referenced by another member’s base_model field. If none exists, it selects a model without quantization suffixes and without its own base_model, breaking ties by the highest downloads count.
Can models from different organizations be grouped into the same family?
Yes. Because _normalize_name strips organization prefixes and common meta-prefixes before comparison, two models uploaded by different users can map to the same normalized key and therefore be treated as variants of a single family.
Does whichllm merge benchmark scores across variants?
Yes. During family construction, the algorithm scans every member in the group and keeps the highest observed value for each metric. These aggregated scores are stored in the best_benchmark dictionary on the final ModelFamily object.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →