Guides

Tool calling : une seule boucle, tous les modèles (Node.js)

TchaviPar Gael — fondateur de Tchavi

Publié le · Mis à jour le · 11 min de lecture

Retour au blog

Sommaire

Tu peux écrire la boucle de tool calling une seule fois, au format OpenAI, et changer de modèle en changeant une chaîne. En octobre 2026, j'ai fait tourner la même boucle sur quatre modèles de fournisseurs différents (OpenAI, Anthropic, DeepSeek, Google), à travers Tchavi, sur quatre questions de boutique. Les quatre ont appelé le bon outil avec les bons arguments, les 16 fois.

Ce guide te donne la boucle complète, avec ses deux garde-fous (un plafond de tours et une détection d'appel répété), et les cas où un modèle a refusé un paramètre.

En bref

  • Tchavi, une passerelle d'API IA compatible OpenAI que tu paies en mobile money, te laisse garder le format tools d'OpenAI pour tous les modèles : pour les modèles Claude, la passerelle traduit les outils vers le format d'Anthropic, et traduit la réponse en retour.
  • 16 essais sur 16 corrects : 4 modèles (GPT-6 Luna, Claude Haiku 5.5, DeepSeek V4 Flash, Gemini 3.1 Flash Lite) sur 4 tâches, avec le bon outil et les bons arguments. Chaque tâche a pris 2 tours, sauf une qui en a pris 3 chez Luna.
  • Les quatre tâches ont coûté de 0,7 à 1,8 crédit au total selon le modèle (de 0,23 à 0,60 F).
  • Un modèle refuse parfois un paramètre, et Tchavi ne simule pas ce que le modèle ne sait pas faire : tu reçois l'erreur. J'en ai mesuré deux cas, plus loin.

La boucle, en une fois

Un agent à outils, c'est une boucle : tu envoies la question et la liste des outils, le modèle répond soit par du texte, soit par une demande d'appel (tool_calls). Si c'est une demande, tu exécutes l'outil, tu renvoies le résultat avec le rôle tool, et tu rappelles le modèle. Tu t'arrêtes quand il répond par du texte.

Mes deux outils, pour une boutique de Cotonou : prix_livraison(ville) et stock(produit). Ils lisent deux petits tableaux, pour que le test soit reproductible.

import OpenAI from 'openai';

const client = new OpenAI({
  apiKey: process.env.TCHAVI_API_KEY,
  baseURL: 'https://tchavi.com/api/v1',
});

const tools = [
  {
    type: 'function',
    function: {
      name: 'prix_livraison',
      description: 'Prix de livraison en F CFA vers une ville.',
      parameters: {
        type: 'object',
        properties: { ville: { type: 'string', description: 'Nom de la ville' } },
        required: ['ville'],
      },
    },
  },
  {
    type: 'function',
    function: {
      name: 'stock',
      description: "Quantité en stock d'un produit de la boutique.",
      parameters: {
        type: 'object',
        properties: { produit: { type: 'string', description: 'Nom du produit' } },
        required: ['produit'],
      },
    },
  },
];

const PRIX = { cotonou: 1000, 'abomey-calavi': 1500, 'porto-novo': 2500 };
const STOCK = { 'chargeur 65 w': 14, 'casque bluetooth': 3 };

function runTool(name, args) {
  if (name === 'prix_livraison') {
    const prix = PRIX[String(args.ville).toLowerCase().trim()];
    return prix === undefined ? { erreur: 'ville non desservie' } : { prix_fcfa: prix };
  }
  if (name === 'stock') {
    const cle = Object.keys(STOCK).find((k) => k === String(args.produit).toLowerCase().trim());
    return cle === undefined ? { erreur: 'produit inconnu' } : { quantite: STOCK[cle] };
  }
  return { erreur: 'outil inconnu' };
}

export async function agent(model, question, maxTurns = 6) {
  const messages = [
    {
      role: 'system',
      content: "Tu es l'assistant de la boutique Chez Akpè. Utilise les outils, n'invente rien.",
    },
    { role: 'user', content: question },
  ];
  const seen = new Set();
  let credits = 0;

  for (let turn = 1; turn <= maxTurns; turn++) {
    const { data, response } = await client.chat.completions
      .create({ model, messages, tools })
      .withResponse();
    credits += Number(response.headers.get('x-credits-used') ?? 0);

    const msg = data.choices[0].message;
    messages.push(msg);
    if (!msg.tool_calls?.length) return { answer: msg.content, turns: turn, credits };

    let repeated = false;
    for (const call of msg.tool_calls) {
      const args = JSON.parse(call.function.arguments || '{}');
      const signature = call.function.name + JSON.stringify(args);
      repeated ||= seen.has(signature);
      seen.add(signature);
      const result = runTool(call.function.name, args);
      messages.push({ role: 'tool', tool_call_id: call.id, content: JSON.stringify(result) });
    }
    if (repeated) return { answer: null, turns: turn, credits, stopped: 'appel répété' };
  }
  return { answer: null, turns: maxTurns, credits, stopped: 'limite de tours' };
}

console.log(
  await agent(
    'gpt-6-luna',
    "Je veux un casque Bluetooth livré à Abomey-Calavi : c'est en stock, et la livraison coûte combien ?",
  ),
);
console.log(
  await agent(
    'claude-haiku-5-5',
    "Je veux un casque Bluetooth livré à Abomey-Calavi : c'est en stock, et la livraison coûte combien ?",
  ),
);

Le seul endroit qui dépend du modèle, c'est le premier argument de agent. Le reste ne change pas : même format de tools, même lecture de tool_calls, même rôle tool pour la réponse.

Ce que Tchavi traduit, et ce qu'il laisse passer

Les modèles Claude ne parlent pas le format OpenAI : Anthropic attend des outils avec un input_schema, des blocs tool_use dans la réponse et des blocs tool_result dans un message utilisateur. Quand tu appelles un modèle Claude avec le format OpenAI, la passerelle fait la conversion :

  • chaque outil function.parameters devient un input_schema ;
  • un message assistant avec tool_calls devient des blocs tool_use ;
  • un message de rôle tool devient un bloc tool_result sur un message utilisateur, puisque Anthropic n'a pas de rôle tool ;
  • tool_choice: 'required' devient any chez Anthropic ;
  • dans la réponse, les blocs tool_use redeviennent des tool_calls, et l'arrêt sur un outil donne finish_reason: 'tool_calls', ce que ta boucle attend.

Pour les trois autres modèles de ce test, la requête est restée au format OpenAI : aucune conversion d'outils de la part de la passerelle, et rien à adapter dans mon code.

Un cas particulier existe pour GPT-6 Luna. OpenAI n'accepte les outils sur ce modèle qu'avec reasoning_effort: 'none', alors que l'effort par défaut est moyen. Sans correction, une boucle écrite avec le SDK OpenAI échouerait. Quand tu ne choisis pas d'effort, Tchavi envoie none à ta place, et c'est pour ça que ma boucle fonctionne sans réglage.

Les quatre tâches et les résultats

Quatre questions, sans changer la boucle :

  • A : « Combien coûte la livraison à Porto-Novo ? » → prix_livraison avec Porto-Novo.
  • B : « Il vous reste des chargeurs 65 W ? » → stock avec le chargeur.
  • C : « Je veux un casque Bluetooth livré à Abomey-Calavi : c'est en stock, et la livraison coûte combien ? » → les deux outils.
  • D : « Vous livrez à Parakou ? » → prix_livraison, qui répond « ville non desservie » : le modèle doit le dire au client, pas inventer un tarif.

Crédits par tâche (3 crédits = 1 F), tours entre parenthèses :

ModèleABCDTotal
GPT-6 Luna0,15 (2)0,15 (2)0,26 (3)0,15 (2)0,70
Claude Haiku 5.50,36 (2)0,35 (2)0,50 (2)0,41 (2)1,62
DeepSeek V4 Flash0,41 (2)0,43 (2)0,49 (2)0,48 (2)1,81
Gemini 3.1 Flash Lite0,26 (2)0,26 (2)0,45 (2)0,26 (2)1,23

Ce sont les crédits mesurés le 17 octobre. Les tarifs de Gemini 3.1 Flash Lite ont augmenté d'environ 5,5 % depuis : les mêmes quatre tâches coûteraient maintenant environ 1,30 crédit.

Dans tous les cas, le bon outil, les bons arguments, aucun appel en trop, et à la tâche D les quatre ont répondu que Parakou n'est pas desservi. Sur la tâche C, Luna a appelé les deux outils l'un après l'autre (3 tours) ; les trois autres les ont appelés dans le même tour (2 tours). Ce n'est qu'une exécution par modèle : un modèle peut demander deux outils dans le même tour ou l'un après l'autre, donc le nombre de tours n'est pas une propriété stable.

Les crédits ne comparent pas la qualité, seulement ce que chaque modèle a consommé ici : Luna a été le moins gourmand sur ces quatre questions. Ces quatre tâches sont faciles, et un test à 16 essais ne départage pas des modèles qui réussissent tous. Pour choisir entre eux, écris quinze cas tirés de ta vraie boutique et compte les réussites.

Les garde-fous

Le plafond de tours. maxTurns = 6 : sans lui, un modèle qui rappelle un outil en boucle te coûte des crédits jusqu'à ce que tu coupes. Quand il est atteint, ne renvoie pas une réponse inventée : renvoie une erreur ou passe à un humain.

La détection d'appel répété. Si le modèle rappelle le même outil avec les mêmes arguments, il ne tirera rien de plus du deuxième appel. La boucle garde une signature (nom + arguments) et s'arrête à la première répétition.

J'ai vérifié que ce second garde-fou sert. J'ai cassé l'outil stock pour qu'il réponde toujours « service indisponible, réessaie », puis j'ai demandé s'il restait des écouteurs filaires. Luna, Haiku 5.5 et Gemini 3.1 Flash Lite ont fait un appel puis dit au client que le service était indisponible. DeepSeek V4 Flash a rappelé stock avec les mêmes arguments, et la détection a coupé la boucle au tour 2. Sans elle, la boucle aurait envoyé une requête de plus, et chaque tour est facturé.

Quand un modèle refuse un paramètre

Tchavi fait suivre ta requête au modèle sans émuler ce qu'il ne fait pas (les principes). Si un modèle ne prend pas un paramètre, tu as l'erreur au lieu d'un comportement approximatif. J'ai essayé sur la tâche A quatre paramètres, un modèle à la fois :

ParamètreLunaHaiku 5.5DeepSeek V4 FlashGemini 3.1 Flash Lite
temperature: 0acceptéacceptéacceptéaccepté
reasoning_effort: 'high'erreur 502acceptéacceptéaccepté
parallel_tool_calls: falseacceptéacceptéerreur 400erreur 400
tool_choice: 'required'acceptéaccepté503 saturé, deux foisaccepté

« Accepté » veut dire une réponse 200 avec le bon outil appelé.

  • DeepSeek V4 Flash et parallel_tool_calls : erreur 400, code unsupported_parameter, avec le nom du paramètre. Retire-le de la requête pour ce modèle. C'est le cas propre : rien n'est facturé, et le message dit quoi enlever.
  • Gemini 3.1 Flash Lite et parallel_tool_calls: false : accepté pendant ce test, il reçoit maintenant la même erreur 400 unsupported_parameter que DeepSeek V4 Flash, car le paramètre n'est pas appliqué sur ce modèle. Retire-le aussi pour Gemini.
  • GPT-6 Luna avec un reasoning_effort explicite et des outils : erreur 502 provider_error, sans détail, sur high comme sur low (rejoué deux fois), et rien n'est facturé. Ne règle pas l'effort sur ce modèle quand tu lui donnes des outils.
  • DeepSeek V4 Flash avec tool_choice: 'required' : deux fois l'erreur 503 provider_saturated. Je ne sais pas si c'est le paramètre ou une saturation passagère, donc je ne conclus pas.

Un conseil de code : gère les erreurs 4xx et 5xx de ta boucle. Les appels refusés ne coûtent rien, mais une boucle qui plante en plein milieu laisse le client sans réponse. La page erreurs liste les codes, et la référence des complétions de chat précise que la prise en charge des outils dépend du modèle : la page de chaque modèle (par exemple GPT-6 Luna, Claude Haiku 5.5, DeepSeek V4 Flash ou Gemini 3.1 Flash Lite) indique s'il accepte les outils.

FAQ

Tous les modèles de Tchavi acceptent-ils les outils ?

Non. Les capacités sont déclarées par modèle : GET /v1/models renvoie capabilities.tools pour chacun, et certains modèles sont à false. Vérifie ce champ, ou l'onglet API de la page du modèle, avant de brancher un agent.

Dois-je traduire les outils pour Claude ?

Non. Tu envoies le format OpenAI et Tchavi traduit pour les modèles Claude, dans les deux sens. Si tu préfères le format Anthropic natif, Tchavi sert aussi l'API Messages avec l'URL https://tchavi.com/api, ce que les clients Anthropic utilisent.

Combien de tours faut-il autoriser ?

Pour ces tâches, 2 ou 3 tours suffisent. J'ai mis 6 : assez pour un enchaînement de deux ou trois outils, assez bas pour qu'une boucle folle coûte peu. Ajuste selon ton agent, et surveille x-credits-used à chaque tour.

Les outils s'exécutent-ils chez Tchavi ?

Non. Le modèle ne fait que demander l'appel ; c'est ton code qui exécute prix_livraison ou stock, et qui renvoie le résultat dans un message de rôle tool.

Pourquoi le nombre de tours change-t-il d'une exécution à l'autre ?

Parce qu'un modèle peut demander deux outils dans le même tour ou l'un après l'autre. Ne fais pas dépendre ton code d'un nombre de tours précis : garde seulement le plafond.

crée ton compte et recharge 1 000 F

Cet article fait partie de la série « Construire un SaaS IA depuis l'Afrique francophone ». Tchavi est une passerelle d'API IA compatible OpenAI : une seule clé donne accès à plus de 100 modèles de texte, d'image, d'audio et de vidéo, et tu la paies en mobile money, sans carte bancaire (pays et opérateurs). Plus de guides sur le blog. Pour suivre les prochains : LinkedIn et X.

Partager

Ton premier appel d'API, payé en mobile money

Sans carte bancaire. Recharge à partir de 1 000 F ; ta première recharge ajoute 150 crédits de bonus, et tes crédits n'expirent pas.