https://github.com/ricky-stone/swiftfm
Super-simple API for working with Foundation Models in Swift
https://github.com/ricky-stone/swiftfm
ai apple-intelligence foundationmodels generative-ai ios language-model machine-learning macos swift swift6 swiftpm
Last synced: 2 months ago
JSON representation
Super-simple API for working with Foundation Models in Swift
- Host: GitHub
- URL: https://github.com/ricky-stone/swiftfm
- Owner: ricky-stone
- Created: 2025-08-16T01:48:35.000Z (10 months ago)
- Default Branch: main
- Last Pushed: 2026-02-14T02:17:19.000Z (4 months ago)
- Last Synced: 2026-02-14T08:59:36.152Z (4 months ago)
- Topics: ai, apple-intelligence, foundationmodels, generative-ai, ios, language-model, machine-learning, macos, swift, swift6, swiftpm
- Language: Swift
- Homepage:
- Size: 57.6 KB
- Stars: 11
- Watchers: 2
- Forks: 1
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- Funding: .github/FUNDING.yml
Awesome Lists containing this project
README
# SwiftFM
[](https://www.swift.org)
[](https://developer.apple.com/documentation/foundationmodels)
[](https://github.com/ricky-stone/SwiftFM/releases)
[](https://github.com/ricky-stone/SwiftFM/blob/main/LICENSE)
[](https://github.com/ricky-stone/SwiftFM/discussions)
[](https://github.com/ricky-stone/SwiftFM/stargazers)
SwiftFM is a beginner-first Swift wrapper around Apple Foundation Models.
Version `2.0.0` keeps the original power-user features, but makes the package feel much more like SwiftUI:
- modifier-style config chains
- modifier-style request chains
- modifier-style prompt chains
- dynamic schemas and structured streaming
- locale helpers, token counting, and feedback attachment export
- custom adapter helpers
If you already use the older `Config(...)` and `RequestConfig(...)` style, it still works.
## Requirements
- Swift `6.2+`
- Xcode `26+`
- iOS `26+`
- macOS `26+`
- visionOS `26+`
- Apple Intelligence enabled on supported hardware
## Installation
Add the package with Swift Package Manager:
```swift
.package(url: "https://github.com/ricky-stone/SwiftFM.git", from: "2.0.0")
```
## 30-Second Start
```swift
import SwiftFM
let fm = SwiftFM()
let text = try await fm.generateText(
for: "Explain a snooker century break in one sentence."
)
print(text)
```
## Beginner Style
This is the new `2.0` feel.
You start from `SwiftFM.configuration()`, `SwiftFM.request()`, or `SwiftFM.prompt(...)`, then chain small modifiers.
```swift
import SwiftFM
let fm = SwiftFM(
config: SwiftFM.configuration()
.system("You are clear, friendly, and concise.")
.model(.general)
.temperature(0.3)
.maximumResponseTokens(180)
.postProcessing(.readableParagraphs)
)
let text = try await fm.generateText(
for: "Write a short beginner explanation of snooker safety play."
)
```
## One-Off Request Overrides
Use `SwiftFM.request()` when you only want to change one call.
```swift
let text = try await fm.generateText(
for: "Write a short match preview.",
request: SwiftFM.request()
.temperature(0.2)
.maximumResponseTokens(120)
.postProcessing(.readableParagraphs)
)
```
## Output Cleanup
`TextPostProcessing` is still here, and now it chains nicely too.
```swift
let fm = SwiftFM(
config: SwiftFM.configuration()
.postProcessing(
.none
.trimmingWhitespace()
.collapsingSpacesAndTabs()
.limitingConsecutiveNewlines(to: 2)
.roundingFloatingPointNumbers(to: 0)
)
)
```
## Prompt Builder
`PromptSpec` now chains cleanly too.
```swift
let spec = SwiftFM.prompt("Write a pre-match analysis.")
.rule("Use plain text only")
.rule("Do not use markdown")
.requirement("Exactly 3 short paragraphs")
.tone("Professional and engaging")
let text = try await fm.generateText(from: spec)
print(text)
```
## Context Models
If your app already has Swift models, pass them directly.
```swift
struct MatchVision: Codable, Sendable {
let home: String
let away: String
let venue: String
let bestOfFrames: Int
}
let vision = MatchVision(
home: "Judd Trump",
away: "Mark Allen",
venue: "Alexandra Palace",
bestOfFrames: 11
)
let summary = try await fm.generateText(
for: "Write a short neutral preview using only this data.",
context: vision,
request: SwiftFM.request()
.postProcessing(.readableParagraphs)
)
```
### Context Formatting
You can still control how the JSON is embedded in the prompt.
```swift
let text = try await fm.generateText(
for: "Summarize this payload for a beginner.",
context: vision,
request: SwiftFM.request()
.contextOptions(
.init()
.heading("Match Payload")
.jsonFormatting(.compactSorted)
)
)
```
## Text Streaming
SwiftFM still supports both full snapshots and delta chunks.
### Snapshot stream
```swift
for try await snapshot in await fm.streamText(
for: "Explain snooker break-building in three short paragraphs."
) {
print(snapshot)
}
```
### Delta stream
```swift
var text = ""
for try await delta in await fm.streamTextDeltas(
for: "Explain snooker break-building in three short paragraphs."
) {
text += delta
}
```
## SwiftUI Example
```swift
import SwiftUI
import SwiftFM
struct HomeView: View {
@State private var text = ""
@State private var isLoading = true
private let fm = SwiftFM(
config: .beginnerFriendly
.system("You explain things simply.")
.temperature(0.3)
)
var body: some View {
ZStack {
ScrollView {
Text(text)
.frame(maxWidth: .infinity, alignment: .leading)
.padding()
}
if isLoading {
ProgressView("Thinking...")
}
}
.task {
do {
for try await delta in await fm.streamTextDeltas(
from: SwiftFM.prompt("Explain one snooker safety drill.")
.requirement("Exactly 2 short paragraphs")
) {
if isLoading { isLoading = false }
text += delta
}
} catch {
isLoading = false
text = "Error: \(error.localizedDescription)"
}
}
}
}
```
## Typed Output with `@Generable`
```swift
import SwiftFM
import FoundationModels
@Generable
struct MatchPrediction: Decodable, Sendable {
@Guide(description: "Home player")
let home: String
@Guide(description: "Away player")
let away: String
@Guide(description: "Predicted winner")
let winner: String
@Guide(description: "Confidence from 0.0 to 1.0")
let confidence: Double
}
let prediction = try await fm.generateJSON(
for: "Predict this match and return home, away, winner, and confidence.",
as: MatchPrediction.self
)
```
### Structured Streaming
New in `2.0`: you can stream partial typed snapshots, not just text.
```swift
for try await partial in await fm.streamJSON(
for: "Generate a snooker match prediction.",
as: MatchPrediction.self
) {
print(partial.winner ?? "Waiting...")
}
```
### Apple `26.4` Nil Handling
If you want to use Apple's newer explicit-nil generation behavior, you can do that directly with Foundation Models and still use SwiftFM normally:
```swift
@Generable(representNilExplicitlyInGeneratedContent: true)
struct OptionalNote: Decodable, Sendable {
let title: String
let subtitle: String?
}
```
## Dynamic Schemas
New in `2.0`: you can generate runtime-structured content without creating a Swift type first.
```swift
import FoundationModels
let schema = DynamicGenerationSchema(
name: "SnookerNote",
properties: [
.init(
name: "title",
description: "Short title",
schema: .init(type: String.self)
),
.init(
name: "frameCount",
description: "Likely number of frames",
schema: .init(type: Int.self, guides: [.range(1 ... 35)])
)
]
)
let content = try await fm.generateContent(
for: "Generate a snooker match note with a title and likely frame count.",
dynamicSchema: schema
)
let title = try content.value(String.self, forProperty: "title")
let frames = try content.value(Int.self, forProperty: "frameCount")
```
### Dynamic Schema Streaming
```swift
for try await snapshot in await fm.streamContent(
for: "Generate a short structured match note.",
dynamicSchema: schema
) {
print(snapshot.jsonString)
}
```
## Tool Calling
Use tools when the model should fetch live data or call app logic.
```swift
import SwiftFM
import FoundationModels
@Generable
struct MatchLookupArgs: Decodable, Sendable {
@Guide(description: "Match id to fetch")
let id: String
}
struct MatchLookupTool: Tool {
let name = "match_lookup"
let description = "Fetches match JSON by id"
func call(arguments: MatchLookupArgs) async throws -> String {
"""
{"id":"\(arguments.id)","home":"Player A","away":"Player B","venue":"Main Arena"}
"""
}
}
let text = try await fm.generateText(
for: "Use match_lookup for id 123, then write a short neutral preview.",
request: SwiftFM.request()
.tool(MatchLookupTool())
)
```
## Sampling and Temperature
If you want more control, the existing sampling features are still available.
```swift
let fm = SwiftFM(
config: SwiftFM.configuration()
.model(.general)
.temperature(0.2)
.maximumResponseTokens(250)
.sampling(.greedy)
)
```
## Model Selection
SwiftFM model options:
- `.default`
- `.general`
- `.contentTagging`
- `.custom(SystemLanguageModel)`
```swift
let summary = try await fm.generateText(
for: "Give one tactical snooker tip.",
using: .general
)
let label = try await fm.generateText(
for: "Return one label only: billing, support, bug. Text: app crashes at launch.",
using: .contentTagging
)
```
## Custom `SystemLanguageModel`
If you want the raw Apple surface, that is still supported too.
```swift
import FoundationModels
let customModel = SystemLanguageModel(
useCase: .general,
guardrails: .default
)
let fm = SwiftFM(
config: SwiftFM.configuration()
.model(.custom(customModel))
)
```
## Custom Adapters
New in `2.0`: adapter helpers make Apple adapter usage easier to discover.
```swift
let fm = SwiftFM(
config: .beginnerFriendly
.model(try .adapter(named: "MyAdapter"))
)
```
You can also load an adapter from disk:
```swift
let model = try SwiftFM.Model.adapter(fileURL: adapterURL)
let fm = SwiftFM(config: .init(model: model))
```
## Availability, Languages, and Locale
```swift
if SwiftFM.isModelAvailable && SwiftFM.supportsCurrentLocale() {
print("Ready")
} else {
print("Unavailable: \(SwiftFM.modelAvailability)")
}
let languages = SwiftFM.supportedLanguages(for: .default)
print(languages)
```
## Token Counting (`26.4+`)
Apple added token counting in `26.4`, and SwiftFM now exposes it.
```swift
if #available(iOS 26.4, macOS 26.4, visionOS 26.4, *) {
let count = try await fm.tokenCount(
from: SwiftFM.prompt("Explain a snooker safety shot.")
.requirement("One sentence only")
)
print("Prompt tokens:", count)
}
```
There are also static helpers for tools, schemas, and transcript entries:
```swift
if #available(iOS 26.4, macOS 26.4, visionOS 26.4, *) {
let count = try await SwiftFM.tokenCount(for: schema)
print(count)
}
```
## Feedback Attachments
Apple recommends exporting feedback attachments when a response is poor or guardrails trigger unexpectedly.
New in `2.0`: you can export that attachment directly from the current session.
```swift
let attachment = await fm.feedbackAttachment(
sentiment: .negative,
issues: [
.init(category: .didNotFollowInstructions, explanation: "It ignored the output format.")
],
desiredResponseText: "A short plain-text answer in exactly two sentences."
)
print("Attachment bytes:", attachment.count)
```
## Session Helpers
```swift
let fm = SwiftFM(
config: .beginnerFriendly
.system("You are concise.")
)
await fm.prewarm(promptPrefix: "Match analysis")
let busy = await fm.isBusy
let transcript = await fm.transcript
await fm.resetConversation()
```
What these do:
- `prewarm(promptPrefix:)`: reduce first-response latency
- `isBusy`: `true` while the session is generating
- `transcript`: inspect the current conversation history
- `resetConversation()`: clear the session and start fresh with the same base config
## Error Handling
```swift
do {
let text = try await fm.generateText(for: "Analyze this match.")
print(text)
} catch let error as SwiftFM.SwiftFMError {
print(error.localizedDescription)
if let generationError = error.generationError {
print("Foundation Models error:", generationError)
}
} catch {
print(error.localizedDescription)
}
```
## Existing APIs Still Work
`2.0.0` adds fluent builder-style usage, but it does not remove the current feature set.
These still work:
- `SwiftFM(config: .init(...))`
- `RequestConfig(...)`
- `PromptSpec(...)`
- `generateText`
- `streamText`
- `streamTextDeltas`
- `generateJSON`
- request-scoped tools
- context embedding options
- post-processing options
- custom `SystemLanguageModel`
## Version
- Current source version: `2.0.0`
## License
SwiftFM is licensed under the MIT License. See `LICENSE`.