Ingeniería
Un extremo, compatible con OpenAI y con Ollama
Runlix sirve una API HTTP en 127.0.0.1, compatible con la API de OpenAI y con la de Ollama: chat, completados y embeddings nativos. Los editores, scripts y conectores que ya hablan con una de las dos apuntan ahí y funcionan.
Actualizado 2026-09-18
La mitad del valor de un modelo que corre en local es todo lo demás que entonces puede usarlo: una extensión del editor, un script, un conector dentro de otro programa. Todos hablan uno de dos protocolos. Runlix es compatible con los dos, desde el mismo puerto, para los mismos modelos — y no es Ollama, ni necesita que esté instalado.
finish_reason significa lo que
dice en vez de ser una constante plausible.
Se enciende en los Ajustes, con el puerto al lado del interruptor. Por defecto 11435, uno por encima del de Ollama, para que tener los dos instalados no sea una pelea. Si el puerto está ocupado el interruptor vuelve atrás y el motivo queda escrito al lado, en vez de que el servidor simplemente no esté.
Qué sirve
| Ruta | Nota | |
|---|---|---|
| GET | / |
Comprobación de salud, respondida como la esperan los clientes de Ollama |
| GET | /api/tags |
La lista de modelos, forma Ollama |
| POST | /api/show |
Ficha del modelo, que los clientes de Ollama piden antes de un chat |
| POST | /api/generate |
NDJSON en flujo, por defecto |
| POST | /api/chat |
NDJSON; llamadas a herramientas en la forma de Ollama |
| GET | /v1/models |
Forma OpenAI |
| POST | /v1/chat/completions |
Eventos SSE; llamadas a herramientas en la forma de OpenAI |
| POST | /v1/embeddings |
Embeddings nativos, forma OpenAI — también en /embeddings |
Los dos protocolos discrepan de maneras que solo se ven frente a clientes reales, y cada discrepancia de abajo ha costado una sesión de investigación.
La comprobación de salud no es adorno: los clientes de la API de Ollama la piden antes que
cualquier otra cosa, y sin la respuesta que esperan se detienen con «something went wrong».
/api/tags incluye un digest porque el cliente de línea de comandos de Ollama recorta ese campo
a doce caracteres sin comprobar que exista y se cae cuando falta. /api/show lee tanto model
como name, porque ese cliente manda los dos y deja uno vacío. Los valores
por defecto del flujo difieren según el protocolo, y cada uno sigue su propia convención: NDJSON
encendido para Ollama, apagado para OpenAI. Las llamadas a herramientas tienen formas distintas —
arguments es un objeto en uno y una cadena en el otro, y solo uno lleva id y type.
Ninguna de estas rarezas está en una especificación: están en el comportamiento de los clientes, y la única manera de encontrarlas fue ejecutar los reales y mirar dónde tropiezan.
O sea: compatible con la API de Ollama para listar, mostrar y conversar, probado contra el propio
cliente de Ollama. No para pull, push, create, copy, delete, ps, embed ni version —
sus modelos, Runlix los descarga y los gestiona él mismo. Cualquier cliente compatible con OpenAI
que necesite /v1/chat/completions, /v1/models o /v1/embeddings funciona.
Qué puedes apuntarle de verdad
Cualquier cosa que hable uno de los dos protocolos, que a estas alturas es casi todo.
Las extensiones de editor que permiten fijar una URL base — Continue, Cline, el asistente de
Zed, el modo compatible con OpenAI de una docena más — necesitan /v1/chat/completions y
/v1/models, y ambos están. Los scripts que usan las bibliotecas oficiales de OpenAI
funcionan apuntando la URL base a http://127.0.0.1:11435/v1 y pasando cualquier cadena como
clave, ya que no hay ninguna que comprobar. Todo lo que ya habla con Ollama encuentra los
extremos que busca, en las formas que espera, porque se probaron contra el cliente de línea de
comandos de Ollama y no contra la documentación.
El puerto es uno por encima del de Ollama a propósito. Si mantienes los dos, ninguno tiene que mudarse, y un cliente configurado para uno no acabará en silencio en el otro respondiendo con un modelo que no elegiste.
Lo que obtienes es el modelo que has nombrado, resuelto entre los que has descargado. Nombra uno que no tengas y el error dice qué nombre ha fallado y que no está entre los descargados — no un cuatrocientos genérico que te deja comparando cadenas a ojo. No mandes ningún nombre y obtienes el modelo cargado, que es el último usado y no uno al azar.
Qué te dice, y con cuánta honestidad
Errores que dicen cuál
Un método equivocado sobre una ruta que existe responde 405, no 404. La diferencia importa mientras depuras un cliente: 404 te manda a buscar una errata en la URL, 405 te dice que la URL estaba bien y el verbo no.
Una segunda generación mientras hay una en curso se rechaza en vez de encolarse. Encolar significaría una espera imprevisible sin manera de saber cuánto dura, y para un servidor local de un solo usuario un rechazo que puedes reintentar es más útil que una promesa que podría tardar cuatro minutos.
Números contados, no estimados
usage viene del motor: tokens del prompt, tokens de la respuesta, total. No
deducidos de los caracteres, que es el atajo habitual y se equivoca en una cantidad distinta
para cada idioma.
finish_reason también lo lee del motor, así que length significa
que la respuesta la cortó de verdad el límite, y no es una constante que parecía plausible.
Los parámetros de muestreo se leen tanto arriba como dentro de options, porque
los clientes de Ollama los ponen ahí, y se aplican en cada petición partiendo
de los valores de reposo reales del motor, para que nada se filtre del llamante anterior.
Manda un seed y dos peticiones idénticas dan la misma respuesta. Sin él,
varían, como debe hacer un modelo — una semilla ausente se queda ausente en vez de convertirse
en cero en silencio.
Cierra la conexión a medias y la generación se detiene. Honestamente: no al instante — se detiene cuando el búfer de escritura se llena y el socket lanza.
El flujo, en los dos dialectos
Los dos protocolos transmiten de manera distinta, y cada uno mantiene su convención, porque un cliente escrito para uno espera esa y ninguna otra.
Los clientes de OpenAI reciben eventos SSE: líneas data: que llevan cada una un trozo de JSON,
una sola identidad compartida por todos los trozos de la misma respuesta, y un data: [DONE]
final. Los clientes de Ollama reciben objetos JSON separados por saltos de línea, uno por línea, y
el último con los recuentos. Ambos llegan a trozos, así que las primeras palabras aparecen
mientras el resto todavía se está generando.
Una excepción, y es querida: cuando hay herramientas de por medio, el flujo retiene. Media llamada JSON llegando a trozos no le sirve a ningún cliente que haya consumido alguna.
Solo localhost, y ninguna autenticación
El servidor se ata a 127.0.0.1 y a nada más. Eso significa alcanzable desde tu máquina y no
desde tu red. No hay token, ni contraseña, ni usuario.
Es una decisión más que un descuido. La autenticación que cuenta es la que alguien mantiene, y una casilla de «exigir una clave» sobre un socket de bucle local aporta sobre todo la sensación de tener una. Si necesitas que sea alcanzable desde otro sitio, pon un proxy delante y déjale decidir quién entra — es la pieza que ya conoce ese oficio.
Poner algo delante
Si de verdad quieres llegar desde otra máquina, la forma que funciona es un proxy inverso en el
mismo Mac, escuchando donde quieras y reenviando a 127.0.0.1:11435. El proxy es el lugar de la
autenticación — un token, TLS mutuo, un proveedor de identidad, según tu situación — porque es una
pieza con un mantenedor y un modelo de amenaza, y una casilla en un panel de ajustes no es ni lo
uno ni lo otro.
Dos cosas a tener en cuenta. La generación va de una en una, así que un extremo compartido rechazará las peticiones que lleguen durante otra; si lo van a usar varias personas, el proxy necesita una cola y una decisión sobre cuánto espera quien llama. Y no hay contabilidad por llamante: los números de uso de cada respuesta valen para esa respuesta y no se suman en ninguna parte.
Embeddings, desde el mismo motor
/v1/embeddings convierte texto en vectores con el mismo motor Rust que ejecuta los modelos del
chat, en el mismo proceso — ningún segundo servidor, nada de Python. Lee las familias de
codificadores que la biblioteca mlx-embeddings cubre para BERT y XLM-RoBERTa, traducidas y
verificadas contra ella del mismo modo en que las familias del chat se verifican contra mlx-lm.
El modelo que usa es el que eliges en los Ajustes, o el primer modelo de embeddings que hayas descargado. Si no hay ninguno responde 503, «ningún modelo de embeddings cargado» — que significa: descarga uno, no que falte el extremo. La forma de la petición es la de OpenAI: una cadena o una lista de cadenas de entrada, un vector por cada una de salida. Demasiados textos en una sola petición son un 413 que dice el límite, no una espera que caduca.
Qué no hace
Las herramientas las devuelve, no las ejecuta. Manda tools con la petición y las llamadas
del modelo te vuelven en la forma correcta para tu protocolo; ejecutarlas es cosa de tu cliente.
El chat de Runlix es el único sitio donde las ejecuta Runlix.
El parámetro stop se acepta y se ignora: el motor sabe hacer secuencias de parada, y la API
ahora mismo no se las pasa. El extremo de embeddings de Ollama no se sirve: los embeddings están en
las rutas de OpenAI. Una generación cada vez.
Los mensajes no se aplanan nunca
Merece señalarse porque es la manera más común en que una API local produce peores respuestas de
las que el modelo es capaz. Los mensajes se pasan como un array estructurado y el chat_template
lo aplica el motor, porque el motor es la pieza que tiene el analizador léxico.
Medido sobre un mismo modelo, mismo prompt: con la plantilla la respuesta es "Paris.". Aplanada
en líneas rol: contenido se convierte en "Paris\nassistant: …\nuser: What" — el modelo sigue
inventándose turnos. No parece un defecto. Parece un modelo peor.