|
| 1 | +--- |
| 2 | +name: foundation-models-on-device |
| 3 | +description: Apple FoundationModels framework for on-device LLM — text generation, guided generation with @Generable, tool calling, and snapshot streaming in iOS 26+. |
| 4 | +--- |
| 5 | + |
| 6 | +# FoundationModels: On-Device LLM (iOS 26) |
| 7 | + |
| 8 | +Patterns for integrating Apple's on-device language model into apps using the FoundationModels framework. Covers text generation, structured output with `@Generable`, custom tool calling, and snapshot streaming — all running on-device for privacy and offline support. |
| 9 | + |
| 10 | +## When to Activate |
| 11 | + |
| 12 | +- Building AI-powered features using Apple Intelligence on-device |
| 13 | +- Generating or summarizing text without cloud dependency |
| 14 | +- Extracting structured data from natural language input |
| 15 | +- Implementing custom tool calling for domain-specific AI actions |
| 16 | +- Streaming structured responses for real-time UI updates |
| 17 | +- Need privacy-preserving AI (no data leaves the device) |
| 18 | + |
| 19 | +## Core Pattern — Availability Check |
| 20 | + |
| 21 | +Always check model availability before creating a session: |
| 22 | + |
| 23 | +```swift |
| 24 | +struct GenerativeView: View { |
| 25 | + private var model = SystemLanguageModel.default |
| 26 | + |
| 27 | + var body: some View { |
| 28 | + switch model.availability { |
| 29 | + case .available: |
| 30 | + ContentView() |
| 31 | + case .unavailable(.deviceNotEligible): |
| 32 | + Text("Device not eligible for Apple Intelligence") |
| 33 | + case .unavailable(.appleIntelligenceNotEnabled): |
| 34 | + Text("Please enable Apple Intelligence in Settings") |
| 35 | + case .unavailable(.modelNotReady): |
| 36 | + Text("Model is downloading or not ready") |
| 37 | + case .unavailable(let other): |
| 38 | + Text("Model unavailable: \(other)") |
| 39 | + } |
| 40 | + } |
| 41 | +} |
| 42 | +``` |
| 43 | + |
| 44 | +## Core Pattern — Basic Session |
| 45 | + |
| 46 | +```swift |
| 47 | +// Single-turn: create a new session each time |
| 48 | +let session = LanguageModelSession() |
| 49 | +let response = try await session.respond(to: "What's a good month to visit Paris?") |
| 50 | +print(response.content) |
| 51 | + |
| 52 | +// Multi-turn: reuse session for conversation context |
| 53 | +let session = LanguageModelSession(instructions: """ |
| 54 | + You are a cooking assistant. |
| 55 | + Provide recipe suggestions based on ingredients. |
| 56 | + Keep suggestions brief and practical. |
| 57 | + """) |
| 58 | + |
| 59 | +let first = try await session.respond(to: "I have chicken and rice") |
| 60 | +let followUp = try await session.respond(to: "What about a vegetarian option?") |
| 61 | +``` |
| 62 | + |
| 63 | +Key points for instructions: |
| 64 | +- Define the model's role ("You are a mentor") |
| 65 | +- Specify what to do ("Help extract calendar events") |
| 66 | +- Set style preferences ("Respond as briefly as possible") |
| 67 | +- Add safety measures ("Respond with 'I can't help with that' for dangerous requests") |
| 68 | + |
| 69 | +## Core Pattern — Guided Generation with @Generable |
| 70 | + |
| 71 | +Generate structured Swift types instead of raw strings: |
| 72 | + |
| 73 | +### 1. Define a Generable Type |
| 74 | + |
| 75 | +```swift |
| 76 | +@Generable(description: "Basic profile information about a cat") |
| 77 | +struct CatProfile { |
| 78 | + var name: String |
| 79 | + |
| 80 | + @Guide(description: "The age of the cat", .range(0...20)) |
| 81 | + var age: Int |
| 82 | + |
| 83 | + @Guide(description: "A one sentence profile about the cat's personality") |
| 84 | + var profile: String |
| 85 | +} |
| 86 | +``` |
| 87 | + |
| 88 | +### 2. Request Structured Output |
| 89 | + |
| 90 | +```swift |
| 91 | +let response = try await session.respond( |
| 92 | + to: "Generate a cute rescue cat", |
| 93 | + generating: CatProfile.self |
| 94 | +) |
| 95 | + |
| 96 | +// Access structured fields directly |
| 97 | +print("Name: \(response.content.name)") |
| 98 | +print("Age: \(response.content.age)") |
| 99 | +print("Profile: \(response.content.profile)") |
| 100 | +``` |
| 101 | + |
| 102 | +### Supported @Guide Constraints |
| 103 | + |
| 104 | +- `.range(0...20)` — numeric range |
| 105 | +- `.count(3)` — array element count |
| 106 | +- `description:` — semantic guidance for generation |
| 107 | + |
| 108 | +## Core Pattern — Tool Calling |
| 109 | + |
| 110 | +Let the model invoke custom code for domain-specific tasks: |
| 111 | + |
| 112 | +### 1. Define a Tool |
| 113 | + |
| 114 | +```swift |
| 115 | +struct RecipeSearchTool: Tool { |
| 116 | + let name = "recipe_search" |
| 117 | + let description = "Search for recipes matching a given term and return a list of results." |
| 118 | + |
| 119 | + @Generable |
| 120 | + struct Arguments { |
| 121 | + var searchTerm: String |
| 122 | + var numberOfResults: Int |
| 123 | + } |
| 124 | + |
| 125 | + func call(arguments: Arguments) async throws -> ToolOutput { |
| 126 | + let recipes = await searchRecipes( |
| 127 | + term: arguments.searchTerm, |
| 128 | + limit: arguments.numberOfResults |
| 129 | + ) |
| 130 | + return .string(recipes.map { "- \($0.name): \($0.description)" }.joined(separator: "\n")) |
| 131 | + } |
| 132 | +} |
| 133 | +``` |
| 134 | + |
| 135 | +### 2. Create Session with Tools |
| 136 | + |
| 137 | +```swift |
| 138 | +let session = LanguageModelSession(tools: [RecipeSearchTool()]) |
| 139 | +let response = try await session.respond(to: "Find me some pasta recipes") |
| 140 | +``` |
| 141 | + |
| 142 | +### 3. Handle Tool Errors |
| 143 | + |
| 144 | +```swift |
| 145 | +do { |
| 146 | + let answer = try await session.respond(to: "Find a recipe for tomato soup.") |
| 147 | +} catch let error as LanguageModelSession.ToolCallError { |
| 148 | + print(error.tool.name) |
| 149 | + if case .databaseIsEmpty = error.underlyingError as? RecipeSearchToolError { |
| 150 | + // Handle specific tool error |
| 151 | + } |
| 152 | +} |
| 153 | +``` |
| 154 | + |
| 155 | +## Core Pattern — Snapshot Streaming |
| 156 | + |
| 157 | +Stream structured responses for real-time UI with `PartiallyGenerated` types: |
| 158 | + |
| 159 | +```swift |
| 160 | +@Generable |
| 161 | +struct TripIdeas { |
| 162 | + @Guide(description: "Ideas for upcoming trips") |
| 163 | + var ideas: [String] |
| 164 | +} |
| 165 | + |
| 166 | +let stream = session.streamResponse( |
| 167 | + to: "What are some exciting trip ideas?", |
| 168 | + generating: TripIdeas.self |
| 169 | +) |
| 170 | + |
| 171 | +for try await partial in stream { |
| 172 | + // partial: TripIdeas.PartiallyGenerated (all properties Optional) |
| 173 | + print(partial) |
| 174 | +} |
| 175 | +``` |
| 176 | + |
| 177 | +### SwiftUI Integration |
| 178 | + |
| 179 | +```swift |
| 180 | +@State private var partialResult: TripIdeas.PartiallyGenerated? |
| 181 | +@State private var errorMessage: String? |
| 182 | + |
| 183 | +var body: some View { |
| 184 | + List { |
| 185 | + ForEach(partialResult?.ideas ?? [], id: \.self) { idea in |
| 186 | + Text(idea) |
| 187 | + } |
| 188 | + } |
| 189 | + .overlay { |
| 190 | + if let errorMessage { Text(errorMessage).foregroundStyle(.red) } |
| 191 | + } |
| 192 | + .task { |
| 193 | + do { |
| 194 | + let stream = session.streamResponse(to: prompt, generating: TripIdeas.self) |
| 195 | + for try await partial in stream { |
| 196 | + partialResult = partial |
| 197 | + } |
| 198 | + } catch { |
| 199 | + errorMessage = error.localizedDescription |
| 200 | + } |
| 201 | + } |
| 202 | +} |
| 203 | +``` |
| 204 | + |
| 205 | +## Key Design Decisions |
| 206 | + |
| 207 | +| Decision | Rationale | |
| 208 | +|----------|-----------| |
| 209 | +| On-device execution | Privacy — no data leaves the device; works offline | |
| 210 | +| 4,096 token limit | On-device model constraint; chunk large data across sessions | |
| 211 | +| Snapshot streaming (not deltas) | Structured output friendly; each snapshot is a complete partial state | |
| 212 | +| `@Generable` macro | Compile-time safety for structured generation; auto-generates `PartiallyGenerated` type | |
| 213 | +| Single request per session | `isResponding` prevents concurrent requests; create multiple sessions if needed | |
| 214 | +| `response.content` (not `.output`) | Correct API — always access results via `.content` property | |
| 215 | + |
| 216 | +## Best Practices |
| 217 | + |
| 218 | +- **Always check `model.availability`** before creating a session — handle all unavailability cases |
| 219 | +- **Use `instructions`** to guide model behavior — they take priority over prompts |
| 220 | +- **Check `isResponding`** before sending a new request — sessions handle one request at a time |
| 221 | +- **Access `response.content`** for results — not `.output` |
| 222 | +- **Break large inputs into chunks** — 4,096 token limit applies to instructions + prompt + output combined |
| 223 | +- **Use `@Generable`** for structured output — stronger guarantees than parsing raw strings |
| 224 | +- **Use `GenerationOptions(temperature:)`** to tune creativity (higher = more creative) |
| 225 | +- **Monitor with Instruments** — use Xcode Instruments to profile request performance |
| 226 | + |
| 227 | +## Anti-Patterns to Avoid |
| 228 | + |
| 229 | +- Creating sessions without checking `model.availability` first |
| 230 | +- Sending inputs exceeding the 4,096 token context window |
| 231 | +- Attempting concurrent requests on a single session |
| 232 | +- Using `.output` instead of `.content` to access response data |
| 233 | +- Parsing raw string responses when `@Generable` structured output would work |
| 234 | +- Building complex multi-step logic in a single prompt — break into multiple focused prompts |
| 235 | +- Assuming the model is always available — device eligibility and settings vary |
| 236 | + |
| 237 | +## When to Use |
| 238 | + |
| 239 | +- On-device text generation for privacy-sensitive apps |
| 240 | +- Structured data extraction from user input (forms, natural language commands) |
| 241 | +- AI-assisted features that must work offline |
| 242 | +- Streaming UI that progressively shows generated content |
| 243 | +- Domain-specific AI actions via tool calling (search, compute, lookup) |
0 commit comments