pub mod arch;
pub mod backends;
pub mod cli;
pub mod convert;
pub mod core;
mod debug;
mod doctor;
pub mod gguf_patch;
pub mod inference;
pub mod input;
pub mod intelligence;
pub mod ir;
pub mod models;
pub mod progress;
pub mod quantize;
mod serve;
use std::path::PathBuf;
use std::process::ExitCode;
use anyhow::{Context, Result};
use clap::Parser;
use tracing::error;
use cli::{Cli, Command};
const EXIT_SUCCESS: u8 = 0;
const EXIT_CONVERSION_ERROR: u8 = 1;
const EXIT_INPUT_ERROR: u8 = 3;
#[derive(Debug)]
enum AppError {
Input(anyhow::Error),
Conversion(anyhow::Error),
Smoke {
code: u8,
msg: anyhow::Error,
},
}
impl AppError {
fn exit_code(&self) -> u8 {
match self {
AppError::Input(_) => EXIT_INPUT_ERROR,
AppError::Conversion(_) => EXIT_CONVERSION_ERROR,
AppError::Smoke { code, .. } => *code,
}
}
}
impl std::fmt::Display for AppError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
AppError::Input(e) => write!(f, "{:#}", e),
AppError::Conversion(e) => write!(f, "{:#}", e),
AppError::Smoke { msg, .. } => write!(f, "{:#}", msg),
}
}
}
fn main() -> ExitCode {
debug::INVESTIGATION_ENV.activate();
let cli = Cli::parse();
use std::io::IsTerminal;
use tracing_subscriber::EnvFilter;
let filter = if let Some(lvl) = cli.log_level {
EnvFilter::new(format!("hf2q={lvl},mlx_native={lvl}", lvl = lvl.as_str()))
} else {
match cli.verbose {
0 => EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("hf2q=warn")),
1 => EnvFilter::new("hf2q=info,mlx_native=info"),
2 => EnvFilter::new("hf2q=debug,mlx_native=debug"),
_ => EnvFilter::new("hf2q=trace,mlx_native=trace"),
}
};
let stderr_is_tty = std::io::stderr().is_terminal();
match cli.log_format {
cli::LogFormat::Text => {
tracing_subscriber::fmt()
.with_env_filter(filter)
.with_writer(std::io::stderr)
.with_ansi(stderr_is_tty)
.without_time()
.init();
}
cli::LogFormat::Json => {
tracing_subscriber::fmt()
.json()
.with_env_filter(filter)
.with_writer(std::io::stderr)
.with_current_span(false)
.with_span_list(false)
.init();
}
}
match run(cli) {
Ok(()) => ExitCode::from(EXIT_SUCCESS),
Err(app_err) => {
let exit_code = app_err.exit_code();
error!("{}", app_err);
eprintln!("Error: {}", app_err);
ExitCode::from(exit_code)
}
}
}
fn run(cli: Cli) -> Result<(), AppError> {
match cli.command {
Command::GgufPatch(args) => cmd_gguf_patch(args),
Command::Info(args) => cmd_info(args).map_err(AppError::Input),
Command::Doctor => doctor::run_doctor().map_err(AppError::Conversion),
Command::Completions(args) => cmd_completions(args).map_err(AppError::Input),
Command::Generate(args) => serve::cmd_generate(args).map_err(AppError::Conversion),
Command::Serve(args) => serve::cmd_serve(args).map_err(AppError::Conversion),
Command::Parity(args) => serve::cmd_parity(args).map_err(AppError::Conversion),
Command::Smoke(args) => cmd_smoke(args),
Command::Cache(args) => serve::cmd_cache(args).map_err(AppError::Input),
Command::Convert(args) => cmd_convert(args),
Command::Tokenizer(args) => cmd_tokenizer(args),
}
}
fn cmd_tokenizer(args: cli::TokenizerArgs) -> Result<(), AppError> {
use cli::TokenizerAction;
match args.action {
TokenizerAction::FixBos {
path,
gguf,
bos_id,
bos_text,
} => {
let (resolved_id, resolved_text) = if let Some(gguf_path) = gguf {
let g = mlx_native::gguf::GgufFile::open(&gguf_path).map_err(|e| {
AppError::Input(anyhow::anyhow!("open GGUF {}: {e}", gguf_path.display()))
})?;
let id = g
.metadata_u32("tokenizer.ggml.bos_token_id")
.ok_or_else(|| {
AppError::Input(anyhow::anyhow!(
"GGUF {} has no tokenizer.ggml.bos_token_id metadata",
gguf_path.display()
))
})?;
(id, bos_text)
} else {
(bos_id, bos_text)
};
let mutated =
core::tokenizer_adapter::fix_tokenizer_json_bos(&path, &resolved_text, resolved_id)
.map_err(|e| {
AppError::Input(anyhow::anyhow!(
"fix_tokenizer_json_bos {}: {e}",
path.display()
))
})?;
if mutated {
println!(
"Patched {}: prepended BOS SpecialToken {:?} (id={}) to post_processor.single",
path.display(),
resolved_text,
resolved_id,
);
} else {
println!(
"No change to {}: post_processor.single already starts with BOS SpecialToken {:?}",
path.display(), resolved_text,
);
}
Ok(())
}
}
}
fn cmd_convert(args: cli::ConvertCliArgs) -> Result<(), AppError> {
use crate::convert::{
run_convert, ConvertArgs, ConvertError, QuantSelector, RemoteConversionSource,
};
let selector = QuantSelector::from_name(&args.quant)
.map_err(|e| AppError::Input(anyhow::anyhow!("{e}")))?;
let source_repo = args.source_repo.clone();
let source_revision = args.source_revision.clone();
let (hf_dir, mut remote_source) = match (args.hf_dir, args.repo, args.revision) {
(Some(_), Some(_), _) => {
return Err(AppError::Input(anyhow::anyhow!(
"{}",
ConvertError::RepoAndDirMutuallyExclusive
)));
}
(Some(_), None, Some(_)) => {
return Err(AppError::Input(anyhow::anyhow!(
"{}",
ConvertError::RevisionRequiresRepo
)));
}
(Some(path), None, None) => (path, None),
(None, Some(repo), revision) => {
validate_hf_repo_id(&repo).map_err(|e| AppError::Input(anyhow::anyhow!("{e}")))?;
let revision = immutable_hf_revision(revision.as_deref())
.map_err(|e| AppError::Input(anyhow::anyhow!("{e}")))?;
let path = download_repo_via_hf_cli(&repo, &revision)
.map_err(|e| AppError::Conversion(anyhow::anyhow!("{e}")))?;
let verified =
crate::input::integrity::verify_remote_conversion_source(&repo, &revision, &path)
.map_err(|e| AppError::Conversion(anyhow::anyhow!("{e}")))?;
let source = RemoteConversionSource::from_verified(repo, revision, &path, &verified)
.map_err(|e| AppError::Conversion(anyhow::anyhow!("{e}")))?;
(path, Some(source))
}
(None, None, _) => {
return Err(AppError::Input(anyhow::anyhow!(
"convert: either positional `<hf_dir>` or `--repo <hf_repo>` is required"
)));
}
};
if let Some(repo) = source_repo {
validate_hf_repo_id(&repo).map_err(|e| AppError::Input(anyhow::anyhow!("{e}")))?;
let revision = immutable_hf_revision(source_revision.as_deref())
.map_err(|e| AppError::Input(anyhow::anyhow!("{e}")))?;
let verified =
crate::input::integrity::verify_remote_conversion_source(&repo, &revision, &hf_dir)
.map_err(|e| AppError::Conversion(anyhow::anyhow!("{e}")))?;
remote_source = Some(
RemoteConversionSource::from_verified(repo, revision, &hf_dir, &verified)
.map_err(|e| AppError::Conversion(anyhow::anyhow!("{e}")))?,
);
}
let resolved = ConvertArgs {
hf_dir,
selector,
output: args.output,
dry_run: args.dry_run,
imatrix: args.imatrix,
imatrix_corpus: args.imatrix_corpus,
imatrix_out: args.imatrix_out,
imatrix_n_ctx: args.imatrix_n_ctx,
mmproj: args.mmproj,
remote_source,
};
run_convert(resolved).map_err(|e| match e {
ConvertError::UnsupportedArch { .. }
| ConvertError::UnmappedTensor { .. }
| ConvertError::MissingHparam { .. }
| ConvertError::IncompleteExpertGroup { .. }
| ConvertError::DuplicateExpertIndex { .. }
| ConvertError::ApexMissingLayerCount
| ConvertError::ApexCustomOutOfScope { .. }
| ConvertError::Apex(_)
| ConvertError::Tokenizer(_)
| ConvertError::Imatrix(_)
| ConvertError::ImatrixRequiredForITier { .. }
| ConvertError::ImatrixNCtxInvalid { .. }
| ConvertError::RepoAndDirMutuallyExclusive
| ConvertError::ImmutableRevisionRequired { .. }
| ConvertError::RevisionRequiresRepo
| ConvertError::InvalidRepoId { .. } => AppError::Input(anyhow::anyhow!("{e}")),
ConvertError::Source(_)
| ConvertError::Orchestrator(_)
| ConvertError::Io(_)
| ConvertError::Integrity(_)
| ConvertError::Receipt(_)
| ConvertError::HfDownload { .. } => AppError::Conversion(anyhow::anyhow!("{e}")),
})
}
fn sanitize_repo_for_cache_dir(repo: &str) -> String {
let mut sanitized = String::with_capacity(repo.len() + 4);
for c in repo.chars() {
match c {
'/' => sanitized.push_str("__"),
c if c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '.') => sanitized.push(c),
_ => sanitized.push('_'),
}
}
if matches!(sanitized.as_str(), "" | "." | "..") {
sanitized.insert(0, '_');
}
sanitized
}
fn immutable_hf_revision(revision: Option<&str>) -> Result<String, crate::convert::ConvertError> {
let Some(revision) = revision else {
return Err(crate::convert::ConvertError::ImmutableRevisionRequired { supplied: None });
};
if revision.len() != 40 || !revision.chars().all(|c| c.is_ascii_hexdigit()) {
return Err(crate::convert::ConvertError::ImmutableRevisionRequired {
supplied: Some(revision.to_string()),
});
}
Ok(revision.to_ascii_lowercase())
}
fn validate_hf_repo_id(repo: &str) -> Result<(), crate::convert::ConvertError> {
let valid = !repo.is_empty()
&& !repo.starts_with('-')
&& repo.split('/').all(|component| {
!component.is_empty()
&& !matches!(component, "." | "..")
&& component
.chars()
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '.'))
});
if valid {
Ok(())
} else {
Err(crate::convert::ConvertError::InvalidRepoId {
repo: repo.to_string(),
})
}
}
fn download_repo_via_hf_cli(
repo: &str,
revision: &str,
) -> Result<PathBuf, crate::convert::ConvertError> {
use crate::convert::ConvertError;
let home = std::env::var("HOME").map_err(|_| ConvertError::HfDownload {
repo: repo.to_string(),
exit_code: None,
stderr: "HOME env var not set — cannot resolve ~/.cache/hf2q/repos/".to_string(),
})?;
let cache_dir = PathBuf::from(home)
.join(".cache")
.join("hf2q")
.join("repos")
.join(sanitize_repo_for_cache_dir(repo))
.join(revision);
std::fs::create_dir_all(&cache_dir).map_err(|e| ConvertError::HfDownload {
repo: repo.to_string(),
exit_code: None,
stderr: format!("failed to create cache dir `{}`: {e}", cache_dir.display()),
})?;
eprintln!(
"[hf2q convert --repo] downloading {repo}@{revision} → {} via hf",
cache_dir.display()
);
let output = hf_download_command(repo, revision, &cache_dir).output();
let output = match output {
Ok(o) => o,
Err(e) => {
return Err(ConvertError::HfDownload {
repo: repo.to_string(),
exit_code: None,
stderr: format!(
"failed to spawn `hf`: {e} \
(is the HuggingFace CLI on PATH? `pip install -U huggingface_hub[cli]`)"
),
});
}
};
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr).into_owned();
return Err(ConvertError::HfDownload {
repo: repo.to_string(),
exit_code: output.status.code(),
stderr,
});
}
Ok(cache_dir)
}
fn hf_download_command(
repo: &str,
revision: &str,
cache_dir: &std::path::Path,
) -> std::process::Command {
let mut command = std::process::Command::new("hf");
command
.arg("download")
.arg(repo)
.arg("--revision")
.arg(revision)
.arg("--local-dir")
.arg(cache_dir);
for pattern in ["*.safetensors", "*.json", "tokenizer.model", "README.md"] {
command.arg("--include").arg(pattern);
}
for pattern in [
"*.gguf",
"*.bin",
"*.pt",
"*.pth",
"*.onnx",
"*.h5",
"*.msgpack",
] {
command.arg("--exclude").arg(pattern);
}
command
}
fn cmd_gguf_patch(args: cli::GgufPatchArgs) -> Result<(), AppError> {
if !args.dry_run && !args.in_place && args.output.is_none() {
return Err(AppError::Input(anyhow::anyhow!(
"gguf-patch requires --output <out> or --in-place unless --dry-run is set"
)));
}
gguf_patch::patch_chat_template_from_arch(gguf_patch::GgufPatchOptions {
input: args.input,
output: args.output,
in_place: args.in_place,
dry_run: args.dry_run,
})
.map(|_| ())
.map_err(AppError::Conversion)
}
fn cmd_smoke(args: cli::SmokeArgs) -> Result<(), AppError> {
let smoke_args = arch::smoke::SmokeArgs {
arch: args.arch,
quant: arch::smoke::normalize_quant_label(&args.quant),
with_vision: args.with_vision,
skip_convert: args.skip_convert,
dry_run: args.dry_run,
fixtures_root: args.fixtures_root,
local_dir: args.local_dir,
convert_output_dir: args.convert_output_dir,
llama_cli_override: args.llama_cli_override,
};
let env = arch::smoke::RealSmokeEnv {
convert_dir: std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
};
let outcome = arch::smoke::dispatch(&smoke_args, &env);
let code = outcome.exit_code();
let rendered = arch::smoke::render_outcome(&outcome);
if matches!(
outcome,
arch::smoke::SmokeOutcome::Pass { .. } | arch::smoke::SmokeOutcome::Skipped { .. }
) {
println!("{}", rendered);
Ok(())
} else {
eprintln!("{}", rendered);
Err(AppError::Smoke {
code,
msg: anyhow::anyhow!("{}", rendered),
})
}
}
fn cmd_info(args: cli::InfoArgs) -> Result<()> {
let input_dir = resolve_info_input(&args)?;
let config_path = input_dir.join("config.json");
if !config_path.exists() {
anyhow::bail!(
"No config.json found in {}. Is this a HuggingFace model directory?",
input_dir.display()
);
}
let metadata =
input::config_parser::parse_config(&config_path).context("Failed to parse model config")?;
println!();
println!("{}", console::style("Model Information").bold().green());
println!("{}", input::config_parser::format_info(&metadata));
println!();
Ok(())
}
fn resolve_info_input(args: &cli::InfoArgs) -> Result<PathBuf> {
match (&args.input, &args.repo) {
(Some(path), None) => {
if !path.exists() {
anyhow::bail!("Input directory does not exist: {}", path.display());
}
Ok(path.clone())
}
(None, Some(repo_id)) => {
let progress = progress::ProgressReporter::new();
let download_dir = input::hf_download::download_model(repo_id, &progress)
.context("Failed to download model from HuggingFace Hub")?;
Ok(download_dir)
}
(None, None) => {
anyhow::bail!("Either --input or --repo must be specified");
}
(Some(_), Some(_)) => {
anyhow::bail!("--input and --repo are mutually exclusive");
}
}
}
fn cmd_completions(args: cli::CompletionsArgs) -> Result<()> {
use clap::CommandFactory;
use clap_complete::generate;
let mut cmd = Cli::command();
generate(args.shell, &mut cmd, "hf2q", &mut std::io::stdout());
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sanitize_repo_for_cache_dir_replaces_slash_with_double_underscore() {
assert_eq!(
sanitize_repo_for_cache_dir("google/gemma-4-26b-a4b-it"),
"google__gemma-4-26b-a4b-it"
);
}
#[test]
fn sanitize_repo_for_cache_dir_passes_through_when_no_slash() {
assert_eq!(sanitize_repo_for_cache_dir("local-only"), "local-only");
}
#[test]
fn sanitize_repo_for_cache_dir_replaces_every_slash() {
assert_eq!(
sanitize_repo_for_cache_dir("org/sub/model"),
"org__sub__model"
);
}
#[test]
fn cmd_convert_rejects_repo_and_dir_both_set() {
let args = cli::ConvertCliArgs {
hf_dir: Some(PathBuf::from("/tmp/example")),
repo: Some("org/repo".to_string()),
revision: Some("a".repeat(40)),
source_repo: None,
source_revision: None,
quant: "q8_0".to_string(),
output: PathBuf::from("/tmp/out.gguf"),
dry_run: false,
imatrix: None,
imatrix_corpus: None,
imatrix_out: None,
imatrix_n_ctx: None,
mmproj: false,
};
let err = cmd_convert(args).expect_err("must error");
match err {
AppError::Input(e) => {
let s = format!("{e:#}");
assert!(
s.contains("mutually exclusive"),
"expected mutually-exclusive diagnostic, got `{s}`"
);
}
other => panic!("expected AppError::Input, got {other:?}"),
}
}
#[test]
fn immutable_revision_accepts_and_normalizes_exact_sha() {
let upper = "A".repeat(40);
assert_eq!(immutable_hf_revision(Some(&upper)).unwrap(), "a".repeat(40));
}
#[test]
fn immutable_revision_rejects_missing_branch_and_short_sha() {
assert!(matches!(
immutable_hf_revision(None),
Err(crate::convert::ConvertError::ImmutableRevisionRequired { supplied: None })
));
for mutable in ["main", "v1.0", "deadbeef"] {
assert!(matches!(
immutable_hf_revision(Some(mutable)),
Err(crate::convert::ConvertError::ImmutableRevisionRequired { supplied: Some(_) })
));
}
}
#[test]
fn cmd_convert_rejects_mutable_remote_revision_before_download() {
let args = cli::ConvertCliArgs {
hf_dir: None,
repo: Some("org/model".into()),
revision: Some("main".into()),
source_repo: None,
source_revision: None,
quant: "q8_0".into(),
output: PathBuf::from("unused.gguf"),
dry_run: false,
imatrix: None,
imatrix_corpus: None,
imatrix_out: None,
imatrix_n_ctx: None,
mmproj: false,
};
let err = cmd_convert(args).expect_err("mutable revision must fail before download");
assert!(matches!(err, AppError::Input(_)));
assert!(err.to_string().contains("40-hex-commit"));
}
#[test]
fn cmd_convert_rejects_mutable_local_source_revision_before_hashing() {
let args = cli::ConvertCliArgs {
hf_dir: Some(PathBuf::from("/tmp/example")),
repo: None,
revision: None,
source_repo: Some("org/model".into()),
source_revision: Some("main".into()),
quant: "deepseek4-agentic-q2".into(),
output: PathBuf::from("unused.gguf"),
dry_run: false,
imatrix: None,
imatrix_corpus: None,
imatrix_out: None,
imatrix_n_ctx: None,
mmproj: false,
};
let err = cmd_convert(args).expect_err("mutable revision must fail before hashing");
assert!(matches!(err, AppError::Input(_)));
assert!(err.to_string().contains("40-hex-commit"));
}
#[test]
fn cache_slug_cannot_resolve_to_parent_component() {
assert_eq!(sanitize_repo_for_cache_dir(".."), "_..");
assert_eq!(
sanitize_repo_for_cache_dir("org/../../model"),
"org__..__..__model"
);
}
#[test]
fn repo_validation_blocks_option_and_path_injection() {
for invalid in ["", "--help", "../model", "org//model", "org/model?"] {
assert!(
validate_hf_repo_id(invalid).is_err(),
"accepted {invalid:?}"
);
}
validate_hf_repo_id("deepseek-ai/DeepSeek-V4").unwrap();
}
#[test]
fn hf_download_command_repeats_source_include_and_quant_exclude_flags() {
let command = hf_download_command(
"org/model",
&"a".repeat(40),
std::path::Path::new("/tmp/hf2q-fixture"),
);
assert_eq!(command.get_program(), "hf");
let args: Vec<_> = command
.get_args()
.map(|arg| arg.to_string_lossy().into_owned())
.collect();
assert_eq!(args.iter().filter(|arg| *arg == "--include").count(), 4);
assert_eq!(args.iter().filter(|arg| *arg == "--exclude").count(), 7);
assert!(args
.windows(2)
.any(|pair| pair[0] == "--revision" && pair[1] == "a".repeat(40)));
assert!(args
.windows(2)
.any(|pair| pair[0] == "--exclude" && pair[1] == "*.gguf"));
}
}