
Actualizaciones OTA de modelos: mantener tu IA en el dispositivo actualizada
Como enviar actualizaciones de modelos a usuarios sin una release en la app store. Verificacion de versiones, descargas en segundo plano, estrategias de rollback y la infraestructura para entrega over-the-air de modelos.
Los modelos de IA en el dispositivo no son estaticos. Tus datos de entrenamiento mejoran, tu fine-tuning mejora y se lanzan nuevos modelos base. Actualizar el modelo no deberia requerir una actualizacion completa de la app a traves del App Store.
Las actualizaciones over-the-air (OTA) de modelos te permiten enviar nuevos archivos GGUF a los usuarios independientemente del binario de la app. La app verifica actualizaciones, descarga el nuevo modelo en segundo plano y lo intercambia sin problemas.
Arquitectura
El manifiesto del modelo
Aloja un manifiesto JSON en tu CDN junto al archivo del modelo:
{
"current_version": "2.1.0",
"models": {
"1b": {
"url": "https://cdn.example.com/models/v2.1.0/model-1b-q4.gguf",
"size_bytes": 612000000,
"sha256": "a1b2c3d4e5f6...",
"min_app_version": "3.0.0",
"release_notes": "Precision de clasificacion mejorada"
},
"3b": {
"url": "https://cdn.example.com/models/v2.1.0/model-3b-q4.gguf",
"size_bytes": 1740000000,
"sha256": "f6e5d4c3b2a1...",
"min_app_version": "3.0.0",
"release_notes": "Mejor calidad de conversacion"
}
},
"rollback_version": "2.0.0",
"rollback_url_1b": "https://cdn.example.com/models/v2.0.0/model-1b-q4.gguf",
"rollback_url_3b": "https://cdn.example.com/models/v2.0.0/model-3b-q4.gguf"
}
El manifiesto le dice a la app: cual es la ultima version, donde descargarla, como verificarla, y a que recurrir si algo sale mal.
Flujo de verificacion de actualizacion
[Lanzamiento de app] -> [Obtener manifiesto del CDN]
-> [Comparar version local con version del manifiesto]
-> [Si hay version mas nueva disponible]:
-> [Verificar WiFi + almacenamiento suficiente]
-> [Descargar nuevo modelo en segundo plano]
-> [Verificar SHA256]
-> [Intercambiar modelo en siguiente inicio de sesion]
-> [Si la version actual coincide]: [Sin accion]
Implementacion
// iOS: Verificar actualizaciones de modelo
class ModelUpdater {
private let manifestURL = URL(string: "https://cdn.example.com/manifest.json")!
func checkForUpdate() async -> ModelUpdate? {
guard let data = try? await URLSession.shared.data(from: manifestURL).0,
let manifest = try? JSONDecoder().decode(ModelManifest.self, from: data)
else { return nil }
let currentVersion = UserDefaults.standard.string(forKey: "model_version") ?? "0.0.0"
if manifest.currentVersion > currentVersion {
return ModelUpdate(
version: manifest.currentVersion,
url: manifest.models[selectedTier]!.url,
size: manifest.models[selectedTier]!.sizeBytes,
hash: manifest.models[selectedTier]!.sha256
)
}
return nil
}
}
// Android: Verificar actualizaciones al lanzar la app
class ModelUpdater(private val context: Context) {
suspend fun checkForUpdate(): ModelUpdate? = withContext(Dispatchers.IO) {
val manifest = fetchManifest() ?: return@withContext null
val currentVersion = prefs.getString("model_version", "0.0.0")
if (manifest.currentVersion > currentVersion) {
val model = manifest.models[selectedTier]
ModelUpdate(
version = manifest.currentVersion,
url = model.url,
sizeBytes = model.sizeBytes,
sha256 = model.sha256
)
} else null
}
}
Descarga en segundo plano
Las descargas de modelos deben ocurrir en segundo plano sin bloquear al usuario:
iOS: URLSession en segundo plano
func downloadUpdate(_ update: ModelUpdate) {
let config = URLSessionConfiguration.background(
withIdentifier: "com.app.model-download"
)
let session = URLSession(configuration: config, delegate: self, delegateQueue: nil)
let task = session.downloadTask(with: update.url)
task.resume()
}
// El delegate maneja la completacion incluso si la app esta suspendida
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask,
didFinishDownloadingTo location: URL) {
let destination = modelDirectory.appendingPathComponent("model-new.gguf")
try? FileManager.default.moveItem(at: location, to: destination)
if verifyHash(destination, expected: pendingUpdate.sha256) {
// El intercambio ocurrira en el siguiente inicio de sesion
UserDefaults.standard.set(pendingUpdate.version, forKey: "pending_model_version")
} else {
try? FileManager.default.removeItem(at: destination)
}
}
Android: WorkManager
class ModelDownloadWorker(
context: Context, params: WorkerParameters
) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
val url = inputData.getString("url") ?: return Result.failure()
val expectedHash = inputData.getString("hash") ?: return Result.failure()
val tempFile = File(applicationContext.cacheDir, "model-new.gguf")
// Descargar
downloadFile(url, tempFile) { progress ->
setProgress(workDataOf("progress" to progress))
}
// Verificar
if (tempFile.sha256() != expectedHash) {
tempFile.delete()
return Result.failure()
}
// Preparar para intercambio
val destination = File(applicationContext.filesDir, "model-pending.gguf")
tempFile.renameTo(destination)
return Result.success()
}
}
// Programar la descarga
fun scheduleModelDownload(url: String, hash: String) {
val request = OneTimeWorkRequestBuilder<ModelDownloadWorker>()
.setConstraints(
Constraints.Builder()
.setRequiredNetworkType(NetworkType.UNMETERED) // Solo WiFi
.setRequiresStorageNotLow(true)
.build()
)
.setInputData(workDataOf("url" to url, "hash" to hash))
.build()
WorkManager.getInstance(context).enqueue(request)
}
Intercambio de modelo
No intercambies el modelo mientras esta cargado. Intercambia en un punto seguro:
Estrategia de intercambio seguro
- La descarga se completa: Nuevo modelo guardado como
model-pending.gguf - En el siguiente lanzamiento de la app (o inicio de siguiente sesion de chat):
a. Descargar modelo actual
b. Renombrar
model-current.ggufamodel-previous.ggufc. Renombrarmodel-pending.ggufamodel-current.ggufd. Cargar nuevo modelo e. Actualizar numero de version almacenado - Si el nuevo modelo falla al cargar: Revertir a
model-previous.gguf
func swapModelIfPending() throws {
let pendingPath = modelDirectory.appendingPathComponent("model-pending.gguf")
let currentPath = modelDirectory.appendingPathComponent("model-current.gguf")
let previousPath = modelDirectory.appendingPathComponent("model-previous.gguf")
guard FileManager.default.fileExists(atPath: pendingPath.path) else { return }
// Descargar modelo actual
engine.unload()
// Rotar archivos
try? FileManager.default.removeItem(at: previousPath) // Eliminar respaldo viejo
try? FileManager.default.moveItem(at: currentPath, to: previousPath) // Respaldar actual
try FileManager.default.moveItem(at: pendingPath, to: currentPath) // Promover pendiente
// Intentar cargar nuevo modelo
do {
try engine.load(at: currentPath.path)
// Exito: actualizar version
UserDefaults.standard.set(pendingVersion, forKey: "model_version")
} catch {
// Rollback
try? FileManager.default.removeItem(at: currentPath)
try? FileManager.default.moveItem(at: previousPath, to: currentPath)
try engine.load(at: currentPath.path)
}
}
Estrategia de rollback
Siempre mantiene la version anterior del modelo disponible:
- Rollback local: Mantiene
model-previous.ggufen el dispositivo. Si el nuevo modelo falla al cargar o produce calidad pobre, revierte inmediatamente. - Rollback remoto: Incluye URLs de rollback en el manifiesto. Si descubres un problema de calidad del modelo, actualiza el manifiesto para apuntar a la version anterior. Todas las apps se "actualizaran" al modelo antiguo que funciona.
- Rollback automatico: Si la app detecta fallos de inferencia o crashes despues de un intercambio de modelo, revierte automaticamente a la version anterior.
Frecuencia de actualizacion
| Escenario | Frecuencia de actualizacion | Notas |
|---|---|---|
| Producto temprano (iterando rapido) | Semanal-bisemanal | Mejoras rapidas de calidad |
| Producto estable | Mensual-trimestral | Mejoras incrementales |
| Nuevo modelo base disponible | Segun necesidad | Saltos importantes de calidad |
| Datos de entrenamiento cambian significativamente | Segun necesidad | Cambios de dominio |
Cada actualizacion es una ejecucion de fine-tuning ($5-50) mas distribucion CDN. El costo es minimo comparado con la mejora de calidad.
Costos de infraestructura
| Usuarios | Descargas/mes | Tamano del modelo | Costo CDN (Cloudflare R2) |
|---|---|---|---|
| 1,000 | ~200 (actualizaciones + nuevos usuarios) | 1.7GB | ~$0.01/mes |
| 10,000 | ~2,000 | 1.7GB | ~$0.05/mes |
| 100,000 | ~20,000 | 1.7GB | ~$0.51/mes |
Con los precios sin egreso de Cloudflare R2, la entrega OTA de modelos es esencialmente gratuita. Incluso con 100K usuarios, el costo CDN es menor a $1/mes.
El paso de fine-tuning y exportacion GGUF es donde plataformas como Ertas agilizan el flujo de trabajo. Re-entrena con datos actualizados, exporta GGUF, sube al CDN, actualiza el manifiesto. Tus usuarios obtienen el modelo mejorado automaticamente.
Ship AI that runs on your users' devices.
Early bird pricing starts at $14.50/mo — locked in for life. Plans for builders and agencies.
Keep reading

Shipping GGUF Models: App Store Bundling vs Post-Install Download
Two ways to get your GGUF model onto the user's device. Bundle it with the app for simplicity, or download post-install for flexibility. Architecture, size limits, and best practices for both.

Migrating from Cloud API to On-Device AI: The Complete Guide
A step-by-step migration plan for moving your mobile app from cloud AI APIs to on-device inference. Data extraction, fine-tuning, integration, testing, rollout, and monitoring.

How to Add AI to Your Mobile App: A Developer's Decision Guide
A comprehensive guide covering every approach to adding AI features to iOS and Android apps. Cloud APIs, on-device models, and hybrid architectures compared with real cost and performance data.