3008 lines
127 KiB
Python
3008 lines
127 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
Scraper for popular LLM models from Hugging Face.
|
||
Fetches model metadata and computes RAM/VRAM requirements from parameter counts.
|
||
Outputs a JSON file consumable by llmfit's models.rs.
|
||
|
||
Usage:
|
||
python3 scrape_hf_models.py # Curated + top 1000 by downloads
|
||
python3 scrape_hf_models.py --threads 8 # Same, with parallel fetches
|
||
python3 scrape_hf_models.py -n 500 # Curated + top 500 by downloads
|
||
python3 scrape_hf_models.py --no-discover # Curated list only
|
||
"""
|
||
|
||
import argparse
|
||
import concurrent.futures
|
||
import json
|
||
import os
|
||
import re
|
||
import sys
|
||
import time
|
||
import urllib.request
|
||
import urllib.error
|
||
|
||
HF_API = "https://huggingface.co/api/models"
|
||
|
||
# Global auth token, set from --token flag or HF_TOKEN / HUGGING_FACE_HUB_TOKEN env var
|
||
_hf_token: str | None = None
|
||
|
||
|
||
def _auth_headers() -> dict[str, str]:
|
||
"""Return HTTP headers with auth if a HuggingFace token is available."""
|
||
headers = {"User-Agent": "llmfit-scraper/1.0"}
|
||
if _hf_token:
|
||
headers["Authorization"] = f"Bearer {_hf_token}"
|
||
return headers
|
||
|
||
# Top text-generation models to scrape (owner/repo)
|
||
TARGET_MODELS = [
|
||
# Meta Llama family
|
||
"meta-llama/Llama-3.1-8B",
|
||
"meta-llama/Llama-3.1-8B-Instruct",
|
||
"meta-llama/Llama-3.1-70B-Instruct",
|
||
"meta-llama/Llama-3.1-405B-Instruct",
|
||
"meta-llama/Llama-3.2-1B",
|
||
"meta-llama/Llama-3.2-3B",
|
||
"meta-llama/Llama-3.2-11B-Vision-Instruct", # NEW: Multimodal vision model
|
||
"meta-llama/Llama-3.3-70B-Instruct",
|
||
# Meta Llama 4 (MoE)
|
||
"meta-llama/Llama-4-Scout-17B-16E-Instruct",
|
||
"meta-llama/Llama-4-Maverick-17B-128E-Instruct",
|
||
# Code Llama
|
||
"meta-llama/CodeLlama-7b-Instruct-hf", # NEW: Popular code model
|
||
"meta-llama/CodeLlama-13b-Instruct-hf", # NEW: Larger code model
|
||
"meta-llama/CodeLlama-34b-Instruct-hf", # NEW: Large code model
|
||
# Mistral
|
||
"mistralai/Mistral-7B-Instruct-v0.3",
|
||
"mistralai/Mixtral-8x7B-Instruct-v0.1",
|
||
"mistralai/Mixtral-8x22B-Instruct-v0.1",
|
||
"mistralai/Mistral-Large-Instruct-2407",
|
||
"mistralai/Mistral-Small-24B-Instruct-2501",
|
||
"mistralai/Mistral-Small-3.1-24B-Instruct-2503",
|
||
"mistralai/Ministral-8B-Instruct-2410",
|
||
"mistralai/Mistral-Nemo-Instruct-2407",
|
||
"mistralai/Devstral-Small-2505",
|
||
# Qwen
|
||
"Qwen/Qwen2.5-7B-Instruct",
|
||
"Qwen/Qwen2.5-14B-Instruct",
|
||
"Qwen/Qwen2.5-32B-Instruct",
|
||
"Qwen/Qwen2.5-72B-Instruct",
|
||
"Qwen/Qwen2.5-Coder-1.5B-Instruct", # NEW: Ultra-lightweight coder
|
||
"Qwen/Qwen2.5-Coder-7B-Instruct", # NEW: Popular coder
|
||
"Qwen/Qwen2.5-Coder-14B-Instruct", # NEW: Mid-size coder
|
||
"Qwen/Qwen2.5-Coder-32B-Instruct", # NEW: Large coder
|
||
"Qwen/Qwen2.5-VL-3B-Instruct", # NEW: Vision-language 3B
|
||
"Qwen/Qwen2.5-VL-7B-Instruct", # NEW: Vision-language 7B
|
||
"Qwen/Qwen3-0.6B",
|
||
"Qwen/Qwen3-1.7B",
|
||
"Qwen/Qwen3-4B",
|
||
"Qwen/Qwen3-8B",
|
||
"Qwen/Qwen3-14B",
|
||
"Qwen/Qwen3-32B",
|
||
"Qwen/Qwen3-30B-A3B",
|
||
"Qwen/Qwen3-235B-A22B",
|
||
"Qwen/Qwen3-Coder-480B-A35B-Instruct",
|
||
"Qwen/Qwen3-Coder-Next",
|
||
# Qwen 3.5 (native multimodal, Feb 2026)
|
||
"Qwen/Qwen3.5-27B",
|
||
"Qwen/Qwen3.5-35B-A3B",
|
||
"Qwen/Qwen3.5-122B-A10B",
|
||
"Qwen/Qwen3.5-397B-A17B",
|
||
# Qwen3.5 Small Series (Instruct)
|
||
"Qwen/Qwen3.5-0.8B",
|
||
"Qwen/Qwen3.5-2B",
|
||
"Qwen/Qwen3.5-4B",
|
||
"Qwen/Qwen3.5-9B",
|
||
# Qwen3.5 Small Series (Base)
|
||
"Qwen/Qwen3.5-0.8B-Base",
|
||
"Qwen/Qwen3.5-2B-Base",
|
||
"Qwen/Qwen3.5-4B-Base",
|
||
"Qwen/Qwen3.5-9B-Base",
|
||
# Qwen 3.5 (Claude Opus 4.6 reasoning, Feb 2026)
|
||
"Jackrong/Qwen3.5-27B-Claude-4.6-Opus-Reasoning-Distilled",
|
||
"Jackrong/Qwen3.5-27B-Claude-4.6-Opus-Reasoning-Distilled-GGUF",
|
||
"Jackrong/Qwen3.5-9B-Claude-4.6-Opus-Reasoning-Distilled-v2",
|
||
"Jackrong/Qwen3.5-9B-Claude-4.6-Opus-Reasoning-Distilled-v2-GGUF",
|
||
"Jackrong/Qwen3.5-9B-Claude-4.6-Opus-Reasoning-Distilled-GGUF",
|
||
"Jackrong/Qwen3.5-35B-A3B-Claude-4.6-Opus-Reasoning-Distilled",
|
||
# Qwen 3.6 (native multimodal + hybrid attention, Apr 2026)
|
||
"Qwen/Qwen3.6-27B",
|
||
"Qwen/Qwen3.6-35B-A3B",
|
||
"huihui-ai/Huihui-Qwen3.6-35B-A3B-abliterated",
|
||
# Microsoft Phi
|
||
"microsoft/phi-3-mini-4k-instruct",
|
||
"microsoft/Phi-3-medium-14b-instruct",
|
||
"microsoft/Phi-3.5-mini-instruct", # NEW: Newer Phi variant
|
||
"microsoft/phi-4",
|
||
"microsoft/Phi-4-mini-instruct",
|
||
# Microsoft Orca
|
||
"microsoft/Orca-2-7b", # NEW: Reasoning model
|
||
"microsoft/Orca-2-13b", # NEW: Larger reasoning model
|
||
# Google Gemma
|
||
"google/gemma-2-2b-it", # NEW: Smaller variant for edge
|
||
"google/gemma-2-9b-it",
|
||
"google/gemma-2-27b-it",
|
||
"google/gemma-3-1b-it",
|
||
"google/gemma-3-4b-it",
|
||
"google/gemma-3-12b-it",
|
||
"google/gemma-3-27b-it",
|
||
# Google Gemma 4
|
||
"google/gemma-4-E2B-it",
|
||
"google/gemma-4-E4B-it",
|
||
"google/gemma-4-31B-it",
|
||
"google/gemma-4-26B-A4B-it",
|
||
# DeepSeek
|
||
"deepseek-ai/DeepSeek-R1-Distill-Qwen-7B",
|
||
"deepseek-ai/DeepSeek-R1-Distill-Qwen-32B",
|
||
"deepseek-ai/DeepSeek-Coder-V2-Lite-Instruct",
|
||
"deepseek-ai/DeepSeek-V3",
|
||
"deepseek-ai/DeepSeek-R1",
|
||
# DeepSeek V4 family (MoE, hybrid attention, Apr 2026)
|
||
"deepseek-ai/DeepSeek-V4-Pro",
|
||
"deepseek-ai/DeepSeek-V4-Pro-Base",
|
||
"deepseek-ai/DeepSeek-V4-Flash",
|
||
"deepseek-ai/DeepSeek-V4-Flash-Base",
|
||
# Cohere
|
||
"CohereForAI/c4ai-command-r-v01",
|
||
"CohereForAI/c4ai-command-r-plus-08-2024",
|
||
"CohereForAI/c4ai-command-a-03-2025",
|
||
# 01.ai Yi family
|
||
"01-ai/Yi-6B-Chat", # NEW: Popular multilingual 6B
|
||
"01-ai/Yi-34B-Chat", # NEW: Popular multilingual 34B
|
||
# Upstage Solar
|
||
"upstage/SOLAR-10.7B-Instruct-v1.0", # NEW: High-performance 10.7B
|
||
# TII Falcon
|
||
"tiiuae/falcon-7b-instruct", # NEW: Popular UAE model
|
||
"tiiuae/falcon-40b-instruct",
|
||
"tiiuae/falcon-180B-chat",
|
||
"tiiuae/Falcon3-3B-Instruct",
|
||
"tiiuae/Falcon3-7B-Instruct",
|
||
"tiiuae/Falcon3-10B-Instruct",
|
||
# HuggingFace Zephyr
|
||
"HuggingFaceH4/zephyr-7b-beta", # NEW: Very popular fine-tune
|
||
# OpenChat
|
||
"openchat/openchat-3.5-0106", # NEW: Popular alternative
|
||
# LMSYS Vicuna
|
||
"lmsys/vicuna-7b-v1.5", # NEW: Popular community model
|
||
"lmsys/vicuna-13b-v1.5", # NEW: Larger Vicuna
|
||
# NousResearch
|
||
"NousResearch/Nous-Hermes-2-Mixtral-8x7B-DPO", # NEW: Popular fine-tune
|
||
# WizardLM
|
||
"WizardLMTeam/WizardLM-13B-V1.2", # NEW: Popular instruction model
|
||
# Code models
|
||
"bigcode/starcoder2-7b",
|
||
"bigcode/starcoder2-15b",
|
||
"WizardLMTeam/WizardCoder-15B-V1.0", # NEW: Code specialist
|
||
# Small / edge models
|
||
"TinyLlama/TinyLlama-1.1B-Chat-v1.0",
|
||
"stabilityai/stablelm-2-1_6b-chat",
|
||
# IBM Granite
|
||
"ibm-granite/granite-3.1-8b-instruct",
|
||
"ibm-granite/granite-4.0-h-tiny",
|
||
"ibm-granite/granite-4.0-h-micro",
|
||
"ibm-granite/granite-4.0-h-small",
|
||
# Allen Institute OLMo
|
||
"allenai/OLMo-2-0325-32B-Instruct",
|
||
# Zhipu GLM
|
||
"THUDM/glm-4-9b-chat",
|
||
# xAI Grok
|
||
"xai-org/grok-1",
|
||
# Moonshot Kimi
|
||
"moonshotai/Kimi-K2-Instruct",
|
||
# BigScience BLOOM
|
||
"bigscience/bloom",
|
||
# Baidu ERNIE
|
||
"baidu/ERNIE-4.5-300B-A47B-Paddle",
|
||
# Rednote dots.llm
|
||
"rednote-hilab/dots.llm1.inst",
|
||
# Meituan LongCat
|
||
"meituan/LongCat-Flash",
|
||
# Ant Group Ling
|
||
"inclusionAI/Ling-lite",
|
||
# Liquid AI LFM2 (dense)
|
||
"LiquidAI/LFM2-350M",
|
||
"LiquidAI/LFM2-700M",
|
||
"LiquidAI/LFM2-1.2B",
|
||
"LiquidAI/LFM2-2.6B",
|
||
"LiquidAI/LFM2-2.6B-Exp",
|
||
# Liquid AI LFM2 (MoE)
|
||
"LiquidAI/LFM2-8B-A1B",
|
||
"LiquidAI/LFM2-24B-A2B",
|
||
# Liquid AI LFM2.5
|
||
"LiquidAI/LFM2.5-1.2B-Base",
|
||
"LiquidAI/LFM2.5-1.2B-Instruct",
|
||
"LiquidAI/LFM2.5-1.2B-Thinking",
|
||
"LiquidAI/LFM2.5-1.2B-JP",
|
||
# Liquid AI LFM2 Vision-Language
|
||
"LiquidAI/LFM2-VL-450M",
|
||
"LiquidAI/LFM2-VL-1.6B",
|
||
"LiquidAI/LFM2-VL-3B",
|
||
"LiquidAI/LFM2.5-VL-1.6B",
|
||
# Liquid AI LFM2 Audio
|
||
"LiquidAI/LFM2-Audio-1.5B",
|
||
"LiquidAI/LFM2.5-Audio-1.5B",
|
||
# Text-to-speech models
|
||
"hexgrad/Kokoro-82M",
|
||
"microsoft/speecht5_tts",
|
||
"facebook/mms-tts-eng",
|
||
"suno/bark",
|
||
"coqui/XTTS-v2",
|
||
# Liquid AI Liquid Nanos (task-specific fine-tunes)
|
||
"LiquidAI/LFM2-1.2B-Tool",
|
||
"LiquidAI/LFM2-1.2B-RAG",
|
||
"LiquidAI/LFM2-1.2B-Extract",
|
||
"LiquidAI/LFM2-350M-Extract",
|
||
"LiquidAI/LFM2-350M-Math",
|
||
"LiquidAI/LFM2-350M-ENJP-MT",
|
||
"LiquidAI/LFM2-350M-PII-Extract-JP",
|
||
"LiquidAI/LFM2-ColBERT-350M",
|
||
"LiquidAI/LFM2-2.6B-Transcript",
|
||
# Embeddings (useful for RAG sizing)
|
||
"nomic-ai/nomic-embed-text-v1.5",
|
||
"BAAI/bge-large-en-v1.5",
|
||
# --- New models added Feb 2026 ---
|
||
# DeepSeek V3.2 family
|
||
"deepseek-ai/DeepSeek-V3.2",
|
||
"deepseek-ai/DeepSeek-V3.2-Speciale",
|
||
# Zhipu/Z.ai GLM-5
|
||
"zai-org/GLM-5",
|
||
# Moonshot Kimi K2.5
|
||
"moonshotai/Kimi-K2.5",
|
||
# MiniMax M3 / M2.7
|
||
"MiniMaxAI/MiniMax-M3",
|
||
"MiniMaxAI/MiniMax-M2.7",
|
||
# Xiaomi MiMo
|
||
"XiaomiMiMo/MiMo-V2-Flash",
|
||
"XiaomiMiMo/MiMo-7B-RL",
|
||
# NVIDIA Nemotron
|
||
"nvidia/Llama-3.3-Nemotron-Super-49B-v1",
|
||
"nvidia/NVIDIA-Nemotron-3-Nano-30B-A3B-BF16",
|
||
"nvidia/NVIDIA-Nemotron-Nano-9B-v2",
|
||
# Microsoft Phi-4 reasoning family
|
||
"microsoft/Phi-4-reasoning",
|
||
"microsoft/Phi-4-mini-reasoning",
|
||
"microsoft/Phi-4-multimodal-instruct",
|
||
# LG AI EXAONE Deep (reasoning)
|
||
"LGAI-EXAONE/EXAONE-Deep-2.4B",
|
||
"LGAI-EXAONE/EXAONE-Deep-32B",
|
||
# LG AI EXAONE 4.0
|
||
"LGAI-EXAONE/EXAONE-4.0-32B",
|
||
"LGAI-EXAONE/EXAONE-4.0-1.2B",
|
||
# HuggingFace SmolLM3
|
||
"HuggingFaceTB/SmolLM3-3B",
|
||
# Google Gemma 3n (effective parameter models)
|
||
"google/gemma-3n-E4B-it",
|
||
"google/gemma-3n-E2B-it",
|
||
# RWKV v7 — pure RNN/SSM, no KV cache (GGUF native via shoumenchougou)
|
||
"shoumenchougou/RWKV7-G1f-1.5B-GGUF",
|
||
"shoumenchougou/RWKV7-G1f-2.9B-GGUF",
|
||
"shoumenchougou/RWKV7-G1f-7.2B-GGUF",
|
||
"shoumenchougou/RWKV7-G1f-13.3B-GGUF",
|
||
# NCAI VAETKI
|
||
"nc-ai-consortium/VAETKI-7B-A1B",
|
||
"nc-ai-consortium/VAETKI-20B-A2B",
|
||
"NC-AI-consortium-VAETKI/VAETKI",
|
||
"nc-ai-consortium/VAETKI-VL-7B-A1B",
|
||
]
|
||
|
||
# Bytes-per-parameter for different quantization levels
|
||
QUANT_BPP = {
|
||
"F32": 4.0,
|
||
"F16": 2.0,
|
||
"BF16": 2.0,
|
||
"Q8_0": 1.0,
|
||
"Q6_K": 0.75,
|
||
"Q5_K_M": 0.625,
|
||
"Q4_K_M": 0.5,
|
||
"Q4_0": 0.5,
|
||
"Q3_K_M": 0.4375,
|
||
"Q2_K": 0.3125,
|
||
"AWQ-4bit": 0.5,
|
||
"AWQ-8bit": 1.0,
|
||
"GPTQ-Int4": 0.5,
|
||
"GPTQ-Int8": 1.0,
|
||
}
|
||
|
||
# Overhead multiplier for runtime memory beyond just model weights
|
||
RUNTIME_OVERHEAD = 1.2 # ~20% overhead for KV cache, activations, OS
|
||
|
||
# Known MoE (Mixture of Experts) architecture configurations
|
||
MOE_CONFIGS = {
|
||
"mixtral": {"num_experts": 8, "active_experts": 2},
|
||
"deepseek_v2": {"num_experts": 64, "active_experts": 6},
|
||
"deepseek_v3": {"num_experts": 256, "active_experts": 8},
|
||
"deepseek_v4": {"num_experts": 384, "active_experts": 6},
|
||
"qwen3_moe": {"num_experts": 128, "active_experts": 8},
|
||
"llama4": {"num_experts": 16, "active_experts": 1},
|
||
"grok": {"num_experts": 8, "active_experts": 2},
|
||
"glm5": {"num_experts": 256, "active_experts": 8},
|
||
"minimax_m2": {"num_experts": 32, "active_experts": 2},
|
||
"mimo_v2": {"num_experts": 128, "active_experts": 8},
|
||
"nemotron3_nano": {"num_experts": 128, "active_experts": 6},
|
||
"qwen3_5_moe": {"num_experts": 256, "active_experts": 8},
|
||
"qwen3_vl_moe": {"num_experts": 256, "active_experts": 8},
|
||
}
|
||
|
||
# Published active parameter counts for well-known MoE models
|
||
MOE_ACTIVE_PARAMS = {
|
||
"mistralai/Mixtral-8x7B-Instruct-v0.1": 12_900_000_000,
|
||
"mistralai/Mixtral-8x22B-Instruct-v0.1": 39_100_000_000,
|
||
"NousResearch/Nous-Hermes-2-Mixtral-8x7B-DPO": 12_900_000_000,
|
||
"deepseek-ai/DeepSeek-Coder-V2-Lite-Instruct": 2_400_000_000,
|
||
"deepseek-ai/DeepSeek-V3": 37_000_000_000,
|
||
"deepseek-ai/DeepSeek-R1": 37_000_000_000,
|
||
"deepseek-ai/DeepSeek-V3.2": 37_000_000_000,
|
||
"deepseek-ai/DeepSeek-V3.2-Speciale": 37_000_000_000,
|
||
"deepseek-ai/DeepSeek-V4-Pro": 49_000_000_000,
|
||
"deepseek-ai/DeepSeek-V4-Pro-Base": 49_000_000_000,
|
||
"deepseek-ai/DeepSeek-V4-Flash": 13_000_000_000,
|
||
"deepseek-ai/DeepSeek-V4-Flash-Base": 13_000_000_000,
|
||
"Qwen/Qwen3-30B-A3B": 3_300_000_000,
|
||
"Qwen/Qwen3-235B-A22B": 22_000_000_000,
|
||
"Qwen/Qwen3-Coder-480B-A35B-Instruct": 35_000_000_000,
|
||
"Qwen/Qwen3-Coder-Next": 3_000_000_000,
|
||
"Qwen/Qwen3.5-35B-A3B": 3_000_000_000,
|
||
"Qwen/Qwen3.5-122B-A10B": 10_000_000_000,
|
||
"Qwen/Qwen3.5-397B-A17B": 17_000_000_000,
|
||
"Qwen/Qwen3.6-35B-A3B": 3_000_000_000,
|
||
"huihui-ai/Huihui-Qwen3.6-35B-A3B-abliterated": 3_000_000_000, # Qwen3.6-35B-A3B finetune
|
||
"meta-llama/Llama-4-Scout-17B-16E-Instruct": 17_000_000_000,
|
||
"meta-llama/Llama-4-Maverick-17B-128E-Instruct": 17_000_000_000,
|
||
"xai-org/grok-1": 86_000_000_000,
|
||
"moonshotai/Kimi-K2-Instruct": 32_000_000_000,
|
||
"moonshotai/Kimi-K2.5": 32_000_000_000,
|
||
"zai-org/GLM-5": 40_000_000_000,
|
||
"MiniMaxAI/MiniMax-M3": 10_000_000_000,
|
||
"MiniMaxAI/MiniMax-M2.7": 10_000_000_000,
|
||
"XiaomiMiMo/MiMo-V2-Flash": 15_000_000_000,
|
||
"nvidia/NVIDIA-Nemotron-3-Nano-30B-A3B-BF16": 3_000_000_000,
|
||
"LiquidAI/LFM2-8B-A1B": 1_500_000_000,
|
||
"LiquidAI/LFM2-24B-A2B": 2_300_000_000, # 23.8B total, 2.3B active
|
||
"google/gemma-4-26B-A4B-it": 4_000_000_000,
|
||
"nc-ai-consortium/VAETKI-7B-A1B": 1_200_000_000,
|
||
"nc-ai-consortium/VAETKI-20B-A2B": 2_200_000_000,
|
||
"NC-AI-consortium-VAETKI/VAETKI": 10_100_000_000,
|
||
"nc-ai-consortium/VAETKI-VL-7B-A1B": 1_200_000_000,
|
||
}
|
||
|
||
# Model card lists 32k context; config.json exposes max_position_embeddings=131072.
|
||
CONTEXT_LENGTH_OVERRIDES = {
|
||
"NC-AI-consortium-VAETKI/VAETKI": 32_768,
|
||
}
|
||
|
||
|
||
def fetch_model_info(repo_id: str) -> dict | None:
|
||
"""Fetch model info from HuggingFace API."""
|
||
url = f"{HF_API}/{repo_id}"
|
||
req = urllib.request.Request(url, headers=_auth_headers())
|
||
try:
|
||
with urllib.request.urlopen(req, timeout=30) as resp:
|
||
return json.loads(resp.read().decode())
|
||
except urllib.error.HTTPError as e:
|
||
if e.code == 401 and not _hf_token:
|
||
print(f" ⚠ HTTP 401 for {repo_id} — model is gated, set HF_TOKEN to access",
|
||
file=sys.stderr)
|
||
else:
|
||
print(f" ⚠ HTTP {e.code} for {repo_id} — skipping", file=sys.stderr)
|
||
return None
|
||
except Exception as e:
|
||
print(f" ⚠ Error fetching {repo_id}: {e}", file=sys.stderr)
|
||
return None
|
||
|
||
|
||
def extract_license(info: dict | None) -> str | None:
|
||
"""Extract normalized license metadata from HuggingFace model info."""
|
||
if not info:
|
||
return None
|
||
|
||
card_data = info.get("cardData") or {}
|
||
license_value = card_data.get("license")
|
||
license_name = card_data.get("license_name")
|
||
|
||
if isinstance(license_name, str) and license_name.strip():
|
||
license_name = license_name.strip().lower()
|
||
else:
|
||
license_name = None
|
||
|
||
if isinstance(license_value, str) and license_value.strip():
|
||
license_value = license_value.strip().lower()
|
||
return license_name if license_value == "other" and license_name else license_value
|
||
if isinstance(license_value, list):
|
||
licenses = [str(item).strip().lower() for item in license_value if str(item).strip()]
|
||
if licenses:
|
||
return ",".join(licenses)
|
||
|
||
for tag in info.get("tags", []):
|
||
if isinstance(tag, str) and tag.startswith("license:"):
|
||
license_tag = tag.removeprefix("license:").strip().lower()
|
||
if license_tag:
|
||
return license_name if license_tag == "other" and license_name else license_tag
|
||
|
||
return None
|
||
|
||
|
||
def format_param_count(total_params: int) -> str:
|
||
"""Convert raw parameter count into human-readable string."""
|
||
if total_params >= 1_000_000_000:
|
||
val = total_params / 1_000_000_000
|
||
return f"{val:.1f}B" if val != int(val) else f"{int(val)}B"
|
||
elif total_params >= 1_000_000:
|
||
val = total_params / 1_000_000
|
||
return f"{val:.0f}M"
|
||
else:
|
||
return f"{total_params / 1_000:.0f}K"
|
||
|
||
|
||
def estimate_ram(total_params: int, quant: str) -> tuple[float, float]:
|
||
"""
|
||
Estimate min RAM (Q4 quantized) and recommended RAM (comfortable headroom).
|
||
Returns (min_ram_gb, recommended_ram_gb).
|
||
"""
|
||
bpp = QUANT_BPP.get(quant, 0.5)
|
||
model_size_gb = (total_params * bpp) / (1024**3)
|
||
min_ram_gb = model_size_gb * RUNTIME_OVERHEAD
|
||
# Recommended: enough for Q4 + generous KV cache + OS headroom
|
||
recommended_ram_gb = model_size_gb * 2.0
|
||
|
||
# Apply sensible floor
|
||
min_ram_gb = max(min_ram_gb, 1.0)
|
||
recommended_ram_gb = max(recommended_ram_gb, 2.0)
|
||
|
||
return round(min_ram_gb, 1), round(recommended_ram_gb, 1)
|
||
|
||
|
||
def estimate_vram(total_params: int, quant: str) -> float:
|
||
"""Estimate minimum VRAM to fit model weights on GPU."""
|
||
bpp = QUANT_BPP.get(quant, 0.5)
|
||
model_size_gb = (total_params * bpp) / (1024**3)
|
||
# VRAM needs to hold weights + some activation memory
|
||
vram_gb = model_size_gb * 1.1
|
||
return round(max(vram_gb, 0.5), 1)
|
||
|
||
|
||
def extract_arch_metadata(config: dict | None) -> dict:
|
||
"""Extract architecture fields for precise KV cache and MoE speed estimation.
|
||
|
||
Checks top-level config and falls back to ``text_config`` for multimodal
|
||
models (e.g. Llama 4 Scout, Qwen-VL). Returns a dict with architecture
|
||
fields (any may be ``None``).
|
||
"""
|
||
cfg = config or {}
|
||
# Try top-level first, then text_config for multimodal wrappers.
|
||
sources = [cfg]
|
||
if isinstance(cfg.get("text_config"), dict):
|
||
sources.append(cfg["text_config"])
|
||
|
||
num_hidden_layers = None
|
||
num_attention_heads = None
|
||
num_key_value_heads = None
|
||
head_dim = None
|
||
hidden_size = None
|
||
vocab_size = None
|
||
moe_intermediate_size = None
|
||
shared_expert_intermediate_size = None
|
||
|
||
for src in sources:
|
||
if num_hidden_layers is None:
|
||
num_hidden_layers = src.get("num_hidden_layers")
|
||
if num_attention_heads is None:
|
||
num_attention_heads = src.get("num_attention_heads")
|
||
if num_key_value_heads is None:
|
||
num_key_value_heads = src.get("num_key_value_heads")
|
||
if head_dim is None:
|
||
head_dim = src.get("head_dim")
|
||
if hidden_size is None:
|
||
hidden_size = src.get("hidden_size")
|
||
if head_dim is None and num_attention_heads and hidden_size:
|
||
head_dim = hidden_size // num_attention_heads
|
||
if vocab_size is None:
|
||
vocab_size = src.get("vocab_size")
|
||
if moe_intermediate_size is None:
|
||
# Prefer explicit moe_intermediate_size (Qwen, DeepSeek), fall
|
||
# back to intermediate_size which is the per-expert FFN dim in
|
||
# Mixtral-style MoE models that don't use a separate key.
|
||
v = src.get("moe_intermediate_size") or src.get("intermediate_size")
|
||
# Some models (e.g. ERNIE-4.5-VL) use a list; take first element.
|
||
if isinstance(v, list):
|
||
v = v[0] if v else None
|
||
moe_intermediate_size = v
|
||
if shared_expert_intermediate_size is None:
|
||
v = src.get("shared_expert_intermediate_size")
|
||
if isinstance(v, list):
|
||
v = v[0] if v else None
|
||
shared_expert_intermediate_size = v
|
||
|
||
# GQA default: if num_key_value_heads missing, assume MHA
|
||
if num_key_value_heads is None:
|
||
num_key_value_heads = num_attention_heads
|
||
|
||
return {
|
||
"num_hidden_layers": num_hidden_layers,
|
||
"num_attention_heads": num_attention_heads,
|
||
"num_key_value_heads": num_key_value_heads,
|
||
"head_dim": head_dim,
|
||
"hidden_size": hidden_size,
|
||
"vocab_size": vocab_size,
|
||
"moe_intermediate_size": moe_intermediate_size,
|
||
"shared_expert_intermediate_size": shared_expert_intermediate_size,
|
||
}
|
||
|
||
|
||
def detect_moe(repo_id: str, config: dict | None, architecture: str,
|
||
total_params: int) -> dict:
|
||
"""Detect MoE architecture and compute active parameters."""
|
||
result = {
|
||
"is_moe": False,
|
||
"num_experts": None,
|
||
"active_experts": None,
|
||
"active_parameters": None,
|
||
}
|
||
|
||
# Check config.json for MoE indicators (also check text_config for
|
||
# multimodal models like Llama 4 that nest MoE fields there)
|
||
num_experts = None
|
||
active_experts = None
|
||
if config:
|
||
num_experts = config.get("num_local_experts") or config.get("num_experts") or config.get("n_routed_experts")
|
||
active_experts = config.get("num_experts_per_tok") or config.get("top_k_experts")
|
||
if (not num_experts or not active_experts) and isinstance(config.get("text_config"), dict):
|
||
tc = config["text_config"]
|
||
num_experts = num_experts or tc.get("num_local_experts") or tc.get("num_experts") or tc.get("n_routed_experts")
|
||
active_experts = active_experts or tc.get("num_experts_per_tok") or tc.get("top_k_experts")
|
||
|
||
# Check if architecture is in known MoE configs
|
||
if architecture in MOE_CONFIGS:
|
||
moe = MOE_CONFIGS[architecture]
|
||
num_experts = num_experts or moe["num_experts"]
|
||
active_experts = active_experts or moe["active_experts"]
|
||
|
||
if num_experts and active_experts:
|
||
result["is_moe"] = True
|
||
result["num_experts"] = num_experts
|
||
result["active_experts"] = active_experts
|
||
|
||
# Use published active params if known, otherwise estimate
|
||
if repo_id in MOE_ACTIVE_PARAMS:
|
||
result["active_parameters"] = MOE_ACTIVE_PARAMS[repo_id]
|
||
else:
|
||
result["active_parameters"] = estimate_active_params(
|
||
total_params, num_experts, active_experts)
|
||
|
||
return result
|
||
|
||
|
||
def estimate_active_params(total_params: int, num_experts: int,
|
||
active_experts: int) -> int:
|
||
"""Estimate active parameters for MoE models.
|
||
|
||
Assumes expert MLP layers are ~95% of total params and
|
||
shared attention/embedding layers are ~5%.
|
||
"""
|
||
shared_fraction = 0.05
|
||
shared = int(total_params * shared_fraction)
|
||
expert_pool = total_params - shared
|
||
per_expert = expert_pool // num_experts
|
||
return shared + active_experts * per_expert
|
||
|
||
|
||
def estimate_params_from_arch(config: dict | None) -> int | None:
|
||
"""Estimate total parameter count from architecture metadata.
|
||
|
||
Uses the transformer parameter formula accounting for MoE expert weights.
|
||
Returns None if insufficient metadata is available.
|
||
"""
|
||
cfg = config or {}
|
||
# Check text_config for multimodal wrappers
|
||
for src in [cfg, cfg.get("text_config", {})]:
|
||
hidden = src.get("hidden_size")
|
||
layers = src.get("num_hidden_layers")
|
||
vocab = src.get("vocab_size")
|
||
if hidden and layers and vocab:
|
||
break
|
||
else:
|
||
return None
|
||
|
||
n_heads = src.get("num_attention_heads") or 1
|
||
n_kv = src.get("num_key_value_heads") or n_heads
|
||
head_dim = src.get("head_dim") or (hidden // n_heads if n_heads else hidden)
|
||
|
||
# Attention: Q + K + V + O projections per layer
|
||
attn = 2 * n_heads * head_dim * hidden + 2 * n_kv * head_dim * hidden
|
||
|
||
# FFN / expert weights
|
||
def _scalar(v, default=None):
|
||
"""Coerce list values (e.g. ERNIE-4.5-VL) to a single int."""
|
||
if isinstance(v, list):
|
||
return v[0] if v else default
|
||
return v if v is not None else default
|
||
|
||
num_experts = src.get("num_local_experts") or src.get("num_experts")
|
||
moe_inter = _scalar(src.get("moe_intermediate_size"))
|
||
shared_inter = _scalar(src.get("shared_expert_intermediate_size"), 0)
|
||
intermediate = _scalar(src.get("intermediate_size"))
|
||
|
||
if num_experts and moe_inter:
|
||
# MoE: per-expert FFN + shared expert
|
||
expert_ffn = num_experts * 3 * hidden * moe_inter
|
||
shared_ffn = 3 * hidden * shared_inter if shared_inter else 0
|
||
router = num_experts * hidden
|
||
ffn_total = expert_ffn + shared_ffn + router
|
||
elif intermediate:
|
||
# Dense: standard SwiGLU FFN (gate + up + down)
|
||
ffn_total = 3 * hidden * intermediate
|
||
else:
|
||
# Fallback: assume 4x hidden
|
||
ffn_total = 4 * hidden * hidden
|
||
|
||
per_layer = attn + ffn_total
|
||
embedding = 2 * vocab * hidden # embedding + lm_head
|
||
|
||
total = layers * per_layer + embedding
|
||
return total if total > 1_000_000 else None
|
||
|
||
|
||
def infer_use_case(repo_id: str, pipeline_tag: str | None, config: dict | None) -> str:
|
||
"""Infer a brief use-case description from model metadata."""
|
||
rid = repo_id.lower()
|
||
if pipeline_tag == "text-to-speech":
|
||
return "Text-to-speech"
|
||
if "embed" in rid or "bge" in rid:
|
||
return "Text embeddings for RAG"
|
||
if "coder" in rid or "starcoder" in rid or "code" in rid:
|
||
return "Code generation and completion"
|
||
if "r1" in rid or "reason" in rid:
|
||
return "Advanced reasoning, chain-of-thought"
|
||
if pipeline_tag in ("image-text-to-text", "any-to-any") or "-vl-" in rid:
|
||
return "Multimodal, vision and text"
|
||
if "instruct" in rid or "chat" in rid:
|
||
return "Instruction following, chat"
|
||
if "tiny" in rid or "small" in rid or "mini" in rid:
|
||
return "Lightweight, edge deployment"
|
||
if pipeline_tag == "text-generation":
|
||
return "General purpose text generation"
|
||
return "General purpose"
|
||
|
||
|
||
def infer_context_length(config: dict | None) -> int:
|
||
"""Try to extract context length from model config."""
|
||
if not config:
|
||
return 4096
|
||
|
||
# Common config keys for max sequence length
|
||
keys_to_check = [
|
||
"max_position_embeddings",
|
||
"max_sequence_length",
|
||
"seq_length",
|
||
"n_positions",
|
||
"sliding_window",
|
||
]
|
||
|
||
def _extract_from(cfg: dict) -> int | None:
|
||
for key in keys_to_check:
|
||
if key in cfg:
|
||
val = cfg[key]
|
||
if isinstance(val, int) and val > 0:
|
||
return val
|
||
return None
|
||
|
||
def _apply_rope_scaling(val: int, cfg: dict) -> int:
|
||
"""Apply RoPE scaling factor when present (e.g., Llama 4 Maverick
|
||
has max_position_embeddings=4096 but a rope_scaling factor of 256,
|
||
giving an effective context of 1M tokens)."""
|
||
rope = cfg.get("rope_scaling")
|
||
if isinstance(rope, dict) and isinstance(rope.get("factor"), (int, float)):
|
||
scaled = int(val * rope["factor"])
|
||
if scaled > val:
|
||
return scaled
|
||
return val
|
||
|
||
# Check top-level config
|
||
val = _extract_from(config)
|
||
if val is not None:
|
||
return _apply_rope_scaling(val, config)
|
||
|
||
# For multimodal models (e.g., Qwen3.5), check text_config
|
||
if "text_config" in config and isinstance(config["text_config"], dict):
|
||
tc = config["text_config"]
|
||
val = _extract_from(tc)
|
||
if val is not None:
|
||
return _apply_rope_scaling(val, tc)
|
||
|
||
return 4096
|
||
|
||
|
||
def fetch_config_json(repo_id: str) -> dict | None:
|
||
"""Fetch the full config.json from a HF repo (has max_position_embeddings)."""
|
||
url = f"https://huggingface.co/{repo_id}/resolve/main/config.json"
|
||
req = urllib.request.Request(url, headers=_auth_headers())
|
||
try:
|
||
with urllib.request.urlopen(req, timeout=15) as resp:
|
||
return json.loads(resp.read().decode())
|
||
except Exception:
|
||
return None
|
||
|
||
|
||
def extract_provider(repo_id: str) -> str:
|
||
"""Map HF org name to a friendly provider name."""
|
||
org = repo_id.split("/")[0].lower()
|
||
mapping = {
|
||
"meta-llama": "Meta",
|
||
"mistralai": "Mistral AI",
|
||
"qwen": "Alibaba",
|
||
"microsoft": "Microsoft",
|
||
"google": "Google",
|
||
"deepseek-ai": "DeepSeek",
|
||
"bigcode": "BigCode",
|
||
"cohereforai": "Cohere",
|
||
"tinyllama": "Community",
|
||
"stabilityai": "Stability AI",
|
||
"nomic-ai": "Nomic",
|
||
"baai": "BAAI",
|
||
"01-ai": "01.ai", # NEW
|
||
"upstage": "Upstage", # NEW
|
||
"tiiuae": "TII", # NEW
|
||
"huggingfaceh4": "HuggingFace", # NEW
|
||
"openchat": "OpenChat", # NEW
|
||
"lmsys": "LMSYS", # NEW
|
||
"nousresearch": "NousResearch", # NEW
|
||
"wizardlmteam": "WizardLM", # NEW
|
||
"liquidai": "Liquid AI",
|
||
"nc-ai-consortium-vaetki": "NCAI",
|
||
"nc-ai-consortium": "NCAI",
|
||
}
|
||
return mapping.get(org, org)
|
||
|
||
|
||
def infer_capabilities(repo_id: str, pipeline_tag: str | None, use_case: str) -> list[str]:
|
||
"""Infer model capabilities like vision and tool use."""
|
||
caps: list[str] = []
|
||
rid = repo_id.lower()
|
||
uc = use_case.lower()
|
||
|
||
if pipeline_tag == "text-to-speech":
|
||
caps.extend(["audio", "tts"])
|
||
|
||
# Vision
|
||
if (
|
||
pipeline_tag == "image-text-to-text"
|
||
or pipeline_tag == "any-to-any"
|
||
or "vision" in rid
|
||
or "-vl-" in rid
|
||
or rid.endswith("-vl")
|
||
or "llava" in rid
|
||
or "onevision" in rid
|
||
or "pixtral" in rid
|
||
or "vision" in uc
|
||
or "multimodal" in uc
|
||
):
|
||
caps.append("vision")
|
||
|
||
# Tool use (known families)
|
||
if (
|
||
"tool" in uc
|
||
or "function call" in uc
|
||
or "qwen3" in rid
|
||
or "qwen2.5" in rid
|
||
or "command-r" in rid
|
||
or ("llama-3" in rid and "instruct" in rid)
|
||
or ("mistral" in rid and "instruct" in rid)
|
||
or "hermes" in rid
|
||
or ("gemma-3" in rid and rid.endswith("-it"))
|
||
or ("gemma-4" in rid and rid.endswith("-it"))
|
||
):
|
||
caps.append("tool_use")
|
||
|
||
return caps
|
||
|
||
|
||
def _looks_like_language_tag(value: str, allow_bare_iso3: bool) -> bool:
|
||
parts = value.split("-")
|
||
primary = parts[0]
|
||
if not primary.isalpha():
|
||
return False
|
||
if len(primary) == 3 and not allow_bare_iso3:
|
||
return False
|
||
if len(primary) not in (2, 3):
|
||
return False
|
||
return all(2 <= len(part) <= 8 and part.isalnum() for part in parts[1:])
|
||
|
||
|
||
def _normalize_language(value: object, explicit_field: bool = False) -> str | None:
|
||
"""Return an explicit HF language tag, or None for non-language metadata."""
|
||
if not isinstance(value, str):
|
||
return None
|
||
lang = value.strip().lower().replace("_", "-")
|
||
prefixed = False
|
||
for prefix in ("language:", "languages:", "lang:"):
|
||
if lang.startswith(prefix):
|
||
lang = lang[len(prefix):]
|
||
prefixed = True
|
||
break
|
||
if _looks_like_language_tag(lang, allow_bare_iso3=prefixed or explicit_field):
|
||
return lang
|
||
return None
|
||
|
||
|
||
def infer_languages(info: dict | None, config: dict | None) -> list[str]:
|
||
"""Extract explicitly declared language metadata from HF fields."""
|
||
values: list[object] = []
|
||
for source in (info or {}, config or {}):
|
||
for key in ("language", "languages", "language_code", "language_codes"):
|
||
val = source.get(key)
|
||
if isinstance(val, list):
|
||
values.extend((item, True) for item in val)
|
||
elif val is not None:
|
||
values.append((val, True))
|
||
values.extend((tag, False) for tag in (info or {}).get("tags", []))
|
||
|
||
# Meta MMS per-language models (facebook/mms-tts-eng, facebook/mms-tts-deu,
|
||
# ...) declare no language metadata via the API; the target language is
|
||
# only encoded as an ISO-639-3 suffix in the repo name.
|
||
repo_id = (info or {}).get("id", "") or (info or {}).get("modelId", "")
|
||
repo_lower = repo_id.lower()
|
||
if "/mms-tts-" in repo_lower:
|
||
suffix = repo_lower.rsplit("mms-tts-", 1)[1]
|
||
if suffix:
|
||
values.append((suffix, True))
|
||
|
||
languages: list[str] = []
|
||
for value, explicit_field in values:
|
||
lang = _normalize_language(value, explicit_field=explicit_field)
|
||
if lang and lang not in languages:
|
||
languages.append(lang)
|
||
return languages
|
||
|
||
|
||
def detect_quant_format(repo_id: str, config: dict | None) -> tuple[str, str]:
|
||
"""Detect quantization format and label from config.json.
|
||
|
||
Returns (format, quant_label) where:
|
||
- format: "gguf", "awq", "gptq", "mlx", or "safetensors"
|
||
- quant_label: e.g. "AWQ-4bit", "GPTQ-Int4", "Q4_K_M"
|
||
"""
|
||
if not config:
|
||
return _detect_format_from_name(repo_id)
|
||
|
||
quant_config = config.get("quantization_config", {})
|
||
if not quant_config:
|
||
return _detect_format_from_name(repo_id)
|
||
|
||
quant_method = quant_config.get("quant_method", "")
|
||
bits = quant_config.get("bits", quant_config.get("num_bits", 4))
|
||
|
||
# AWQ
|
||
if quant_method == "awq":
|
||
label = f"AWQ-{bits}bit"
|
||
return ("awq", label)
|
||
|
||
# GPTQ (including gptq_marlin)
|
||
if quant_method.startswith("gptq"):
|
||
label = f"GPTQ-Int{bits}"
|
||
return ("gptq", label)
|
||
|
||
# AutoRound — pre-quantized safetensors, cannot be dynamically re-quantized
|
||
if quant_method == "auto-round":
|
||
label = f"AutoRound-{bits}bit"
|
||
return ("autoround", label)
|
||
|
||
# compressed-tensors: dig into config_groups for bits, check name for format
|
||
if quant_method == "compressed-tensors":
|
||
# Try to extract bits from config_groups
|
||
config_groups = quant_config.get("config_groups", {})
|
||
for group in config_groups.values():
|
||
if isinstance(group, dict):
|
||
weights = group.get("weights", {})
|
||
if "num_bits" in weights:
|
||
bits = weights["num_bits"]
|
||
break
|
||
|
||
name_upper = repo_id.upper()
|
||
if "-AWQ" in name_upper:
|
||
label = f"AWQ-{bits}bit"
|
||
return ("awq", label)
|
||
elif "-GPTQ" in name_upper:
|
||
label = f"GPTQ-Int{bits}"
|
||
return ("gptq", label)
|
||
elif "-AUTOROUND" in name_upper:
|
||
label = f"AutoRound-{bits}bit"
|
||
return ("autoround", label)
|
||
|
||
return _detect_format_from_name(repo_id)
|
||
|
||
|
||
def _detect_format_from_name(repo_id: str) -> tuple[str, str]:
|
||
"""Fallback: detect format from model name patterns."""
|
||
name_upper = repo_id.upper()
|
||
|
||
if "-AWQ-8BIT" in name_upper:
|
||
return ("awq", "AWQ-8bit")
|
||
if "-AWQ" in name_upper:
|
||
return ("awq", "AWQ-4bit")
|
||
if "-GPTQ-INT8" in name_upper or "-GPTQ-8BIT" in name_upper:
|
||
return ("gptq", "GPTQ-Int8")
|
||
if "-GPTQ" in name_upper:
|
||
return ("gptq", "GPTQ-Int4")
|
||
if "-AUTOROUND" in name_upper:
|
||
return ("autoround", "AutoRound-4bit")
|
||
if "-MLX-" in name_upper or name_upper.endswith("-MLX"):
|
||
return ("mlx", "Q4_K_M") # MLX uses its own quant scheme handled elsewhere
|
||
|
||
return ("gguf", "Q4_K_M")
|
||
|
||
|
||
def scrape_model(repo_id: str) -> dict | None:
|
||
"""Scrape a single model and return an LlmModel-compatible dict."""
|
||
if is_test_stub(repo_id):
|
||
print(f" ⚠ Skipping test stub {repo_id}", file=sys.stderr)
|
||
return None
|
||
info = fetch_model_info(repo_id)
|
||
if not info:
|
||
return None
|
||
|
||
# Extract parameter count from safetensors metadata
|
||
safetensors = info.get("safetensors", {})
|
||
total_params = safetensors.get("total")
|
||
if not total_params:
|
||
params_by_dtype = safetensors.get("parameters", {})
|
||
if params_by_dtype:
|
||
total_params = max(params_by_dtype.values())
|
||
|
||
if not total_params:
|
||
print(f" ⚠ No parameter count found for {repo_id}", file=sys.stderr)
|
||
return None
|
||
|
||
config = info.get("config", {})
|
||
pipeline_tag = info.get("pipeline_tag")
|
||
|
||
# Fetch full config.json for accurate context length
|
||
full_config = fetch_config_json(repo_id)
|
||
|
||
# Detect quantization format from config.json
|
||
model_format, default_quant = detect_quant_format(repo_id, full_config)
|
||
if pipeline_tag == "text-to-speech":
|
||
model_format, default_quant = ("safetensors", "F16")
|
||
context_length = CONTEXT_LENGTH_OVERRIDES.get(
|
||
repo_id,
|
||
infer_context_length(full_config) if full_config else infer_context_length(config),
|
||
)
|
||
|
||
# Correct parameters_raw when safetensors reports quantized element counts
|
||
# instead of true parameter count (common in FP8/INT4/INT8 repos).
|
||
arch_params = estimate_params_from_arch(full_config)
|
||
if arch_params and arch_params > total_params * 2:
|
||
total_params = arch_params
|
||
|
||
min_ram, rec_ram = estimate_ram(total_params, default_quant)
|
||
min_vram = estimate_vram(total_params, default_quant)
|
||
|
||
architecture = config.get("model_type", "unknown")
|
||
|
||
# Detect MoE architecture
|
||
moe_info = detect_moe(repo_id, full_config, architecture, total_params)
|
||
|
||
use_case_str = infer_use_case(repo_id, pipeline_tag, config)
|
||
|
||
# Architecture metadata for the precise KV cache formula. All optional;
|
||
# absent fields cause the Rust side to fall back to the linear approx.
|
||
arch_meta = extract_arch_metadata(full_config)
|
||
|
||
license_name = extract_license(info)
|
||
|
||
result = {
|
||
"name": repo_id,
|
||
"provider": extract_provider(repo_id),
|
||
"parameter_count": format_param_count(total_params),
|
||
"parameters_raw": total_params,
|
||
"min_ram_gb": min_ram,
|
||
"recommended_ram_gb": rec_ram,
|
||
"min_vram_gb": min_vram,
|
||
"quantization": default_quant,
|
||
"format": model_format,
|
||
"context_length": context_length,
|
||
"use_case": use_case_str,
|
||
"capabilities": infer_capabilities(repo_id, pipeline_tag, use_case_str),
|
||
"languages": infer_languages(info, full_config or config),
|
||
"pipeline_tag": pipeline_tag or "unknown",
|
||
"architecture": architecture,
|
||
"hf_downloads": info.get("downloads", 0),
|
||
"hf_likes": info.get("likes", 0),
|
||
"release_date": (info.get("createdAt") or "")[:10] or None,
|
||
**arch_meta,
|
||
}
|
||
|
||
if license_name:
|
||
result["license"] = license_name
|
||
|
||
# Add MoE fields if detected
|
||
if moe_info["is_moe"]:
|
||
result["is_moe"] = True
|
||
result["num_experts"] = moe_info["num_experts"]
|
||
result["active_experts"] = moe_info["active_experts"]
|
||
result["active_parameters"] = moe_info["active_parameters"]
|
||
|
||
return result
|
||
|
||
|
||
def scrape_models_parallel(repo_ids: list[str], threads: int) -> tuple[list[dict], set[str]]:
|
||
"""Scrape a batch of models with optional parallelism.
|
||
|
||
Returns (results, scraped_names).
|
||
"""
|
||
results: list[dict] = []
|
||
scraped_names: set[str] = set()
|
||
total = len(repo_ids)
|
||
|
||
if threads <= 1:
|
||
for i, repo_id in enumerate(repo_ids, 1):
|
||
print(f"[{i}/{total}] {repo_id}...")
|
||
model = scrape_model(repo_id)
|
||
if model:
|
||
print(f" ✓ {model['parameter_count']} params, "
|
||
f"min {model['min_ram_gb']} GB RAM, "
|
||
f"ctx {model['context_length']}")
|
||
results.append(model)
|
||
scraped_names.add(repo_id)
|
||
# Be polite to the API in single-thread mode.
|
||
time.sleep(0.3)
|
||
return results, scraped_names
|
||
|
||
print(f"Using {threads} threads for model scraping")
|
||
with concurrent.futures.ThreadPoolExecutor(max_workers=threads) as executor:
|
||
# executor.map keeps output aligned with input ordering while running concurrently.
|
||
for i, (repo_id, model) in enumerate(
|
||
zip(repo_ids, executor.map(scrape_model, repo_ids)),
|
||
1,
|
||
):
|
||
print(f"[{i}/{total}] {repo_id}...")
|
||
if model:
|
||
print(f" ✓ {model['parameter_count']} params, "
|
||
f"min {model['min_ram_gb']} GB RAM, "
|
||
f"ctx {model['context_length']}")
|
||
results.append(model)
|
||
scraped_names.add(repo_id)
|
||
|
||
return results, scraped_names
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# GGUF source enrichment — find pre-quantized GGUF repos for known models
|
||
# ---------------------------------------------------------------------------
|
||
|
||
# Providers known to publish high-quality GGUF quantizations
|
||
GGUF_PROVIDERS = ["unsloth", "bartowski", "ggml-org", "TheBloke", "mradermacher"]
|
||
|
||
GGUF_CACHE_FILE = os.path.join(os.path.dirname(__file__), "..", "data", "gguf_sources_cache.json")
|
||
GGUF_CACHE_MAX_AGE_DAYS = 7 # Re-check repos older than this
|
||
|
||
|
||
def _load_gguf_cache() -> dict:
|
||
"""Load the GGUF source cache from disk.
|
||
|
||
Returns dict mapping model repo_id -> {"sources": [...], "checked": ISO timestamp}
|
||
"""
|
||
try:
|
||
with open(GGUF_CACHE_FILE) as f:
|
||
return json.load(f)
|
||
except (FileNotFoundError, json.JSONDecodeError):
|
||
return {}
|
||
|
||
|
||
def _save_gguf_cache(cache: dict):
|
||
"""Save the GGUF source cache to disk."""
|
||
os.makedirs(os.path.dirname(GGUF_CACHE_FILE), exist_ok=True)
|
||
with open(GGUF_CACHE_FILE, "w") as f:
|
||
json.dump(cache, f, indent=2)
|
||
|
||
|
||
def _cache_entry_fresh(entry: dict) -> bool:
|
||
"""Check if a cache entry is still valid."""
|
||
try:
|
||
from datetime import datetime, timedelta, timezone
|
||
checked = datetime.fromisoformat(entry["checked"])
|
||
return (datetime.now(timezone.utc) - checked) < timedelta(days=GGUF_CACHE_MAX_AGE_DAYS)
|
||
except (KeyError, ValueError):
|
||
return False
|
||
|
||
|
||
def _model_gguf_repo_candidates(repo_id: str) -> list[tuple[str, str]]:
|
||
"""Generate candidate GGUF repo names for a model.
|
||
|
||
Returns list of (provider, candidate_repo_id) tuples.
|
||
e.g. for "meta-llama/Llama-3.1-8B-Instruct" →
|
||
[("unsloth", "unsloth/Llama-3.1-8B-Instruct-GGUF"),
|
||
("bartowski", "bartowski/Llama-3.1-8B-Instruct-GGUF")]
|
||
"""
|
||
model_name = repo_id.split("/")[-1]
|
||
candidates = []
|
||
for provider in GGUF_PROVIDERS:
|
||
candidates.append((provider, f"{provider}/{model_name}-GGUF"))
|
||
return candidates
|
||
|
||
|
||
def _base_models_from_tags(tags: list) -> list[str]:
|
||
"""Extract base-model repo ids from HF tags.
|
||
|
||
Quant repos carry tags like `base_model:tomaszki/gemma-3` and
|
||
`base_model:quantized:tomaszki/gemma-3` — the repo id is always the
|
||
segment after the last colon.
|
||
"""
|
||
return [
|
||
t.rsplit(":", 1)[-1].lower()
|
||
for t in tags
|
||
if isinstance(t, str) and t.startswith("base_model:")
|
||
]
|
||
|
||
|
||
_REPO_PARAMS_CACHE: dict[str, int | None] = {}
|
||
_MIRROR_PARAMS_TOLERANCE = 0.30
|
||
|
||
|
||
def _repo_total_params(repo_id: str) -> int | None:
|
||
"""Total parameter count of a repo from its safetensors metadata."""
|
||
if repo_id in _REPO_PARAMS_CACHE:
|
||
return _REPO_PARAMS_CACHE[repo_id]
|
||
url = f"{HF_API}/{repo_id}"
|
||
req = urllib.request.Request(url, headers=_auth_headers())
|
||
total = None
|
||
try:
|
||
with urllib.request.urlopen(req, timeout=10) as resp:
|
||
info = json.loads(resp.read().decode())
|
||
st = info.get("safetensors") or {}
|
||
raw = st.get("total")
|
||
total = int(raw) if raw else None
|
||
except Exception:
|
||
pass
|
||
_REPO_PARAMS_CACHE[repo_id] = total
|
||
return total
|
||
|
||
|
||
def check_gguf_repo_exists(
|
||
repo_id: str,
|
||
source_repo_id: str | None = None,
|
||
source_params: int | None = None,
|
||
) -> bool:
|
||
"""Check that a HuggingFace repo exists, has GGUF files, and — when the
|
||
repo declares `base_model` tags — was actually quantized from
|
||
`source_repo_id`.
|
||
|
||
Candidate repo names are built from the bare model name only, so different
|
||
orgs' models with the same name (e.g. `tiny-random/gemma-3` vs
|
||
`tomaszki/gemma-3`) would otherwise be linked to the wrong quant.
|
||
|
||
A base_model mismatch is still accepted when the declared base has ~the
|
||
same parameter count as the source model (`source_params`): that's a
|
||
mirror/re-upload of the same weights (e.g. unsloth re-uploads pointing at
|
||
the canonical upstream). Repos without base_model tags are accepted as
|
||
before (unverifiable).
|
||
"""
|
||
url = f"{HF_API}/{repo_id}"
|
||
req = urllib.request.Request(url, headers=_auth_headers())
|
||
try:
|
||
with urllib.request.urlopen(req, timeout=10) as resp:
|
||
info = json.loads(resp.read().decode())
|
||
tags = info.get("tags", [])
|
||
if "gguf" not in tags:
|
||
return False
|
||
if source_repo_id:
|
||
bases = _base_models_from_tags(tags)
|
||
if bases and source_repo_id.lower() not in bases:
|
||
if not source_params:
|
||
return False
|
||
base_params = next(
|
||
(p for p in (_repo_total_params(b) for b in bases) if p),
|
||
None,
|
||
)
|
||
if not base_params:
|
||
return False
|
||
ratio = base_params / source_params
|
||
return abs(ratio - 1.0) <= _MIRROR_PARAMS_TOLERANCE
|
||
return True
|
||
except Exception:
|
||
return False
|
||
|
||
|
||
def _resolve_gguf_sources(
|
||
repo_id: str, source_params: int | None = None
|
||
) -> tuple[list[dict], list[tuple[str, bool]]]:
|
||
"""Resolve GGUF sources for a single model repo.
|
||
|
||
Returns (sources, checks) where checks is [(candidate_repo, exists), ...].
|
||
"""
|
||
sources: list[dict] = []
|
||
checks: list[tuple[str, bool]] = []
|
||
for provider, candidate_repo in _model_gguf_repo_candidates(repo_id):
|
||
exists = check_gguf_repo_exists(
|
||
candidate_repo, source_repo_id=repo_id, source_params=source_params
|
||
)
|
||
checks.append((candidate_repo, exists))
|
||
if exists:
|
||
sources.append({"repo": candidate_repo, "provider": provider})
|
||
time.sleep(0.15) # Be polite to the API
|
||
return sources, checks
|
||
|
||
|
||
def enrich_gguf_sources(models: list[dict], threads: int = 1) -> int:
|
||
"""Add gguf_sources to models by checking GGUF provider repos.
|
||
|
||
Uses a persistent cache to avoid re-checking repos on every scrape.
|
||
Returns the number of models enriched.
|
||
"""
|
||
cache = _load_gguf_cache()
|
||
enriched = 0
|
||
cache_hits = 0
|
||
total = len(models)
|
||
from datetime import datetime, timezone
|
||
|
||
to_check: list[tuple[int, str, int | None]] = []
|
||
|
||
for i, model in enumerate(models, 1):
|
||
repo_id = model["name"]
|
||
|
||
# Skip non-GGUF models (AWQ/GPTQ don't use GGUF sources)
|
||
if model.get("format", "gguf") != "gguf":
|
||
continue
|
||
|
||
# Check cache first
|
||
if repo_id in cache and _cache_entry_fresh(cache[repo_id]):
|
||
sources = cache[repo_id]["sources"]
|
||
cache_hits += 1
|
||
else:
|
||
to_check.append((i, repo_id, model.get("parameters_raw")))
|
||
continue
|
||
|
||
if sources:
|
||
model["gguf_sources"] = sources
|
||
enriched += 1
|
||
|
||
# Resolve cache misses, optionally in parallel.
|
||
if to_check:
|
||
def _apply_checked_sources(idx: int, repo_id: str, sources: list[dict]):
|
||
nonlocal enriched
|
||
cache[repo_id] = {
|
||
"sources": sources,
|
||
"checked": datetime.now(timezone.utc).isoformat(),
|
||
}
|
||
if sources:
|
||
models[idx - 1]["gguf_sources"] = sources
|
||
enriched += 1
|
||
|
||
if threads <= 1:
|
||
for idx, repo_id, params_raw in to_check:
|
||
sources, checks = _resolve_gguf_sources(repo_id, params_raw)
|
||
print(f" [{idx}/{total}] {repo_id}")
|
||
for candidate_repo, exists in checks:
|
||
mark = "✓" if exists else "✗"
|
||
print(f" {mark} {candidate_repo}")
|
||
print(f" -> {len(sources)} source(s)")
|
||
_apply_checked_sources(idx, repo_id, sources)
|
||
else:
|
||
print(f" Using {threads} threads for GGUF source checks")
|
||
future_to_meta: dict[concurrent.futures.Future, tuple[int, str]] = {}
|
||
with concurrent.futures.ThreadPoolExecutor(max_workers=threads) as executor:
|
||
for idx, repo_id, params_raw in to_check:
|
||
future = executor.submit(_resolve_gguf_sources, repo_id, params_raw)
|
||
future_to_meta[future] = (idx, repo_id)
|
||
|
||
for future in concurrent.futures.as_completed(future_to_meta):
|
||
idx, repo_id = future_to_meta[future]
|
||
sources, checks = future.result()
|
||
print(f" [{idx}/{total}] {repo_id}")
|
||
for candidate_repo, exists in checks:
|
||
mark = "✓" if exists else "✗"
|
||
print(f" {mark} {candidate_repo}")
|
||
print(f" -> {len(sources)} source(s)")
|
||
_apply_checked_sources(idx, repo_id, sources)
|
||
|
||
_save_gguf_cache(cache)
|
||
print(f" Cache: {cache_hits} hits, {total - cache_hits} API checks")
|
||
return enriched
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Auto-discovery from HuggingFace trending / most-downloaded
|
||
# ---------------------------------------------------------------------------
|
||
|
||
# Pipeline tags to search for discoverable models
|
||
DISCOVER_PIPELINES = [
|
||
"text-generation",
|
||
"text2text-generation",
|
||
"image-text-to-text",
|
||
"feature-extraction", # Embedding models (useful for RAG sizing)
|
||
"text-to-speech",
|
||
]
|
||
|
||
PRIMARY_DISCOVER_PIPELINE = "text-generation"
|
||
|
||
# Orgs to skip — test fixtures and legacy mirrors only.
|
||
# Quantization/repack orgs (TheBloke, bartowski, unsloth, etc.) are kept
|
||
# because they provide popular quantised variants users actually run.
|
||
SKIP_ORGS = {
|
||
"trl-internal-testing", # Test fixtures
|
||
}
|
||
|
||
# Markers of CI/test-stub repos: randomly initialized micro-models that look
|
||
# like real model families by name (e.g. `tiny-random/gemma-3` is 9M params).
|
||
# They poison installed-detection and throughput estimates, so they never
|
||
# belong in the catalog regardless of download counts.
|
||
TEST_STUB_MARKERS = ("tiny-random", "tiny-dummy", "-random-init", "ci-random-")
|
||
|
||
|
||
def is_test_stub(repo_id: str) -> bool:
|
||
rid = repo_id.lower()
|
||
if any(marker in rid for marker in TEST_STUB_MARKERS):
|
||
return True
|
||
name = rid.split("/")[-1]
|
||
return name.startswith(("test-", "testing-", "test_")) or bool(re.match(r"^test\d", name))
|
||
|
||
# Sort strategies to query — results are merged and deduplicated.
|
||
# Each strategy surfaces models that the others might miss.
|
||
DISCOVER_SORT_STRATEGIES = [
|
||
"downloads", # All-time most downloaded
|
||
"trendingScore", # Currently trending (recent velocity)
|
||
"likes30d", # Most liked in the last 30 days
|
||
]
|
||
|
||
|
||
def _fetch_models_page(url: str) -> tuple[list[dict], str | None]:
|
||
"""Fetch a page of models from the HuggingFace API.
|
||
|
||
Returns (models, next_url) where next_url is parsed from the Link header
|
||
for cursor-based pagination, or None if there are no more pages.
|
||
"""
|
||
req = urllib.request.Request(url, headers=_auth_headers())
|
||
with urllib.request.urlopen(req, timeout=60) as resp:
|
||
# Parse cursor-based pagination from Link header
|
||
next_url = None
|
||
link_header = resp.headers.get("Link", "")
|
||
if 'rel="next"' in link_header:
|
||
# Format: <url>; rel="next"
|
||
next_url = link_header.split(">")[0].lstrip("<")
|
||
models = json.loads(resp.read().decode())
|
||
return models, next_url
|
||
|
||
|
||
def _build_first_page_url(pipeline: str, sort: str, page_size: int) -> str:
|
||
"""Build the initial API URL for a pipeline query."""
|
||
return (
|
||
f"{HF_API}?"
|
||
f"pipeline_tag={pipeline}&"
|
||
f"sort={sort}&"
|
||
f"direction=-1&"
|
||
f"limit={page_size}&"
|
||
f"expand[]=safetensors&"
|
||
f"expand[]=config&"
|
||
f"expand[]=cardData"
|
||
)
|
||
|
||
|
||
def _estimate_params_from_config(config: dict) -> int | None:
|
||
"""Try to estimate parameter count from model config fields.
|
||
|
||
This is a fallback for models that don't expose safetensors metadata
|
||
in the listing API. Uses common config.json fields to estimate.
|
||
"""
|
||
# Some configs directly state the param count
|
||
for key in ("num_parameters", "n_params", "total_params"):
|
||
val = config.get(key)
|
||
if val and isinstance(val, (int, float)) and val > 1000:
|
||
return int(val)
|
||
|
||
# Estimate from architecture dimensions (rough but useful)
|
||
hidden = config.get("hidden_size") or config.get("d_model")
|
||
layers = config.get("num_hidden_layers") or config.get("n_layer")
|
||
vocab = config.get("vocab_size")
|
||
intermediate = config.get("intermediate_size") or config.get("d_ff")
|
||
|
||
if hidden and layers and vocab:
|
||
# Rough transformer parameter estimate:
|
||
# ~12 * L * H^2 (attention + FFN) + V * H (embeddings)
|
||
ffn_factor = (intermediate / hidden) if intermediate else 4.0
|
||
params = int(layers * hidden * hidden * (4 + 2 * ffn_factor) + vocab * hidden)
|
||
if params > 1_000_000: # sanity check: at least 1M params
|
||
return params
|
||
|
||
return None
|
||
|
||
|
||
def _process_listing(
|
||
m: dict,
|
||
pipeline: str,
|
||
curated: set[str],
|
||
seen_ids: set[str],
|
||
min_downloads: int,
|
||
require_downloads_floor: bool,
|
||
stats: dict,
|
||
) -> dict | None:
|
||
"""Check a single model listing against filters.
|
||
|
||
Returns the listing with _total_params attached if accepted, else None.
|
||
Mutates seen_ids and stats as side effects.
|
||
"""
|
||
repo_id = m.get("id", "")
|
||
if not repo_id or "/" not in repo_id:
|
||
return None
|
||
stats["total_seen"] += 1
|
||
|
||
if repo_id in curated:
|
||
stats["skip_curated"] += 1
|
||
return None
|
||
|
||
if repo_id in seen_ids:
|
||
stats["skip_duplicate"] += 1
|
||
return None
|
||
seen_ids.add(repo_id)
|
||
|
||
org = repo_id.split("/")[0]
|
||
if org in SKIP_ORGS:
|
||
stats["skip_org"] += 1
|
||
return None
|
||
|
||
if is_test_stub(repo_id):
|
||
stats["skip_test_stub"] += 1
|
||
return None
|
||
|
||
downloads_raw = m.get("downloads")
|
||
downloads = downloads_raw or 0
|
||
if downloads < min_downloads and (require_downloads_floor or downloads_raw is not None):
|
||
stats["skip_downloads"] += 1
|
||
return None
|
||
|
||
tags = set(m.get("tags", []))
|
||
if tags & {"adapter", "merge", "lora", "qlora"}:
|
||
stats["skip_tags"] += 1
|
||
return None
|
||
|
||
# Try safetensors metadata first
|
||
safetensors = m.get("safetensors", {})
|
||
total_params = safetensors.get("total")
|
||
if not total_params:
|
||
params_by_dtype = safetensors.get("parameters", {})
|
||
if params_by_dtype:
|
||
total_params = max(params_by_dtype.values())
|
||
|
||
param_source = "safetensors"
|
||
|
||
# Fallback: fetch full config.json and estimate from arch dims.
|
||
# Cap config fetches to avoid excessive network calls during discovery.
|
||
config_attempts = stats["params_from_config"] + stats.get("skip_no_params", 0)
|
||
if not total_params and config_attempts < 500:
|
||
full_cfg = fetch_config_json(repo_id)
|
||
if full_cfg:
|
||
total_params = _estimate_params_from_config(full_cfg)
|
||
param_source = "config"
|
||
|
||
if not total_params:
|
||
stats["skip_no_params"] += 1
|
||
return None
|
||
|
||
if param_source == "safetensors":
|
||
stats["params_from_safetensors"] += 1
|
||
else:
|
||
stats["params_from_config"] += 1
|
||
|
||
m["_total_params"] = total_params
|
||
m["_pipeline_tag"] = m.get("pipeline_tag") or pipeline
|
||
stats["accepted"] += 1
|
||
return m
|
||
|
||
|
||
def discover_trending_models(limit: int = 30, min_downloads: int = 10000) -> list[dict]:
|
||
"""Discover popular models from HuggingFace using multiple sort strategies.
|
||
|
||
Queries the HF API with three sort strategies (all-time downloads,
|
||
trending score, and 30-day likes) across all pipeline types, then
|
||
merges and deduplicates the results. This surfaces both established
|
||
popular models and newly trending ones.
|
||
|
||
Uses cursor-based pagination and falls back to estimating params from
|
||
config.json when safetensors metadata is unavailable.
|
||
|
||
Returns a list of dicts with model listing data for models NOT already
|
||
in TARGET_MODELS.
|
||
"""
|
||
curated = set(TARGET_MODELS)
|
||
discovered = []
|
||
seen_ids = set()
|
||
|
||
# Keep --discover-limit as the mainstream LLM discovery budget. Other
|
||
# pipelines are additive so TTS/audio discovery does not take slots away
|
||
# from text-generation coverage.
|
||
side_quota = max(1, limit // len(DISCOVER_PIPELINES))
|
||
pipeline_limits = {
|
||
pipeline: limit if pipeline == PRIMARY_DISCOVER_PIPELINE else side_quota
|
||
for pipeline in DISCOVER_PIPELINES
|
||
}
|
||
pipeline_counts = {pipeline: 0 for pipeline in DISCOVER_PIPELINES}
|
||
|
||
def _quotas_full() -> bool:
|
||
return all(pipeline_counts[p] >= pipeline_limits[p] for p in DISCOVER_PIPELINES)
|
||
|
||
PAGE_SIZE = 1000
|
||
|
||
stats = {
|
||
"total_seen": 0,
|
||
"skip_curated": 0,
|
||
"skip_duplicate": 0,
|
||
"skip_org": 0,
|
||
"skip_test_stub": 0,
|
||
"skip_downloads": 0,
|
||
"skip_tags": 0,
|
||
"skip_no_params": 0,
|
||
"params_from_safetensors": 0,
|
||
"params_from_config": 0,
|
||
"accepted": 0,
|
||
}
|
||
|
||
for sort_strategy in DISCOVER_SORT_STRATEGIES:
|
||
strategy_accepted = 0
|
||
# Trending/likes sorts surface newly popular models that may not
|
||
# have high all-time downloads yet — use a lower floor for them.
|
||
effective_min = (min_downloads if sort_strategy == "downloads"
|
||
else max(1000, min_downloads // 10))
|
||
# Cap pages for non-download sorts since they aren't ordered by
|
||
# downloads and would otherwise scan endlessly.
|
||
max_pages = 50 if sort_strategy == "downloads" else 5
|
||
|
||
for pipeline in DISCOVER_PIPELINES:
|
||
if pipeline_counts[pipeline] >= pipeline_limits[pipeline]:
|
||
continue
|
||
|
||
next_url: str | None = _build_first_page_url(
|
||
pipeline, sort_strategy, PAGE_SIZE
|
||
)
|
||
pipeline_accepted = 0
|
||
hit_floor = False
|
||
page_num = 0
|
||
|
||
while (pipeline_counts[pipeline] < pipeline_limits[pipeline]
|
||
and next_url and page_num < max_pages):
|
||
page_num += 1
|
||
try:
|
||
models, next_url = _fetch_models_page(next_url)
|
||
except Exception as e:
|
||
print(f" ⚠ {pipeline} page {page_num}: {e}",
|
||
file=sys.stderr)
|
||
break
|
||
|
||
if not models:
|
||
break
|
||
|
||
below_min_this_page = 0
|
||
|
||
for m in models:
|
||
result = _process_listing(
|
||
m,
|
||
pipeline,
|
||
curated,
|
||
seen_ids,
|
||
effective_min,
|
||
sort_strategy == "downloads",
|
||
stats,
|
||
)
|
||
if result is None:
|
||
# Track download-floor hits for early stop
|
||
downloads = m.get("downloads")
|
||
repo_id = m.get("id", "")
|
||
if (repo_id and "/" in repo_id
|
||
and repo_id not in curated
|
||
and downloads is not None
|
||
and downloads < effective_min):
|
||
below_min_this_page += 1
|
||
continue
|
||
|
||
discovered.append(result)
|
||
pipeline_counts[pipeline] += 1
|
||
pipeline_accepted += 1
|
||
strategy_accepted += 1
|
||
if pipeline_counts[pipeline] >= pipeline_limits[pipeline]:
|
||
break
|
||
|
||
# For download-sorted queries, stop when most results are
|
||
# below the threshold. For trending/likes sorts, always
|
||
# exhaust pages since ordering isn't by downloads.
|
||
if sort_strategy == "downloads":
|
||
if below_min_this_page > len(models) * 0.8:
|
||
hit_floor = True
|
||
break
|
||
|
||
if len(models) < PAGE_SIZE:
|
||
break
|
||
|
||
time.sleep(0.2)
|
||
|
||
suffix = f", hit download floor" if hit_floor else ""
|
||
if pipeline_accepted > 0 or page_num > 0:
|
||
print(f" {pipeline}: +{pipeline_accepted}"
|
||
f" (pages: {page_num}{suffix})")
|
||
|
||
if _quotas_full():
|
||
break
|
||
|
||
print(f" sort={sort_strategy} (min_dl={effective_min:,}): "
|
||
f"+{strategy_accepted} new models")
|
||
|
||
if _quotas_full():
|
||
break
|
||
|
||
# Print filter statistics
|
||
print(f"\n Discovery filter stats:")
|
||
print(f" Total listings seen: {stats['total_seen']:>6}")
|
||
print(f" Skipped (curated dupe): {stats['skip_curated']:>6}")
|
||
print(f" Skipped (seen/duplicate):{stats['skip_duplicate']:>6}")
|
||
print(f" Skipped (skip org): {stats['skip_org']:>6}")
|
||
print(f" Skipped (low downloads): {stats['skip_downloads']:>6}")
|
||
print(f" Skipped (adapter/merge): {stats['skip_tags']:>6}")
|
||
print(f" Skipped (no params): {stats['skip_no_params']:>6}")
|
||
print(f" Params from safetensors: {stats['params_from_safetensors']:>6}")
|
||
print(f" Params from config est.: {stats['params_from_config']:>6}")
|
||
print(f" Accepted: {stats['accepted']:>6}")
|
||
print(f" Pipeline quotas: {pipeline_limits}")
|
||
|
||
return discovered
|
||
|
||
|
||
def _build_discovered_model(listing: dict) -> dict | None:
|
||
"""Build model dict from a listing returned by discover_trending_models.
|
||
|
||
Only fetches config.json for accurate context length; all other metadata
|
||
comes from the listing data already obtained via expand fields.
|
||
"""
|
||
repo_id = listing["id"]
|
||
total_params = listing["_total_params"]
|
||
config = listing.get("config", {})
|
||
pipeline_tag = listing.get("pipeline_tag") or listing.get("_pipeline_tag")
|
||
|
||
# Listings from non-download sort strategies (trending, likes) omit
|
||
# downloads/likes/tags, which would zero out popularity metadata and
|
||
# lose language tags. Backfill those fields with a full info fetch.
|
||
if listing.get("downloads") is None or listing.get("likes") is None:
|
||
info = fetch_model_info(repo_id)
|
||
if info:
|
||
for key in ("downloads", "likes", "createdAt", "tags"):
|
||
if listing.get(key) is None and info.get(key) is not None:
|
||
listing[key] = info[key]
|
||
|
||
full_config = fetch_config_json(repo_id)
|
||
|
||
model_format, default_quant = detect_quant_format(repo_id, full_config)
|
||
if pipeline_tag == "text-to-speech":
|
||
model_format, default_quant = ("safetensors", "F16")
|
||
context_length = CONTEXT_LENGTH_OVERRIDES.get(
|
||
repo_id,
|
||
infer_context_length(full_config) if full_config else infer_context_length(config),
|
||
)
|
||
|
||
# Correct parameters_raw when safetensors reports quantized element counts
|
||
arch_params = estimate_params_from_arch(full_config)
|
||
if arch_params and arch_params > total_params * 2:
|
||
total_params = arch_params
|
||
|
||
min_ram, rec_ram = estimate_ram(total_params, default_quant)
|
||
min_vram = estimate_vram(total_params, default_quant)
|
||
|
||
architecture = config.get("model_type", "unknown")
|
||
moe_info = detect_moe(repo_id, full_config, architecture, total_params)
|
||
use_case_str = infer_use_case(repo_id, pipeline_tag, config)
|
||
|
||
# Architecture metadata for the precise KV cache formula.
|
||
arch_meta = extract_arch_metadata(full_config)
|
||
|
||
license_name = extract_license(listing)
|
||
|
||
model = {
|
||
"name": repo_id,
|
||
"provider": extract_provider(repo_id),
|
||
"parameter_count": format_param_count(total_params),
|
||
"parameters_raw": total_params,
|
||
"min_ram_gb": min_ram,
|
||
"recommended_ram_gb": rec_ram,
|
||
"min_vram_gb": min_vram,
|
||
"quantization": default_quant,
|
||
"format": model_format,
|
||
"context_length": context_length,
|
||
"use_case": use_case_str,
|
||
"capabilities": infer_capabilities(repo_id, pipeline_tag, use_case_str),
|
||
"languages": infer_languages(listing, full_config or config),
|
||
"pipeline_tag": pipeline_tag or "unknown",
|
||
"architecture": architecture,
|
||
"hf_downloads": listing.get("downloads", 0),
|
||
"hf_likes": listing.get("likes", 0),
|
||
"release_date": (listing.get("createdAt") or "")[:10] or None,
|
||
**arch_meta,
|
||
"_discovered": True,
|
||
}
|
||
|
||
if license_name:
|
||
model["license"] = license_name
|
||
|
||
if moe_info["is_moe"]:
|
||
model["is_moe"] = True
|
||
model["num_experts"] = moe_info["num_experts"]
|
||
model["active_experts"] = moe_info["active_experts"]
|
||
model["active_parameters"] = moe_info["active_parameters"]
|
||
|
||
return model
|
||
|
||
|
||
def main():
|
||
parser = argparse.ArgumentParser(
|
||
description="Scrape LLM model metadata from HuggingFace for llmfit."
|
||
)
|
||
parser.add_argument(
|
||
"--discover", action="store_true", default=True,
|
||
help="Auto-discover top models by download count from HuggingFace "
|
||
"in addition to the curated TARGET_MODELS list (default: enabled)."
|
||
)
|
||
parser.add_argument(
|
||
"--no-discover", action="store_false", dest="discover",
|
||
help="Disable auto-discovery, only scrape curated TARGET_MODELS list."
|
||
)
|
||
parser.add_argument(
|
||
"-n", "--discover-limit", type=int, default=1000,
|
||
help="Max number of top-downloaded models to discover (default: 1000). "
|
||
"Duplicates of curated models are skipped automatically."
|
||
)
|
||
parser.add_argument(
|
||
"--min-downloads", type=int, default=10000,
|
||
help="Minimum download count for discovered models (default: 10000)."
|
||
)
|
||
parser.add_argument(
|
||
"--gguf-sources", action="store_true", default=True,
|
||
help="Enrich models with known GGUF download sources from "
|
||
"providers like unsloth and bartowski on HuggingFace (default: enabled)."
|
||
)
|
||
parser.add_argument(
|
||
"--no-gguf-sources", action="store_false", dest="gguf_sources",
|
||
help="Skip GGUF download source enrichment (faster scrape)."
|
||
)
|
||
parser.add_argument(
|
||
"--token", type=str, default=None,
|
||
help="HuggingFace API token for accessing gated models. "
|
||
"Can also be set via HF_TOKEN or HUGGING_FACE_HUB_TOKEN env var."
|
||
)
|
||
parser.add_argument(
|
||
"--threads", type=int, default=1,
|
||
help="Number of worker threads for parallel model metadata scraping "
|
||
"(default: 1, which preserves current sequential behavior)."
|
||
)
|
||
args = parser.parse_args()
|
||
|
||
if args.threads < 1:
|
||
parser.error("--threads must be >= 1")
|
||
|
||
# Resolve auth token: CLI flag > HF_TOKEN > HUGGING_FACE_HUB_TOKEN
|
||
global _hf_token
|
||
_hf_token = (
|
||
args.token
|
||
or os.environ.get("HF_TOKEN")
|
||
or os.environ.get("HUGGING_FACE_HUB_TOKEN")
|
||
)
|
||
if _hf_token:
|
||
print(f"🔑 Authenticated with HuggingFace token ({_hf_token[:4]}...{_hf_token[-4:]})")
|
||
else:
|
||
print("ℹ No HF token set. Gated models will use fallback data.")
|
||
print(" Set HF_TOKEN env var or pass --token to access gated models.\n")
|
||
|
||
# Fallback entries for gated/auth-required models where the API
|
||
# doesn't return safetensors metadata without a token.
|
||
FALLBACKS = [
|
||
{
|
||
"name": "meta-llama/Llama-3.3-70B-Instruct",
|
||
"provider": "Meta", "parameter_count": "70.6B",
|
||
"parameters_raw": 70_553_706_496,
|
||
"min_ram_gb": 39.4, "recommended_ram_gb": 65.7, "min_vram_gb": 36.1,
|
||
"quantization": "Q4_K_M", "context_length": 131072,
|
||
"use_case": "Instruction following, chat",
|
||
"pipeline_tag": "text-generation", "architecture": "llama",
|
||
"hf_downloads": 0, "hf_likes": 0, "release_date": None,
|
||
},
|
||
{
|
||
"name": "mistralai/Mistral-Small-24B-Instruct-2501",
|
||
"provider": "Mistral AI", "parameter_count": "24B",
|
||
"parameters_raw": 24_000_000_000,
|
||
"min_ram_gb": 13.4, "recommended_ram_gb": 22.4, "min_vram_gb": 12.3,
|
||
"quantization": "Q4_K_M", "context_length": 32768,
|
||
"use_case": "Instruction following, chat",
|
||
"pipeline_tag": "text-generation", "architecture": "mistral",
|
||
"hf_downloads": 0, "hf_likes": 0, "release_date": None,
|
||
},
|
||
{
|
||
"name": "Qwen/Qwen2.5-14B-Instruct",
|
||
"provider": "Alibaba", "parameter_count": "14.8B",
|
||
"parameters_raw": 14_770_000_000,
|
||
"min_ram_gb": 8.2, "recommended_ram_gb": 13.7, "min_vram_gb": 7.6,
|
||
"quantization": "Q4_K_M", "context_length": 131072,
|
||
"use_case": "Instruction following, chat",
|
||
"pipeline_tag": "text-generation", "architecture": "qwen2",
|
||
"hf_downloads": 0, "hf_likes": 0, "release_date": None,
|
||
},
|
||
{
|
||
"name": "Qwen/Qwen2.5-32B-Instruct",
|
||
"provider": "Alibaba", "parameter_count": "32.5B",
|
||
"parameters_raw": 32_510_000_000,
|
||
"min_ram_gb": 18.2, "recommended_ram_gb": 30.3, "min_vram_gb": 16.7,
|
||
"quantization": "Q4_K_M", "context_length": 131072,
|
||
"use_case": "Instruction following, chat",
|
||
"pipeline_tag": "text-generation", "architecture": "qwen2",
|
||
"hf_downloads": 0, "hf_likes": 0, "release_date": None,
|
||
},
|
||
{
|
||
"name": "microsoft/phi-3-mini-4k-instruct",
|
||
"provider": "Microsoft", "parameter_count": "3.8B",
|
||
"parameters_raw": 3_821_000_000,
|
||
"min_ram_gb": 2.1, "recommended_ram_gb": 3.6, "min_vram_gb": 2.0,
|
||
"quantization": "Q4_K_M", "context_length": 4096,
|
||
"use_case": "Lightweight, edge deployment",
|
||
"pipeline_tag": "text-generation", "architecture": "phi3",
|
||
"hf_downloads": 0, "hf_likes": 0, "release_date": None,
|
||
},
|
||
{
|
||
"name": "microsoft/phi-4",
|
||
"provider": "Microsoft", "parameter_count": "14B",
|
||
"parameters_raw": 14_000_000_000,
|
||
"min_ram_gb": 7.8, "recommended_ram_gb": 13.0, "min_vram_gb": 7.2,
|
||
"quantization": "Q4_K_M", "context_length": 16384,
|
||
"use_case": "Reasoning, STEM, code generation",
|
||
"pipeline_tag": "text-generation", "architecture": "phi",
|
||
"hf_downloads": 0, "hf_likes": 0, "release_date": None,
|
||
},
|
||
{
|
||
"name": "google/gemma-3-12b-it",
|
||
"provider": "Google", "parameter_count": "12B",
|
||
"parameters_raw": 12_000_000_000,
|
||
"min_ram_gb": 6.7, "recommended_ram_gb": 11.2, "min_vram_gb": 6.1,
|
||
"quantization": "Q4_K_M", "context_length": 131072,
|
||
"use_case": "Multimodal, vision and text",
|
||
"capabilities": ["vision", "tool_use"],
|
||
"pipeline_tag": "image-text-to-text", "architecture": "gemma3",
|
||
"hf_downloads": 0, "hf_likes": 0, "release_date": None,
|
||
},
|
||
{
|
||
"name": "deepseek-ai/DeepSeek-V3",
|
||
"provider": "DeepSeek", "parameter_count": "685B",
|
||
"parameters_raw": 685_000_000_000,
|
||
"min_ram_gb": 382.8, "recommended_ram_gb": 638.0, "min_vram_gb": 351.3,
|
||
"quantization": "Q4_K_M", "context_length": 131072,
|
||
"use_case": "State-of-the-art, MoE architecture",
|
||
"pipeline_tag": "text-generation", "architecture": "deepseek_v3",
|
||
"is_moe": True, "num_experts": 256, "active_experts": 8,
|
||
"active_parameters": 37_000_000_000,
|
||
"hf_downloads": 0, "hf_likes": 0, "release_date": None,
|
||
},
|
||
{
|
||
"name": "CohereForAI/c4ai-command-r-v01",
|
||
"provider": "Cohere", "parameter_count": "35B",
|
||
"parameters_raw": 35_000_000_000,
|
||
"min_ram_gb": 19.5, "recommended_ram_gb": 32.6, "min_vram_gb": 17.9,
|
||
"quantization": "Q4_K_M", "context_length": 131072,
|
||
"use_case": "RAG, tool use, agents",
|
||
"pipeline_tag": "text-generation", "architecture": "cohere",
|
||
"hf_downloads": 0, "hf_likes": 0, "release_date": None,
|
||
},
|
||
{
|
||
"name": "bigcode/starcoder2-15b",
|
||
"provider": "BigCode", "parameter_count": "15.7B",
|
||
"parameters_raw": 15_700_000_000,
|
||
"min_ram_gb": 8.8, "recommended_ram_gb": 14.6, "min_vram_gb": 8.0,
|
||
"quantization": "Q4_K_M", "context_length": 16384,
|
||
"use_case": "Code generation and completion",
|
||
"pipeline_tag": "text-generation", "architecture": "starcoder2",
|
||
"hf_downloads": 0, "hf_likes": 0, "release_date": None,
|
||
},
|
||
{
|
||
"name": "nomic-ai/nomic-embed-text-v1.5",
|
||
"provider": "Nomic", "parameter_count": "137M",
|
||
"parameters_raw": 137_000_000,
|
||
"min_ram_gb": 1.0, "recommended_ram_gb": 2.0, "min_vram_gb": 0.5,
|
||
"quantization": "F16", "context_length": 8192,
|
||
"use_case": "Text embeddings for RAG",
|
||
"pipeline_tag": "feature-extraction", "architecture": "nomic_bert",
|
||
"hf_downloads": 0, "hf_likes": 0, "release_date": None,
|
||
},
|
||
{
|
||
"name": "deepseek-ai/DeepSeek-Coder-V2-Lite-Instruct",
|
||
"provider": "DeepSeek", "parameter_count": "16B",
|
||
"parameters_raw": 15_700_000_000,
|
||
"min_ram_gb": 8.8, "recommended_ram_gb": 14.6, "min_vram_gb": 8.0,
|
||
"quantization": "Q4_K_M", "context_length": 131072,
|
||
"use_case": "Code generation and completion",
|
||
"pipeline_tag": "text-generation", "architecture": "deepseek_v2",
|
||
"is_moe": True, "num_experts": 64, "active_experts": 6,
|
||
"active_parameters": 2_400_000_000,
|
||
"hf_downloads": 0, "hf_likes": 0, "release_date": None,
|
||
},
|
||
{
|
||
"name": "microsoft/Phi-3-medium-14b-instruct",
|
||
"provider": "Microsoft", "parameter_count": "14B",
|
||
"parameters_raw": 14_000_000_000,
|
||
"min_ram_gb": 7.8, "recommended_ram_gb": 13.0, "min_vram_gb": 7.2,
|
||
"quantization": "Q4_K_M", "context_length": 4096,
|
||
"use_case": "Balanced performance and size",
|
||
"pipeline_tag": "text-generation", "architecture": "phi3",
|
||
"hf_downloads": 0, "hf_likes": 0, "release_date": None,
|
||
},
|
||
# NEW FALLBACKS for popular models
|
||
{
|
||
"name": "google/gemma-2-2b-it",
|
||
"provider": "Google", "parameter_count": "2.6B",
|
||
"parameters_raw": 2614341376,
|
||
"min_ram_gb": 1.5, "recommended_ram_gb": 2.4, "min_vram_gb": 1.3,
|
||
"quantization": "Q4_K_M", "context_length": 8192,
|
||
"use_case": "Lightweight, edge deployment",
|
||
"pipeline_tag": "text-generation", "architecture": "gemma2",
|
||
"hf_downloads": 0, "hf_likes": 0, "release_date": None,
|
||
},
|
||
{
|
||
"name": "meta-llama/CodeLlama-7b-Instruct-hf",
|
||
"provider": "Meta", "parameter_count": "7.0B",
|
||
"parameters_raw": 7016400896,
|
||
"min_ram_gb": 3.9, "recommended_ram_gb": 6.5, "min_vram_gb": 3.6,
|
||
"quantization": "Q4_K_M", "context_length": 16384,
|
||
"use_case": "Code generation and completion",
|
||
"pipeline_tag": "text-generation", "architecture": "llama",
|
||
"hf_downloads": 0, "hf_likes": 0, "release_date": None,
|
||
},
|
||
{
|
||
"name": "meta-llama/CodeLlama-13b-Instruct-hf",
|
||
"provider": "Meta", "parameter_count": "13.0B",
|
||
"parameters_raw": 13015864320,
|
||
"min_ram_gb": 7.3, "recommended_ram_gb": 12.1, "min_vram_gb": 6.7,
|
||
"quantization": "Q4_K_M", "context_length": 16384,
|
||
"use_case": "Code generation and completion",
|
||
"pipeline_tag": "text-generation", "architecture": "llama",
|
||
"hf_downloads": 0, "hf_likes": 0, "release_date": None,
|
||
},
|
||
{
|
||
"name": "meta-llama/CodeLlama-34b-Instruct-hf",
|
||
"provider": "Meta", "parameter_count": "34.0B",
|
||
"parameters_raw": 34018971648,
|
||
"min_ram_gb": 19.0, "recommended_ram_gb": 31.7, "min_vram_gb": 17.4,
|
||
"quantization": "Q4_K_M", "context_length": 16384,
|
||
"use_case": "Code generation and completion",
|
||
"pipeline_tag": "text-generation", "architecture": "llama",
|
||
"hf_downloads": 0, "hf_likes": 0, "release_date": None,
|
||
},
|
||
{
|
||
"name": "meta-llama/Llama-3.2-11B-Vision-Instruct",
|
||
"provider": "Meta", "parameter_count": "11.0B",
|
||
"parameters_raw": 10665463808,
|
||
"min_ram_gb": 6.0, "recommended_ram_gb": 9.9, "min_vram_gb": 5.5,
|
||
"quantization": "Q4_K_M", "context_length": 131072,
|
||
"use_case": "Multimodal, vision and text",
|
||
"pipeline_tag": "image-text-to-text", "architecture": "llama",
|
||
"hf_downloads": 0, "hf_likes": 0, "release_date": None,
|
||
},
|
||
{
|
||
"name": "mistralai/Ministral-8B-Instruct-2410",
|
||
"provider": "Mistral AI", "parameter_count": "8.0B",
|
||
"parameters_raw": 8030261248,
|
||
"min_ram_gb": 4.5, "recommended_ram_gb": 7.5, "min_vram_gb": 4.1,
|
||
"quantization": "Q4_K_M", "context_length": 32768,
|
||
"use_case": "Instruction following, chat",
|
||
"pipeline_tag": "text-generation", "architecture": "mistral",
|
||
"hf_downloads": 0, "hf_likes": 0, "release_date": None,
|
||
},
|
||
{
|
||
"name": "mistralai/Mistral-Nemo-Instruct-2407",
|
||
"provider": "Mistral AI", "parameter_count": "12.2B",
|
||
"parameters_raw": 12247076864,
|
||
"min_ram_gb": 6.8, "recommended_ram_gb": 11.4, "min_vram_gb": 6.3,
|
||
"quantization": "Q4_K_M", "context_length": 131072,
|
||
"use_case": "Instruction following, chat",
|
||
"pipeline_tag": "text-generation", "architecture": "mistral",
|
||
"hf_downloads": 0, "hf_likes": 0, "release_date": None,
|
||
},
|
||
{
|
||
"name": "microsoft/Phi-3.5-mini-instruct",
|
||
"provider": "Microsoft", "parameter_count": "3.8B",
|
||
"parameters_raw": 3821000000,
|
||
"min_ram_gb": 2.1, "recommended_ram_gb": 3.6, "min_vram_gb": 2.0,
|
||
"quantization": "Q4_K_M", "context_length": 131072,
|
||
"use_case": "Lightweight, long context",
|
||
"pipeline_tag": "text-generation", "architecture": "phi3",
|
||
"hf_downloads": 0, "hf_likes": 0, "release_date": None,
|
||
},
|
||
{
|
||
"name": "microsoft/Orca-2-7b",
|
||
"provider": "Microsoft", "parameter_count": "7.0B",
|
||
"parameters_raw": 7016400896,
|
||
"min_ram_gb": 3.9, "recommended_ram_gb": 6.5, "min_vram_gb": 3.6,
|
||
"quantization": "Q4_K_M", "context_length": 4096,
|
||
"use_case": "Reasoning, step-by-step solutions",
|
||
"pipeline_tag": "text-generation", "architecture": "llama",
|
||
"hf_downloads": 0, "hf_likes": 0, "release_date": None,
|
||
},
|
||
{
|
||
"name": "microsoft/Orca-2-13b",
|
||
"provider": "Microsoft", "parameter_count": "13.0B",
|
||
"parameters_raw": 13015864320,
|
||
"min_ram_gb": 7.3, "recommended_ram_gb": 12.1, "min_vram_gb": 6.7,
|
||
"quantization": "Q4_K_M", "context_length": 4096,
|
||
"use_case": "Reasoning, step-by-step solutions",
|
||
"pipeline_tag": "text-generation", "architecture": "llama",
|
||
"hf_downloads": 0, "hf_likes": 0, "release_date": None,
|
||
},
|
||
{
|
||
"name": "01-ai/Yi-6B-Chat",
|
||
"provider": "01.ai", "parameter_count": "6.1B",
|
||
"parameters_raw": 6061356032,
|
||
"min_ram_gb": 3.4, "recommended_ram_gb": 5.6, "min_vram_gb": 3.1,
|
||
"quantization": "Q4_K_M", "context_length": 4096,
|
||
"use_case": "Multilingual, Chinese/English chat",
|
||
"pipeline_tag": "text-generation", "architecture": "yi",
|
||
"hf_downloads": 0, "hf_likes": 0, "release_date": None,
|
||
},
|
||
{
|
||
"name": "01-ai/Yi-34B-Chat",
|
||
"provider": "01.ai", "parameter_count": "34.4B",
|
||
"parameters_raw": 34386780160,
|
||
"min_ram_gb": 19.2, "recommended_ram_gb": 32.0, "min_vram_gb": 17.6,
|
||
"quantization": "Q4_K_M", "context_length": 4096,
|
||
"use_case": "Multilingual, Chinese/English chat",
|
||
"pipeline_tag": "text-generation", "architecture": "yi",
|
||
"hf_downloads": 0, "hf_likes": 0, "release_date": None,
|
||
},
|
||
{
|
||
"name": "upstage/SOLAR-10.7B-Instruct-v1.0",
|
||
"provider": "Upstage", "parameter_count": "10.7B",
|
||
"parameters_raw": 10700000000,
|
||
"min_ram_gb": 6.0, "recommended_ram_gb": 10.0, "min_vram_gb": 5.5,
|
||
"quantization": "Q4_K_M", "context_length": 4096,
|
||
"use_case": "High-performance instruction following",
|
||
"pipeline_tag": "text-generation", "architecture": "llama",
|
||
"hf_downloads": 0, "hf_likes": 0, "release_date": None,
|
||
},
|
||
{
|
||
"name": "tiiuae/falcon-7b-instruct",
|
||
"provider": "TII", "parameter_count": "7.0B",
|
||
"parameters_raw": 7000000000,
|
||
"min_ram_gb": 3.9, "recommended_ram_gb": 6.5, "min_vram_gb": 3.6,
|
||
"quantization": "Q4_K_M", "context_length": 2048,
|
||
"use_case": "Instruction following, chat",
|
||
"pipeline_tag": "text-generation", "architecture": "falcon",
|
||
"hf_downloads": 0, "hf_likes": 0, "release_date": None,
|
||
},
|
||
{
|
||
"name": "tiiuae/falcon-40b-instruct",
|
||
"provider": "TII", "parameter_count": "40.0B",
|
||
"parameters_raw": 40000000000,
|
||
"min_ram_gb": 22.4, "recommended_ram_gb": 37.3, "min_vram_gb": 20.5,
|
||
"quantization": "Q4_K_M", "context_length": 2048,
|
||
"use_case": "Instruction following, chat",
|
||
"pipeline_tag": "text-generation", "architecture": "falcon",
|
||
"hf_downloads": 0, "hf_likes": 0, "release_date": None,
|
||
},
|
||
{
|
||
"name": "HuggingFaceH4/zephyr-7b-beta",
|
||
"provider": "HuggingFace", "parameter_count": "7.2B",
|
||
"parameters_raw": 7241732096,
|
||
"min_ram_gb": 4.0, "recommended_ram_gb": 6.7, "min_vram_gb": 3.7,
|
||
"quantization": "Q4_K_M", "context_length": 32768,
|
||
"use_case": "Instruction following, chat",
|
||
"pipeline_tag": "text-generation", "architecture": "mistral",
|
||
"hf_downloads": 0, "hf_likes": 0, "release_date": None,
|
||
},
|
||
{
|
||
"name": "openchat/openchat-3.5-0106",
|
||
"provider": "OpenChat", "parameter_count": "7.0B",
|
||
"parameters_raw": 7000000000,
|
||
"min_ram_gb": 3.9, "recommended_ram_gb": 6.5, "min_vram_gb": 3.6,
|
||
"quantization": "Q4_K_M", "context_length": 8192,
|
||
"use_case": "Instruction following, chat",
|
||
"pipeline_tag": "text-generation", "architecture": "mistral",
|
||
"hf_downloads": 0, "hf_likes": 0, "release_date": None,
|
||
},
|
||
{
|
||
"name": "lmsys/vicuna-7b-v1.5",
|
||
"provider": "LMSYS", "parameter_count": "7.0B",
|
||
"parameters_raw": 6738415616,
|
||
"min_ram_gb": 3.8, "recommended_ram_gb": 6.3, "min_vram_gb": 3.4,
|
||
"quantization": "Q4_K_M", "context_length": 4096,
|
||
"use_case": "Instruction following, chat",
|
||
"pipeline_tag": "text-generation", "architecture": "llama",
|
||
"hf_downloads": 0, "hf_likes": 0, "release_date": None,
|
||
},
|
||
{
|
||
"name": "lmsys/vicuna-13b-v1.5",
|
||
"provider": "LMSYS", "parameter_count": "13.0B",
|
||
"parameters_raw": 13015864320,
|
||
"min_ram_gb": 7.3, "recommended_ram_gb": 12.1, "min_vram_gb": 6.7,
|
||
"quantization": "Q4_K_M", "context_length": 4096,
|
||
"use_case": "Instruction following, chat",
|
||
"pipeline_tag": "text-generation", "architecture": "llama",
|
||
"hf_downloads": 0, "hf_likes": 0, "release_date": None,
|
||
},
|
||
{
|
||
"name": "NousResearch/Nous-Hermes-2-Mixtral-8x7B-DPO",
|
||
"provider": "NousResearch", "parameter_count": "46.7B",
|
||
"parameters_raw": 46702792704,
|
||
"min_ram_gb": 26.1, "recommended_ram_gb": 43.5, "min_vram_gb": 23.9,
|
||
"quantization": "Q4_K_M", "context_length": 32768,
|
||
"use_case": "Instruction following, chat",
|
||
"pipeline_tag": "text-generation", "architecture": "mixtral",
|
||
"is_moe": True, "num_experts": 8, "active_experts": 2,
|
||
"active_parameters": 12900000000,
|
||
"hf_downloads": 0, "hf_likes": 0, "release_date": None,
|
||
},
|
||
{
|
||
"name": "WizardLMTeam/WizardLM-13B-V1.2",
|
||
"provider": "WizardLM", "parameter_count": "13.0B",
|
||
"parameters_raw": 13015864320,
|
||
"min_ram_gb": 7.3, "recommended_ram_gb": 12.1, "min_vram_gb": 6.7,
|
||
"quantization": "Q4_K_M", "context_length": 4096,
|
||
"use_case": "Instruction following, chat",
|
||
"pipeline_tag": "text-generation", "architecture": "llama",
|
||
"hf_downloads": 0, "hf_likes": 0, "release_date": None,
|
||
},
|
||
{
|
||
"name": "WizardLMTeam/WizardCoder-15B-V1.0",
|
||
"provider": "WizardLM", "parameter_count": "15.5B",
|
||
"parameters_raw": 15515334656,
|
||
"min_ram_gb": 8.7, "recommended_ram_gb": 14.5, "min_vram_gb": 7.9,
|
||
"quantization": "Q4_K_M", "context_length": 8192,
|
||
"use_case": "Code generation and completion",
|
||
"pipeline_tag": "text-generation", "architecture": "starcoder",
|
||
"hf_downloads": 0, "hf_likes": 0, "release_date": None,
|
||
},
|
||
{
|
||
"name": "Qwen/Qwen2.5-Coder-1.5B-Instruct",
|
||
"provider": "Alibaba", "parameter_count": "1.5B",
|
||
"parameters_raw": 1539938304,
|
||
"min_ram_gb": 1.0, "recommended_ram_gb": 2.0, "min_vram_gb": 0.8,
|
||
"quantization": "Q4_K_M", "context_length": 32768,
|
||
"use_case": "Code generation and completion",
|
||
"pipeline_tag": "text-generation", "architecture": "qwen2",
|
||
"hf_downloads": 0, "hf_likes": 0, "release_date": None,
|
||
},
|
||
{
|
||
"name": "Qwen/Qwen2.5-Coder-7B-Instruct",
|
||
"provider": "Alibaba", "parameter_count": "7.6B",
|
||
"parameters_raw": 7615616000,
|
||
"min_ram_gb": 4.3, "recommended_ram_gb": 7.1, "min_vram_gb": 3.9,
|
||
"quantization": "Q4_K_M", "context_length": 32768,
|
||
"use_case": "Code generation and completion",
|
||
"pipeline_tag": "text-generation", "architecture": "qwen2",
|
||
"hf_downloads": 0, "hf_likes": 0, "release_date": None,
|
||
},
|
||
{
|
||
"name": "Qwen/Qwen2.5-Coder-14B-Instruct",
|
||
"provider": "Alibaba", "parameter_count": "14.7B",
|
||
"parameters_raw": 14770000000,
|
||
"min_ram_gb": 8.2, "recommended_ram_gb": 13.7, "min_vram_gb": 7.6,
|
||
"quantization": "Q4_K_M", "context_length": 32768,
|
||
"use_case": "Code generation and completion",
|
||
"pipeline_tag": "text-generation", "architecture": "qwen2",
|
||
"hf_downloads": 0, "hf_likes": 0, "release_date": None,
|
||
},
|
||
{
|
||
"name": "Qwen/Qwen2.5-Coder-32B-Instruct",
|
||
"provider": "Alibaba", "parameter_count": "32.5B",
|
||
"parameters_raw": 32510000000,
|
||
"min_ram_gb": 18.2, "recommended_ram_gb": 30.3, "min_vram_gb": 16.7,
|
||
"quantization": "Q4_K_M", "context_length": 32768,
|
||
"use_case": "Code generation and completion",
|
||
"pipeline_tag": "text-generation", "architecture": "qwen2",
|
||
"hf_downloads": 0, "hf_likes": 0, "release_date": None,
|
||
},
|
||
{
|
||
"name": "Qwen/Qwen2.5-VL-3B-Instruct",
|
||
"provider": "Alibaba", "parameter_count": "3.8B",
|
||
"parameters_raw": 3821000000,
|
||
"min_ram_gb": 2.1, "recommended_ram_gb": 3.6, "min_vram_gb": 2.0,
|
||
"quantization": "Q4_K_M", "context_length": 32768,
|
||
"use_case": "Multimodal, vision and text",
|
||
"pipeline_tag": "image-text-to-text", "architecture": "qwen2_vl",
|
||
"hf_downloads": 0, "hf_likes": 0, "release_date": None,
|
||
},
|
||
{
|
||
"name": "Qwen/Qwen2.5-VL-7B-Instruct",
|
||
"provider": "Alibaba", "parameter_count": "8.3B",
|
||
"parameters_raw": 8290000000,
|
||
"min_ram_gb": 4.6, "recommended_ram_gb": 7.7, "min_vram_gb": 4.2,
|
||
"quantization": "Q4_K_M", "context_length": 32768,
|
||
"use_case": "Multimodal, vision and text",
|
||
"pipeline_tag": "image-text-to-text", "architecture": "qwen2_vl",
|
||
"hf_downloads": 0, "hf_likes": 0, "release_date": None,
|
||
},
|
||
{
|
||
"name": "Qwen/Qwen3-14B",
|
||
"provider": "Alibaba", "parameter_count": "14.8B",
|
||
"parameters_raw": 14770000000,
|
||
"min_ram_gb": 8.2, "recommended_ram_gb": 13.7, "min_vram_gb": 7.6,
|
||
"quantization": "Q4_K_M", "context_length": 131072,
|
||
"use_case": "General purpose text generation",
|
||
"pipeline_tag": "text-generation", "architecture": "qwen3",
|
||
"hf_downloads": 0, "hf_likes": 0, "release_date": None,
|
||
},
|
||
# --- New fallbacks added Feb 2026 ---
|
||
{
|
||
"name": "deepseek-ai/DeepSeek-V3.2",
|
||
"provider": "DeepSeek", "parameter_count": "685B",
|
||
"parameters_raw": 685000000000,
|
||
"min_ram_gb": 383.2, "recommended_ram_gb": 638.7, "min_vram_gb": 351.3,
|
||
"quantization": "Q4_K_M", "context_length": 131072,
|
||
"use_case": "State-of-the-art, MoE architecture",
|
||
"pipeline_tag": "text-generation", "architecture": "deepseek_v3",
|
||
"is_moe": True, "num_experts": 256, "active_experts": 8,
|
||
"active_parameters": 37000000000,
|
||
"hf_downloads": 0, "hf_likes": 0, "release_date": "2025-12-01",
|
||
},
|
||
{
|
||
"name": "deepseek-ai/DeepSeek-V3.2-Speciale",
|
||
"provider": "DeepSeek", "parameter_count": "685B",
|
||
"parameters_raw": 685000000000,
|
||
"min_ram_gb": 383.2, "recommended_ram_gb": 638.7, "min_vram_gb": 351.3,
|
||
"quantization": "Q4_K_M", "context_length": 131072,
|
||
"use_case": "Advanced reasoning, chain-of-thought",
|
||
"pipeline_tag": "text-generation", "architecture": "deepseek_v3",
|
||
"is_moe": True, "num_experts": 256, "active_experts": 8,
|
||
"active_parameters": 37000000000,
|
||
"hf_downloads": 0, "hf_likes": 0, "release_date": "2025-12-01",
|
||
},
|
||
{
|
||
"name": "zai-org/GLM-5",
|
||
"provider": "Zhipu AI", "parameter_count": "744B",
|
||
"parameters_raw": 744000000000,
|
||
"min_ram_gb": 416.2, "recommended_ram_gb": 693.6, "min_vram_gb": 381.4,
|
||
"quantization": "Q4_K_M", "context_length": 200000,
|
||
"use_case": "State-of-the-art, MoE architecture",
|
||
"pipeline_tag": "text-generation", "architecture": "glm",
|
||
"is_moe": True, "num_experts": 256, "active_experts": 8,
|
||
"active_parameters": 40000000000,
|
||
"hf_downloads": 0, "hf_likes": 0, "release_date": "2026-02-11",
|
||
},
|
||
{
|
||
"name": "moonshotai/Kimi-K2.5",
|
||
"provider": "Moonshot", "parameter_count": "171B",
|
||
"parameters_raw": 171000000000,
|
||
"min_ram_gb": 95.6, "recommended_ram_gb": 159.4, "min_vram_gb": 87.7,
|
||
"quantization": "Q4_K_M", "context_length": 262144,
|
||
"use_case": "Multimodal, vision and text",
|
||
"pipeline_tag": "image-text-to-text", "architecture": "kimi",
|
||
"hf_downloads": 0, "hf_likes": 0, "release_date": "2026-01-26",
|
||
},
|
||
{
|
||
"name": "MiniMaxAI/MiniMax-M3",
|
||
"provider": "MiniMax", "parameter_count": "230B",
|
||
"parameters_raw": 230000000000,
|
||
"min_ram_gb": 128.6, "recommended_ram_gb": 214.4, "min_vram_gb": 117.9,
|
||
"quantization": "Q4_K_M", "context_length": 1000000,
|
||
"use_case": "Latest flagship: 1M context, 128K max output, image input",
|
||
"pipeline_tag": "text-generation", "architecture": "minimax",
|
||
"is_moe": True, "num_experts": 32, "active_experts": 2,
|
||
"active_parameters": 10000000000,
|
||
"hf_downloads": 0, "hf_likes": 0, "release_date": "2026-06-03",
|
||
},
|
||
{
|
||
"name": "MiniMaxAI/MiniMax-M2.7",
|
||
"provider": "MiniMax", "parameter_count": "230B",
|
||
"parameters_raw": 230000000000,
|
||
"min_ram_gb": 128.6, "recommended_ram_gb": 214.4, "min_vram_gb": 117.9,
|
||
"quantization": "Q4_K_M", "context_length": 131072,
|
||
"use_case": "Previous flagship with enhanced reasoning and coding",
|
||
"pipeline_tag": "text-generation", "architecture": "minimax",
|
||
"is_moe": True, "num_experts": 32, "active_experts": 2,
|
||
"active_parameters": 10000000000,
|
||
"hf_downloads": 0, "hf_likes": 0, "release_date": "2026-03-18",
|
||
},
|
||
{
|
||
"name": "XiaomiMiMo/MiMo-V2-Flash",
|
||
"provider": "Xiaomi", "parameter_count": "309B",
|
||
"parameters_raw": 309000000000,
|
||
"min_ram_gb": 172.8, "recommended_ram_gb": 288.0, "min_vram_gb": 158.4,
|
||
"quantization": "Q4_K_M", "context_length": 131072,
|
||
"use_case": "Efficient reasoning, coding",
|
||
"pipeline_tag": "text-generation", "architecture": "mimo",
|
||
"is_moe": True, "num_experts": 128, "active_experts": 8,
|
||
"active_parameters": 15000000000,
|
||
"hf_downloads": 0, "hf_likes": 0, "release_date": "2025-12-01",
|
||
},
|
||
{
|
||
"name": "XiaomiMiMo/MiMo-7B-RL",
|
||
"provider": "Xiaomi", "parameter_count": "7.0B",
|
||
"parameters_raw": 7000000000,
|
||
"min_ram_gb": 3.9, "recommended_ram_gb": 6.5, "min_vram_gb": 3.6,
|
||
"quantization": "Q4_K_M", "context_length": 32768,
|
||
"use_case": "Advanced reasoning, math and code",
|
||
"pipeline_tag": "text-generation", "architecture": "mimo",
|
||
"hf_downloads": 0, "hf_likes": 0, "release_date": "2025-05-01",
|
||
},
|
||
{
|
||
"name": "nvidia/NVIDIA-Nemotron-3-Nano-30B-A3B-BF16",
|
||
"provider": "NVIDIA", "parameter_count": "30B",
|
||
"parameters_raw": 30000000000,
|
||
"min_ram_gb": 16.8, "recommended_ram_gb": 28.0, "min_vram_gb": 15.4,
|
||
"quantization": "Q4_K_M", "context_length": 1048576,
|
||
"use_case": "Efficient MoE, agentic tasks",
|
||
"pipeline_tag": "text-generation", "architecture": "nemotron",
|
||
"is_moe": True, "num_experts": 128, "active_experts": 6,
|
||
"active_parameters": 3000000000,
|
||
"hf_downloads": 0, "hf_likes": 0, "release_date": "2025-06-01",
|
||
},
|
||
{
|
||
"name": "nvidia/NVIDIA-Nemotron-Nano-9B-v2",
|
||
"provider": "NVIDIA", "parameter_count": "9B",
|
||
"parameters_raw": 9000000000,
|
||
"min_ram_gb": 5.0, "recommended_ram_gb": 8.4, "min_vram_gb": 4.6,
|
||
"quantization": "Q4_K_M", "context_length": 131072,
|
||
"use_case": "Hybrid Mamba2, reasoning",
|
||
"pipeline_tag": "text-generation", "architecture": "nemotron",
|
||
"hf_downloads": 0, "hf_likes": 0, "release_date": "2025-06-01",
|
||
},
|
||
{
|
||
"name": "microsoft/Phi-4-reasoning",
|
||
"provider": "Microsoft", "parameter_count": "14B",
|
||
"parameters_raw": 14000000000,
|
||
"min_ram_gb": 7.8, "recommended_ram_gb": 13.0, "min_vram_gb": 7.2,
|
||
"quantization": "Q4_K_M", "context_length": 32768,
|
||
"use_case": "Advanced reasoning, math and code",
|
||
"pipeline_tag": "text-generation", "architecture": "phi4",
|
||
"hf_downloads": 0, "hf_likes": 0, "release_date": "2025-04-01",
|
||
},
|
||
{
|
||
"name": "microsoft/Phi-4-mini-reasoning",
|
||
"provider": "Microsoft", "parameter_count": "3.8B",
|
||
"parameters_raw": 3800000000,
|
||
"min_ram_gb": 2.1, "recommended_ram_gb": 3.5, "min_vram_gb": 1.9,
|
||
"quantization": "Q4_K_M", "context_length": 16384,
|
||
"use_case": "Lightweight reasoning",
|
||
"pipeline_tag": "text-generation", "architecture": "phi4",
|
||
"hf_downloads": 0, "hf_likes": 0, "release_date": "2025-04-01",
|
||
},
|
||
{
|
||
"name": "microsoft/Phi-4-multimodal-instruct",
|
||
"provider": "Microsoft", "parameter_count": "14B",
|
||
"parameters_raw": 14000000000,
|
||
"min_ram_gb": 7.8, "recommended_ram_gb": 13.0, "min_vram_gb": 7.2,
|
||
"quantization": "Q4_K_M", "context_length": 131072,
|
||
"use_case": "Multimodal, vision and audio",
|
||
"pipeline_tag": "image-text-to-text", "architecture": "phi4",
|
||
"hf_downloads": 0, "hf_likes": 0, "release_date": "2025-04-01",
|
||
},
|
||
{
|
||
"name": "LGAI-EXAONE/EXAONE-4.0-32B",
|
||
"provider": "LG AI", "parameter_count": "32B",
|
||
"parameters_raw": 32000000000,
|
||
"min_ram_gb": 17.9, "recommended_ram_gb": 29.8, "min_vram_gb": 16.4,
|
||
"quantization": "Q4_K_M", "context_length": 131072,
|
||
"use_case": "Hybrid reasoning, multilingual",
|
||
"pipeline_tag": "text-generation", "architecture": "exaone",
|
||
"hf_downloads": 0, "hf_likes": 0, "release_date": "2025-07-15",
|
||
},
|
||
{
|
||
"name": "LGAI-EXAONE/EXAONE-4.0-1.2B",
|
||
"provider": "LG AI", "parameter_count": "1.2B",
|
||
"parameters_raw": 1200000000,
|
||
"min_ram_gb": 0.7, "recommended_ram_gb": 1.1, "min_vram_gb": 0.6,
|
||
"quantization": "Q4_K_M", "context_length": 32768,
|
||
"use_case": "Lightweight, on-device",
|
||
"pipeline_tag": "text-generation", "architecture": "exaone",
|
||
"hf_downloads": 0, "hf_likes": 0, "release_date": "2025-07-15",
|
||
},
|
||
{
|
||
"name": "HuggingFaceTB/SmolLM3-3B",
|
||
"provider": "HuggingFace", "parameter_count": "3B",
|
||
"parameters_raw": 3000000000,
|
||
"min_ram_gb": 1.7, "recommended_ram_gb": 2.8, "min_vram_gb": 1.5,
|
||
"quantization": "Q4_K_M", "context_length": 131072,
|
||
"use_case": "Lightweight, multilingual reasoning",
|
||
"pipeline_tag": "text-generation", "architecture": "smollm",
|
||
"hf_downloads": 0, "hf_likes": 0, "release_date": "2025-07-08",
|
||
},
|
||
{
|
||
"name": "google/gemma-3n-E4B-it",
|
||
"provider": "Google", "parameter_count": "8B",
|
||
"parameters_raw": 8000000000,
|
||
"min_ram_gb": 4.5, "recommended_ram_gb": 7.5, "min_vram_gb": 4.1,
|
||
"quantization": "Q4_K_M", "context_length": 131072,
|
||
"use_case": "Multimodal, on-device (effective 4B)",
|
||
"pipeline_tag": "image-text-to-text", "architecture": "gemma3n",
|
||
"hf_downloads": 0, "hf_likes": 0, "release_date": "2025-06-25",
|
||
},
|
||
{
|
||
"name": "google/gemma-3n-E2B-it",
|
||
"provider": "Google", "parameter_count": "4B",
|
||
"parameters_raw": 4000000000,
|
||
"min_ram_gb": 2.2, "recommended_ram_gb": 3.7, "min_vram_gb": 2.1,
|
||
"quantization": "Q4_K_M", "context_length": 131072,
|
||
"use_case": "Multimodal, on-device (effective 2B)",
|
||
"pipeline_tag": "image-text-to-text", "architecture": "gemma3n",
|
||
"hf_downloads": 0, "hf_likes": 0, "release_date": "2025-06-25",
|
||
},
|
||
# Google Gemma 4 family
|
||
{
|
||
"name": "google/gemma-4-E2B-it",
|
||
"provider": "Google", "parameter_count": "5.1B",
|
||
"parameters_raw": 5100000000,
|
||
"min_ram_gb": 2.9, "recommended_ram_gb": 4.8, "min_vram_gb": 2.6,
|
||
"quantization": "Q4_K_M", "context_length": 131072,
|
||
"use_case": "Multimodal, on-device (effective 2B)",
|
||
"pipeline_tag": "image-text-to-text", "architecture": "gemma4",
|
||
"hf_downloads": 0, "hf_likes": 0, "release_date": "2025-07-30",
|
||
},
|
||
{
|
||
"name": "google/gemma-4-E4B-it",
|
||
"provider": "Google", "parameter_count": "8B",
|
||
"parameters_raw": 8000000000,
|
||
"min_ram_gb": 4.5, "recommended_ram_gb": 7.5, "min_vram_gb": 4.1,
|
||
"quantization": "Q4_K_M", "context_length": 131072,
|
||
"use_case": "Multimodal, on-device (effective 4B)",
|
||
"pipeline_tag": "image-text-to-text", "architecture": "gemma4",
|
||
"hf_downloads": 0, "hf_likes": 0, "release_date": "2025-07-30",
|
||
},
|
||
{
|
||
"name": "google/gemma-4-31B-it",
|
||
"provider": "Google", "parameter_count": "31B",
|
||
"parameters_raw": 31000000000,
|
||
"min_ram_gb": 17.3, "recommended_ram_gb": 28.9, "min_vram_gb": 15.9,
|
||
"quantization": "Q4_K_M", "context_length": 262144,
|
||
"use_case": "Multimodal, vision and text",
|
||
"pipeline_tag": "image-text-to-text", "architecture": "gemma4",
|
||
"hf_downloads": 0, "hf_likes": 0, "release_date": "2025-07-30",
|
||
},
|
||
{
|
||
"name": "google/gemma-4-26B-A4B-it",
|
||
"provider": "Google", "parameter_count": "26B",
|
||
"parameters_raw": 26000000000,
|
||
"min_ram_gb": 14.5, "recommended_ram_gb": 24.2, "min_vram_gb": 13.3,
|
||
"quantization": "Q4_K_M", "context_length": 262144,
|
||
"use_case": "Multimodal, vision and text",
|
||
"pipeline_tag": "image-text-to-text", "architecture": "gemma4",
|
||
"is_moe": True, "num_experts": 128, "active_experts": 8,
|
||
"active_parameters": 4_000_000_000,
|
||
"hf_downloads": 0, "hf_likes": 0, "release_date": "2025-07-30",
|
||
},
|
||
# Qwen3-Coder-Next (80B MoE, 3B active, Jan 2026)
|
||
{
|
||
"name": "Qwen/Qwen3-Coder-Next",
|
||
"provider": "Alibaba", "parameter_count": "80B",
|
||
"parameters_raw": 80000000000,
|
||
"min_ram_gb": 44.8, "recommended_ram_gb": 74.6, "min_vram_gb": 41.0,
|
||
"quantization": "Q4_K_M", "context_length": 262144,
|
||
"use_case": "Code generation, agentic coding",
|
||
"pipeline_tag": "text-generation", "architecture": "qwen3_next",
|
||
"is_moe": True, "num_experts": 64, "active_experts": 4,
|
||
"active_parameters": 3000000000,
|
||
"hf_downloads": 0, "hf_likes": 0, "release_date": "2026-01-30",
|
||
},
|
||
{
|
||
"name": "Qwen/Qwen3.5-27B",
|
||
"provider": "Alibaba", "parameter_count": "27.8B",
|
||
"parameters_raw": 27781427952,
|
||
"min_ram_gb": 15.5, "recommended_ram_gb": 25.9, "min_vram_gb": 14.2,
|
||
"quantization": "Q4_K_M", "context_length": 262144,
|
||
"use_case": "Multimodal, vision and text",
|
||
"pipeline_tag": "image-text-to-text", "architecture": "qwen3_5",
|
||
"hf_downloads": 0, "hf_likes": 0, "release_date": None,
|
||
},
|
||
{
|
||
"name": "Qwen/Qwen3.5-35B-A3B",
|
||
"provider": "Alibaba", "parameter_count": "36.0B",
|
||
"parameters_raw": 35951822704,
|
||
"min_ram_gb": 20.1, "recommended_ram_gb": 33.5, "min_vram_gb": 18.4,
|
||
"quantization": "Q4_K_M", "context_length": 262144,
|
||
"use_case": "Multimodal, vision and text",
|
||
"pipeline_tag": "image-text-to-text", "architecture": "qwen3_5_moe",
|
||
"hf_downloads": 0, "hf_likes": 0, "release_date": None,
|
||
"is_moe": True, "num_experts": 256, "active_experts": 8,
|
||
"active_parameters": 3_000_000_000,
|
||
},
|
||
{
|
||
"name": "Qwen/Qwen3.5-122B-A10B",
|
||
"provider": "Alibaba", "parameter_count": "125.1B",
|
||
"parameters_raw": 125086497008,
|
||
"min_ram_gb": 69.9, "recommended_ram_gb": 116.5, "min_vram_gb": 64.1,
|
||
"quantization": "Q4_K_M", "context_length": 262144,
|
||
"use_case": "Multimodal, vision and text",
|
||
"pipeline_tag": "image-text-to-text", "architecture": "qwen3_5_moe",
|
||
"hf_downloads": 0, "hf_likes": 0, "release_date": None,
|
||
"is_moe": True, "num_experts": 256, "active_experts": 8,
|
||
"active_parameters": 10_000_000_000,
|
||
},
|
||
{
|
||
"name": "Qwen/Qwen3.5-397B-A17B",
|
||
"provider": "Alibaba", "parameter_count": "403.4B",
|
||
"parameters_raw": 403397928944,
|
||
"min_ram_gb": 225.4, "recommended_ram_gb": 375.7, "min_vram_gb": 206.6,
|
||
"quantization": "Q4_K_M", "context_length": 262144,
|
||
"use_case": "Multimodal, vision and text",
|
||
"pipeline_tag": "image-text-to-text", "architecture": "qwen3_5_moe",
|
||
"hf_downloads": 0, "hf_likes": 0, "release_date": None,
|
||
"is_moe": True, "num_experts": 256, "active_experts": 8,
|
||
"active_parameters": 17_000_000_000,
|
||
},
|
||
# Liquid AI LFM2 dense models
|
||
{
|
||
"name": "LiquidAI/LFM2-350M",
|
||
"provider": "Liquid AI", "parameter_count": "354M",
|
||
"parameters_raw": 354483968,
|
||
"min_ram_gb": 1.0, "recommended_ram_gb": 2.0, "min_vram_gb": 0.5,
|
||
"quantization": "Q4_K_M", "context_length": 128000,
|
||
"use_case": "Lightweight, edge deployment",
|
||
"pipeline_tag": "text-generation", "architecture": "lfm2",
|
||
"hf_downloads": 0, "hf_likes": 0, "release_date": "2025-11-28",
|
||
},
|
||
{
|
||
"name": "LiquidAI/LFM2-700M",
|
||
"provider": "Liquid AI", "parameter_count": "742M",
|
||
"parameters_raw": 742489344,
|
||
"min_ram_gb": 1.0, "recommended_ram_gb": 2.0, "min_vram_gb": 0.5,
|
||
"quantization": "Q4_K_M", "context_length": 128000,
|
||
"use_case": "Lightweight, edge deployment",
|
||
"pipeline_tag": "text-generation", "architecture": "lfm2",
|
||
"hf_downloads": 0, "hf_likes": 0, "release_date": "2025-11-28",
|
||
},
|
||
{
|
||
"name": "LiquidAI/LFM2-1.2B",
|
||
"provider": "Liquid AI", "parameter_count": "1.2B",
|
||
"parameters_raw": 1170340608,
|
||
"min_ram_gb": 1.0, "recommended_ram_gb": 2.0, "min_vram_gb": 0.6,
|
||
"quantization": "Q4_K_M", "context_length": 128000,
|
||
"use_case": "General purpose text generation",
|
||
"pipeline_tag": "text-generation", "architecture": "lfm2",
|
||
"hf_downloads": 0, "hf_likes": 0, "release_date": "2025-11-28",
|
||
},
|
||
{
|
||
"name": "LiquidAI/LFM2-2.6B",
|
||
"provider": "Liquid AI", "parameter_count": "2.6B",
|
||
"parameters_raw": 2569272320,
|
||
"min_ram_gb": 1.4, "recommended_ram_gb": 2.4, "min_vram_gb": 1.3,
|
||
"quantization": "Q4_K_M", "context_length": 128000,
|
||
"use_case": "General purpose text generation",
|
||
"pipeline_tag": "text-generation", "architecture": "lfm2",
|
||
"hf_downloads": 0, "hf_likes": 0, "release_date": "2025-11-28",
|
||
},
|
||
{
|
||
"name": "LiquidAI/LFM2-2.6B-Exp",
|
||
"provider": "Liquid AI", "parameter_count": "2.6B",
|
||
"parameters_raw": 2569272320,
|
||
"min_ram_gb": 1.4, "recommended_ram_gb": 2.4, "min_vram_gb": 1.3,
|
||
"quantization": "Q4_K_M", "context_length": 128000,
|
||
"use_case": "Instruction following, math, knowledge",
|
||
"pipeline_tag": "text-generation", "architecture": "lfm2",
|
||
"hf_downloads": 0, "hf_likes": 0, "release_date": "2025-11-28",
|
||
},
|
||
# Liquid AI LFM2 MoE models
|
||
{
|
||
"name": "LiquidAI/LFM2-8B-A1B",
|
||
"provider": "Liquid AI", "parameter_count": "8.3B",
|
||
"parameters_raw": 8300000000,
|
||
"min_ram_gb": 4.6, "recommended_ram_gb": 7.7, "min_vram_gb": 4.3,
|
||
"quantization": "Q4_K_M", "context_length": 128000,
|
||
"use_case": "General purpose, edge MoE",
|
||
"pipeline_tag": "text-generation", "architecture": "lfm2",
|
||
"is_moe": True, "num_experts": 32, "active_experts": 4,
|
||
"active_parameters": 1500000000,
|
||
"hf_downloads": 0, "hf_likes": 0, "release_date": "2025-11-28",
|
||
},
|
||
{
|
||
"name": "LiquidAI/LFM2-24B-A2B",
|
||
"provider": "Liquid AI", "parameter_count": "23.8B",
|
||
"parameters_raw": 23_843_661_440,
|
||
"min_ram_gb": 13.3, "recommended_ram_gb": 22.2, "min_vram_gb": 12.2,
|
||
"quantization": "Q4_K_M", "context_length": 128000,
|
||
"use_case": "Agentic tasks, RAG, summarization",
|
||
"pipeline_tag": "text-generation", "architecture": "lfm2",
|
||
"is_moe": True, "num_experts": 32, "active_experts": 4,
|
||
"active_parameters": 2300000000,
|
||
"hf_downloads": 0, "hf_likes": 0, "release_date": "2025-11-28",
|
||
},
|
||
# Liquid AI LFM2.5 models
|
||
{
|
||
"name": "LiquidAI/LFM2.5-1.2B-Base",
|
||
"provider": "Liquid AI", "parameter_count": "1.2B",
|
||
"parameters_raw": 1170340608,
|
||
"min_ram_gb": 1.0, "recommended_ram_gb": 2.0, "min_vram_gb": 0.6,
|
||
"quantization": "Q4_K_M", "context_length": 128000,
|
||
"use_case": "General purpose text generation",
|
||
"pipeline_tag": "text-generation", "architecture": "lfm2",
|
||
"hf_downloads": 0, "hf_likes": 0, "release_date": "2025-11-28",
|
||
},
|
||
{
|
||
"name": "LiquidAI/LFM2.5-1.2B-Instruct",
|
||
"provider": "Liquid AI", "parameter_count": "1.2B",
|
||
"parameters_raw": 1170340608,
|
||
"min_ram_gb": 1.0, "recommended_ram_gb": 2.0, "min_vram_gb": 0.6,
|
||
"quantization": "Q4_K_M", "context_length": 128000,
|
||
"use_case": "Instruction following, chat",
|
||
"pipeline_tag": "text-generation", "architecture": "lfm2",
|
||
"hf_downloads": 0, "hf_likes": 0, "release_date": "2025-11-28",
|
||
},
|
||
{
|
||
"name": "LiquidAI/LFM2.5-1.2B-Thinking",
|
||
"provider": "Liquid AI", "parameter_count": "1.2B",
|
||
"parameters_raw": 1170340608,
|
||
"min_ram_gb": 1.0, "recommended_ram_gb": 2.0, "min_vram_gb": 0.6,
|
||
"quantization": "Q4_K_M", "context_length": 128000,
|
||
"use_case": "Advanced reasoning, chain-of-thought",
|
||
"pipeline_tag": "text-generation", "architecture": "lfm2",
|
||
"hf_downloads": 0, "hf_likes": 0, "release_date": "2025-11-28",
|
||
},
|
||
{
|
||
"name": "LiquidAI/LFM2.5-1.2B-JP",
|
||
"provider": "Liquid AI", "parameter_count": "1.2B",
|
||
"parameters_raw": 1170340608,
|
||
"min_ram_gb": 1.0, "recommended_ram_gb": 2.0, "min_vram_gb": 0.6,
|
||
"quantization": "Q4_K_M", "context_length": 128000,
|
||
"use_case": "Japanese language, multilingual chat",
|
||
"pipeline_tag": "text-generation", "architecture": "lfm2",
|
||
"hf_downloads": 0, "hf_likes": 0, "release_date": "2025-11-28",
|
||
},
|
||
# Liquid AI LFM2 Vision-Language models
|
||
{
|
||
"name": "LiquidAI/LFM2-VL-450M",
|
||
"provider": "Liquid AI", "parameter_count": "451M",
|
||
"parameters_raw": 450822656,
|
||
"min_ram_gb": 1.0, "recommended_ram_gb": 2.0, "min_vram_gb": 0.5,
|
||
"quantization": "Q4_K_M", "context_length": 32768,
|
||
"use_case": "Multimodal, vision and text",
|
||
"pipeline_tag": "image-text-to-text", "architecture": "lfm2",
|
||
"hf_downloads": 0, "hf_likes": 0, "release_date": "2025-11-28",
|
||
},
|
||
{
|
||
"name": "LiquidAI/LFM2-VL-1.6B",
|
||
"provider": "Liquid AI", "parameter_count": "1.6B",
|
||
"parameters_raw": 1584804000,
|
||
"min_ram_gb": 1.0, "recommended_ram_gb": 2.0, "min_vram_gb": 0.8,
|
||
"quantization": "Q4_K_M", "context_length": 32768,
|
||
"use_case": "Multimodal, vision and text",
|
||
"pipeline_tag": "image-text-to-text", "architecture": "lfm2",
|
||
"hf_downloads": 0, "hf_likes": 0, "release_date": "2025-11-28",
|
||
},
|
||
{
|
||
"name": "LiquidAI/LFM2-VL-3B",
|
||
"provider": "Liquid AI", "parameter_count": "3.0B",
|
||
"parameters_raw": 2998975216,
|
||
"min_ram_gb": 1.7, "recommended_ram_gb": 2.8, "min_vram_gb": 1.5,
|
||
"quantization": "Q4_K_M", "context_length": 32768,
|
||
"use_case": "Multimodal, vision and text",
|
||
"pipeline_tag": "image-text-to-text", "architecture": "lfm2",
|
||
"hf_downloads": 0, "hf_likes": 0, "release_date": "2025-11-28",
|
||
},
|
||
{
|
||
"name": "LiquidAI/LFM2.5-VL-1.6B",
|
||
"provider": "Liquid AI", "parameter_count": "1.6B",
|
||
"parameters_raw": 1596625904,
|
||
"min_ram_gb": 1.0, "recommended_ram_gb": 2.0, "min_vram_gb": 0.8,
|
||
"quantization": "Q4_K_M", "context_length": 32768,
|
||
"use_case": "Multimodal, vision and text",
|
||
"pipeline_tag": "image-text-to-text", "architecture": "lfm2",
|
||
"hf_downloads": 0, "hf_likes": 0, "release_date": "2025-11-28",
|
||
},
|
||
# Liquid AI LFM2 Audio models
|
||
{
|
||
"name": "LiquidAI/LFM2-Audio-1.5B",
|
||
"provider": "Liquid AI", "parameter_count": "1.5B",
|
||
"parameters_raw": 1500000000,
|
||
"min_ram_gb": 1.0, "recommended_ram_gb": 2.0, "min_vram_gb": 0.8,
|
||
"quantization": "Q4_K_M", "context_length": 32768,
|
||
"use_case": "Speech-to-speech, ASR, TTS",
|
||
"pipeline_tag": "audio-to-audio", "architecture": "lfm2",
|
||
"hf_downloads": 0, "hf_likes": 0, "release_date": "2025-11-28",
|
||
},
|
||
{
|
||
"name": "LiquidAI/LFM2.5-Audio-1.5B",
|
||
"provider": "Liquid AI", "parameter_count": "1.5B",
|
||
"parameters_raw": 1500000000,
|
||
"min_ram_gb": 1.0, "recommended_ram_gb": 2.0, "min_vram_gb": 0.8,
|
||
"quantization": "Q4_K_M", "context_length": 32768,
|
||
"use_case": "Speech-to-speech, ASR, TTS",
|
||
"pipeline_tag": "audio-to-audio", "architecture": "lfm2",
|
||
"hf_downloads": 0, "hf_likes": 0, "release_date": "2025-11-28",
|
||
},
|
||
# Liquid AI Liquid Nanos (task-specific fine-tunes)
|
||
{
|
||
"name": "LiquidAI/LFM2-1.2B-Tool",
|
||
"provider": "Liquid AI", "parameter_count": "1.2B",
|
||
"parameters_raw": 1170340608,
|
||
"min_ram_gb": 1.0, "recommended_ram_gb": 2.0, "min_vram_gb": 0.6,
|
||
"quantization": "Q4_K_M", "context_length": 128000,
|
||
"use_case": "Tool calling, function calling",
|
||
"pipeline_tag": "text-generation", "architecture": "lfm2",
|
||
"hf_downloads": 0, "hf_likes": 0, "release_date": "2025-11-28",
|
||
},
|
||
{
|
||
"name": "LiquidAI/LFM2-1.2B-RAG",
|
||
"provider": "Liquid AI", "parameter_count": "1.2B",
|
||
"parameters_raw": 1170340608,
|
||
"min_ram_gb": 1.0, "recommended_ram_gb": 2.0, "min_vram_gb": 0.6,
|
||
"quantization": "Q4_K_M", "context_length": 128000,
|
||
"use_case": "Retrieval-augmented generation",
|
||
"pipeline_tag": "text-generation", "architecture": "lfm2",
|
||
"hf_downloads": 0, "hf_likes": 0, "release_date": "2025-11-28",
|
||
},
|
||
{
|
||
"name": "LiquidAI/LFM2-1.2B-Extract",
|
||
"provider": "Liquid AI", "parameter_count": "1.2B",
|
||
"parameters_raw": 1170340608,
|
||
"min_ram_gb": 1.0, "recommended_ram_gb": 2.0, "min_vram_gb": 0.6,
|
||
"quantization": "Q4_K_M", "context_length": 128000,
|
||
"use_case": "Data extraction, structured output",
|
||
"pipeline_tag": "text-generation", "architecture": "lfm2",
|
||
"hf_downloads": 0, "hf_likes": 0, "release_date": "2025-11-28",
|
||
},
|
||
{
|
||
"name": "LiquidAI/LFM2-350M-Extract",
|
||
"provider": "Liquid AI", "parameter_count": "354M",
|
||
"parameters_raw": 354483968,
|
||
"min_ram_gb": 1.0, "recommended_ram_gb": 2.0, "min_vram_gb": 0.5,
|
||
"quantization": "Q4_K_M", "context_length": 128000,
|
||
"use_case": "Data extraction, structured output",
|
||
"pipeline_tag": "text-generation", "architecture": "lfm2",
|
||
"hf_downloads": 0, "hf_likes": 0, "release_date": "2025-11-28",
|
||
},
|
||
{
|
||
"name": "LiquidAI/LFM2-350M-Math",
|
||
"provider": "Liquid AI", "parameter_count": "354M",
|
||
"parameters_raw": 354483968,
|
||
"min_ram_gb": 1.0, "recommended_ram_gb": 2.0, "min_vram_gb": 0.5,
|
||
"quantization": "Q4_K_M", "context_length": 128000,
|
||
"use_case": "Math reasoning, chain-of-thought",
|
||
"pipeline_tag": "text-generation", "architecture": "lfm2",
|
||
"hf_downloads": 0, "hf_likes": 0, "release_date": "2025-11-28",
|
||
},
|
||
{
|
||
"name": "LiquidAI/LFM2-350M-ENJP-MT",
|
||
"provider": "Liquid AI", "parameter_count": "354M",
|
||
"parameters_raw": 354483968,
|
||
"min_ram_gb": 1.0, "recommended_ram_gb": 2.0, "min_vram_gb": 0.5,
|
||
"quantization": "Q4_K_M", "context_length": 128000,
|
||
"use_case": "English-Japanese translation",
|
||
"pipeline_tag": "translation", "architecture": "lfm2",
|
||
"hf_downloads": 0, "hf_likes": 0, "release_date": "2025-11-28",
|
||
},
|
||
{
|
||
"name": "LiquidAI/LFM2-350M-PII-Extract-JP",
|
||
"provider": "Liquid AI", "parameter_count": "354M",
|
||
"parameters_raw": 354483968,
|
||
"min_ram_gb": 1.0, "recommended_ram_gb": 2.0, "min_vram_gb": 0.5,
|
||
"quantization": "Q4_K_M", "context_length": 128000,
|
||
"use_case": "PII extraction, Japanese",
|
||
"pipeline_tag": "text-generation", "architecture": "lfm2",
|
||
"hf_downloads": 0, "hf_likes": 0, "release_date": "2025-11-28",
|
||
},
|
||
{
|
||
"name": "LiquidAI/LFM2-ColBERT-350M",
|
||
"provider": "Liquid AI", "parameter_count": "353M",
|
||
"parameters_raw": 353322752,
|
||
"min_ram_gb": 1.0, "recommended_ram_gb": 2.0, "min_vram_gb": 0.5,
|
||
"quantization": "Q4_K_M", "context_length": 128000,
|
||
"use_case": "Semantic search, sentence similarity",
|
||
"pipeline_tag": "sentence-similarity", "architecture": "lfm2",
|
||
"hf_downloads": 0, "hf_likes": 0, "release_date": "2025-11-28",
|
||
},
|
||
{
|
||
"name": "LiquidAI/LFM2-2.6B-Transcript",
|
||
"provider": "Liquid AI", "parameter_count": "2.6B",
|
||
"parameters_raw": 2569272320,
|
||
"min_ram_gb": 1.4, "recommended_ram_gb": 2.4, "min_vram_gb": 1.3,
|
||
"quantization": "Q4_K_M", "context_length": 128000,
|
||
"use_case": "Meeting transcription, summarization",
|
||
"pipeline_tag": "text-generation", "architecture": "lfm2",
|
||
"hf_downloads": 0, "hf_likes": 0, "release_date": "2025-11-28",
|
||
},
|
||
{
|
||
"name": "hexgrad/Kokoro-82M",
|
||
"provider": "hexgrad", "parameter_count": "82M",
|
||
"parameters_raw": 82_000_000,
|
||
"min_ram_gb": 1.0, "recommended_ram_gb": 2.0, "min_vram_gb": 0.5,
|
||
"quantization": "F16", "format": "safetensors", "context_length": 4096,
|
||
"use_case": "Text-to-speech",
|
||
"capabilities": ["audio", "tts"], "languages": [],
|
||
"pipeline_tag": "text-to-speech", "architecture": "unknown",
|
||
"hf_downloads": 0, "hf_likes": 0, "release_date": None,
|
||
},
|
||
{
|
||
"name": "microsoft/speecht5_tts",
|
||
"provider": "Microsoft", "parameter_count": "144M",
|
||
"parameters_raw": 144_000_000,
|
||
"min_ram_gb": 1.0, "recommended_ram_gb": 2.0, "min_vram_gb": 0.5,
|
||
"quantization": "F16", "format": "safetensors", "context_length": 4096,
|
||
"use_case": "Text-to-speech",
|
||
"capabilities": ["audio", "tts"], "languages": [],
|
||
"pipeline_tag": "text-to-speech", "architecture": "speecht5",
|
||
"hf_downloads": 0, "hf_likes": 0, "release_date": None,
|
||
},
|
||
{
|
||
"name": "facebook/mms-tts-eng",
|
||
"provider": "Meta", "parameter_count": "36M",
|
||
"parameters_raw": 36_000_000,
|
||
"min_ram_gb": 1.0, "recommended_ram_gb": 2.0, "min_vram_gb": 0.5,
|
||
"quantization": "F16", "format": "safetensors", "context_length": 4096,
|
||
"use_case": "Text-to-speech",
|
||
"capabilities": ["audio", "tts"], "languages": [],
|
||
"pipeline_tag": "text-to-speech", "architecture": "vits",
|
||
"hf_downloads": 0, "hf_likes": 0, "release_date": None,
|
||
},
|
||
# RWKV v7 G1f: GGUF-native repos — no safetensors metadata, fallback required
|
||
{
|
||
"name": "shoumenchougou/RWKV7-G1f-1.5B-GGUF",
|
||
"provider": "RWKV", "parameter_count": "1.5B",
|
||
"parameters_raw": 1_500_000_000,
|
||
"min_ram_gb": 1.0, "recommended_ram_gb": 2.0, "min_vram_gb": 0.8,
|
||
"quantization": "Q4_K_M", "format": "gguf", "context_length": 8192,
|
||
"use_case": "General purpose text generation",
|
||
"capabilities": [],
|
||
"pipeline_tag": "text-generation", "architecture": "rwkv",
|
||
"hf_downloads": 0, "hf_likes": 0, "release_date": None,
|
||
},
|
||
{
|
||
"name": "shoumenchougou/RWKV7-G1f-2.9B-GGUF",
|
||
"provider": "RWKV", "parameter_count": "2.9B",
|
||
"parameters_raw": 2_900_000_000,
|
||
"min_ram_gb": 1.6, "recommended_ram_gb": 2.7, "min_vram_gb": 1.5,
|
||
"quantization": "Q4_K_M", "format": "gguf", "context_length": 8192,
|
||
"use_case": "General purpose text generation",
|
||
"capabilities": [],
|
||
"pipeline_tag": "text-generation", "architecture": "rwkv",
|
||
"hf_downloads": 0, "hf_likes": 0, "release_date": None,
|
||
},
|
||
{
|
||
"name": "shoumenchougou/RWKV7-G1f-7.2B-GGUF",
|
||
"provider": "RWKV", "parameter_count": "7.2B",
|
||
"parameters_raw": 7_200_000_000,
|
||
"min_ram_gb": 4.0, "recommended_ram_gb": 6.7, "min_vram_gb": 3.7,
|
||
"quantization": "Q4_K_M", "format": "gguf", "context_length": 8192,
|
||
"use_case": "General purpose text generation",
|
||
"capabilities": [],
|
||
"pipeline_tag": "text-generation", "architecture": "rwkv",
|
||
"hf_downloads": 0, "hf_likes": 0, "release_date": None,
|
||
},
|
||
{
|
||
"name": "shoumenchougou/RWKV7-G1f-13.3B-GGUF",
|
||
"provider": "RWKV", "parameter_count": "13.3B",
|
||
"parameters_raw": 13_300_000_000,
|
||
"min_ram_gb": 7.4, "recommended_ram_gb": 12.4, "min_vram_gb": 6.8,
|
||
"quantization": "Q4_K_M", "format": "gguf", "context_length": 8192,
|
||
"use_case": "General purpose text generation",
|
||
"capabilities": [],
|
||
"pipeline_tag": "text-generation", "architecture": "rwkv",
|
||
"hf_downloads": 0, "hf_likes": 0, "release_date": None,
|
||
},
|
||
]
|
||
|
||
print(f"Scraping {len(TARGET_MODELS)} curated models from HuggingFace...\n")
|
||
|
||
results, scraped_names = scrape_models_parallel(TARGET_MODELS, args.threads)
|
||
|
||
# Fill in fallbacks for models that couldn't be scraped
|
||
fallback_count = 0
|
||
for fb in FALLBACKS:
|
||
if fb["name"] not in scraped_names:
|
||
print(f" + Fallback: {fb['name']} ({fb['parameter_count']})")
|
||
results.append(fb)
|
||
scraped_names.add(fb["name"])
|
||
fallback_count += 1
|
||
|
||
# Auto-discover trending models if --discover flag is set
|
||
discovered_count = 0
|
||
if args.discover:
|
||
print(f"\nDiscovering top models by downloads (limit={args.discover_limit}, "
|
||
f"min_downloads={args.min_downloads:,})...")
|
||
trending = discover_trending_models(
|
||
limit=args.discover_limit,
|
||
min_downloads=args.min_downloads,
|
||
)
|
||
already_scraped = sum(1 for l in trending if l["id"] in scraped_names)
|
||
print(f"\n Discovery returned {len(trending)} candidates"
|
||
f" ({already_scraped} already scraped)\n")
|
||
|
||
candidates = [l for l in trending if l["id"] not in scraped_names]
|
||
|
||
if args.threads <= 1:
|
||
for i, listing in enumerate(candidates, 1):
|
||
repo_id = listing["id"]
|
||
print(f"[discover {i}/{len(candidates)}] {repo_id}...")
|
||
model = _build_discovered_model(listing)
|
||
if model:
|
||
print(f" ✓ {model['parameter_count']} params, "
|
||
f"{model['hf_downloads']:,} downloads, "
|
||
f"ctx {model['context_length']}")
|
||
results.append(model)
|
||
scraped_names.add(repo_id)
|
||
discovered_count += 1
|
||
time.sleep(0.15)
|
||
else:
|
||
with concurrent.futures.ThreadPoolExecutor(max_workers=args.threads) as executor:
|
||
for i, (listing, model) in enumerate(
|
||
zip(candidates, executor.map(_build_discovered_model, candidates)),
|
||
1,
|
||
):
|
||
repo_id = listing["id"]
|
||
print(f"[discover {i}/{len(candidates)}] {repo_id}...")
|
||
if model:
|
||
print(f" ✓ {model['parameter_count']} params, "
|
||
f"{model['hf_downloads']:,} downloads, "
|
||
f"ctx {model['context_length']}")
|
||
results.append(model)
|
||
scraped_names.add(repo_id)
|
||
discovered_count += 1
|
||
|
||
# --- Additive merge with existing database ---
|
||
# The database is additive: models from previous runs are preserved.
|
||
# Freshly scraped models update existing entries; historical models
|
||
# that are no longer in the top discovered set are kept as-is.
|
||
output_paths = ["llmfit-core/data/hf_models.json"]
|
||
|
||
# Build a map of freshly scraped models (name -> model dict)
|
||
fresh_by_name = {m["name"]: m for m in results}
|
||
|
||
# Load existing database and merge
|
||
existing_count = 0
|
||
retained_count = 0
|
||
updated_count = 0
|
||
for output_path in output_paths:
|
||
if os.path.exists(output_path):
|
||
try:
|
||
with open(output_path) as f:
|
||
existing = json.load(f)
|
||
existing_count = max(existing_count, len(existing))
|
||
for old_model in existing:
|
||
name = old_model.get("name", "")
|
||
if name in fresh_by_name:
|
||
fresh_model = fresh_by_name[name]
|
||
if old_model.get("license") and not fresh_model.get("license"):
|
||
fresh_model["license"] = old_model["license"]
|
||
if old_model.get("gguf_sources") and not fresh_model.get("gguf_sources"):
|
||
fresh_model["gguf_sources"] = old_model["gguf_sources"]
|
||
# Fallback stubs and trending listings carry no
|
||
# popularity/date/language metadata — never let them
|
||
# clobber real values from a previous scrape.
|
||
for key in ("hf_downloads", "hf_likes", "release_date", "languages"):
|
||
if old_model.get(key) and not fresh_model.get(key):
|
||
fresh_model[key] = old_model[key]
|
||
updated_count += 1
|
||
elif name:
|
||
# Historical model not in current scrape — keep it
|
||
results.append(old_model)
|
||
fresh_by_name[name] = old_model
|
||
scraped_names.add(name)
|
||
retained_count += 1
|
||
except (json.JSONDecodeError, KeyError):
|
||
pass
|
||
break # Only need to load from one path
|
||
|
||
if existing_count:
|
||
print(f"\nMerged with existing database ({existing_count} models):")
|
||
print(f" Updated: {updated_count}, Retained historical: {retained_count}")
|
||
|
||
# Keep additive/retained entries on the current schema even if they were
|
||
# produced by an older scraper version.
|
||
for model in results:
|
||
model.setdefault("capabilities", [])
|
||
if not model.get("languages"):
|
||
model.pop("languages", None)
|
||
|
||
# Sort by parameter count
|
||
results.sort(key=lambda m: m["parameters_raw"])
|
||
|
||
# Enrich with GGUF download sources if requested
|
||
gguf_enriched = 0
|
||
if args.gguf_sources:
|
||
print(f"\nEnriching {len(results)} models with GGUF download sources...")
|
||
gguf_enriched = enrich_gguf_sources(results, threads=args.threads)
|
||
print(f" Found GGUF sources for {gguf_enriched} models")
|
||
|
||
# Write to llmfit-core/data (compiled into the binary via include_str!)
|
||
for output_path in output_paths:
|
||
os.makedirs(os.path.dirname(output_path), exist_ok=True)
|
||
with open(output_path, "w") as f:
|
||
json.dump(results, f, indent=2)
|
||
|
||
print(f"\n✅ Wrote {len(results)} models to {', '.join(output_paths)}")
|
||
print(f" Curated: {len(TARGET_MODELS)}, Fallbacks: {fallback_count}, "
|
||
f"Discovered: {discovered_count}, Retained: {retained_count}, "
|
||
f"GGUF-sourced: {gguf_enriched}")
|
||
|
||
# Print summary table
|
||
print(f"\n{'Model':<50} {'Params':>8} {'Min RAM':>8} {'Rec RAM':>8} {'VRAM':>6}")
|
||
print("─" * 84)
|
||
for m in results:
|
||
print(f"{m['name']:<50} {m['parameter_count']:>8} "
|
||
f"{m['min_ram_gb']:>7.1f}G {m['recommended_ram_gb']:>7.1f}G "
|
||
f"{m['min_vram_gb']:>5.1f}G")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|