@@ -42,6 +42,41 @@ impl RepoKind {
4242 RepoKind :: Model => "models--" ,
4343 }
4444 }
45+
46+ fn to_hub_type ( self ) -> hf_hub:: RepoType {
47+ match self {
48+ RepoKind :: Dataset => hf_hub:: RepoType :: Dataset ,
49+ RepoKind :: Model => hf_hub:: RepoType :: Model ,
50+ }
51+ }
52+ }
53+
54+ /// Order in which `larql pull` probes HF for an `hf://owner/name` path.
55+ /// `larql publish` defaults to `repo_type = "model"`, so model is tried
56+ /// first; dataset stays as the fallback for older vindexes that were
57+ /// uploaded before the publish default flipped (and for docs examples
58+ /// that pin `--repo-type dataset`).
59+ const HF_PULL_REPO_KINDS : [ RepoKind ; 2 ] = [ RepoKind :: Model , RepoKind :: Dataset ] ;
60+
61+ /// Build a typed `ApiRepo` handle for a given `(repo_id, revision, kind)`.
62+ /// Centralised so the three pull entry points share one constructor and
63+ /// the with/without-revision branching lives in one place.
64+ fn hf_repo (
65+ api : & hf_hub:: api:: sync:: Api ,
66+ repo_id : & str ,
67+ revision : Option < & str > ,
68+ kind : RepoKind ,
69+ ) -> hf_hub:: api:: sync:: ApiRepo {
70+ let repo_type = kind. to_hub_type ( ) ;
71+ if let Some ( rev) = revision {
72+ api. repo ( hf_hub:: Repo :: with_revision (
73+ repo_id. to_string ( ) ,
74+ repo_type,
75+ rev. to_string ( ) ,
76+ ) )
77+ } else {
78+ api. repo ( hf_hub:: Repo :: new ( repo_id. to_string ( ) , repo_type) )
79+ }
4580}
4681
4782/// Resolve an `hf://` path to a local directory, downloading if needed.
@@ -68,26 +103,32 @@ pub fn resolve_hf_vindex(hf_path: &str) -> Result<PathBuf, VindexError> {
68103 let api = hf_hub:: api:: sync:: Api :: new ( )
69104 . map_err ( |e| VindexError :: Parse ( format ! ( "HuggingFace API init failed: {e}" ) ) ) ?;
70105
71- let repo = if let Some ( ref rev) = revision {
72- api. repo ( hf_hub:: Repo :: with_revision (
73- repo_id. clone ( ) ,
74- hf_hub:: RepoType :: Dataset ,
75- rev. clone ( ) ,
76- ) )
77- } else {
78- api. repo ( hf_hub:: Repo :: new (
79- repo_id. clone ( ) ,
80- hf_hub:: RepoType :: Dataset ,
81- ) )
82- } ;
83-
84- // Download index.json first (small, tells us what we need)
85- let index_path = repo. get ( INDEX_JSON ) . map_err ( |e| {
86- VindexError :: Parse ( format ! (
87- "failed to download index.json from hf://{}: {e}" ,
88- repo_id
89- ) )
90- } ) ?;
106+ // `larql publish` defaults to model repos, but older vindexes and
107+ // some docs examples live as dataset repos. Probe in publish-default
108+ // order; the first kind that yields index.json wins, the rest are
109+ // skipped.
110+ let mut last_err: Option < String > = None ;
111+ let ( repo, index_path) = HF_PULL_REPO_KINDS
112+ . into_iter ( )
113+ . find_map ( |kind| {
114+ let repo = hf_repo ( & api, & repo_id, revision. as_deref ( ) , kind) ;
115+ match repo. get ( INDEX_JSON ) {
116+ Ok ( path) => Some ( ( repo, path) ) ,
117+ Err ( e) => {
118+ last_err = Some ( e. to_string ( ) ) ;
119+ None
120+ }
121+ }
122+ } )
123+ . ok_or_else ( || {
124+ let suffix = last_err
125+ . as_deref ( )
126+ . map ( |e| format ! ( ": {e}" ) )
127+ . unwrap_or_default ( ) ;
128+ VindexError :: Parse ( format ! (
129+ "failed to download index.json from hf://{repo_id}{suffix}"
130+ ) )
131+ } ) ?;
91132
92133 let vindex_dir = index_path
93134 . parent ( )
@@ -121,24 +162,24 @@ pub fn download_hf_weights(hf_path: &str) -> Result<(), VindexError> {
121162 let api = hf_hub:: api:: sync:: Api :: new ( )
122163 . map_err ( |e| VindexError :: Parse ( format ! ( "HuggingFace API init failed: {e}" ) ) ) ?;
123164
124- let repo = if let Some ( ref rev) = revision {
125- api. repo ( hf_hub:: Repo :: with_revision (
126- repo_id. clone ( ) ,
127- hf_hub:: RepoType :: Dataset ,
128- rev. clone ( ) ,
129- ) )
130- } else {
131- api. repo ( hf_hub:: Repo :: new (
132- repo_id. clone ( ) ,
133- hf_hub:: RepoType :: Dataset ,
134- ) )
135- } ;
136-
137- for filename in VINDEX_WEIGHT_FILES {
138- let _ = repo. get ( filename) ; // optional, skip if not in repo
165+ // Same model-first-then-dataset probe order as `resolve_hf_vindex`.
166+ // We use index.json as the "does this repo type exist?" probe so we
167+ // don't accidentally fetch weight files from a stale dataset repo
168+ // when the live vindex lives on the model side.
169+ for kind in HF_PULL_REPO_KINDS {
170+ let repo = hf_repo ( & api, & repo_id, revision. as_deref ( ) , kind) ;
171+ if repo. get ( INDEX_JSON ) . is_err ( ) {
172+ continue ;
173+ }
174+ for filename in VINDEX_WEIGHT_FILES {
175+ let _ = repo. get ( filename) ; // optional, skip if not in repo
176+ }
177+ return Ok ( ( ) ) ;
139178 }
140179
141- Ok ( ( ) )
180+ Err ( VindexError :: Parse ( format ! (
181+ "failed to fetch index.json from hf://{repo_id}"
182+ ) ) )
142183}
143184
144185/// Re-exported from hf-hub 0.5 so callers don't have to depend on
@@ -306,58 +347,57 @@ where
306347 let api = hf_hub:: api:: sync:: Api :: new ( )
307348 . map_err ( |e| VindexError :: Parse ( format ! ( "HuggingFace API init failed: {e}" ) ) ) ?;
308349
309- let repo = if let Some ( ref rev) = revision {
310- api. repo ( hf_hub:: Repo :: with_revision (
311- repo_id. clone ( ) ,
312- hf_hub:: RepoType :: Dataset ,
313- rev. clone ( ) ,
314- ) )
315- } else {
316- api. repo ( hf_hub:: Repo :: new (
317- repo_id. clone ( ) ,
318- hf_hub:: RepoType :: Dataset ,
319- ) )
320- } ;
321-
322- // Helper: one file, with cache short-circuit. Returns the resolved
323- // on-disk path. The cache check fires the progress reporter so the
324- // bar shows a filled-to-100% track tagged with the filename — users
325- // see that the file was served from cache, not re-downloaded.
326- let mut fetch = |filename : & str , label : & str | -> Option < PathBuf > {
327- if let Some ( ( cached_path, size) ) =
328- cached_snapshot_file ( RepoKind :: Dataset , & repo_id, revision. as_deref ( ) , filename)
329- {
330- // Tag the progress message so the bar visibly distinguishes
331- // "cached" from "just downloaded very fast". Callers rendering
332- // the bar see the prefix at init time and can restyle.
333- let mut p = progress ( label) ;
334- let tagged = format ! ( "{filename} [cached]" ) ;
335- p. init ( size as usize , & tagged) ;
336- p. update ( size as usize ) ;
337- p. finish ( ) ;
338- return Some ( cached_path) ;
339- }
340- repo. download_with_progress ( filename, progress ( label) ) . ok ( )
341- } ;
342-
343- // index.json drives everything — we need its snapshot dir to know
344- // where the rest of the files live. Cache-hit or download.
345- let index_path = fetch ( INDEX_JSON , INDEX_JSON ) . ok_or_else ( || {
346- VindexError :: Parse ( format ! ( "failed to fetch index.json from hf://{repo_id}" ) )
347- } ) ?;
348- let vindex_dir = index_path
349- . parent ( )
350- . ok_or_else ( || VindexError :: Parse ( "cannot determine vindex directory" . into ( ) ) ) ?
351- . to_path_buf ( ) ;
350+ // Probe each repo kind in publish-default order. The first kind that
351+ // returns index.json (cache hit or download) is the winner; we then
352+ // fetch the rest of `VINDEX_CORE_FILES` from that same handle.
353+ for kind in HF_PULL_REPO_KINDS {
354+ let repo = hf_repo ( & api, & repo_id, revision. as_deref ( ) , kind) ;
355+
356+ // Helper: one file, with cache short-circuit. Returns the resolved
357+ // on-disk path. The cache check fires the progress reporter so the
358+ // bar shows a filled-to-100% track tagged with the filename — users
359+ // see that the file was served from cache, not re-downloaded.
360+ let mut fetch = |filename : & str , label : & str | -> Option < PathBuf > {
361+ if let Some ( ( cached_path, size) ) =
362+ cached_snapshot_file ( kind, & repo_id, revision. as_deref ( ) , filename)
363+ {
364+ // Tag the progress message so the bar visibly distinguishes
365+ // "cached" from "just downloaded very fast". Callers rendering
366+ // the bar see the prefix at init time and can restyle.
367+ let mut p = progress ( label) ;
368+ let tagged = format ! ( "{filename} [cached]" ) ;
369+ p. init ( size as usize , & tagged) ;
370+ p. update ( size as usize ) ;
371+ p. finish ( ) ;
372+ return Some ( cached_path) ;
373+ }
374+ repo. download_with_progress ( filename, progress ( label) ) . ok ( )
375+ } ;
352376
353- for filename in VINDEX_CORE_FILES {
354- if * filename == INDEX_JSON {
377+ // index.json drives everything — we need its snapshot dir to know
378+ // where the rest of the files live. If this kind doesn't have it,
379+ // try the next kind.
380+ let Some ( index_path) = fetch ( INDEX_JSON , INDEX_JSON ) else {
355381 continue ;
382+ } ;
383+ let vindex_dir = index_path
384+ . parent ( )
385+ . ok_or_else ( || VindexError :: Parse ( "cannot determine vindex directory" . into ( ) ) ) ?
386+ . to_path_buf ( ) ;
387+
388+ for filename in VINDEX_CORE_FILES {
389+ if * filename == INDEX_JSON {
390+ continue ;
391+ }
392+ // Optional files — ignore failures (missing from repo is fine).
393+ let _ = fetch ( filename, filename) ;
356394 }
357- // Optional files — ignore failures (missing from repo is fine).
358- let _ = fetch ( filename, filename) ;
395+ return Ok ( vindex_dir) ;
359396 }
360- Ok ( vindex_dir)
397+
398+ Err ( VindexError :: Parse ( format ! (
399+ "failed to fetch index.json from hf://{repo_id}"
400+ ) ) )
361401}
362402
363403/// Resolve an `hf://` model repo path to a local snapshot directory,
@@ -571,18 +611,15 @@ mod tests {
571611
572612 #[ test]
573613 #[ serial]
574- fn resolve_hf_vindex_errors_on_404_index_json ( ) {
575- // mockito returns 404 for /datasets/owner/repo/resolve/main/index.json
576- // → repo.get(INDEX_JSON) errors → resolve_hf_vindex returns
577- // the wrapped "failed to download index.json" error. Exercises:
578- // hf:// strip, no-revision branch, Api::new(), repo.get error path .
614+ fn resolve_hf_vindex_errors_when_both_repo_kinds_404 ( ) {
615+ // mockito returns 404 for every URL → both Model and Dataset
616+ // probes fail in turn → resolve_hf_vindex returns the wrapped
617+ // "failed to download index.json" error. Exercises: hf:// strip,
618+ // no-revision branch, Api::new(), full HF_PULL_REPO_KINDS loop .
579619 let mut server = mockito:: Server :: new ( ) ;
580620 let _g = HfTestEnv :: new ( & server. url ( ) ) ;
581621 let _m = server
582- . mock (
583- "GET" ,
584- mockito:: Matcher :: Regex ( r"/datasets/owner/repo/resolve/.*/index\.json" . into ( ) ) ,
585- )
622+ . mock ( "GET" , mockito:: Matcher :: Any )
586623 . with_status ( 404 )
587624 . create ( ) ;
588625
@@ -597,14 +634,14 @@ mod tests {
597634 #[ serial]
598635 fn resolve_hf_vindex_errors_with_revision_pinned ( ) {
599636 // Same as above but with `@v2.0` revision. The split path takes
600- // a different `repo` constructor (with_revision) — verify both
601- // branches by exercising them with the same 404 mock.
637+ // a different `repo` constructor (with_revision) — verify the
638+ // revision-bearing branch with the same all- 404 mock.
602639 let mut server = mockito:: Server :: new ( ) ;
603640 let _g = HfTestEnv :: new ( & server. url ( ) ) ;
604641 let _m = server
605642 . mock (
606643 "GET" ,
607- mockito:: Matcher :: Regex ( r"/datasets/owner/repo/ resolve/v2\.0/index\.json" . into ( ) ) ,
644+ mockito:: Matcher :: Regex ( r"/resolve/v2\.0/index\.json" . into ( ) ) ,
608645 )
609646 . with_status ( 404 )
610647 . create ( ) ;
@@ -618,18 +655,23 @@ mod tests {
618655
619656 #[ test]
620657 #[ serial]
621- fn download_hf_weights_silently_skips_missing_files ( ) {
622- // download_hf_weights iterates VINDEX_WEIGHT_FILES with `let _ =
623- // repo.get(filename)` — every miss is silenced. Pin that contract:
624- // even when every file 404s, the function returns Ok(()).
658+ fn download_hf_weights_errors_when_no_repo_kind_has_index_json ( ) {
659+ // `download_hf_weights` now uses index.json as the "does this repo
660+ // type exist?" probe. When both Model and Dataset 404 on
661+ // index.json, the function returns the "failed to fetch
662+ // index.json" error rather than silently succeeding.
625663 let mut server = mockito:: Server :: new ( ) ;
626664 let _g = HfTestEnv :: new ( & server. url ( ) ) ;
627665 let _m = server
628666 . mock ( "GET" , mockito:: Matcher :: Any )
629667 . with_status ( 404 )
630668 . create ( ) ;
631669
632- download_hf_weights ( "hf://owner/repo" ) . expect ( "missing files are non-fatal" ) ;
670+ let err = download_hf_weights ( "hf://owner/repo" ) . expect_err ( "no index.json on either side" ) ;
671+ assert ! (
672+ err. to_string( ) . contains( "failed to fetch index.json" ) ,
673+ "got: {err}"
674+ ) ;
633675 }
634676
635677 #[ test]
0 commit comments