Exemple d'appel d'outils API Ollama avec le SDK OpenAI
L'appel d'outils permet au modèle de demander à votre code d'exécuter une fonction, puis d'utiliser le résultat dans sa réponse. Voici un exemple complet avec le SDK OpenAI contre un endpoint hébergé, incluant le streaming et les erreurs courantes.
Mis à jour

Points clés
- Envoyez votre fonction sous forme de schéma JSON dans tools ; le modèle répond avec tool_calls au lieu de texte.
- Vous exécutez la fonction, ajoutez le résultat en tant que message de rôle tool, puis appelez à nouveau l'API.
- Analysez toujours les arguments de manière défensive, car un modèle peut renvoyer du JSON malformé ou inattendu.
- Les appels d'outils en streaming arrivent par fragments que vous devez assembler par index.
À quoi ressemble la boucle
Si vous cherchez un exemple d'appel d'outils API Ollama fonctionnel contre un endpoint hébergé, la structure est celle standard d'OpenAI. Vous décrivez vos fonctions dans un tableau tools. Le modèle répond soit en texte, soit renvoie une liste tool_calls nommant la fonction et ses arguments. Le modèle n'exécute rien. Votre code exécute la fonction, renvoie la sortie, et le modèle rédige la réponse finale.
Cela représente un aller-retour d'utilisation d'outil, soit deux appels API :
- Envoyez le message utilisateur et les définitions d'outils.
- Recevez un message assistant avec
tool_calls. Exécutez chaque appel localement. - Envoyez les messages originaux, le message assistant et un message
role: "tool"par appel avec son résultat. - Recevez la réponse textuelle finale.
Les exemples utilisent l'URL de base https://api.ollamaapi.com/v1 et l'ID de modèle uncensored. L'endpoint étant un service hébergé, la clé provient de la page Clé API. Le même code s'exécute également contre un serveur Ollama local si vous modifiez l'URL de base, la clé et le nom du modèle, tant que le modèle local prend en charge les outils.
Un exemple Python complet
L'exemple donne au modèle une seule fonction, get_order_status, alimentée par un petit dictionnaire. Une application réelle interrogerait une base de données ou un service interne à ce stade.
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)Trois détails sont importants ici. Le message assistant contenant tool_calls doit être réintégré dans l'historique avant les résultats d'outils, sinon la prochaine requête sera rejetée ou confuse. Chaque message outil doit porter le tool_call_id de l'appel auquel il répond. Et la deuxième requête doit toujours inclure tools, car le modèle pourrait vouloir appeler une autre fonction après avoir vu le premier résultat.
La même requête avec curl
Tester avec curl retire le SDK de l'équation, ce qui aide à déterminer si un problème vient de votre code ou de la requête.
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"
}'Une première réponse réussie a finish_reason défini sur tool_calls, un content vide ou null, et un tableau tool_calls. Le champ arguments est une chaîne contenant du JSON, et non un objet imbriqué, d'où l'appel à json.loads dans l'exemple Python.
Le champ tool_choice contrôle la force de la requête. auto laisse le modèle décider. Passer un objet nommant une fonction demande cette fonction spécifique. Utilisez auto sauf si vous avez une raison, car forcer un appel sur un message qui n'en a pas génère des arguments inventés.
La même boucle en Node
La version Node utilise le package officiel openai et suit les mêmes étapes. Celle-ci gère plusieurs appels par tour avec 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);
}Streaming des appels de fonctions
Avec stream: true, la réponse arrive via SSE, et les appels d'outils arrivent par morceaux. Chaque fragment contient une entrée delta.tool_calls avec un index. Le premier fragment pour un index contient l'ID d'appel et le nom de la fonction. Les fragments suivants ajoutent des tranches de la chaîne arguments. Concaténez les tranches par index, et n'analysez le JSON qu'à la fin du 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"]))N'oubliez pas qu'une requête en streaming conserve son slot de concurrence jusqu'à la fin du flux. Une clé complète permet 8 requêtes simultanées et 300 par minute, une clé d'essai 2 et 60. Si vous exécutez des boucles d'outils pour de nombreux utilisateurs en parallèle, comptez chaque flux ouvert pour le premier nombre.
Écueils et comment les corriger
Arguments malformés ou manquants
Le modèle est un modèle à poids ouverts, et les modèles ouverts retournent parfois des arguments qui ne sont pas du JSON valide, omettent un champ obligatoire ou en inventent un supplémentaire. Enveloppez json.loads dans un bloc try, validez contre votre schéma et, en cas d’échec, renvoyez l’erreur en tant que message d’outil. Le modèle se corrige généralement au tour suivant. Limitez la boucle à quelques itérations pour qu’un échec tenace ne puisse pas s’exécuter indéfiniment.
Descriptions trop vagues
Le modèle choisit une fonction en lisant son nom et sa description. Une description comme « récupère des données » ne lui laisse rien sur quoi s'appuyer. Indiquez ce que fait la fonction, ce que signifient les entrées, et incluez une valeur d'exemple dans la description du paramètre.
Trop d'outils à la fois
Chaque définition d'outil compte comme des tokens d'entrée sur chaque requête. À $0,15 par 1 M de tokens d'entrée, dix outils de 150 tokens chacun ajoutent 1 500 tokens, soit $0,000225 par requête, petit mais non nul à volume élevé. Plus important encore, un menu long rend la sélection moins fiable. Ne proposez que les outils pertinents pour l'étape actuelle.
Résultats d'outils trop volumineux
Tout ce que vous renvoyez est réinjecté en entrée. Réduisez les lignes de base de données ou les pages web aux champs dont le modèle a besoin. La fenêtre de contexte fait 100 000 tokens au total, donc un résultat très volumineux peut étouffer la conversation.
Mélanger les outils avec le mode JSON
Le mode JSON (response_format avec json_object) est disponible, mais utilisez-le pour la réponse finale, pas pour les tours où vous voulez que le modèle appelle une fonction. Testez la combinaison sur vos propres prompts avant de vous y fier.
Passer d'Ollama local et ce qu'il faut vérifier
Si vous avez construit votre première boucle d'outils contre un serveur Ollama local, presque rien ne change. Remplacez l'URL de base par https://api.ollamaapi.com/v1, utilisez une vraie clé, et définissez le modèle sur uncensored. Les routes natives Ollama comme /api/chat n'existent pas ici. Seuls POST /v1/chat/completions et GET /v1/models sont disponibles, donc le code écrit pour l'API native doit utiliser l'appel de style OpenAI.
Avant de déployer, exécutez cette courte liste de contrôle :
- Confirmez que la clé fonctionne avec
GET /v1/models. - Journalisez l'en-tête
X-Request-Idpour chaque appel, afin qu'une exécution défaillante puisse être tracée. - Surveillez
X-Balance-USDdans les en-têtes de réponse si vous exécutez des boucles d'outils sans surveillance. - Gérez le HTTP 429 : attendez sur
rate_limitedouconcurrency, et respectezRetry-After: 5en cas de surcharge. - Définissez un plafond strict sur les itérations de boucle.
Une boucle d'outils génère au moins deux requêtes par question utilisateur, donc les coûts augmentent en conséquence. Par exemple, 2 000 boucles avec 1 200 tokens d'entrée et 150 tokens de sortie par requête, deux requêtes chacune, représentent 4 000 requêtes, 4,8 M de tokens d'entrée et 0,6 M de tokens de sortie. Cela coûte 0,72 $ plus 0,30 $, soit 1,02 $ au total. La page de tarification propose des options de rechargement, et la documentation liste les champs de requête pris en charge. Les nouveaux utilisateurs peuvent tester tout cela avec le crédit d'essai de 0,50 $, sans carte bancaire nécessaire.
Plus de guides
Questions et réponses
L'endpoint prend-il en charge l'appel de fonctions ?
Oui. Il accepte les outils et tool_choice, retourne des tool_calls, les diffuse en streaming sous forme de deltas, et renvoie les résultats en tant que messages avec le rôle tool.
Pourquoi la deuxième requête échoue-t-elle après un appel d'outil ?
La cause habituelle est l’absence d’un message assistant ou d’un message d’outil sans tool_call_id correspondant. Ajoutez d’abord le tour assistant qui contient les tool_calls, puis un message outil par appel.
Le modèle peut-il appeler plusieurs outils en un seul tour ?
Il peut renvoyer une liste avec plusieurs entrées ; parcourez-les toutes et envoyez un résultat pour chacune. Ne supposez pas que la liste ne contient qu’un seul élément.
Ai-je besoin de la bibliothèque Ollama pour cela ?
Non. Les SDK officiels OpenAI suffisent. Pointez l’URL de base vers l’endpoint et utilisez le modèle uncensored.
Votre clé est à un formulaire de vous
Créez un compte, copiez la clé, modifiez l'URL de base. C'est toute la configuration.