Skip to content
Closed
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
1b1a090
implement remote gguf inspection on hugging face
pipe1os Jun 27, 2026
71ef3a3
split print_model_info test to comply with codacy method size limit
pipe1os Jun 27, 2026
d0c5474
fix codacy issues: add read limit, honor gpu_util, modularize hf pars…
pipe1os Jun 27, 2026
bebe2c1
refactor: split concurrent shards fetching to lower cyclomatic comple…
pipe1os Jun 27, 2026
6555e0e
fix codacy issues: compute GGUF group variant overhead dynamically
pipe1os Jun 27, 2026
357ee16
docs: document remote gguf inspection options in README.md
pipe1os Jun 27, 2026
5a8e6e2
fix: strip trailing slashes from model paths at entrypoint
pipe1os Jun 27, 2026
7dc8576
fix: handle reverse tensor shape ordering for gguf shape guessing
pipe1os Jun 27, 2026
0ef126b
fix: treat paths starting with local prefix as local files to prevent…
pipe1os Jun 27, 2026
b0b9744
fix: handle concurrent remote shard download failures gracefully
pipe1os Jun 27, 2026
9ba40d0
fix: resolve safetensors shard index prefix splitting
pipe1os Jun 27, 2026
738e291
merge: integrate branch 004-fix-safetensors-shard-prefix
pipe1os Jun 27, 2026
1b1c58f
merge: integrate branch 005-graceful-shard-downloads
pipe1os Jun 27, 2026
56bbf66
merge: integrate branch 006-fix-gguf-shape-guessing
pipe1os Jun 27, 2026
0cfe2e5
merge: integrate branch 007-refine-remote-detection
pipe1os Jun 27, 2026
2deb25a
merge: integrate branch 008-fix-comparison-trailing-slash
pipe1os Jun 27, 2026
32bf12b
fix: address codacy review feedback on disk size, regex, path parsing…
pipe1os Jun 27, 2026
aa331eb
merge: sync with main and apply updates
pipe1os Jun 27, 2026
1acb0cb
merge: sync with main
pipe1os Jun 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,17 @@ Inspect a local model checkpoint:
modelinfo mistral-7b.safetensors
```

Inspect a remote model directly from the Hugging Face Hub:
Inspect a remote model directly from the Hugging Face Hub (both SafeTensors and GGUF):

```bash
# Inspect a remote SafeTensors repository
modelinfo meta-llama/Llama-2-7b-hf

# Inspect a remote GGUF repository (shows a comparison table of all quantizations)
modelinfo bartowski/Meta-Llama-3-8B-Instruct-GGUF

# Inspect a specific remote GGUF file in a repository
modelinfo bartowski/Meta-Llama-3-8B-Instruct-GGUF/Meta-Llama-3-8B-Instruct-Q4_K_M.gguf
```

For gated models (e.g., Llama 2), you must provide authentication by setting the `HF_TOKEN` environment variable. You can create a token in your [Hugging Face settings](https://huggingface.co/settings/tokens).
Expand Down
13 changes: 9 additions & 4 deletions src/modelinfo/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,12 @@ def analyze_model(

file_path_lower = file_path.lower()

if not os.path.exists(file_path) and not file_path_lower.endswith((".safetensors", ".gguf", ".pt", ".bin", ".index.json")):
is_remote = False
if not os.path.exists(file_path):
if "/" in file_path or not file_path_lower.endswith((".safetensors", ".gguf", ".pt", ".bin", ".index.json")):
is_remote = True

if is_remote:
from modelinfo.parsers.huggingface import fetch_huggingface_repo
tensors, config, format_name, disk_size = fetch_huggingface_repo(
file_path, fetch_tensors=fetch_tensors, timeout=timeout
Expand Down Expand Up @@ -180,7 +185,7 @@ def analyze_model(
max_context = None
if config:
max_context = config.get("max_position_embeddings")
elif format_name == "GGUF":
elif format_name in ("GGUF", "GGUF_group"):
metadata = tensors.get("__metadata__", {})
gen_arch = metadata.get("general.architecture")
if gen_arch:
Expand All @@ -207,8 +212,8 @@ def analyze_model(
num_layers = footprint["num_layers"]
arch_name = identify_architecture_name(tensors, num_layers, config)

if format_name != "SafeTensors" or os.path.exists(file_path):
disk_size = os.path.getsize(file_path) if os.path.exists(file_path) else 0.0
if os.path.exists(file_path):
disk_size = os.path.getsize(file_path)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 MEDIUM RISK

Local disk_size calculation for sharded SafeTensors (.index.json) currently reports the size of the index JSON file itself instead of the total weight size of all shards. This creates a discrepancy between local and remote model reporting.


tensor_count = len([k for k in tensors.keys() if k != "__metadata__"])

Expand Down
81 changes: 44 additions & 37 deletions src/modelinfo/parsers/gguf.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,46 +44,53 @@ def _read_gguf_value(f: Any, val_type: int) -> Any:
raise ValueError(f"Unknown GGUF value type: {val_type}")


def parse_gguf_header(path: str) -> Dict[str, Any]:
def parse_gguf_header(path_or_file: str | Any) -> Dict[str, Any]:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚪ LOW RISK

Nitpick: Consider using typing.BinaryIO for the stream parameter instead of Any to improve type safety and static analysis for binary file-like objects.

"""Parses a GGUF file header and extracts tensor information."""
if isinstance(path_or_file, str):
with open(path_or_file, "rb") as f:
return _parse_gguf_header_from_stream(f)
else:
return _parse_gguf_header_from_stream(path_or_file)


def _parse_gguf_header_from_stream(f: Any) -> Dict[str, Any]:
tensors: Dict[str, Any] = {}
magic = f.read(4)
if magic != b"GGUF":
raise ValueError("Invalid GGUF file: Magic bytes missing.")

version = struct.unpack("<I", f.read(4))[0]
if version < 2:
raise ValueError(f"Unsupported GGUF version: {version}")

tensor_count = struct.unpack("<Q", f.read(8))[0]
kv_count = struct.unpack("<Q", f.read(8))[0]

with open(path, "rb") as f:
magic = f.read(4)
if magic != b"GGUF":
raise ValueError("Invalid GGUF file: Magic bytes missing.")

version = struct.unpack("<I", f.read(4))[0]
if version < 2:
raise ValueError(f"Unsupported GGUF version: {version}")

tensor_count = struct.unpack("<Q", f.read(8))[0]
kv_count = struct.unpack("<Q", f.read(8))[0]
metadata = {}
for _ in range(kv_count):
key_len = struct.unpack("<Q", f.read(8))[0]
key_name = f.read(key_len).decode("utf-8")
val_type = struct.unpack("<I", f.read(4))[0]
metadata[key_name] = _read_gguf_value(f, val_type)

metadata = {}
for _ in range(kv_count):
key_len = struct.unpack("<Q", f.read(8))[0]
key_name = f.read(key_len).decode("utf-8")
val_type = struct.unpack("<I", f.read(4))[0]
metadata[key_name] = _read_gguf_value(f, val_type)

tensors["__metadata__"] = metadata

for _ in range(tensor_count):
name_len = struct.unpack("<Q", f.read(8))[0]
name = f.read(name_len).decode("utf-8")

n_dims = struct.unpack("<I", f.read(4))[0]
shape = []
for _ in range(n_dims):
shape.append(struct.unpack("<Q", f.read(8))[0])

t_type = struct.unpack("<I", f.read(4))[0]
f.read(8) # skip offset bytes

# Strict GGUF tensor type mapping
dtype = GGML_TYPE_MAP.get(t_type, "Unknown")

tensors[name] = {"shape": shape, "dtype": dtype}
tensors["__metadata__"] = metadata

for _ in range(tensor_count):
name_len = struct.unpack("<Q", f.read(8))[0]
name = f.read(name_len).decode("utf-8")

n_dims = struct.unpack("<I", f.read(4))[0]
shape = []
for _ in range(n_dims):
shape.append(struct.unpack("<Q", f.read(8))[0])

t_type = struct.unpack("<I", f.read(4))[0]
f.read(8) # skip offset bytes

# Strict GGUF tensor type mapping
dtype = GGML_TYPE_MAP.get(t_type, "Unknown")

tensors[name] = {"shape": shape, "dtype": dtype}

return tensors

Loading