Skip to main content

foundation_models/
error.rs

1//! Errors produced by the `FoundationModels` bridge.
2
3use core::ffi::c_char;
4use core::fmt;
5
6use crate::ffi;
7
8/// Top-level error type returned by all fallible APIs in this crate.
9#[derive(Debug, Clone, PartialEq, Eq)]
10#[non_exhaustive]
11pub enum FMError {
12    /// `FoundationModels` is not available on this device.
13    ///
14    /// See [`Unavailability`] for the specific reason.
15    ModelUnavailable {
16        reason: Unavailability,
17        message: String,
18    },
19    /// The model refused to produce a response because the prompt or
20    /// generated content tripped a safety guardrail.
21    GuardrailViolation(String),
22    /// The combined prompt + history exceeds the model's context window.
23    ContextWindowExceeded(String),
24    /// The requested locale or language is not supported by the on-device model.
25    UnsupportedLanguage(String),
26    /// On-device model assets are still downloading or otherwise unavailable.
27    AssetsUnavailable(String),
28    /// The session was rate-limited (typically only relevant on Mac with
29    /// extended generation budgets).
30    RateLimited(String),
31    /// Structured generation failed to decode the model's output into the
32    /// requested `Generable` schema.
33    DecodingFailure(String),
34    /// The model refused the request (distinct from a guardrail violation —
35    /// the model itself declined to answer).
36    Refusal(String),
37    /// Too many concurrent generation requests against the same session.
38    ConcurrentRequests(String),
39    /// The supplied [`GenerationGuide`] is unsupported by the on-device model.
40    UnsupportedGuide(String),
41    /// The generation Task was cancelled before completion.
42    Cancelled,
43    /// An invalid argument crossed the FFI boundary (e.g. a NUL byte in a prompt).
44    InvalidArgument(String),
45    /// Catch-all for unmapped Swift errors. Inspect [`code`](Self::code) and
46    /// [`message`](Self::message) for diagnostics.
47    Unknown { code: i32, message: String },
48}
49
50/// Reason why [`SystemLanguageModel`](crate::SystemLanguageModel) is unavailable.
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52#[non_exhaustive]
53pub enum Unavailability {
54    /// The hardware does not support Apple Intelligence (e.g. Intel Mac, M1).
55    DeviceNotEligible,
56    /// Apple Intelligence is supported but disabled in System Settings.
57    AppleIntelligenceNotEnabled,
58    /// Model assets are still downloading.
59    ModelNotReady,
60    /// The host OS is older than macOS 26.0.
61    OsTooOld,
62    /// `FoundationModels` reported an unavailability reason this crate doesn't
63    /// recognise — most likely added in a newer SDK.
64    Unknown,
65}
66
67impl FMError {
68    /// Numeric status code reported by the Swift bridge. Useful for matching
69    /// against [`crate::ffi::status`] constants.
70    #[must_use]
71    pub const fn code(&self) -> i32 {
72        match self {
73            Self::ModelUnavailable { .. } => ffi::status::MODEL_UNAVAILABLE,
74            Self::GuardrailViolation(_) => ffi::status::GUARDRAIL_VIOLATION,
75            Self::ContextWindowExceeded(_) => ffi::status::CONTEXT_WINDOW_EXCEEDED,
76            Self::UnsupportedLanguage(_) => ffi::status::UNSUPPORTED_LANGUAGE,
77            Self::AssetsUnavailable(_) => ffi::status::ASSETS_UNAVAILABLE,
78            Self::RateLimited(_) => ffi::status::RATE_LIMITED,
79            Self::DecodingFailure(_) => ffi::status::DECODING_FAILURE,
80            Self::Refusal(_) => ffi::status::REFUSAL,
81            Self::ConcurrentRequests(_) => ffi::status::CONCURRENT_REQUESTS,
82            Self::UnsupportedGuide(_) => ffi::status::UNSUPPORTED_GUIDE,
83            Self::Cancelled => ffi::status::CANCELLED,
84            Self::InvalidArgument(_) => ffi::status::INVALID_ARGUMENT,
85            Self::Unknown { code, .. } => *code,
86        }
87    }
88
89    /// Human-readable description (forwarded from `Error.localizedDescription`).
90    #[must_use]
91    pub fn message(&self) -> &str {
92        match self {
93            Self::ModelUnavailable { message, .. }
94            | Self::GuardrailViolation(message)
95            | Self::ContextWindowExceeded(message)
96            | Self::UnsupportedLanguage(message)
97            | Self::AssetsUnavailable(message)
98            | Self::RateLimited(message)
99            | Self::DecodingFailure(message)
100            | Self::Refusal(message)
101            | Self::ConcurrentRequests(message)
102            | Self::UnsupportedGuide(message)
103            | Self::InvalidArgument(message)
104            | Self::Unknown { message, .. } => message,
105            Self::Cancelled => "generation cancelled",
106        }
107    }
108}
109
110impl fmt::Display for FMError {
111    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
112        write!(f, "{} (code {})", self.message(), self.code())
113    }
114}
115
116impl std::error::Error for FMError {}
117
118/// Build an `FMError` from a status code + error message returned by Swift.
119///
120/// Takes ownership of `error_str` (a heap-allocated C string from the
121/// Swift bridge) and frees it via `fm_string_free` after copying.
122pub(crate) unsafe fn from_swift(status: i32, error_str: *mut c_char) -> FMError {
123    let message = if error_str.is_null() {
124        String::new()
125    } else {
126        let s = core::ffi::CStr::from_ptr(error_str)
127            .to_string_lossy()
128            .into_owned();
129        ffi::fm_string_free(error_str);
130        s
131    };
132
133    match status {
134        ffi::status::MODEL_UNAVAILABLE => FMError::ModelUnavailable {
135            reason: Unavailability::Unknown,
136            message,
137        },
138        ffi::status::GUARDRAIL_VIOLATION => FMError::GuardrailViolation(message),
139        ffi::status::CONTEXT_WINDOW_EXCEEDED => FMError::ContextWindowExceeded(message),
140        ffi::status::UNSUPPORTED_LANGUAGE => FMError::UnsupportedLanguage(message),
141        ffi::status::ASSETS_UNAVAILABLE => FMError::AssetsUnavailable(message),
142        ffi::status::RATE_LIMITED => FMError::RateLimited(message),
143        ffi::status::DECODING_FAILURE => FMError::DecodingFailure(message),
144        ffi::status::REFUSAL => FMError::Refusal(message),
145        ffi::status::CONCURRENT_REQUESTS => FMError::ConcurrentRequests(message),
146        ffi::status::UNSUPPORTED_GUIDE => FMError::UnsupportedGuide(message),
147        ffi::status::CANCELLED => FMError::Cancelled,
148        ffi::status::INVALID_ARGUMENT => FMError::InvalidArgument(message),
149        code => FMError::Unknown { code, message },
150    }
151}