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