Skip to content

pull: add Hugging Face org/repo[:quant] downloads and serve --hf - #623

Open
timtoole02 wants to merge 1 commit into
mainfrom
feat/hf-pull
Open

pull: add Hugging Face org/repo[:quant] downloads and serve --hf#623
timtoole02 wants to merge 1 commit into
mainfrom
feat/hf-pull

Conversation

@timtoole02

Copy link
Copy Markdown
Owner

Summary

  • camelid pull org/repo[:quant] downloads any public Hugging Face repo that ships single-file, top-level GGUFs — the CLI twin of the Models page's "Experimental (Hugging Face)" browse lane. Any pull query containing / is a spec; curated ids never contain one, so existing pull behavior is untouched.
  • serve --hf and chat --hf download-if-missing, then load through the ordinary explicit-model path. camelid pull --dry-run resolves and prints the target without moving bytes.
  • Selection fails closed rather than guessing: multi-GGUF repos require an explicit :quant, mmproj companions and multi-part shards are unselectable, and a same-named file with different bytes is an error rather than an overwrite.
  • New module src/hf_pull.rs; discovery and download reuse the existing audited paths rather than adding a second downloader.

Why this change exists

Camelid could only pull the ~30 curated catalog rows from the terminal. Anything else required the web UI's Models page, so there was no one-line command to fetch and run an arbitrary GGUF.

This is also the prerequisite for listing Camelid in the Hugging Face Hub's "Use this model" dropdown (their Local Apps registry): that snippet has to be a single spec-addressable command, the way llama.cpp's llama serve -hf org/repo:Q4_K_M is. A separate PR to huggingface/huggingface.js will follow.

No support-contract change. This lane is experimental — unverified, no parity claim. A download path is not a support claim, and runnability is still decided at load time by the inspect-first typed-blocker flow, fail-closed. Recorded as DECISIONS.md D21.

Design notes

Reuse over reinvention. Discovery goes through hf_browse's LFS-aware tree fetch (extracted from repo_gguf_files as list_gguf_files_blocking). The download adopts the web installer's semantics: .part + rename promotion so a loadable GGUF never exists half-written, resume via curl -C -, retries, stall detection, and the download ceiling enforced before and during transfer.

Stricter than the web install path in two places: repo ids are gated by fit_dims::is_safe_hf_component (the /catalog/fit contract, which the install handler skips), and Windows reserved device stems are rejected. The latter is a local check for now — model_default::valid_local_model_filename is shared with the web lane and hardening it there is filed as separate follow-up work.

Quant labeling fix with blast radius beyond this feature. hf_browse::guess_quant was a plain substring matcher, so …-Q6_K_L.gguf was labeled Q6_K and …-Q4_0_8_8.gguf was labeled Q4_0. In the standard bartowski layout that made plain tags ambiguous, and in a repo shipping only a superstring variant it could have silently substituted a different quantization. It now matches on token boundaries and knows the _L/_XL/Q4_0_x_y variants. This also improves the advisory quant shown in the web UI's browse rows.

Env-var safety. --hf is CLI-only (no env alias) and deliberately not a clap conflicts_with = "model": because --model carries env = "CAMELID_MODEL", a hard conflict would make --hf unusable for anyone with that variable exported. A typed --hf simply wins at dispatch. No inherited variable can start a multi-gigabyte download on desktop app open.

Directory correctness. serve --hf downloads into the same directory the server scans (api::resolve_models_dir, now pub), not a CWD-relative ./models, so a pulled file always appears in the Models page in the shipped exe-dir layout. serve --hf also honors --max-download-bytes, not just the env var.

Validation

  • git diff --check
  • cargo fmt --all -- --check
  • cargo test --all-targets — 2041 passed, 0 failed (macOS; --all-features is omitted because it enables the cuda feature, which has no cudarc dep on this target — pre-existing, unrelated to this change)
  • cargo clippy --all-targets -- -D warnings
  • cd frontend && npm run build — not run; this change touches no frontend source
  • Other evidence attached below

Public-repo privacy check

  • No private hostnames, SSH commands, user home paths, key paths, local validation details, raw SSH stderr, or raw infrastructure failure output are included
  • Remote validation blockers are summarized generically — remote Linux x86_64 and Windows validation were unavailable during this cycle; no fresh same-host timing or parity claim is made
  • bash scripts/check-public-scrub.sh
  • node scripts/audit-evidence-bundle-privacy.mjs --strict

Support-contract check

  • This PR does not overclaim support
  • TinyLlama Q8_0 remains the current full-support gate; exact Llama 3.2 1B/3B and Llama 3 8B Q8_0 rows stay limited to their documented exact-row smoke envelopes — this PR adds no evidence and changes no row
  • No 1B / 3B / 8B wording changed; COMPATIBILITY.md, STATUS.md, and /api/capabilities are untouched

The user-facing wording for this lane is the web UI's, verbatim: "Experimental — unverified, no parity claim", followed by "A download path is not a support claim; whether this file loads is decided at load time, fail-closed." The post-download hint is hedged the same way ("loads only if the file's architecture is implemented; fails closed otherwise") rather than promising a working chat.

Evidence / artifacts

Live end-to-end run against unsloth/SmolLM2-135M-Instruct-GGUF:Q2_K (88,201,792 bytes) covering all four download paths:

  1. First download.part promoted by rename, exact byte count, no leftover partial.
  2. Re-pull — "already downloaded … (0.1 GB, size-verified)", no transfer.
  3. Resume — from a 40 MB truncated .part, completed to exactly 88,201,792 bytes.
  4. Stale partial — a 120 MB oversized .part is discarded and re-downloaded fresh rather than failing "re-run to resume" forever.

Resolution behavior against prism-ml/Ternary-Bonsai-27B-gguf (6 selectable GGUFs plus 2 mmproj companions): a bare spec lists the files with selectable tags and refuses to guess; :Q2_0 selects Q2_0 and not PQ2_0; :pq2_0 and hf.co/… URL prefixes both resolve; an unknown repo produces a clean error naming the anonymous-download limitation.

22 unit tests cover spec parsing, selection, the destination trichotomy, ceiling parsing, and the download pre-gates. One invariant test asserts that every tag the CLI prints in its listing actually selects that exact file — the listing is the tool's own suggestion surface, so a printed tag that errors would be a dead end.

The implementation was reviewed by a multi-agent adversarial pass across correctness, security, product-policy, and test-coverage lenses; 20 confirmed findings were fixed before this commit, including the two .part bugs, the quant-labeling collision, the CAMELID_MODEL conflict, the models-dir divergence, and a parser panic on a multi-byte character straddling a URL-prefix boundary.

Docs impact

  • README.md — quick start gains the serve --hf one-liner with the experimental-lane caveat; the camelid pull catalog section notes that a / query resolves as a Hugging Face spec.
  • docs/CONFIGURATION.md — download-ceiling section documents which lanes honor the flag versus the env var, and the anonymous/gated-repo limitation.
  • DECISIONS.md — new D21 recording the lane, its fail-closed selection rules, and what it explicitly does not decide (no sha256 pinning for arbitrary files, no multi-part or vision-companion support).
  • DOCS.md — decision-log index updated to D21.
  • COMPATIBILITY.md, STATUS.md, SUPPORT_MATRIX_v0.1.md, ROADMAP.md — unchanged, deliberately.

🤖 Generated with Claude Code

Camelid could only pull the ~30 curated catalog rows from the CLI; arbitrary
Hugging Face GGUFs were reachable only through the Models page's experimental
browse lane. This adds the CLI twin of that lane:

  camelid pull prism-ml/Ternary-Bonsai-27B-gguf:Q2_0
  camelid serve --hf prism-ml/Ternary-Bonsai-27B-gguf:Q2_0

Any pull query containing '/' is a Hugging Face spec (curated ids never contain
one); serve/chat --hf download-if-missing then load through the ordinary
explicit-model path. Prerequisite for a Hugging Face Local Apps entry, whose
"Use this model" snippet needs a one-line spec-addressable command.

Support contract unchanged: this lane is experimental — unverified, no parity
claim. A download path is not a support claim, and runnability is still decided
at load time by the inspect-first typed-blocker flow, fail-closed. See D21.

Reuse over reinvention: discovery goes through hf_browse's LFS-aware tree fetch
(extracted as list_gguf_files_blocking); the download adopts the web installer's
semantics (.part + rename promotion, resume, retries, stall detection, download
ceiling). Stricter than the web install path in two places: repo ids are gated
by fit_dims::is_safe_hf_component (the /catalog/fit contract, which install
skips) and Windows reserved device stems are rejected.

Selection fails closed rather than guessing: a multi-GGUF repo requires an
explicit :quant; recognized labels must match exactly (:Q4_K never resolves to
Q4_K_M) and unrecognized ones match on filename token boundaries (:F16 never
picks BF16); mmproj companions and multi-part shards are unselectable; a
same-named file with different bytes is an error, never an overwrite; and a
stale oversized .part is discarded rather than resumed forever.

guess_quant now matches on token boundaries and knows the _L/_XL/Q4_0_x_y
variants — as a substring matcher it labeled Q6_K_L as "Q6_K", which made plain
tags ambiguous in the standard bartowski layout and could silently substitute a
different quantization.

--hf is CLI-only (no env alias) and not a clap conflict with --model, so an
exported CAMELID_MODEL cannot make it unusable and no inherited variable can
start a download on desktop app open; a typed --hf wins at dispatch. serve --hf
honors --max-download-bytes and downloads into the same directory the server
scans (api::resolve_models_dir), so pulled files always appear in the Models
page.

Validation: cargo fmt --check, clippy --all-targets -D warnings, and
cargo test --all-targets (2041 passed, 0 failed) all green on macOS; live
end-to-end pull of unsloth/SmolLM2-135M-Instruct-GGUF:Q2_K exercised first
download, size-verified skip, resume from a truncated partial, and stale-partial
recovery. Docs updated in the same change: README quick start and pull-catalog
sections, docs/CONFIGURATION.md ceiling table, DECISIONS.md D21, DOCS.md index.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant