fm_rs/
model.rs

1//! `SystemLanguageModel` and `ModelAvailability` types.
2
3use std::ffi::CStr;
4use std::ptr::{self, NonNull};
5
6use crate::error::{Error, Result};
7use crate::ffi::{self, AvailabilityCode, SwiftPtr};
8
9/// Represents the availability status of a `FoundationModel`.
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum ModelAvailability {
12    /// Model is available and ready to use.
13    Available,
14    /// Device is not eligible for Apple Intelligence.
15    DeviceNotEligible,
16    /// Apple Intelligence is not enabled in system settings.
17    AppleIntelligenceNotEnabled,
18    /// Model is not ready (downloading or other system reasons).
19    ModelNotReady,
20    /// Unavailability for an unknown reason.
21    Unknown,
22}
23
24impl ModelAvailability {
25    /// Returns an error describing why the model is unavailable.
26    pub fn into_error(self) -> Option<Error> {
27        match self {
28            ModelAvailability::Available => None,
29            ModelAvailability::DeviceNotEligible => Some(Error::DeviceNotEligible),
30            ModelAvailability::AppleIntelligenceNotEnabled => {
31                Some(Error::AppleIntelligenceNotEnabled)
32            }
33            ModelAvailability::ModelNotReady => Some(Error::ModelNotReady),
34            ModelAvailability::Unknown => Some(Error::ModelNotAvailable),
35        }
36    }
37}
38
39impl From<AvailabilityCode> for ModelAvailability {
40    fn from(code: AvailabilityCode) -> Self {
41        match code {
42            AvailabilityCode::Available => ModelAvailability::Available,
43            AvailabilityCode::DeviceNotEligible => ModelAvailability::DeviceNotEligible,
44            AvailabilityCode::AppleIntelligenceNotEnabled => {
45                ModelAvailability::AppleIntelligenceNotEnabled
46            }
47            AvailabilityCode::ModelNotReady => ModelAvailability::ModelNotReady,
48            AvailabilityCode::Unknown => ModelAvailability::Unknown,
49        }
50    }
51}
52
53/// The system language model provided by Apple Intelligence.
54///
55/// This is the main entry point for using on-device AI capabilities.
56/// Use [`SystemLanguageModel::new()`] to get the default model.
57///
58/// # Example
59///
60/// ```rust,no_run
61/// use fm_rs::SystemLanguageModel;
62///
63/// let model = SystemLanguageModel::new()?;
64/// if model.is_available() {
65///     println!("Model is ready to use!");
66/// }
67/// # Ok::<(), fm_rs::Error>(())
68/// ```
69pub struct SystemLanguageModel {
70    ptr: NonNull<std::ffi::c_void>,
71}
72
73impl SystemLanguageModel {
74    /// Creates the default system language model.
75    ///
76    /// # Errors
77    ///
78    /// Returns an error if the model cannot be created or if `FoundationModels`
79    /// is not available on the device.
80    pub fn new() -> Result<Self> {
81        let mut error: SwiftPtr = ptr::null_mut();
82
83        let ptr = unsafe { ffi::fm_model_default(&raw mut error) };
84
85        if !error.is_null() {
86            return Err(error_from_swift(error));
87        }
88
89        NonNull::new(ptr).map(|ptr| Self { ptr }).ok_or_else(|| {
90            Error::InternalError(
91                "SystemLanguageModel creation returned null without error. \
92                 This may indicate FoundationModels.framework is unavailable."
93                    .to_string(),
94            )
95        })
96    }
97
98    /// Returns a raw pointer to the underlying Swift object.
99    ///
100    /// This is used internally for FFI calls.
101    pub(crate) fn as_ptr(&self) -> SwiftPtr {
102        self.ptr.as_ptr()
103    }
104
105    /// Checks if the model is available for use.
106    ///
107    /// Returns `true` if the model is available and ready to generate responses.
108    pub fn is_available(&self) -> bool {
109        unsafe { ffi::fm_model_is_available(self.ptr.as_ptr()) }
110    }
111
112    /// Gets the current availability status of the model.
113    ///
114    /// This provides more detailed information about why the model might not be available.
115    pub fn availability(&self) -> ModelAvailability {
116        let code = unsafe { ffi::fm_model_availability(self.ptr.as_ptr()) };
117        AvailabilityCode::from(code).into()
118    }
119
120    /// Returns a reason-specific error if the model is unavailable.
121    pub fn ensure_available(&self) -> Result<()> {
122        match self.availability().into_error() {
123            Some(err) => Err(err),
124            None => Ok(()),
125        }
126    }
127}
128
129impl std::fmt::Debug for SystemLanguageModel {
130    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
131        f.debug_struct("SystemLanguageModel")
132            .field("availability", &self.availability())
133            .finish()
134    }
135}
136
137impl Drop for SystemLanguageModel {
138    fn drop(&mut self) {
139        unsafe {
140            ffi::fm_model_free(self.ptr.as_ptr());
141        }
142    }
143}
144
145// SAFETY: SystemLanguageModel is a wrapper around a Swift object that is
146// internally thread-safe (uses DispatchQueue for async operations).
147unsafe impl Send for SystemLanguageModel {}
148unsafe impl Sync for SystemLanguageModel {}
149
150/// Converts a Swift error pointer to a Rust Error.
151pub(crate) fn error_from_swift(error: SwiftPtr) -> Error {
152    use crate::error::ToolCallError;
153
154    if error.is_null() {
155        return Error::InternalError(
156            "FFI error object was null; unable to retrieve error details".to_string(),
157        );
158    }
159
160    let code = unsafe { ffi::fm_error_code(error) };
161    let msg_ptr = unsafe { ffi::fm_error_message(error) };
162
163    let message = if msg_ptr.is_null() {
164        "Error message unavailable (null pointer from Swift)".to_string()
165    } else {
166        unsafe { CStr::from_ptr(msg_ptr).to_string_lossy().into_owned() }
167    };
168
169    // Extract tool context if this is a tool error
170    let tool_name = unsafe {
171        let ptr = ffi::fm_error_tool_name(error);
172        if ptr.is_null() {
173            None
174        } else {
175            Some(CStr::from_ptr(ptr).to_string_lossy().into_owned())
176        }
177    };
178
179    let tool_arguments = unsafe {
180        let ptr = ffi::fm_error_tool_arguments(error);
181        if ptr.is_null() {
182            None
183        } else {
184            let json_str = CStr::from_ptr(ptr).to_string_lossy().into_owned();
185            serde_json::from_str(&json_str).ok()
186        }
187    };
188
189    unsafe {
190        ffi::fm_error_free(error);
191    }
192
193    match ffi::ErrorCode::from(code) {
194        ffi::ErrorCode::ModelNotAvailable => Error::ModelNotAvailable,
195        ffi::ErrorCode::GenerationFailed => Error::GenerationError(message),
196        ffi::ErrorCode::Cancelled => Error::GenerationError("Operation cancelled".to_string()),
197        ffi::ErrorCode::Timeout => Error::Timeout(message),
198        ffi::ErrorCode::ToolError => {
199            // Construct ToolCallError with context if available
200            Error::ToolCall(ToolCallError {
201                tool_name: tool_name.unwrap_or_else(|| "unknown".to_string()),
202                arguments: tool_arguments.unwrap_or(serde_json::Value::Null),
203                inner_error: message,
204            })
205        }
206        ffi::ErrorCode::InvalidInput => Error::InvalidInput(message),
207        ffi::ErrorCode::Unknown => Error::InternalError(message),
208    }
209}