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