Intermédiaire 15 minAPI

Intégrer Ollama dans une application Python via l'API REST

Ollama expose deux APIs HTTP sur le port 11434 : une API native (/api/generate, /api/chat) et une API OpenAI-compatible (/v1/chat/completions). Cette dernière est la voie royale pour l'intégration Ollama API Python : votre code utilise exactement le même SDK qu'avec GPT-4, mais tourne sur votre machine. Ce guide couvre les patterns concrets — streaming, JSON structuré, function calling — avec des exemples FastAPI et Flask prêts à coller dans un projet.

Par Mohamed Meguedmi·Màj 2026-05-15·Testé sur Windows, macOS, Linux

#Pourquoi passer par l'API REST

La CLI ollama run est pratique pour tester, mais elle n'est pas faite pour être appelée depuis une application. L'API REST, elle, est conçue pour ça : requêtes HTTP standards, JSON en entrée et sortie, streaming via Server-Sent Events. C'est ce que toutes les interfaces (Open WebUI, Continue.dev, LangChain) utilisent sous le capot.

Compatibilité OpenAI
L'endpoint /v1/chat/completions accepte exactement le même payload que api.openai.com/v1/chat/completions. Vous changez l'URL et la clé, et votre code existant fonctionne.
Pas de réinvention
Le SDK officiel openai en Python (ou n'importe quel client HTTP) parle directement à Ollama. Aucun client spécifique à apprendre.
Découplage du runtime
Votre app Python tourne dans son conteneur, Ollama dans le sien. Le jour où vous passez à vLLM ou LM Studio, vous ne changez que la base_url.
Multi-clients simultanés
Plusieurs scripts Python, un notebook Jupyter et Open WebUI peuvent taper sur la même instance Ollama. Le daemon gère la file d'attente tout seul.
i
API native vs OpenAI-compatible
Ollama maintient les deux. L'API native (/api/chat) expose des paramètres spécifiques (num_ctx, num_predict, mirostat) mais elle est moins portable. L'API OpenAI-compatible couvre 95 % des besoins et reste utilisable avec n'importe quel autre fournisseur. Par défaut, partez sur celle-ci.

#Prérequis

Ollama installé et démarré
Le daemon doit écouter sur http://localhost:11434. Vérifiez avec curl http://localhost:11434 — vous devez voir "Ollama is running".
Python 3.10+
Les SDK récents (openai 1.x) demandent au moins Python 3.8, mais 3.10+ pour les annotations modernes.
Un modèle compatible chat
ollama pull llama3.1:8b ou qwen3:14b. Pour le function calling, choisissez un modèle qui le supporte : Llama 3.1, Qwen 3, Mistral Instruct v0.3, Granite 3.
VRAM suffisante
Un 7B Q4 demande environ 5 Go de VRAM, un 14B environ 9 Go. Sans GPU, ça tourne aussi mais à 5-10 tok/s.

#1. Les deux APIs côté Ollama

Avant d'écrire du Python, jetons un œil aux endpoints depuis le terminal pour bien voir ce qui se passe. Avec curl, on parle directement au daemon, sans aucune abstraction.

API native — /api/chat
curl http://localhost:11434/api/chat -d '{
  "model": "llama3.1:8b",
  "messages": [{"role": "user", "content": "Bonjour"}],
  "stream": false
}'
API OpenAI — /v1/chat/completions
curl http://localhost:11434/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{
    "model": "llama3.1:8b",
    "messages": [{"role": "user", "content": "Bonjour"}]
  }'

Le second renvoie un payload strictement identique à celui d'OpenAI : champs choices[0].message.content, id, model, usage. C'est ce qui rend le drop-in possible.

La clé API est ignorée mais obligatoire
Le SDK openai exige un paramètre api_key. Ollama ne vérifie rien — passez "ollama" ou n'importe quelle chaîne non-vide. Si vous mettez votre vraie clé OpenAI par habitude, elle reste sur votre machine, mais préférez une chaîne neutre pour éviter les confusions.

#2. Le SDK OpenAI pointé sur Ollama

Le pattern de base d'une intégration Ollama API Python tient en cinq lignes : on installe le SDK OpenAI, on l'instancie avec la base_url locale, et on appelle chat.completions.create comme d'habitude.

Installation
pip install openai
client.py — appel de base
from openai import OpenAI

client = OpenAI(
    base_url="http://localhost:11434/v1",
    api_key="ollama",  # ignoré, mais requis par le SDK
)

reponse = client.chat.completions.create(
    model="llama3.1:8b",
    messages=[
        {"role": "system", "content": "Tu réponds en français, de façon concise."},
        {"role": "user", "content": "Explique en une phrase ce qu'est un LLM."},
    ],
    temperature=0.3,
)

print(reponse.choices[0].message.content)

Lancez le script. Si Ollama tourne et que le modèle est téléchargé, vous obtenez une phrase. Si vous voyez une ConnectionRefusedError, vérifiez avec ollama ps que le daemon est bien actif.

model
Le nom exact tel que listé par ollama list (llama3.1:8b, qwen3:14b, mistral:7b-instruct, etc.).
messages
La liste des tours de conversation. Rôles supportés : system, user, assistant, tool.
temperature
0 pour déterministe, 0.7 pour créatif. Pour de l'extraction de données, restez à 0 ou 0.1.
max_tokens
Limite supérieure sur la réponse. Optionnel — Ollama applique un défaut de num_predict raisonnable.

#3. Streaming token par token avec SSE

Pour une UX correcte (chatbot, génération longue), vous voulez afficher les tokens au fur et à mesure plutôt qu'attendre la fin. Ollama supporte le streaming via Server-Sent Events, et le SDK OpenAI en fait une boucle Python triviale.

streaming.py
from openai import OpenAI

client = OpenAI(base_url="http://localhost:11434/v1", api_key="ollama")

flux = client.chat.completions.create(
    model="llama3.1:8b",
    messages=[{"role": "user", "content": "Raconte une courte histoire de robot."}],
    stream=True,
)

for chunk in flux:
    delta = chunk.choices[0].delta.content
    if delta:
        print(delta, end="", flush=True)
print()

Chaque chunk contient un delta (le morceau de texte ajouté). Le dernier chunk a delta.content à None et finish_reason rempli — c'est le signal d'arrêt.

!
Ne pas oublier flush=True
Sans flush=True, Python bufferise stdout par ligne et l'effet streaming disparaît dans le terminal. Pour une API HTTP en revanche, c'est le serveur web (uvicorn, gunicorn) qui flush — vous n'avez pas à vous en occuper.

#4. JSON mode pour des sorties structurées

Quand vous voulez parser la réponse (extraction, classification, génération de payload), demander "renvoie du JSON" dans le prompt ne suffit pas — le modèle insère souvent du texte autour. Le JSON mode force le décodeur à ne produire que du JSON valide.

json_mode.py
from openai import OpenAI
import json

client = OpenAI(base_url="http://localhost:11434/v1", api_key="ollama")

reponse = client.chat.completions.create(
    model="llama3.1:8b",
    messages=[
        {"role": "system", "content": (
            "Tu extrais des informations structurées. "
            "Réponds uniquement avec un objet JSON contenant les clés : "
            "nom (string), age (int), ville (string)."
        )},
        {"role": "user", "content": "Marie a 34 ans, elle habite à Lyon."},
    ],
    response_format={"type": "json_object"},
    temperature=0,
)

donnees = json.loads(reponse.choices[0].message.content)
print(donnees)
# {'nom': 'Marie', 'age': 34, 'ville': 'Lyon'}

response_format={"type": "json_object"} active le mode JSON. Côté Ollama, ça se traduit par une contrainte au niveau du sampler : tout token qui produirait un JSON invalide est rejeté. C'est plus fiable que de prompter "réponds en JSON" et de prier.

Mentionnez "JSON" dans le prompt
Comme chez OpenAI, le mode JSON impose au moins une mention du mot "JSON" dans la conversation (system ou user). Sans ça, certains modèles produisent un objet vide. Décrivez le schéma attendu dans le system prompt — c'est ce qui guide le contenu, le mode JSON ne fait que garantir la syntaxe.

#5. Function calling (tool use)

Le function calling permet au modèle de signaler qu'il veut appeler une fonction Python plutôt que de répondre directement. Tous les modèles ne le supportent pas — vérifiez sur ollama.com/library que la mention "tools" apparaît dans les capacités. Llama 3.1, Qwen 3, Mistral Instruct v0.3 et Granite 3 le supportent nativement.

tools.py
from openai import OpenAI
import json

client = OpenAI(base_url="http://localhost:11434/v1", api_key="ollama")

# 1. Une fonction Python réelle
def meteo(ville: str) -> dict:
    # En vrai, vous appelleriez Open-Meteo ou autre
    return {"ville": ville, "temperature_c": 18, "conditions": "nuageux"}

# 2. Sa description au format OpenAI
outils = [{
    "type": "function",
    "function": {
        "name": "meteo",
        "description": "Donne la météo actuelle d'une ville française.",
        "parameters": {
            "type": "object",
            "properties": {
                "ville": {"type": "string", "description": "Nom de la ville"},
            },
            "required": ["ville"],
        },
    },
}]

messages = [{"role": "user", "content": "Quel temps fait-il à Bordeaux ?"}]

# 3. Premier appel : le modèle décide d'appeler la fonction
reponse = client.chat.completions.create(
    model="llama3.1:8b",
    messages=messages,
    tools=outils,
)

appel = reponse.choices[0].message.tool_calls[0]
args = json.loads(appel.function.arguments)
resultat = meteo(**args)

# 4. Second appel : on renvoie le résultat au modèle pour la réponse finale
messages.append(reponse.choices[0].message)
messages.append({
    "role": "tool",
    "tool_call_id": appel.id,
    "content": json.dumps(resultat),
})

finale = client.chat.completions.create(model="llama3.1:8b", messages=messages)
print(finale.choices[0].message.content)

La boucle a deux tours : le premier renvoie un tool_calls (le modèle dit "appelle meteo avec ville=Bordeaux"), le second renvoie la réponse naturelle après que vous avez exécuté la fonction et injecté son résultat. En production, vous bouclez tant que tool_calls est rempli.

!
Tous les modèles ne sont pas égaux
Avec un modèle qui supporte mal les tools (vieux Llama 2, Mistral 7B v0.1), vous obtiendrez des appels mal formés ou des arguments hallucinés. Si ça vous arrive : (1) confirmez que le modèle supporte officiellement les tools, (2) baissez la température à 0, (3) simplifiez le schéma des paramètres.

#6. Exposer Ollama via FastAPI

Cas typique : votre frontend appelle votre backend Python, qui appelle Ollama. FastAPI gère l'asynchrone proprement et le streaming arrive jusqu'au navigateur via un StreamingResponse.

Dépendances
pip install fastapi uvicorn openai
main.py
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
from pydantic import BaseModel
from openai import OpenAI

app = FastAPI()
client = OpenAI(base_url="http://localhost:11434/v1", api_key="ollama")

class Question(BaseModel):
    message: str
    model: str = "llama3.1:8b"

@app.post("/chat")
def chat(q: Question):
    reponse = client.chat.completions.create(
        model=q.model,
        messages=[{"role": "user", "content": q.message}],
    )
    return {"reponse": reponse.choices[0].message.content}

@app.post("/chat/stream")
def chat_stream(q: Question):
    def generateur():
        flux = client.chat.completions.create(
            model=q.model,
            messages=[{"role": "user", "content": q.message}],
            stream=True,
        )
        for chunk in flux:
            delta = chunk.choices[0].delta.content
            if delta:
                yield delta
    return StreamingResponse(generateur(), media_type="text/plain")
Lancer le serveur
uvicorn main:app --reload --port 8000
Tester en CLI
curl -N -X POST http://localhost:8000/chat/stream \
  -H "Content-Type: application/json" \
  -d '{"message": "Écris un haïku sur Paris."}'

L'option -N (--no-buffer) de curl désactive le buffering côté client pour voir le streaming en direct. Côté frontend JS, vous lisez le ReadableStream de la réponse fetch — pareil que pour l'API OpenAI.

#7. Chatbot Flask avec historique

Pour un chatbot complet, il faut garder l'historique des messages entre les tours. Voici une version Flask minimaliste qui stocke la conversation en mémoire (à remplacer par une vraie session/DB en production).

Dépendances
pip install flask openai
app.py
from flask import Flask, request, jsonify, Response
from openai import OpenAI
from collections import defaultdict

app = Flask(__name__)
client = OpenAI(base_url="http://localhost:11434/v1", api_key="ollama")

# Historiques par session — en prod : Redis, Postgres, etc.
historiques: dict[str, list] = defaultdict(lambda: [
    {"role": "system", "content": "Tu es un assistant en français, concis et utile."},
])

@app.post("/chat/<session_id>")
def chat(session_id: str):
    message = request.json["message"]
    historique = historiques[session_id]
    historique.append({"role": "user", "content": message})

    reponse = client.chat.completions.create(
        model="llama3.1:8b",
        messages=historique,
    )
    contenu = reponse.choices[0].message.content
    historique.append({"role": "assistant", "content": contenu})
    return jsonify({"reponse": contenu})

@app.post("/chat/<session_id>/stream")
def chat_stream(session_id: str):
    message = request.json["message"]
    historique = historiques[session_id]
    historique.append({"role": "user", "content": message})

    def generateur():
        morceaux = []
        flux = client.chat.completions.create(
            model="llama3.1:8b",
            messages=historique,
            stream=True,
        )
        for chunk in flux:
            delta = chunk.choices[0].delta.content
            if delta:
                morceaux.append(delta)
                yield delta
        historique.append({"role": "assistant", "content": "".join(morceaux)})

    return Response(generateur(), mimetype="text/plain")

@app.delete("/chat/<session_id>")
def reset(session_id: str):
    historiques.pop(session_id, None)
    return "", 204

if __name__ == "__main__":
    app.run(port=5000, debug=True)
i
Limite de contexte
Plus l'historique grossit, plus vous consommez de tokens à chaque appel. Pour Llama 3.1, la fenêtre par défaut côté Ollama est de 2048 tokens — au-delà, les anciens messages sont silencieusement coupés. Augmentez via l'API native ou en surchargeant avec un Modelfile (num_ctx 8192 ou 32768).

#Pour la production

Exposer Ollama sur le réseau
Par défaut, le daemon n'écoute que sur 127.0.0.1. Pour autoriser d'autres machines, lancez avec OLLAMA_HOST=0.0.0.0 — et mettez un reverse proxy avec auth devant, sinon n'importe qui sur le réseau peut tirer sur vos modèles.
Concurrence et file d'attente
Ollama sérialise les requêtes par modèle. Pour servir plusieurs utilisateurs en parallèle, lancez plusieurs instances ou passez à vLLM qui gère le batching dynamique nativement.
Timeouts côté client
Une requête sur un modèle non-chargé peut prendre 10-30 s (chargement en VRAM). Réglez le timeout du client OpenAI : OpenAI(..., timeout=120) plutôt que le défaut de 10 minutes côté lib mais souvent court côté reverse proxy.
Garder le modèle chaud
Par défaut, Ollama décharge un modèle après 5 minutes d'inactivité. En API, passez keep_alive="30m" via l'API native /api/chat, ou laissez un ping périodique pour éviter le froid sur la première requête utilisateur.
Observabilité
Logguez systématiquement model, prompt_tokens, completion_tokens (présent dans reponse.usage). Ce sont vos métriques d'inférence — utiles pour spotter un modèle qui ralentit ou un prompt qui explose.
Migrer depuis l'API OpenAI
Si vous avez déjà du code qui parle à api.openai.com, la bascule vers Ollama tient en deux lignes : changez base_url="https://api.openai.com/v1" en base_url="http://localhost:11434/v1" et adaptez le nom du modèle. Tout le reste — streaming, JSON mode, tools — fonctionne pareil. C'est le grand intérêt de l'endpoint OpenAI-compatible.

#Pour aller plus loin

Vous avez les briques de base. Trois directions pour aller plus loin selon votre cas d'usage :

Construire un agent qui décide tout seul
Le guide sur les agents IA locaux en Python avec LangChain pousse le function calling jusqu'à la boucle d'agent complète, avec gestion d'outils multiples et raisonnement multi-étapes.
Ajouter du RAG sur vos documents
Pour que votre app réponde à partir d'un corpus interne (PDF, notes, code), branchez une base vectorielle. Le guide d'introduction au RAG local pose les fondations.
Personnaliser le comportement du modèle
Plutôt que de répéter le system prompt à chaque appel, créez une variante via Modelfile. Le guide personnalisation Ollama Modelfile montre comment figer un assistant FR ou un mode coder dans un nom de modèle réutilisable.
Ce guide vous a aidé ?

Un retour, une erreur, une précision ? Faites-nous signe, ça améliore le guide pour tout le monde.