Aller au contenu

Ingénierie

Un point d'accès, compatible avec OpenAI et avec Ollama

Runlix sert une API HTTP sur 127.0.0.1, compatible avec l'API d'OpenAI et avec celle d'Ollama : chat, complétions et embeddings natifs. Les éditeurs, scripts et connecteurs qui parlent déjà à l'une des deux pointent dessus et fonctionnent.

Mis à jour 2026-09-18

La moitié de l'intérêt d'un modèle qui tourne en local, c'est tout le reste qui peut alors s'en servir : une extension d'éditeur, un script, un connecteur dans un autre programme. Tous parlent l'un de deux protocoles. Runlix est compatible avec les deux, depuis le même port, pour les mêmes modèles — et n'est pas Ollama, ni n'a besoin qu'il soit installé.

Deux dialectes, un seul port OpenAI et Ollama, les mêmes modèles derrière les deux — ce que votre éditeur parle déjà fonctionne.
Comptés, pas estimés Les nombres de tokens viennent du moteur, et finish_reason veut dire ce qu'il dit au lieu d'être une constante plausible.
Loopback seulement, pas de clé Lié à 127.0.0.1 et à rien d'autre. S'il doit être joignable, mettez un proxy devant : c'est la pièce qui sait faire.
Des erreurs qui disent laquelle 405 plutôt que 404 quand le verbe est faux, et un refus plutôt qu'une file silencieuse quand une génération tourne déjà.

Il s'allume dans les Réglages, avec le port à côté de l'interrupteur. Par défaut 11435, un au-dessus de celui d'Ollama, pour qu'avoir les deux installés ne soit pas une bagarre. Si le port est pris, l'interrupteur revient en arrière et la raison est écrite à côté, plutôt que le serveur ne soit simplement pas là.

Ce qu'il sert

Chemin Note
GET / Contrôle de santé, répondu comme l'attendent les clients d'Ollama
GET /api/tags La liste des modèles, forme Ollama
POST /api/show Fiche du modèle, que les clients d'Ollama demandent avant un chat
POST /api/generate NDJSON en flux par défaut
POST /api/chat NDJSON ; appels d'outils à la forme d'Ollama
GET /v1/models Forme OpenAI
POST /v1/chat/completions Événements SSE ; appels d'outils à la forme d'OpenAI
POST /v1/embeddings Embeddings natifs, forme OpenAI — aussi sur /embeddings

Les deux protocoles se contredisent d'une façon qui n'apparaît que face à de vrais clients, et chaque désaccord ci-dessous a coûté une séance d'enquête.

Le contrôle de santé n'est pas décoratif : les clients de l'API d'Ollama le demandent avant toute autre chose, et sans la réponse qu'ils attendent ils s'arrêtent sur « something went wrong ». /api/tags contient un digest parce que le client en ligne de commande d'Ollama tronque ce champ à douze caractères sans vérifier qu'il existe et s'effondre quand il manque. /api/show lit à la fois model et name, parce que ce client envoie les deux et en laisse un vide. Les valeurs par défaut du flux diffèrent selon le protocole, et chacun suit sa propre convention : NDJSON allumé pour Ollama, éteint pour OpenAI. Les appels d'outils ont des formes différentes — arguments est un objet chez l'un et une chaîne chez l'autre, et un seul porte id et type.

Aucune de ces particularités ne figure dans une spécification : elles sont dans le comportement des clients, et la seule façon de les trouver a été de faire tourner les vrais et de regarder où ils achoppent.

Donc : compatible avec l'API d'Ollama pour lister, montrer et discuter, éprouvé contre le propre client d'Ollama. Pas pour pull, push, create, copy, delete, ps, embed ni version — ses modèles, Runlix les télécharge et les gère lui-même. N'importe quel client compatible OpenAI qui a besoin de /v1/chat/completions, /v1/models ou /v1/embeddings fonctionne.

Ce que vous pouvez vraiment pointer dessus

Tout ce qui parle l'un des deux protocoles, c'est-à-dire à peu près tout désormais.

Les extensions d'éditeur qui permettent de fixer une URL de base — Continue, Cline, l'assistant de Zed, le mode compatible OpenAI d'une douzaine d'autres — ont besoin de /v1/chat/completions et /v1/models, et les deux sont là. Les scripts qui utilisent les bibliothèques officielles d'OpenAI fonctionnent en pointant l'URL de base sur http://127.0.0.1:11435/v1 et en passant n'importe quelle chaîne comme clé, puisqu'il n'y en a aucune à vérifier. Tout ce qui parle déjà à Ollama trouve les points d'accès qu'il cherche, dans les formes qu'il attend, parce qu'ils ont été éprouvés contre le client en ligne de commande d'Ollama et pas contre la documentation.

Le port est un au-dessus de celui d'Ollama exprès. Si vous faites tourner les deux, aucun n'a à déménager, et un client configuré pour l'un n'atterrira pas en silence chez l'autre en répondant avec un modèle que vous n'avez pas choisi.

Ce que vous obtenez, c'est le modèle que vous avez nommé, résolu parmi ceux que vous avez téléchargés. Nommez-en un que vous n'avez pas et l'erreur dit quel nom a échoué et qu'il n'est pas parmi les téléchargés — pas un quatre-cents générique qui vous laisse comparer des chaînes à l'œil. N'envoyez aucun nom et vous obtenez le modèle chargé, qui est le dernier utilisé et non un pris au hasard.

Ce qu'il vous dit, et avec quelle honnêteté

Des erreurs qui disent laquelle

Une mauvaise méthode sur un chemin qui existe répond 405, pas 404. La différence compte quand vous cherchez le défaut d'un client : 404 vous envoie chercher une faute de frappe dans l'URL, 405 vous dit que l'URL était bonne et le verbe non.

Une deuxième génération pendant qu'une autre tourne est refusée plutôt que mise en file. Mettre en file voudrait dire une attente imprévisible sans moyen d'en connaître la durée, et pour un serveur local à un seul utilisateur, un refus que vous pouvez réessayer est plus utile qu'une promesse qui pourrait prendre quatre minutes.

Des chiffres comptés, pas estimés

usage vient du moteur : tokens du prompt, tokens de la réponse, total. Pas déduits du nombre de caractères, ce qui est le raccourci habituel et se trompe d'une quantité différente pour chaque langue.

finish_reason aussi est lu depuis le moteur, donc length signifie que la réponse a réellement été coupée par la limite, et n'est pas une constante qui paraissait plausible.

Les paramètres d'échantillonnage sont lus à la fois en tête et à l'intérieur d'options, parce que les clients Ollama les mettent là, et appliqués à chaque requête depuis les vraies valeurs de repos du moteur, pour que rien ne s'échappe de l'appelant précédent.

Envoyez un seed et deux requêtes identiques donnent la même réponse. Sans, elles varient, comme un modèle doit le faire — une graine absente reste absente au lieu de devenir zéro en silence.

Fermez la connexion à mi-chemin et la génération s'arrête. Honnêtement : pas à l'instant — elle s'arrête quand le tampon d'écriture se remplit et que la socket lève.

Le flux, dans les deux dialectes

Les deux protocoles diffusent différemment, et chacun garde sa convention, parce qu'un client écrit pour l'un attend celle-là et rien d'autre.

Les clients OpenAI reçoivent des événements SSE : des lignes data: portant chacune un fragment de JSON, une identité unique partagée par tous les fragments d'une même réponse, et un data: [DONE] final. Les clients Ollama reçoivent des objets JSON séparés par des sauts de ligne, un par ligne, le dernier portant les comptes. Les deux arrivent par morceaux, les premiers mots apparaissent donc pendant que le reste s'engendre encore.

Une exception, et elle est voulue : quand des outils sont en jeu, le flux retient. Un demi- appel de fonction JSON arrivant par morceaux ne sert à aucun client qui en ait jamais consommé un.

Uniquement localhost, et aucune authentification

Le serveur se lie à 127.0.0.1 et à rien d'autre. Cela veut dire joignable depuis votre machine et pas depuis votre réseau. Il n'y a ni jeton, ni mot de passe, ni utilisateur.

C'est une décision plutôt qu'un oubli. L'authentification qui compte est celle que quelqu'un entretient, et une case « exiger une clé » sur une socket de bouclage fournit surtout le sentiment d'en avoir une. Si vous avez besoin que ce soit joignable d'ailleurs, mettez un proxy devant et laissez-le décider qui entre — c'est la pièce qui connaît déjà ce métier.

Mettre quelque chose devant

Si vous voulez vraiment y accéder depuis une autre machine, la forme qui fonctionne est un proxy inverse sur le même Mac, écoutant là où vous le souhaitez et relayant vers 127.0.0.1:11435. Le proxy est l'endroit de l'authentification — un jeton, du TLS mutuel, un fournisseur d'identité, selon votre situation — parce que c'est une pièce avec un mainteneur et un modèle de menace, et qu'une case dans un panneau de réglages n'est ni l'un ni l'autre.

Deux choses à garder en tête. La génération se fait une à la fois, un point d'accès partagé refusera donc les requêtes qui arrivent pendant une autre ; si plusieurs personnes doivent l'utiliser, le proxy a besoin d'une file et d'une décision sur la durée d'attente. Et il n'y a aucune comptabilité par appelant : les chiffres d'usage dans chaque réponse valent pour cette réponse et ne sont totalisés nulle part.

Les embeddings, depuis le même moteur

/v1/embeddings transforme du texte en vecteurs avec le même moteur Rust qui fait tourner les modèles du chat, dans le même processus — aucun second serveur, pas de Python. Il lit les familles d'encodeurs que la bibliothèque mlx-embeddings couvre pour BERT et XLM-RoBERTa, traduites et vérifiées contre elle de la même façon que les familles du chat le sont contre mlx-lm.

Le modèle utilisé est celui que vous choisissez dans les Réglages, ou le premier modèle d'embeddings téléchargé. S'il n'y en a aucun, il répond 503, « aucun modèle d'embeddings chargé » — ce qui veut dire : téléchargez-en un, pas que le point d'accès manque. La forme de la requête est celle d'OpenAI : une chaîne ou une liste de chaînes en entrée, un vecteur par entrée en sortie. Trop de textes dans une seule requête, c'est un 413 qui nomme la limite, pas une attente qui expire.

Ce qu'il ne fait pas

Les outils, il les renvoie, il ne les exécute pas. Envoyez tools avec votre requête et les appels du modèle vous reviennent à la forme correcte pour votre protocole ; les exécuter est l'affaire de votre client. Le chat de Runlix est le seul endroit où Runlix les exécute lui-même.

Le paramètre stop est accepté et ignoré : le moteur sait faire les séquences d'arrêt, et l'API ne les lui transmet pas actuellement. Le point d'accès d'embeddings d'Ollama n'est pas servi : les embeddings sont sur les chemins d'OpenAI. Une génération à la fois.

Les messages ne sont jamais aplatis

À signaler parce que c'est la façon la plus courante dont une API locale produit de moins bonnes réponses que le modèle n'en est capable. Les messages sont passés en tableau structuré et c'est le moteur qui applique le chat_template du modèle, parce que c'est lui qui tient l'analyseur lexical.

Mesuré sur un même modèle, même prompt : avec le modèle de conversation, la réponse est "Paris.". Aplatie en lignes rôle : contenu, elle devient "Paris\nassistant: …\nuser: What" — le modèle continue d'inventer des tours de parole. Cela n'a pas l'air d'un défaut. Cela a l'air d'un moins bon modèle.