feat: drag-and-drop model import and context size configuration

This commit is contained in:
Evandro Fonseca Junior committed 2026-03-26 21:38:58 -03:00
1 parent 18d8b03074
commit d1d77b83ef
4 files changed
+306 -33

No files matched your search

+49 -7
View File
@@ -6,8 +6,6 @@ use tauri::{Manager, State};
pub struct ServerState(pub Mutex<Option<Child>>);
/// Query GPU info via nvidia-smi.
/// Returns (gpu_name, vram_mib, recommended_layers).
/// recommended_layers is estimated as vram_mib / 80 (rough heuristic for 4-bit quant).
#[tauri::command]
fn query_gpu() -> Result<GpuInfo, String> {
let output = Command::new("nvidia-smi")
@@ -28,7 +26,6 @@ fn query_gpu() -> Result<GpuInfo, String> {
return Err("No GPU found".into());
}
// Format: "GPU Name, 8192"
let parts: Vec<&str> = line.splitn(2, ',').collect();
if parts.len() < 2 {
return Err(format!("Unexpected nvidia-smi output: {line}"));
@@ -36,9 +33,6 @@ fn query_gpu() -> Result<GpuInfo, String> {
let name = parts[0].trim().to_string();
let vram_mib: u32 = parts[1].trim().parse().unwrap_or(0);
// Heuristic: ~80 MiB per layer for Q4/Q6 models (conservative)
// Reserve 1 GiB for OS/other usage
let usable = vram_mib.saturating_sub(1024);
let recommended_layers = (usable / 80).min(200) as i32;
@@ -56,12 +50,14 @@ pub struct GpuInfo {
pub recommended_layers: i32,
}
/// Start the inference server with full configuration.
#[tauri::command]
fn start_server(
state: State<ServerState>,
model_path: String,
addr: String,
gpu_layers: i32,
ctx_size: u32,
) -> Result<String, String> {
let mut guard = state.0.lock().map_err(|e| e.to_string())?;
@@ -79,6 +75,7 @@ fn start_server(
.env("LOCALLLM_MODEL", &model_path)
.env("LOCALLLM_ADDR", &addr)
.env("LOCALLLM_GPU_LAYERS", gpu_layers.to_string())
.env("LOCALLLM_CTX", ctx_size.to_string())
.spawn()
.map_err(|e| format!("Failed to start server: {e}"))?;
@@ -116,7 +113,6 @@ fn server_status(state: State<ServerState>) -> String {
}
/// Scan a directory for .gguf model files.
/// Returns a list of { name, path } objects.
#[tauri::command]
fn scan_models(dir: String) -> Result<Vec<ModelFile>, String> {
let path = Path::new(&dir);
@@ -154,6 +150,51 @@ fn scan_models(dir: String) -> Result<Vec<ModelFile>, String> {
Ok(models)
}
/// Copy a .gguf file into the models directory.
/// Returns the new ModelFile on success.
#[tauri::command]
fn import_model(src_path: String, dest_dir: String) -> Result<ModelFile, String> {
let src = Path::new(&src_path);
// Validate source
if !src.exists() {
return Err(format!("Source file not found: {src_path}"));
}
if !src.is_file() {
return Err("Source is not a file".into());
}
let ext = src.extension().unwrap_or_default();
if !ext.eq_ignore_ascii_case("gguf") {
return Err("Only .gguf files are supported".into());
}
// Ensure destination directory exists
let dest_dir_path = Path::new(&dest_dir);
if !dest_dir_path.exists() {
std::fs::create_dir_all(dest_dir_path)
.map_err(|e| format!("Failed to create models directory: {e}"))?;
}
let file_name = src.file_name().unwrap_or_default();
let dest_path = dest_dir_path.join(file_name);
// If already in the same directory, just return it
if src == dest_path {
return Ok(ModelFile {
name: file_name.to_string_lossy().to_string(),
path: dest_path.to_string_lossy().to_string(),
});
}
// Copy the file
std::fs::copy(src, &dest_path).map_err(|e| format!("Failed to copy model: {e}"))?;
Ok(ModelFile {
name: file_name.to_string_lossy().to_string(),
path: dest_path.to_string_lossy().to_string(),
})
}
#[derive(serde::Serialize, Clone)]
pub struct ModelFile {
pub name: String,
@@ -168,6 +209,7 @@ pub fn run() {
.invoke_handler(tauri::generate_handler![
query_gpu,
scan_models,
import_model,
start_server,
stop_server,
server_status