1use 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#[derive(Debug, Clone, Copy)]
94pub struct SystemLanguageModel;
95
96impl SystemLanguageModel {
97 #[must_use]
99 pub fn is_available() -> bool {
100 unsafe { ffi::fm_system_model_is_available() }
101 }
102
103 #[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 #[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 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 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 #[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 #[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 #[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
183pub struct ConfiguredSystemLanguageModel {
185 pub(crate) ptr: *mut c_void,
186}
187
188impl ConfiguredSystemLanguageModel {
189 #[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 #[must_use]
197 pub fn is_available(&self) -> bool {
198 matches!(self.availability(), Availability::Available)
199 }
200
201 #[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 #[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 #[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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
248pub enum UseCase {
249 General,
251 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
266pub enum Guardrails {
267 Default,
269 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
282pub struct Adapter {
284 pub(crate) ptr: *mut c_void,
285}
286
287impl Adapter {
288 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 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 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 #[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 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 #[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 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
392unsafe 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
416#[non_exhaustive]
417pub enum Availability {
418 Available,
420 Unavailable(Unavailability),
422}