Salta al contenuto

Ingegneria

Un endpoint, compatibile con OpenAI e con Ollama

Runlix serve un'API HTTP su 127.0.0.1, compatibile con l'API di OpenAI e con quella di Ollama: chat, completamenti ed embedding nativi. Editor, script e connettori che parlano già con l'una o con l'altra la puntano e funzionano.

Aggiornato 2026-09-18

Il pannello «API HTTP» nelle impostazioni di Runlix: l'interruttore acceso, la porta 11435, e la riga che dichiara l'ascolto su 127.0.0.1.

Metà del valore di far girare un modello in locale è tutto il resto che a quel punto può usarlo: un'estensione dell'editor, uno script, un connettore dentro un altro programma. Tutti parlano uno di due protocolli. Runlix è compatibile con entrambi, dalla stessa porta, sugli stessi modelli — e non è Ollama, né ha bisogno che sia installato.

Si accende nelle Impostazioni, con la porta accanto all'interruttore. La predefinita è 11435, una sopra quella di Ollama, così che avere installati entrambi non sia una lite. Se la porta è occupata l'interruttore torna indietro e il motivo è scritto lì accanto, invece che il server semplicemente non ci sia.

Due dialetti, una porta OpenAI e Ollama, gli stessi modelli dietro entrambi — quindi qualunque cosa parli già il tuo editor, funziona.
Contati, non stimati I conteggi dei token vengono dal motore, e finish_reason dice quello che dice invece di essere una costante plausibile.
Solo loopback, nessuna chiave Legato a 127.0.0.1 e nient'altro. Se ti serve raggiungibile, mettici davanti un proxy: è il pezzo che sa come si fa.
Errori che dicono quale sbaglio 405 invece di 404 quando il verbo è sbagliato, e un rifiuto invece di una coda muta quando una generazione è già in corso.

Cosa serve

Percorso Note
GET / Controllo di salute, risposto come se lo aspettano i client di Ollama
GET /api/tags L'elenco dei modelli, forma Ollama
POST /api/show Scheda del modello, che i client di Ollama chiedono prima di una chat
POST /api/generate NDJSON in streaming, per impostazione predefinita
POST /api/chat NDJSON; chiamate a strumenti nella forma di Ollama
GET /v1/models Forma OpenAI
POST /v1/chat/completions Eventi SSE; chiamate a strumenti nella forma di OpenAI
POST /v1/embeddings Embedding nativi, forma OpenAI — anche su /embeddings

I due protocolli non vanno d'accordo in modi che si vedono solo contro i client veri, e ognuno dei disaccordi qui sotto è costato una sessione di indagine.

Il controllo di salute non è decorazione: i client dell'API di Ollama lo chiedono prima di qualunque altra cosa, e senza la risposta che si aspettano si fermano con «something went wrong». /api/tags include un digest perché il client da riga di comando di Ollama lo tronca a dodici caratteri senza controllare che ci sia e va in panic quando manca. /api/show legge sia model sia name, perché quel client li manda tutti e due e ne lascia uno vuoto. Lo streaming ha predefiniti diversi fra i due protocolli e ognuno segue la propria convenzione: NDJSON acceso per Ollama, spento per OpenAI. Le chiamate a strumenti hanno forme diverse — arguments è un oggetto in uno e una stringa nell'altro, e solo uno porta id e type.

Quindi: compatibile con l'API di Ollama per elencare, mostrare e chattare, provato contro il client di Ollama stesso. Non per pull, push, create, copy, delete, ps, embed o version — i modelli Runlix li scarica e li gestisce da sé. Qualunque client compatibile OpenAI che abbia bisogno di /v1/chat/completions, /v1/models o /v1/embeddings funziona.

Cosa puoi davvero puntarci contro

Qualunque cosa parli uno dei due protocolli, che ormai è quasi tutto.

Le estensioni per editor che permettono di impostare un URL di base — Continue, Cline, l'assistente di Zed, la modalità compatibile OpenAI di una dozzina d'altri — hanno bisogno di /v1/chat/completions e /v1/models, e ci sono entrambi. Gli script che usano le librerie ufficiali di OpenAI funzionano puntando l'URL di base a http://127.0.0.1:11435/v1 e passando una stringa qualsiasi come chiave, visto che non c'è nessuna chiave da controllare. Qualunque cosa parli già con Ollama trova gli endpoint che cerca, nelle forme che si aspetta, perché sono stati provati contro il client da riga di comando di Ollama e non contro la documentazione.

La porta è una sopra quella di Ollama apposta. Se li tieni tutti e due, nessuno deve spostarsi, e un client configurato per uno non finirà in silenzio sull'altro rispondendo con un modello che non hai scelto.

Quello che ottieni è il modello che hai nominato, risolto fra quelli che hai scaricato. Nominane uno che non hai e l'errore dice quale nome ha fallito e che non è fra gli scaricati — non un quattrocento generico che ti lascia a confrontare stringhe a occhio. Non mandare nessun nome e ottieni il modello caricato, che è l'ultimo usato e non uno preso a caso.

Cosa ti risponde, e con quanta onestà

Errori che dicono quale sbaglio

Un metodo sbagliato su un percorso che esiste risponde 405, non 404. La differenza conta quando stai correggendo un client: 404 ti manda a cercare un refuso nell'indirizzo, 405 ti dice che l'indirizzo era giusto e il verbo no.

Una seconda generazione mentre una è in corso viene rifiutata invece che accodata. Accodare vorrebbe dire un'attesa imprevedibile senza modo di sapere quanto, e per un server locale a un utente solo un rifiuto che puoi riprovare è più utile di una promessa che potrebbe prendere quattro minuti.

Numeri contati

usage viene dal motore — prompt, completamento, totale. Non dedotto dai caratteri, che è la scorciatoia abituale ed è sbagliata di una quantità diversa per ogni lingua.

Anche finish_reason si legge dal motore, quindi length vuol dire che la risposta è stata davvero tagliata e non è una costante che sembrava plausibile.

I parametri di campionamento si leggono sia in cima sia dentro options, perché i client di Ollama li mettono lì, e si applicano a ogni richiesta partendo dai valori di riposo veri del motore, così niente trapela da chi ha chiamato prima.

Manda un seed e due richieste identiche danno due risposte identiche. Senza, variano, come deve fare un modello — un seme assente resta assente invece di diventare zero in silenzio.

Chiudi la connessione a metà e la generazione si ferma. Onestamente: non all'istante — si ferma quando il buffer di scrittura si riempie e il socket lancia.

Streaming, nei due dialetti

I due protocolli mandano il flusso in modo diverso e ognuno tiene la propria convenzione, perché un client scritto per uno si aspetta quella e nient'altro.

I client OpenAI ricevono eventi SSE: righe data: che portano ognuna un pezzo di JSON, un'identità sola condivisa da tutti i pezzi della stessa risposta, e un data: [DONE] finale. I client Ollama ricevono oggetti JSON separati da un a capo, uno per riga, con l'ultimo che porta i conteggi. Entrambi arrivano a pezzi, così le prime parole compaiono mentre il resto si sta ancora generando.

Un'eccezione, ed è voluta: quando ci sono di mezzo gli strumenti, il flusso trattiene. Mezza chiamata JSON che arriva a pezzi non serve a nessun client che ne abbia mai consumata una, quindi la decisione si manda intera o non si manda.

Solo localhost, e nessuna autenticazione

Il server si lega a 127.0.0.1 e a nient'altro. Non c'è un gettone, non c'è una password, non c'è un utente.

È una decisione più che una dimenticanza. L'autenticazione che conta è quella che qualcuno mantiene, e una casella «richiedi una chiave» su una socket di loopback fornisce soprattutto la sensazione di averne una. Se ti serve raggiungibile da altrove, mettici davanti un proxy e lascia decidere a lui chi entra — è il pezzo che quel mestiere lo sa già fare.

Gli embedding, dallo stesso motore

/v1/embeddings trasforma il testo in vettori con lo stesso motore Rust che fa girare i modelli della chat, nello stesso processo — nessun secondo server, niente Python. Legge le famiglie di encoder che la libreria mlx-embeddings copre per BERT e XLM-RoBERTa, tradotte e verificate contro di lei nello stesso modo in cui le famiglie della chat si verificano contro mlx-lm.

Il modello che usa è quello che scegli nelle Impostazioni, o il primo modello di embedding che hai scaricato. Se non ne hai nessuno risponde 503, «nessun modello di embedding caricato» — che vuol dire scaricane uno, non che l'endpoint manchi. La forma della richiesta è quella di OpenAI: una stringa o una lista di stringhe in ingresso, un vettore per ciascuna in uscita. Troppi testi in una sola richiesta sono un 413 che dice il limite, non un'attesa che scade.

Cosa non fa

Gli strumenti li restituisce, non li esegue. Manda tools con la richiesta e le chiamate del modello ti tornano nella forma giusta per il tuo protocollo; eseguirle è compito del tuo client. La chat di Runlix è l'unico posto in cui le esegue Runlix.

Se il chat_template di un modello non sa cosa siano gli strumenti, la richiesta riesce lo stesso e un avviso va nei log — il protocollo OpenAI non ha un campo per dire «ho passato questa roba e non l'ha capita nessuno».

Il parametro stop viene accettato e ignorato: il motore implementa le sequenze di arresto, e l'API oggi non gliele passa. L'endpoint degli embedding di Ollama non c'è: gli embedding stanno sui percorsi di OpenAI.

Una generazione per volta; una seconda richiesta in contemporanea viene rifiutata invece di restare in coda dietro a un'attesa che nessuno sa quanto dura.

Metterci davanti qualcosa

Se vuoi davvero raggiungerla da un'altra macchina, la forma che funziona è un proxy inverso sullo stesso Mac, in ascolto dove vuoi tu e che inoltra a 127.0.0.1:11435. È il proxy il posto dell'autenticazione — un gettone, TLS mutuo, un fornitore di identità, quello che il tuo caso richiede — perché è un pezzo che ha un manutentore e un modello di minaccia, e una casella in un pannello di impostazioni non è né l'uno né l'altro.

Due cose da tenere a mente se lo fai. La generazione è una per volta, quindi un endpoint condiviso rifiuterà le richieste che arrivano durante un'altra; se devono usarlo in più persone, il proxy ha bisogno di una coda e di una decisione su quanto un chiamante aspetta. E non c'è nessuna contabilità per chiamante: i numeri d'uso in ogni risposta sono veri per quella risposta e non vengono sommati da nessuna parte.

I messaggi non si appiattiscono mai

Vale la pena dirlo perché è il modo più comune in cui un'API locale produce risposte peggiori di quanto il modello sappia fare. I messaggi si passano come array strutturato e il chat_template lo applica il motore, perché il motore è il pezzo che ha in mano il tokenizer.

Misurato su uno stesso modello, stesso prompt: col template la risposta è "Paris.". Appiattita in righe ruolo: contenuto diventa "Paris\nassistant: …\nuser: What" — il modello continua a inventarsi turni. Non sembra un difetto. Sembra un modello peggiore.