# hf-fetch-model v0.8.0 Roadmap

**Date:** March 17, 2026
**Status:** Proposed
**Context:** v0.7.2 delivers a mature download pipeline (multi-connection chunked transfers, offline cache, retry, checksums). v0.8.0 shifts focus from *downloading faster* to *understanding what you downloaded* and *managing what you have*.

---

## Goals

1. **Safetensors remote introspection** — inspect tensor metadata without downloading full files
2. **Cache management** — verify, measure, and prune the local HF cache
3. **Dry-run / download plan** — show what would be downloaded before committing
4. **NPZ/NPY parsing** — framework-agnostic NumPy array parsing for SAE weights

---

## Feature 1: Safetensors Remote Header Inspection

### Why

Safetensors files store a JSON header (tensor names, shapes, dtypes, byte offsets) in the first bytes of the file. Two small HTTP Range requests are enough to read it — no full download needed. No existing Rust CLI tool exposes this for remote HF repos.

### Scope

- **Library API:** `inspect_safetensors(repo_id, filename, config) -> SafetensorsHeader`
  - Fetches header size (first 8 bytes), then header JSON (second Range request)
  - Returns tensor names, shapes, dtypes, byte offsets, total parameter count
  - Works on both remote repos and locally cached files
- **CLI:** `hf-fm inspect <REPO_ID> [filename.safetensors]`
  - If no filename given, inspect all `*.safetensors` in the repo (metadata-only listing first)
  - Output: table of tensor names, shapes, dtypes, param count, total size

### Types

```rust
pub struct SafetensorsHeader {
    /// Tensor metadata entries, keyed by tensor name.
    pub tensors: IndexMap<String, TensorInfo>,
    /// Total parameter count across all tensors.
    pub total_params: u64,
}

pub struct TensorInfo {
    /// Tensor name (e.g., `"model.layers.5.self_attn.q_proj.weight"`).
    pub name: String,
    /// Element dtype (e.g., `"BF16"`, `"F32"`).
    pub dtype: String,
    /// Tensor shape (e.g., `[2304, 2048]`).
    pub shape: Vec<usize>,
    /// Byte offset range within the safetensors file `[start, end)`.
    pub data_offsets: (u64, u64),
}
```

### Dependencies

None beyond `reqwest` (already present) and `serde_json` (already present). The safetensors header is plain JSON — no new crate needed.

---

## Feature 2: Cache Management

### Why

Everyone's `~/.cache/huggingface/hub/` grows unbounded. There is no good CLI for verifying integrity, measuring usage, or pruning stale entries. The `status` subcommand (v0.5.0) shows per-repo file state but cannot act on it.

### Scope

Three new CLI subcommands (and corresponding library functions):

#### `hf-fm du [REPO_ID]`

Disk usage summary. Without arguments: all cached repos, sorted by size descending. With a repo: per-file breakdown.

```
$ hf-fm du
  5.1 GB  google/gemma-2-2b-it          (8 files)
  2.8 GB  EleutherAI/pythia-1.4b        (12 files)
  1.2 GB  google/gemma-scope-2b-pt-res  (3 files)
  ───────
  9.1 GB  total (3 repos, 23 files)
```

#### `hf-fm verify [REPO_ID]`

Re-verify SHA256 checksums of cached files against HF LFS metadata. Reports corrupted or incomplete files.

```
$ hf-fm verify google/gemma-2-2b-it
  ✓ model-00001-of-00002.safetensors  (2.5 GB, SHA256 OK)
  ✓ model-00002-of-00002.safetensors  (2.6 GB, SHA256 OK)
  ✓ config.json                       (1.2 KB, SHA256 OK)
  3/3 files verified
```

#### `hf-fm clean [--older-than <DAYS>] [--except <REPO_ID>]`

Prune cached repos. Defaults to interactive confirmation. `--older-than` filters by last access time. `--except` preserves specific repos.

```
$ hf-fm clean --older-than 30
  Will remove:
    EleutherAI/pythia-1.4b  (2.8 GB, last accessed 45 days ago)
  Keep:
    google/gemma-2-2b-it    (accessed 2 days ago)
  Proceed? [y/N]
```

### Library API

```rust
/// Per-repo disk usage entry.
pub struct CacheUsage {
    pub repo_id: String,
    pub total_bytes: u64,
    pub file_count: usize,
}

pub fn cache_disk_usage() -> Result<Vec<CacheUsage>, FetchError>;
pub fn verify_cache(repo_id: String) -> Result<Vec<FileVerification>, FetchError>;
pub fn clean_cache(older_than: Option<Duration>, except: &[String]) -> Result<CleanResult, FetchError>;
```

---

## Feature 3: Dry-Run / Download Plan

### Why

Before committing to a multi-GB download, users want to know: how many files, total size, what's already cached. More importantly, the plan has all the information needed to compute an *optimal* download configuration — turning the dry-run from a passive preview into an active optimization step. Downloading models is energy-intensive; anything that avoids wasted bandwidth and redundant connections matters.

### Scope

- **CLI flag:** `hf-fm download <REPO_ID> --dry-run`
- **Library API:** `download_plan(repo_id, config) -> DownloadPlan`
- **Plan-to-config pipeline:** `DownloadPlan::recommended_config() -> FetchConfig`

### Behavior

Fetches the file listing and metadata (sizes, checksums) but downloads nothing. Compares against local cache to classify each file. Then computes an optimal `FetchConfig` based on the file size distribution of the *remaining* (uncached) files.

```
$ hf-fm download google/gemma-2-2b-it --preset safetensors --dry-run
  Repo:     google/gemma-2-2b-it
  Revision: main (abc1234)
  Filter:   *.safetensors

  File                                   Size      Status
  model-00001-of-00002.safetensors       2.5 GB    cached ✓
  model-00002-of-00002.safetensors       2.6 GB    to download
  ──────────────────────────────────────────────────────────
  Total: 5.1 GB (2 files, 1 cached, 1 to download)
  Download: 2.6 GB

  Recommended config:
    concurrency:        1  (single large file remaining)
    connections/file:   8  (file exceeds chunk threshold)
    chunk threshold:  100 MiB
```

### Plan-to-config optimization strategy

`DownloadPlan::recommended_config()` analyzes the uncached files and tunes:

| Signal | Optimization | Rationale |
|--------|-------------|-----------|
| Few large files (e.g., 1-2 files > 1 GB) | Lower `concurrency`, maximize `connections_per_file` | Saturate bandwidth on fewer streams; avoid connection contention |
| Many small files (e.g., 20 files < 10 MB) | Higher `concurrency`, single connection per file | Chunked overhead exceeds benefit for small files; parallelize at file level instead |
| Mixed sizes | Moderate `concurrency`, chunked only for files above threshold | Balance both strategies |
| All files cached | No download needed | `recommended_config()` returns a config with an empty skip-list; `download_with_plan` is a no-op |
| Single very large file (> 5 GB) | Increase `connections_per_file` to 16 | More parallelism for very large transfers |

The user can always override: `plan.recommended_config()` returns a `FetchConfig` that can be further customized via the builder before passing to `download_with_plan`.

### Two-step API (plan then execute)

```rust
// Step 1: compute the plan (metadata only, no downloads)
let plan = hf_fetch_model::download_plan(repo_id.clone(), &config).await?;

// User inspects: plan.download_bytes, plan.files, etc.

// Step 2a: execute with recommended config
let optimized = plan.recommended_config();
let outcome = hf_fetch_model::download_with_plan(&plan, &optimized).await?;

// Step 2b: or override specific settings
let custom = plan.recommended_config_builder()
    .concurrency(2)       // user override
    .on_progress(|e| { /* ... */ })
    .build()?;
let outcome = hf_fetch_model::download_with_plan(&plan, &custom).await?;
```

### Implicit plan (zero-friction default)

When the user calls `download_with_config` directly (no explicit dry-run), the download pipeline internally computes a plan and applies the recommended config, merged with the user's explicit settings. User-specified values always win; the plan only fills in unset fields. This means **every download benefits from plan-based optimization**, even without `--dry-run`.

### Types

```rust
pub struct DownloadPlan {
    /// Repository identifier.
    pub repo_id: String,
    /// Resolved revision (commit hash).
    pub revision: String,
    /// Per-file plan entries.
    pub files: Vec<FilePlan>,
    /// Total bytes across all files.
    pub total_bytes: u64,
    /// Bytes already cached locally.
    pub cached_bytes: u64,
    /// Bytes that need downloading.
    pub download_bytes: u64,
}

pub struct FilePlan {
    /// Filename within the repository.
    pub filename: String,
    /// File size in bytes.
    pub size: u64,
    /// Whether the file is already cached locally.
    pub cached: bool,
}

impl DownloadPlan {
    /// Compute an optimized `FetchConfig` based on the size distribution
    /// of uncached files. Returns a ready-to-use config.
    pub fn recommended_config(&self) -> FetchConfig;

    /// Like `recommended_config`, but returns a builder so the caller
    /// can override specific fields before finalizing.
    pub fn recommended_config_builder(&self) -> FetchConfigBuilder;

    /// Number of files that need downloading.
    pub fn files_to_download(&self) -> usize;

    /// Whether all files are already cached (download would be a no-op).
    pub fn fully_cached(&self) -> bool;
}
```

---

## Feature 4: NPZ/NPY Download Integration

### Why

NPZ is the weight format for Gemma Scope SAEs. candle-mi currently has a tightly-coupled NPZ parser. The pure parsing logic now lives in **anamnesis** (Phase 3), which is the tensor format transformation crate. anamnesis wraps the `npyz` crate (mature NPY/NPZ parser, 4.6M downloads) and adds a bf16 interpretation layer. hf-fetch-model provides the download integration layer on top.

### Scope

Pure parsing API is provided by anamnesis (`parse_npz`, `NpzTensor`, `NpzDtype`). Full parsing design in [hf-fetch-npz-features-api.md](hf-fetch-npz-features-api.md). hf-fetch-model adds:

- **`download_and_parse_npz(repo_id, filename, config)`** — download + `anamnesis::parse_npz()` in one call
- **Feature-gated** behind `npz` feature (adds `anamnesis/npz` dependency)
- **CLI:** `hf-fm inspect-npz <REPO_ID> <filename>` — download + print array names, shapes, dtypes
- **Error variant:** `FetchError::NpzParse { reason }` (wraps `anamnesis::AnamnesisError`)

---

## Dogfooding Observations (anamnesis, 2026-03-21)

While building anamnesis (tensor format transformation crate), hf-fetch-model was used to discover and download FP8 test models. Observations:

1. **Missing `list-files` subcommand.** There is no way to see what files a remote repo contains without attempting a download. `hf-fm download-file` requires knowing the exact filename. A lightweight `hf-fm list-files <REPO_ID>` subcommand (shows filenames + sizes, no download) would fill this gap. `--dry-run` partially covers this but requires going through the download flow. Consider adding `list-files` as part of Feature 3 (dry-run), since it reuses the same metadata fetch path.

2. **Main command help is unclear.** `hf-fm <REPO_ID>` downloads a model, but the help text doesn't explain what gets downloaded (all files? safetensors only?). A one-liner clarification would help first-time users.

3. **`search --exact` works well.** Immediately returns model metadata (tags, license, pipeline). Clean and informative output.

4. **`download-file` help is clear.** Arguments and options are well documented.

---

## Implementation Order

| Order | Feature | Rationale |
|-------|---------|-----------|
| 1 | Dry-run + plan-to-config | Foundation: `DownloadPlan` becomes the central orchestration type. Once this exists, `download_with_config` itself benefits from plan-based optimization. Every subsequent feature builds on the metadata fetch path exercised here. |
| 2 | Cache management (`du`, `verify`, `clean`) | Builds on `cache.rs`; `verify` reuses `checksum.rs`; `du` reuses the same file-size metadata that `DownloadPlan` collects |
| 3 | Safetensors inspection | New capability (HTTP Range for headers); independent of download pipeline |
| 4 | NPZ download integration | Thin wrapper over `anamnesis::load_npz()`; depends on anamnesis Phase 3 |

## Version milestones

Only the final milestone is tagged and published to crates.io; earlier milestones are internal checkpoints that stay on the main branch without a tag or release.

| Version | Content |
|---------|---------|
| v0.8.0-alpha | Feature 3 (dry-run + plan-to-config pipeline); retrofit implicit plan into `download_with_config` |
| | *Internal checkpoint — no tag, no publish* |
| v0.8.0-beta | Feature 2 (cache management) + Feature 1 (safetensors inspection) |
| | *Internal checkpoint — no tag, no publish* |
| v0.8.0 | Feature 4 (NPZ download integration via anamnesis) + stabilize all |
| | **Release:** set `version = "0.8.0"` in Cargo.toml, commit, `git tag v0.8.0`, `git push origin v0.8.0` |
