Intermedio 15 minAPI

Integrar Ollama en una aplicación Python a través de la API REST

Ollama expone dos APIs HTTP en el puerto 11434: una API nativa (/api/generate, /api/chat) y una API compatible con OpenAI (/v1/chat/completions). Esta última es la vía ideal para integrar la API de Ollama en Python: tu código utiliza exactamente el mismo SDK que con GPT-4, pero se ejecuta en tu máquina. Esta guía cubre patrones concretos —streaming, JSON estructurado, llamadas a funciones— con ejemplos de FastAPI y Flask listos para pegar en un proyecto.

Por Mohamed Meguedmi·Actualización 2026-08-27·Probado en Windows, macOS y Linux

#¿Por qué usar la API REST?

La CLI ollama run est es práctica para probar, pero no está diseñada para ser llamada desde una aplicación. La API REST, en cambio, sí está pensada para eso: peticiones HTTP estándar, entrada y salida en JSON, streaming mediante Server-Sent Events. Esto es lo que todas las interfaces (Open WebUI, Cline, LangChain) usan en el fondo.

Compatibilidad con OpenAI
El endpoint /v1/chat/completions acepta exactamente el mismo payload que api.openai.com/v1/chat/completions. Cambias la URL y la clave, y tu código existente funciona.
Sin reinventar
El SDK oficial de OpenAI en Python (o cualquier cliente HTTP) habla directamente con Ollama. No se necesita aprender ningún cliente específico.
Desacoplamiento del runtime
Tu aplicación Python corre dentro de su contenedor, Ollama dentro del suyo. El día en que pasas a vLLM o LM Studio, solo cambias la base_url.
Múltiples clientes simultáneos
Varios scripts en Python, un notebook Jupyter y Open WebUI pueden interactuar con la misma instancia Ollama. El demonio gestiona la cola por sí solo.
i
API nativa vs compatible con OpenAI
Ollama mantiene ambas API. La API nativa (/api/chat) expone parámetros específicos (num_ctx, num_predict, mirostat), pero es menos portátil. La API compatible con OpenAI cubre el 95 % de las necesidades y puede seguir utilizándose con cualquier otro proveedor. Por defecto, opta por esta última.

#Prerrequisitos

El kit Copiloto Local

Esta guía te lleva al modelo. El kit te lleva al copiloto que programa en tu editor.

  • Espacio en línea de por vida
  • PDF + archivos
  • Actualizaciones de por vida
Ollama instalado y arrancado
El daemon debe escuchar en http://localhost:11434. Verifica con curl http://localhost:11434 — debes ver "Ollama is running".
Python 3.10+
Los SDK recientes (openai 1.x) requieren al menos Python 3.8, pero 3.10+ para las anotaciones modernas.
Un modelo compatible con chat
ollama pull qwen3.5:9b o gemma4:12b. Para el function calling, elige un modelo que lo admita: Qwen 3.5, Granite 4.2, Mistral Small 24B, Devstral.
VRAM suficiente
Un modelo 9B Q4 (como Qwen 3.5 9B) requiere aproximadamente 6-7 GB de VRAM, un 12B (Gemma 4 12B) alrededor de 8 GB. Sin GPU, también funciona, pero a 5-10 tok/s.

#1. Las dos API de Ollama

Antes de escribir Python, echemos un vistazo a los endpoints desde el terminal para ver bien qué sucede. Con curl, hablamos directamente con el daemon, sin ninguna abstracción.

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

El segundo devuelve un payload estrictamente idéntico al de OpenAI: los campos choices[0].message.content, id, model, usage. Esto es lo que permite sustituir uno por otro directamente.

→
La clave API se ignora pero es obligatoria
El SDK openai exige un parámetro api_key. Ollama no comprueba nada: pasa "ollama" o cualquier cadena no vacía. Si introduces tu clave real de OpenAI por costumbre, permanece en tu equipo, pero es preferible usar una cadena neutra para evitar confusiones.

#2. El SDK de OpenAI configurado para conectarse a Ollama

El patrón básico para integrar la API de Ollama en Python cabe en cinco líneas: se instala el SDK de OpenAI, se instancia con la base_url local y se llama a chat.completions.create como de costumbre.

Instalación
pip install openai
client.py — llamada básica
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="qwen3.5:9b",
    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)

Ejecuta el script. Si Ollama está en ejecución y el modelo ya está descargado, obtienes una frase. Si ves una ConnectionRefusedError, comprueba con ollama ps que el daemon esté activo.

model
El nombre exacto tal como lo lista ollama list (qwen3.5:9b, gemma4:12b, mistral-small, etc.).
messages
La lista de turnos de conversación. Roles soportados: system, user, assistant, tool.
temperature
0 para respuestas deterministas, 0.7 para respuestas creativas. Para la extracción de datos, mantente en 0 o 0.1.
max_tokens
Límite superior para la respuesta. Opcional: Ollama aplica un valor predeterminado razonable de num_predict.

#3. Transmisión token por token con SSE

Para una experiencia de usuario adecuada (chatbot, generación larga), querrás mostrar los tokens a medida que se generan en lugar de esperar hasta el final. Ollama admite el streaming mediante Server-Sent Events, y el SDK de OpenAI permite hacerlo con un sencillo bucle de Python.

streaming.py
from openai import OpenAI

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

flux = client.chat.completions.create(
    model="qwen3.5:9b",
    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()

Cada chunk contiene un delta (el fragmento de texto añadido). En el último chunk, delta.content tiene el valor None y finish_reason contiene un valor: esa es la señal de finalización.

!
No olvidar flush=True
Sin flush=True, Python almacena stdout en un búfer por líneas y el efecto de streaming desaparece en el terminal. Para una API HTTP, en cambio, es el servidor web (uvicorn, gunicorn) el que realiza el flush; no tienes que preocuparte de ello.

#4. Modo JSON para salidas estructuradas

Cuando quieres analizar la respuesta (extracción, clasificación, generación de payload), pedir "devuelve JSON" en el prompt no basta — el modelo a menudo inserta texto adicional. El modo JSON fuerza al decodificador a producir solo JSON válido.

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="qwen3.5:9b",
    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"} activa el modo JSON. En Ollama, esto se traduce en una restricción a nivel del sampler: se rechaza cualquier token que produciría un JSON inválido. Es más fiable que pedir mediante un prompt "responde en JSON" y rezar.

→
Menciona "JSON" en el prompt
Como en OpenAI, el modo JSON exige al menos una mención de la palabra "JSON" en la conversación (system o user). Sin ella, algunos modelos producen un objeto vacío. Describe el esquema esperado en el prompt de sistema: eso es lo que guía el contenido; el modo JSON solo garantiza la sintaxis.

#5. Llamada a funciones (uso de herramientas)

La llamada de funciones permite al modelo indicar que quiere llamar a una función en Python en lugar de responder directamente. No todos los modelos lo soportan —revísalo en ollama.com/library y asegúrate de que aparezca la mención "tools" en las capacidades. Qwen 3.5, Granite 4.2, Mistral Small 24B y Devstral lo soportan nativamente.

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="qwen3.5:9b",
    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="qwen3.5:9b", messages=messages)
print(finale.choices[0].message.content)

El bucle tiene dos turnos: el primero devuelve un tool_calls (el modelo dice "llama a meteo con ville=Bordeaux"), el segundo devuelve la respuesta en lenguaje natural después de que hayas ejecutado la función e inyectado su resultado. En producción, repites el bucle mientras tool_calls no esté vacío.

!
No todos los modelos son iguales
Con un modelo que maneja mal las herramientas (antiguos Llama 2, Mistral 7B v0.1), obtendrás llamadas mal formadas o argumentos alucinados. Si ocurre: (1) verifica que el modelo soporte oficialmente las herramientas, (2) reduce la temperatura a 0, (3) simplifica el esquema de parámetros.

#6. Exponer Ollama a través de FastAPI

Caso típico: tu frontend llama a tu backend en Python, que llama a Ollama. FastAPI gestiona la asincronía correctamente y el streaming llega hasta el navegador a través de un StreamingResponse.

Dependencias
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 = "qwen3.5:9b"

@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")
Iniciar el servidor
uvicorn main:app --reload --port 8000
Probar en CLI
curl -N -X POST http://localhost:8000/chat/stream \
  -H "Content-Type: application/json" \
  -d '{"message": "Écris un haïku sur Paris."}'

La opción -N (--no-buffer) de curl desactiva el buffering del lado del cliente para ver el streaming en directo. Del lado del frontend JS, lees el ReadableStream de la respuesta fetch — igual que en la API OpenAI.

#7. Chatbot Flask con historial

Para un chatbot completo, hay que mantener el historial de mensajes entre turnos. Aquí tienes una versión mínima de Flask que almacena la conversación en memoria (debe reemplazarse por una verdadera sesión/BD en producción).

Dependencias
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="qwen3.5:9b",
        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="qwen3.5:9b",
            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
Límite de contexto
Cuanto más largo sea el historial, más tokens consumes en cada llamada. Para Qwen 3.5, la ventana predeterminada en Ollama es de 2048 tokens; al superar ese límite, los mensajes antiguos se recortan sin aviso. Amplíala mediante la API nativa o sobrescribiendo el valor con un Modelfile (num_ctx 8192 o 32768).

#Para la producción

Exponer Ollama en red
Por defecto, el daemon solo escucha en 127.0.0.1. Para permitir otras máquinas, inicia con OLLAMA_HOST=0.0.0.0 — y coloca un reverse proxy con autenticación delante, de lo contrario, cualquier persona en la red puede acceder a tus modelos.
Concurrencia y cola
Ollama serializa las solicitudes por modelo. Para atender a varios usuarios simultáneamente, arranca varias instancias o pasa a vLLM que gestiona el batching dinámico nativamente.
Tiempos de espera del lado del cliente
Una solicitud a un modelo no cargado puede tardar 10-30 s (carga en VRAM). Ajusta el tiempo de espera del cliente OpenAI: OpenAI(..., timeout=120), en lugar de dejar el valor predeterminado de 10 minutos de la biblioteca; en el proxy inverso, el tiempo de espera suele ser corto.
Mantener el modelo cargado
Por defecto, Ollama libera de la memoria un modelo después de 5 minutos de inactividad. Al usar la API, pasa keep_alive="30m" mediante la API nativa /api/chat, o mantén un ping periódico para evitar un arranque en frío en la primera solicitud del usuario.
Observabilidad
Registra sistemáticamente model, prompt_tokens y completion_tokens (presentes en reponse.usage). Son tus métricas de inferencia: sirven para detectar un modelo que se ralentiza o un prompt cuyo tamaño se dispara.
→
Migrar desde la API de OpenAI
Si ya tienes código que habla con api.openai.com, la transición hacia Ollama se reduce a dos líneas: cambia base_url="https://api.openai.com/v1" por base_url="http://localhost:11434/v1" y ajusta el nombre del modelo. Todo el resto — streaming, modo JSON, herramientas — funciona igual. Es el gran beneficio del endpoint compatible con OpenAI.

#Para ir más allá

Tienes los elementos básicos. Tres vías para profundizar según tu caso de uso:

Crear un agente que decida por sí solo
La guía sobre agentes de IA locales en Python con LangChain lleva las llamadas a funciones hasta el bucle completo de un agente, con gestión de múltiples herramientas y razonamiento en varios pasos.
Añadir RAG a tus documentos
Para que tu aplicación responda a partir de un corpus interno (PDF, notas, código), conecta una base vectorial. La guía de introducción al RAG local establece las bases.
Personalizar el comportamiento del modelo
En lugar de repetir el prompt del sistema en cada llamada, crea una variante mediante Modelfile. La guía de personalización con Ollama Modelfile muestra cómo dejar preconfigurado un asistente en francés o un modo de programación bajo un nombre de modelo reutilizable.
¿Esta guía te ha ayudado?

¿Un comentario, un error, una precisión? Avísanos, eso mejora la guía para todos.