1 //! Tauri command `chat_set_model`: switches the active on-device model.
2 //!
3 //! Updates the selected model, unloads any currently loaded model, and loads
4 //! the new one. Like `chat_send_message`, this returns `Ok(())` as soon as the
5 //! work is dispatched: switching can trigger a multi-gigabyte HuggingFace
6 //! download, so progress and completion are reported via the
7 //! `chat_status_changed` event (Loading → Ready, or Error) rather than a
8 //! blocking return that the WebView could GC out from under us.
9
10 use tauri::AppHandle;
11
12 #[cfg(any(target_os = "macos", target_os = "ios", target_os = "android"))]
13 use {
14 super::{
15 config_for_model_id, emit_chat_status, fmt_duration, sampling_config, CHAT_SYSTEM_PROMPT,
16 ENGINE, SELECTED_MODEL,
17 },
18 crate::constants::ChatStatus,
19 log::{error, info},
20 };
21
22 /// Switch Siti to the model identified by `model_id` (a HuggingFace repo id
23 /// from `chat_list_models`). The new model is loaded asynchronously; watch the
24 /// `chat_status_changed` event for `Loading` → `Ready`/`Error`.
25 #[cfg(any(target_os = "macos", target_os = "ios", target_os = "android"))]
26 #[tauri::command]
27 pub async fn chat_set_model(app: AppHandle, model_id: String) -> Result<(), String> {
28 // ── 1. Validate & resolve the requested model ────────────────────────
29 let config = config_for_model_id(&model_id)
30 .ok_or_else(|| format!("Unknown or unsupported model id: {model_id}"))?;
31 let display_name = config.display_name.clone();
32
33 // ── 2. Persist the selection so model_config()/sampling_config() agree ─
34 // NB: never hold the std Mutex guard across an `.await` (it would make this
35 // command future `!Send`), so resolve the loaded-state first.
36 let already_loaded = ENGINE.is_loaded().await;
37 {
38 let mut selected = SELECTED_MODEL
39 .lock()
40 .map_err(|e| format!("Failed to update selected model: {e}"))?;
41 if *selected == model_id && already_loaded {
42 info!("Model {display_name} is already selected and loaded; nothing to do.");
43 return Ok(());
44 }
45 *selected = model_id.clone();
46 }
47
48 info!("chat_set_model: switching to {display_name} ({model_id})");
49 emit_chat_status(&app, ChatStatus::Loading, Some(&display_name), None);
50
51 // ── 3. Swap models off-thread; report progress via events ────────────
52 tokio::task::spawn(async move {
53 if ENGINE.is_loaded().await {
54 ENGINE.unload_model().await;
55 }
56
57 let result = ENGINE
58 .load_gguf_model(
59 config,
60 Some(CHAT_SYSTEM_PROMPT.to_string()),
61 Some(sampling_config()),
62 )
63 .await;
64
65 match result {
66 Ok(elapsed) => {
67 info!("Model {display_name} loaded in {}", fmt_duration(elapsed));
68 emit_chat_status(&app, ChatStatus::Ready, Some(&display_name), None);
69 }
70 Err(e) => {
71 let msg = format!("Failed to load model {display_name}: {e}");
72 error!("{msg}");
73 emit_chat_status(&app, ChatStatus::Error, Some(&display_name), Some(&msg));
74 }
75 }
76 });
77
78 Ok(())
79 }
80
81 #[cfg(not(any(target_os = "macos", target_os = "ios", target_os = "android")))]
82 #[tauri::command]
83 pub async fn chat_set_model(_app: AppHandle, _model_id: String) -> Result<(), String> {
84 log::debug!("Model switching is only supported on macOS, iOS, and Android.");
85 Err("Model switching is only supported on macOS, iOS, and Android for now.".to_string())
86 }