# hf-fetch-model: Fast HuggingFace Model Downloads for Rust

> An embeddable Rust library for downloading HuggingFace models with maximum throughput

**Date:** February 28, 2026
**Status:** Phase 4 complete (pre-publish)
**Context:** During the development of plip-rs and candle-mi, model downloads were a recurring bottleneck. No existing Rust crate provides a fast, ergonomic, embeddable library for downloading HuggingFace model repositories. hf-fetch-model fills this gap, and candle-mi will use it as its download backend.

---

## 1. The Gap

### 1.1 What Exists Today

| Crate / Tool | Type | Parallel Chunks | Download Repo | File Filtering | Progress | Embeddable |
|---|---|---|---|---|---|---|
| **hf-hub** v0.5.0 | Library | `.high()` mode | No (file-by-file) | No | No | Yes |
| **rust-hf-downloader** v1.4.0 | Binary (TUI/CLI) | Yes | Yes | Yes (TUI) | Yes (TUI) | **No** |
| **hf_transfer** | Python+Rust (PyO3) | Yes | No (single file) | No | No | **No** (Python only) |
| **xet-core / hf_xet** | Storage backend | Yes (dedup) | N/A | N/A | N/A | N/A |

**hf-hub** is the official HuggingFace Rust client. Its API is file-by-file only:

```rust
let repo = api.model("google/gemma-2-2b-it".to_string());
let path = repo.get("model-00001-of-00002.safetensors").unwrap();
```

The caller must know exact filenames, orchestrate multi-file downloads, and implement progress reporting. The `.high()` builder method enables parallel chunked downloads for individual files, but there is no repo-level orchestration.

**rust-hf-downloader** has the features we need (parallel downloads, progress, filtering) but is a binary-only application — its internals are private and it cannot be used as a library dependency.

**hf_transfer** is a Python package with a Rust core (PyO3). It accelerates single-file downloads but does not provide a Rust API.

**xet-core / hf_xet** is HuggingFace's next-generation storage backend with chunk-based deduplication. It operates at the infrastructure layer, not the user-facing download layer.

### 1.2 What Is Missing

No existing Rust crate provides all of the following as a **library API**:

1. **Repo-level download** — "give me this model, download all files"
2. **Maximum throughput by default** — parallel chunked downloads enabled out of the box
3. **Progress reporting** — per-file and overall, via callbacks for library consumers
4. **File filtering** — download only `*.safetensors`, or exclude `*.bin`, etc.
5. **Checksum verification** — validate downloaded files against HuggingFace metadata
6. **Embeddable** — usable as a Cargo dependency by crates like candle-mi

### 1.3 xet-core and the LFS-to-Xet Migration

[Xet](https://huggingface.co/docs/hub/en/xet/index) is HuggingFace's replacement for Git LFS as the Hub's storage backend. Unlike Git LFS (which stores entire file revisions), Xet uses [content-defined chunking](https://huggingface.co/docs/xet/en/deduplication) to deduplicate data at the chunk level — yielding ~50% storage savings and 2–3x faster transfers in [benchmarks](https://huggingface.co/blog/from-chunks-to-blocks).

**Migration timeline:**
- **Jan 2025:** Xet deployed, handling ~6% of Hub downloads
- **May 2025:** became the default for all new users and organizations
- **Jul 2025:** rolling migration of existing repos from LFS to Xet
- **No LFS deprecation date announced** — backward compatibility is maintained

[xet-core](https://github.com/huggingface/xet-core) is the Rust workspace implementing the chunking/deduplication client. It is consumed internally by `huggingface_hub` (Python) via `hf_xet` (PyO3 bindings). It is explicitly **not meant to be used directly** — it is infrastructure plumbing.

**Implications for hf-fetch-model:** The migration is transparent. The `hf-hub` Rust crate continues to work through HuggingFace's [Git LFS Bridge](https://huggingface.co/blog/migrating-the-hub-to-xet), which reconstructs files from Xet storage on-demand and returns presigned S3 URLs mimicking the LFS protocol. If/when `hf-hub` gains native Xet support, hf-fetch-model inherits it for free — this validates the "wrap `hf-hub`" strategy rather than threatening it.

### 1.4 Value Proposition

hf-fetch-model wraps hf-hub (the official client, with cache compatibility and auth support) and adds the repo-level orchestration layer that is missing. The hard problem — parallel chunked HTTP — is already solved by hf-hub's `.high()` mode. hf-fetch-model provides the ergonomic layer above it.

---

## 2. Architecture

```
┌─────────────────────────────────────┐
│           candle-mi                 │
│   download_model() convenience fn  │
└──────────────┬──────────────────────┘
               │ optional dep (feature = "fast-download")
┌──────────────▼──────────────────────┐
│        hf-fetch-model               │
│  • repo file listing               │
│  • file filtering (glob patterns)  │
│  • parallel file orchestration     │
│  • progress callbacks              │
│  • checksum verification           │
│  • resume / retry                  │
└──────────────┬──────────────────────┘
               │ dep
┌──────────────▼──────────────────────┐
│     hf-hub (tokio, .high())        │
│  • HTTP chunked parallel download  │
│  • HF cache layout compatibility   │
│  • auth token handling             │
└─────────────────────────────────────┘
```

### 2.1 Key Design Decisions

| Decision | Choice | Rationale |
|---|---|---|
| Build from scratch vs. wrap hf-hub | **Wrap hf-hub** | Reuse official HTTP layer, cache layout, and auth; avoid reimplementing chunked HTTP |
| Async vs. sync API | **Async-first (tokio)**; optional sync wrapper (Phase 1) | `.high()` requires tokio; sync wrapper via `tokio::runtime::Runtime::block_on` for simple use cases |
| Separate crate vs. in candle-mi | **Separate crate** | Useful beyond MI; keeps candle-mi focused on interpretability |
| Progress reporting | **`ProgressEvent` struct + closure callback** on `FetchConfig`; optional `indicatif` feature | Library consumers choose their UI; CLI gets progress bars for free |
| Error handling | **thiserror** enum | Consistent with candle-mi conventions; no panics in library code |

### 2.2 Target API (Sketch)

```rust
use hf_fetch_model::{FetchConfig, ProgressEvent};

// Minimal usage — download a model with defaults (Phase 0)
// Grit Rule 6: async functions take owned types
let path = hf_fetch_model::download("google/gemma-2-2b-it".to_owned()).await?;

// Full control (Phase 1+)
let config = FetchConfig::builder()
    .revision("main")
    .filter("*.safetensors")
    .token_from_env()            // reads HF_TOKEN
    .concurrency(4)              // parallel file downloads
    .on_progress(|e: &ProgressEvent| {
        println!("{}: {:.1}%", e.filename, e.percent);
    })
    .build()?;

let path = hf_fetch_model::download_with_config("google/gemma-2-2b-it".to_owned(), &config).await?;
```

---

## 3. Coding Conventions (Grit)

hf-fetch-model follows [Grit — Strict Rust for AI-Assisted Development](https://github.com/PCfVW/Amphigraphic-Strict/tree/master/Grit), the same foundation used by candle-mi. Grit is a strict subset of Rust that makes implicit behaviors explicit, targeting patterns where AI-generated code commonly fails.

### 3.1 Grit Rules (all 12 apply)

| Rule | Name | Enforcement | Key constraint |
|------|------|-------------|----------------|
| 1 | Explicit lifetimes | `#![deny(elided_lifetimes_in_paths)]` | All public function signatures must have explicit lifetimes |
| 2 | Explicit conversions | `#![warn(clippy::as_conversions)]` | No implicit Deref coercion; use `.as_str()`, `.as_bytes()`, `.to_owned()` |
| 3 | No panic in libraries | `#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::indexing_slicing)]` | Use `Result` + `?`; never `.unwrap()`, `.expect()`, or `panic!()` |
| 4 | No type erasure | Code review | No `Box<dyn Any>`; prefer generics. `// TRAIT_OBJECT:` annotation when dynamic dispatch is genuinely needed |
| 5 | Unsafe isolation | `#![forbid(unsafe_code)]` | No unsafe code. hf-fetch-model wraps safe APIs only |
| 6 | Owned types in async | Code review | Async functions take owned types (`String`, not `&str`); eliminates async lifetime complexity |
| 7 | Exhaustive matching | `#![deny(clippy::wildcard_enum_match_arm)]` | All enum variants matched explicitly; no `_` catch-all without documentation |
| 8 | Standard error pattern | `thiserror` | `FetchError` uses `thiserror`; `#[from]` for wrapped errors; `# Errors` doc section on all fallible functions |
| 9 | Prefer iterators | `#![warn(clippy::explicit_iter_loop, clippy::manual_filter_map)]` | Iterator chains over imperative loops for transformations |
| 10 | Single async runtime | Code review | Tokio only; no async-std or smol |
| 11 | `#[non_exhaustive]` | Code review | Public enums that may gain variants (`FetchError`, `Filter`) must be `#[non_exhaustive]` |
| 12 | `#[must_use]` | `#![warn(clippy::must_use_candidate)]` | All public functions/methods returning a value with no side effects must be `#[must_use]` |

**Rules from candle-mi that do NOT apply** (tensor/MI-specific):
- Shape documentation — no tensors
- Hook purity contract — no hooks
- PROMOTE / CONTIGUOUS annotations — tensor-specific

### 3.3 Annotation Patterns

Every annotation is mandatory when the corresponding situation applies.

| Annotation | When required | Example |
|------------|---------------|---------|
| `// TRAIT_OBJECT: <reason>` | Every `Box<dyn Trait>` or `&dyn Trait` usage | `// TRAIT_OBJECT: heterogeneous progress handlers` |
| `// EXHAUSTIVE: <reason>` | On `#[allow(clippy::exhaustive_enums)]` | `// EXHAUSTIVE: internal dispatch enum; crate owns all variants` |
| `// EXPLICIT: <reason>` | Intentional no-op match arm, or imperative loop over iterator chain | `// EXPLICIT: no action needed for this variant` |
| `// BORROW: <what>` | Explicit `.as_str()`, `.as_bytes()`, `.to_owned()` conversions | `// BORROW: explicit .as_str() instead of Deref coercion` |
| `// SAFETY: <invariants>` | Every `unsafe` block (not expected: `#![forbid(unsafe_code)]`) | N/A |

### 3.4 Cargo.toml Lint Configuration

```toml
[lints.rust]
unsafe_code = "forbid"
elided_lifetimes_in_paths = "deny"

[lints.clippy]
# Rule 3: No panics
unwrap_used = "deny"
expect_used = "deny"
panic = "deny"
indexing_slicing = "deny"

# Rule 7: Exhaustive matching
wildcard_enum_match_arm = "deny"

# Rule 2: Explicit conversions
as_conversions = "warn"

# Rule 9: Prefer iterators
explicit_iter_loop = "warn"
manual_filter_map = "warn"
manual_find_map = "warn"
needless_range_loop = "warn"

# Rule 12: #[must_use]
must_use_candidate = "warn"

# Additional strictness
pedantic = { level = "warn", priority = -1 }
missing_errors_doc = "warn"
missing_panics_doc = "warn"

# Allow noisy pedantic lints
module_name_repetitions = "allow"
too_many_lines = "allow"
```

### 3.5 CI Enforcement

Every push and PR triggers CI checks that must all pass before merging. This is set up in Phase 0 alongside the first code.

**CI pipeline (`ci.yml`) runs:**

1. `cargo fmt --check` — formatting must match `rustfmt` defaults; no local style drift
2. `cargo clippy --all-targets -- -D warnings` — all Grit lints from §3.4 are enforced; warnings are errors
3. `cargo clippy --all-targets --all-features -- -D warnings` — verify CLI code compiles cleanly (added Phase 4)
4. `cargo test` — all tests must pass

**Publish pipeline (`publish.yml`)** runs fmt, clippy, clippy --all-features, test, and `cargo doc --all-features --no-deps` inline before publishing to crates.io (added Phase 4).

### 3.6 SPDX Headers

Every `.rs` source file must begin with an SPDX license identifier:

```rust
// SPDX-License-Identifier: MIT OR Apache-2.0
```

---

## 4. Roadmap

### Phase 0 — Minimal Viable Download → `v0.1.0`

**Goal:** A working library crate that downloads an entire model repo faster than naive hf-hub usage.

**Deliverables:**
- [x] Initialize crate: `Cargo.toml` (with Grit lint configuration from §3.4), `src/lib.rs`, `#![forbid(unsafe_code)]`, SPDX headers
- [x] Set up CI: `ci.yml` enforcing `cargo fmt --check`, `cargo clippy -- -D warnings`, `cargo test` on every push/PR (see §3.5)
- [x] Set up `publish.yml` with `workflow_dispatch` for manual re-runs from the GitHub Actions dashboard
- [x] Depend on `hf-hub` v0.5 with `tokio` feature, using `ApiBuilder::new().high()`
- [x] Implement repo file listing via HuggingFace API (`repo::list_repo_files()` using `info().siblings`)
- [x] Implement `download(repo_id: String) -> Result<PathBuf>`: download all files, return cache directory
- [x] Respect HF cache layout (`~/.cache/huggingface/hub/`) for compatibility with Python tooling
- [x] Auth: `HF_TOKEN` environment variable, delegated to hf-hub's `ApiBuilder`
- [x] Error type: `FetchError` enum with `thiserror` v2 (`#[non_exhaustive]`, variants: `Api`, `Io`, `RepoNotFound`, `Auth`)
- [x] Basic integration test: download `julien-c/dummy-unknown`, verify cache path exists

**Exit criteria:** `hf_fetch_model::download("some/small-model".to_owned()).await?` returns a valid cache path with all files present. ✅ Met.

### Phase 1 — Progress & Filtering → `v0.2.0`

**Goal:** Users can see download progress and select which files to download.

**Deliverables:**
- [x] `FetchConfig` builder with `revision`, `token`, `filter` (glob), `exclude` (glob), `concurrency`
- [x] `ProgressEvent` struct and `on_progress` closure callback on `FetchConfig`
- [x] `ProgressEvent` fields: `filename`, `bytes_downloaded`, `bytes_total`, `percent`, `files_remaining`
- [x] Optional `indicatif` feature gate: `IndicatifProgress` multi-progress bar (per-file + overall)
- [x] `download_with_config()` public API
- [x] Common filter presets: `Filter::safetensors()`, `Filter::gguf()`, `Filter::config_only()`
- [x] Optional sync wrapper: `download_blocking()` and `download_with_config_blocking()` for non-async callers

**Exit criteria:** Downloading a multi-shard model shows per-file progress bars and respects `.filter("*.safetensors")`. ✅ Met.

### Phase 2 — Reliability → `v0.3.0`

**Goal:** Downloads succeed on flaky connections and files are verified.

**Deliverables:**
- [x] App-level resume on restart: hf-hub's `.get()` skips already-cached files automatically; cross-session resume is inherited from hf-hub's cache layer
- [x] Retry with exponential backoff + jitter (base 300ms, cap 10s, max 3 retries) — `retry.rs`
- [x] SHA256 checksum verification against `HuggingFace` LFS metadata via direct REST API (`repo::list_repo_files_with_metadata()`) — `checksum.rs`
- [x] Timeout configuration (per-file and overall) — `FetchConfig::timeout_per_file()`, `FetchConfig::timeout_total()`
- [x] Structured error reporting: `FetchError::PartialDownload` with `Vec<FileFailure>` (filename, reason, retryable flag)

**Exit criteria:** Killing the process mid-download and restarting completes without re-downloading finished files. Corrupted files are detected and re-fetched. ✅ Met.

### Phase 3 — candle-mi Integration (no hf-fetch-model release)

**Goal:** candle-mi users can download models with a single function call.

**Deliverables:**
- [x] Add `hf-fetch-model` as optional dependency of candle-mi: `features = ["fast-download"]`
- [x] `candle_mi::download_model(repo_id)` convenience function (async + blocking)
- [x] Wire progress reporting to `tracing` (candle-mi's logging layer)
- [x] Update candle-mi examples to use `download_model()` where appropriate
- [x] Documentation: how to use fast downloads in candle-mi

**Exit criteria:** `candle_mi::download_model("julien-c/dummy-unknown".to_owned()).await?` downloads and returns the path, with progress visible via `tracing`. (Uses the project's lightweight test repo; real models like Pythia are exercised in Phase 4.) ✅ Met.

### Phase 4 — CLI & Publish → `v0.4.0`

**Goal:** Standalone CLI tool and crates.io publication.

**Deliverables:**
- [x] CLI binaries (thin wrapper): `hf-fetch-model EleutherAI/pythia-1.4b --preset safetensors` (also available as `hf-fm`)
- [x] `--revision`, `--token`, `--filter`, `--exclude`, `--preset`, `--output-dir`, `--concurrency` flags
- [x] `--preset` maps to `Filter` presets: `safetensors`, `gguf`, `config-only`
- [x] `hf-fetch-model list-families` subcommand: scan local HF cache (`config.json` files), print cached `model_type` values with repo names
- [x] `hf-fetch-model discover` subcommand: query HF Hub API (`GET /api/models?config=true&sort=downloads&direction=-1`), collect `model_type` values from top models, diff against local cache, print families not yet cached (see Appendix B)
- [x] Benchmarks: `examples/bench.rs` timing comparison vs. plain hf-hub sequential download
- [x] Pre-publish audit: all Grit rules (§3) satisfied, CI green, `cargo doc --all-features --no-deps` clean
- [x] README with usage examples, API docs, architecture diagram, "Used by" section
- [x] Repo ID validation: org/model format check with helpful error message
- [x] `FetchError::InvalidArgument` variant for CLI validation errors
- [x] `FetchConfig::output_dir()` builder method plumbed via hf-hub's `with_cache_dir()`
- [x] `cache` module: `hf_cache_dir()`, `list_cached_families()`
- [x] `discover` module: `discover_new_families()` with HF Hub API pagination
- [x] Progress bar: overall bar correctly derives total from `files_remaining`
- [x] Publish workflow gates on CI checks (fmt, clippy, test, doc) before publishing
- [ ] Publish to crates.io (requires `CARGO_REGISTRY_TOKEN` secret in GitHub)

**Exit criteria:** `hf-fm EleutherAI/pythia-1.4b --preset safetensors` downloads weights + config only; `hf-fm list-families` lists cached architectures with header/separator; `hf-fm discover` shows new families available on the Hub. ✅ Met (except crates.io publish).

---

## 5. Project Structure

```
hf-fetch-model/
├── Cargo.toml                  # Single crate; features, Grit lints (§3.4)                [Phase 0] ✓
├── Cargo.lock                  # Pinned dependency versions                               [Phase 0] ✓
├── CONVENTIONS.md              # Grit rules and annotation patterns (extracted from §3)   [Phase 0] ✓
├── LICENSE-MIT                 # Dual license (MIT / Apache-2.0)                          [Phase 0] ✓
├── LICENSE-APACHE              #                                                          [Phase 0] ✓
├── README.md                   # Title, badges, one-liner                                 [Phase 0] ✓
├── CHANGELOG.md                # Keep a Changelog format                                  [Phase 0] ✓
├── hf-fetch-model-roadmap.md   # This file                                                [Phase 0] ✓
├── .github/
│   └── workflows/
│       ├── ci.yml              # cargo fmt, clippy, test on push/PR                       [Phase 0] ✓
│       └── publish.yml         # crates.io publish on tag + workflow_dispatch              [Phase 0] ✓
├── src/
│   ├── lib.rs                  # Public API: download(), download_with_config(), etc.      [Phase 1] ✓
│   ├── error.rs                # FetchError enum (9 variants, thiserror) + FileFailure    [Phase 4] ✓
│   ├── repo.rs                 # Repo file listing + extended metadata via HF REST API    [Phase 2] ✓
│   ├── download.rs             # Orchestration: filter, progress, retry, checksum, timeout [Phase 2] ✓
│   ├── config.rs               # FetchConfig builder, Filter presets, output_dir           [Phase 4] ✓
│   ├── progress.rs             # ProgressEvent struct, indicatif implementation           [Phase 4] ✓
│   ├── checksum.rs             # SHA256 verification against HF LFS metadata              [Phase 2] ✓
│   ├── retry.rs                # Exponential backoff + jitter logic                       [Phase 2] ✓
│   ├── cache.rs                # HF cache resolution + local model family scanning        [Phase 4] ✓
│   └── discover.rs             # Model family discovery via HF Hub API                    [Phase 4] ✓
├── src/bin/
│   └── main.rs                 # CLI binary (clap-based)                                  [Phase 4] ✓
│                               # Installed as both `hf-fetch-model` and `hf-fm`
├── tests/                         # All network tests use julien-c/dummy-unknown (tiny public repo)
│   ├── integration.rs          # Download julien-c/dummy-unknown, verify cache path       [Phase 0] ✓
│   ├── filter.rs               # Glob filtering, progress callback, presets tests         [Phase 1] ✓
│   └── reliability.rs          # Checksum, retry, timeout, structured error tests         [Phase 2] ✓
├── examples/
│   ├── basic.rs                # Minimal download example                                 [Phase 4] ✓
│   ├── progress.rs             # Download with indicatif progress bars                    [Phase 4] ✓
│   └── bench.rs                # Timing comparison vs. plain hf-hub sequential download   [Phase 4] ✓
└── benches/
    └── throughput.rs            # Benchmark placeholder (points to examples/bench.rs)      [Phase 4] ✓
```

`cargo install hf-fetch-model` will install two binaries from the same source (added in Phase 4):

```toml
# Cargo.toml (Phase 4 additions)
[[bin]]
name = "hf-fetch-model"        # explicit, discoverable
path = "src/bin/main.rs"

[[bin]]
name = "hf-fm"                 # short alias for daily use
path = "src/bin/main.rs"
```

### Module Responsibilities

| Module | Phase | Status | Responsibility |
|---|---|---|---|
| `lib.rs` | 0–4 | ✓ Phase 4 | `download()`, `download_with_config()`, `download_files()`, `download_files_with_config()`, blocking wrappers; re-exports; registers `cache` and `discover` modules |
| `error.rs` | 0–4 | ✓ Phase 4 | `FetchError` enum (9 variants: `Api`, `Io`, `RepoNotFound`, `Auth`, `InvalidPattern`, `Checksum`, `Timeout`, `PartialDownload`, `Http`, `InvalidArgument`); `FileFailure` struct |
| `repo.rs` | 0–2 | ✓ Phase 2 | `list_repo_files()` via hf-hub; `list_repo_files_with_metadata()` via HF REST API (sizes, SHA256) |
| `download.rs` | 0–2 | ✓ Phase 2 | `download_all_files()`, `download_all_files_map()`: orchestrate with filtering, progress, retry, checksum, timeout, structured errors |
| `config.rs` | 1–4 | ✓ Phase 4 | `FetchConfig` builder: revision, filters, token, `on_progress`, concurrency, `output_dir`, `timeout_per_file`, `timeout_total`, `max_retries`, `verify_checksums`; `Filter` presets |
| `progress.rs` | 1–4 | ✓ Phase 4 | `ProgressEvent` struct; optional `IndicatifProgress` with auto-derived total count; `completed_event()` helper |
| `checksum.rs` | 2 | ✓ | `verify_sha256()`: compute SHA256 on blocking thread, compare against expected hash |
| `retry.rs` | 2–4 | ✓ Phase 4 | `retry_async()`: exponential backoff + jitter; `is_retryable()` classification; `RetryPolicy` config |
| `cache.rs` | 4 | ✓ | `hf_cache_dir()`: HF cache resolution; `list_cached_families()`: scan models, group by `model_type` |
| `discover.rs` | 4 | ✓ | `discover_new_families()`: paginate HF Hub API, diff against local cache, return new families |
| `bin/main.rs` | 4 | ✓ | CLI: `hf-fm <repo> [--filter] [--preset] [--revision] [--token] [--output-dir] [--concurrency]`; subcommands: `list-families`, `discover` |

---

## 6. Dependencies

| Crate | Version | Purpose | Phase | Status |
|---|---|---|---|---|
| `hf-hub` (tokio feature) | 0.5 | HTTP downloads, cache, auth | 0 | ✓ |
| `tokio` | 1 | Async runtime | 0 | ✓ |
| `thiserror` | 2 | Error types | 0 | ✓ |
| `globset` | 0.4 | File filtering | 1 | ✓ |
| `indicatif` (optional) | 0.17 | Progress bars | 1 | ✓ |
| `sha2` | 0.10 | Checksum verification | 2 | ✓ |
| `reqwest` (json feature) | 0.12 | Direct HF API calls for metadata + discover | 2 | ✓ |
| `serde` (derive feature) | 1 | JSON deserialization | 2 | ✓ |
| `serde_json` | 1 | Parsing `config.json` for model family scanning | 4 | ✓ |
| `dirs` | 6 | Cross-platform home directory resolution | 4 | ✓ |
| `clap` (derive, optional) | 4 | CLI argument parsing (behind `cli` feature) | 4 | ✓ |
| `tracing` | — | Structured logging (candle-mi integration) | 3 | Via candle-mi |

---

## 7. What hf-fetch-model Does NOT Do

- **Upload** — Out of scope. Download only.
- **Replace hf-hub** — hf-fetch-model depends on hf-hub; it is a layer above, not a replacement.
- **Compete with xet-core** — xet-core is a storage backend; hf-fetch-model is a user-facing download orchestrator. They operate at different layers (see §1.3).
- **Model inference** — No tensor loading, no forward pass. That is candle-mi's job.
- **Reimplementing chunked HTTP** — hf-hub's `.high()` already does this well.

---

## 8. Risks

| Risk | Mitigation |
|---|---|
| hf-hub API instability (maintainers warn of changes) | Pin to known-good version; isolate hf-hub behind internal adapter trait |
| HuggingFace moves entirely to xet backend | hf-fetch-model wraps hf-hub, which will adapt via the LFS Bridge (see §1.3); monitor xet-core for a Rust library API |
| `.high()` performance insufficient | Benchmark early (Phase 0); if needed, fall back to direct reqwest with Range headers |
| HF API rate limiting on file listing | Cache repo metadata; respect rate limit headers |

---

## Appendix A. Candidate Models for Download Testing

The Phase 4 CLI test uses **EleutherAI/pythia-1.4b** — a model from the Pythia suite, purpose-built for mechanistic interpretability research. It introduces a new architecture family (GPT-NeoX) not already present in the local HuggingFace cache, and is small enough (~2.8 GB BF16) to run with ample room on an RTX 5060 Ti (16 GB).

### Primary test model

| Model | Family | Params | BF16 size | Why |
|---|---|---|---|---|
| `EleutherAI/pythia-1.4b` | GPT-NeoX | 1.4B | ~2.8 GB | Gold standard for MI research; training checkpoints at every step; parallel attn+MLP layout is a new axis for candle-mi |

### Future candidates

These models represent additional architecture families worth testing for fast download benchmarks and candle-mi integration:

| Model | Family | Params | BF16 size | Why |
|---|---|---|---|---|
| `allenai/OLMo-2-0425-1B` | OLMo 2 | 1B | ~2 GB | Fully open (weights, data, code, checkpoints); ideal for MI reproducibility; LLaMA-like but with QK-norm |
| `tiiuae/falcon-rw-1b` | Falcon | 1B | ~2 GB | Multi-query attention (single KV head); architecturally distinct from all cached models |
| `state-spaces/mamba-1.4b` | Mamba (SSM) | 1.4B | ~2.8 GB | State Space Model — no attention; completely different paradigm; would require new candle-mi backend |

---

## Appendix B. Model Family Discovery via HF Hub API

The `list-families` and `discover` subcommands (Phase 4) rely on the HuggingFace Hub REST API exposing `model_type` metadata without requiring a full model download.

### API capabilities

| Capability | Endpoint | Notes |
|---|---|---|
| Get `model_type` for a single model | `GET /api/models/{repo_id}` | Returns `config.model_type` (e.g., `"gpt_neox"`, `"llama"`) |
| Get `model_type` in bulk | `GET /api/models?config=true&limit=100` | `config` field included in listing responses |
| Filter by architecture | `GET /api/models?filter=gpt_neox&limit=3` | Architecture tags in `tags[]` match `model_type` |
| Sort by popularity | `sort=downloads&direction=-1` | Ensures top models are scanned first |
| List all unique `model_type` values | **Not available** | No dedicated endpoint; requires scanning |

### `list-families` implementation

1. Scan local HF cache directory (`~/.cache/huggingface/hub/models--*/snapshots/*/config.json`)
2. Parse each `config.json`, extract `model_type` field
3. Group by `model_type`, list repo names under each family

```
$ hf-fm list-families
Family          Models
--------------------------------------------------------------------------------
gemma           google/codegemma-7b-it
gemma2          google/gemma-2-2b
gpt_neox        EleutherAI/pythia-1.4b
llama           codellama/CodeLlama-7b-hf, meta-llama/Llama-3.2-1B
phi3            microsoft/Phi-3-mini-4k-instruct
qwen2           Qwen/Qwen2.5-Coder-3B-Instruct, Qwen/Qwen2.5-Coder-7B-Instruct
starcoder2      bigcode/starcoder2-3b
```

### `discover` implementation

1. Run `list-families` to get the set of locally cached `model_type` values
2. Query `GET /api/models?config=true&sort=downloads&direction=-1&limit=100` (paginate up to ~500 models)
3. Collect all distinct `config.model_type` values from responses
4. Diff: remote families minus local families → "new" families
5. For each new family, show the most-downloaded representative model

```
$ hf-fm discover
New families not in local cache (top models by downloads):

Family          Top Model
--------------------------------------------------------------------------------
bart            facebook/bart-large-mnli
bert            sentence-transformers/all-MiniLM-L6-v2
gpt2            openai-community/gpt2
whisper         openai/whisper-large-v3
...
```

### Constraints

- **No auth required** for public model metadata
- **Rate limits apply** — paginating 500 models requires ~5 API calls; well within limits
- **Not all models have `config.json`** — GGUF-only repos or custom frameworks may lack `model_type`; these are skipped
