use std::net::IpAddr;
use std::path::{Path, PathBuf};
use anyhow::Context;
use clap::{Parser, ValueHint};
use crate::model::Model;
const MAX_FULL_SNIFF_BYTES: u64 = 64 * 1024 * 1024;
#[derive(Parser, Debug)]
#[command(
name = "modelc",
version = crate::CLI_VERSION,
about = "Package and run model files — single-file artifacts, local inference"
)]
pub struct Cli {
#[command(subcommand)]
pub command: Commands,
}
#[derive(clap::Subcommand, Debug)]
pub enum Commands {
#[command(about = "Compile a model to a standalone executable")]
Compile {
#[arg(help = "Path to model weights file", value_hint = ValueHint::FilePath)]
input: PathBuf,
#[arg(short, long, help = "Output binary path", value_hint = ValueHint::FilePath)]
output: Option<PathBuf>,
#[arg(short = 'f', long = "format", help = "Input weight format", value_enum)]
format: Option<WeightFormat>,
#[arg(
long,
help = "Model architecture hint (overrides parsed value when set)",
value_enum
)]
arch: Option<ModelArch>,
#[arg(
short = 'p',
long,
default_value_t = 8080u16,
help = "Listening port (with --bind unless --listen is set)"
)]
port: u16,
#[arg(
long,
default_value = "0.0.0.0",
value_name = "IP",
help = "IP address the generated binary binds (ignored if --listen is set)"
)]
bind: String,
#[arg(
long = "listen",
value_name = "ADDR:PORT",
help = "Full bind address for the generated server (overrides --bind and --port)"
)]
listen: Option<String>,
#[arg(long, help = "Target triple for cross-compilation")]
target: Option<String>,
#[arg(
long,
help = "Build generated `model-serve` without `cargo --release` (debug artifacts)"
)]
debug: bool,
},
#[command(about = "Inspect a model weight file")]
Inspect {
#[arg(help = "Path to model weights file", value_hint = ValueHint::FilePath)]
input: PathBuf,
#[arg(short = 'f', long = "format", help = "Input weight format", value_enum)]
format: Option<WeightFormat>,
#[arg(long, help = "Generate a Markdown model card from metadata")]
readme: bool,
#[arg(
long = "quant-sizes",
help = "Preview artifact size for fp32/fp16/int8/int4/q4_0 without quantizing"
)]
quant_sizes: bool,
},
#[command(about = "Pack model weights into a single .modelc artifact")]
Pack {
#[arg(help = "Path to model weights file", value_hint = ValueHint::FilePath)]
input: PathBuf,
#[arg(short, long, help = "Output artifact path", value_hint = ValueHint::FilePath)]
output: Option<PathBuf>,
#[arg(short = 'f', long = "format", help = "Input weight format", value_enum)]
format: Option<WeightFormat>,
#[arg(
long,
help = "Model architecture hint (overrides parsed value when set)",
value_enum
)]
arch: Option<ModelArch>,
#[arg(long, help = "Compress tensor data with zstd")]
compress: bool,
#[arg(long, help = "Quantize FP32 tensors (fp16, int8, int4)", value_enum)]
quantize: Option<QuantizeMode>,
#[arg(
long,
help = "Prune weights with abs(value) < threshold",
value_name = "THRESHOLD"
)]
prune: Option<f32>,
},
#[command(about = "Run a .modelc artifact (starts local HTTP server)")]
Run {
#[arg(help = "Path to .modelc artifact or model name", value_hint = ValueHint::FilePath)]
input: String,
#[arg(short = 'p', long, default_value_t = 8080u16, help = "Listening port")]
port: u16,
#[arg(
long,
default_value = "127.0.0.1",
value_name = "IP",
help = "IP address to bind"
)]
bind: String,
#[arg(long, help = "Print per-operation timing for each inference request")]
profile: bool,
#[arg(long, value_name = "N", help = "Default maximum tokens to generate")]
max_tokens: Option<usize>,
#[arg(
long,
value_name = "FLOAT",
help = "Default sampling temperature (0.0 = greedy)"
)]
temperature: Option<f32>,
#[arg(long, value_name = "N", help = "Random seed for reproducible sampling")]
seed: Option<u64>,
#[arg(
long,
value_name = "FLOAT",
help = "Repetition penalty. Values > 1.0 discourage token repetition"
)]
repetition_penalty: Option<f32>,
#[arg(
long,
value_name = "FLOAT",
help = "OpenAI-style presence penalty (0.0 = disabled)"
)]
presence_penalty: Option<f32>,
#[arg(
long,
value_name = "FLOAT",
help = "OpenAI-style frequency penalty (0.0 = disabled)"
)]
frequency_penalty: Option<f32>,
#[arg(
long,
value_name = "FLOAT",
help = "Min-p sampling threshold (0.0 = disabled)"
)]
min_p: Option<f32>,
#[arg(
long,
value_name = "N",
help = "Maximum context length before KV cache shifting"
)]
max_context: Option<usize>,
#[arg(
long,
value_name = "N",
help = "Number of initial anchor tokens to preserve during context shifting"
)]
anchor_tokens: Option<usize>,
#[arg(
long,
value_name = "REGEX",
help = "Default regex grammar constraint for all requests"
)]
grammar: Option<String>,
#[arg(
long,
value_name = "KEY",
help = "Require Bearer token authentication on all endpoints"
)]
api_key: Option<String>,
#[arg(
long,
value_name = "N",
help = "Max requests per minute per client IP (0 = unlimited)"
)]
rate_limit: Option<u32>,
#[arg(
long,
value_name = "N",
help = "Max concurrent inference requests (0 = unlimited)"
)]
max_concurrent: Option<usize>,
},
#[command(about = "List installed model packages")]
List,
#[command(about = "Search installed models by name or architecture")]
Search {
#[arg(help = "Query string (matches name or architecture)")]
query: String,
},
#[command(about = "Pull a model package from a URL or path into the local store")]
Pull {
#[arg(help = "Source URL or file path")]
source: String,
#[arg(short, long, help = "Local name for the model")]
name: Option<String>,
#[arg(short, long, help = "Version tag (saves as <name>.v<version>.modelc)")]
version: Option<u32>,
},
#[command(about = "Benchmark inference latency on a .modelc artifact")]
Bench {
#[arg(help = "Path to .modelc artifact or model name")]
input: String,
#[arg(
short,
long,
default_value_t = 100,
help = "Number of warmup iterations"
)]
warmup: usize,
#[arg(
short,
long,
default_value_t = 1000,
help = "Number of benchmark iterations"
)]
iterations: usize,
},
#[command(about = "Verify a .modelc artifact integrity")]
Verify {
#[arg(help = "Path to .modelc artifact or model name")]
input: String,
},
#[command(about = "Export a .modelc artifact to Safetensors")]
Export {
#[arg(help = "Path to .modelc artifact or model name")]
input: String,
#[arg(short, long, help = "Output path", value_hint = ValueHint::FilePath)]
output: Option<PathBuf>,
},
#[command(about = "List versions of an installed model")]
Versions {
#[arg(help = "Model name")]
name: String,
},
#[command(about = "Switch active version of a model")]
Switch {
#[arg(help = "Model name")]
name: String,
#[arg(help = "Version number (e.g. 1, 2)")]
version: u32,
},
#[command(about = "Generate a minimal Docker image for a .modelc artifact")]
Containerize {
#[arg(help = "Path to .modelc artifact or model name")]
input: String,
#[arg(short, long, help = "Output directory", value_hint = ValueHint::DirPath)]
output: Option<PathBuf>,
#[arg(long, help = "Base image", default_value = "debian:bookworm-slim")]
base_image: String,
},
#[command(about = "Apply a LoRA adapter to a model artifact")]
Lora {
#[arg(help = "Path to .modelc artifact or model name")]
model: String,
#[arg(help = "Path to LoRA adapter (.safetensors)")]
adapter: PathBuf,
#[arg(short, long, default_value_t = 1.0, help = "LoRA alpha scaling factor")]
alpha: f32,
#[arg(short, long, help = "Output artifact path")]
output: Option<PathBuf>,
},
#[command(about = "Copy a model in the local store to a new name")]
Cp {
#[arg(help = "Source model name or path")]
source: String,
#[arg(help = "Destination model name")]
dest: String,
},
#[command(about = "Remove a model from the local store")]
Rm {
#[arg(help = "Model name to remove")]
name: String,
#[arg(short, long, help = "Also delete all versioned copies")]
all: bool,
#[arg(short, long, help = "Force deletion even if versioned copies exist")]
force: bool,
},
}
pub fn compile_listen(
bind: &str,
port: u16,
listen: Option<&str>,
) -> anyhow::Result<std::net::SocketAddr> {
if let Some(s) = listen {
return s.trim().parse().with_context(|| {
format!(
"invalid socket address {:?} (expected e.g. 127.0.0.1:8080 or [::1]:8080)",
s.trim()
)
});
}
let addr: IpAddr = bind
.trim()
.parse()
.with_context(|| format!("invalid bind IP {:?}", bind.trim()))?;
Ok(std::net::SocketAddr::new(addr, port))
}
pub fn apply_arch_hint(model: &mut Model, arch: Option<&ModelArch>) {
if let Some(a) = arch {
model.architecture = a.as_str().to_string();
return;
}
if model.architecture.is_empty()
|| model.architecture == "generic"
|| model.architecture == "unknown"
{
let inferred = model.infer_architecture();
if inferred != "generic" {
model.architecture = inferred;
}
}
}
#[derive(clap::ValueEnum, Clone, Copy, Debug, PartialEq, Eq)]
pub enum WeightFormat {
Safetensors,
Gguf,
Onnx,
Pytorch,
}
impl WeightFormat {
pub fn detect(path: &Path) -> Option<Self> {
let name = path.to_string_lossy().to_lowercase();
if name.ends_with(".safetensors") {
return Some(Self::Safetensors);
}
if name.ends_with(".gguf") {
return Some(Self::Gguf);
}
if name.ends_with(".onnx") {
return Some(Self::Onnx);
}
if name.ends_with(".pt") || name.ends_with(".pth") {
return Some(Self::Pytorch);
}
if name.ends_with(".bin") {
if name.contains("ggml") {
return Some(Self::Gguf);
}
if name.contains("pytorch") {
return Some(Self::Pytorch);
}
return sniff_path(path).ok().flatten();
}
sniff_path(path).ok().flatten()
}
}
fn sniff_path(path: &std::path::Path) -> std::io::Result<Option<WeightFormat>> {
let meta = std::fs::metadata(path)?;
let len = meta.len();
let read_n = (len as usize).min(512);
let mut head = vec![0u8; read_n];
if read_n > 0 {
use std::io::Read;
std::fs::File::open(path)?.read_exact(&mut head)?;
}
if head.starts_with(b"GGUF") {
return Ok(Some(WeightFormat::Gguf));
}
if head.starts_with(b"PK\x03\x04") {
return Ok(Some(WeightFormat::Pytorch));
}
if len <= MAX_FULL_SNIFF_BYTES {
let data = std::fs::read(path)?;
if safetensors::SafeTensors::deserialize(&data).is_ok() {
return Ok(Some(WeightFormat::Safetensors));
}
}
Ok(None)
}
#[derive(clap::ValueEnum, Clone, Copy, Debug, PartialEq, Eq)]
pub enum QuantizeMode {
Fp16,
Int8,
Int4,
}
#[derive(clap::ValueEnum, Clone, Copy, Debug, PartialEq, Eq)]
pub enum ModelArch {
Llama,
Gpt2,
Bert,
Mlp,
Generic,
}
impl ModelArch {
pub fn as_str(&self) -> &'static str {
match self {
Self::Llama => "llama",
Self::Gpt2 => "gpt2",
Self::Bert => "bert",
Self::Mlp => "mlp",
Self::Generic => "generic",
}
}
}
#[cfg(test)]
mod tests_listen {
use super::*;
#[test]
fn compile_listen_bind_port() {
let a = compile_listen("127.0.0.1", 9000, None).unwrap();
assert_eq!(a.port(), 9000);
}
}