|
| 1 | +--- |
| 2 | +title: Using Claude with Apple Foundation Models |
| 3 | +description: Bringing frontier models to LanguageModelSession with model selection and BYOK |
| 4 | +cover: /images/using-claude-with-apple-foundation-models/cover.png |
| 5 | +date: '2026-06-12' |
| 6 | +categories: foundation-models, ai, llm, claude |
| 7 | +author: |
| 8 | + - artem-novichkov |
| 9 | +--- |
| 10 | + |
| 11 | +At WWDC26 Apple extended the Foundation Models framework with support for server-side language models. The idea is simple: the same `LanguageModelSession` API that drives the on-device model can now drive any remote model that conforms to the `LanguageModel` protocol. Anthropic was quick to adopt it and released [ClaudeForFoundationModels](https://github.com/anthropics/ClaudeForFoundationModels) — a Swift package that makes Claude a drop-in model for your sessions. Streaming, guided generation with `@Generable`, and tool calling work exactly the same way. |
| 12 | + |
| 13 | +In this post, we'll build GiftGenie — a small app that generates gift ideas — and let the user switch between the on-device model and Claude with their own API key. |
| 14 | + |
| 15 | +## Requirements |
| 16 | + |
| 17 | +- Xcode 27 beta; |
| 18 | +- iOS 27 beta (macOS, visionOS, and watchOS 27 are supported as well); |
| 19 | +- A Claude API key from [Claude Console](https://platform.claude.com). |
| 20 | + |
| 21 | +Requests go directly from the app to the Claude API — Apple is not in the request path and doesn't see prompts or responses. Usage is billed to your Anthropic account at standard API pricing. |
| 22 | + |
| 23 | +## Adding the package |
| 24 | + |
| 25 | +Add the package via **File > Add Package Dependencies…** or directly in `Package.swift`: |
| 26 | + |
| 27 | +```swift |
| 28 | +dependencies: [ |
| 29 | + .package(url: "https://github.com/anthropics/ClaudeForFoundationModels.git", from: "0.1.0") |
| 30 | +] |
| 31 | +``` |
| 32 | + |
| 33 | +## First request |
| 34 | + |
| 35 | +`ClaudeLanguageModel` is the entry point. Pass it to `LanguageModelSession` and use the session as usual: |
| 36 | + |
| 37 | +```swift |
| 38 | +import FoundationModels |
| 39 | +import ClaudeForFoundationModels |
| 40 | + |
| 41 | +let model = ClaudeLanguageModel( |
| 42 | + name: .sonnet4_6, |
| 43 | + auth: .apiKey("YOUR_API_KEY") |
| 44 | +) |
| 45 | + |
| 46 | +let session = LanguageModelSession(model: model) |
| 47 | +let response = try await session.respond(to: "Suggest a gift for a sci-fi fan.") |
| 48 | +print(response.content) |
| 49 | +``` |
| 50 | + |
| 51 | +That's the whole integration. Everything else — instructions, transcripts, structured output — works like with `SystemLanguageModel`. |
| 52 | + |
| 53 | +<Callout> |
| 54 | +A key bundled into an app is extractable from the shipping binary. Use `.apiKey` for development or BYOK scenarios only. For production, the package provides `.proxied(headers:)` mode that routes requests through your own backend, and Anthropic promises App Attest-based authentication soon. |
| 55 | +</Callout> |
| 56 | + |
| 57 | +## Generating gift ideas |
| 58 | + |
| 59 | +GiftGenie collects a few details about the recipient and asks the model for structured suggestions. The `@Generable` macro works with Claude out of the box: |
| 60 | + |
| 61 | +```swift |
| 62 | +@Generable |
| 63 | +struct GiftIdea: Equatable { |
| 64 | + @Guide(description: "Short, catchy gift name") |
| 65 | + var name: String |
| 66 | + @Guide(description: "Why this gift fits the recipient") |
| 67 | + var reasoning: String |
| 68 | + @Guide(description: "Estimated price range, e.g. $20–40") |
| 69 | + var estimatedPrice: String |
| 70 | + @Guide(description: "Category, e.g. Tech, Books, Experience") |
| 71 | + var category: String |
| 72 | +} |
| 73 | + |
| 74 | +@Generable |
| 75 | +struct GiftIdeas: Equatable { |
| 76 | + @Guide(description: "Gift ideas ordered by relevance", .count(5)) |
| 77 | + var ideas: [GiftIdea] |
| 78 | +} |
| 79 | +``` |
| 80 | + |
| 81 | +Under the hood the package uses [structured outputs](https://platform.claude.com/docs/en/build-with-claude/structured-outputs), so the response is guaranteed to match the schema. If you pick a model without structured output support, the package throws `LanguageModelError.unsupportedGenerationGuide` instead of silently degrading. |
| 82 | + |
| 83 | +Where do the recipient details come from? The app's main screen is a form where the user describes who the gift is for. Its fields are collected into a plain struct: |
| 84 | + |
| 85 | +```swift |
| 86 | +struct GiftRequest { |
| 87 | + var relationship: String = "" |
| 88 | + var age: Int = 30 |
| 89 | + var interests: String = "" |
| 90 | + var occasion: String = "" |
| 91 | + var budget: String = "" |
| 92 | +} |
| 93 | +``` |
| 94 | + |
| 95 | +The form stays deliberately small — five fields are enough to build focused instructions without turning the UI into prompt engineering. Throughout this post I'm looking for a surprise gift for my wife who likes hiking and knitting, with a $50 budget: |
| 96 | + |
| 97 | +<p align="center"> |
| 98 | + <img src="/images/using-claude-with-apple-foundation-models/gift-form.jpg" alt="GiftGenie form filled with a gift request for a wife who likes hiking and knitting" style={{maxWidth: "50%"}} /> |
| 99 | +</p> |
| 100 | + |
| 101 | +## Dynamic instructions and profiles |
| 102 | + |
| 103 | +Both `SystemLanguageModel` and `ClaudeLanguageModel` conform to the `LanguageModel` protocol, so models are interchangeable — the session doesn't care which one it gets. GiftGenie stores the user's choice in an observable `AppSettings` object backed by `UserDefaults` and Keychain; check the example project for the implementation. But how do we hand the selected model to the session? iOS 27 brings one more API that fits perfectly here — [dynamic profiles](https://developer.apple.com/documentation/foundationmodels/composing-dynamic-sessions-with-instructions-and-profiles). By default a session evaluates instructions once at initialization, and they stay static. With dynamic profiles, the framework re-evaluates instructions, tools, and model configuration before every model request, so the session always sees a snapshot of the current app state. |
| 104 | + |
| 105 | +Start with `DynamicInstructions` — a declarative type whose `body` builds instructions from the gift request: |
| 106 | + |
| 107 | +```swift |
| 108 | +struct GiftInstructions: DynamicInstructions { |
| 109 | + |
| 110 | + var request: GiftRequest |
| 111 | + |
| 112 | + var body: some DynamicInstructions { |
| 113 | + Instructions { |
| 114 | + "You are a thoughtful gift advisor. Suggest specific, creative gifts." |
| 115 | + "Recipient: \(request.relationship), age \(request.age)." |
| 116 | + "Interests: \(request.interests)." |
| 117 | + "Occasion: \(request.occasion)." |
| 118 | + "Budget: \(request.budget)." |
| 119 | + } |
| 120 | + } |
| 121 | +} |
| 122 | +``` |
| 123 | + |
| 124 | +Next, `DynamicProfile` orchestrates which profile is active. A profile associates instructions with session-level configuration via modifiers, and `.model()` accepts any `LanguageModel` — including `ClaudeLanguageModel`: |
| 125 | + |
| 126 | +```swift |
| 127 | +struct GiftProfile: LanguageModelSession.DynamicProfile { |
| 128 | + |
| 129 | + var settings: AppSettings |
| 130 | + var request: GiftRequest |
| 131 | + |
| 132 | + var body: some LanguageModelSession.DynamicProfile { |
| 133 | + switch settings.modelChoice { |
| 134 | + case .onDevice: |
| 135 | + Profile { |
| 136 | + GiftInstructions(request: request) |
| 137 | + } |
| 138 | + case .claude: |
| 139 | + Profile { |
| 140 | + GiftInstructions(request: request) |
| 141 | + } |
| 142 | + .model(ClaudeLanguageModel( |
| 143 | + name: settings.claudeModel, |
| 144 | + auth: .apiKey(settings.apiKey) |
| 145 | + )) |
| 146 | + } |
| 147 | + } |
| 148 | +} |
| 149 | +``` |
| 150 | + |
| 151 | +The result builder enforces at compile time that exactly one profile is active. Because `body` re-evaluates before each request, changing the model in Settings takes effect on the next request — even in a long-lived session. Profiles support more modifiers like `.temperature()`, `.reasoningLevel()`, and `.maximumResponseTokens()`, lifecycle hooks like `onToolCall` for approval gates, and `historyTransform` — a neat trick for multi-model apps: compress the history for the small on-device context window and send the full history to Claude. |
| 152 | + |
| 153 | +When should you escalate to Claude? Apple's on-device model is fast, private, and works offline, but it's sized for lightweight tasks and limited by a 4096-token context window — I covered measuring it in [Tracking token usage in Foundation Models](https://www.artemnovichkov.com/blog/tracking-token-usage-in-foundation-models). Claude brings larger context, frontier reasoning, and server-side tools. With dynamic profiles, switching is one branch in `GiftProfile`. |
| 154 | + |
| 155 | +## Streaming partial content |
| 156 | + |
| 157 | +Frontier models take longer to respond than the on-device one, so streaming is a must for good UX. `streamResponse(to:generating:)` returns cumulative snapshots of partially generated content, and the UI updates as new gift ideas arrive: |
| 158 | + |
| 159 | +```swift |
| 160 | +struct GiftResultsView: View { |
| 161 | + |
| 162 | + let request: GiftRequest |
| 163 | + |
| 164 | + @Environment(AppSettings.self) private var settings |
| 165 | + @State private var giftIdeas: GiftIdeas.PartiallyGenerated? |
| 166 | + @State private var errorMessage: String? |
| 167 | + |
| 168 | + var body: some View { |
| 169 | + List { |
| 170 | + ForEach(giftIdeas?.ideas ?? []) { idea in |
| 171 | + VStack(alignment: .leading, spacing: 8) { |
| 172 | + Text(idea.name ?? "") |
| 173 | + .font(.headline) |
| 174 | + Text(idea.reasoning ?? "") |
| 175 | + } |
| 176 | + } |
| 177 | + } |
| 178 | + .task { |
| 179 | + await generate() |
| 180 | + } |
| 181 | + } |
| 182 | + |
| 183 | + private func generate() async { |
| 184 | + do { |
| 185 | + let session = LanguageModelSession( |
| 186 | + profile: GiftProfile(settings: settings, request: request) |
| 187 | + ) |
| 188 | + let stream = session.streamResponse(to: "Suggest gift ideas for the recipient.", |
| 189 | + generating: GiftIdeas.self) |
| 190 | + for try await partial in stream { |
| 191 | + giftIdeas = partial.content |
| 192 | + } |
| 193 | + } catch { |
| 194 | + print(error.localizedDescription) |
| 195 | + } |
| 196 | + } |
| 197 | +} |
| 198 | +``` |
| 199 | + |
| 200 | +Every field of `PartiallyGenerated` is optional, so each row renders what has already arrived. The full version in the example project shows all fields, fades them in with opacity transitions, and covers the wait before the first snapshot with a progress overlay. If you want to polish the streaming UI without burning tokens, check out my post about [working with partially generated content in Xcode Previews](https://www.artemnovichkov.com/blog/working-with-partially-generated-content-in-xcode-previews). |
| 201 | + |
| 202 | +Let's check the result — every row is mapped from the `GiftIdea` schema: |
| 203 | + |
| 204 | +<p align="center"> |
| 205 | + <img src="/images/using-claude-with-apple-foundation-models/generated-ideas.jpg" alt="GiftGenie generated ideas screen with Claude suggestions for hiking and knitting gifts" style={{maxWidth: "50%"}} /> |
| 206 | +</p> |
| 207 | + |
| 208 | +<Callout> |
| 209 | +In my testing, streaming didn't work — neither on the Simulator nor on a device: the response arrives only as a final snapshot. I haven't found a solution yet; remember that both the OS betas and the package are in beta. I'll update the article once it's resolved. |
| 210 | +</Callout> |
| 211 | + |
| 212 | +## Error handling |
| 213 | + |
| 214 | +The package maps Claude API errors onto Apple's `LanguageModelError` where one fits: context-window overflow surfaces as `.contextSizeExceeded`, HTTP 429 as `.rateLimited`, and request timeouts as `.timeout`. Provider-specific errors surface as `ClaudeError`: |
| 215 | + |
| 216 | +```swift |
| 217 | +do { |
| 218 | + let response = try await session.respond(to: prompt) |
| 219 | +} catch ClaudeError.missingCredential { |
| 220 | + // Prompt for an API key |
| 221 | +} catch let error as LanguageModelError { |
| 222 | + // Rate limits, guardrails, context length |
| 223 | +} catch { |
| 224 | + // Transport errors |
| 225 | +} |
| 226 | +``` |
| 227 | + |
| 228 | +## Bonus: server-side tools |
| 229 | + |
| 230 | +Claude can use server-side tools — web search, web fetch, and code execution — that run on Anthropic's infrastructure within a single round trip. They are configured on the model rather than on the session, because the session type belongs to Apple: |
| 231 | + |
| 232 | +```swift |
| 233 | +let model = ClaudeLanguageModel( |
| 234 | + name: .sonnet4_6, |
| 235 | + auth: auth, |
| 236 | + serverTools: [ |
| 237 | + .webSearch(maxUses: 5), |
| 238 | + .codeExecution, |
| 239 | + ] |
| 240 | +) |
| 241 | +``` |
| 242 | + |
| 243 | +With web search enabled, GiftGenie could check actual prices or find trending gifts. |
| 244 | + |
| 245 | +## Conclusion |
| 246 | + |
| 247 | +Foundation Models is becoming a universal interface for language models on Apple platforms: one session API, one `@Generable` macro, and swappable models — from on-device to frontier. The integration takes minutes, and dynamic profiles make model selection declarative — your users choose between privacy and power, the session picks it up on the next request. |
| 248 | + |
| 249 | +If you like this approach but can't require iOS 27, check out [AnyLanguageModel](https://github.com/huggingface/AnyLanguageModel) from Hugging Face — a drop-in replacement for the Foundation Models framework that works back to iOS 17 and supports many backends behind the same session API: MLX, llama.cpp, Ollama, Core ML, and remote providers like Anthropic, OpenAI, and Gemini. |
| 250 | + |
| 251 | +I created [GiftGenieExample](https://github.com/artemnovichkov/GiftGenieExample) with all the code from this post, so you can run it in Xcode right away. Note that both the OS 27 betas and the package are in beta, so APIs may change before general availability. Happy gifting! |
| 252 | + |
| 253 | +## Resources |
| 254 | + |
| 255 | +- [Composing dynamic sessions with instructions and profiles](https://developer.apple.com/documentation/FoundationModels/composing-dynamic-sessions-with-instructions-and-profiles) — Apple Documentation |
| 256 | +- [What's new in the Foundation Models framework](https://developer.apple.com/videos/play/wwdc2026/241/) — WWDC26 session |
0 commit comments