foundation-models 0.11.3

Safe Rust bindings for Apple's FoundationModels framework - on-device LLM on macOS 26+
Documentation
import Foundation

#if canImport(FoundationModels) && FOUNDATION_MODELS_HAS_MACOS26_SDK
import FoundationModels
#endif

#if canImport(FoundationModels) && FOUNDATION_MODELS_HAS_MACOS26_SDK
@available(macOS 26.0, *)
func feedbackSentiment(from rawValue: String?) -> LanguageModelFeedback.Sentiment? {
    switch rawValue {
    case nil:
        return nil
    case "positive":
        return .positive
    case "negative":
        return .negative
    default:
        return .neutral
    }
}

@available(macOS 26.0, *)
func feedbackIssueCategory(from rawValue: String) -> LanguageModelFeedback.Issue.Category {
    switch rawValue {
    case "too_verbose":
        return .tooVerbose
    case "did_not_follow_instructions":
        return .didNotFollowInstructions
    case "incorrect":
        return .incorrect
    case "stereotype_or_bias":
        return .stereotypeOrBias
    case "suggestive_or_sexual":
        return .suggestiveOrSexual
    case "vulgar_or_offensive":
        return .vulgarOrOffensive
    case "triggered_guardrail_unexpectedly":
        return .triggeredGuardrailUnexpectedly
    default:
        return .unhelpful
    }
}
#endif

@_cdecl("fm_session_create_ex")
public func fm_session_create_ex(
    _ modelPtr: UnsafeMutableRawPointer?,
    _ instructionsJSON: UnsafePointer<CChar>?,
    _ transcriptJSON: UnsafePointer<CChar>?,
    _ toolsJSON: UnsafePointer<CChar>?,
    _ toolContext: UnsafeMutableRawPointer?,
    _ toolCallback: (@convention(c) (
        UnsafeMutableRawPointer?,
        UnsafePointer<CChar>?,
        UnsafePointer<CChar>?,
        UnsafeMutablePointer<UnsafeMutablePointer<CChar>?>?,
        UnsafeMutablePointer<UnsafeMutablePointer<CChar>?>?
    ) -> Int32)?,
    _ errorOut: UnsafeMutablePointer<UnsafeMutablePointer<CChar>?>?
) -> UnsafeMutableRawPointer? {
    #if canImport(FoundationModels) && FOUNDATION_MODELS_HAS_MACOS26_SDK
    if #available(macOS 26.0, *) {
        do {
            let model = systemModel(from: modelPtr)
            let tools = try buildTools(
                specsJSON: toolsJSON.map { String(cString: $0) },
                context: toolContext,
                callback: toolCallback
            )
            let session: LanguageModelSession
            if let transcriptJSON {
                let transcript = try decodeTranscript(from: String(cString: transcriptJSON))
                session = LanguageModelSession(model: model, tools: tools, transcript: transcript)
            } else if let instructionsJSON {
                let instructionsBridge = try decodeBridge(String(cString: instructionsJSON), as: BridgeInstructions.self)
                let instructions = try buildInstructions(from: instructionsBridge)
                session = LanguageModelSession(model: model, tools: tools, instructions: instructions)
            } else {
                session = LanguageModelSession(model: model, tools: tools, instructions: nil)
            }
            return Unmanaged.passRetained(session).toOpaque()
        } catch {
            writeErrorOut(errorOut, error.localizedDescription)
            return nil
        }
    }
    #endif
    writeErrorOut(errorOut, "FoundationModels requires macOS 26.0 or newer")
    return nil
}

@_cdecl("fm_session_prewarm_prompt_json")
public func fm_session_prewarm_prompt_json(
    _ sessionPtr: UnsafeMutableRawPointer,
    _ promptJSON: UnsafePointer<CChar>?,
    _ errorOut: UnsafeMutablePointer<UnsafeMutablePointer<CChar>?>?
) -> Int32 {
    #if canImport(FoundationModels) && FOUNDATION_MODELS_HAS_MACOS26_SDK
    if #available(macOS 26.0, *) {
        do {
            let session = Unmanaged<LanguageModelSession>.fromOpaque(sessionPtr).takeUnretainedValue()
            let prompt = try promptJSON.map { try buildPrompt(from: decodeBridge(String(cString: $0), as: BridgePrompt.self)) }
            session.prewarm(promptPrefix: prompt)
            return FM_OK
        } catch {
            let (code, message) = mapError(error)
            writeErrorOut(errorOut, message)
            return code
        }
    }
    #endif
    writeErrorOut(errorOut, "FoundationModels requires macOS 26.0 or newer")
    return FM_MODEL_UNAVAILABLE
}

@_cdecl("fm_session_respond_request_json")
public func fm_session_respond_request_json(
    _ sessionPtr: UnsafeMutableRawPointer,
    _ requestJSON: UnsafePointer<CChar>,
    _ context: UnsafeMutableRawPointer?,
    _ callback: @convention(c) (
        UnsafeMutableRawPointer?,
        UnsafeMutablePointer<CChar>?,
        UnsafeMutablePointer<CChar>?,
        Int32
    ) -> Void
) {
    #if canImport(FoundationModels) && FOUNDATION_MODELS_HAS_MACOS26_SDK
    if #available(macOS 26.0, *) {
        let session = Unmanaged<LanguageModelSession>.fromOpaque(sessionPtr).takeUnretainedValue()
        let requestJSONString = String(cString: requestJSON)
        Task.detached {
            do {
                let request = try decodeBridge(requestJSONString, as: BridgeResponseRequest.self)
                let prompt = try buildPrompt(from: request.prompt)
                let options = buildOptions(from: request.options)
                if let schemaJSON = request.schemaJSON {
                    let schema = try decodeGenerationSchema(from: schemaJSON)
                    let response = try await session.respond(
                        to: prompt,
                        schema: schema,
                        includeSchemaInPrompt: request.includeSchemaInPrompt ?? true,
                        options: options
                    )
                    let transcriptJSON = try encodeTranscriptJSON(entries: response.transcriptEntries)
                    let payload = BridgeStructuredResponse(
                        content: bridgeGeneratedContent(response.content),
                        rawContent: bridgeGeneratedContent(response.rawContent),
                        transcriptJSON: transcriptJSON
                    )
                    callback(context, ffiString(try encodeBridge(payload)), nil, FM_OK)
                } else {
                    let response = try await session.respond(to: prompt, options: options)
                    let transcriptJSON = try encodeTranscriptJSON(entries: response.transcriptEntries)
                    let payload = BridgeTextResponse(
                        content: response.content,
                        rawContent: bridgeGeneratedContent(response.rawContent),
                        transcriptJSON: transcriptJSON
                    )
                    callback(context, ffiString(try encodeBridge(payload)), nil, FM_OK)
                }
            } catch {
                let (code, message) = mapError(error)
                callback(context, nil, ffiString(message), code)
            }
        }
        return
    }
    #endif
    callback(context, nil, ffiString("FoundationModels requires macOS 26.0 or newer"), FM_MODEL_UNAVAILABLE)
}

@_cdecl("fm_session_stream_request_json")
public func fm_session_stream_request_json(
    _ sessionPtr: UnsafeMutableRawPointer,
    _ requestJSON: UnsafePointer<CChar>,
    _ context: UnsafeMutableRawPointer?,
    _ callback: @convention(c) (
        UnsafeMutableRawPointer?,
        UnsafeMutablePointer<CChar>?,
        Bool,
        Int32
    ) -> Void
) {
    #if canImport(FoundationModels) && FOUNDATION_MODELS_HAS_MACOS26_SDK
    if #available(macOS 26.0, *) {
        let session = Unmanaged<LanguageModelSession>.fromOpaque(sessionPtr).takeUnretainedValue()
        let requestJSONString = String(cString: requestJSON)
        Task.detached {
            do {
                let request = try decodeBridge(requestJSONString, as: BridgeResponseRequest.self)
                let prompt = try buildPrompt(from: request.prompt)
                let options = buildOptions(from: request.options)
                if let schemaJSON = request.schemaJSON {
                    let schema = try decodeGenerationSchema(from: schemaJSON)
                    let stream = session.streamResponse(
                        to: prompt,
                        schema: schema,
                        includeSchemaInPrompt: request.includeSchemaInPrompt ?? true,
                        options: options
                    )
                    for try await snapshot in stream {
                        let payload = BridgeStructuredStreamSnapshot(
                            content: bridgeGeneratedContent(snapshot.content),
                            rawContent: bridgeGeneratedContent(snapshot.rawContent),
                            isComplete: snapshot.content.isComplete
                        )
                        callback(context, ffiString(try encodeBridge(payload)), false, FM_OK)
                    }
                } else {
                    let stream = session.streamResponse(to: prompt, options: options)
                    var lastEmitted = ""
                    for try await snapshot in stream {
                        let full = snapshot.content
                        let delta: String
                        if full.hasPrefix(lastEmitted) {
                            delta = String(full.dropFirst(lastEmitted.count))
                        } else {
                            delta = full
                        }
                        lastEmitted = full
                        let payload = BridgeTextStreamSnapshot(
                            delta: delta,
                            content: full,
                            rawContent: bridgeGeneratedContent(snapshot.rawContent)
                        )
                        callback(context, ffiString(try encodeBridge(payload)), false, FM_OK)
                    }
                }
                callback(context, nil, true, FM_OK)
            } catch {
                let (code, message) = mapError(error)
                callback(context, ffiString(message), true, code)
            }
        }
        return
    }
    #endif
    callback(context, ffiString("FoundationModels requires macOS 26.0 or newer"), true, FM_MODEL_UNAVAILABLE)
}

@_cdecl("fm_session_log_feedback_attachment_json")
public func fm_session_log_feedback_attachment_json(
    _ sessionPtr: UnsafeMutableRawPointer,
    _ requestJSON: UnsafePointer<CChar>,
    _ lengthOut: UnsafeMutablePointer<Int>?,
    _ errorOut: UnsafeMutablePointer<UnsafeMutablePointer<CChar>?>?
) -> UnsafeMutableRawPointer? {
    #if canImport(FoundationModels) && FOUNDATION_MODELS_HAS_MACOS26_SDK
    if #available(macOS 26.0, *) {
        do {
            let session = Unmanaged<LanguageModelSession>.fromOpaque(sessionPtr).takeUnretainedValue()
            let request = try decodeBridge(String(cString: requestJSON), as: BridgeFeedbackRequest.self)
            let issues = request.issues.map {
                LanguageModelFeedback.Issue(
                    category: feedbackIssueCategory(from: $0.category),
                    explanation: $0.explanation
                )
            }
            let sentiment = feedbackSentiment(from: request.sentiment)
            let data: Data
            if let desiredOutputTranscriptJSON = request.desiredOutputTranscriptJSON {
                data = session.logFeedbackAttachment(
                    sentiment: sentiment,
                    issues: issues,
                    desiredOutput: try firstTranscriptEntry(from: desiredOutputTranscriptJSON)
                )
            } else if let desiredResponseText = request.desiredResponseText {
                data = session.logFeedbackAttachment(
                    sentiment: sentiment,
                    issues: issues,
                    desiredResponseText: desiredResponseText
                )
            } else if let desiredResponseContent = request.desiredResponseContent {
                data = session.logFeedbackAttachment(
                    sentiment: sentiment,
                    issues: issues,
                    desiredResponseContent: try buildGeneratedContent(from: desiredResponseContent)
                )
            } else {
                data = session.logFeedbackAttachment(
                    sentiment: sentiment,
                    issues: issues,
                    desiredOutput: nil
                )
            }
            lengthOut?.pointee = data.count
            return copyDataToHeap(data)
        } catch {
            writeErrorOut(errorOut, error.localizedDescription)
            return nil
        }
    }
    #endif
    writeErrorOut(errorOut, "FoundationModels requires macOS 26.0 or newer")
    return nil
}