Ir al contenido

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.

Dos dialectos, un puerto OpenAI y Ollama, los mismos modelos detrás de ambos — lo que tu editor ya habla, funciona.
Contados, no estimados Las cuentas de tokens vienen del motor, y finish_reason significa lo que dice en vez de ser una constante plausible.
Solo loopback, sin clave Atado a 127.0.0.1 y a nada más. Si lo necesitas alcanzable, pon un proxy delante: esa es la pieza que sabe hacerlo.
Errores que dicen cuál 405 y no 404 cuando el verbo está mal, y un rechazo en vez de una cola silenciosa cuando ya hay una generación en curso.

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.