Raw
1 //! Tauri command: `chat_list_models`
2 //!
3 //! Returns the on-device models the user can switch between, sourced from the
4 //! onde SDK's `SUPPORTED_MODEL_INFO` catalogue (filtered to the subset the
5 //! engine can actually load), with the currently selected model flagged.
6
7 use super::ModelInfo;
8
9 /// Normalise a model description for Apple builds.
10 ///
11 /// The shared onde model catalogue is cross-platform and also names sibling
12 /// products, so its descriptions mention Android/Windows/Linux and "siGit Code".
13 /// Apple's App Store Review Guideline 2.3.10 (Accurate Metadata) forbids
14 /// referencing other platforms in an iOS/macOS app's UI or metadata, so on
15 /// Apple builds we drop those mentions before the text reaches Settings. We
16 /// also neutralise the cross-product reference, which would only confuse a Siti
17 /// user. Apple's own platforms (iOS, macOS, tvOS) are kept.
18 #[cfg(any(target_os = "macos", target_os = "ios"))]
19 fn sanitize_description(desc: &str) -> String {
20 desc.replace("iOS & Android", "iOS")
21 .replace("macOS & Android", "macOS")
22 .replace("macOS, Linux, and Windows", "macOS")
23 .replace(", Linux, and Windows", "")
24 .replace(", Linux and Windows", "")
25 .replace(" & Android", "")
26 .replace(" and Android", "")
27 .replace("siGit Code", "coding tasks")
28 }
29
30 /// On non-Apple builds the catalogue text is already accurate, so pass it through.
31 #[cfg(any(target_os = "android", target_os = "windows"))]
32 fn sanitize_description(desc: &str) -> String {
33 desc.to_string()
34 }
35
36 /// List the supported on-device models, smallest first, with the currently
37 /// selected model marked. Used to populate the Settings model dropdown.
38 #[cfg(any(
39 target_os = "macos",
40 target_os = "ios",
41 target_os = "android",
42 target_os = "windows"
43 ))]
44 #[tauri::command]
45 pub async fn chat_list_models() -> Vec<ModelInfo> {
46 use super::{config_for_model_id, is_model_downloaded, SELECTED_MODEL};
47
48 let selected = SELECTED_MODEL.lock().map(|g| g.clone()).unwrap_or_default();
49
50 let mut models: Vec<ModelInfo> = crate::inference::models::SUPPORTED_MODEL_INFO
51 .iter()
52 // Only include models the engine can build a loadable config for.
53 .filter_map(|info| {
54 config_for_model_id(info.id).map(|cfg| ModelInfo {
55 id: info.id.to_string(),
56 name: info.name.to_string(),
57 org: info.org.to_string(),
58 description: sanitize_description(info.description),
59 approx_memory: cfg.approx_memory,
60 size_bytes: Some(info.expected_size_bytes),
61 is_downloaded: is_model_downloaded(info.id, info.expected_size_bytes),
62 is_selected: info.id == selected,
63 })
64 })
65 .collect();
66
67 // Qwen 3 UQFF models: pre-quantised (Q4K) safetensors loaded via onde's
68 // UQFF path on every inference platform. Not in onde's GGUF catalogue, so
69 // they're appended here from Siti's own `UQFF_MODELS` table. A few ~45 GB
70 // entries are marked `desktop_only` and skipped on iOS/Android, where
71 // neither the storage nor the memory budget could realistically fit them.
72 {
73 use super::UQFF_MODELS;
74 for entry in UQFF_MODELS {
75 if entry.desktop_only && !cfg!(any(target_os = "macos", target_os = "windows")) {
76 continue;
77 }
78 models.push(ModelInfo {
79 id: entry.id.to_string(),
80 name: entry.name.to_string(),
81 org: "Qwen / Alibaba".to_string(),
82 description: sanitize_description(entry.description),
83 approx_memory: entry.approx_memory.to_string(),
84 size_bytes: Some(entry.expected_size_bytes),
85 is_downloaded: is_model_downloaded(entry.id, entry.expected_size_bytes),
86 is_selected: entry.id == selected,
87 });
88 }
89 }
90
91 // Gemma is not in onde's GGUF catalogue: mistral.rs's GGUF loader has no
92 // Gemma architecture, so Gemma can only load via the ISQ (safetensors)
93 // path, which is Metal-only. Offer it on macOS exclusively.
94 #[cfg(target_os = "macos")]
95 {
96 use super::{gemma2_2b_isq_config, GEMMA2_2B_IT_ISQ_ID};
97 const GEMMA2_2B_EXPECTED_BYTES: u64 = 5_228_717_512;
98 let cfg = gemma2_2b_isq_config();
99 models.push(ModelInfo {
100 id: GEMMA2_2B_IT_ISQ_ID.to_string(),
101 name: "Gemma 2 2B (ISQ)".to_string(),
102 org: "Google".to_string(),
103 description: sanitize_description(
104 "Google's Gemma 2 2B Instruct, quantised on-device to 4-bit (~1.6 GB). \
105 Loaded from safetensors via ISQ; requires 8+ GB RAM.",
106 ),
107 approx_memory: cfg.approx_memory,
108 // Full bf16 safetensors download before in-situ quantisation (~5.2 GB).
109 size_bytes: Some(GEMMA2_2B_EXPECTED_BYTES),
110 is_downloaded: is_model_downloaded(GEMMA2_2B_IT_ISQ_ID, GEMMA2_2B_EXPECTED_BYTES),
111 is_selected: GEMMA2_2B_IT_ISQ_ID == selected,
112 });
113 }
114
115 // Smallest (most device-friendly) first.
116 models.sort_by_key(|m| m.size_bytes.unwrap_or(u64::MAX));
117 models
118 }
119
120 #[cfg(not(any(
121 target_os = "macos",
122 target_os = "ios",
123 target_os = "android",
124 target_os = "windows"
125 )))]
126 #[tauri::command]
127 pub async fn chat_list_models() -> Vec<ModelInfo> {
128 log::debug!("On-device models are not supported on this platform.");
129 Vec::new()
130 }