-
Notifications
You must be signed in to change notification settings - Fork 0
chore: add scaffoldings for Apple foundational model #48
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 1 commit
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
b92c436
chore: add scaffoldings for Apple foundational model
EggerMarc 050c1ab
chore: add lora adapters
EggerMarc 4802bff
chore: improve configs
EggerMarc 251c389
chore: error state on builder
EggerMarc cad7e58
chore: add stream for applefm
EggerMarc File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| //! Probe whether this machine can run the Apple on-device foundation model. | ||
| //! | ||
| //! Run with: | ||
| //! ```sh | ||
| //! cargo run --example applefm-availability --features applefm | ||
| //! ``` | ||
| //! | ||
| //! Exits 0 when the model is usable, 1 otherwise (with the reason). | ||
|
|
||
| fn main() { | ||
| let probe = chat_rs::applefm::availability(); | ||
| if probe.available { | ||
| println!("Apple on-device model: AVAILABLE"); | ||
| } else { | ||
| println!( | ||
| "Apple on-device model: UNAVAILABLE — {}", | ||
| probe.reason.as_deref().unwrap_or("no reason given") | ||
| ); | ||
| std::process::exit(1); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| [package] | ||
| name = "chat-applefm" | ||
| version = "0.1.0" | ||
| description = "Apple on-device foundation model provider for chat-rs, built on the FoundationModels framework." | ||
| edition.workspace = true | ||
| authors.workspace = true | ||
| license.workspace = true | ||
| repository.workspace = true | ||
| rust-version.workspace = true | ||
|
|
||
| [dependencies] | ||
| serde.workspace = true | ||
| serde_json.workspace = true | ||
|
EggerMarc marked this conversation as resolved.
Outdated
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| // swift-tools-version: 6.0 | ||
| import PackageDescription | ||
|
|
||
| let package = Package( | ||
| name: "AppleFMBridge", | ||
| // Minimum macOS 15 so host binaries still run on machines without the | ||
| // FoundationModels framework; every use of the framework is guarded | ||
| // with `#available(macOS 26.0, *)` and the framework is weak-linked | ||
| // by the crate's build script. | ||
| platforms: [.macOS(.v15)], | ||
| products: [ | ||
| .library(name: "AppleFMBridge", type: .static, targets: ["AppleFMBridge"]) | ||
| ], | ||
| targets: [ | ||
| .target(name: "AppleFMBridge") | ||
| ] | ||
| ) |
61 changes: 61 additions & 0 deletions
61
providers/applefm/bridge/Sources/AppleFMBridge/Exports.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| // The C surface of the bridge. | ||
| // | ||
| // This file is the only place where Rust and Swift meet: each | ||
| // `@_cdecl` function below is callable as a plain C function from the | ||
| // crate's `src/ffi.rs`. Payloads cross the boundary as JSON strings — | ||
| // the same idea as an HTTP provider's wire format, minus the network. | ||
| // | ||
| // Memory contract: every `char*` returned here is `strdup`-allocated | ||
| // and must be released by the caller via `afm_string_free`. | ||
|
|
||
| import Foundation | ||
| #if canImport(FoundationModels) | ||
| import FoundationModels | ||
| #endif | ||
|
|
||
| private func cString(_ s: String) -> UnsafeMutablePointer<CChar>? { | ||
| strdup(s) | ||
| } | ||
|
|
||
| private func availabilityJSON() -> String { | ||
| #if canImport(FoundationModels) | ||
| if #available(macOS 26.0, *) { | ||
| switch SystemLanguageModel.default.availability { | ||
| case .available: | ||
| return #"{"available":true}"# | ||
| case .unavailable(let reason): | ||
| let why: String | ||
| switch reason { | ||
| case .deviceNotEligible: | ||
| why = "this device is not eligible for Apple Intelligence" | ||
| case .appleIntelligenceNotEnabled: | ||
| why = "Apple Intelligence is not enabled in System Settings" | ||
| case .modelNotReady: | ||
| why = "model assets are not downloaded yet; retry once Apple Intelligence finishes setting up" | ||
| @unknown default: | ||
| why = "the model is unavailable for an unknown reason" | ||
| } | ||
| return #"{"available":false,"reason":"\#(why)"}"# | ||
| @unknown default: | ||
| return #"{"available":false,"reason":"unrecognized availability state"}"# | ||
| } | ||
| } else { | ||
| return #"{"available":false,"reason":"macOS 26 or later is required"}"# | ||
| } | ||
| #else | ||
| return #"{"available":false,"reason":"built against an SDK without the FoundationModels framework"}"# | ||
| #endif | ||
| } | ||
|
|
||
| /// Probe whether the on-device Apple foundation model can be used. | ||
| /// Returns `{"available": bool, "reason"?: string}`. | ||
| @_cdecl("afm_availability") | ||
| public func afm_availability() -> UnsafeMutablePointer<CChar>? { | ||
| cString(availabilityJSON()) | ||
| } | ||
|
|
||
| /// Release a string previously returned by this bridge. | ||
| @_cdecl("afm_string_free") | ||
| public func afm_string_free(_ ptr: UnsafeMutablePointer<CChar>?) { | ||
| free(ptr) | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| //! Compiles the Swift bridge (`bridge/`) and links it into the crate. | ||
| //! | ||
| //! The bridge is the only piece of this provider that talks to Apple's | ||
| //! FoundationModels framework — see `bridge/Sources/AppleFMBridge`. It is | ||
| //! built with SwiftPM into a static library and linked here, so users only | ||
| //! ever run `cargo build`. | ||
| //! | ||
| //! On non-macOS targets (or when `APPLEFM_SKIP_BRIDGE` is set, or on | ||
| //! docs.rs) the bridge is not built and the crate falls back to a stub | ||
| //! that reports the model as unavailable. This keeps the workspace | ||
| //! compiling everywhere without Xcode. | ||
|
|
||
| use std::env; | ||
| use std::path::PathBuf; | ||
| use std::process::Command; | ||
|
|
||
| fn main() { | ||
| println!("cargo:rustc-check-cfg=cfg(applefm_bridge)"); | ||
| println!("cargo:rerun-if-changed=bridge/Sources"); | ||
| println!("cargo:rerun-if-changed=bridge/Package.swift"); | ||
| println!("cargo:rerun-if-env-changed=APPLEFM_SKIP_BRIDGE"); | ||
|
|
||
| let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap_or_default(); | ||
| let docsrs = env::var("DOCS_RS").is_ok(); | ||
| let skip = env::var("APPLEFM_SKIP_BRIDGE").is_ok(); | ||
| if target_os != "macos" || docsrs || skip { | ||
| // Stub mode: `cfg(applefm_bridge)` is not emitted. | ||
| return; | ||
| } | ||
|
|
||
| let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()); | ||
| let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap()); | ||
| let scratch = out_dir.join("bridge-build"); | ||
|
|
||
| let status = Command::new("swift") | ||
| .arg("build") | ||
| .args(["-c", "release"]) | ||
| .arg("--package-path") | ||
| .arg(manifest_dir.join("bridge")) | ||
| .arg("--scratch-path") | ||
| .arg(&scratch) | ||
| .status() | ||
| .expect( | ||
| "failed to invoke `swift build` — building chat-applefm on macOS \ | ||
| requires the Xcode command line tools (xcode-select --install)", | ||
| ); | ||
| assert!( | ||
| status.success(), | ||
| "swift build of the AppleFMBridge package failed" | ||
| ); | ||
|
|
||
| println!( | ||
| "cargo:rustc-link-search=native={}", | ||
| scratch.join("release").display() | ||
| ); | ||
| println!("cargo:rustc-link-lib=static=AppleFMBridge"); | ||
|
|
||
| // Swift runtime stubs from the SDK so the autolinked -lswiftCore etc. | ||
| // resolve at link time. At run time the OS runtime in /usr/lib/swift | ||
| // is used. | ||
| let sdk_path = Command::new("xcrun") | ||
| .args(["--show-sdk-path"]) | ||
| .output() | ||
| .expect("failed to invoke `xcrun --show-sdk-path`"); | ||
| let sdk_path = String::from_utf8_lossy(&sdk_path.stdout).trim().to_string(); | ||
| println!("cargo:rustc-link-search=native={sdk_path}/usr/lib/swift"); | ||
| println!("cargo:rustc-link-search=native=/usr/lib/swift"); | ||
|
|
||
| // Weak-link so binaries still load on macOS < 26; the bridge guards | ||
| // every use behind `#available` and reports unavailability instead. | ||
| println!("cargo:rustc-link-arg=-Wl,-weak_framework,FoundationModels"); | ||
|
|
||
| println!("cargo:rustc-cfg=applefm_bridge"); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| //! Safe wrappers over the bridge's C surface. | ||
| //! | ||
| //! When the crate is built without the bridge (non-macOS target, | ||
| //! docs.rs, or `APPLEFM_SKIP_BRIDGE=1`), the stub module below stands in | ||
| //! and reports the model as unavailable — same shape, no linking. | ||
|
|
||
| #[cfg(applefm_bridge)] | ||
| mod real { | ||
| use std::ffi::{CStr, c_char}; | ||
|
|
||
| unsafe extern "C" { | ||
| fn afm_availability() -> *mut c_char; | ||
| fn afm_string_free(ptr: *mut c_char); | ||
| } | ||
|
|
||
| pub fn availability_json() -> String { | ||
| // SAFETY: the bridge returns a NUL-terminated string allocated | ||
| // with `strdup`; we copy it out and hand it straight back to | ||
| // `afm_string_free`. A null return is mapped to an error value. | ||
| unsafe { | ||
| let ptr = afm_availability(); | ||
| if ptr.is_null() { | ||
| return r#"{"available":false,"reason":"bridge returned null"}"#.to_owned(); | ||
| } | ||
| let json = CStr::from_ptr(ptr).to_string_lossy().into_owned(); | ||
| afm_string_free(ptr); | ||
| json | ||
| } | ||
| } | ||
| } | ||
|
|
||
| #[cfg(not(applefm_bridge))] | ||
| mod stub { | ||
| pub fn availability_json() -> String { | ||
| r#"{"available":false,"reason":"chat-applefm was built without the Swift bridge (non-macOS target, docs build, or APPLEFM_SKIP_BRIDGE set)"}"# | ||
| .to_owned() | ||
| } | ||
| } | ||
|
|
||
| #[cfg(applefm_bridge)] | ||
| pub(crate) use real::availability_json; | ||
| #[cfg(not(applefm_bridge))] | ||
| pub(crate) use stub::availability_json; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| //! Apple on-device foundation model provider for chat-rs. | ||
| //! | ||
| //! Talks to the ~3B-parameter model that ships with Apple Intelligence | ||
| //! (macOS 26+) through Apple's FoundationModels framework. There is no | ||
| //! HTTP and no weights file: the OS owns the model; this crate owns the | ||
| //! translation. | ||
| //! | ||
| //! ## How it connects | ||
| //! | ||
| //! Rust cannot call Swift directly, so the crate embeds a small Swift | ||
| //! package (`bridge/`) exposing a few plain C functions. Requests and | ||
| //! responses cross that boundary as JSON strings — the same mental model | ||
| //! as an HTTP provider's wire format, minus the network. The bridge is | ||
| //! compiled automatically by `build.rs`; users only run `cargo build`. | ||
| //! | ||
| //! On non-macOS targets the crate still compiles (with a stub bridge) | ||
| //! and reports the model as unavailable. | ||
| //! | ||
| //! ## Current scope | ||
| //! | ||
| //! This is the first slice: probing model availability. | ||
| //! | ||
| //! ```no_run | ||
| //! let probe = chat_applefm::availability(); | ||
| //! if probe.available { | ||
| //! println!("on-device model ready"); | ||
| //! } else { | ||
| //! println!("unavailable: {}", probe.reason.as_deref().unwrap_or("?")); | ||
| //! } | ||
| //! ``` | ||
|
|
||
| mod ffi; | ||
|
|
||
| use serde::Deserialize; | ||
|
|
||
| /// Result of probing whether the on-device Apple model is usable here. | ||
| #[derive(Debug, Clone, Deserialize)] | ||
| pub struct Availability { | ||
| /// Whether a session can be created on this machine right now. | ||
| pub available: bool, | ||
| /// Human-readable explanation when `available` is `false` | ||
| /// (ineligible hardware, Apple Intelligence disabled, assets still | ||
| /// downloading, OS too old, or a bridge-less build). | ||
| #[serde(default)] | ||
| pub reason: Option<String>, | ||
| } | ||
|
|
||
| /// Ask the OS whether the Apple Intelligence on-device model can be used. | ||
| /// | ||
| /// Cheap to call; performs no generation. This is the recommended first | ||
| /// call before constructing a client, and what router strategies should | ||
| /// consult when deciding whether this provider is eligible at all. | ||
| pub fn availability() -> Availability { | ||
| let json = ffi::availability_json(); | ||
| serde_json::from_str(&json).unwrap_or_else(|_| Availability { | ||
| available: false, | ||
| reason: Some(format!("malformed bridge reply: {json}")), | ||
| }) | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.