fm_rs/lib.rs
1//! Rust bindings for Apple's FoundationModels.framework
2//!
3//! This crate provides safe, idiomatic Rust bindings to Apple's `FoundationModels` framework,
4//! which enables on-device AI capabilities powered by Apple Intelligence.
5//!
6//! # Platform Requirements
7//!
8//! - **Minimum OS**: macOS 26.0+, iOS 26.0+, iPadOS 26.0+, visionOS 26.0+, tvOS 26.0+, watchOS 26.0+
9//! - **Apple Intelligence**: Must be enabled on the device
10//! - **Device**: Must support Apple Intelligence
11//!
12//! # Quick Start
13//!
14//! ```rust,no_run
15//! use fm_rs::{SystemLanguageModel, Session, GenerationOptions};
16//!
17//! // Create the default system language model
18//! let model = SystemLanguageModel::new()?;
19//!
20//! // Check availability
21//! if !model.is_available() {
22//! println!("FoundationModels is not available on this device");
23//! return Ok(());
24//! }
25//!
26//! // Create a session with instructions
27//! let session = Session::with_instructions(&model, "You are a helpful assistant.")?;
28//!
29//! // Send a prompt
30//! let options = GenerationOptions::builder()
31//! .temperature(0.7)
32//! .build();
33//! let response = session.respond("What is the capital of France?", &options)?;
34//!
35//! println!("Response: {}", response.content());
36//! # Ok::<(), fm_rs::Error>(())
37//! ```
38//!
39//! # Streaming Responses
40//!
41//! ```rust,no_run
42//! use fm_rs::{SystemLanguageModel, Session, GenerationOptions};
43//!
44//! let model = SystemLanguageModel::new()?;
45//! let session = Session::new(&model)?;
46//!
47//! session.stream_response("Tell me a story", &GenerationOptions::default(), |chunk| {
48//! print!("{}", chunk);
49//! })?;
50//! # Ok::<(), fm_rs::Error>(())
51//! ```
52//!
53//! # Tool Calling
54//!
55//! ```rust,no_run
56//! use fm_rs::{SystemLanguageModel, Session, Tool, ToolOutput, GenerationOptions};
57//! use serde_json::{json, Value};
58//! use std::sync::Arc;
59//!
60//! struct WeatherTool;
61//!
62//! impl Tool for WeatherTool {
63//! fn name(&self) -> &str { "get_weather" }
64//! fn description(&self) -> &str { "Gets the current weather for a location" }
65//! fn arguments_schema(&self) -> Value {
66//! json!({
67//! "type": "object",
68//! "properties": {
69//! "location": { "type": "string" }
70//! },
71//! "required": ["location"]
72//! })
73//! }
74//! fn call(&self, args: Value) -> fm_rs::Result<ToolOutput> {
75//! let location = args["location"].as_str().unwrap_or("Unknown");
76//! Ok(ToolOutput::new(format!("Sunny, 72°F in {location}")))
77//! }
78//! }
79//!
80//! let model = SystemLanguageModel::new()?;
81//! let tools: Vec<Arc<dyn Tool>> = vec![Arc::new(WeatherTool)];
82//! let session = Session::with_tools(&model, &tools)?;
83//!
84//! let response = session.respond("What's the weather in Paris?", &GenerationOptions::default())?;
85//! # Ok::<(), fm_rs::Error>(())
86//! ```
87//!
88//! # Resource Management
89//!
90//! ## Session Lifecycle
91//!
92//! - **Ownership**: [`Session`] owns its underlying Swift `LanguageModelSession`. When
93//! a `Session` is dropped, the Swift session is freed.
94//! - **Thread Safety**: Both [`SystemLanguageModel`] and [`Session`] are `Send + Sync`.
95//! They can be shared across threads, though concurrent calls to the same session
96//! may block waiting for the model.
97//! - **Drop Behavior**: Dropping a `Session` with active tool callbacks will wait
98//! (up to 1 second) for in-flight callbacks to complete before freeing resources.
99//!
100//! ## Memory Considerations
101//!
102//! - Sessions maintain conversation history in memory. Long conversations can consume
103//! significant memory. Use [`Session::transcript_json()`] to persist and
104//! [`Session::from_transcript()`] to restore conversations.
105//! - Monitor context usage with [`context_usage_from_transcript()`] and implement
106//! compaction strategies using [`compact_transcript()`] when approaching limits.
107//! - The default context window is approximately [`DEFAULT_CONTEXT_TOKENS`] tokens,
108//! though this may vary by device and model version.
109//!
110//! ## Blocking Operations
111//!
112//! - [`Session::respond()`] blocks until generation completes. For long generations,
113//! consider using [`Session::respond_with_timeout()`] or [`Session::stream_response()`].
114//! - Tool callbacks are invoked synchronously during generation. Long-running tools
115//! will block the generation pipeline.
116//!
117//! ## No Persistence Across Restarts
118//!
119//! Sessions do not persist across process restarts. To resume a conversation:
120//! 1. Save the transcript with [`Session::transcript_json()`] before shutdown
121//! 2. Restore with [`Session::from_transcript()`] on next launch
122//!
123//! ## Error Recovery
124//!
125//! - If a session enters an error state, create a new session rather than retrying.
126//! - Tool errors are reported via [`Error::ToolCall`] with context about which tool
127//! failed and the arguments that were passed.
128
129#![warn(missing_docs)]
130#![warn(clippy::all)]
131
132mod context;
133mod error;
134mod ffi;
135mod model;
136mod options;
137mod session;
138mod tool;
139
140/// Trait for types that can provide a JSON Schema for structured generation.
141pub trait Generable {
142 /// Returns a JSON Schema describing the type.
143 fn schema() -> serde_json::Value;
144}
145
146/// Re-export the derive macro when the `derive` feature is enabled.
147#[cfg(feature = "derive")]
148pub use fm_rs_derive::Generable;
149
150/// Re-export `serde_json` so derive macro output doesn't require a direct dependency.
151///
152/// Named `__serde_json` to avoid namespace conflicts with serde's internal derive paths.
153#[doc(hidden)]
154pub use serde_json as __serde_json;
155
156// Re-export public API
157pub use crate::context::{
158 CompactionConfig, ContextLimit, ContextUsage, DEFAULT_CONTEXT_TOKENS, compact_transcript,
159 context_usage_from_transcript, estimate_tokens, transcript_to_text,
160};
161pub use crate::error::{Error, Result, ToolCallError};
162pub use crate::model::{ModelAvailability, SystemLanguageModel};
163pub use crate::options::{GenerationOptions, GenerationOptionsBuilder, Sampling};
164pub use crate::session::{Response, Session};
165pub use crate::tool::{Tool, ToolOutput};
166
167// FFI exports for Swift to call back into Rust
168
169/// Frees a string allocated by Rust (via `CString::into_raw`).
170///
171/// This function must be called by Swift to properly deallocate strings
172/// returned from Rust callbacks (e.g., tool callback results).
173///
174/// # Safety
175///
176/// The pointer must have been allocated by Rust using `CString::into_raw()`.
177/// Passing a pointer from any other source (e.g., Swift's strdup) is undefined behavior.
178///
179/// This function is called from Swift via FFI. The clippy `not_unsafe_ptr_arg_deref`
180/// lint is allowed because the unsafety contract is on the FFI caller side (Swift).
181#[unsafe(no_mangle)]
182#[allow(clippy::not_unsafe_ptr_arg_deref)]
183pub extern "C" fn fm_rust_string_free(s: *mut std::ffi::c_char) {
184 if !s.is_null() {
185 // SAFETY: The pointer was created by CString::into_raw() in Rust,
186 // as documented in the function contract.
187 unsafe {
188 drop(std::ffi::CString::from_raw(s));
189 }
190 }
191}