Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b867a6276e | ||
|
|
c18ddb5475 | ||
|
|
ef9b3738fd | ||
|
|
924ec403bd | ||
|
|
0c91f09113 |
+22
-1
@@ -3,7 +3,28 @@
|
|||||||
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.24] - 2026-07-21
|
## [0.2.29] - 2026-07-21
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- **Duplicate Local Model Registration**: Fixed `duplicate managed model id` conflict in `register_import` by cleanly routing managed CMF models directly into `[[cmf.servers]]` with automatic free port allocation without pushing duplicate entries to `[[models]]`.
|
||||||
|
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- **Quantization Badge & Title for Pre-Converted Models**: Automatically detect quantization (`Q1`, `Q1T`) for ready-to-run `.cmf` models (e.g. `infosave/Bonsai-27Bcmf`) from repo name and downloaded filename, resolving default `Q8_2F` badge fallback. Updated section title to "Загрузки / Конвертации" ("Downloads & Conversions").
|
||||||
|
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- **Automatic CMF Local Server Lifecycle**: Registering an imported `.cmf` model automatically adds it to `[[cmf.servers]]` and reloads the runtime (`cmf_runtime::manage`), automatically spawning `cortiq serve` on port 8090 so chat completion requests route seamlessly without `upstream unavailable` errors.
|
||||||
|
|
||||||
|
|
||||||
|
### 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
|
||||||
|
- **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
|
### 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.
|
- **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.
|
||||||
|
|||||||
Generated
+1
-1
@@ -285,7 +285,7 @@ checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cortiq-gateway"
|
name = "cortiq-gateway"
|
||||||
version = "0.2.24"
|
version = "0.2.29"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"arc-swap",
|
"arc-swap",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "cortiq-gateway"
|
name = "cortiq-gateway"
|
||||||
version = "0.2.24"
|
version = "0.2.29"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.75"
|
rust-version = "1.75"
|
||||||
license = "Apache-2.0"
|
license = "Apache-2.0"
|
||||||
|
|||||||
+28
-2
@@ -1304,9 +1304,34 @@ async fn register_import(State(state): State<SharedState>, Path(job): Path<Strin
|
|||||||
.unwrap_or_else(|| "cmf-local".into());
|
.unwrap_or_else(|| "cmf-local".into());
|
||||||
let id = format!("cmf-{base}");
|
let id = format!("cmf-{base}");
|
||||||
let mut cfg = current_cfg(&state);
|
let mut cfg = current_cfg(&state);
|
||||||
if cfg.models.iter().any(|m| m.id == id) {
|
|
||||||
return Err(ApiError::bad(format!("model '{id}' already registered")));
|
let is_in_models = cfg.models.iter().any(|m| m.id == id);
|
||||||
|
let is_in_servers = cfg
|
||||||
|
.cmf
|
||||||
|
.servers
|
||||||
|
.iter()
|
||||||
|
.any(|s| s.id == id || s.model == j.output);
|
||||||
|
|
||||||
|
if is_in_models || is_in_servers {
|
||||||
|
return Err(ApiError::bad(format!("model '{id}' is already registered")));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if cfg.cmf.manage_server {
|
||||||
|
let host = if cfg.cmf.local_host.trim().is_empty() {
|
||||||
|
"127.0.0.1"
|
||||||
|
} else {
|
||||||
|
&cfg.cmf.local_host
|
||||||
|
};
|
||||||
|
let base_port = cfg.cmf.servers.iter().map(|s| s.port).max().unwrap_or(8089);
|
||||||
|
let port = next_free_port(host, base_port).unwrap_or_else(|| base_port + 1);
|
||||||
|
cfg.cmf.servers.push(crate::config::CmfServer {
|
||||||
|
id: id.clone(),
|
||||||
|
model: j.output.clone(),
|
||||||
|
port,
|
||||||
|
threads: cfg.cmf.threads,
|
||||||
|
gpu: cfg.cmf.gpu,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
cfg.models.push(crate::config::ModelCfg {
|
cfg.models.push(crate::config::ModelCfg {
|
||||||
id: id.clone(),
|
id: id.clone(),
|
||||||
provider: "openai".into(),
|
provider: "openai".into(),
|
||||||
@@ -1319,6 +1344,7 @@ async fn register_import(State(state): State<SharedState>, Path(job): Path<Strin
|
|||||||
api_key_env: None,
|
api_key_env: None,
|
||||||
caps: Vec::new(),
|
caps: Vec::new(),
|
||||||
});
|
});
|
||||||
|
}
|
||||||
state.reload(cfg)?;
|
state.reload(cfg)?;
|
||||||
ok(json!({ "ok": true, "model_id": id }))
|
ok(json!({ "ok": true, "model_id": id }))
|
||||||
}
|
}
|
||||||
|
|||||||
+197
-2
@@ -160,6 +160,14 @@ impl JobStore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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) {
|
fn set_state(&self, id: &str, state: &str) {
|
||||||
if let Some(j) = self.jobs.lock().unwrap().get_mut(id) {
|
if let Some(j) = self.jobs.lock().unwrap().get_mut(id) {
|
||||||
j.state = state.into();
|
j.state = state.into();
|
||||||
@@ -273,6 +281,153 @@ 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<String>,
|
||||||
|
store: Arc<JobStore>,
|
||||||
|
id: String,
|
||||||
|
cancel: Arc<Notify>,
|
||||||
|
) {
|
||||||
|
store.set_progress(&id, 0.0, "listing files".into());
|
||||||
|
|
||||||
|
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");
|
||||||
|
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::<serde_json::Value>().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")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
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 cmd = tokio::process::Command::new("curl");
|
||||||
|
cmd.arg("-f").arg("-L").arg("-#").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 {
|
||||||
|
use tokio::io::AsyncReadExt;
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
/// into the job log. Fails fast if the converter script is missing.
|
/// into the job log. Fails fast if the converter script is missing.
|
||||||
pub fn start_import(store: Arc<JobStore>, cfg: &CmfCfg, p: ImportParams) -> Result<String, String> {
|
pub fn start_import(store: Arc<JobStore>, cfg: &CmfCfg, p: ImportParams) -> Result<String, String> {
|
||||||
if p.repo.trim().is_empty() {
|
if p.repo.trim().is_empty() {
|
||||||
@@ -331,13 +486,37 @@ 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 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") {
|
||||||
|
"Q1".to_string()
|
||||||
|
} else if 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 {
|
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,
|
||||||
@@ -353,6 +532,14 @@ 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 && !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
|
// Program + args: `cortiq convert` by default, or the Python converter for
|
||||||
// advanced options. Both stream `@PROGRESS <frac>` markers into the log.
|
// advanced options. Both stream `@PROGRESS <frac>` markers into the log.
|
||||||
let (program, args, workdir): (String, Vec<String>, std::path::PathBuf) = if use_python {
|
let (program, args, workdir): (String, Vec<String>, std::path::PathBuf) = if use_python {
|
||||||
@@ -555,4 +742,12 @@ mod tests {
|
|||||||
assert_eq!(map_quant("Q1T"), "q1t");
|
assert_eq!(map_quant("Q1T"), "q1t");
|
||||||
assert_eq!(map_quant("F16"), "f16");
|
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"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -145,6 +145,14 @@ impl AppState {
|
|||||||
let live = Live::build(new_cfg.clone(), &self.secrets, self.router_status.clone())?;
|
let live = Live::build(new_cfg.clone(), &self.secrets, self.router_status.clone())?;
|
||||||
new_cfg.save(&self.config_path)?;
|
new_cfg.save(&self.config_path)?;
|
||||||
self.live.store(Arc::new(live));
|
self.live.store(Arc::new(live));
|
||||||
|
|
||||||
|
let cmf_rt = self.cmf.clone();
|
||||||
|
let cmf_cfg = new_cfg.cmf.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
cmf_rt.stop().await;
|
||||||
|
crate::cmf_runtime::manage(cmf_rt, cmf_cfg).await;
|
||||||
|
});
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Vendored
+1
-1
@@ -374,7 +374,7 @@ export default {
|
|||||||
"import.failed": "Conversion failed — see the log.",
|
"import.failed": "Conversion failed — see the log.",
|
||||||
"import.register": "Register as local model",
|
"import.register": "Register as local model",
|
||||||
"import.registered": "Registered as '{id}'. Run cortiq-server to serve it.",
|
"import.registered": "Registered as '{id}'. Run cortiq-server to serve it.",
|
||||||
"import.jobs": "Conversions",
|
"import.jobs": "Downloads & Conversions",
|
||||||
"import.cancel": "Cancel",
|
"import.cancel": "Cancel",
|
||||||
"import.cancelling": "Cancelling — cleaning up partial output…",
|
"import.cancelling": "Cancelling — cleaning up partial output…",
|
||||||
"import.delete": "Delete",
|
"import.delete": "Delete",
|
||||||
|
|||||||
Vendored
+1
-1
@@ -373,7 +373,7 @@ export default {
|
|||||||
"import.failed": "Конвертация не удалась — смотрите лог.",
|
"import.failed": "Конвертация не удалась — смотрите лог.",
|
||||||
"import.register": "Подключить как локальную модель",
|
"import.register": "Подключить как локальную модель",
|
||||||
"import.registered": "Подключено как '{id}'. Запустите cortiq-server для обслуживания.",
|
"import.registered": "Подключено как '{id}'. Запустите cortiq-server для обслуживания.",
|
||||||
"import.jobs": "Конвертации",
|
"import.jobs": "Загрузки / Конвертации",
|
||||||
"import.cancel": "Отменить",
|
"import.cancel": "Отменить",
|
||||||
"import.cancelling": "Отмена — удаляю недописанный файл…",
|
"import.cancelling": "Отмена — удаляю недописанный файл…",
|
||||||
"import.delete": "Удалить",
|
"import.delete": "Удалить",
|
||||||
|
|||||||
Vendored
+4
-4
@@ -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");
|
||||||
|
|||||||
Reference in New Issue
Block a user