Claude y Apple Foundation Models: una misma API para modelos locales y remotos
Arturo Rivas Arias
🤖 Foundation Models está dejando de ser una API pensada únicamente para invocar el modelo local de Apple. Con las novedades presentadas en la WWDC26, el framework empieza a comportarse como una capa de abstracción para trabajar con distintos modelos de lenguaje desde Swift: el modelo en dispositivo, Private Cloud Compute, implementaciones locales basadas en Core AI o MLX, y proveedores externos como Anthropic o Google.
La idea es muy potente porque cambia totalmente el enfoque de la integración. En lugar de diseñar una capa distinta para cada proveedor, la aplicación puede trabajar contra LanguageModelSession y dejar que el modelo concreto sea una dependencia intercambiable. Si el caso de uso requiere privacidad, baja latencia y funcionamiento offline, puede usarse el modelo en dispositivo. Si se necesita más contexto, razonamiento más avanzado o herramientas de servidor, se puede escalar a un modelo remoto como Claude manteniendo la misma forma general de construir prompts, pedir respuestas estructuradas o procesar las respuestas.
Anthropic ha publicado ClaudeForFoundationModels, un paquete Swift que adapta Claude al protocolo LanguageModel de Apple. Eso permite crear una instancia de ClaudeLanguageModel, pasarla a LanguageModelSession y usarla casi igual que usaríamos SystemLanguageModel. La diferencia importante está fuera de la sintaxis: las peticiones a Claude salen de la app hacia la API de Anthropic, no pasan por Apple, requieren autenticación propia y se facturan según el uso del proveedor.
Apple también lo plantea así en su sesión What’s new in the Foundation Models framework. La capa de abstracción se apoya en un nuevo protocolo LanguageModel, de modo que tanto modelos locales como modelos de servidor puedan alimentar una LanguageModelSession. Apple menciona además que, al usar modelos de terceros, hay que tratar explícitamente autenticación, facturación y seguridad de credenciales. Es decir, la API se unifica, pero las responsabilidades de producto no desaparecen.
🧩 El ejemplo inicial con Claude es sorprendentemente sencillo. Tras añadir el paquete con Swift Package Manager, basta con importar FoundationModels y ClaudeForFoundationModels, construir el modelo y pasarlo a una sesión:
import FoundationModels
import ClaudeForFoundationModels
let model = ClaudeLanguageModel(
name: .sonnet4_6,
auth: .apiKey("YOUR_API_KEY")
)
let session = LanguageModelSession(model: model)
let response = try await session.respond(
to: "Resume los puntos de riesgo de este informe técnico."
)
print(response.content)
Este código sirve para entender la integración, pero no debería llegar a producción así. Una clave incluida en el binario de una app puede extraerse. No importa que esté en una constante, en un fichero de configuración o ligeramente ofuscada: si viaja dentro de la app, deja de ser un secreto. El propio paquete distingue entre .apiKey, pensado para desarrollo, y .proxied, pensado para producción mediante un backend que añade la credencial real en el servidor.
🔐 Una arquitectura más razonable para producción consiste en que la app nunca conozca la clave de Anthropic. La aplicación se autentica contra tu backend, el backend valida al usuario, aplica cuotas, registra métricas y reenvía la petición a Claude con la credencial privada. En el cliente solo queda un token propio de sesión o una cabecera de autorización temporal.
import FoundationModels
import ClaudeForFoundationModels
struct ClaudeModelFactory {
let userToken: String
func makeModel() throws -> ClaudeLanguageModel {
guard let baseURL = URL(string: "https://api.miapp.com/llm/claude") else {
throw URLError(.badURL)
}
return ClaudeLanguageModel(
name: .sonnet4_6,
auth: .proxied(headers: [
"Authorization": "Bearer \(userToken)"
]),
baseURL: baseURL
)
}
}
Este patrón también permite resolver un problema que no es técnico, sino de negocio: el coste. Cuando una app usa el modelo local de Apple, el coste directo por petición no existe para el desarrollador. Cuando usa Claude, Gemini u otro proveedor remoto, normalmente aparece facturación por tokens. Por eso tiene sentido centralizar el acceso en un backend: puedes limitar usos abusivos, diferenciar planes gratuitos y de pago, activar modelos distintos por usuario o deshabilitar temporalmente una ruta si el coste se dispara.
🧠 Lo interesante de LanguageModel no es solo poder cambiar un tipo por otro, sino construir la aplicación alrededor de sus capacidades. Una misma pantalla puede empezar usando el modelo en dispositivo para tareas ligeras y escalar a Claude cuando necesite más contexto, mejor razonamiento o herramientas alojadas en un servidor.
Imaginemos una app interna que ayuda a revisar incidencias de un sistema. Para un resumen breve de los logs recientes, el modelo local puede ser suficiente. Para correlacionar muchos eventos, generar una hipótesis y proponer pasos de mitigación, quizá prefiramos Claude.
enum ModelRoute: String, CaseIterable {
case onDevice
case claude
}
@Observable
final class AssistantSettings {
var route: ModelRoute = .onDevice
var userToken: String = ""
}
struct IncidentAssistant {
let settings: AssistantSettings
func makeSession() throws -> LanguageModelSession {
switch settings.route {
case .onDevice:
return LanguageModelSession(model: SystemLanguageModel())
case .claude:
let model = try ClaudeModelFactory(
userToken: settings.userToken
).makeModel()
return LanguageModelSession(model: model)
}
}
}
La ventaja para el código de la aplicación es clara: la vista o el caso de uso no tiene por qué saber si detrás hay un modelo local o remoto. Puede recibir una sesión y trabajar con ella. A partir de ahí, la decisión de enrutado se convierte en una política de producto: privacidad, latencia, coste, disponibilidad offline, tamaño del contexto o nivel de razonamiento requerido.
📦 Otro punto clave es la generación estructurada. Foundation Models permite definir tipos Swift con @Generable y guiar al modelo para que devuelva datos con forma conocida. El paquete de Anthropic mantiene esa idea y, cuando el modelo soporta salidas estructuradas, adapta la petición al esquema correspondiente.
Un ejemplo diferente al típico asistente de regalos podría ser una app que resume incidencias técnicas en estructuras que luego se muestran en una lista, se guardan en SwiftData o se envían a un sistema de seguimiento.
import FoundationModels
@Generable
struct IncidentSummary: Equatable {
@Guide(description: "Título breve de la incidencia")
var title: String
@Guide(description: "Severidad estimada: low, medium, high o critical")
var severity: String
@Guide(description: "Explicación de la causa más probable")
var probableCause: String
@Guide(description: "Primeras acciones recomendadas")
var nextSteps: [String]
}
func summarizeIncident(
logs: String,
using session: LanguageModelSession
) async throws -> IncidentSummary {
let response = try await session.respond(
to: "Analiza estos logs y genera un resumen operativo:\n\n\(logs)",
generating: IncidentSummary.self
)
return response.content
}
Este enfoque reduce simplifica la relación entre IA y UI. En vez de pedir texto en formato libre y parsearlo después, el contrato se expresa como un tipo Swift. El modelo puede seguir equivocándose, pero al menos la frontera con la app queda tipada: la vista espera un IncidentSummary, no un bloque de Markdown difícil de validar.
🌊 El streaming también cambia la experiencia de usuario. Los modelos remotos suelen tardar más que el modelo en dispositivo, especialmente cuando se activa razonamiento o se trabaja con contextos grandes. Si esperamos a la respuesta final, la interfaz puede parecer que se ha bloqueado. Con streamResponse, la app puede ir renderizando resultados parciales.
@MainActor
@Observable
final class IncidentViewModel {
var partialSummary: IncidentSummary.PartiallyGenerated?
var errorMessage: String?
func analyze(logs: String, session: LanguageModelSession) async {
do {
let stream = session.streamResponse(
to: "Resume la incidencia y prioriza las acciones:\n\n\(logs)",
generating: IncidentSummary.self
)
for try await partial in stream {
partialSummary = partial.content
}
} catch {
errorMessage = error.localizedDescription
}
}
}
Hay un detalle importante: en contenido parcialmente generado, los campos pueden no estar disponibles todavía. La UI debe asumir valores opcionales y mostrar lo que ya exista sin forzar unwraps ni esperar que el objeto esté completo. Es un patrón muy SwiftUI: la vista refleja el estado disponible en cada momento.
🧭 Donde iOS 27 añade una pieza especialmente interesante es en los perfiles dinámicos. DynamicProfile permite que una misma sesión pueda resolver instrucciones, herramientas, modelos y configuración en función del estado actual de la aplicación. No se trata solo de cambiar de modelo manualmente, sino de describir perfiles activos y dejar que el framework recomponga el contexto adecuado.
Podemos imaginar una app de documentación técnica con dos modos. En modo “local”, responde solo con conocimiento del dispositivo y documentos indexados. En modo “profundo”, usa Claude para razonar sobre un conjunto más amplio de información.
struct DocumentationProfile: LanguageModelSession.DynamicProfile {
let settings: AssistantSettings
let topic: String
var body: some LanguageModelSession.DynamicProfile {
switch settings.route {
case .onDevice:
Profile {
Instructions {
"Eres un asistente de documentación técnica."
"Responde de forma breve y solo sobre el tema: \(topic)."
"Prioriza información disponible localmente."
}
}
case .claude:
Profile {
Instructions {
"Eres un asistente senior de arquitectura de software."
"Analiza el tema con más profundidad: \(topic)."
"Devuelve riesgos, alternativas y una recomendación clara."
}
}
.model(
try! ClaudeModelFactory(
userToken: settings.userToken
).makeModel()
)
.reasoningLevel(.deep)
}
}
}
(En código real evitaría el try! y construiría el modelo antes de llegar al perfil)
El ejemplo muestra la intención: una rama usa una configuración conservadora, otra usa un proveedor remoto con razonamiento más profundo. La decisión no queda dispersa por la UI, sino encapsulada en una pieza declarativa.
⚖️ Este tipo de abstracción obliga a pensar en límites. El modelo local es privado, rápido y puede funcionar sin conexión, pero tiene una ventana de contexto y capacidades más contenidas. Private Cloud Compute ofrece más potencia dentro del ecosistema de Apple, con una ventana de contexto mayor y sin que el desarrollador gestione claves de terceros. Claude aporta modelos frontera, contexto amplio y herramientas de servidor como búsqueda web o ejecución de código, pero introduce red, coste, dependencia externa y responsabilidad sobre credenciales.
Por eso no conviene vender esta integración como “usa Claude en lugar del modelo de Apple”. La lectura correcta es otra: Foundation Models empieza a convertirse en una interfaz común para decidir qué inteligencia necesita cada tarea. Algunas pantallas deberían quedarse siempre en dispositivo. Otras pueden ofrecer un botón de “análisis avanzado”. Otras pueden elegir automáticamente según tamaño del prompt,* conectividad, sensibilidad de los datos o plan del usuario.
🧱 Una forma práctica de diseñar esta decisión es crear una capa propia de política antes de instanciar la sesión:
struct PromptMetadata {
var containsSensitiveData: Bool
var estimatedTokens: Int
var requiresFreshInformation: Bool
var userAllowsRemoteModels: Bool
}
struct ModelPolicy {
func route(for metadata: PromptMetadata) -> ModelRoute {
if metadata.containsSensitiveData {
return .onDevice
}
if metadata.requiresFreshInformation,
metadata.userAllowsRemoteModels {
return .claude
}
if metadata.estimatedTokens > 6_000,
metadata.userAllowsRemoteModels {
return .claude
}
return .onDevice
}
}
Esta política no tiene por qué ser perfecta al principio. Lo importante es que exista. Cuando se trabaja con varios modelos, dejar la decisión repartida en botones, vistas y servicios termina generando inconsistencias difíciles de auditar. Centralizarla permite explicar por qué una petición fue local o remota y facilita aplicar cambios de privacidad o coste más adelante.
🚨 La gestión de errores también merece una capa específica. El artículo original menciona que el paquete intenta mapear errores de Claude a errores conocidos de Foundation Models cuando encajan: límite de contexto, rate limit o timeout. Aun así, habrá errores propios del proveedor, como credenciales ausentes o problemas de transporte.
enum AssistantError: LocalizedError {
case missingCredential
case rateLimited
case contextTooLarge
case providerUnavailable
case unknown(Error)
var errorDescription: String? {
switch self {
case .missingCredential:
"Falta la credencial para usar el modelo remoto."
case .rateLimited:
"Se ha alcanzado el límite temporal de uso."
case .contextTooLarge:
"El contenido es demasiado grande para el modelo seleccionado."
case .providerUnavailable:
"El proveedor remoto no está disponible ahora mismo."
case .unknown(let error):
error.localizedDescription
}
}
}
func mapModelError(_ error: Error) -> AssistantError {
if error is ClaudeError {
return .providerUnavailable
}
if let languageModelError = error as? LanguageModelError {
switch languageModelError {
case .contextSizeExceeded:
return .contextTooLarge
case .rateLimited:
return .rateLimited
default:
return .unknown(languageModelError)
}
}
return .unknown(error)
}
La UI no debería mostrar directamente errores de bajo nivel del proveedor. Es mejor traducirlos a acciones comprensibles: pedir al usuario que conecte su cuenta, sugerir reducir el contenido, reintentar más tarde o cambiar temporalmente al modelo local.
🛠️ Claude añade además herramientas de servidor configurables en el modelo, como búsqueda web, lectura de páginas o ejecución de código. Esto es distinto de las herramientas cliente de Foundation Models, que se ejecutan dentro del flujo de la app. La diferencia arquitectónica es importante: una herramienta local puede consultar datos privados del usuario dentro del dispositivo; una herramienta de servidor opera en la infraestructura del proveedor y debe reservarse para datos que puedan salir de la app.
let model = ClaudeLanguageModel(
name: .sonnet4_6,
auth: .proxied(headers: ["Authorization": "Bearer \(token)"]),
baseURL: URL(string: "https://api.miapp.com/llm/claude")!,
serverTools: [
.webSearch(maxUses: 3),
.codeExecution
]
)
let session = LanguageModelSession(model: model)
Un caso razonable sería una app para desarrolladores que permite investigar documentación pública, comparar APIs o validar pequeños fragmentos de código. Un caso mucho más delicado sería enviar datos personales, historiales médicos, mensajes privados o contenido corporativo sensible a una herramienta remota sin consentimiento explícito.
📊 A partir de iOS 27, Apple también está poniendo más énfasis en medición. La sesión de Foundation Models muestra propiedades de uso para inspeccionar tokens de entrada, tokens cacheados, tokens de salida y tokens empleados en razonamiento. En un mundo con modelos intercambiables, estas métricas son fundamentales: sirven para depurar calidad, controlar coste y tomar decisiones de producto basadas en datos.
let response = try await session.respond(
to: "Propón una estrategia de migración para este módulo legacy.",
contextOptions: ContextOptions(reasoningLevel: .moderate)
)
print("Input:", response.usage.input.totalTokenCount)
print("Cached:", response.usage.input.cachedTokenCount)
print("Output:", response.usage.output.totalTokenCount)
print("Reasoning:", response.usage.output.reasoningTokenCount)
Este tipo de información permite construir límites preventivos. Por ejemplo, avisar al usuario antes de enviar un análisis muy grande a un modelo remoto, recortar historial antiguo, resumir contexto o degradar automáticamente a una ruta más barata cuando no haga falta razonamiento profundo.
🏁 La integración de Claude con Foundation Models no es solo una curiosidad técnica. Es una señal clara de hacia dónde se mueven las apps Apple con IA: menos SDKs aislados por proveedor y más composición alrededor de sesiones, perfiles, herramientas, generación estructurada y políticas de enrutado.
Para quienes desarrollamos en Swift, la oportunidad está en diseñar esa capa con cuidado. No basta con cambiar SystemLanguageModel() por ClaudeLanguageModel(). Hay que decidir qué datos pueden salir del dispositivo, cómo se protegen las credenciales, cómo se controlan los costes, qué modelo encaja con cada tarea, cómo se informa al usuario y cómo se mide la calidad de las respuestas.
La buena noticia es que Foundation Models empieza a ofrecer una base común para todo eso. Y cuando una misma API permite moverse entre privacidad local, potencia de servidor y proveedores externos, el trabajo interesante deja de estar en “cómo llamo a este modelo” y pasa a estar en “cuándo, por qué y con qué límites debería usarlo”.