Compare commits

..
1 Commits
Author SHA1 Message Date
infosave2007 924ec403bd chore(release): v0.2.26 — robust multi-GB streaming downloads & ready CMF quants
Docker / publish (push) Canceled after 0s
Release / crates-io (push) Canceled after 0s
Release / release (macos-14, aarch64-apple-darwin) (push) Canceled after 0s
Release / release (ubuntu-latest, x86_64-unknown-linux-gnu) (push) Canceled after 0s
Release / release (windows-latest, x86_64-pc-windows-msvc) (push) Canceled after 0s
2026-07-21 23:08:02 +03:00
5 changed files with 94 additions and 95 deletions
+6 -1
View File
@@ -3,7 +3,12 @@
All notable changes to this project are documented here. All notable changes to this project are documented here.
The format is based on [Keep a Changelog](https://keepachangelog.com/). The format is based on [Keep a Changelog](https://keepachangelog.com/).
## [0.2.25] - 2026-07-21 ## [0.2.26] - 2026-07-21
### Fixed
- **Multi-GB Ready CMF Download Stream**: Replaced HTTP stream decoder with a streaming process downloader (`curl`) for pre-converted `.cmf` models to support multi-gigabyte Hugging Face LFS repositories without 30s/60s socket timeouts.
- **Ready CMF Quantization & UI Labels**: Set correct quantization badges (`Q1`, `Q1T`) for featured ready-to-run `.cmf` models and removed misleading "converting" log messages for direct downloads.
### Fixed ### 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`. - **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`.
Generated
+1 -1
View File
@@ -285,7 +285,7 @@ checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
[[package]] [[package]]
name = "cortiq-gateway" name = "cortiq-gateway"
version = "0.2.25" version = "0.2.26"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"arc-swap", "arc-swap",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "cortiq-gateway" name = "cortiq-gateway"
version = "0.2.25" version = "0.2.26"
edition = "2021" edition = "2021"
rust-version = "1.75" rust-version = "1.75"
license = "Apache-2.0" license = "Apache-2.0"
+83 -89
View File
@@ -286,23 +286,10 @@ async fn download_cmf_repo(
id: String, id: String,
cancel: Arc<Notify>, cancel: Arc<Notify>,
) { ) {
store.push_line(&id, format!("→ downloading ready .cmf model from {}", repo));
store.set_progress(&id, 0.0, "listing files".into()); 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 tree_url = format!("https://huggingface.co/api/models/{repo}/tree/main?recursive=true");
let client = reqwest::Client::new();
let mut req = client.get(&tree_url).header("User-Agent", "cortiq-gateway"); let mut req = client.get(&tree_url).header("User-Agent", "cortiq-gateway");
if let Some(t) = &hf_token { if let Some(t) = &hf_token {
req = req.bearer_auth(t); req = req.bearer_auth(t);
@@ -338,93 +325,83 @@ async fn download_cmf_repo(
store.set_progress(&id, 0.05, "downloading".into()); store.set_progress(&id, 0.05, "downloading".into());
let download_url = format!("https://huggingface.co/{repo}/resolve/main/{cmf_path}"); let download_url = format!("https://huggingface.co/{repo}/resolve/main/{cmf_path}");
let mut req = client
.get(&download_url) let mut cmd = tokio::process::Command::new("curl");
.header("User-Agent", "cortiq-gateway"); cmd.arg("-f").arg("-L").arg("-#").arg("-o").arg(&output_abs);
if let Some(t) = &hf_token { if let Some(t) = &hf_token {
req = req.bearer_auth(t); cmd.arg("-H").arg(format!("Authorization: Bearer {t}"));
} }
let mut resp = match req.send().await { cmd.arg(&download_url);
Ok(r) if r.status().is_success() => r, cmd.stderr(std::process::Stdio::piped());
Ok(r) => { cmd.stdout(std::process::Stdio::null());
store.push_line(&id, format!("✗ HF download error: HTTP {}", r.status()));
store.finish(&id, false); let mut child = match cmd.spawn() {
store.drop_cancel(&id); Ok(c) => c,
return;
}
Err(e) => { Err(e) => {
store.push_line(&id, format!("✗ network error: {e}")); store.push_line(&id, format!("✗ failed to start curl download: {e}"));
store.finish(&id, false); store.finish(&id, false);
store.drop_cancel(&id); store.drop_cancel(&id);
return; return;
} }
}; };
let total_bytes = resp.content_length(); let stderr = child.stderr.take();
let mut file = match tokio::fs::File::create(&output_abs).await { let mut t_err = None;
Ok(f) => f, if let Some(err_stream) = stderr {
Err(e) => { let store_clone = store.clone();
store.push_line(&id, format!("✗ failed to create file {output_abs}: {e}")); let id_clone = id.clone();
store.finish(&id, false); t_err = Some(tokio::spawn(async move {
store.drop_cancel(&id); use tokio::io::AsyncReadExt;
return; let mut reader = err_stream;
let mut buf = [0u8; 256];
while let Ok(n) = reader.read(&mut buf).await {
if n == 0 {
break;
}
let text = String::from_utf8_lossy(&buf[..n]);
for word in text.split_whitespace() {
if let Some(num_str) = word.strip_suffix('%') {
if let Ok(pct) = num_str.parse::<f32>() {
let frac = (pct / 100.0).clamp(0.0, 1.0);
store_clone.set_progress(&id_clone, frac, "downloading".into());
}
}
}
}
}));
} }
};
use tokio::io::AsyncWriteExt;
let mut downloaded: u64 = 0;
loop {
tokio::select! { tokio::select! {
_ = cancel.notified() => { status_res = child.wait() => {
let _ = tokio::fs::remove_file(&output_abs).await; if let Some(t) = t_err { t.abort(); }
store.push_line(&id, "✗ cancelled by user — partial output removed".into()); match status_res {
store.set_state(&id, "cancelled"); Ok(status) if status.success() => {
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.push_line(&id, "✓ done".into());
store.finish(&id, true); store.finish(&id, true);
}
Ok(status) => {
cleanup_output(&output_abs);
store.push_line(&id, format!("✗ download failed: exit code {:?}", 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); store.drop_cancel(&id);
} }
@@ -486,13 +463,30 @@ pub fn start_import(store: Arc<JobStore>, cfg: &CmfCfg, p: ImportParams) -> Resu
.unwrap_or_else(|_| output.clone()); .unwrap_or_else(|_| output.clone());
let id = gen_id(&p.repo); let id = gen_id(&p.repo);
let is_cmf = is_cmf_repo(&p.repo);
let quant_display = if p.quant.is_empty() || p.quant == "auto" {
if is_cmf {
"READY_CMF".to_string()
} else {
"q8".to_string()
}
} else {
p.quant.clone()
};
store.insert(Job { store.insert(Job {
id: id.clone(), id: id.clone(),
repo: p.repo.clone(), repo: p.repo.clone(),
quant: p.quant.clone(), quant: quant_display.clone(),
output: output_abs.clone(), output: output_abs.clone(),
state: "running".into(), state: "running".into(),
log: vec![format!("→ converting {} to {} ({})", p.repo, base, p.quant)], 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(), started: now(),
finished: None, finished: None,
size_bytes: None, size_bytes: None,
@@ -508,7 +502,7 @@ pub fn start_import(store: Arc<JobStore>, cfg: &CmfCfg, p: ImportParams) -> Resu
std::env::var(&cfg.hf_token_env).ok() std::env::var(&cfg.hf_token_env).ok()
}; };
if is_cmf_repo(&p.repo) && !use_python { if is_cmf && !use_python {
let ret_id = id.clone(); let ret_id = id.clone();
tokio::spawn(async move { tokio::spawn(async move {
download_cmf_repo(p.repo, output_abs, hf_token, store, id, cancel).await; download_cmf_repo(p.repo, output_abs, hf_token, store, id, cancel).await;
+4 -4
View File
@@ -21,9 +21,9 @@ const QUANTS = [
]; ];
const FEATURED_REPOS = [ const FEATURED_REPOS = [
{ id: "infosave/Bonsai-1.7Bcmf", label: "Bonsai 1.7B CMF", badge: "1.7B · Ready .cmf" }, { id: "infosave/Bonsai-1.7Bcmf", label: "Bonsai 1.7B CMF", badge: "1.7B · Q1", quant: "Q1" },
{ id: "infosave/Bonsai-8B_2bit_cmf", label: "Bonsai 8B 2-bit CMF", badge: "8B · Ready .cmf" }, { id: "infosave/Bonsai-8B_2bit_cmf", label: "Bonsai 8B 2-bit CMF", badge: "8B · Q1T", quant: "Q1T" },
{ id: "infosave/Bonsai-27Bcmf", label: "Bonsai 27B CMF", badge: "27B · Ready .cmf" }, { id: "infosave/Bonsai-27Bcmf", label: "Bonsai 27B CMF", badge: "27B · Q1", quant: "Q1" },
]; ];
@@ -390,7 +390,7 @@ export function renderImport() {
try { try {
await api.startImport({ await api.startImport({
repo: item.id, repo: item.id,
quant: "Q8_2F", quant: item.quant || "READY_CMF",
name: item.id.split("/").pop().toLowerCase(), name: item.id.split("/").pop().toLowerCase(),
}); });
toast(t("import.started"), "ok"); toast(t("import.started"), "ok");