1use crate::error::{CliError, Result};
4use colored::Colorize;
5use pacha::fetcher::{FetchConfig, ModelFetcher};
6use pacha::format::ModelFormat;
7use serde::{Deserialize, Serialize};
8use std::collections::{HashMap, HashSet};
9use std::io::{self, Read, Write};
10use std::path::Path;
11
12#[derive(Debug)]
17pub(crate) enum ResolvedModel {
18 SingleFile(String),
20 Sharded {
22 org: String,
23 repo: String,
24 shard_files: Vec<String>,
25 },
26}
27
28#[derive(Debug, Serialize, Deserialize)]
33pub struct ShardManifest {
34 pub version: u32,
35 pub repo: String,
36 pub files: HashMap<String, FileChecksum>,
37}
38
39#[derive(Debug, Serialize, Deserialize)]
41pub struct FileChecksum {
42 pub size: u64,
43 pub blake3: String,
44}
45
46#[provable_contracts_macros::contract(
48 "apr-cli-operations-v1",
49 equation = "mutating_output_contract"
50)]
51pub fn run(
52 model_ref: &str,
53 force: bool,
54 dry_run: bool,
55 revision: Option<&str>,
56 offline: bool,
57 json: bool,
58) -> Result<()> {
59 contract_pre_pull_cache_integrity!();
60
61 if dry_run {
74 return run_dry_run(model_ref, revision, offline, json);
75 }
76
77 println!("{}", "=== APR Pull ===".cyan().bold());
78 println!();
79
80 let _offline_scope = super::offline::scope(offline);
95
96 let resolved = resolve_hf_model(model_ref)?;
98
99 let result = match resolved {
100 ResolvedModel::SingleFile(ref uri) => run_single_file(uri, force),
101 ResolvedModel::Sharded {
102 ref org,
103 ref repo,
104 ref shard_files,
105 } => run_sharded(org, repo, shard_files, force),
106 };
107 if let Ok(ref r) = result {
108 contract_post_pull_cache_integrity!(r);
109 }
110 result
111}
112
113fn run_single_file(model_ref: &str, force: bool) -> Result<()> {
119 println!("Model: {}", model_ref.cyan());
120
121 if model_ref.starts_with("hf://") {
123 return run_single_file_streaming(model_ref, force);
124 }
125
126 let mut fetcher = ModelFetcher::with_config(FetchConfig::default()).map_err(|e| {
127 CliError::ValidationFailed(format!("Failed to initialize model fetcher: {e}"))
128 })?;
129
130 if !force && fetcher.is_cached(model_ref) {
131 return handle_cached_model(&mut fetcher, model_ref);
132 }
133
134 let result = download_single_model(&mut fetcher, model_ref)?;
135 ensure_safetensors_companions(&result)?;
136 print_pull_usage(&result.path, true);
137 Ok(())
138}
139
140fn run_single_file_streaming(model_ref: &str, force: bool) -> Result<()> {
147 let (org, repo, filename) = parse_hf_single_uri(model_ref)?;
148 let url = format!("https://huggingface.co/{org}/{repo}/resolve/main/{filename}");
149
150 let cache_dir = get_pacha_cache_dir()?;
151 std::fs::create_dir_all(&cache_dir)?;
152 let (extension, cache_path) = build_single_cache_path(&cache_dir, model_ref, &filename);
153
154 if !force && cache_path.exists() {
155 return report_cached_single(&cache_path);
156 }
157
158 stream_and_post_process(&url, &cache_path, model_ref, &extension)?;
159 print_pull_usage(&cache_path, true);
160 Ok(())
161}
162
163fn stream_and_post_process(
164 url: &str,
165 cache_path: &std::path::Path,
166 model_ref: &str,
167 extension: &str,
168) -> Result<()> {
169 println!();
170 println!("{}", "Downloading (streaming)...".yellow());
171 let checksum = download_file_with_progress(url, cache_path)?;
172 report_downloaded_single(cache_path, &checksum);
173
174 if extension == "safetensors" {
175 fetch_safetensors_companions(cache_path, model_ref)?;
176 convert_safetensors_formats(cache_path)?;
177 }
178 Ok(())
179}
180
181fn parse_hf_single_uri(model_ref: &str) -> Result<(String, String, String)> {
182 let path = model_ref.strip_prefix("hf://").unwrap_or(model_ref);
183 let parts: Vec<&str> = path.split('/').collect();
184 if parts.len() < 3 {
185 return Err(CliError::ValidationFailed(format!(
186 "HuggingFace URI must include a filename: {model_ref}"
187 )));
188 }
189 Ok((
190 parts[0].to_string(),
191 parts[1].to_string(),
192 parts[2..].join("/"),
193 ))
194}
195
196pub(crate) fn build_single_cache_path(
197 cache_dir: &std::path::Path,
198 model_ref: &str,
199 filename: &str,
200) -> (String, std::path::PathBuf) {
201 let uri_hash = blake3::hash(model_ref.as_bytes()).to_hex().to_string();
202 let extension = std::path::Path::new(filename)
203 .extension()
204 .and_then(|e| e.to_str())
205 .unwrap_or("bin")
206 .to_string();
207 let cache_filename = format!("{}.{extension}", &uri_hash[..16]);
208 let cache_path = cache_dir.join(&cache_filename);
209 (extension, cache_path)
210}
211
212fn report_cached_single(cache_path: &std::path::Path) -> Result<()> {
213 let metadata = std::fs::metadata(cache_path)?;
214 println!("{} Model already cached", "✓".green());
215 println!(" Path: {}", cache_path.display());
216 println!(" Size: {}", format_bytes(metadata.len()));
217 print_pull_usage(cache_path, true);
218 Ok(())
219}
220
221fn report_downloaded_single(cache_path: &std::path::Path, checksum: &FileChecksum) {
222 println!();
223 println!("{} Downloaded successfully", "✓".green());
224 println!(" Path: {}", cache_path.display().to_string().green());
225 println!(" Size: {}", format_bytes(checksum.size).yellow());
226 println!(" Hash: {}", &checksum.blake3[..16]);
227}
228
229pub(crate) fn get_pacha_cache_dir() -> Result<std::path::PathBuf> {
231 if let Ok(cache_home) = std::env::var("XDG_CACHE_HOME") {
232 return Ok(std::path::PathBuf::from(cache_home)
233 .join("pacha")
234 .join("models"));
235 }
236 Ok(dirs::home_dir()
237 .ok_or_else(|| CliError::ValidationFailed("Cannot find home directory".to_string()))?
238 .join(".cache")
239 .join("pacha")
240 .join("models"))
241}
242
243fn handle_cached_model(fetcher: &mut ModelFetcher, model_ref: &str) -> Result<()> {
245 println!("{} Model already cached", "✓".green());
246 let result = fetcher
247 .pull_quiet(model_ref)
248 .map_err(|e| CliError::ValidationFailed(format!("Failed to get cached model: {e}")))?;
249
250 println!(" Path: {}", result.path.display());
251 println!(" Size: {}", result.size_human());
252 println!(" Format: {}", result.format.name());
253
254 ensure_safetensors_companions(&result)?;
255 print_pull_usage(&result.path, false);
256 Ok(())
257}
258
259fn download_single_model(
261 fetcher: &mut ModelFetcher,
262 model_ref: &str,
263) -> Result<pacha::fetcher::FetchResult> {
264 super::offline::guard(&format!("download {model_ref}"))?;
268 println!();
269 println!("{}", "Downloading...".yellow());
270
271 let result = fetcher
272 .pull(model_ref, |progress| {
273 let pct = progress.percent();
274 print!(
275 "\r [{:50}] {:5.1}% ({}/{})",
276 "=".repeat((pct / 2.0) as usize),
277 pct,
278 format_bytes(progress.downloaded_bytes),
279 format_bytes(progress.total_bytes)
280 );
281 io::stdout().flush().ok();
282 })
283 .map_err(|e| CliError::NetworkError(format!("Download failed: {e}")))?;
284
285 println!();
286 println!();
287
288 if result.cache_hit {
289 println!("{} Model retrieved from cache", "✓".green());
290 } else {
291 println!("{} Downloaded successfully", "✓".green());
292 }
293
294 println!(" Path: {}", result.path.display().to_string().green());
295 println!(" Size: {}", result.size_human().yellow());
296 println!(" Format: {}", result.format.name());
297 println!(" Hash: {}", &result.hash[..16]);
298 Ok(result)
299}
300
301fn ensure_safetensors_companions(result: &pacha::fetcher::FetchResult) -> Result<()> {
303 if matches!(result.format, ModelFormat::SafeTensors(_)) {
304 fetch_safetensors_companions(&result.path, &result.resolved_uri)?;
305 convert_safetensors_formats(&result.path)?;
306 }
307 Ok(())
308}
309
310fn print_pull_usage(path: &Path, show_serve: bool) {
312 println!();
313 println!("{}", "Usage:".cyan().bold());
314 println!(" apr run {}", path.display());
315 if show_serve {
316 println!(" apr serve {}", path.display());
317 }
318}
319
320fn run_sharded(org: &str, repo: &str, shard_files: &[String], force: bool) -> Result<()> {
322 println!(
323 "Model: {}/{} ({} shards)",
324 org.cyan(),
325 repo.cyan(),
326 shard_files.len().to_string().yellow()
327 );
328
329 let cache_dir = resolve_shard_cache_dir(org, repo)?;
330 std::fs::create_dir_all(&cache_dir)?;
331
332 let base_url = format!("https://huggingface.co/{org}/{repo}/resolve/main");
333
334 if shard_files
338 .iter()
339 .all(|f| f.to_lowercase().ends_with(".gguf"))
340 {
341 return run_sharded_gguf(org, repo, &cache_dir, &base_url, shard_files, force);
342 }
343
344 let index_path = cache_dir.join("model.safetensors.index.json");
345
346 download_index_if_needed(&base_url, &index_path, force)?;
347
348 let manifest_path = cache_dir.join(".apr-manifest.json");
349 let existing_manifest = load_existing_manifest(&manifest_path, force);
350
351 let file_checksums = download_all_shards(
352 &cache_dir,
353 &base_url,
354 shard_files,
355 force,
356 existing_manifest.as_ref(),
357 )?;
358
359 download_companion_files(&cache_dir, &base_url, force)?;
360 write_shard_manifest(&manifest_path, org, repo, file_checksums)?;
361
362 println!();
363 println!("{} Downloaded successfully", "✓".green());
364 println!(" Path: {}", index_path.display().to_string().green());
365 println!(" Shards: {}", shard_files.len().to_string().yellow());
366
367 convert_safetensors_formats(&index_path)?;
368
369 println!();
370 println!("{}", "Usage:".cyan().bold());
371 println!(" apr run {}", index_path.display());
372 println!(" apr serve {}", index_path.display());
373 Ok(())
374}
375
376fn run_sharded_gguf(
381 org: &str,
382 repo: &str,
383 cache_dir: &Path,
384 base_url: &str,
385 shard_files: &[String],
386 force: bool,
387) -> Result<()> {
388 let manifest_path = cache_dir.join(".apr-manifest.json");
389 let existing_manifest = load_existing_manifest(&manifest_path, force);
390
391 let file_checksums = download_all_shards(
392 cache_dir,
393 base_url,
394 shard_files,
395 force,
396 existing_manifest.as_ref(),
397 )?;
398
399 download_companion_files(cache_dir, base_url, force)?;
400 write_shard_manifest(&manifest_path, org, repo, file_checksums)?;
401
402 println!();
403 println!(
404 "{} Downloaded {} GGUF shards",
405 "✓".green(),
406 shard_files.len().to_string().yellow()
407 );
408
409 let part_paths: Vec<std::path::PathBuf> =
413 shard_files.iter().map(|f| cache_dir.join(f)).collect();
414 let merged_path = cache_dir.join("model.gguf");
415 match aprender::format::gguf::merge_gguf_shards(&part_paths, &merged_path) {
416 Ok(()) => {
417 for part in &part_paths {
420 if let Err(e) = std::fs::remove_file(part) {
421 eprintln!(
422 " {} could not remove shard {} ({e})",
423 "!".yellow(),
424 part.display()
425 );
426 }
427 }
428 println!(
429 " {} merged {} parts → model.gguf",
430 "✓".green(),
431 shard_files.len().to_string().yellow()
432 );
433 println!(" Path: {}", merged_path.display().to_string().green());
434 println!();
435 println!("{}", "Usage:".cyan().bold());
436 println!(" apr run {}", merged_path.display());
437 println!(" apr serve {}", merged_path.display());
438 }
439 Err(e) => {
440 eprintln!(" {} could not assemble the sharded model: {e}", "✗".red());
443 eprintln!(
444 " The {} parts were downloaded to {} but cannot be run \
445 individually. Please file an issue (#1893) with the model name.",
446 shard_files.len(),
447 cache_dir.display()
448 );
449 return Err(CliError::ValidationFailed(format!(
450 "sharded GGUF merge failed: {e}"
451 )));
452 }
453 }
454 Ok(())
455}
456
457pub(crate) fn resolve_cache_dir_for_ref(model_ref: &str) -> Result<std::path::PathBuf> {
463 let as_path = std::path::Path::new(model_ref);
465 if as_path.is_dir() {
466 return Ok(as_path.to_path_buf());
467 }
468 let trimmed = model_ref
469 .trim_start_matches("hf://")
470 .trim_start_matches("https://huggingface.co/")
471 .trim_matches('/');
472 let mut parts = trimmed.splitn(2, '/');
473 match (parts.next(), parts.next()) {
474 (Some(org), Some(repo)) if !org.is_empty() && !repo.is_empty() => {
475 resolve_shard_cache_dir(org, repo)
476 }
477 _ => Err(CliError::ValidationFailed(format!(
478 "Cannot resolve a cache directory from '{model_ref}'. \
479 Expected `org/repo`, `hf://org/repo`, or an existing directory."
480 ))),
481 }
482}
483
484fn resolve_shard_cache_dir(org: &str, repo: &str) -> Result<std::path::PathBuf> {
486 Ok(dirs::home_dir()
487 .ok_or_else(|| CliError::ValidationFailed("Cannot find home directory".to_string()))?
488 .join(".apr")
489 .join("cache")
490 .join("hf")
491 .join(org)
492 .join(repo))
493}
494
495fn download_index_if_needed(base_url: &str, index_path: &Path, force: bool) -> Result<()> {
497 if force || !index_path.exists() {
498 println!();
499 println!(" {} model.safetensors.index.json", "Downloading".yellow());
500 download_file(
501 &format!("{base_url}/model.safetensors.index.json"),
502 index_path,
503 )?;
504 } else {
505 println!(" {} model.safetensors.index.json (cached)", "✓".green());
506 }
507 Ok(())
508}
509
510fn load_existing_manifest(manifest_path: &Path, force: bool) -> Option<ShardManifest> {
512 if force || !manifest_path.exists() {
513 return None;
514 }
515 std::fs::read_to_string(manifest_path)
516 .ok()
517 .and_then(|s| serde_json::from_str(&s).ok())
518}
519
520fn download_all_shards(
522 cache_dir: &Path,
523 base_url: &str,
524 shard_files: &[String],
525 force: bool,
526 existing_manifest: Option<&ShardManifest>,
527) -> Result<HashMap<String, FileChecksum>> {
528 let mut file_checksums: HashMap<String, FileChecksum> = HashMap::new();
529 let total = shard_files.len();
530 for (i, shard_file) in shard_files.iter().enumerate() {
531 download_or_verify_shard(
532 cache_dir,
533 base_url,
534 shard_file,
535 i,
536 total,
537 force,
538 existing_manifest,
539 &mut file_checksums,
540 )?;
541 }
542 Ok(file_checksums)
543}
544
545fn download_or_verify_shard(
547 cache_dir: &Path,
548 base_url: &str,
549 shard_file: &str,
550 index: usize,
551 total: usize,
552 force: bool,
553 existing_manifest: Option<&ShardManifest>,
554 checksums: &mut HashMap<String, FileChecksum>,
555) -> Result<()> {
556 let shard_path = cache_dir.join(shard_file);
557
558 if !force && shard_path.exists() {
559 if let Some(manifest) = existing_manifest {
560 if let Some(expected) = manifest.files.get(shard_file) {
561 let actual_size = std::fs::metadata(&shard_path).map(|m| m.len()).unwrap_or(0);
562 if actual_size == expected.size {
563 checksums.insert(
564 shard_file.to_string(),
565 FileChecksum {
566 size: expected.size,
567 blake3: expected.blake3.clone(),
568 },
569 );
570 println!(
571 " {} [{}/{}] {} (cached, verified)",
572 "✓".green(),
573 index + 1,
574 total,
575 shard_file
576 );
577 return Ok(());
578 }
579 println!(
580 " {} [{}/{}] {} (size mismatch: {} vs {} bytes, re-downloading)",
581 "⚠".yellow(),
582 index + 1,
583 total,
584 shard_file,
585 actual_size,
586 expected.size
587 );
588 }
590 } else {
591 println!(
592 " {} [{}/{}] {} (cached)",
593 "✓".green(),
594 index + 1,
595 total,
596 shard_file
597 );
598 return Ok(());
599 }
600 }
601
602 let shard_url = format!("{base_url}/{shard_file}");
603 print!(
604 " {} [{}/{}] {}...",
605 "↓".yellow(),
606 index + 1,
607 total,
608 shard_file
609 );
610 io::stdout().flush().ok();
611
612 let checksum = download_file_with_progress(&shard_url, &shard_path)?;
613 checksums.insert(shard_file.to_string(), checksum);
614 println!(" {}", "done".green());
615 Ok(())
616}
617
618fn download_companion_files(cache_dir: &Path, base_url: &str, force: bool) -> Result<()> {
623 let companions = [
625 ("tokenizer.json", false),
626 ("config.json", true),
627 ("tokenizer_config.json", false),
628 ("tokenizer.model", false),
629 ];
630 for (filename, required) in &companions {
631 let companion_path = cache_dir.join(filename);
632 if !force && companion_path.exists() {
633 println!(" {} {} (cached)", "✓".green(), filename);
634 continue;
635 }
636
637 let url = format!("{base_url}/{filename}");
638 match download_file(&url, &companion_path) {
639 Ok(()) => println!(" {} {}", "✓".green(), filename),
640 Err(CliError::HttpNotFound(_)) if *required => {
641 return Err(CliError::ValidationFailed(format!(
642 "{filename} is required for inference but was not found (HTTP 404) at {url}"
643 )));
644 }
645 Err(CliError::HttpNotFound(_)) => {
646 println!(" {} {} (not found in repo)", "⚠".yellow(), filename);
647 }
648 Err(e) if *required => {
649 return Err(CliError::ValidationFailed(format!(
650 "{filename} is required for inference but download failed: {e}"
651 )));
652 }
653 Err(_) => println!(" {} {} (not available, optional)", "⚠".yellow(), filename),
654 }
655 }
656
657 let tokenizer_files = ["tokenizer.json", "tokenizer.model", "tokenizer_config.json"];
659 let has_tokenizer = tokenizer_files.iter().any(|f| cache_dir.join(f).exists());
660 if !has_tokenizer {
661 return Err(CliError::ValidationFailed(format!(
662 "No tokenizer found for this model. Tried: {}.\n\
663 The model may require a custom tokenizer not hosted in the repository.",
664 tokenizer_files.join(", ")
665 )));
666 }
667
668 Ok(())
669}
670
671fn write_shard_manifest(
673 manifest_path: &Path,
674 org: &str,
675 repo: &str,
676 file_checksums: HashMap<String, FileChecksum>,
677) -> Result<()> {
678 if file_checksums.is_empty() {
679 return Ok(());
680 }
681 let manifest = ShardManifest {
682 version: 1,
683 repo: format!("{org}/{repo}"),
684 files: file_checksums,
685 };
686 let manifest_json = serde_json::to_string_pretty(&manifest)
687 .map_err(|e| CliError::ValidationFailed(format!("Failed to serialize manifest: {e}")))?;
688 std::fs::write(manifest_path, manifest_json)?;
689 println!(" {} .apr-manifest.json (integrity checksums)", "✓".green());
690 Ok(())
691}
692
693fn run_dry_run(
709 model_ref: &str,
710 revision: Option<&str>,
711 offline_flag: bool,
712 json: bool,
713) -> Result<()> {
714 let report = build_dry_run_report(model_ref, revision, offline_flag)?;
715 println!("{}", report.stdout(json));
716 Ok(())
717}
718
719#[derive(Debug)]
725pub(crate) struct DryRunReport {
726 pub(crate) model: String,
727 pub(crate) resolved: String,
728 pub(crate) revision: String,
729 pub(crate) revision_kind: String,
730 pub(crate) offline: bool,
731}
732
733impl DryRunReport {
734 pub(crate) fn stdout(&self, json: bool) -> String {
737 if json {
738 self.to_json()
739 } else {
740 self.to_human()
741 }
742 }
743
744 #[allow(clippy::disallowed_methods)]
746 fn to_json(&self) -> String {
747 let doc = serde_json::json!({
748 "model": self.model,
749 "resolved": self.resolved,
750 "revision": self.revision,
751 "revision_kind": self.revision_kind,
752 "offline": self.offline,
753 "mode": "dry-run",
754 });
755 serde_json::to_string_pretty(&doc).unwrap_or_default()
756 }
757
758 fn to_human(&self) -> String {
759 let offline = if self.offline {
760 "true".green()
761 } else {
762 "false".yellow()
763 };
764 format!(
765 "{}\n\nModel: {}\nResolved: {}\nRevision: {} ({})\nOffline: {}\nMode: {} (no network I/O)",
766 "=== APR Pull ===".cyan().bold(),
767 self.model.cyan(),
768 self.resolved.green(),
769 self.revision.green(),
770 self.revision_kind,
771 offline,
772 "dry-run".yellow(),
773 )
774 }
775}
776
777pub(crate) fn build_dry_run_report(
778 model_ref: &str,
779 revision: Option<&str>,
780 offline_flag: bool,
781) -> Result<DryRunReport> {
782 use super::aliases;
783 use super::offline;
784 use super::revision as rev;
785
786 let resolved = if let Some(url) = aliases::resolve_short_name(model_ref) {
787 url
788 } else if !model_ref.contains("://") && model_ref.contains('/') {
789 format!("hf://{model_ref}")
790 } else {
791 return Err(unknown_short_name_error(model_ref));
792 };
793
794 let rev_spec = revision.unwrap_or(rev::DEFAULT_REVISION);
795 let rev_kind = rev::classify_revision(rev_spec).map_err(|msg| {
796 CliError::ValidationFailed(format!("CRUX-A-03: invalid --revision {rev_spec:?}: {msg}"))
797 })?;
798
799 let env = offline::read_offline_env();
801 let env_borrowed: Vec<(&str, &str)> =
802 env.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect();
803 let is_offline = offline::is_offline(offline_flag, env_borrowed.iter().copied());
804
805 Ok(DryRunReport {
806 model: model_ref.to_string(),
807 resolved,
808 revision: rev_spec.to_string(),
809 revision_kind: format!("{rev_kind:?}"),
810 offline: is_offline,
811 })
812}
813
814fn unknown_short_name_error(name: &str) -> CliError {
817 use super::aliases;
818
819 let suggestions = aliases::did_you_mean(name, 2);
820 let hint = if suggestions.is_empty() {
821 "Run `apr registry aliases --json` to list known short names.".to_string()
822 } else {
823 format!(
824 "did you mean {}? (run `apr registry aliases --json` for the full list)",
825 suggestions
826 .iter()
827 .map(|s| format!("`{s}`"))
828 .collect::<Vec<_>>()
829 .join(", ")
830 )
831 };
832 CliError::ValidationFailed(format!(
833 "CRUX-A-01: unknown short name '{name}' and not a fully-qualified URI. {hint}"
834 ))
835}
836
837include!("pull_list.rs");
838include!("pull_remove_resolve_model.rs");
839include!("pull_extract_shard.rs");
840include!("pull_04.rs");
841include!("pull_dataset.rs");
842
843#[cfg(all(test, feature = "inference"))]
844mod sharded_gguf_interop_tests {
845 use aprender::format::gguf::{
846 export_tensors_to_gguf, merge_gguf_shards, GgmlType, GgufTensor, GgufValue,
847 };
848 use std::path::Path;
849
850 fn write_part(path: &Path, tensors: &[GgufTensor], meta: &[(String, GgufValue)]) {
851 let mut buf = Vec::new();
852 export_tensors_to_gguf(&mut buf, tensors, meta).expect("export part");
853 std::fs::write(path, &buf).expect("write part");
854 }
855
856 #[test]
862 fn merged_sharded_gguf_loads_in_realizar() {
863 let dir = std::env::temp_dir().join(format!("apr-merge-interop-{}", std::process::id()));
864 std::fs::create_dir_all(&dir).expect("mkdir");
865 let p0 = dir.join("model-00001-of-00002.gguf");
866 let p1 = dir.join("model-00002-of-00002.gguf");
867 let merged = dir.join("model.gguf");
868
869 let tensor = |name: &str, fill: u8| GgufTensor {
870 name: name.into(),
871 shape: vec![4],
872 dtype: GgmlType::F32,
873 data: vec![fill; 16],
874 };
875 write_part(
876 &p0,
877 &[tensor("blk.0.weight", 1)],
878 &[
879 (
880 "general.architecture".into(),
881 GgufValue::String("gemma".into()),
882 ),
883 ("gemma.embedding_length".into(), GgufValue::Uint32(2048)),
884 ("gemma.block_count".into(), GgufValue::Uint32(18)),
885 ("split.no".into(), GgufValue::Uint16(0)),
886 ("split.count".into(), GgufValue::Uint16(2)),
887 ],
888 );
889 write_part(
890 &p1,
891 &[tensor("blk.1.weight", 2)],
892 &[("split.no".into(), GgufValue::Uint16(1))],
893 );
894
895 merge_gguf_shards(&[p0, p1], &merged).expect("merge");
896 let bytes = std::fs::read(&merged).expect("read merged");
897
898 let parsed = realizar::gguf::GGUFModel::from_bytes(&bytes);
899 assert!(
900 parsed.is_ok(),
901 "realizar's GGUF loader must accept the merged sharded file: {:?}",
902 parsed.err()
903 );
904
905 std::fs::remove_dir_all(&dir).ok();
906 }
907}