From 59e2eb321557fe826c4f2474890f400646a74c20 Mon Sep 17 00:00:00 2001 From: infosave2007 Date: Tue, 21 Jul 2026 22:51:58 +0300 Subject: [PATCH] =?UTF-8?q?chore(release):=20v0.2.25=20=E2=80=94=20direct?= =?UTF-8?q?=20stream=20download=20for=20ready=20CMF=20repos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 6 +- Cargo.lock | 2 +- Cargo.toml | 2 +- src/import.rs | 171 ++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 178 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d5f5a1b..c1168f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,11 @@ All notable changes to this project are documented here. The format is based on [Keep a Changelog](https://keepachangelog.com/). -## [0.2.24] - 2026-07-21 +## [0.2.25] - 2026-07-21 + +### Fixed +- **Direct Ready CMF Download**: Importing pre-converted `.cmf` model repositories (e.g. `infosave/Bonsai-8B_2bit_cmf`) directly streams the ready `.cmf` file from Hugging Face instead of attempting a safetensors conversion without `config.json`. + ### Added - **New Quantization Schemes**: Added support for `Q4_TILED` (q4t), `Q1T` (1.58-bit ternary), `Q1S` (1-bit sparse), `Q1P` (1-bit PTQ error diffusion), and `Q1` (pure binary) in converter backend and web admin UI. diff --git a/Cargo.lock b/Cargo.lock index 667bae8..e46178e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -285,7 +285,7 @@ checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "cortiq-gateway" -version = "0.2.24" +version = "0.2.25" dependencies = [ "anyhow", "arc-swap", diff --git a/Cargo.toml b/Cargo.toml index 9db6454..c89837b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cortiq-gateway" -version = "0.2.24" +version = "0.2.25" edition = "2021" rust-version = "1.75" license = "Apache-2.0" diff --git a/src/import.rs b/src/import.rs index d026072..92f7466 100644 --- a/src/import.rs +++ b/src/import.rs @@ -273,6 +273,161 @@ fn map_quant(q: &str) -> &'static str { } } +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, + store: Arc, + id: String, + cancel: Arc, +) { + store.push_line(&id, format!("→ downloading ready .cmf model from {}", repo)); + store.set_progress(&id, 0.0, "listing files".into()); + + let client = match reqwest::Client::builder() + .timeout(std::time::Duration::from_secs(30)) + .build() + { + Ok(c) => c, + Err(e) => { + store.push_line(&id, format!("✗ failed to build HTTP client: {e}")); + store.finish(&id, false); + store.drop_cancel(&id); + return; + } + }; + + let tree_url = format!("https://huggingface.co/api/models/{repo}/tree/main?recursive=true"); + 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_rel_path = None; + if let Ok(resp) = req.send().await { + if resp.status().is_success() { + if let Ok(json) = resp.json::().await { + if let Some(arr) = json.as_array() { + for item in arr { + if let Some(path) = item.get("path").and_then(|p| p.as_str()) { + if path.to_ascii_lowercase().ends_with(".cmf") { + cmf_rel_path = Some(path.to_string()); + break; + } + } + } + } + } + } + } + + let cmf_path = match cmf_rel_path { + Some(p) => p, + None => { + let repo_name = repo.split('/').last().unwrap_or(&repo); + format!("{repo_name}.cmf") + } + }; + + store.push_line(&id, format!("→ downloading {cmf_path} from HuggingFace...")); + store.set_progress(&id, 0.05, "downloading".into()); + + let download_url = format!("https://huggingface.co/{repo}/resolve/main/{cmf_path}"); + let mut req = client + .get(&download_url) + .header("User-Agent", "cortiq-gateway"); + if let Some(t) = &hf_token { + req = req.bearer_auth(t); + } + + let mut resp = match req.send().await { + Ok(r) if r.status().is_success() => r, + Ok(r) => { + store.push_line(&id, format!("✗ HF download error: HTTP {}", r.status())); + store.finish(&id, false); + store.drop_cancel(&id); + return; + } + Err(e) => { + store.push_line(&id, format!("✗ network error: {e}")); + store.finish(&id, false); + store.drop_cancel(&id); + return; + } + }; + + let total_bytes = resp.content_length(); + let mut file = match tokio::fs::File::create(&output_abs).await { + Ok(f) => f, + Err(e) => { + store.push_line(&id, format!("✗ failed to create file {output_abs}: {e}")); + store.finish(&id, false); + store.drop_cancel(&id); + return; + } + }; + + use tokio::io::AsyncWriteExt; + let mut downloaded: u64 = 0; + + loop { + tokio::select! { + _ = cancel.notified() => { + let _ = tokio::fs::remove_file(&output_abs).await; + store.push_line(&id, "✗ cancelled by user — partial output removed".into()); + store.set_state(&id, "cancelled"); + store.drop_cancel(&id); + return; + } + chunk_opt = resp.chunk() => { + match chunk_opt { + Ok(Some(chunk)) => { + if let Err(e) = file.write_all(&chunk).await { + store.push_line(&id, format!("✗ write error: {e}")); + cleanup_output(&output_abs); + store.finish(&id, false); + store.drop_cancel(&id); + return; + } + downloaded += chunk.len() as u64; + if let Some(total) = total_bytes { + if total > 0 { + let frac = (downloaded as f32 / total as f32).clamp(0.0, 1.0); + store.set_progress(&id, frac, "downloading".into()); + } + } + } + Ok(None) => break, + Err(e) => { + store.push_line(&id, format!("✗ download stream error: {e}")); + cleanup_output(&output_abs); + store.finish(&id, false); + store.drop_cancel(&id); + return; + } + } + } + } + } + + if let Err(e) = file.flush().await { + store.push_line(&id, format!("✗ flush error: {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); +} + /// into the job log. Fails fast if the converter script is missing. pub fn start_import(store: Arc, cfg: &CmfCfg, p: ImportParams) -> Result { if p.repo.trim().is_empty() { @@ -353,6 +508,14 @@ pub fn start_import(store: Arc, cfg: &CmfCfg, p: ImportParams) -> Resu std::env::var(&cfg.hf_token_env).ok() }; + if is_cmf_repo(&p.repo) && !use_python { + let ret_id = id.clone(); + tokio::spawn(async move { + download_cmf_repo(p.repo, output_abs, hf_token, store, id, cancel).await; + }); + return Ok(ret_id); + } + // Program + args: `cortiq convert` by default, or the Python converter for // advanced options. Both stream `@PROGRESS ` markers into the log. let (program, args, workdir): (String, Vec, std::path::PathBuf) = if use_python { @@ -555,4 +718,12 @@ mod tests { 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")); + } }