Skip to main content

foundation_models/model/
mod.rs

1//! [`SystemLanguageModel`] — entry point for querying device capability and
2//! building configured model handles.
3
4use core::ffi::{c_char, c_void};
5use std::ffi::CString;
6use std::path::Path;
7use std::ptr;
8use std::sync::mpsc;
9
10use serde_json::Value;
11
12use crate::error::{from_swift, FMError, Unavailability};
13use crate::ffi;
14
15fn availability_from_code(code: i32) -> Availability {
16    match code {
17        0 => Availability::Available,
18        1 => Availability::Unavailable(Unavailability::DeviceNotEligible),
19        2 => Availability::Unavailable(Unavailability::AppleIntelligenceNotEnabled),
20        3 => Availability::Unavailable(Unavailability::ModelNotReady),
21        -1 => Availability::Unavailable(Unavailability::OsTooOld),
22        _ => Availability::Unavailable(Unavailability::Unknown),
23    }
24}
25
26fn owned_string(ptr: *mut c_char) -> String {
27    if ptr.is_null() {
28        return String::new();
29    }
30    let string = unsafe { core::ffi::CStr::from_ptr(ptr) }
31        .to_string_lossy()
32        .into_owned();
33    unsafe { ffi::fm_string_free(ptr) };
34    string
35}
36
37fn json_string(ptr: *mut c_char) -> String {
38    if ptr.is_null() {
39        return String::from("[]");
40    }
41    owned_string(ptr)
42}
43
44/// The on-device system language model namespace.
45#[derive(Debug, Clone, Copy)]
46pub struct SystemLanguageModel;
47
48impl SystemLanguageModel {
49    /// Convenience: `availability() == Availability::Available`.
50    #[must_use]
51    pub fn is_available() -> bool {
52        unsafe { ffi::fm_system_model_is_available() }
53    }
54
55    /// Detailed availability state of the default model.
56    #[must_use]
57    pub fn availability() -> Availability {
58        let code = unsafe { ffi::fm_system_model_availability_code() };
59        availability_from_code(code)
60    }
61
62    /// Borrow the SDK's shared default model as a configured handle.
63    #[must_use]
64    pub fn default_model() -> Option<ConfiguredSystemLanguageModel> {
65        let ptr = unsafe { ffi::fm_system_model_create_default() };
66        (!ptr.is_null()).then_some(ConfiguredSystemLanguageModel { ptr })
67    }
68
69    /// Build a configured system model for the supplied use case and guardrails.
70    ///
71    /// # Errors
72    ///
73    /// Returns an [`FMError`] if the current OS does not expose FoundationModels.
74    pub fn with_use_case(
75        use_case: UseCase,
76        guardrails: Guardrails,
77    ) -> Result<ConfiguredSystemLanguageModel, FMError> {
78        let mut error: *mut c_char = ptr::null_mut();
79        let ptr = unsafe {
80            ffi::fm_system_model_create(use_case.as_ffi(), guardrails.as_ffi(), &mut error)
81        };
82        if ptr.is_null() {
83            return Err(from_swift(ffi::status::MODEL_UNAVAILABLE, error));
84        }
85        Ok(ConfiguredSystemLanguageModel { ptr })
86    }
87
88    /// Build a configured system model backed by an adapter.
89    ///
90    /// # Errors
91    ///
92    /// Returns an [`FMError`] if the adapter is invalid or the current OS does
93    /// not expose FoundationModels.
94    pub fn with_adapter(
95        adapter: &Adapter,
96        guardrails: Guardrails,
97    ) -> Result<ConfiguredSystemLanguageModel, FMError> {
98        let mut error: *mut c_char = ptr::null_mut();
99        let ptr = unsafe {
100            ffi::fm_system_model_create_with_adapter(adapter.ptr, guardrails.as_ffi(), &mut error)
101        };
102        if ptr.is_null() {
103            return Err(from_swift(ffi::status::MODEL_UNAVAILABLE, error));
104        }
105        Ok(ConfiguredSystemLanguageModel { ptr })
106    }
107
108    /// Languages supported by the default system model.
109    #[must_use]
110    pub fn supported_languages() -> Vec<String> {
111        let json = unsafe { ffi::fm_system_model_supported_languages_json(ptr::null_mut()) };
112        serde_json::from_str(&json_string(json)).unwrap_or_default()
113    }
114
115    /// Whether the default model supports a locale.
116    #[must_use]
117    pub fn supports_locale(locale_identifier: &str) -> bool {
118        CString::new(locale_identifier).map_or(false, |locale| unsafe {
119            ffi::fm_system_model_supports_locale(ptr::null_mut(), locale.as_ptr())
120        })
121    }
122}
123
124/// A configured `SystemLanguageModel` instance.
125pub struct ConfiguredSystemLanguageModel {
126    pub(crate) ptr: *mut c_void,
127}
128
129impl ConfiguredSystemLanguageModel {
130    /// Detailed availability of this configured model.
131    #[must_use]
132    pub fn availability(&self) -> Availability {
133        availability_from_code(unsafe { ffi::fm_system_model_availability_code_for(self.ptr) })
134    }
135
136    /// Convenience: `availability() == Availability::Available`.
137    #[must_use]
138    pub fn is_available(&self) -> bool {
139        matches!(self.availability(), Availability::Available)
140    }
141
142    /// Supported languages for this configured model.
143    #[must_use]
144    pub fn supported_languages(&self) -> Vec<String> {
145        let json = unsafe { ffi::fm_system_model_supported_languages_json(self.ptr) };
146        serde_json::from_str(&json_string(json)).unwrap_or_default()
147    }
148
149    /// Whether this configured model supports a locale.
150    #[must_use]
151    pub fn supports_locale(&self, locale_identifier: &str) -> bool {
152        CString::new(locale_identifier).map_or(false, |locale| unsafe {
153            ffi::fm_system_model_supports_locale(self.ptr, locale.as_ptr())
154        })
155    }
156}
157
158impl Drop for ConfiguredSystemLanguageModel {
159    fn drop(&mut self) {
160        if !self.ptr.is_null() {
161            unsafe { ffi::fm_object_release(self.ptr) };
162        }
163    }
164}
165
166impl core::fmt::Debug for ConfiguredSystemLanguageModel {
167    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
168        f.debug_struct("ConfiguredSystemLanguageModel")
169            .field("availability", &self.availability())
170            .finish()
171    }
172}
173
174/// One of the public system-model use cases.
175#[derive(Debug, Clone, Copy, PartialEq, Eq)]
176pub enum UseCase {
177    /// The default general-purpose model.
178    General,
179    /// Optimized for content-tagging style prompts.
180    ContentTagging,
181}
182
183impl UseCase {
184    const fn as_ffi(self) -> i32 {
185        match self {
186            Self::General => 0,
187            Self::ContentTagging => 1,
188        }
189    }
190}
191
192/// One of the public system-model guardrail configurations.
193#[derive(Debug, Clone, Copy, PartialEq, Eq)]
194pub enum Guardrails {
195    /// The SDK default guardrail policy.
196    Default,
197    /// A looser policy for content transformation tasks.
198    PermissiveContentTransformations,
199}
200
201impl Guardrails {
202    const fn as_ffi(self) -> i32 {
203        match self {
204            Self::Default => 0,
205            Self::PermissiveContentTransformations => 1,
206        }
207    }
208}
209
210/// A system model adapter.
211pub struct Adapter {
212    pub(crate) ptr: *mut c_void,
213}
214
215impl Adapter {
216    /// Load an adapter from a file path.
217    ///
218    /// # Errors
219    ///
220    /// Returns an [`FMError`] if the adapter file is invalid.
221    pub fn from_file(path: impl AsRef<Path>) -> Result<Self, FMError> {
222        let path = CString::new(path.as_ref().to_string_lossy().into_owned()).map_err(|error| {
223            FMError::InvalidArgument(format!(
224                "adapter path contains an interior NUL byte: {error}"
225            ))
226        })?;
227        let mut error: *mut c_char = ptr::null_mut();
228        let ptr = unsafe { ffi::fm_adapter_create_from_file(path.as_ptr(), &mut error) };
229        if ptr.is_null() {
230            return Err(from_swift(ffi::status::ADAPTER_INVALID_ASSET, error));
231        }
232        Ok(Self { ptr })
233    }
234
235    /// Load a named adapter.
236    ///
237    /// # Errors
238    ///
239    /// Returns an [`FMError`] if the adapter name is invalid.
240    pub fn from_name(name: &str) -> Result<Self, FMError> {
241        let name = CString::new(name).map_err(|error| {
242            FMError::InvalidArgument(format!("adapter name contains NUL byte: {error}"))
243        })?;
244        let mut error: *mut c_char = ptr::null_mut();
245        let ptr = unsafe { ffi::fm_adapter_create_from_name(name.as_ptr(), &mut error) };
246        if ptr.is_null() {
247            return Err(from_swift(ffi::status::ADAPTER_INVALID_NAME, error));
248        }
249        Ok(Self { ptr })
250    }
251
252    /// Compile the adapter.
253    ///
254    /// # Errors
255    ///
256    /// Returns an [`FMError`] if compilation fails.
257    pub fn compile(&self) -> Result<(), FMError> {
258        let (tx, rx) = mpsc::channel();
259        let tx_box: Box<mpsc::Sender<Result<(), FMError>>> = Box::new(tx);
260        let context = Box::into_raw(tx_box).cast::<c_void>();
261        unsafe { ffi::fm_adapter_compile(self.ptr, context, adapter_compile_trampoline) };
262        rx.recv().map_err(|_| FMError::Unknown {
263            code: ffi::status::UNKNOWN,
264            message: "Swift bridge dropped the adapter compile callback".into(),
265        })?
266    }
267
268    /// Creator-defined metadata as raw JSON.
269    #[must_use]
270    pub fn creator_defined_metadata_json(&self) -> String {
271        let ptr = unsafe { ffi::fm_adapter_metadata_json(self.ptr) };
272        owned_string(ptr)
273    }
274
275    /// Creator-defined metadata as a `serde_json::Value`.
276    pub fn creator_defined_metadata(&self) -> Result<Value, FMError> {
277        serde_json::from_str(&self.creator_defined_metadata_json())
278            .map_err(|error| FMError::DecodingFailure(error.to_string()))
279    }
280
281    /// Compatible adapter identifiers for a logical adapter name.
282    #[must_use]
283    pub fn compatible_adapter_identifiers(name: &str) -> Vec<String> {
284        let Ok(name) = CString::new(name) else {
285            return Vec::new();
286        };
287        let ptr = unsafe { ffi::fm_adapter_compatible_identifiers_json(name.as_ptr()) };
288        serde_json::from_str(&json_string(ptr)).unwrap_or_default()
289    }
290
291    /// Remove obsolete compiled adapters.
292    ///
293    /// # Errors
294    ///
295    /// Returns an [`FMError`] if cleanup fails.
296    pub fn remove_obsolete_adapters() -> Result<(), FMError> {
297        let mut error: *mut c_char = ptr::null_mut();
298        let status = unsafe { ffi::fm_adapter_remove_obsolete(&mut error) };
299        if status != ffi::status::OK {
300            return Err(from_swift(status, error));
301        }
302        Ok(())
303    }
304}
305
306impl Drop for Adapter {
307    fn drop(&mut self) {
308        if !self.ptr.is_null() {
309            unsafe { ffi::fm_object_release(self.ptr) };
310        }
311    }
312}
313
314impl core::fmt::Debug for Adapter {
315    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
316        f.debug_struct("Adapter").finish_non_exhaustive()
317    }
318}
319
320unsafe extern "C" fn adapter_compile_trampoline(
321    context: *mut c_void,
322    response: *mut c_char,
323    error: *mut c_char,
324    status: i32,
325) {
326    let tx = Box::from_raw(context.cast::<mpsc::Sender<Result<(), FMError>>>());
327    if !response.is_null() {
328        unsafe { ffi::fm_string_free(response) };
329    }
330    let result = if status == ffi::status::OK {
331        Ok(())
332    } else {
333        Err(from_swift(status, error))
334    };
335    let _ = tx.send(result);
336}
337
338/// Result of [`SystemLanguageModel::availability`].
339#[derive(Debug, Clone, Copy, PartialEq, Eq)]
340#[non_exhaustive]
341pub enum Availability {
342    /// Model is loaded and ready to generate.
343    Available,
344    /// Model cannot be used; the inner value explains why.
345    Unavailable(Unavailability),
346}