Coverage Report

Created: 2026-01-23 22:53

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/noah/src/realizar/src/main.rs
Line
Count
Source
1
//! Realizar CLI - Pure Rust ML inference server
2
//!
3
//! An ollama-like experience for the PAIML ML stack.
4
//!
5
//! # Commands
6
//!
7
//! - `run` - Run a model for inference
8
//! - `chat` - Interactive chat mode
9
//! - `list` - List available models
10
//! - `pull` - Pull a model from registry
11
//! - `push` - Push a model to registry
12
//! - `serve` - Start inference server
13
//! - `bench` - Run benchmarks
14
//! - `viz` - Visualize benchmark results
15
//! - `info` - Show version info
16
17
use clap::{Parser, Subcommand};
18
#[cfg(feature = "registry")]
19
use pacha::resolver::{ModelResolver, ModelSource};
20
#[cfg(feature = "registry")]
21
use pacha::uri::ModelUri;
22
use realizar::{cli, error::Result};
23
use realizar::cli::inference::{run_gguf_inference, run_safetensors_inference, run_apr_inference};
24
25
/// Realizar - Pure Rust ML inference engine
26
///
27
/// A lightweight, fast alternative to ollama for local model inference.
28
#[derive(Parser)]
29
#[command(name = "realizar")]
30
#[command(version, about, long_about = None)]
31
struct Cli {
32
    #[command(subcommand)]
33
    command: Commands,
34
}
35
36
#[derive(Subcommand)]
37
enum Commands {
38
    /// Run a model for inference (like `ollama run`)
39
    Run {
40
        /// Model reference (pacha://name:version, hf://org/model, or path)
41
        #[arg(value_name = "MODEL")]
42
        model: String,
43
44
        /// Optional prompt (interactive mode if omitted)
45
        #[arg(value_name = "PROMPT")]
46
        prompt: Option<String>,
47
48
        /// Maximum tokens to generate
49
        #[arg(short = 'n', long, default_value = "256")]
50
        max_tokens: usize,
51
52
        /// Sampling temperature (0.0 = deterministic)
53
        #[arg(short, long, default_value = "0.7")]
54
        temperature: f32,
55
56
        /// Output format: text, json, or stream
57
        #[arg(short, long, default_value = "text")]
58
        format: String,
59
60
        /// System prompt for chat template
61
        #[arg(short, long)]
62
        system: Option<String>,
63
64
        /// Disable chat template formatting (send raw prompt)
65
        #[arg(long)]
66
        raw: bool,
67
68
        /// Force GPU acceleration (requires CUDA feature)
69
        #[arg(long)]
70
        gpu: bool,
71
72
        /// Show verbose output (loading details, performance stats)
73
        #[arg(short, long)]
74
        verbose: bool,
75
    },
76
    /// Interactive chat mode (like `ollama chat`)
77
    Chat {
78
        /// Model reference
79
        #[arg(value_name = "MODEL")]
80
        model: String,
81
82
        /// System prompt to set context
83
        #[arg(short, long)]
84
        system: Option<String>,
85
86
        /// History file for conversation persistence
87
        #[arg(long)]
88
        history: Option<String>,
89
    },
90
    /// List available models (like `ollama list`)
91
    List {
92
        /// Show remote registry models
93
        #[arg(short, long)]
94
        remote: Option<String>,
95
96
        /// Output format: table, json
97
        #[arg(short, long, default_value = "table")]
98
        format: String,
99
    },
100
    /// Pull a model from registry (like `ollama pull`)
101
    Pull {
102
        /// Model reference to pull
103
        #[arg(value_name = "MODEL")]
104
        model: String,
105
106
        /// Force re-download even if cached
107
        #[arg(short, long)]
108
        force: bool,
109
110
        /// Quantization format (q4, q8, f16)
111
        #[arg(short, long)]
112
        quantize: Option<String>,
113
    },
114
    /// Push a model to registry (like `ollama push`)
115
    Push {
116
        /// Model to push
117
        #[arg(value_name = "MODEL")]
118
        model: String,
119
120
        /// Target registry URL
121
        #[arg(long)]
122
        to: Option<String>,
123
    },
124
    /// Start the inference server (with OpenAI-compatible API)
125
    Serve {
126
        /// Host to bind to
127
        #[arg(short = 'H', long, default_value = "127.0.0.1")]
128
        host: String,
129
130
        /// Port to bind to
131
        #[arg(short, long, default_value = "8080")]
132
        port: u16,
133
134
        /// Path to model file (APR, GGUF, or SafeTensors)
135
        #[arg(short, long)]
136
        model: Option<String>,
137
138
        /// Use demo model for testing
139
        #[arg(long)]
140
        demo: bool,
141
142
        /// Enable OpenAI-compatible API at /v1/*
143
        #[arg(long, default_value = "true")]
144
        openai_api: bool,
145
146
        /// Enable batch inference for M4 parity (PARITY-093)
147
        /// Uses continuous batching scheduler for 3-4x throughput at high concurrency
148
        #[arg(long)]
149
        batch: bool,
150
151
        /// Force GPU acceleration (requires CUDA feature)
152
        #[arg(long)]
153
        gpu: bool,
154
    },
155
    /// Run performance benchmarks (wraps cargo bench)
156
    Bench {
157
        /// Benchmark suite to run
158
        #[arg(value_name = "SUITE")]
159
        suite: Option<String>,
160
161
        /// List available benchmark suites
162
        #[arg(short, long)]
163
        list: bool,
164
165
        /// Runtime to benchmark (realizar, llama-cpp, vllm, ollama)
166
        #[arg(long)]
167
        runtime: Option<String>,
168
169
        /// Model path or name for inference benchmarks
170
        #[arg(long)]
171
        model: Option<String>,
172
173
        /// Server URL for external runtime benchmarking (e.g., http://localhost:11434)
174
        #[arg(long)]
175
        url: Option<String>,
176
177
        /// Output file for JSON results (v1.1 schema)
178
        #[arg(short, long)]
179
        output: Option<String>,
180
    },
181
    /// Run convoy test for continuous batching validation (spec 2.4)
182
    BenchConvoy {
183
        /// Runtime to benchmark
184
        #[arg(long)]
185
        runtime: Option<String>,
186
187
        /// Model path for inference
188
        #[arg(long)]
189
        model: Option<String>,
190
191
        /// Output file for JSON results
192
        #[arg(short, long)]
193
        output: Option<String>,
194
    },
195
    /// Run saturation stress test (spec 2.5)
196
    BenchSaturation {
197
        /// Runtime to benchmark
198
        #[arg(long)]
199
        runtime: Option<String>,
200
201
        /// Model path for inference
202
        #[arg(long)]
203
        model: Option<String>,
204
205
        /// Output file for JSON results
206
        #[arg(short, long)]
207
        output: Option<String>,
208
    },
209
    /// Compare two benchmark result files
210
    BenchCompare {
211
        /// First benchmark result file (JSON)
212
        #[arg(value_name = "FILE1")]
213
        file1: String,
214
215
        /// Second benchmark result file (JSON)
216
        #[arg(value_name = "FILE2")]
217
        file2: String,
218
219
        /// Significance threshold percentage (default: 5.0)
220
        #[arg(short, long, default_value = "5.0")]
221
        threshold: f64,
222
    },
223
    /// Detect performance regressions between baseline and current
224
    BenchRegression {
225
        /// Baseline benchmark result file (JSON)
226
        #[arg(value_name = "BASELINE")]
227
        baseline: String,
228
229
        /// Current benchmark result file (JSON)
230
        #[arg(value_name = "CURRENT")]
231
        current: String,
232
233
        /// Strict mode: fail on any regression
234
        #[arg(long)]
235
        strict: bool,
236
    },
237
    /// Visualize benchmark results (terminal output)
238
    Viz {
239
        /// Use ANSI color output
240
        #[arg(short, long)]
241
        color: bool,
242
243
        /// Number of samples to generate
244
        #[arg(short, long, default_value = "100")]
245
        samples: usize,
246
    },
247
    /// Show version and configuration info
248
    Info,
249
}
250
251
#[tokio::main]
252
0
async fn main() -> Result<()> {
253
0
    let parsed = Cli::parse();
254
255
0
    match parsed.command {
256
0
        Commands::Run {
257
0
            model,
258
0
            prompt,
259
0
            max_tokens,
260
0
            temperature,
261
0
            format,
262
0
            system,
263
0
            raw,
264
0
            gpu,
265
0
            verbose,
266
0
        } => {
267
0
            run_model(
268
0
                &model,
269
0
                prompt.as_deref(),
270
0
                max_tokens,
271
0
                temperature,
272
0
                &format,
273
0
                system.as_deref(),
274
0
                raw,
275
0
                gpu,
276
0
                verbose,
277
0
            )
278
0
            .await?;
279
0
        },
280
0
        Commands::Chat {
281
0
            model,
282
0
            system,
283
0
            history,
284
0
        } => {
285
0
            run_chat(&model, system.as_deref(), history.as_deref()).await?;
286
0
        },
287
0
        Commands::List { remote, format } => {
288
0
            list_models(remote.as_deref(), &format)?;
289
0
        },
290
0
        Commands::Pull {
291
0
            model,
292
0
            force,
293
0
            quantize,
294
0
        } => {
295
0
            pull_model(&model, force, quantize.as_deref()).await?;
296
0
        },
297
0
        Commands::Push { model, to } => {
298
0
            push_model(&model, to.as_deref()).await?;
299
0
        },
300
0
        Commands::Serve {
301
0
            host,
302
0
            port,
303
0
            model,
304
0
            demo,
305
0
            openai_api: _,
306
0
            batch,
307
0
            gpu,
308
0
        } => {
309
0
            if demo {
310
0
                serve_demo(&host, port).await?;
311
0
            } else if let Some(model_path) = model {
312
0
                serve_model(&host, port, &model_path, batch, gpu).await?;
313
0
            } else {
314
0
                eprintln!("Error: Either --model or --demo must be specified");
315
0
                eprintln!();
316
0
                eprintln!("Usage:");
317
0
                eprintln!("  realizar serve --demo              # Use demo model");
318
0
                eprintln!("  realizar serve --model path.gguf   # Load GGUF model");
319
0
                eprintln!(
320
0
                    "  realizar serve --model path.gguf --batch  # Enable M4 parity batch mode"
321
0
                );
322
0
                std::process::exit(1);
323
0
            }
324
0
        },
325
0
        Commands::Bench {
326
0
            suite,
327
0
            list,
328
0
            runtime,
329
0
            model,
330
0
            url,
331
0
            output,
332
0
        } => {
333
0
            cli::run_benchmarks(suite, list, runtime, model, url, output)?;
334
0
        },
335
0
        Commands::BenchConvoy {
336
0
            runtime,
337
0
            model,
338
0
            output,
339
0
        } => {
340
0
            cli::run_convoy_test(runtime, model, output)?;
341
0
        },
342
0
        Commands::BenchSaturation {
343
0
            runtime,
344
0
            model,
345
0
            output,
346
0
        } => {
347
0
            cli::run_saturation_test(runtime, model, output)?;
348
0
        },
349
0
        Commands::BenchCompare {
350
0
            file1,
351
0
            file2,
352
0
            threshold,
353
0
        } => {
354
0
            cli::run_bench_compare(&file1, &file2, threshold)?;
355
0
        },
356
0
        Commands::BenchRegression {
357
0
            baseline,
358
0
            current,
359
0
            strict,
360
0
        } => {
361
0
            if cli::run_bench_regression(&baseline, &current, strict).is_err() {
362
0
                std::process::exit(1);
363
0
            }
364
0
        },
365
0
        Commands::Viz { color, samples } => {
366
0
            cli::run_visualization(color, samples);
367
0
        },
368
0
        Commands::Info => {
369
0
            cli::print_info();
370
0
        },
371
0
    }
372
0
373
0
    Ok(())
374
0
}
375
376
/// Demo server - delegates to cli::serve_demo for testability
377
0
async fn serve_demo(host: &str, port: u16) -> Result<()> {
378
0
    cli::serve_demo(host, port).await
379
0
}
380
381
/// Serve a model - delegates to cli::serve_model for testability
382
///
383
/// This is a thin wrapper that calls the library function.
384
/// All logic is in cli.rs where it can be unit tested.
385
0
async fn serve_model(
386
0
    host: &str,
387
0
    port: u16,
388
0
    model_path: &str,
389
0
    batch_mode: bool,
390
0
    force_gpu: bool,
391
0
) -> Result<()> {
392
0
    cli::serve_model(host, port, model_path, batch_mode, force_gpu).await
393
0
}
394
395
// ============================================================================
396
// Model Commands (run, chat, list, pull, push)
397
// ============================================================================
398
399
#[cfg(feature = "registry")]
400
#[allow(clippy::too_many_arguments)]
401
async fn run_model(
402
    model_ref: &str,
403
    prompt: Option<&str>,
404
    max_tokens: usize,
405
    temperature: f32,
406
    format: &str,
407
    system_prompt: Option<&str>,
408
    raw_mode: bool,
409
    force_gpu: bool,
410
    verbose: bool,
411
) -> Result<()> {
412
    use presentar_terminal::cli::Spinner;
413
    use realizar::chat_template::{auto_detect_template, ChatMessage};
414
415
    // Ollama-style: spinner while loading, then just the response
416
    let spinner = if !verbose {
417
        Some(Spinner::new().start())
418
    } else {
419
        println!("Loading model: {model_ref}");
420
        if force_gpu {
421
            println!("GPU: FORCED (--gpu flag)");
422
        }
423
        None
424
    };
425
426
    let file_data = match ModelUri::parse(model_ref) {
427
        Ok(uri) => {
428
            let resolver = ModelResolver::new_default().map_err(|e| {
429
                realizar::error::RealizarError::UnsupportedOperation {
430
                    operation: "init_resolver".to_string(),
431
                    reason: format!("Failed to initialize Pacha resolver: {e}"),
432
                }
433
            })?;
434
435
            match resolver.resolve(&uri) {
436
                Ok(resolved) => {
437
                    if verbose {
438
                        match &resolved.source {
439
                            ModelSource::LocalFile(path) => {
440
                                println!("  Source: local file ({path})");
441
                            },
442
                            ModelSource::PachaLocal { name, version } => {
443
                                println!("  Source: Pacha registry ({name}:{version})");
444
                            },
445
                            ModelSource::PachaRemote {
446
                                host,
447
                                name,
448
                                version,
449
                            } => {
450
                                println!("  Source: Remote registry {host} ({name}:{version})");
451
                            },
452
                            ModelSource::HuggingFace { repo_id, revision } => {
453
                                let rev = revision.as_deref().unwrap_or("main");
454
                                println!("  Source: HuggingFace ({repo_id}@{rev})");
455
                            },
456
                        }
457
                    }
458
                    resolved.data
459
                },
460
                Err(e) => {
461
                    if std::path::Path::new(model_ref).exists() {
462
                        println!("  Source: local file");
463
                        std::fs::read(model_ref).map_err(|e| {
464
                            realizar::error::RealizarError::UnsupportedOperation {
465
                                operation: "read_model".to_string(),
466
                                reason: format!("Failed to read {model_ref}: {e}"),
467
                            }
468
                        })?
469
                    } else {
470
                        return Err(realizar::error::RealizarError::UnsupportedOperation {
471
                            operation: "resolve_model".to_string(),
472
                            reason: format!("Failed to resolve model: {e}"),
473
                        });
474
                    }
475
                },
476
            }
477
        },
478
        Err(_) => {
479
            if !std::path::Path::new(model_ref).exists() {
480
                return Err(realizar::error::RealizarError::ModelNotFound(
481
                    model_ref.to_string(),
482
                ));
483
            }
484
            println!("  Source: local file");
485
            std::fs::read(model_ref).map_err(|e| {
486
                realizar::error::RealizarError::UnsupportedOperation {
487
                    operation: "read_model".to_string(),
488
                    reason: format!("Failed to read {model_ref}: {e}"),
489
                }
490
            })?
491
        },
492
    };
493
494
    if verbose {
495
        cli::display_model_info(model_ref, &file_data)?;
496
        println!();
497
    }
498
499
    if let Some(prompt_text) = prompt {
500
        // Apply chat template formatting unless --raw mode
501
        let formatted_prompt = if raw_mode {
502
            if verbose {
503
                println!("Prompt (raw): {prompt_text}");
504
            }
505
            prompt_text.to_string()
506
        } else {
507
            // Auto-detect template from model name
508
            let template = auto_detect_template(model_ref);
509
            if verbose {
510
                println!("Chat template: {:?}", template.format());
511
            }
512
513
            // Build messages
514
            let mut messages = Vec::new();
515
            if let Some(sys) = system_prompt {
516
                messages.push(ChatMessage::system(sys));
517
            }
518
            messages.push(ChatMessage::user(prompt_text));
519
520
            // Format using detected template
521
            match template.format_conversation(&messages) {
522
                Ok(formatted) => {
523
                    if verbose {
524
                        println!("Prompt (formatted):");
525
                        // Show first 200 chars of formatted prompt
526
                        let preview: String = formatted.chars().take(200).collect();
527
                        println!(
528
                            "  {}{}",
529
                            preview,
530
                            if formatted.len() > 200 { "..." } else { "" }
531
                        );
532
                    }
533
                    formatted
534
                },
535
                Err(e) => {
536
                    eprintln!("Warning: chat template failed ({e}), using raw prompt");
537
                    prompt_text.to_string()
538
                },
539
            }
540
        };
541
542
        if verbose {
543
            println!("Max tokens: {max_tokens}");
544
            println!("Temperature: {temperature}");
545
            println!("Format: {format}");
546
            println!();
547
        }
548
549
        // Stop spinner before inference output
550
        if let Some(sp) = spinner {
551
            sp.stop();
552
        }
553
554
        // Run actual GGUF inference with TruenoInferenceEngine
555
        run_gguf_inference(
556
            model_ref,
557
            &file_data,
558
            &formatted_prompt,
559
            max_tokens,
560
            temperature,
561
            format,
562
            force_gpu,
563
            verbose,
564
        )?;
565
    } else {
566
        println!("Interactive mode (Ctrl+D to exit)");
567
        println!();
568
        println!("Model loaded ({} bytes)", file_data.len());
569
        println!("Use a prompt argument:");
570
        println!("  realizar run {model_ref} \"Your prompt here\"");
571
    }
572
573
    Ok(())
574
}
575
576
/// Run GGUF inference with performance timing
577
///
578
/// IMP-130: Zero-copy model loading for <500ms startup time.
579
/// Uses OwnedQuantizedModel for fast CPU inference.
580
/// When `force_gpu` is true, uses OwnedQuantizedModelCuda with CUDA acceleration.
581
#[allow(clippy::too_many_arguments)]
582
#[cfg(not(feature = "registry"))]
583
#[allow(clippy::too_many_arguments)]
584
0
async fn run_model(
585
0
    model_ref: &str,
586
0
    prompt: Option<&str>,
587
0
    max_tokens: usize,
588
0
    temperature: f32,
589
0
    format: &str,
590
0
    system_prompt: Option<&str>,
591
0
    raw_mode: bool,
592
0
    force_gpu: bool,
593
0
    verbose: bool,
594
0
) -> Result<()> {
595
    use presentar_terminal::cli::Spinner;
596
    use realizar::chat_template::{auto_detect_template, ChatMessage};
597
598
    // Ollama-style: spinner while loading, then just the response
599
0
    let spinner = if !verbose {
600
0
        Some(Spinner::new().start())
601
    } else {
602
0
        println!("Loading model: {model_ref}");
603
0
        if force_gpu {
604
0
            println!("GPU: FORCED (--gpu flag)");
605
0
        }
606
0
        None
607
    };
608
609
0
    if cli::is_local_file_path(model_ref) {
610
0
        if !std::path::Path::new(model_ref).exists() {
611
0
            return Err(realizar::error::RealizarError::ModelNotFound(
612
0
                model_ref.to_string(),
613
0
            ));
614
0
        }
615
0
        if verbose {
616
0
            println!("  Source: local file");
617
0
        }
618
0
    } else if model_ref.starts_with("pacha://") || model_ref.contains(':') {
619
0
        println!("  Source: Pacha registry");
620
0
        println!();
621
0
        println!("Enable registry support: --features registry");
622
0
        println!("Or use a local file path:");
623
0
        println!("  realizar run ./model.gguf \"Your prompt\"");
624
0
        return Ok(());
625
0
    } else if model_ref.starts_with("hf://") {
626
0
        println!("  Source: HuggingFace Hub");
627
0
        println!();
628
0
        println!("Enable registry support: --features registry");
629
0
        println!("Or download manually and use:");
630
0
        println!("  realizar run ./model.gguf \"Your prompt\"");
631
0
        return Ok(());
632
0
    }
633
634
0
    let file_data = std::fs::read(model_ref).map_err(|e| {
635
0
        realizar::error::RealizarError::UnsupportedOperation {
636
0
            operation: "read_model".to_string(),
637
0
            reason: format!("Failed to read {model_ref}: {e}"),
638
0
        }
639
0
    })?;
640
641
0
    if verbose {
642
0
        cli::display_model_info(model_ref, &file_data)?;
643
0
        println!();
644
0
    }
645
646
0
    if let Some(prompt_text) = prompt {
647
        // Apply chat template formatting unless --raw mode
648
0
        let formatted_prompt = if raw_mode {
649
0
            if verbose {
650
0
                println!("Prompt (raw): {prompt_text}");
651
0
            }
652
0
            prompt_text.to_string()
653
        } else {
654
            // Auto-detect template from model name
655
0
            let template = auto_detect_template(model_ref);
656
0
            if verbose {
657
0
                println!("Chat template: {:?}", template.format());
658
0
            }
659
660
            // Build messages
661
0
            let mut messages = Vec::new();
662
0
            if let Some(sys) = system_prompt {
663
0
                messages.push(ChatMessage::system(sys));
664
0
            }
665
0
            messages.push(ChatMessage::user(prompt_text));
666
667
            // Format using detected template
668
0
            match template.format_conversation(&messages) {
669
0
                Ok(formatted) => {
670
0
                    if verbose {
671
0
                        println!("Prompt (formatted):");
672
                        // Show first 200 chars of formatted prompt
673
0
                        let preview: String = formatted.chars().take(200).collect();
674
0
                        println!(
675
0
                            "  {}{}",
676
                            preview,
677
0
                            if formatted.len() > 200 { "..." } else { "" }
678
                        );
679
0
                    }
680
0
                    formatted
681
                },
682
0
                Err(e) => {
683
0
                    eprintln!("Warning: chat template failed ({e}), using raw prompt");
684
0
                    prompt_text.to_string()
685
                },
686
            }
687
        };
688
689
0
        if verbose {
690
0
            println!("Max tokens: {max_tokens}");
691
0
            println!("Temperature: {temperature}");
692
0
            println!("Format: {format}");
693
0
            println!();
694
0
        }
695
696
        // Stop spinner before inference output
697
0
        if let Some(sp) = spinner {
698
0
            sp.stop();
699
0
        }
700
701
        // Detect format and run appropriate inference
702
        use realizar::format::{detect_format, ModelFormat};
703
0
        let detected_format = detect_format(&file_data).unwrap_or(ModelFormat::Gguf);
704
705
0
        match detected_format {
706
            ModelFormat::Apr => {
707
0
                run_apr_inference(
708
0
                    model_ref,
709
0
                    &file_data,
710
0
                    &formatted_prompt,
711
0
                    max_tokens,
712
0
                    temperature,
713
0
                    format,
714
0
                )?;
715
            },
716
            ModelFormat::SafeTensors => {
717
0
                run_safetensors_inference(
718
0
                    model_ref,
719
0
                    &formatted_prompt,
720
0
                    max_tokens,
721
0
                    temperature,
722
0
                    format,
723
0
                )?;
724
            },
725
            ModelFormat::Gguf => {
726
0
                run_gguf_inference(
727
0
                    model_ref,
728
0
                    &file_data,
729
0
                    &formatted_prompt,
730
0
                    max_tokens,
731
0
                    temperature,
732
0
                    format,
733
0
                    force_gpu,
734
0
                    verbose,
735
0
                )?;
736
            },
737
        }
738
    } else {
739
        // Stop spinner before interactive mode message
740
0
        if let Some(sp) = spinner {
741
0
            sp.stop();
742
0
        }
743
0
        println!("Interactive mode (Ctrl+D to exit)");
744
0
        println!();
745
0
        println!("Model loaded ({} bytes)", file_data.len());
746
0
        println!("Use a prompt argument:");
747
0
        println!("  realizar run {model_ref} \"Your prompt here\"");
748
    }
749
750
0
    Ok(())
751
0
}
752
753
#[cfg(feature = "registry")]
754
async fn run_chat(
755
    model_ref: &str,
756
    system_prompt: Option<&str>,
757
    history_file: Option<&str>,
758
) -> Result<()> {
759
    use std::io::{BufRead, Write};
760
761
    println!("Loading model: {model_ref}");
762
763
    let file_data = match ModelUri::parse(model_ref) {
764
        Ok(uri) => {
765
            let resolver = ModelResolver::new_default().map_err(|e| {
766
                realizar::error::RealizarError::UnsupportedOperation {
767
                    operation: "init_resolver".to_string(),
768
                    reason: format!("Failed to initialize resolver: {e}"),
769
                }
770
            })?;
771
772
            match resolver.resolve(&uri) {
773
                Ok(resolved) => {
774
                    println!("  Source: {:?}", resolved.source);
775
                    resolved.data
776
                },
777
                Err(e) => {
778
                    if std::path::Path::new(model_ref).exists() {
779
                        std::fs::read(model_ref).map_err(|e| {
780
                            realizar::error::RealizarError::UnsupportedOperation {
781
                                operation: "read_model".to_string(),
782
                                reason: format!("Failed to read: {e}"),
783
                            }
784
                        })?
785
                    } else {
786
                        return Err(realizar::error::RealizarError::UnsupportedOperation {
787
                            operation: "resolve_model".to_string(),
788
                            reason: format!("Failed to resolve: {e}"),
789
                        });
790
                    }
791
                },
792
            }
793
        },
794
        Err(_) => {
795
            if !std::path::Path::new(model_ref).exists() {
796
                return Err(realizar::error::RealizarError::ModelNotFound(
797
                    model_ref.to_string(),
798
                ));
799
            }
800
            std::fs::read(model_ref).map_err(|e| {
801
                realizar::error::RealizarError::UnsupportedOperation {
802
                    operation: "read_model".to_string(),
803
                    reason: format!("Failed to read: {e}"),
804
                }
805
            })?
806
        },
807
    };
808
809
    cli::display_model_info(model_ref, &file_data)?;
810
    println!("  Size: {} bytes", file_data.len());
811
    println!();
812
813
    let mut history: Vec<(String, String)> = if let Some(path) = history_file {
814
        if std::path::Path::new(path).exists() {
815
            let content = std::fs::read_to_string(path).unwrap_or_default();
816
            serde_json::from_str(&content).unwrap_or_default()
817
        } else {
818
            Vec::new()
819
        }
820
    } else {
821
        Vec::new()
822
    };
823
824
    if let Some(sys) = system_prompt {
825
        println!("System: {sys}");
826
        println!();
827
    }
828
829
    println!("Chat mode active. Type 'exit' or Ctrl+D to quit.");
830
    println!("Commands: /clear (clear history), /history (show history)");
831
    println!();
832
833
    let stdin = std::io::stdin();
834
    let mut stdout = std::io::stdout();
835
836
    loop {
837
        print!(">>> ");
838
        stdout.flush().ok();
839
840
        let mut input = String::new();
841
        match stdin.lock().read_line(&mut input) {
842
            Ok(0) => {
843
                println!();
844
                break;
845
            },
846
            Ok(_) => {
847
                let input = input.trim();
848
849
                if input.is_empty() {
850
                    continue;
851
                }
852
853
                if input == "exit" || input == "/exit" || input == "/quit" {
854
                    break;
855
                }
856
857
                if input == "/clear" {
858
                    history.clear();
859
                    println!("History cleared.");
860
                    continue;
861
                }
862
863
                if input == "/history" {
864
                    if history.is_empty() {
865
                        println!("No history.");
866
                    } else {
867
                        for (i, (user, assistant)) in history.iter().enumerate() {
868
                            println!("[{}] User: {}", i + 1, user);
869
                            println!("    Assistant: {}", assistant);
870
                        }
871
                    }
872
                    continue;
873
                }
874
875
                let response = format!("[Model loaded: {} bytes] Echo: {}", file_data.len(), input);
876
877
                println!();
878
                println!("{response}");
879
                println!();
880
881
                history.push((input.to_string(), response));
882
            },
883
            Err(e) => {
884
                eprintln!("Error reading input: {e}");
885
                break;
886
            },
887
        }
888
    }
889
890
    if let Some(path) = history_file {
891
        if let Ok(json) = serde_json::to_string_pretty(&history) {
892
            let _ = std::fs::write(path, json);
893
            println!("History saved to {path}");
894
        }
895
    }
896
897
    println!("Goodbye!");
898
    Ok(())
899
}
900
901
#[cfg(not(feature = "registry"))]
902
0
async fn run_chat(
903
0
    model_ref: &str,
904
0
    system_prompt: Option<&str>,
905
0
    history_file: Option<&str>,
906
0
) -> Result<()> {
907
    use std::io::{BufRead, Write};
908
909
0
    println!("Loading model: {model_ref}");
910
911
0
    if !std::path::Path::new(model_ref).exists()
912
0
        && !model_ref.starts_with("pacha://")
913
0
        && !model_ref.starts_with("hf://")
914
    {
915
0
        return Err(realizar::error::RealizarError::ModelNotFound(
916
0
            model_ref.to_string(),
917
0
        ));
918
0
    }
919
920
0
    if model_ref.starts_with("pacha://") || model_ref.starts_with("hf://") {
921
0
        println!("Registry URIs require --features registry");
922
0
        println!("Use a local file path instead.");
923
0
        return Ok(());
924
0
    }
925
926
0
    let file_data = std::fs::read(model_ref).map_err(|e| {
927
0
        realizar::error::RealizarError::UnsupportedOperation {
928
0
            operation: "read_model".to_string(),
929
0
            reason: format!("Failed to read: {e}"),
930
0
        }
931
0
    })?;
932
933
0
    cli::display_model_info(model_ref, &file_data)?;
934
0
    println!("  Size: {} bytes", file_data.len());
935
0
    println!();
936
937
0
    let mut history: Vec<(String, String)> = if let Some(path) = history_file {
938
0
        if std::path::Path::new(path).exists() {
939
0
            let content = std::fs::read_to_string(path).unwrap_or_default();
940
0
            serde_json::from_str(&content).unwrap_or_default()
941
        } else {
942
0
            Vec::new()
943
        }
944
    } else {
945
0
        Vec::new()
946
    };
947
948
0
    if let Some(sys) = system_prompt {
949
0
        println!("System: {sys}");
950
0
        println!();
951
0
    }
952
953
0
    println!("Chat mode active. Type 'exit' or Ctrl+D to quit.");
954
0
    println!("Commands: /clear (clear history), /history (show history)");
955
0
    println!();
956
957
0
    let stdin = std::io::stdin();
958
0
    let mut stdout = std::io::stdout();
959
960
    loop {
961
0
        print!(">>> ");
962
0
        stdout.flush().ok();
963
964
0
        let mut input = String::new();
965
0
        match stdin.lock().read_line(&mut input) {
966
            Ok(0) => {
967
0
                println!();
968
0
                break;
969
            },
970
            Ok(_) => {
971
0
                let input = input.trim();
972
973
0
                if input.is_empty() {
974
0
                    continue;
975
0
                }
976
977
0
                if input == "exit" || input == "/exit" || input == "/quit" {
978
0
                    break;
979
0
                }
980
981
0
                if input == "/clear" {
982
0
                    history.clear();
983
0
                    println!("History cleared.");
984
0
                    continue;
985
0
                }
986
987
0
                if input == "/history" {
988
0
                    if history.is_empty() {
989
0
                        println!("No history.");
990
0
                    } else {
991
0
                        for (i, (user, assistant)) in history.iter().enumerate() {
992
0
                            println!("[{}] User: {}", i + 1, user);
993
0
                            println!("    Assistant: {}", assistant);
994
0
                        }
995
                    }
996
0
                    continue;
997
0
                }
998
999
0
                let response = format!("[Model loaded: {} bytes] Echo: {}", file_data.len(), input);
1000
1001
0
                println!();
1002
0
                println!("{response}");
1003
0
                println!();
1004
1005
0
                history.push((input.to_string(), response));
1006
            },
1007
0
            Err(e) => {
1008
0
                eprintln!("Error reading input: {e}");
1009
0
                break;
1010
            },
1011
        }
1012
    }
1013
1014
0
    if let Some(path) = history_file {
1015
0
        if let Ok(json) = serde_json::to_string_pretty(&history) {
1016
0
            let _ = std::fs::write(path, json);
1017
0
            println!("History saved to {path}");
1018
0
        }
1019
0
    }
1020
1021
0
    println!("Goodbye!");
1022
0
    Ok(())
1023
0
}
1024
1025
#[cfg(feature = "registry")]
1026
fn list_models(remote: Option<&str>, format: &str) -> Result<()> {
1027
    println!("Available Models");
1028
    println!("================");
1029
    println!();
1030
1031
    if let Some(remote_url) = remote {
1032
        println!("Remote registry: {remote_url}");
1033
        println!();
1034
        println!("Note: Remote registry listing requires --features remote in Pacha.");
1035
        return Ok(());
1036
    }
1037
1038
    let resolver = match ModelResolver::new_default() {
1039
        Ok(r) => r,
1040
        Err(_) => {
1041
            println!("No Pacha registry found.");
1042
            println!();
1043
            println!("Initialize registry:");
1044
            println!("  pacha init");
1045
            println!();
1046
            println!("Or run a local file:");
1047
            println!("  realizar run ./model.gguf \"prompt\"");
1048
            return Ok(());
1049
        },
1050
    };
1051
1052
    if !resolver.has_registry() {
1053
        println!("No Pacha registry found.");
1054
        println!();
1055
        println!("Initialize registry:");
1056
        println!("  pacha init");
1057
        return Ok(());
1058
    }
1059
1060
    let models = match resolver.list_models() {
1061
        Ok(m) => m,
1062
        Err(e) => {
1063
            println!("Failed to list models: {e}");
1064
            return Ok(());
1065
        },
1066
    };
1067
1068
    if models.is_empty() {
1069
        println!("No models found in local registry.");
1070
        println!();
1071
        println!("Pull a model:");
1072
        println!("  realizar pull llama3:8b");
1073
        println!();
1074
        println!("Or run a local file:");
1075
        println!("  realizar run ./model.gguf \"prompt\"");
1076
    } else {
1077
        match format {
1078
            "json" => {
1079
                let json_models: Vec<_> = models
1080
                    .iter()
1081
                    .map(|name| {
1082
                        let versions = resolver.list_versions(name).unwrap_or_default();
1083
                        serde_json::json!({
1084
                            "name": name,
1085
                            "versions": versions.len()
1086
                        })
1087
                    })
1088
                    .collect();
1089
                println!(
1090
                    "{}",
1091
                    serde_json::to_string_pretty(&json_models).unwrap_or_default()
1092
                );
1093
            },
1094
            _ => {
1095
                println!("{:<40} {:>12}", "NAME", "VERSIONS");
1096
                println!("{}", "-".repeat(54));
1097
                for name in &models {
1098
                    let versions = resolver.list_versions(name).unwrap_or_default();
1099
                    println!("{:<40} {:>12}", name, versions.len());
1100
                }
1101
            },
1102
        }
1103
    }
1104
1105
    Ok(())
1106
}
1107
1108
#[cfg(not(feature = "registry"))]
1109
0
fn list_models(remote: Option<&str>, format: &str) -> Result<()> {
1110
0
    println!("Available Models");
1111
0
    println!("================");
1112
0
    println!();
1113
1114
0
    if let Some(remote_url) = remote {
1115
0
        println!("Remote registry: {remote_url}");
1116
0
        println!();
1117
0
        println!("Note: Remote registry listing requires --features registry.");
1118
0
        return Ok(());
1119
0
    }
1120
1121
0
    let pacha_dir = cli::home_dir()
1122
0
        .map(|h| h.join(".pacha").join("models"))
1123
0
        .unwrap_or_else(|| std::path::PathBuf::from(".pacha/models"));
1124
1125
0
    if !pacha_dir.exists() {
1126
0
        println!("No models found in local registry.");
1127
0
        println!();
1128
0
        println!("Pull a model:");
1129
0
        println!("  realizar pull llama3:8b");
1130
0
        println!();
1131
0
        println!("Or run a local file:");
1132
0
        println!("  realizar run ./model.gguf \"prompt\"");
1133
0
        return Ok(());
1134
0
    }
1135
1136
0
    let mut models_found = Vec::new();
1137
0
    if let Ok(entries) = std::fs::read_dir(&pacha_dir) {
1138
0
        for entry in entries.flatten() {
1139
0
            let path = entry.path();
1140
0
            if path.is_file() {
1141
0
                let name = path.file_name().unwrap_or_default().to_string_lossy();
1142
0
                if name.ends_with(".gguf")
1143
0
                    || name.ends_with(".safetensors")
1144
0
                    || name.ends_with(".apr")
1145
                {
1146
0
                    let size = std::fs::metadata(&path).map(|m| m.len()).unwrap_or(0);
1147
0
                    models_found.push((name.to_string(), size));
1148
0
                }
1149
0
            }
1150
        }
1151
0
    }
1152
1153
0
    if models_found.is_empty() {
1154
0
        println!("No models found in {}", pacha_dir.display());
1155
0
    } else {
1156
0
        match format {
1157
0
            "json" => {
1158
0
                let json_models: Vec<_> = models_found
1159
0
                    .iter()
1160
0
                    .map(|(name, size)| {
1161
0
                        serde_json::json!({
1162
0
                            "name": name,
1163
0
                            "size_bytes": size,
1164
0
                            "size_human": cli::format_size(*size)
1165
                        })
1166
0
                    })
1167
0
                    .collect();
1168
0
                println!(
1169
0
                    "{}",
1170
0
                    serde_json::to_string_pretty(&json_models).unwrap_or_default()
1171
                );
1172
            },
1173
            _ => {
1174
0
                println!("{:<40} {:>12}", "NAME", "SIZE");
1175
0
                println!("{}", "-".repeat(54));
1176
0
                for (name, size) in &models_found {
1177
0
                    println!("{:<40} {:>12}", name, cli::format_size(*size));
1178
0
                }
1179
            },
1180
        }
1181
    }
1182
1183
0
    Ok(())
1184
0
}
1185
1186
#[cfg(feature = "registry")]
1187
async fn pull_model(model_ref: &str, force: bool, quantize: Option<&str>) -> Result<()> {
1188
    println!("Pulling model: {model_ref}");
1189
    if force {
1190
        println!("  Force: re-downloading even if cached");
1191
    }
1192
    if let Some(q) = quantize {
1193
        println!("  Quantize: {q}");
1194
    }
1195
    println!();
1196
1197
    let uri = ModelUri::parse(model_ref).map_err(|e| {
1198
        realizar::error::RealizarError::UnsupportedOperation {
1199
            operation: "parse_uri".to_string(),
1200
            reason: format!("Invalid model reference: {e}"),
1201
        }
1202
    })?;
1203
1204
    let resolver = ModelResolver::new_default().map_err(|e| {
1205
        realizar::error::RealizarError::UnsupportedOperation {
1206
            operation: "init_resolver".to_string(),
1207
            reason: format!("Failed to initialize Pacha resolver: {e}"),
1208
        }
1209
    })?;
1210
1211
    if !force && resolver.exists(&uri) {
1212
        println!("Model already cached locally.");
1213
        println!("Use --force to re-download.");
1214
        return Ok(());
1215
    }
1216
1217
    println!("Downloading...");
1218
    let resolved = resolver.resolve(&uri).map_err(|e| {
1219
        realizar::error::RealizarError::UnsupportedOperation {
1220
            operation: "pull_model".to_string(),
1221
            reason: format!("Failed to pull model: {e}"),
1222
        }
1223
    })?;
1224
1225
    println!("  Downloaded: {} bytes", resolved.data.len());
1226
1227
    match &resolved.source {
1228
        ModelSource::LocalFile(path) => {
1229
            println!("  Source: local file ({path})");
1230
        },
1231
        ModelSource::PachaLocal { name, version } => {
1232
            println!("  Source: Pacha local ({name}:{version})");
1233
        },
1234
        ModelSource::PachaRemote {
1235
            host,
1236
            name,
1237
            version,
1238
        } => {
1239
            println!("  Source: Remote {host} ({name}:{version})");
1240
            println!("  Cached to local registry.");
1241
        },
1242
        ModelSource::HuggingFace { repo_id, revision } => {
1243
            let rev = revision.as_deref().unwrap_or("main");
1244
            println!("  Source: HuggingFace ({repo_id}@{rev})");
1245
        },
1246
    }
1247
1248
    println!();
1249
    println!("Model ready! Run with:");
1250
    println!("  realizar run {model_ref} \"Your prompt\"");
1251
1252
    Ok(())
1253
}
1254
1255
#[cfg(not(feature = "registry"))]
1256
0
async fn pull_model(model_ref: &str, force: bool, quantize: Option<&str>) -> Result<()> {
1257
0
    println!("Pulling model: {model_ref}");
1258
0
    if force {
1259
0
        println!("  Force: re-downloading even if cached");
1260
0
    }
1261
0
    if let Some(q) = quantize {
1262
0
        println!("  Quantize: {q}");
1263
0
    }
1264
0
    println!();
1265
1266
0
    if let Some(hf_path) = model_ref.strip_prefix("hf://") {
1267
0
        println!("Source: HuggingFace Hub");
1268
0
        println!("Model: {hf_path}");
1269
0
        println!();
1270
0
        println!("Enable registry support: --features registry");
1271
0
        println!("Or manual download:");
1272
0
        println!("  huggingface-cli download {hf_path}");
1273
0
    } else if let Some(pacha_path) = model_ref.strip_prefix("pacha://") {
1274
0
        println!("Source: Pacha Registry");
1275
0
        println!("Model: {pacha_path}");
1276
0
        println!();
1277
0
        println!("Enable registry support: --features registry");
1278
0
    } else {
1279
0
        println!("Source: Default registry (Pacha)");
1280
0
        println!("Model: {model_ref}");
1281
0
        println!();
1282
0
        println!("Enable registry support: --features registry");
1283
0
        println!("Or download manually and use:");
1284
0
        println!("  realizar run ./downloaded-model.gguf \"prompt\"");
1285
0
    }
1286
1287
0
    Ok(())
1288
0
}
1289
1290
#[cfg(feature = "registry")]
1291
async fn push_model(model_ref: &str, target: Option<&str>) -> Result<()> {
1292
    use pacha::Registry;
1293
1294
    println!("Pushing model: {model_ref}");
1295
1296
    let (name, version_str) = if let Some(idx) = model_ref.rfind(':') {
1297
        (&model_ref[..idx], &model_ref[idx + 1..])
1298
    } else {
1299
        (model_ref, "latest")
1300
    };
1301
1302
    println!("  Name: {name}");
1303
    println!("  Version: {version_str}");
1304
1305
    if let Some(t) = target {
1306
        println!("  Target: {t}");
1307
        println!();
1308
        println!("Remote push requires --features remote in Pacha.");
1309
        println!("Use pacha CLI for remote operations:");
1310
        println!("  pacha push {model_ref} --to {t}");
1311
    } else {
1312
        println!("  Target: local Pacha registry");
1313
        println!();
1314
1315
        let local_path = format!("{name}.gguf");
1316
        if !std::path::Path::new(&local_path).exists() {
1317
            println!("Local file not found: {local_path}");
1318
            println!();
1319
            println!("To push a model to registry:");
1320
            println!("  1. Have the model file: {name}.gguf");
1321
            println!("  2. Run: realizar push {name}:{version_str}");
1322
            return Ok(());
1323
        }
1324
1325
        let data = std::fs::read(&local_path).map_err(|e| {
1326
            realizar::error::RealizarError::UnsupportedOperation {
1327
                operation: "read_model".to_string(),
1328
                reason: format!("Failed to read {local_path}: {e}"),
1329
            }
1330
        })?;
1331
1332
        let registry = Registry::open_default().map_err(|e| {
1333
            realizar::error::RealizarError::UnsupportedOperation {
1334
                operation: "open_registry".to_string(),
1335
                reason: format!("Failed to open Pacha registry: {e}"),
1336
            }
1337
        })?;
1338
1339
        let version = parse_model_version(version_str)?;
1340
1341
        let card = pacha::model::ModelCard::new(format!("Model {name} pushed via realizar"));
1342
        registry
1343
            .register_model(name, &version, &data, card)
1344
            .map_err(|e| realizar::error::RealizarError::UnsupportedOperation {
1345
                operation: "register_model".to_string(),
1346
                reason: format!("Failed to register model: {e}"),
1347
            })?;
1348
1349
        println!("Model registered successfully!");
1350
        println!();
1351
        println!("Run with:");
1352
        println!("  realizar run pacha://{name}:{version_str} \"Your prompt\"");
1353
    }
1354
1355
    Ok(())
1356
}
1357
1358
#[cfg(not(feature = "registry"))]
1359
0
async fn push_model(model_ref: &str, target: Option<&str>) -> Result<()> {
1360
0
    println!("Pushing model: {model_ref}");
1361
0
    if let Some(t) = target {
1362
0
        println!("  Target: {t}");
1363
0
    } else {
1364
0
        println!("  Target: default Pacha registry");
1365
0
    }
1366
0
    println!();
1367
0
    println!("Enable registry support: --features registry");
1368
0
    Ok(())
1369
0
}
1370
1371
#[cfg(feature = "registry")]
1372
fn parse_model_version(s: &str) -> Result<pacha::model::ModelVersion> {
1373
    let parts: Vec<&str> = s.split('.').collect();
1374
    if parts.len() == 3 {
1375
        let major: u32 =
1376
            parts[0]
1377
                .parse()
1378
                .map_err(|_| realizar::error::RealizarError::UnsupportedOperation {
1379
                    operation: "parse_version".to_string(),
1380
                    reason: format!("Invalid version: {s}"),
1381
                })?;
1382
        let minor: u32 =
1383
            parts[1]
1384
                .parse()
1385
                .map_err(|_| realizar::error::RealizarError::UnsupportedOperation {
1386
                    operation: "parse_version".to_string(),
1387
                    reason: format!("Invalid version: {s}"),
1388
                })?;
1389
        let patch: u32 =
1390
            parts[2]
1391
                .parse()
1392
                .map_err(|_| realizar::error::RealizarError::UnsupportedOperation {
1393
                    operation: "parse_version".to_string(),
1394
                    reason: format!("Invalid version: {s}"),
1395
                })?;
1396
        return Ok(pacha::model::ModelVersion::new(major, minor, patch));
1397
    }
1398
1399
    if s == "latest" {
1400
        return Ok(pacha::model::ModelVersion::new(1, 0, 0));
1401
    }
1402
    if let Ok(major) = s.parse::<u32>() {
1403
        return Ok(pacha::model::ModelVersion::new(major, 0, 0));
1404
    }
1405
1406
    Err(realizar::error::RealizarError::UnsupportedOperation {
1407
        operation: "parse_version".to_string(),
1408
        reason: format!("Invalid version format: {s}. Expected: x.y.z"),
1409
    })
1410
}