Live 18/11 · Cohorte 318 nov à 19h00 MSK — atelier pratique gratuit sur les agents IA · La Cohorte 3 démarre le 23/11Atelier 18/11 →Cohorte 3 (23/11)
SortieAnalyse · Sur le radarHabr AI→ original

Un développeur crée un client Ollama en KMP : deux bugs de sérialisation et de streaming

Un développeur a montré comment connecter une application mobile Kotlin Multiplatform à un serveur Ollama local : UI partagée avec Compose, Koin pour les dépendances, Ktor pour le réseau. Le serveur s'ouvre aux téléphones du réseau avec une seule variable, OLLAMA_HOST=0.0.0.0. L'essentiel du temps a été perdu sur deux bugs discrets : un champ stream qui disparaît et une réponse qui arrive d'un bloc au lieu d'un flux. Les deux se corrigent en quelques lignes, à condition de savoir où chercher.

Traité par IA depuis Habr AI ; édité par Hamidun News
Un développeur crée un client Ollama en KMP : deux bugs de sérialisation et de streaming
Source : Habr AI. Collage: Hamidun News.
Écouter l'article
Fiche synthèse et chiffres clés
ParamètreDonnées
Source primaireHabr AI ↗
Rédacteur en chef
Chiffre clé #1encodeDefaults = true rétablit stream=false dans la requête JSON
Chiffre clé #2Pour le flux, il faut preparePost().execute et non un simple post()
Pour le HTTPusesCleartextTraffic sur Android, NSAllowsArbitraryLoads sur iOS

Lancer un modèle local sur un ordinateur portable est aujourd'hui simple. Il est plus difficile de lui construire un vrai client mobile qui fonctionne à la fois sur Android et sur iPhone, sans obliger à écrire l'UI deux fois. L'auteur d'un article sur Habr a suivi ce chemin avec Kotlin Multiplatform et décrit honnêtement là où il a trébuché. Pour ceux qui comptent faire la même chose, c'est plus utile que n'importe quelle théorie.

Le serveur : trois commandes et une variable

Ollama s'installe via brew ou via un script d'installation sur Ubuntu. Par défaut, le serveur n'écoute que sur localhost : l'émulateur et un vrai téléphone sur le même réseau ne peuvent donc pas l'atteindre. Pour ouvrir l'accès, on lance le serveur avec OLLAMA_HOST=0.0.0.0:11434. Ensuite, le modèle est téléchargé via ollama pull ; sur les Mac à puce Apple, l'auteur prend des builds MLX, par exemple gemma4:12b-mlx. La commande ollama ps montre que le modèle tient entièrement dans la mémoire du GPU (dans la sortie, PROCESSOR 100% GPU).

On peut vérifier le serveur avec un simple curl sur /api/generate. Si la requête contient stream: false, la réponse arrive en un seul JSON.

Le client : Compose, Koin, Ktor

Un projet KMP avec UI partagée : Koin gère les dépendances et les ViewModel, Ktor le réseau, Navigation3 la navigation. L'auteur a choisi non pas un startKoin global, mais le composable KoinApplication. Le conteneur est créé dans la fonction App(), si bien que @Preview dans Android Studio n'entre pas en conflit avec un état global et qu'il n'est pas nécessaire de toucher au code Swift généré par l'IDE. Le prix de ce choix : une partie de l'API n'est pas disponible, notamment by inject(), by viewModel() et androidContext(). Comment découper correctement en modules une grande application avec cette approche, l'auteur ne l'a pas encore déterminé.

Xcode et macOS restent de toute façon nécessaires pour compiler pour iOS. KMP évite d'écrire l'UI en Swift, mais pas les outils d'Apple.

Deux pièges qui ont coûté des heures

Le premier piège concerne le sérialiseur. Kotlinx.Serialization n'écrit pas par défaut dans le JSON les champs qui ont leur valeur par défaut. Si vous avez stream = false, le champ disparaît tout simplement de la requête, et Ollama active alors le mode streaming en son absence. Résultat : vous attendez une seule réponse et vous recevez une pluie de lignes JSON. Le remède est le réglage Json { encodeDefaults = true }. Le Network Inspector d'Android Studio a aidé à trouver la cause.

Le second piège concerne le streaming lui-même. La réponse arrivait d'un bloc à la fin ; l'auteur a d'abord soupçonné une mise en tampon dans OkHttp et a même réparti le code par plateforme via expect/actual. Le problème était ailleurs : au lieu de post(), il faut utiliser preparePost() et lire le corps via execute et bodyAsChannel, ligne par ligne. Chaque ligne est un chunk JSON distinct avec les champs message, done et error. Côté client, tout cela est converti en Flow d'événements : Token, Thinking, Done et Error. L'annulation de la coroutine ferme la connexion et arrête la génération. Les tokens de raisonnement (Thinking) arrivent séparément, si le modèle les fournit.

Autre détail : pour /api/chat, le rôle de chaque message est obligatoire, alors que /api/generate n'a pas de rôles, on y passe simplement un prompt.

Ce qu'il faut retenir en pratique

Si le serveur fonctionne en HTTP, il faut usesCleartextTraffic="true" sur Android, et pour iOS il faudra ajouter NSAllowsArbitraryLoads dans Info.plist. À vrai dire, dans l'expérience de l'auteur, l'émulateur iPhone a laissé passer les requêtes même sans cela. En production, on ne doit pas utiliser du HTTP non chiffré ni un serveur sur 0.0.0.0 sans authentification : le texte ne dit pas un mot de la sécurité, il s'agit donc d'un banc d'essai domestique ou interne, et non d'une architecture prête à l'emploi.

Le projet est encore brut : l'interface reste à peaufiner et il n'y a pas d'historique des conversations. L'auteur ajoute qu'un client pour les modèles cloud de cloud.ru, avec images et audio, est déjà écrit. La tâche de renommage du projet a pris au modèle environ cinq minutes et environ 50 roubles, mais le prix dépend fortement du modèle choisi.

Conclusion pour le développeur : si vous faites un client mobile pour Ollama, activez tout de suite encodeDefaults, utilisez d'emblée preparePost pour les flux et ne cherchez pas le problème dans OkHttp. Les sources sont sur GitHub (app-z/OllameChatKmp) et peuvent servir de base de départ.

Dossier

Questions clés sur cet article

Que sait-on sur Le serveur : trois commandes et une variable ?

Ollama s'installe via brew ou via un script d'installation sur Ubuntu. Par défaut, le serveur n'écoute que sur localhost : l'émulateur et un vrai téléphone sur le même réseau ne peuvent donc pas l'atteindre. Pour ouvrir l'accès, on lance le serveur avec OLLAMA_HOST=0.0.0.0:11434. Ensuite, le modèle est téléchargé via ollama pull ; sur les Mac à puce Apple, l'auteur prend des builds MLX, par exemple gemma4:12b-mlx.

Que sait-on sur Le client : Compose, Koin, Ktor ?

Un projet KMP avec UI partagée : Koin gère les dépendances et les ViewModel, Ktor le réseau, Navigation3 la navigation. L'auteur a choisi non pas un startKoin global, mais le composable KoinApplication. Le conteneur est créé dans la fonction App(), si bien que @Preview dans Android Studio n'entre pas en conflit avec un état global et qu'il n'est pas nécessaire de toucher au code Swift généré par l'IDE.

Que sait-on sur Deux pièges qui ont coûté des heures ?

Le premier piège concerne le sérialiseur. Kotlinx.Serialization n'écrit pas par défaut dans le JSON les champs qui ont leur valeur par défaut. Si vous avez stream = false, le champ disparaît tout simplement de la requête, et Ollama active alors le mode streaming en son absence. Résultat : vous attendez une seule réponse et vous recevez une pluie de lignes JSON. Le remède est le réglage Json { encodeDefaults = true }. Le Network Inspector d'Android Studio a aidé à trouver la cause.

ZK
Zhemal Khamidun — CPO d'Alpina Digital, product manager d'AlpinaGPT. Conçoit des produits IA depuis 2019. Hamidun News est un projet personnel devenu un agrégateur en production avec plus de 6 000 lecteurs.
✦ Unified AI Workspace · Nomad

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.

Try Nomad AI →Launch Nomad Agent
Qu'en pensez-vous ?
Chargement des commentaires…