API OpenAI-compatible in locale: puntare i tuoi tool su Ollama o vLLM senza riscrivere il client

Cambi base_url e il client resta uguale. Le cose che invece si rompono puntando i tuoi tool su Ollama, LM Studio, llama.cpp o vLLM: nomi dei modelli, tool calling, streaming e due default silenziosi.

Francesco Pio Nocerino6 minAI
PercorsoAI Engineering: gli articoliParte 10 di 23
API OpenAI-compatible in locale: puntare i tuoi tool su Ollama o vLLM senza riscrivere il client
Indice

Questo pezzo è il seguito pratico della puntata 2 della Guida AI locale: hai scelto il runtime, ora devi collegarci i tuoi tool. Vive nell'hub AI Engineering ed è per chi ha già codice che parla con l'SDK di OpenAI e non vuole riscriverlo.

In breve: Ollama, LM Studio, llama.cpp (llama-server) e vLLM espongono un'API in stile OpenAI sotto /v1. Per la chat semplice basta cambiare base_url, api_key e model. Quello che si rompe sta altrove. I nomi dei modelli. Il tool calling, che sul server va acceso e dipende dal modello. Lo streaming con tool e usage. E due default silenziosi su contesto e sampling. Prima di dichiarare "compatibile", fai girare un test di cinque richieste.

Cosa vuol dire davvero "OpenAI-compatible"

Un'API OpenAI-compatible è un server che accetta le stesse richieste HTTP dell'API di OpenAI (stessi path, stessi campi JSON, stesso formato di risposta), così il client ufficiale funziona cambiando solo l'indirizzo. La voce in poche righe è anche nel glossario (API OpenAI-compatible).

La parola chiave è *sottoinsieme*. La documentazione di Ollama lo dice in apertura: supporta una parte dell'API, con una lista di campi spuntati e non spuntati. Anche vLLM elenca cosa ignora e cosa aggiunge. Compatibile non significa identico: significa che il caso comune passa.

Non "funziona con il mio client?". È: quali campi usa il mio client, e il server li onora o li ignora in silenzio?

Il cambio che basta: base_url, chiave, modello

Per una chat senza tool, il cambio è davvero di tre righe. Gli indirizzi di default sono questi (verificali sulla versione che installi):

Runtime

base_url di default

Chiave

Nome del modello

Ollama

http://localhost:11434/v1

richiesta dal client, ignorata dal server

il tag scaricato, es. qwen3:8b

LM Studio

http://localhost:1234/v1

non serve in locale

l'identificativo mostrato dall'app

llama.cpp (llama-server)

http://localhost:8080/v1

opzionale con --api-key

quello del file, o un alias con --alias

vLLM

http://localhost:8000/v1

opzionale con --api-key

l'id Hugging Face, o --served-model-name

Le porte vengono dalle rispettive documentazioni: Ollama, LM Studio, server di llama.cpp e l'esempio ufficiale di vLLM.

La regola che ti salva dopo è semplice: questi tre valori non vanno mai scritti nel codice. Stanno in variabili d'ambiente, così passi da cloud a locale (e da Ollama a vLLM) senza toccare una riga. Lo stesso schema vale quando metti un gateway in mezzo: con Cloudflare AI Gateway cambi ancora base_url e tieni log, cache e tetto di spesa davanti al provider.

import os
from openai import OpenAI

client = OpenAI(
    base_url=os.environ.get("LLM_BASE_URL", "http://localhost:11434/v1"),
    api_key=os.environ.get("LLM_API_KEY", "locale"),  # il client la vuole comunque
)
MODEL = os.environ["LLM_MODEL"]

r = client.chat.completions.create(
    model=MODEL,
    messages=[{"role": "user", "content": "Rispondi solo: ok"}],
)
print(r.choices[0].message.content)

Con Ollama la chiave è obbligatoria per il client ma il server la ignora, come scrive la sua stessa documentazione. Mettici una stringa qualsiasi e vai avanti. Se aggiorni Ollama, dalla 0.40.2 i modelli scaricati prima vengono convertiti in background e la copia vecchia resta come backup (per un po' ogni modello occupa circa il doppio su disco): cosa controllare è in Ollama 0.40.

Prima cosa che si rompe: i nomi dei modelli

Il codice scritto per il cloud ha quasi sempre un nome di modello cablato. In locale quel nome non esiste. Ogni runtime ha la sua convenzione: tag in Ollama, id Hugging Face in vLLM, nome del file o alias in llama-server.

Le soluzioni sono due. La pulita è leggere il modello da configurazione, come sopra. La scorciatoia, per tool di terzi che non ti lasciano scegliere, è dare al modello locale il nome che il tool si aspetta. Ollama documenta ollama cp proprio per copiare un modello sotto un nome come gpt-3.5-turbo. vLLM ha --served-model-name, llama-server ha --alias.

Un controllo utile prima di tutto: GET /v1/models. Ti dice quale nome il server si aspetta davvero, invece di indovinarlo.

curl -s http://localhost:11434/v1/models

Seconda: il tool calling non è acceso per default

Qui sta la differenza tra "la chat funziona" e "il mio agente funziona". Il campo tools passa, ma il comportamento dipende da tre cose: il runtime, come l'hai avviato e il modello.

  • vLLM: per lasciare che il modello decida da solo quando chiamare un tool servono due flag all'avvio, --enable-auto-tool-choice e --tool-call-parser con il parser giusto per la famiglia di modello. La documentazione sul tool calling li segna come obbligatori. In compenso tool_choice accetta auto, required, none e la chiamata a una funzione specifica.
  • Ollama: tools è supportato su /v1/chat/completions, ma nella sua tabella di compatibilità tool_choice non è tra i campi spuntati. Se il tuo codice forza una chiamata con tool_choice, non contare che venga rispettato.
  • llama.cpp: llama-server gestisce le chiamate ai tool quando lo avvii con --jinja, cioè usando il template di chat del modello (guida function calling).

E poi c'è il modello. Se non è addestrato per chiamare tool, nessun flag lo rende affidabile. È lo stesso problema del tool mal descritto che rompe il loop, con un livello in più: qui può essere il server a non capire la risposta del modello.

Su vLLM c'è un dettaglio che vale la pena conoscere. Con tool_choice="auto" e tool senza strict: true, la documentazione avverte che markup malformato può finire nella risposta; l'opzione --tool-strict-level alza il vincolo lato server. Se i tuoi tool arrivano da un framework che non imposta strict, è il primo posto dove guardare.

Terza: streaming, usage e tool insieme

Lo streaming semplice di testo di solito passa ovunque. I problemi arrivano quando combini streaming con altro.

Il conteggio dei token, in streaming, arriva solo se lo chiedi con stream_options={"include_usage": True}. Ollama lo elenca tra i campi supportati. Se il tuo client conta costi o limiti su usage senza chiederlo esplicitamente, in locale trovi un valore vuoto. Non un errore.

I tool in streaming sono il caso da provare sempre a mano. La chiamata arriva a pezzi nei delta, e il modo in cui il server li spezza dipende dal parser e dal modello. Il client ufficiale li ricompone; un parser scritto in casa per il cloud a volte no.

I modelli che "ragionano" aggiungono un'altra variabile: i runtime espongono il ragionamento con campi e controlli propri (Ollama, per esempio, documenta reasoning_effort con valori che dipendono dal modello). Se il tuo client mostra tutto quello che arriva, verifica cosa finisce a schermo.

Due default che non vedi: contesto e sampling

Il primo è il contesto. L'API di OpenAI non ha un campo per impostare la finestra di contesto, quindi il server usa la sua. Ollama lo scrive chiaramente: per cambiarla crei un Modelfile con PARAMETER num_ctx e chiami il nuovo nome. Se passi un documento lungo e il modello sembra "dimenticare" l'inizio, il problema spesso è questo, non il modello.

Il secondo è il sampling. vLLM per default applica il generation_config.json del modello su Hugging Face, se esiste, e quindi alcuni parametri di sampling cambiano rispetto a quelli che ti aspetti. Si disattiva con --generation-config vllm. Se confronti risposte tra cloud e locale, fissa temperature e gli altri parametri nella richiesta invece di fidarti dei default.

La chiave non è un firewall

In locale viene naturale avviare il server, aprirlo sulla rete e passare oltre. Non farlo senza pensarci.

Con Ollama la chiave nel client è decorativa. Con vLLM la documentazione avverte che --api-key protegge solo i path /v1, /v2 e /inference: altri endpoint dello stesso server, come /invocations, restano aperti e danno accesso alle stesse capacità. Il consiglio ufficiale è un reverse proxy davanti. Se il server deve stare su un'altra macchina, tienilo su una rete privata o dietro un proxy con autenticazione vera.

Un test di compatibilità in cinque richieste

Prima di dire "il mio tool funziona in locale", fai girare queste cinque richieste con il client vero, non con curl:

  • Modelli: GET /v1/models e controlla che il nome configurato ci sia.
  • Chat: una richiesta semplice, risposta non vuota.
  • Streaming con usage: stream=True con include_usage, e verifica che l'ultimo chunk porti i token.
  • Tool: una richiesta con un tool finto e una domanda che lo richiede; controlla che tool_calls arrivi strutturato e non come testo nel content.
  • Giro completo: rimanda il risultato del tool con il messaggio role: "tool" e verifica che il modello lo usi nella risposta finale.

Se uno dei cinque fallisce, sai già dove guardare: nome, flag del server, template di chat o modello. È lo stesso spirito del pezzo sugli agenti che non servono: un controllo deterministico prima di fidarti del loop.

Cosa conviene fare adesso

Sposta base_url, chiave e modello in variabili d'ambiente, oggi, anche se resti sul cloud. Poi avvia il runtime scelto nella puntata 2 e fai girare il test delle cinque richieste. Se passa, hai un fallback locale senza fork del codice. Se non passa, sai quale dei tre punti sistemare.

La prossima puntata della serie parla di modelli, quantizzazione e VRAM: cioè quanto modello ci sta davvero sulla tua macchina. Se arrivi da qui senza contesto, parti dalla puntata 1 e dall'hub AI Engineering.

[ DOMANDE FREQUENTI ]

Domande frequenti

01Devo riscrivere il client per usare un modello locale?

Per chat, completamenti ed embedding di solito no: cambi base_url, chiave e nome del modello. Va rivisto il codice che dipende da campi non supportati dal server, come tool_choice su Ollama, o da default di contesto e sampling.

02Quale runtime è più compatibile con l'SDK di OpenAI?

Dipende dai campi che usi, non da una classifica. vLLM copre più opzioni di tool_choice ma chiede flag all'avvio; Ollama è il più rapido da collegare ma ha una tabella di campi non supportati. Leggi la tabella del runtime contro le chiamate che fa davvero il tuo codice.

03Serve una chiave API in locale?

Il client ufficiale la richiede comunque, quindi metti un valore. Lato server serve solo se il servizio è raggiungibile da altri, e anche allora non basta da sola: su vLLM la chiave non copre tutti gli endpoint.

Condividi