use crate::config::CmfCfg;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::process::Stdio;
use std::sync::{Arc, Mutex};
use std::time::{SystemTime, UNIX_EPOCH};
use tokio::io::{AsyncBufReadExt, BufReader};
use tokio::sync::Notify;
fn parse_progress(line: &str) -> Option<(f32, String)> {
let rest = line.strip_prefix("@PROGRESS ")?;
let mut it = rest.splitn(2, ' ');
let frac: f32 = it.next()?.trim().parse().ok()?;
let phase = it.next().unwrap_or("").trim().to_string();
Some((frac.clamp(0.0, 1.0), phase))
}
fn cleanup_output(path: &str) {
let p = std::path::Path::new(path);
if p.is_dir() {
let _ = std::fs::remove_dir_all(p);
} else if p.exists() {
let _ = std::fs::remove_file(p);
}
}
pub fn path_size(p: &std::path::Path) -> u64 {
match std::fs::metadata(p) {
Ok(m) if m.is_file() => m.len(),
Ok(m) if m.is_dir() => std::fs::read_dir(p)
.map(|rd| {
rd.filter_map(|e| e.ok())
.map(|e| path_size(&e.path()))
.sum()
})
.unwrap_or(0),
_ => 0,
}
}
struct AbortOnDrop(tokio::task::JoinHandle<()>);
impl Drop for AbortOnDrop {
fn drop(&mut self) {
self.0.abort();
}
}
fn spawn_size_poller(store: Arc<JobStore>, id: String, path: String) -> AbortOnDrop {
AbortOnDrop(tokio::spawn(async move {
let p = std::path::PathBuf::from(&path);
loop {
tokio::time::sleep(std::time::Duration::from_millis(600)).await;
let done = path_size(&p);
if done > 0 {
store.set_bytes(&id, done);
}
}
}))
}
fn now() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs()
}
pub fn gen_id(repo: &str) -> String {
use std::hash::{Hash, Hasher};
let mut h = std::collections::hash_map::DefaultHasher::new();
repo.hash(&mut h);
format!("{:016x}", h.finish())
}
#[derive(Clone, Debug, Deserialize)]
pub struct ImportParams {
pub repo: String,
#[serde(default = "default_quant")]
pub quant: String,
#[serde(default)]
pub name: String, #[serde(default)]
pub variant: Option<String>,
#[serde(default)]
pub linear_core: Option<String>, #[serde(default)]
pub nphase: Option<u32>,
#[serde(default)]
pub vbit_shape: Option<String>, #[serde(default)]
pub mean_bits: Option<f32>,
#[serde(default)]
pub shard_max_gb: Option<f32>,
#[serde(default)]
pub skip_mtp: bool,
#[serde(default)]
pub o1: Option<String>,
#[serde(default)]
pub o1_m: Option<usize>,
#[serde(default)]
pub o1_window: Option<usize>,
#[serde(default)]
pub o1_sink: Option<usize>,
}
fn default_quant() -> String {
"Q8_2F".into()
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Job {
pub id: String,
pub repo: String,
pub quant: String,
pub output: String,
pub name: String,
pub variant: Option<String>,
pub state: String, pub log: Vec<String>,
pub started: u64,
pub finished: Option<u64>,
pub size_bytes: Option<u64>,
pub progress: Option<f32>, pub phase: Option<String>, pub total_bytes: Option<u64>,
pub done_bytes: Option<u64>,
}
#[derive(Default)]
pub struct JobStore {
jobs: Mutex<HashMap<String, Job>>,
cancels: Mutex<HashMap<String, Arc<Notify>>>,
persist: Mutex<Option<std::path::PathBuf>>,
last_save: Mutex<Option<std::time::Instant>>,
}
impl JobStore {
pub fn new() -> Arc<Self> {
Arc::new(Self::default())
}
pub fn attach_persistence(&self, path: std::path::PathBuf) {
if let Ok(text) = std::fs::read_to_string(&path) {
if let Ok(mut jobs) = serde_json::from_str::<Vec<Job>>(&text) {
let mut g = self.jobs.lock().unwrap();
for j in jobs.drain(..) {
let mut j = j;
if j.state == "running" {
j.state = "error".into();
j.log.push("✗ interrupted by a gateway restart".into());
j.finished = Some(now());
}
g.insert(j.id.clone(), j);
}
}
}
*self.persist.lock().unwrap() = Some(path);
}
fn save(&self, force: bool) {
let Some(path) = self.persist.lock().unwrap().clone() else {
return;
};
if !force {
let mut last = self.last_save.lock().unwrap();
if let Some(t) = *last {
if t.elapsed() < std::time::Duration::from_secs(2) {
return;
}
}
*last = Some(std::time::Instant::now());
}
let jobs: Vec<Job> = self.jobs.lock().unwrap().values().cloned().collect();
if let Ok(json) = serde_json::to_vec(&jobs) {
let tmp = path.with_extension("json.tmp");
if std::fs::write(&tmp, json).is_ok() {
let _ = std::fs::rename(&tmp, &path);
}
}
}
fn insert(&self, job: Job) {
self.jobs.lock().unwrap().insert(job.id.clone(), job);
self.save(true);
}
fn set_cancel(&self, id: &str, n: Arc<Notify>) {
self.cancels.lock().unwrap().insert(id.to_string(), n);
}
fn drop_cancel(&self, id: &str) {
self.cancels.lock().unwrap().remove(id);
}
pub fn cancel(&self, id: &str) -> bool {
let running = matches!(
self.jobs.lock().unwrap().get(id).map(|j| j.state.as_str()),
Some("running")
);
if !running {
return false;
}
if let Some(n) = self.cancels.lock().unwrap().get(id) {
n.notify_one();
true
} else {
false
}
}
fn push_line(&self, id: &str, line: String) {
if let Some(j) = self.jobs.lock().unwrap().get_mut(id) {
j.log.push(line);
let n = j.log.len();
if n > 200 {
j.log.drain(0..n - 200); }
}
}
fn set_progress(&self, id: &str, frac: f32, phase: String) {
if let Some(j) = self.jobs.lock().unwrap().get_mut(id) {
let f = frac.clamp(0.0, 1.0);
let cur = j.progress.unwrap_or(0.0);
if j.progress.is_none() || f > cur + 0.001 {
j.progress = Some(f);
}
if !phase.is_empty() {
j.phase = Some(phase);
}
}
self.save(false);
}
fn set_total(&self, id: &str, total: u64) {
if let Some(j) = self.jobs.lock().unwrap().get_mut(id) {
if total > 0 {
j.total_bytes = Some(total);
}
}
}
fn set_bytes(&self, id: &str, done: u64) {
let total = {
let mut g = self.jobs.lock().unwrap();
let Some(j) = g.get_mut(id) else { return };
if j.state != "running" {
return;
}
if done > j.done_bytes.unwrap_or(0) {
j.done_bytes = Some(done);
}
j.total_bytes
};
if let Some(t) = total.filter(|t| *t > 0) {
self.set_progress(id, done as f32 / t as f32, "downloading".into());
}
}
fn set_phase(&self, id: &str, phase: String) {
if let Some(j) = self.jobs.lock().unwrap().get_mut(id) {
j.phase = Some(phase);
if j.progress == Some(0.0) {
j.progress = None;
}
}
}
fn update_quant(&self, id: &str, quant: String) {
if let Some(j) = self.jobs.lock().unwrap().get_mut(id) {
j.quant = quant.clone();
if !j.log.is_empty() && j.log[0].starts_with("→ downloading ready .cmf model") {
j.log[0] = format!("→ downloading ready .cmf model {} ({})", j.repo, quant);
}
}
}
fn set_state(&self, id: &str, state: &str) {
if let Some(j) = self.jobs.lock().unwrap().get_mut(id) {
j.state = state.into();
j.finished = Some(now());
}
self.save(true);
}
fn finish(&self, id: &str, ok: bool) {
if let Some(j) = self.jobs.lock().unwrap().get_mut(id) {
if j.state == "cancelled" {
return; }
j.finished = Some(now());
let on_disk = path_size(std::path::Path::new(&j.output));
j.size_bytes = if on_disk > 0 { Some(on_disk) } else { None };
let done = ok && on_disk > 0;
j.state = if done { "done" } else { "error" }.into();
if done {
j.progress = Some(1.0);
j.done_bytes = Some(on_disk);
j.total_bytes = Some(j.total_bytes.unwrap_or(on_disk).max(on_disk));
}
}
self.save(true);
}
pub fn get(&self, id: &str) -> Option<Job> {
self.jobs.lock().unwrap().get(id).cloned()
}
pub fn delete(&self, id: &str) -> Result<bool, String> {
let job = self.jobs.lock().unwrap().get(id).cloned();
let Some(job) = job else { return Ok(false) };
if job.state == "running" {
return Err("cancel the running job before deleting".into());
}
cleanup_output(&job.output);
self.cancels.lock().unwrap().remove(id);
self.jobs.lock().unwrap().remove(id);
self.save(true);
Ok(true)
}
pub fn list(&self) -> Vec<Job> {
let mut map: HashMap<String, Job> = HashMap::new();
for j in self.jobs.lock().unwrap().values().cloned() {
let key = format!("{}::{}", j.repo, j.variant.clone().unwrap_or_default());
match map.get(&key) {
Some(prev) if prev.started >= j.started => {}
_ => {
map.insert(key, j);
}
}
}
let mut v: Vec<Job> = map.into_values().collect();
v.sort_by_key(|x| std::cmp::Reverse(x.started));
v.truncate(20);
v
}
}
pub async fn hf_search(
query: &str,
limit: usize,
token: Option<&str>,
) -> Result<serde_json::Value, String> {
let q: String = url_escape(query);
let sort = if query.trim().is_empty() {
"trendingScore"
} else {
"downloads"
};
let url = format!(
"https://huggingface.co/api/models?search={q}&sort={sort}&direction=-1&limit={limit}&full=false"
);
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(15))
.build()
.map_err(|e| e.to_string())?;
let mut req = client.get(&url).header("User-Agent", "cortiq-gateway");
if let Some(t) = token {
req = req.bearer_auth(t);
}
let resp = req.send().await.map_err(|e| e.to_string())?;
if !resp.status().is_success() {
return Err(format!("HuggingFace API {}", resp.status()));
}
resp.json().await.map_err(|e| e.to_string())
}
pub async fn hf_search_author(
author: &str,
limit: usize,
token: Option<&str>,
) -> Result<serde_json::Value, String> {
let url = format!(
"https://huggingface.co/api/models?author={}&limit={}&sort=trendingScore&direction=-1",
url_escape(author),
limit
);
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(15))
.build()
.map_err(|e| e.to_string())?;
let mut req = client.get(&url).header("User-Agent", "cortiq-gateway");
if let Some(t) = token {
req = req.bearer_auth(t);
}
let resp = req.send().await.map_err(|e| e.to_string())?;
if !resp.status().is_success() {
return Err(format!("HuggingFace API {}", resp.status()));
}
resp.json().await.map_err(|e| e.to_string())
}
pub async fn hf_model_size(id: &str, token: Option<&str>) -> Option<u64> {
let url = format!("https://huggingface.co/api/models/{}", url_escape(id));
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(8))
.build()
.ok()?;
let mut req = client.get(&url).header("User-Agent", "cortiq-gateway");
if let Some(t) = token {
req = req.bearer_auth(t);
}
let resp = req.send().await.ok()?;
if !resp.status().is_success() {
return None;
}
let v: serde_json::Value = resp.json().await.ok()?;
v.get("usedStorage").and_then(|x| x.as_u64())
}
pub async fn hf_repo_variants(id: &str, token: Option<&str>) -> Vec<(String, u64)> {
let url = format!(
"https://huggingface.co/api/models/{}/tree/main?recursive=true",
url_escape(id)
);
let client = match reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(10))
.build()
{
Ok(c) => c,
Err(_) => return Vec::new(),
};
let mut req = client.get(&url).header("User-Agent", "cortiq-gateway");
if let Some(t) = token {
req = req.bearer_auth(t);
}
let resp = match req.send().await {
Ok(r) if r.status().is_success() => r,
_ => return Vec::new(),
};
let arr: serde_json::Value = match resp.json().await {
Ok(v) => v,
Err(_) => return Vec::new(),
};
let list = match arr.as_array() {
Some(a) => a,
None => return Vec::new(),
};
use std::collections::HashMap;
let mut dir_sizes: HashMap<String, u64> = HashMap::new();
let mut single_cmf: Vec<(String, u64)> = Vec::new();
for item in list {
if item.get("type").and_then(|t| t.as_str()) != Some("file") {
continue;
}
let path = match item.get("path").and_then(|p| p.as_str()) {
Some(p) => p,
None => continue,
};
let sz = item
.get("size")
.and_then(|v| v.as_u64())
.or_else(|| {
item.get("lfs")
.and_then(|l| l.get("size"))
.and_then(|v| v.as_u64())
})
.unwrap_or(0);
if sz == 0 {
continue;
}
if path.to_ascii_lowercase().ends_with(".cmf") {
single_cmf.push((path.to_string(), sz));
} else if path.contains('/') && path.starts_with("parts-") {
let top = path.split('/').next().unwrap_or(path).to_string();
*dir_sizes.entry(top).or_default() += sz;
}
}
if !single_cmf.is_empty() {
return single_cmf;
}
let mut out: Vec<(String, u64)> = dir_sizes.into_iter().collect();
out.sort_by(|a, b| a.0.cmp(&b.0));
out
}
async fn head_content_length(url: &str, token: Option<&str>) -> Option<u64> {
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(10))
.build()
.ok()?;
let mut req = client.head(url).header("User-Agent", "cortiq-gateway");
if let Some(t) = token {
req = req.bearer_auth(t);
}
let resp = req.send().await.ok()?;
if !resp.status().is_success() {
return None;
}
resp.headers()
.get(reqwest::header::CONTENT_LENGTH)?
.to_str()
.ok()?
.parse()
.ok()
}
pub fn format_bytes(n: u64) -> String {
if n >= 1_000_000_000 {
format!("{:.1} GB", n as f64 / 1_000_000_000.0)
} else if n >= 1_000_000 {
format!("{} MB", n / 1_000_000)
} else if n >= 1_000 {
format!("{} KB", n / 1_000)
} else {
format!("{n} B")
}
}
fn url_escape(s: &str) -> String {
s.chars()
.map(|c| match c {
'a'..='z' | 'A'..='Z' | '0'..='9' | '-' | '_' | '.' | '/' => c.to_string(),
' ' => "+".to_string(),
_ => format!("%{:02X}", c as u32),
})
.collect()
}
fn sanitize(s: &str) -> String {
s.chars()
.map(|c| {
if c.is_ascii_alphanumeric() || c == '-' || c == '_' {
c
} else {
'-'
}
})
.collect()
}
pub fn sanitize_for_id(s: &str) -> String {
sanitize(&s.to_lowercase())
}
fn map_quant(q: &str) -> &'static str {
match q.to_ascii_uppercase().as_str() {
s if s.starts_with("Q4T") || s == "Q4_TILED" => "q4t",
s if s.starts_with("Q4") => "q4",
"Q8_2F" | "Q82F" => "q8_2f",
"F16" | "FP16" => "f16",
"VBIT" => "vbit",
"Q1" => "q1",
"Q1P" | "Q1_PTQ" => "q1p",
"Q1S" | "Q1_MASK" => "q1s",
"Q1T" | "Q1_TERNARY" => "q1t",
_ => "q8",
}
}
fn is_cmf_repo(repo: &str) -> bool {
let lower = repo.to_ascii_lowercase();
lower.contains("cmf") || lower.ends_with(".cmf")
}
async fn download_cmf_repo(
repo: String,
output_abs: String,
hf_token: Option<String>,
store: Arc<JobStore>,
id: String,
cancel: Arc<Notify>,
variant: Option<String>,
) {
store.set_phase(&id, "preparing".into());
let tree_url = format!("https://huggingface.co/api/models/{repo}/tree/main?recursive=true");
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(10))
.build()
.unwrap_or_else(|_| reqwest::Client::new());
let mut req = client.get(&tree_url).header("User-Agent", "cortiq-gateway");
if let Some(t) = &hf_token {
req = req.bearer_auth(t);
}
let mut cmf_files: Vec<(String, u64)> = Vec::new();
let mut sharded_dirs: std::collections::HashMap<String, Vec<String>> =
std::collections::HashMap::new();
let mut dir_sizes: std::collections::HashMap<String, u64> = std::collections::HashMap::new();
if let Ok(resp) = req.send().await {
if resp.status().is_success() {
if let Ok(json) = resp.json::<serde_json::Value>().await {
if let Some(arr) = json.as_array() {
for item in arr {
if item.get("type").and_then(|t| t.as_str()) != Some("file") {
continue;
}
if let Some(path) = item.get("path").and_then(|p| p.as_str()) {
let sz = item
.get("size")
.and_then(|v| v.as_u64())
.or_else(|| {
item.get("lfs")
.and_then(|l| l.get("size"))
.and_then(|v| v.as_u64())
})
.unwrap_or(0);
if path.to_ascii_lowercase().ends_with(".cmf") {
cmf_files.push((path.to_string(), sz));
} else if path.contains('/') && path.starts_with("parts-") {
let top = path.split('/').next().unwrap_or(path).to_string();
sharded_dirs
.entry(top.clone())
.or_default()
.push(path.to_string());
*dir_sizes.entry(top).or_default() += sz;
}
}
}
}
}
}
}
let is_sharded = !sharded_dirs.is_empty();
let chosen_variant = variant
.as_deref()
.map(|v| v.trim())
.filter(|v| !v.is_empty());
if is_sharded {
let target_dir = if let Some(v) = chosen_variant {
if sharded_dirs.contains_key(v) {
v.to_string()
} else {
sharded_dirs.keys().next().cloned().unwrap_or_default()
}
} else {
let mut keys: Vec<String> = sharded_dirs.keys().cloned().collect();
keys.sort();
keys.into_iter().next().unwrap_or_default()
};
let files = sharded_dirs.get(&target_dir).cloned().unwrap_or_default();
if files.is_empty() {
store.push_line(&id, format!("✗ variant '{target_dir}' not found"));
store.finish(&id, false);
store.drop_cancel(&id);
return;
}
let total_sz = dir_sizes.get(&target_dir).copied().unwrap_or(0);
let q = if target_dir.contains("q2tp") {
"Q2TP"
} else if target_dir.contains("q4tp") {
"Q4TP"
} else {
"READY_CMF"
};
store.update_quant(&id, q.to_string());
store.push_line(
&id,
format!(
"→ downloading {} ({} files, {}) from HuggingFace...",
target_dir,
files.len(),
format_bytes(total_sz)
),
);
store.set_total(&id, total_sz);
store.set_progress(&id, 0.001, "downloading".into());
let out_dir = std::path::Path::new(&output_abs);
cleanup_output(&output_abs);
let _ = std::fs::create_dir_all(out_dir);
let _poller = spawn_size_poller(store.clone(), id.clone(), output_abs.clone());
for (idx, rel) in files.iter().enumerate() {
let filename = rel.rsplit('/').next().unwrap_or(rel);
let dest = out_dir.join(filename);
let url = format!("https://huggingface.co/{repo}/resolve/main/{rel}");
store.push_line(&id, format!(" [{}/{}] {filename}", idx + 1, files.len()));
let mut cmd = tokio::process::Command::new("curl");
cmd.arg("-f")
.arg("-L")
.arg("-s")
.arg("-S")
.arg("-o")
.arg(&dest)
.arg(&url);
if let Some(t) = &hf_token {
cmd.arg("-H").arg(format!("Authorization: Bearer {t}"));
}
cmd.stdout(Stdio::null());
cmd.stderr(Stdio::piped());
let mut child = match cmd.spawn() {
Ok(c) => c,
Err(e) => {
store.push_line(&id, format!("✗ failed {filename}: {e}"));
cleanup_output(&output_abs);
store.finish(&id, false);
store.drop_cancel(&id);
return;
}
};
let t_err = child.stderr.take().map(|s| {
let st = store.clone();
let jid = id.clone();
tokio::spawn(async move {
let mut lines = BufReader::new(s).lines();
while let Ok(Some(l)) = lines.next_line().await {
let l = l.trim().to_string();
if !l.is_empty() {
st.push_line(&jid, l);
}
}
})
});
let status = tokio::select! {
r = child.wait() => {
if let Some(t) = t_err { t.abort(); }
r.map_err(|e| e.to_string())
}
_ = cancel.notified() => {
let _ = child.start_kill();
let _ = child.wait().await;
if let Some(t) = t_err { t.abort(); }
cleanup_output(&output_abs);
store.push_line(&id, "✗ cancelled by user — partial output removed".into());
store.set_state(&id, "cancelled");
store.drop_cancel(&id);
return;
}
};
match status {
Ok(s) if s.success() => {
let on_disk = path_size(out_dir);
if total_sz > 0 {
store.set_bytes(&id, on_disk);
} else {
let frac = (idx + 1) as f32 / files.len() as f32;
store.set_progress(&id, frac, "downloading".into());
}
}
Ok(s) => {
store.push_line(&id, format!("✗ failed {filename}: exit {:?}", s.code()));
cleanup_output(&output_abs);
store.finish(&id, false);
store.drop_cancel(&id);
return;
}
Err(e) => {
store.push_line(&id, format!("✗ failed {filename}: {e}"));
cleanup_output(&output_abs);
store.finish(&id, false);
store.drop_cancel(&id);
return;
}
}
}
store.push_line(&id, "✓ done".into());
store.finish(&id, true);
store.drop_cancel(&id);
return;
}
let (cmf_path, cmf_size) = if !cmf_files.is_empty() {
if let Some(v) = chosen_variant {
cmf_files
.iter()
.find(|(p, _)| p.contains(v))
.cloned()
.unwrap_or_else(|| cmf_files[0].clone())
} else {
cmf_files[0].clone()
}
} else {
let repo_name = repo.rsplit('/').next().unwrap_or(&repo);
(format!("{repo_name}.cmf"), 0)
};
let path_lower = cmf_path.to_ascii_lowercase();
let detected_quant =
if path_lower.contains("-q1t.") || repo.to_ascii_lowercase().contains("2bit") {
"Q1T".to_string()
} else if path_lower.contains("-q1.") || repo.to_ascii_lowercase().contains("cmf") {
"Q1".to_string()
} else if path_lower.contains("-q4.") {
"Q4".to_string()
} else if path_lower.contains("-q8.") {
"Q8".to_string()
} else {
"READY_CMF".to_string()
};
store.update_quant(&id, detected_quant);
let download_url = format!("https://huggingface.co/{repo}/resolve/main/{cmf_path}");
let total = if cmf_size > 0 {
cmf_size
} else {
head_content_length(&download_url, hf_token.as_deref())
.await
.unwrap_or(0)
};
store.push_line(&id, {
let sz = if total > 0 {
format!(" ({})", format_bytes(total))
} else {
String::new()
};
format!("→ downloading {cmf_path}{sz} from HuggingFace...")
});
store.set_total(&id, total);
store.set_progress(&id, 0.001, "downloading".into());
cleanup_output(&output_abs);
let _poller = spawn_size_poller(store.clone(), id.clone(), output_abs.clone());
let mut cmd = tokio::process::Command::new("curl");
cmd.arg("-f")
.arg("-L")
.arg("-s")
.arg("-S")
.arg("-o")
.arg(&output_abs);
if let Some(t) = &hf_token {
cmd.arg("-H").arg(format!("Authorization: Bearer {t}"));
}
cmd.arg(&download_url);
cmd.stderr(std::process::Stdio::piped());
cmd.stdout(std::process::Stdio::null());
let mut child = match cmd.spawn() {
Ok(c) => c,
Err(e) => {
store.push_line(&id, format!("✗ failed to start curl download: {e}"));
store.finish(&id, false);
store.drop_cancel(&id);
return;
}
};
let stderr = child.stderr.take();
let mut t_err = None;
if let Some(err_stream) = stderr {
let store_clone = store.clone();
let id_clone = id.clone();
t_err = Some(tokio::spawn(async move {
let mut lines = BufReader::new(err_stream).lines();
while let Ok(Some(l)) = lines.next_line().await {
let l = l.trim().to_string();
if !l.is_empty() {
store_clone.push_line(&id_clone, l);
}
}
}));
}
tokio::select! {
status_res = child.wait() => {
if let Some(t) = t_err { t.abort(); }
match status_res {
Ok(status) if status.success() => {
store.push_line(&id, "✓ done".into());
store.finish(&id, true);
}
Ok(status) => {
cleanup_output(&output_abs);
let hint = match status.code() {
Some(23) => " (write error — is the models dir writable?)",
Some(22) => " (HTTP error — repo/file not found or auth required)",
_ => "",
};
store.push_line(&id, format!("✗ download failed: exit code {:?}{hint}", status.code()));
store.finish(&id, false);
}
Err(e) => {
cleanup_output(&output_abs);
store.push_line(&id, format!("✗ download process error: {e}"));
store.finish(&id, false);
}
}
}
_ = cancel.notified() => {
let _ = child.start_kill();
let _ = child.wait().await;
if let Some(t) = t_err { t.abort(); }
cleanup_output(&output_abs);
store.push_line(&id, "✗ cancelled by user — partial output removed".into());
store.set_state(&id, "cancelled");
}
}
store.drop_cancel(&id);
}
pub fn start_import(store: Arc<JobStore>, cfg: &CmfCfg, p: ImportParams) -> Result<String, String> {
if p.repo.trim().is_empty() {
return Err("empty repo id".into());
}
let use_python = p.linear_core.is_some() || p.vbit_shape.is_some() || p.mean_bits.is_some();
if use_python && !std::path::Path::new(&cfg.converter).exists() {
return Err(format!(
"advanced options need the Python converter, not found: {}",
cfg.converter
));
}
let o1 =
p.o1.as_deref()
.map(str::trim)
.filter(|s| !s.is_empty() && *s != "off")
.map(str::to_string);
if o1.is_some() {
if use_python {
return Err(
"O(1) attention is supported by the native converter only — remove the \
linear-core / v-bit shape / mean-bits options to use it"
.into(),
);
}
if p.repo.to_ascii_lowercase().contains("gguf") {
return Err("O(1) attention is not supported for GGUF imports yet".into());
}
let ver = crate::cmf_runtime::installed_version(&cfg.cortiq_bin);
if let Some(v) = &ver {
if crate::cmf_runtime::version_lt(v, "0.2.0") {
return Err(format!(
"O(1) attention needs cortiq ≥ 0.2.0 (installed: {v}) — update the runtime \
in Settings → Local models"
));
}
}
}
std::fs::create_dir_all(&cfg.models_dir).map_err(|e| {
format!(
"cannot create models dir {}: {e} — check that the gateway has write \
access (in Docker, mount a writable volume at the models path)",
cfg.models_dir
)
})?;
let mut base = if p.name.trim().is_empty() {
sanitize(p.repo.rsplit('/').next().unwrap_or(&p.repo))
} else {
sanitize(&p.name)
};
if let Some(v) = p
.variant
.as_deref()
.map(|s| s.trim())
.filter(|s| !s.is_empty())
{
if v.starts_with("parts-") {
let vs = sanitize(v);
if !base.ends_with(&vs) {
base = format!("{base}-{vs}");
}
}
}
let output = std::path::Path::new(&cfg.models_dir)
.join(format!("{base}.cmf"))
.to_string_lossy()
.to_string();
let output_abs = std::fs::canonicalize(&cfg.models_dir)
.map(|d| d.join(format!("{base}.cmf")).to_string_lossy().to_string())
.unwrap_or_else(|_| output.clone());
let id = gen_id(&format!(
"{}::{}",
p.repo,
p.variant.as_deref().unwrap_or("")
));
if let Some(existing) = store.get(&id) {
if existing.state == "running" {
return Ok(id);
}
}
let is_cmf = is_cmf_repo(&p.repo);
let quant_display = if is_cmf {
let r = p.repo.to_lowercase();
if r.contains("2bit") || r.contains("q1t") {
"Q1T".to_string()
} else if r.contains("1.7bcmf")
|| r.contains("27bcmf")
|| r.contains("cmf")
|| p.quant.is_empty()
|| p.quant == "auto"
|| p.quant == "Q8_2F"
{
"Q1".to_string()
} else {
p.quant.clone()
}
} else if p.quant.is_empty() || p.quant == "auto" {
"q8".to_string()
} else {
p.quant.clone()
};
store.insert(Job {
id: id.clone(),
repo: p.repo.clone(),
quant: quant_display.clone(),
output: output_abs.clone(),
name: base.clone(),
variant: p
.variant
.as_deref()
.map(str::trim)
.filter(|s| !s.is_empty())
.map(str::to_string),
state: "running".into(),
log: if is_cmf {
vec![format!(
"→ downloading ready .cmf model {} ({})",
p.repo, quant_display
)]
} else {
vec![format!("→ converting {} to {} ({})", p.repo, base, p.quant)]
},
started: now(),
finished: None,
size_bytes: None,
progress: Some(0.0),
phase: Some("starting".into()),
total_bytes: None,
done_bytes: None,
});
let cancel = Arc::new(Notify::new());
store.set_cancel(&id, cancel.clone());
let hf_token = if cfg.hf_token_env.is_empty() {
None
} else {
std::env::var(&cfg.hf_token_env).ok()
};
if is_cmf && !use_python {
let ret_id = id.clone();
let variant = p.variant.clone();
tokio::spawn(async move {
download_cmf_repo(p.repo, output_abs, hf_token, store, id, cancel, variant).await;
});
return Ok(ret_id);
}
let (program, args, workdir): (String, Vec<String>, std::path::PathBuf) = if use_python {
let conv = std::path::Path::new(&cfg.converter);
let mut a = vec![
std::fs::canonicalize(conv)
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_else(|_| cfg.converter.clone()),
"--model".into(),
p.repo.clone(),
"--quant".into(),
p.quant.clone(),
"--output".into(),
output_abs.clone(),
];
if let Some(lc) = &p.linear_core {
a.push("--linear-core".into());
a.push(lc.clone());
}
if let Some(n) = p.nphase {
a.push("--nphase".into());
a.push(n.to_string());
}
if let Some(vs) = &p.vbit_shape {
a.push("--vbit-shape".into());
a.push(vs.clone());
}
if let Some(mb) = p.mean_bits {
a.push("--mean-bits".into());
a.push(mb.to_string());
}
if let Some(g) = p.shard_max_gb {
a.push("--shard-max-gb".into());
a.push(g.to_string());
}
if p.skip_mtp {
a.push("--skip-mtp".into());
}
let wd = conv
.parent()
.map(|d| d.to_path_buf())
.unwrap_or_else(|| std::path::PathBuf::from("."));
(cfg.python_bin.clone(), a, wd)
} else if p.repo.to_ascii_lowercase().contains("gguf") {
let mut a = vec![
"import-gguf".into(),
p.repo.clone(),
"--quant".into(),
map_quant(&p.quant).into(),
"--output".into(),
output_abs.clone(),
];
if let Some(t) = &hf_token {
a.push("--hf-token".into());
a.push(t.clone());
}
(
cfg.cortiq_bin.clone(),
a,
std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")),
)
} else {
let mut a = vec![
"convert".into(),
"--model".into(),
p.repo.clone(),
"--quant".into(),
map_quant(&p.quant).into(),
"--output".into(),
output_abs.clone(),
];
if let Some(t) = &hf_token {
a.push("--hf-token".into());
a.push(t.clone());
}
if let Some(spec) = &o1 {
a.push("--o1".into());
a.push(spec.clone());
if let Some(m) = p.o1_m {
a.push("--o1-m".into());
a.push(m.to_string());
}
if let Some(w) = p.o1_window {
a.push("--o1-window".into());
a.push(w.to_string());
}
if let Some(s) = p.o1_sink {
a.push("--o1-sink".into());
a.push(s.to_string());
}
}
(
cfg.cortiq_bin.clone(),
a,
std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")),
)
};
let ret_id = id.clone();
let output_for_task = output_abs.clone();
tokio::spawn(async move {
let mut cmd = tokio::process::Command::new(&program);
cmd.args(&args)
.current_dir(&workdir)
.env("PYTHONUNBUFFERED", "1")
.stdout(Stdio::piped())
.stderr(Stdio::piped());
if use_python {
if let Some(tok) = hf_token {
cmd.env("HF_TOKEN", &tok).env("HUGGING_FACE_HUB_TOKEN", tok);
}
}
let mut child = match cmd.spawn() {
Ok(c) => c,
Err(e) => {
store.push_line(&id, format!("✗ spawn failed: {e}"));
store.finish(&id, false);
store.drop_cancel(&id);
return;
}
};
let stdout = child.stdout.take();
let stderr = child.stderr.take();
let s1 = store.clone();
let id1 = id.clone();
let t_out = tokio::spawn(async move {
if let Some(o) = stdout {
let mut lines = BufReader::new(o).lines();
while let Ok(Some(l)) = lines.next_line().await {
if let Some((frac, phase)) = parse_progress(&l) {
s1.set_progress(&id1, frac, phase);
} else {
s1.push_line(&id1, l);
}
}
}
});
let s2 = store.clone();
let id2 = id.clone();
let t_err = tokio::spawn(async move {
if let Some(e) = stderr {
let mut lines = BufReader::new(e).lines();
while let Ok(Some(l)) = lines.next_line().await {
s2.push_line(&id2, l);
}
}
});
tokio::select! {
status = child.wait() => {
let _ = tokio::join!(t_out, t_err);
let ok = status.map(|s| s.success()).unwrap_or(false);
if !ok {
cleanup_output(&output_for_task); }
store.push_line(
&id,
if ok { "✓ done".into() } else { "✗ converter exited with error".into() },
);
store.finish(&id, ok);
}
_ = cancel.notified() => {
let _ = child.start_kill();
let _ = child.wait().await;
t_out.abort();
t_err.abort();
cleanup_output(&output_for_task);
store.push_line(&id, "✗ cancelled by user — partial output removed".into());
store.set_state(&id, "cancelled");
}
}
store.drop_cancel(&id);
});
Ok(ret_id)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_map_quant_variants() {
assert_eq!(map_quant("Q8_2F"), "q8_2f");
assert_eq!(map_quant("q82f"), "q8_2f");
assert_eq!(map_quant("Q8_ROW"), "q8");
assert_eq!(map_quant("Q4_BLOCK"), "q4");
assert_eq!(map_quant("Q4_TILED"), "q4t");
assert_eq!(map_quant("q4t"), "q4t");
assert_eq!(map_quant("vbit"), "vbit");
assert_eq!(map_quant("q1"), "q1");
assert_eq!(map_quant("Q1P"), "q1p");
assert_eq!(map_quant("q1s"), "q1s");
assert_eq!(map_quant("Q1T"), "q1t");
assert_eq!(map_quant("F16"), "f16");
}
#[test]
fn test_is_cmf_repo_check() {
assert!(is_cmf_repo("infosave/Bonsai-8B_2bit_cmf"));
assert!(is_cmf_repo("infosave/Bonsai-1.7Bcmf"));
assert!(is_cmf_repo("user/model.cmf"));
assert!(!is_cmf_repo("Qwen/Qwen2.5-0.5B-Instruct"));
}
}