Un desarrollador crea un cliente KMP para Ollama: dos errores de serialización y streaming
Un desarrollador mostró cómo conectar una aplicación móvil en Kotlin Multiplatform a un servidor local de Ollama: UI compartida en Compose, Koin para dependencias, Ktor para la red. El servidor se abre a los teléfonos de la red con una sola variable, OLLAMA_HOST=0.0.0.0. La mayor parte del tiempo se fue en dos errores sutiles: el campo stream que desaparece y una respuesta que llega entera en lugar de como flujo. Ambos se arreglan con un par de líneas si se sabe dónde buscar.
Procesado por IA desde Habr AI; editado por Hamidun News
Hoy es fácil ejecutar un modelo local en un portátil. Lo difícil es crear un buen cliente móvil que funcione tanto en Android como en iPhone sin obligar a escribir la UI dos veces. El autor de un artículo en Habr recorrió ese camino con Kotlin Multiplatform y describió con honestidad dónde tropezó. Para quien piense hacer lo mismo, esto es más útil que cualquier teoría.
El servidor: tres comandos y una variable
Ollama se instala con brew o con un script de instalación en Ubuntu. Por defecto el servidor escucha solo en localhost, por lo que ni el emulador ni un teléfono real en la misma red pueden alcanzarlo. Para abrir el acceso, el servidor se inicia con OLLAMA_HOST=0.0.0.0:11434. Después el modelo se descarga con ollama pull y, en los Mac con chips Apple, el autor usa builds MLX, por ejemplo gemma4:12b-mlx. El comando ollama ps muestra que el modelo está por completo en la memoria de la GPU (en la salida, PROCESSOR 100% GPU).
El servidor se puede comprobar con un curl normal a /api/generate. Si la petición incluye stream: false, la respuesta llega en un solo JSON.
El cliente: Compose, Koin, Ktor
El proyecto es una app KMP con UI compartida: Koin gestiona las dependencias y el ViewModel, Ktor la red y Navigation3 la navegación. El autor eligió no el startKoin global, sino el KoinApplication composable. El contenedor se crea dentro de la función App(), así que @Preview en Android Studio no choca con el estado global y no hace falta tocar el código Swift generado por el IDE. El precio de esta decisión: parte de la API no está disponible, incluidos by inject(), by viewModel() y androidContext(). Cómo dividir correctamente en módulos una aplicación grande con este enfoque, el autor todavía no lo ha averiguado.
Xcode y macOS siguen siendo necesarios para compilar para iOS. KMP evita la UI en Swift, pero no las herramientas de Apple.
Dos trampas que costaron horas
La primera trampa tiene que ver con el serializador. Kotlinx.Serialization por defecto no escribe en el JSON los campos con valores por defecto. Si tienes stream = false, el campo simplemente desaparece de la petición, y sin él Ollama activa el modo de streaming. El resultado: esperas una sola respuesta y recibes un montón de líneas JSON. Se soluciona con la configuración Json { encodeDefaults = true }. Network Inspector de Android Studio ayudó a encontrar la causa.
La segunda trampa afecta al propio streaming. La respuesta llegaba entera al final, y el autor sospechó primero del buffering en OkHttp e incluso separó el código por plataformas con expect/actual. El problema era otro: en lugar de post() hay que usar preparePost() y leer el cuerpo línea por línea con execute y bodyAsChannel. Cada línea es un fragmento JSON independiente con los campos message, done y error. En el cliente todo esto se convierte en un Flow de eventos: Token, Thinking, Done y Error. Cancelar la corrutina cierra la conexión y detiene la generación. Los tokens de razonamiento (Thinking) llegan aparte, si el modelo los emite.
Otro detalle: en /api/chat el rol es obligatorio en cada mensaje, mientras que /api/generate no tiene roles, solo prompt.
Qué tener en cuenta en la práctica
Si el servidor funciona por HTTP, en Android hace falta usesCleartextTraffic="true" y para iOS habrá que añadir NSAllowsArbitraryLoads en Info.plist. Eso sí, en el experimento del autor el emulador de iPhone dejó pasar las peticiones incluso sin ello. En producción no se puede usar HTTP sin cifrar ni un servidor en 0.0.0.0 sin autorización: el texto no dice nada sobre protección, así que es un banco de pruebas doméstico o interno, no una arquitectura lista.
El proyecto todavía está en bruto: hay que pulir la interfaz y no hay historial de chats. El autor añade que ya ha escrito un cliente para los modelos en la nube de cloud.ru con imágenes y audio. La tarea de renombrar el proyecto le llevó al modelo unos cinco minutos y aproximadamente 50 rublos, aunque el precio depende mucho del modelo elegido.
Conclusión para el desarrollador: si haces un cliente móvil para Ollama, activa encodeDefaults desde el principio, usa preparePost para los flujos y no busques el problema en OkHttp. El código fuente está en GitHub (app-z/OllameChatKmp) y puede servir como plantilla inicial.
- 12 nov 2026n8n lanza Nod, un agente de código abierto que funciona en tu ordenador
- 12 nov 2026Claude Code y Codex frenan la ingeniería inversa: OpenAI baneó cuentas por error
- 12 nov 2026Los ingenieros de QA de SpaceWeb encargaron a la IA escribir autotests: un script funcional en 50 minutos
- 12 nov 2026MiniMax publica los pesos de H3-Image: imágenes 4K en 10–16 segundos y 16 referencias
Preguntas frecuentes sobre la noticia
¿Qué se sabe sobre El servidor: tres comandos y una variable?
Ollama se instala con brew o con un script de instalación en Ubuntu. Por defecto el servidor escucha solo en localhost, por lo que ni el emulador ni un teléfono real en la misma red pueden alcanzarlo. Para abrir el acceso, el servidor se inicia con OLLAMA_HOST=0.0.0.0:11434. Después el modelo se descarga con ollama pull y, en los Mac con chips Apple, el autor usa builds MLX, por ejemplo gemma4:12b-mlx.
¿Qué se sabe sobre El cliente: Compose, Koin, Ktor?
El proyecto es una app KMP con UI compartida: Koin gestiona las dependencias y el ViewModel, Ktor la red y Navigation3 la navegación. El autor eligió no el startKoin global, sino el KoinApplication composable. El contenedor se crea dentro de la función App(), así que @Preview en Android Studio no choca con el estado global y no hace falta tocar el código Swift generado por el IDE. El precio de esta decisión: parte de la API no está disponible, incluidos by inject(), by viewModel() y androidContext().
¿Qué se sabe sobre Dos trampas que costaron horas?
La primera trampa tiene que ver con el serializador. Kotlinx.Serialization por defecto no escribe en el JSON los campos con valores por defecto. Si tienes stream = false, el campo simplemente desaparece de la petición, y sin él Ollama activa el modo de streaming. El resultado: esperas una sola respuesta y recibes un montón de líneas JSON. Se soluciona con la configuración Json { encodeDefaults = true }. Network Inspector de Android Studio ayudó a encontrar la causa. La segunda trampa afecta al propio streaming.
Test Frontier LLMs & Autonomous Agents in One Workspace
Run Claude Opus 5.5, GPT-6, Gemini 3.1, and DeepSeek side-by-side in Nomad AI (nomadnet.ai) — unified model routing, deep research, and multimodal workflows under one subscription.
Lo esencial de la IA — una vez por semana
Siete historias que de verdad importaron, elegidas a mano. Sin ruido ni notas de prensa.
¡Listo! Revisa tu correo para la confirmación.