| 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 | } |