Apple Intelligence on-device LLM from a single Swift file
macOS 26 ships FoundationModels — a framework that gives you direct access to the on-device
~3B Apple Intelligence model. No API key, no network, no Xcode project needed. A single .swift
file compiled with swiftc is enough.
swiftc main.swift -framework FoundationModels -o ask
./ask "what is the capital of Ukraine?"
# The capital of Ukraine is Kyiv.The smallest possible demo:
import Foundation
import FoundationModels
guard CommandLine.arguments.count > 1 else { print("Usage: ask <question>"); exit(1) }
let question = CommandLine.arguments.dropFirst().joined(separator: " ")
let model = SystemLanguageModel.default
guard model.isAvailable else { print("Apple Intelligence not available: \(model.availability)"); exit(1) }
let session = LanguageModelSession()
let response = try await session.respond(to: question)
print(response.content)Top-level await in main.swift just works in Swift 6 — no @main, no Task {} wrapper needed.
@Generable — typed structured output
The more interesting feature. Mark a struct with @Generable and the model fills it directly
via constrained decoding. No JSON wrangling, no parsing, the schema is guaranteed.
@Generable
struct Person {
@Guide(description: "The person's full name")
var name: String
@Guide(description: "The person's age as a number")
var age: Int
@Guide(description: "The city where the person lives")
var city: String? // nil if not mentioned
}
let response = try await session.respond(to: input, generating: Person.self)
let person = response.content // Person, not String
print(person.name, person.age, person.city ?? "unknown")./extract "Sarah is a 28-year-old software engineer from Tokyo"
# Name: Sarah
# Age: 28
# City: Tokyo
./extract "My name is Maria and I am 19"
# Name: Maria
# Age: 19
# City: unknownOptional fields (String?, Int?) come back as nil when the model can't extract them.
Enums work too — @Generable enum gives you a classification output.
Multi-pass pipeline: classify then extract
The interesting pattern is chaining passes — each small and focused, letting the model do one thing at a time. Good demo is parsing raw voice transcript (as if from WhisperKit) into a typed task or todo list.
Three passes:
- clean the raw transcript — strip filler words, false starts, self-corrections
- classify — single task or list?
- extract into the matching
@Generablestruct
import Foundation
import FoundationModels
// Compile:
// swiftc todo.swift -framework FoundationModels -o todo
//
// Try it (simulating raw WhisperKit output):
//
// Single tasks:
// ./todo "remind me to, uh, call dentist tomorrow"
// ./todo "buy milk, or wait, buy milk and eggs and also bread"
// ./todo "i need to schedual, erm, schedule a meeting with the team on friday"
// ./todo "send the, no wait, forward the report to Sarah by end of day"
//
// Todo lists:
// ./todo "i need to do the following, erm, clean the house, fix the bike, and reply to John"
// ./todo "groceries for the week so, apples, hmm, apples bananas and, ugh, and yogurt and coffee"
// ./todo "things for the trip, pack clothes, or actually pack clothes and charger, book taxi, and don't forget pasport, i mean passport"
// ./todo "work stuff today: finish the, finish the design doc, review pull request from Mike, and update the roadmap prezentation"
//
// Russian:
// ./todo "напомни мне позвонить врачу завтра утром"
// ./todo "список покупок: молоко, хлеб, ну и ещё яйца и масло"
// ./todo "надо сделать сегодня, э, помыть машину, отправить письмо Ивану, и заплатить за интернет"
// --- Schemas ---
@Generable
enum TaskType {
case singleTask
case todoList
}
@Generable
struct SingleTask {
@Guide(description: "The task to do, clean and concise")
var title: String
@Guide(description: "Optional deadline or time mentioned")
var deadline: String?
}
@Generable
struct TodoList {
@Guide(description: "An optional title or topic for the list")
var title: String?
@Guide(description: "Each individual task as a clean list item")
var items: [String]
}
// --- Input ---
guard CommandLine.arguments.count > 1 else {
print("Usage: todo <raw voice transcript>")
exit(1)
}
let raw = CommandLine.arguments.dropFirst().joined(separator: " ")
let model = SystemLanguageModel.default
guard model.isAvailable else {
print("Apple Intelligence is not available: \(model.availability)")
exit(1)
}
let session = LanguageModelSession()
// Low temperature across all passes — the goal is faithful extraction, not creativity.
// Seed makes classification and extraction fully deterministic: same input, same output.
// Pass 1 has no seed intentionally — minor phrasing variation is fine, hallucination is not.
let cleanOptions = GenerationOptions(temperature: 0.2)
let strictOptions = GenerationOptions(sampling: .random(top: 50, seed: 42), temperature: 0.2)
// --- Pass 1: clean up the raw transcript ---
// Explicit English output avoids the guardrail error the model sometimes throws
// when asked to generate non-English text (unsupportedLanguageOrLocale).
// To preserve the original language instead, replace "Translate to English if needed"
// with "Keep the output in the same language as the input" — but expect occasional
// guardrail failures on Russian/Ukrainian input.
let cleaned = try await session.respond(
to: "Clean up this voice transcript. Remove false starts, filler words, and self-corrections. Keep only the final intended meaning in plain text. Translate to English if needed. Transcript: \(raw)",
options: cleanOptions
).content
// --- Pass 2: classify ---
let kind = try await session.respond(
to: "Does this describe one single thing to do, or two or more separate tasks? Text: \(cleaned)",
generating: TaskType.self,
options: strictOptions
).content
// --- Pass 3: extract into the right shape ---
switch kind {
case .singleTask:
let task = try await session.respond(
to: cleaned,
generating: SingleTask.self,
options: strictOptions
).content
print("Task: \(task.title)")
print("Deadline: \(task.deadline ?? "none")")
case .todoList:
let list = try await session.respond(
to: cleaned,
generating: TodoList.self,
options: strictOptions
).content
if let title = list.title { print("List: \(title)") }
for (i, item) in list.items.enumerated() {
print(" \(i + 1). \(item)")
}
}./todo "remind me to, uh, call dentist tomorrow"
# Task: Call dentist tomorrow
# Deadline: tomorrow
./todo "things for the trip, pack clothes, or actually pack clothes and charger, book taxi, and don't forget pasport, i mean passport"
# List: Trip Essentials
# 1. Pack clothes and charger
# 2. Book a taxi
# 3. Don't forget your passport
./todo "надо сделать сегодня, э, помыть машину, отправить письмо Ивану, и заплатить за интернет"
# List: Today's Tasks
# 1. Wash the car
# 2. Send a letter to Ivan
# 3. Pay for the internetNotes:
-
temperature matters a lot here. Started with
0.7on pass 1 and got hallucinations on Russian input —"Make today","Make a list of tasks for today"— content completely lost. Dropping to0.2across all passes fixed it. The goal is faithful extraction, not creativity, so low temperature is correct. -
seed for determinism.
GenerationOptions(sampling: .random(top: 50, seed: 42), temperature: 0.2)on classification and extraction passes makes same input always produce same output. Pass 1 intentionally has no seed — minor phrasing variation is fine. -
unsupportedLanguageOrLocaleon non-English generation. If you ask the model to output Russian text it sometimes throws this error non-deterministically. The fix is to explicitly tell it to output English in the cleaning prompt:"Translate to English if needed". This also conveniently translates Russian/Ukrainian input through the pipeline so downstream passes always see English, which they handle reliably. -
classification prompt precision matters.
"Does this describe a single task or multiple tasks?"was too vague — comma-separated items often classified as single task. Changing to"Does this describe one single thing to do, or two or more separate tasks?"fixed it. -
the model is available from
SystemLanguageModel.default. Checkmodel.isAvailablebefore use — it returns false if Apple Intelligence is not enabled in Settings, device is not eligible (requires Apple Silicon, A17 Pro or later), or model assets are still downloading. -
context window is 4096 tokens total (input + output combined). For anything larger you need to chunk manually or escalate to a cloud model.
-
LanguageModelSessionis stateful — callingrespond()whileisResponding == truethrows. For parallel calls, create separate sessions.