Ejemplo de llamadas a funciones de la API de Ollama con el SDK de OpenAI
Las llamadas a funciones permiten que el modelo solicite a tu código ejecutar una función y luego usar el resultado en su respuesta. A continuación hay un ejemplo completo con el SDK de OpenAI contra un endpoint alojado, además de streaming y los errores que causan la mayoría de los fallos.
Actualizado

Puntos clave
- Envía tu función como un esquema JSON en tools; el modelo responde con tool_calls en lugar de texto.
- Ejecutas la función, añades el resultado como un mensaje con role tool y vuelves a llamar a la API.
- Analiza siempre los argumentos de forma defensiva, porque un modelo puede devolver JSON incorrecto o inesperado.
- Las llamadas a funciones en streaming llegan en fragmentos que debes unir por índice.
Cómo se ve el bucle
Si buscas un ejemplo de llamadas a funciones de la API de Ollama que funcione contra un endpoint alojado, la estructura es la estándar de OpenAI. Describes tus funciones en un array tools. El modelo o bien responde en texto o devuelve una lista tool_calls que nombra la función y sus argumentos. El modelo nunca ejecuta nada. Tu código ejecuta la función, envía la salida de vuelta y el modelo escribe la respuesta final.
Esto hace que un ciclo de uso de funciones requiera dos llamadas a la API:
- Envía el mensaje del usuario y las definiciones de herramientas.
- Recibe un mensaje del asistente con
tool_calls. Ejecuta cada llamada localmente. - Envía los mensajes originales, el mensaje del asistente y un mensaje
role: "tool"por llamada con su resultado. - Recibe la respuesta de texto final.
Los ejemplos usan la base URL https://api.ollamaapi.com/v1 y el ID de modelo uncensored. El endpoint es un servicio alojado, así que la clave proviene de la página de clave de API. El mismo código también funciona contra un servidor local de Ollama si cambias la base URL, la clave y el nombre del modelo, siempre que el modelo local admita herramientas.
Un ejemplo completo en Python
El ejemplo proporciona al modelo una función, get_order_status, respaldada por un pequeño diccionario. Una aplicación real consultaría una base de datos o un servicio interno en ese momento.
import json
from openai import OpenAI
client = OpenAI(base_url="https://api.ollamaapi.com/v1", api_key="YOUR_KEY")
ORDERS = {"A1001": "shipped", "A1002": "processing", "A1003": "delivered"}
def get_order_status(order_id: str) -> str:
return ORDERS.get(order_id, "not found")
tools = [{
"type": "function",
"function": {
"name": "get_order_status",
"description": "Look up the status of an order by its ID.",
"parameters": {
"type": "object",
"properties": {
"order_id": {"type": "string", "description": "Order ID such as A1001"}
},
"required": ["order_id"],
},
},
}]
messages = [{"role": "user", "content": "Where is order A1002?"}]
first = client.chat.completions.create(
model="uncensored",
messages=messages,
tools=tools,
tool_choice="auto",
)
msg = first.choices[0].message
if msg.tool_calls:
messages.append(msg) # keep the assistant turn with its tool_calls
for call in msg.tool_calls:
args = json.loads(call.function.arguments)
result = get_order_status(**args)
messages.append({
"role": "tool",
"tool_call_id": call.id,
"content": result,
})
final = client.chat.completions.create(
model="uncensored",
messages=messages,
tools=tools,
)
print(final.choices[0].message.content)
else:
print(msg.content)Aquí importan tres detalles. El mensaje del asistente que contiene tool_calls debe volver al historial antes de los resultados de las herramientas, o la siguiente petición se rechazará o confundirá. Cada mensaje de herramienta debe incluir el tool_call_id de la llamada que responde. Y la segunda petición debe seguir incluyendo tools, porque el modelo podría querer llamar a otra función tras ver el primer resultado.
La misma petición con curl
Probar con curl saca al SDK de la ecuación, lo que ayuda cuando no estás seguro de si un problema está en tu código o en la petición.
curl https://api.ollamaapi.com/v1/chat/completions \
-H "Authorization: Bearer YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "uncensored",
"messages": [{"role": "user", "content": "Where is order A1002?"}],
"tools": [{
"type": "function",
"function": {
"name": "get_order_status",
"description": "Look up the status of an order by its ID.",
"parameters": {
"type": "object",
"properties": {"order_id": {"type": "string"}},
"required": ["order_id"]
}
}
}],
"tool_choice": "auto"
}'Una primera respuesta exitosa tiene finish_reason establecido en tool_calls, un content vacío o nulo y un array tool_calls. El campo arguments es una cadena que contiene JSON, no un objeto anidado, por eso el ejemplo de Python llama a json.loads.
El campo tool_choice controla qué tan forzada es la petición. auto permite que el modelo decida. Pasar un objeto que nombra una función pide esa función específica. Usa auto a menos que tengas una razón, porque forzar una llamada en un mensaje que no la necesita produce argumentos inventados.
El mismo bucle en Node
La versión de Node usa el paquete oficial openai y sigue los mismos pasos. Esta maneja más de una llamada por turno con Promise.all.
import OpenAI from "openai";
const client = new OpenAI({
baseURL: "https://api.ollamaapi.com/v1",
apiKey: process.env.API_KEY,
});
const handlers = {
get_order_status: async ({ order_id }) =>
({ A1001: "shipped", A1002: "processing" })[order_id] ?? "not found",
};
const tools = [{
type: "function",
function: {
name: "get_order_status",
description: "Look up the status of an order by its ID.",
parameters: {
type: "object",
properties: { order_id: { type: "string" } },
required: ["order_id"],
},
},
}];
const messages = [{ role: "user", content: "Where is order A1001?" }];
const first = await client.chat.completions.create({
model: "uncensored", messages, tools,
});
const msg = first.choices[0].message;
if (msg.tool_calls?.length) {
messages.push(msg);
const results = await Promise.all(msg.tool_calls.map(async (call) => {
const args = JSON.parse(call.function.arguments);
const out = await handlers[call.function.name](args);
return { role: "tool", tool_call_id: call.id, content: String(out) };
}));
messages.push(...results);
const final = await client.chat.completions.create({
model: "uncensored", messages, tools,
});
console.log(final.choices[0].message.content);
} else {
console.log(msg.content);
}Llamadas a funciones en streaming
Con stream: true la respuesta llega por SSE, y las llamadas a funciones llegan por fragmentos. Cada fragmento contiene una entrada delta.tool_calls con un index. El primer fragmento para un índice contiene el ID de la llamada y el nombre de la función. Los fragmentos posteriores añaden trozos de la cadena arguments. Concatenas los trozos por índice y solo analizas el JSON cuando termina el streaming.
stream = client.chat.completions.create(
model="uncensored",
messages=[{"role": "user", "content": "Where is order A1003?"}],
tools=tools,
stream=True,
)
calls = {}
for chunk in stream:
if not chunk.choices:
continue
delta = chunk.choices[0].delta
for part in (delta.tool_calls or []):
slot = calls.setdefault(part.index, {"id": "", "name": "", "args": ""})
if part.id:
slot["id"] = part.id
if part.function and part.function.name:
slot["name"] = part.function.name
if part.function and part.function.arguments:
slot["args"] += part.function.arguments
for slot in calls.values():
print(slot["name"], json.loads(slot["args"]))Recuerda que una petición en streaming mantiene su slot de concurrencia hasta que finalice el streaming. Una clave completa permite 8 peticiones a la vez y 300 por minuto; una clave de prueba, 2 y 60. Si ejecutas bucles de llamadas a funciones para muchos usuarios en paralelo, cuenta cada streaming abierto contra el primer número.
Errores comunes y cómo solucionarlos
Argumentos malformados o faltantes
El modelo es un modelo de pesos abiertos, y los modelos abiertos a veces devuelven argumentos que no son JSON válido, omiten un campo obligatorio o inventan uno extra. Envuelve json.loads en un bloque try, valida contra tu esquema y, cuando falle, envía el error como un mensaje de herramienta. El modelo suele corregirse en el siguiente turno. Limita el bucle a unas pocas iteraciones para que un fallo obstinado no se ejecute para siempre.
Descripciones demasiado vagas
El modelo elige una función leyendo su nombre y descripción. Una descripción como "obtiene datos" no le da nada con qué trabajar. Di qué hace la función, qué significan las entradas e incluye un valor de ejemplo en la descripción del parámetro.
Demasiadas herramientas a la vez
Cada definición de herramienta cuenta como tokens de entrada en cada petición. A $0.15 por 1M de tokens de entrada, diez herramientas de 150 tokens cada una suman 1,500 tokens, lo que es $0.000225 por petición, poco pero no cero a volumen. Más importante aún, un menú largo hace que la selección sea menos fiable. Ofrece solo las herramientas relevantes para el paso actual.
Resultados de herramientas demasiado grandes
Lo que devuelvas se alimenta de vuelta como entrada. Recorta filas grandes de base de datos o páginas web a los campos que necesita el modelo. La ventana de contexto es de 100,000 tokens en total, así que un resultado muy grande puede desplazar la conversación.
Mezclar herramientas con modo JSON
El modo JSON (response_format con json_object) está disponible, pero úsalo para la respuesta final, no para turnos donde quieras que el modelo llame a una función. Prueba la combinación en tus propios prompts antes de confiar en ella.
Pasando de Ollama local y qué verificar
Si construiste tu primer bucle de herramientas contra un servidor local de Ollama, casi nada cambia. Reemplaza la base URL con https://api.ollamaapi.com/v1, usa una clave real y establece el modelo en uncensored. Las rutas nativas de Ollama como /api/chat no existen aquí. Solo POST /v1/chat/completions y GET /v1/models están disponibles, así que el código escrito para la API nativa necesita la llamada al estilo OpenAI en su lugar.
Antes de lanzar, ejecuta esta breve lista de verificación:
- Confirma que la clave funciona con
GET /v1/models. - Registra el encabezado
X-Request-Idpara cada llamada, para que una ejecución fallida pueda rastrearse. - Observa
X-Balance-USDen los encabezados de respuesta si ejecutas bucles de herramientas sin supervisión. - Maneja HTTP 429: espera en
rate_limitedoconcurrencyy honraRetry-After: 5en sobrecarga. - Establece un límite estricto en las iteraciones del bucle.
Un bucle de herramientas realiza al menos dos peticiones por pregunta del usuario, por lo que los costes escalan con eso. Por ejemplo, 2,000 bucles con 1,200 tokens de entrada y 150 tokens de salida por petición, dos peticiones cada uno, equivalen a 4,000 peticiones, 4.8M de tokens de entrada y 0.6M de tokens de salida. Eso es $0.72 más $0.30, o $1.02 en total. La página de precios tiene las opciones de recarga, y la documentación enumera los campos de petición admitidos. Los nuevos usuarios pueden probar todo esto con el crédito de prueba de $0.50, que no requiere tarjeta.
Más guías
Preguntas y respuestas
¿El endpoint admite llamadas a funciones?
Sí. Acepta herramientas y tool_choice, devuelve tool_calls, los transmite como deltas y recibe los resultados como mensajes con role tool.
¿Por qué falla la segunda petición después de una llamada a función?
La causa habitual es un mensaje del asistente ausente o un mensaje de herramienta sin el tool_call_id correspondiente. Añade primero el turno del asistente que contiene tool_calls, y luego un mensaje de herramienta por cada llamada.
¿Puede el modelo llamar a varias herramientas en un solo turno?
Puede devolver una lista con más de un elemento, así que recorre todos y envía un resultado por cada uno. No asumas que la lista tenga un solo elemento.
¿Necesito la biblioteca de Ollama para esto?
No. Los SDK oficiales de OpenAI son suficientes. Apunta la base URL al endpoint y usa el modelo uncensored.
Tu clave está a un formulario de distancia
Crea una cuenta, copia la clave, cambia la URL base. Esa es toda la configuración.