Files
2026-07-13 12:12:21 +08:00

298 lines
10 KiB
Python

#!/usr/bin/env python3
"""
Scraper for Docker Model Runner available models.
Queries the Docker Hub API for models in the 'ai/' namespace,
cross-references against llmfit's HF model database and Ollama mapping table,
and outputs a JSON mapping of HF model names to Docker Model Runner tags.
Usage:
python3 scripts/scrape_docker_models.py
"""
import json
import os
import sys
import urllib.request
import urllib.error
DOCKER_HUB_API = "https://hub.docker.com/v2/repositories/ai/"
PAGE_SIZE = 100
# Same mapping as OLLAMA_MAPPINGS in providers.rs.
# Maps lowercased HF repo suffix → Ollama-style tag (without ai/ prefix).
OLLAMA_MAPPINGS = {
# Meta Llama family
"llama-3.3-70b-instruct": "llama3.3:70b",
"llama-3.2-11b-vision-instruct": "llama3.2-vision:11b",
"llama-3.2-3b-instruct": "llama3.2:3b",
"llama-3.2-3b": "llama3.2:3b",
"llama-3.2-1b-instruct": "llama3.2:1b",
"llama-3.2-1b": "llama3.2:1b",
"llama-3.1-405b-instruct": "llama3.1:405b",
"llama-3.1-405b": "llama3.1:405b",
"llama-3.1-70b-instruct": "llama3.1:70b",
"llama-3.1-8b-instruct": "llama3.1:8b",
"llama-3.1-8b": "llama3.1:8b",
"meta-llama-3-8b-instruct": "llama3:8b",
"meta-llama-3-8b": "llama3:8b",
"llama-2-7b-hf": "llama2:7b",
"codellama-34b-instruct-hf": "codellama:34b",
"codellama-13b-instruct-hf": "codellama:13b",
"codellama-7b-instruct-hf": "codellama:7b",
# Google Gemma
"gemma-3-12b-it": "gemma3:12b",
"gemma-2-27b-it": "gemma2:27b",
"gemma-2-9b-it": "gemma2:9b",
"gemma-2-2b-it": "gemma2:2b",
# Microsoft Phi
"phi-4": "phi4",
"phi-4-mini-instruct": "phi4-mini",
"phi-3.5-mini-instruct": "phi3.5",
"phi-3-mini-4k-instruct": "phi3",
"phi-3-medium-14b-instruct": "phi3:14b",
"phi-2": "phi",
"orca-2-7b": "orca2:7b",
"orca-2-13b": "orca2:13b",
# Mistral
"mistral-7b-instruct-v0.3": "mistral:7b",
"mistral-7b-instruct-v0.2": "mistral:7b",
"mistral-nemo-instruct-2407": "mistral-nemo",
"mistral-small-24b-instruct-2501": "mistral-small:24b",
"mistral-large-instruct-2407": "mistral-large",
"mixtral-8x7b-instruct-v0.1": "mixtral:8x7b",
"mixtral-8x22b-instruct-v0.1": "mixtral:8x22b",
# Qwen 2 / 2.5
"qwen2-1.5b-instruct": "qwen2:1.5b",
"qwen2.5-72b-instruct": "qwen2.5:72b",
"qwen2.5-32b-instruct": "qwen2.5:32b",
"qwen2.5-14b-instruct": "qwen2.5:14b",
"qwen2.5-7b-instruct": "qwen2.5:7b",
"qwen2.5-7b": "qwen2.5:7b",
"qwen2.5-3b-instruct": "qwen2.5:3b",
"qwen2.5-1.5b-instruct": "qwen2.5:1.5b",
"qwen2.5-1.5b": "qwen2.5:1.5b",
"qwen2.5-0.5b-instruct": "qwen2.5:0.5b",
"qwen2.5-0.5b": "qwen2.5:0.5b",
"qwen2.5-coder-32b-instruct": "qwen2.5-coder:32b",
"qwen2.5-coder-14b-instruct": "qwen2.5-coder:14b",
"qwen2.5-coder-7b-instruct": "qwen2.5-coder:7b",
"qwen2.5-coder-1.5b-instruct": "qwen2.5-coder:1.5b",
"qwen2.5-coder-0.5b-instruct": "qwen2.5-coder:0.5b",
"qwen2.5-vl-7b-instruct": "qwen2.5vl:7b",
"qwen2.5-vl-3b-instruct": "qwen2.5vl:3b",
# Qwen 3
"qwen3-235b-a22b": "qwen3:235b",
"qwen3-32b": "qwen3:32b",
"qwen3-30b-a3b": "qwen3:30b-a3b",
"qwen3-30b-a3b-instruct-2507": "qwen3:30b-a3b",
"qwen3-14b": "qwen3:14b",
"qwen3-8b": "qwen3:8b",
"qwen3-4b": "qwen3:4b",
"qwen3-4b-instruct-2507": "qwen3:4b",
"qwen3-1.7b-base": "qwen3:1.7b",
"qwen3-0.6b": "qwen3:0.6b",
"qwen3-coder-30b-a3b-instruct": "qwen3-coder",
# Qwen 3.5
"qwen3.5-27b": "qwen3.5",
"qwen3.5-35b-a3b": "qwen3.5:35b",
"qwen3.5-122b-a10b": "qwen3.5:122b",
# Qwen3-Coder-Next
"qwen3-coder-next": "qwen3-coder-next",
# DeepSeek
"deepseek-v3": "deepseek-v3",
"deepseek-v3.2": "deepseek-v3",
"deepseek-r1": "deepseek-r1",
"deepseek-r1-0528": "deepseek-r1",
"deepseek-r1-distill-qwen-32b": "deepseek-r1:32b",
"deepseek-r1-distill-qwen-14b": "deepseek-r1:14b",
"deepseek-r1-distill-qwen-7b": "deepseek-r1:7b",
"deepseek-coder-v2-lite-instruct": "deepseek-coder-v2:16b",
# Community / other
"tinyllama-1.1b-chat-v1.0": "tinyllama",
"stablelm-2-1_6b-chat": "stablelm2:1.6b",
"yi-6b-chat": "yi:6b",
"yi-34b-chat": "yi:34b",
"starcoder2-7b": "starcoder2:7b",
"starcoder2-15b": "starcoder2:15b",
"falcon-7b-instruct": "falcon:7b",
"falcon-40b-instruct": "falcon:40b",
"falcon-180b-chat": "falcon:180b",
"falcon3-7b-instruct": "falcon3:7b",
"openchat-3.5-0106": "openchat:7b",
"vicuna-7b-v1.5": "vicuna:7b",
"vicuna-13b-v1.5": "vicuna:13b",
"glm-4-9b-chat": "glm4:9b",
"solar-10.7b-instruct-v1.0": "solar:10.7b",
"zephyr-7b-beta": "zephyr:7b",
"c4ai-command-r-v01": "command-r",
"nous-hermes-2-mixtral-8x7b-dpo": "nous-hermes2-mixtral:8x7b",
"hermes-3-llama-3.1-8b": "hermes3:8b",
"nomic-embed-text-v1.5": "nomic-embed-text",
"bge-large-en-v1.5": "bge-large",
"smollm2-135m-instruct": "smollm2:135m",
"smollm2-135m": "smollm2:135m",
# Google Gemma 3n
"gemma-3n-e4b-it": "gemma3n:e4b",
"gemma-3n-e2b-it": "gemma3n:e2b",
# Microsoft Phi-4 reasoning
"phi-4-reasoning": "phi4-reasoning",
"phi-4-mini-reasoning": "phi4-mini-reasoning",
# DeepSeek V3.2 Speciale
"deepseek-v3.2-speciale": "deepseek-v3",
# Liquid AI LFM2
"lfm2-350m": "lfm2:350m",
"lfm2-700m": "lfm2:700m",
"lfm2-1.2b": "lfm2:1.2b",
"lfm2-2.6b": "lfm2:2.6b",
"lfm2-2.6b-exp": "lfm2:2.6b",
"lfm2-8b-a1b": "lfm2:8b-a1b",
"lfm2-24b-a2b": "lfm2:24b",
# Liquid AI LFM2.5
"lfm2.5-1.2b-instruct": "lfm2.5:1.2b",
"lfm2.5-1.2b-thinking": "lfm2.5-thinking:1.2b",
}
def fetch_docker_hub_models() -> list[str]:
"""Fetch all model names from the Docker Hub ai/ namespace."""
models = []
url = f"{DOCKER_HUB_API}?page_size={PAGE_SIZE}"
while url:
req = urllib.request.Request(url, headers={"User-Agent": "llmfit-scraper/1.0"})
try:
with urllib.request.urlopen(req, timeout=10) as resp:
data = json.loads(resp.read().decode())
except (urllib.error.URLError, urllib.error.HTTPError) as e:
print(f"Error fetching {url}: {e}", file=sys.stderr)
break
for repo in data.get("results", []):
name = repo.get("name", "")
if name:
models.append(name)
url = data.get("next")
return models
def fetch_tags_for_model(model_name: str) -> list[str]:
"""Fetch available tags for a Docker Hub ai/ model."""
url = f"{DOCKER_HUB_API}{model_name}/tags/?page_size=100"
req = urllib.request.Request(url, headers={"User-Agent": "llmfit-scraper/1.0"})
try:
with urllib.request.urlopen(req, timeout=10) as resp:
data = json.loads(resp.read().decode())
except (urllib.error.URLError, urllib.error.HTTPError):
return []
return [t["name"] for t in data.get("results", []) if t.get("name")]
def ollama_tag_to_docker_repo(ollama_tag: str) -> str:
"""Extract the Docker Hub repo name from an Ollama tag.
E.g. "llama3.1:8b" → "llama3.1", "phi4" → "phi4"
"""
return ollama_tag.split(":")[0]
def lookup_ollama_tag(hf_name: str) -> str | None:
"""Mirror the Rust lookup_ollama_tag logic.
Extract the repo suffix (after last '/'), lowercase it,
and look it up in the OLLAMA_MAPPINGS dict.
"""
suffix = hf_name.rsplit("/", 1)[-1].lower()
return OLLAMA_MAPPINGS.get(suffix)
def main():
script_dir = os.path.dirname(os.path.abspath(__file__))
project_root = os.path.dirname(script_dir)
output_file = os.path.join(project_root, "llmfit-core", "data", "docker_models.json")
# Load the HF model database to get all model names
hf_models_file = os.path.join(project_root, "llmfit-core", "data", "hf_models.json")
with open(hf_models_file) as f:
hf_models = json.load(f)
print(f"Loaded {len(hf_models)} models from HF database")
# Fetch all available Docker Hub ai/ models
print("Fetching Docker Hub ai/ namespace...")
docker_repos = fetch_docker_hub_models()
# Filter out vllm/safetensors variants — these are alternative serving formats,
# not standard Model Runner models
docker_repos = [r for r in docker_repos if not r.endswith(("-vllm", "-safetensors"))]
docker_repo_set = set(docker_repos)
print(f"Found {len(docker_repos)} Docker Model Runner repos (excl. vllm/safetensors variants)")
# Fetch tags for each available repo
print("Fetching tags for each repo...")
repo_tags: dict[str, list[str]] = {}
for repo in sorted(docker_repos):
tags = fetch_tags_for_model(repo)
repo_tags[repo] = tags
tag_str = ", ".join(tags[:5])
if len(tags) > 5:
tag_str += f", ... ({len(tags)} total)"
print(f" ai/{repo}: [{tag_str}]")
# Cross-reference: for each HF model, check if its Ollama tag maps to a
# Docker Hub repo. Uses the same lookup logic as Rust's lookup_ollama_tag().
mappings = []
matched = 0
unmatched_models = []
for model in hf_models:
hf_name = model["name"]
ollama_tag = lookup_ollama_tag(hf_name)
if not ollama_tag:
unmatched_models.append(hf_name)
continue
docker_repo = ollama_tag_to_docker_repo(ollama_tag)
if docker_repo not in docker_repo_set:
unmatched_models.append(hf_name)
continue
# Build the full Docker tag: ai/<repo>:<size> or ai/<repo>
docker_tag = f"ai/{ollama_tag}"
available_tags = repo_tags.get(docker_repo, [])
mappings.append({
"hf_name": hf_name,
"docker_tag": docker_tag,
"docker_repo": f"ai/{docker_repo}",
"available_tags": available_tags,
})
matched += 1
print()
print(f"Matched: {matched}/{len(hf_models)} models have Docker Model Runner images")
if unmatched_models:
print(f"Unmatched: {len(unmatched_models)} models (no Ollama mapping or no Docker repo)")
# Write output
output = {
"generated_by": "scrape_docker_models.py",
"docker_hub_repo_count": len(docker_repos),
"matched_model_count": matched,
"models": mappings,
}
with open(output_file, "w") as f:
json.dump(output, f, indent=2)
f.write("\n")
print(f"\nWrote {output_file}")
if __name__ == "__main__":
main()