El límite duro de Foundation Models en iOS 26 nunca fue la calidad del modelo, sino la aritmética: 4096 tokens para todo — instrucciones, definiciones de herramientas y la transcripción entera de la conversación. En iOS 27 esa misma sesión ganó un modo con 32K y un razonamiento bastante más fuerte, y se activa con una línea. Lo que cuesta no es dinero ni una API key, y esa parte resulta menos familiar.
La guía base del framework la escribí en agosto sobre iOS 26 — SystemLanguageModel, @Generable, tool calling. Aquí repaso solo la diferencia: lo que llegó hasta septiembre de 2026 y qué decisiones de producto cambia.
Versiones: todo lo de abajo es iOS 27, iPadOS 27, macOS 27 y visionOS 27 salvo que se indique otra cosa. Una excepción que conviene marcar: watchOS recibió Foundation Models solo en 27 — en 26 el framework no estaba allí.
El framework ya no va de un único modelo de Apple#
Hace un año la documentación lo describía como acceso al modelo de Apple Intelligence. La primera línea del resumen dice hoy otra cosa: acceso a cualquier modelo de lenguaje grande — en el dispositivo, en servidor o uno que traigas tú. Existe un protocolo LanguageModel, y los tres son ciudadanos de primera clase de la misma API.
En la práctica: LanguageModelSession, Instructions, Tool, @Generable, el streaming — eso se escribe una vez, y lo que cambia es el modelo que enchufas. Van tres de esos modelos.
Private Cloud Compute: la misma sesión, otro techo#
El cambio se ve así:
// Sesión sobre el modelo de servidor.
let session = LanguageModelSession(model: PrivateCloudComputeLanguageModel())Todo lo demás se traslada intacto: respond, streaming, herramientas, instrucciones. Tanto SystemLanguageModel como PrivateCloudComputeLanguageModel cumplen el protocolo LanguageModel, así que el inicializador de la sesión es el mismo.
Lo que da: 32K de contexto y un razonamiento más fuerte, para documentos largos y conversaciones de muchos turnos. Lo que quita: el funcionamiento sin red. PCC necesita conexión, y cuando una petición falla por conectividad el propio consejo de Apple es reintentarla en el modelo del dispositivo — o sea, el fallback lo escribes igual.
La disponibilidad se comprueba aparte y por sus propios motivos:
let model = PrivateCloudComputeLanguageModel()
switch model.availability {
case .available:
// Mostramos la interfaz de IA.
case .unavailable(.deviceNotEligible):
// Mostramos una alternativa.
case .unavailable(.systemNotReady):
// PCC todavía no puede atender peticiones.
case .unavailable(let other):
// No disponible por un motivo desconocido.
}Más una comprobación #available de iOS 27 / macOS 27 / watchOS 27 / visionOS 27, con vuelta al modelo del dispositivo en versiones anteriores.
Y aquí está el pago poco habitual. Para desarrollar con PCC hay que cumplir los requisitos de elegibilidad de Apple y solicitar el managed entitlement com.apple.developer.private-cloud-compute. Esto no es una casilla en Capabilities: es una solicitud. Si planificas una función sobre PCC, reserva tiempo de calendario para el acceso, o en la demo tendrás código precioso que no arranca.
La cuota es del usuario, no tuya#
El modelo habitual con un LLM de servidor: tú pagas los tokens y el usuario nunca se entera de que existen. PCC lo invierte. No hay autenticación ni claves en absoluto; a cambio, cada persona tiene un límite diario de peticiones que se amplía mejorando su suscripción a iCloud+.
Para la arquitectura eso es un giro. No puedes comprar margen en nombre del usuario ni predecir cuánto le queda cuando se abre tu pantalla. Por eso el framework expone el estado de la cuota, y su sitio es la interfaz, no una alerta que se descarta:
let model = PrivateCloudComputeLanguageModel()
if model.quotaUsage.isLimitReached {
Text("Límite diario agotado")
.foregroundStyle(Color.red)
} else if case .belowLimit(let info) = model.quotaUsage.status {
if info.isApproachingLimit {
Text("El límite está cerca")
.foregroundStyle(Color.orange)
}
}
if let suggestion = model.quotaUsage.limitIncreaseSuggestion {
Button("Ver opciones") {
suggestion.show()
}
}limitIncreaseSuggestion.show() levanta la interfaz de mejora del sistema: no construyes tu propia pantalla de precios y, por cómo lo formula Apple, tampoco se espera que lo hagas.
Agotar la cuota llega como un error propio, distinto del rate limiting: con rate limiting la persona espera; con la cuota agotada espera al reinicio o mejora el plan. El framework da la fecha de reinicio, aunque puede venir vacía.
Se puede probar sin quemar un límite real: Product > Scheme > Edit Scheme > Run > Options > Simulated Apple Foundation Models Availability, con «Approaching Quota Usage Limit» y «Quota Usage Limit Reached».
El razonamiento pasó a ser un mando que giras tú#
let response = try await session.respond(
to: "¿Qué compromisos hay en esta arquitectura?",
contextOptions: ContextOptions(reasoningLevel: .deep)
)Hay tres niveles; los extremos son .light y .deep. Cuanto más profundo el razonamiento, más latencia y más ventana de contexto se va en el texto de razonamiento del propio modelo. Lo segundo molesta más en la práctica: activaste .deep buscando calidad y te encontraste un exceededContextWindowSize en el tercer turno.
Los segmentos de razonamiento no acaban en el contenido de la respuesta: se ven en la transcripción, lo cual viene bien cuando investigas por qué el modelo dijo algo raro.
Apple recomienda empezar por el nivel mínimo y subir según la evaluación. Consejo aburrido, pero ahora respaldado: el framework sumó herramientas de evaluación de prompts y un instrumento Foundation Models que muestra latencia, prompts, salida del modelo, llamadas a herramientas y consumo de tokens.
Imágenes en el prompt#
La multimodalidad llegó al respond de siempre:
func compareImages(imageOne: CGImage, imageTwo: CGImage) async throws -> String {
let session = LanguageModelSession()
let response = try await session.respond {
"Compara estas dos imágenes en tres puntos:"
Attachment(imageOne)
// Si la imagen no lleva rotación aplicada — por ejemplo un fotograma
// de AVFoundation — indica la orientación y el framework hace la
// transformación.
Attachment(imageTwo, orientation: .right)
}
return response.content
}El escalado y la conversión de formatos corren por cuenta del framework; no hay que preparar la imagen. Acepta CGImage, datos y URL de archivo (el tipo se deduce del UTType).
Combinarlo con @Generable funciona mejor que el texto libre. Clasificación en una sola llamada:
@Generable
enum ImageLabel {
case cat
case dog
case frog
case bird
}
func classifyImage(_ image: CGImage) async throws -> ImageLabel {
let session = LanguageModelSession()
let response = try await session.respond(
generating: ImageLabel.self,
options: GenerationOptions(samplingMode: .greedy)
) {
"Elige la etiqueta que mejor representa esta imagen:"
Attachment(image)
}
return response.content
}.greedy se gana su sitio aquí: sin él el modelo puede elegir una etiqueta «casi correcta», que en un clasificador es un fallo silencioso.
También hay herramientas de imagen listas: lectura de códigos de barras (BarcodeReaderTool) y extracción de texto. Con varias herramientas en juego, conviene etiquetar la imagen — Attachment(image).label("barcode-image") — para que el modelo sepa a qué aplicar cada una.
Un detalle de prompt con efecto desproporcionado: «Enumera todos los alimentos de esta foto» funciona bastante mejor que «¿Qué hay en esta imagen?».
Perfiles dinámicos: instrucciones que se rehacen antes de cada petición#
Antes la sesión congelaba las instrucciones al crearse. Para un flujo de varios pasos — buscar receta, sustituir ingredientes, revisar la despensa, guiar la cocción — eso dejaba dos opciones: una instrucción enorme para todos los casos, o recrear la sesión perdiendo el historial.
Ahora el cuerpo de DynamicInstructions se reevalúa antes de cada petición al modelo:
struct PresentationInstructions: DynamicInstructions {
var isEditingImage = true
var isEditingAnimation = false
var body: some DynamicInstructions {
// La parte común a cualquier estado.
Instructions {
"Ayuda a la persona a mejorar su presentación."
}
ListPhotosTool()
AddPhotoTool()
// Lo que pide el estado actual de la app.
if isEditingImage {
ImageEditingInstructions()
}
if isEditingAnimation {
AnimationEditingInstructions()
}
}
}
let session = LanguageModelSession(
dynamicInstructions: PresentationInstructions()
)Un nivel por encima está Profile, que une instrucciones con ajustes de sesión, y DynamicProfile, que alterna entre perfiles. El cambio lo verifica el compilador: solo puede haber un perfil activo, así que las ramas se escriben con if / else if / else y no con bloques if paralelos.
Profile {
// Instrucciones y herramientas para una tarea creativa.
}
.model(pccModel)
.temperature(likesPoetry ? 0.8 : 0.1)
.reasoningLevel(likesAstronomy ? .deep : .light)Los modificadores se resuelven en tres niveles de prioridad: las opciones pasadas directamente a respond(to:options:) ganan a todo; el modificador de un subperfil gana al del perfil dinámico; el perfil dinámico fija los valores por defecto.
Una regla sin la cual los perfiles empeoran las cosas#
El orden de declaración dentro de body afecta al rendimiento, y lo afecta mucho.
Una sesión se despliega como una secuencia de tokens: primero las instrucciones, luego las definiciones de herramientas, luego la transcripción. La caché KV del proveedor vale hasta el primer token que cambia; todo lo que viene detrás se recalcula. Así que las instrucciones y herramientas estáticas van arriba del cuerpo y los bloques condicionales abajo. Pon un condicional primero y cada cambio de ese flag invalidará la caché de toda la conversación.
De ahí salen consecuencias fáciles de incumplir con buena intención:
- Fija el conjunto de herramientas al crear la sesión. Añadir una a mitad de conversación rompe la caché y además funciona mal: el modelo ya aprendió el patrón de los turnos anteriores.
- Al quitar una herramienta, limpia también sus llamadas de la transcripción; si no, el modelo ve referencias a algo que ya no existe en las definiciones.
- Recorta la transcripción poco y en bloque. Recortar tras cada turno es invalidar tras cada turno; una sola consolidación cerca del límite de contexto sale más barata.
- Cambiar de perfil reescribe el prefijo entero, o sea, vacía la caché. Hazlo en fronteras naturales del flujo, no en cada turno.
Cuando sabes que la petición está a uno o dos segundos vista, prewarm(promptPrefix:) calcula el prefijo en la caché antes de que llegue.
Todo esto se mide en Instruments: divide los tokens de entrada cacheados entre los tokens de entrada totales. Una proporción baja entre turnos significa que la caché se tira y el modelo vuelve a masticar el prefijo cada vez.
Qué hacer con todo esto#
El orden que mantendría hoy: empezar por el modelo del dispositivo y evaluar la función con números. Si choca con los 4096 tokens o con la calidad del razonamiento, añadir PCC — habiendo pedido el entitlement pronto y dibujado el estado de la cuota. Los perfiles, cuando el flujo tenga de verdad varios modos, no porque la API sea nueva.
Hay una tercera opción que no pasa por un modelo de Apple. El protocolo LanguageModel está abierto, y un modelo exportado con Core AI entra en esa misma LanguageModelSession: eso corta la dependencia de Apple Intelligence y funciona en dispositivos donde no lo hay.



