tensorfs 0.3.22__tar.gz → 0.3.23__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. {tensorfs-0.3.22 → tensorfs-0.3.23}/Cargo.lock +2 -2
  2. {tensorfs-0.3.22 → tensorfs-0.3.23}/Cargo.toml +1 -1
  3. {tensorfs-0.3.22 → tensorfs-0.3.23}/PKG-INFO +1 -1
  4. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/src/providers.rs +117 -8
  5. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/src/source_artifact.rs +1 -1
  6. {tensorfs-0.3.22 → tensorfs-0.3.23}/pyproject.toml +1 -1
  7. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/Cargo.toml +0 -0
  8. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/build.rs +0 -0
  9. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/examples/h3-swiglu-layout.md +0 -0
  10. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/examples/optional-cache-backfill.md +0 -0
  11. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/examples/resident-conversion.md +0 -0
  12. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/examples/resident_conversion_profile.rs +0 -0
  13. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/src/b64.rs +0 -0
  14. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/src/bin/tfs/ckpt.rs +0 -0
  15. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/src/bin/tfs/conform.rs +0 -0
  16. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/src/bin/tfs/fetch.rs +0 -0
  17. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/src/bin/tfs/ingest.rs +0 -0
  18. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/src/bin/tfs/main.rs +0 -0
  19. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/src/bin/tfs/rd.rs +0 -0
  20. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/src/bin/tfs/repo.rs +0 -0
  21. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/src/bin/tfs/snap.rs +0 -0
  22. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/src/bin/tfs/watch.rs +0 -0
  23. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/src/canon.rs +0 -0
  24. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/src/capability.rs +0 -0
  25. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/src/catalog.rs +0 -0
  26. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/src/cbor.rs +0 -0
  27. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/src/checkpoint.rs +0 -0
  28. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/src/corpus.rs +0 -0
  29. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/src/derived/adoption.rs +0 -0
  30. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/src/derived/graft_tests.rs +0 -0
  31. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/src/derived/progress.rs +0 -0
  32. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/src/derived/resume_tests.rs +0 -0
  33. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/src/derived/retention.rs +0 -0
  34. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/src/derived/roots.rs +0 -0
  35. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/src/derived.rs +0 -0
  36. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/src/dtype.rs +0 -0
  37. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/src/durability.rs +0 -0
  38. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/src/err.rs +0 -0
  39. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/src/fetch.rs +0 -0
  40. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/src/fill.rs +0 -0
  41. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/src/fit.rs +0 -0
  42. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/src/gc.rs +0 -0
  43. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/src/header.rs +0 -0
  44. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/src/home.rs +0 -0
  45. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/src/ids.rs +0 -0
  46. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/src/ingest/carrier.rs +0 -0
  47. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/src/ingest/convert.rs +0 -0
  48. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/src/ingest/fingerprint.rs +0 -0
  49. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/src/ingest/gguf.rs +0 -0
  50. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/src/ingest/journal.rs +0 -0
  51. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/src/ingest/mod.rs +0 -0
  52. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/src/ingest/preflight.rs +0 -0
  53. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/src/ingest/source.rs +0 -0
  54. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/src/ingest/stamp.rs +0 -0
  55. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/src/ingest/transaction.rs +0 -0
  56. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/src/jcs.rs +0 -0
  57. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/src/lib.rs +0 -0
  58. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/src/limits.rs +0 -0
  59. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/src/machine.rs +0 -0
  60. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/src/manifest.rs +0 -0
  61. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/src/meta.rs +0 -0
  62. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/src/project.rs +0 -0
  63. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/src/read.rs +0 -0
  64. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/src/receipt.rs +0 -0
  65. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/src/reclaim.rs +0 -0
  66. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/src/registry.rs +0 -0
  67. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/src/relation.rs +0 -0
  68. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/src/repo_cache.rs +0 -0
  69. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/src/repository.rs +0 -0
  70. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/src/sha256.rs +0 -0
  71. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/src/spec.rs +0 -0
  72. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/src/staging.rs +0 -0
  73. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/src/storage.rs +0 -0
  74. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/src/store.rs +0 -0
  75. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/src/transport/api.rs +0 -0
  76. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/src/transport/http.rs +0 -0
  77. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/src/transport/hub.rs +0 -0
  78. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/src/transport/ledger.rs +0 -0
  79. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/src/transport/mod.rs +0 -0
  80. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/src/transport/policy.rs +0 -0
  81. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/src/transport/prefix.rs +0 -0
  82. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/src/transport/pull.rs +0 -0
  83. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/src/transport/push.rs +0 -0
  84. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/src/transport/ranged.rs +0 -0
  85. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/src/transport/tests.rs +0 -0
  86. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/src/vectors.rs +0 -0
  87. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/tests/fetch_url_progress.rs +0 -0
  88. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/tests/fit.rs +0 -0
  89. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/tests/fixtures/repo_apply/blobs.jsonl +0 -0
  90. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_list.jsonl +0 -0
  91. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_repository.json +0 -0
  92. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_rows.jsonl +0 -0
  93. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/tests/fixtures/repo_apply/header.cbor +0 -0
  94. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/tests/fixtures/repo_apply/manifest.json +0 -0
  95. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/tests/fixtures/repo_apply/mutation.json +0 -0
  96. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/tests/h3_swiglu.rs +0 -0
  97. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/tests/home.rs +0 -0
  98. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/tests/liveness.rs +0 -0
  99. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/tests/read_plan.rs +0 -0
  100. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/tests/reclaim.rs +0 -0
  101. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/tests/reclaim_run.rs +0 -0
  102. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/tests/repo_cache.rs +0 -0
  103. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/tests/repo_store.rs +0 -0
  104. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/tests/resident_admission.rs +0 -0
  105. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/tests/sha256_backends.rs +0 -0
  106. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/tests/sharded_cas.rs +0 -0
  107. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/tests/source_artifacts.rs +0 -0
  108. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/tests/source_plan.rs +0 -0
  109. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/tests/source_prefixes.rs +0 -0
  110. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/tests/source_preflight.rs +0 -0
  111. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/tests/staging.rs +0 -0
  112. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/tests/store_boundaries.rs +0 -0
  113. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/tests/tensor_schema.rs +0 -0
  114. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/tests/version.rs +0 -0
  115. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-core/tests/write_through.rs +0 -0
  116. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-py/Cargo.toml +0 -0
  117. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-py/src/docs.rs +0 -0
  118. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-py/src/errors.rs +0 -0
  119. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-py/src/lib.rs +0 -0
  120. {tensorfs-0.3.22 → tensorfs-0.3.23}/crates/tensorfs-py/src/store.rs +0 -0
  121. {tensorfs-0.3.22 → tensorfs-0.3.23}/python/tensorfs/__init__.py +0 -0
  122. {tensorfs-0.3.22 → tensorfs-0.3.23}/python/tensorfs/_ext.pyi +0 -0
  123. {tensorfs-0.3.22 → tensorfs-0.3.23}/python/tensorfs/errors.py +0 -0
  124. {tensorfs-0.3.22 → tensorfs-0.3.23}/python/tensorfs/py.typed +0 -0
@@ -392,7 +392,7 @@ checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
392
392
 
393
393
  [[package]]
394
394
  name = "tensorfs-core"
395
- version = "0.3.22"
395
+ version = "0.3.23"
396
396
  dependencies = [
397
397
  "fs2",
398
398
  "minicbor",
@@ -404,7 +404,7 @@ dependencies = [
404
404
 
405
405
  [[package]]
406
406
  name = "tensorfs-py"
407
- version = "0.3.22"
407
+ version = "0.3.23"
408
408
  dependencies = [
409
409
  "pyo3",
410
410
  "tensorfs-core",
@@ -3,5 +3,5 @@ resolver = "2"
3
3
  members = ["crates/tensorfs-core", "crates/tensorfs-py"]
4
4
 
5
5
  [workspace.package]
6
- version = "0.3.22"
6
+ version = "0.3.23"
7
7
  edition = "2021"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tensorfs
3
- Version: 0.3.22
3
+ Version: 0.3.23
4
4
  Classifier: Programming Language :: Python :: 3
5
5
  Classifier: Programming Language :: Python :: Implementation :: CPython
6
6
  Classifier: Programming Language :: Rust
@@ -392,12 +392,18 @@ fn list_observed(
392
392
  format!("member name {name:?} is outside the grammar"),
393
393
  );
394
394
  }
395
+ let file_id = json_u64(file, "id")
396
+ .filter(|id| *id > 0 && *id < (1u64 << 53))
397
+ .ok_or_else(|| Refusal {
398
+ code: Code::KEY_GRAMMAR,
399
+ detail: "a Civitai file requires a positive exact integer id".into(),
400
+ })?;
395
401
  let sha256 = json_get(file, "hashes")
396
402
  .and_then(|hashes| json_str(hashes, "SHA256"))
397
403
  .and_then(digest64);
398
404
  let url = match json_str(file, "downloadUrl") {
399
405
  Some(explicit) => explicit.to_string(),
400
- None => format!("{base}/api/download/models/{version}"),
406
+ None => format!("{base}/api/download/models/{version}?fileId={file_id}"),
401
407
  };
402
408
  // A Civitai version is a bag of files (weights, a VAE, a config); the
403
409
  // one to take is the `primary` SafeTensors file, exactly as the Go
@@ -413,7 +419,9 @@ fn list_observed(
413
419
  // sizeKB is kilobytes as a float — an approximation, not a length.
414
420
  // The exact length is probed from the origin at pull time.
415
421
  members.push(SourceMember {
416
- member: name.to_string(),
422
+ // Display filenames may change; the provider file ID is the reviewed
423
+ // profile member identity, shared with Creator and retained source trees.
424
+ member: format!("civitai/files/{file_id}"),
417
425
  length: None,
418
426
  sha256,
419
427
  url,
@@ -1040,6 +1048,14 @@ fn is_safetensors(member: &str) -> bool {
1040
1048
  lower_ends_with(member, ".safetensors")
1041
1049
  }
1042
1050
 
1051
+ /// Civitai carrier members have provider IDs rather than filename extensions.
1052
+ pub(crate) fn is_civitai_member(member: &str) -> bool {
1053
+ member.strip_prefix("civitai/files/").is_some_and(|id| {
1054
+ id.parse::<u64>()
1055
+ .is_ok_and(|value| value > 0 && value < (1u64 << 53) && value.to_string() == id)
1056
+ })
1057
+ }
1058
+
1043
1059
  /// Resolve `relative` against the directory of `member`, the way the Go resolver's
1044
1060
  /// `path.Clean(path.Join(path.Dir(member), shard))` does. `None` when the reference walks
1045
1061
  /// above the repository root — an index may not name its way out of its own tree.
@@ -1970,12 +1986,16 @@ mod tests {
1970
1986
  /// so the exact length comes from a ranged probe through the same fence.
1971
1987
  #[test]
1972
1988
  fn resolve_takes_the_one_primary_civitai_safetensors() {
1973
- let body: Vec<u8> = (0u32..9_000).flat_map(|i| i.to_le_bytes()).collect();
1989
+ let header =
1990
+ br#"{"model.diffusion_model.weight":{"dtype":"F32","shape":[1],"data_offsets":[0,4]}}"#;
1991
+ let mut body = (header.len() as u64).to_le_bytes().to_vec();
1992
+ body.extend_from_slice(header);
1993
+ body.extend_from_slice(&1f32.to_le_bytes());
1974
1994
  let object = ObjectRef::of(&body);
1975
1995
  let listing = format!(
1976
1996
  r#"{{"id":777,"files":[
1977
- {{"name":"model.safetensors","primary":true,"sizeKB":35.15,"hashes":{{"SHA256":"{}"}},"metadata":{{"format":"SafeTensor"}}}},
1978
- {{"name":"extra.safetensors","primary":false,"sizeKB":1.0,"hashes":{{"SHA256":"{}"}}}}
1997
+ {{"id":92696,"name":"model.safetensors","primary":true,"sizeKB":35.15,"hashes":{{"SHA256":"{}"}},"metadata":{{"format":"SafeTensor"}}}},
1998
+ {{"id":92697,"name":"extra.safetensors","primary":false,"sizeKB":1.0,"hashes":{{"SHA256":"{}"}}}}
1979
1999
  ]}}"#,
1980
2000
  object.sha256.to_ascii_uppercase(),
1981
2001
  "11".repeat(32)
@@ -1999,12 +2019,101 @@ mod tests {
1999
2019
  };
2000
2020
  let resolved = resolve(&uri, &endpoints, &Anonymous, Deadline::none()).unwrap();
2001
2021
  assert_eq!(resolved.members.len(), 1);
2002
- assert_eq!(resolved.members[0].member, "model.safetensors");
2022
+ assert_eq!(resolved.members[0].member, "civitai/files/92696");
2003
2023
  assert_eq!(resolved.members[0].object, object);
2004
2024
  assert_eq!(resolved.members[0].provenance, Provenance::Declared);
2005
2025
  assert!(resolved.members[0].carrier);
2006
2026
  assert_eq!(resolved.canonical, "civitai://777");
2007
2027
  handle.join().unwrap();
2028
+
2029
+ // The real source tree must keep the carrier visible to conversion despite its
2030
+ // lack of a filename extension, under the member the reviewed profile selects.
2031
+ let root = temporary("civitai-member");
2032
+ let store = Store::init(&root).unwrap();
2033
+ store
2034
+ .put_stream(&mut body.as_slice(), Some(&object), &Fault::default())
2035
+ .unwrap();
2036
+ let tree = content_manifest(&resolved).unwrap();
2037
+ let owner = ObjectRef::of(b"civitai-member-owner").id();
2038
+ crate::source_artifact::create(&store, &owner, &tree).unwrap();
2039
+ let (_, roster, landed) =
2040
+ crate::source_artifact::conversion_roster(&store, &owner).unwrap();
2041
+ assert_eq!(roster.len(), 1);
2042
+ assert_eq!(roster[0].member, "civitai/files/92696");
2043
+ assert_eq!(roster[0].header, body[..8 + header.len()]);
2044
+ assert_eq!(landed, vec!["civitai/files/92696"]);
2045
+ let registry = crate::ingest::fingerprint::FingerprintRegistry::parse(
2046
+ crate::ingest::source::BUILTIN_REGISTRY_BYTES,
2047
+ )
2048
+ .unwrap();
2049
+ let profile = registry
2050
+ .source_profiles
2051
+ .iter()
2052
+ .find(|profile| profile.name == "civitai/101055/128078/single-file-fp16")
2053
+ .unwrap();
2054
+ assert!(
2055
+ profile
2056
+ .components
2057
+ .iter()
2058
+ .all(|component| component.source_member.as_deref()
2059
+ == Some(roster[0].member.as_str()))
2060
+ );
2061
+ std::fs::remove_dir_all(root).unwrap();
2062
+ }
2063
+
2064
+ #[test]
2065
+ fn civitai_refuses_missing_invalid_and_duplicate_file_ids() {
2066
+ for id in ["null", "0", "-1", "1.5", "9007199254740992", "\"92696\""] {
2067
+ let listing = format!(r#"{{"files":[{{"id":{id},"name":"model.safetensors"}}]}}"#);
2068
+ let (base, _, handle) =
2069
+ origin(1, move |_| ok_body(listing.as_bytes(), "application/json"));
2070
+ let endpoints = Endpoints {
2071
+ civitai: base,
2072
+ allow_local: true,
2073
+ ..Endpoints::default()
2074
+ };
2075
+ let refusal = list(
2076
+ &SourceUri::Civitai { version: 777 },
2077
+ &endpoints,
2078
+ &Anonymous,
2079
+ Deadline::none(),
2080
+ )
2081
+ .unwrap_err();
2082
+ let expected = if id == "9007199254740992" {
2083
+ Code::NUMBER_RANGE // The shared JSON decoder refuses before provider parsing.
2084
+ } else {
2085
+ Code::KEY_GRAMMAR
2086
+ };
2087
+ assert_eq!(refusal.code, expected, "{id}");
2088
+ handle.join().unwrap();
2089
+ }
2090
+ for (listing, code) in [
2091
+ (
2092
+ r#"{"files":[{"name":"missing-id.safetensors"}]}"#,
2093
+ Code::KEY_GRAMMAR,
2094
+ ),
2095
+ (
2096
+ r#"{"files":[{"id":92696,"name":"a.safetensors"},{"id":92696,"name":"b.safetensors"}]}"#,
2097
+ Code::DUPLICATE_KEY,
2098
+ ),
2099
+ ] {
2100
+ let (base, _, handle) =
2101
+ origin(1, move |_| ok_body(listing.as_bytes(), "application/json"));
2102
+ let endpoints = Endpoints {
2103
+ civitai: base,
2104
+ allow_local: true,
2105
+ ..Endpoints::default()
2106
+ };
2107
+ let refusal = list(
2108
+ &SourceUri::Civitai { version: 777 },
2109
+ &endpoints,
2110
+ &Anonymous,
2111
+ Deadline::none(),
2112
+ )
2113
+ .unwrap_err();
2114
+ assert_eq!(refusal.code, code);
2115
+ handle.join().unwrap();
2116
+ }
2008
2117
  }
2009
2118
 
2010
2119
  /// The credential is scoped to the host that holds it. A relative redirect stays on
@@ -2065,7 +2174,7 @@ mod tests {
2065
2174
  // No downloadUrl in the fixture: the handler must fall back to the provider's
2066
2175
  // canonical download route under the configured base.
2067
2176
  let listing = format!(
2068
- r#"{{"id":777,"files":[{{"name":"model.safetensors","sizeKB":273.4,"hashes":{{"SHA256":"{}"}}}}]}}"#,
2177
+ r#"{{"id":777,"files":[{{"id":92696,"name":"model.safetensors","sizeKB":273.4,"hashes":{{"SHA256":"{}"}}}}]}}"#,
2069
2178
  object.sha256.to_ascii_uppercase()
2070
2179
  );
2071
2180
  let payload = body.clone();
@@ -2074,7 +2183,7 @@ mod tests {
2074
2183
  let (base, _, handle) = origin(5, move |request| {
2075
2184
  if request.path == "/api/v1/model-versions/777" {
2076
2185
  ok_body(listing.as_bytes(), "application/json")
2077
- } else if request.path == "/api/download/models/777" {
2186
+ } else if request.path == "/api/download/models/777?fileId=92696" {
2078
2187
  if request.range.as_deref() == Some("bytes=0-0") {
2079
2188
  format!(
2080
2189
  "HTTP/1.1 206 Partial Content\r\nContent-Length: 1\r\nContent-Range: bytes 0-0/{length}\r\nETag: \"tag\"\r\nConnection: close\r\n\r\n\0"
@@ -459,7 +459,7 @@ pub fn conversion_roster(
459
459
  .read_to_end(&mut bytes)
460
460
  .map_err(io)?;
461
461
  bytes
462
- } else if member.ends_with(".safetensors") {
462
+ } else if member.ends_with(".safetensors") || crate::providers::is_civitai_member(member) {
463
463
  let mut prefix = [0u8; 8];
464
464
  file.read_exact(&mut prefix).map_err(io)?;
465
465
  let length = u64::from_le_bytes(prefix);
@@ -4,7 +4,7 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "tensorfs"
7
- version = "0.3.22"
7
+ version = "0.3.23"
8
8
  description = "TensorFS — canonical tensor artifact substrate"
9
9
  requires-python = ">=3.12"
10
10
  classifiers = [