## 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
    }