Raw
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(
13 target_os = "macos",
14 target_os = "ios",
15 target_os = "android",
16 target_os = "windows"
17 ))]
18 use {
19 super::{
20 emit_chat_status, fmt_duration, resolve_model_id, resolved_model_config, sampling_config,
21 CHAT_SYSTEM_PROMPT, ENGINE, SELECTED_MODEL,
22 },
23 crate::constants::ChatStatus,
24 log::{error, info},
25 };
26
27 /// Switch Siti to the model identified by `model_id` (a HuggingFace repo id
28 /// from `chat_list_models`). The new model is loaded asynchronously; watch the
29 /// `chat_status_changed` event for `Loading` → `Ready`/`Error`.
30 #[cfg(any(
31 target_os = "macos",
32 target_os = "ios",
33 target_os = "android",
34 target_os = "windows"
35 ))]
36 #[tauri::command]
37 pub async fn chat_set_model(app: AppHandle, model_id: String) -> Result<(), String> {
38 // ── 1. Validate & resolve the requested model ────────────────────────
39 let display_name = resolve_model_id(&model_id)
40 .ok_or_else(|| format!("Unknown or unsupported model id: {model_id}"))?
41 .display_name();
42
43 // ── 2. Persist the selection so resolved_model_config()/sampling_config() agree ─
44 // NB: never hold the std Mutex guard across an `.await` (it would make this
45 // command future `!Send`), so resolve the loaded-state first.
46 let already_loaded = ENGINE.is_loaded().await;
47 {
48 let mut selected = SELECTED_MODEL
49 .lock()
50 .map_err(|e| format!("Failed to update selected model: {e}"))?;
51 if *selected == model_id && already_loaded {
52 info!("Model {display_name} is already selected and loaded; nothing to do.");
53 return Ok(());
54 }
55 *selected = model_id.clone();
56 }
57
58 info!("chat_set_model: switching to {display_name} ({model_id})");
59 emit_chat_status(&app, ChatStatus::Loading, Some(&display_name), None);
60
61 // ── 3. Swap models off-thread; report progress via events ────────────
62 // Re-resolve from the (now-persisted) selection inside the task so the
63 // GGUF/ISQ dispatch matches exactly what `chat_send_message` would load.
64 tokio::task::spawn(async move {
65 if ENGINE.is_loaded().await {
66 ENGINE.unload_model().await;
67 }
68
69 let result = resolved_model_config()
70 .load(
71 Some(CHAT_SYSTEM_PROMPT.to_string()),
72 Some(sampling_config()),
73 )
74 .await;
75
76 match result {
77 Ok(elapsed) => {
78 info!("Model {display_name} loaded in {}", fmt_duration(elapsed));
79 emit_chat_status(&app, ChatStatus::Ready, Some(&display_name), None);
80 }
81 Err(e) => {
82 let msg = format!("Failed to load model {display_name}: {e}");
83 error!("{msg}");
84 emit_chat_status(&app, ChatStatus::Error, Some(&display_name), Some(&msg));
85 }
86 }
87 });
88
89 Ok(())
90 }
91
92 #[cfg(not(any(
93 target_os = "macos",
94 target_os = "ios",
95 target_os = "android",
96 target_os = "windows"
97 )))]
98 #[tauri::command]
99 pub async fn chat_set_model(_app: AppHandle, _model_id: String) -> Result<(), String> {
100 log::debug!("Model switching is not supported on this platform.");
101 Err("Model switching is not supported on this platform.".to_string())
102 }