Today every op resolves to a catalog model. CoreAI.read is GLM-OCR at 1.6 GB even when the
page is plain prose and VNRecognizeTextRequest — free, already installed, on every device —
would have answered it. The op layer ignores what Apple already ships and always takes the
heaviest path.
Changing that is the single largest lever on adoption, because it moves the entry cost of most ops from gigabytes to zero, and the entry cost is what decides whether an engineer adds something on a Tuesday afternoon or takes it to a meeting.
Nothing in the call.
let text = try await CoreAI.read(image)
Underneath, the op tries the cheapest backend that satisfies the request and escalates only when it must:
Apple system framework (0 bytes, every device, immediate)
↓ not available, or not good enough for what was asked
catalog model (gigabytes, better, downloads once)
Three consequences, and they are the point:
unsupportedDevice becomes a fallback
rather than an absence — an A16 phone gets Vision’s answer instead of no feature.From TASK_MAP.md, with the demand figures that make each row worth wiring.
| Op | Apple backend | Catalog backend | Wiring worth it |
|---|---|---|---|
transcribe |
SFSpeechRecognizer / SpeechAnalyzer |
Whisper, Nemotron, Parakeet | yes — highest demand of all |
speak |
AVSpeechSynthesizer |
Kokoro, VibeVoice, VoxCPM | yes — second highest |
summarize, extract, proofread |
Foundation Models | 23 chat models | yes — Apple’s is free and adequate for short text |
read |
VNRecognizeTextRequest |
GLM-OCR, MinerU | yes — but see the caveat below |
translate |
TranslationSession |
chat models | yes — Apple’s is genuinely hard to beat |
extractEntities, redact |
NaturalLanguage |
GLiNER2 | partly — Apple’s taxonomy is fixed, the model’s is not |
search |
NLEmbedding |
EmbeddingGemma | partly — quality gap is real |
detect |
Vision (faces, rectangles, barcodes) | RF-DETR, YOLOX | no — different job; Vision does not do open vocabulary |
caption, estimateDepth, upscale, forecast, compose, separate, recognizeAction |
nothing | catalog only | n/a |
The caller must always be able to see which backend answered.
let result = try await CoreAI.read(image)
result.text
result.backend // .system(.vision) | .model("glm-ocr")
Silently returning a worse answer because it was cheaper is not a convenience, it is a defect that surfaces as a mystery in someone’s product. Any op that can route must say what it did.
And routing must be overridable in both directions:
try await CoreAI.read(image, options: .model("glm-ocr")) // force the upgrade
try await CoreAI.read(image, options: .systemOnly) // never download anything
.systemOnly matters more than it looks: it is how an app ships a feature with a hard
zero-bytes guarantee, which is a real product requirement and currently impossible.
The plumbing is a switch. What does not exist is the basis for the decision — for each op, where the free backend stops being good enough.
That question has never been answered publicly for any of these pairs. It is the same kind of work as the model measurements already done here, and it is the reason this is defensible: the switch is easy to copy, the table behind it is not.
First measurement, because it decides the most contested row:
Vision OCR vs GLM-OCR, same documents, same scoring. Vision returns words; GLM-OCR returns structure. The hypothesis is that the 1.6 GB buys nothing on plain prose and buys everything on tables and forms. If that holds,
readroutes on document type, not on quality thresholds — and that is a much simpler rule than a score.
Until a row has been measured, that op does not route. A guessed threshold is worse than no routing, because it produces a quality difference nobody asked for and nobody can see.
capability(_:) answer rather than
discovering them at call time..backend, and OpOptions gains .systemOnly and model forcing. No
routing yet — just the surface that makes routing describable.translate, speak, summarize.detect, caption, estimateDepth and the rest catalog-only — Apple has no answer,
so there is nothing to route to.