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: unknown

Optional 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:

  1. clean the raw transcript — strip filler words, false starts, self-corrections
  2. classify — single task or list?
  3. extract into the matching @Generable struct

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 internet

Notes:

  • temperature matters a lot here. Started with 0.7 on pass 1 and got hallucinations on Russian input — "Make today", "Make a list of tasks for today" — content completely lost. Dropping to 0.2 across 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.

  • unsupportedLanguageOrLocale on 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. Check model.isAvailable before 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.

  • LanguageModelSession is stateful — calling respond() while isResponding == true throws. For parallel calls, create separate sessions.