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

> Écris une boucle de tool calling au format OpenAI et change de modèle avec une chaîne. Code Node.js, garde-fous, et ce que 4 modèles ont fait en octobre 2026.

- Published: 2026-10-17
- Updated: 2026-11-02
- Author: Gael — fondateur de Tchavi
- Canonical: https://tchavi.com/fr/blog/tool-calling-une-boucle-tous-les-modeles

---


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.

```js
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èle                | A        | B        | C        | D        | Total |
| --------------------- | -------- | -------- | -------- | -------- | ----- |
| GPT-6 Luna            | 0,15 (2) | 0,15 (2) | 0,26 (3) | 0,15 (2) | 0,70  |
| Claude Haiku 5.5      | 0,36 (2) | 0,35 (2) | 0,50 (2) | 0,41 (2) | 1,62  |
| DeepSeek V4 Flash     | 0,41 (2) | 0,43 (2) | 0,49 (2) | 0,48 (2) | 1,81  |
| Gemini 3.1 Flash Lite | 0,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](https://tchavi.com/fr/docs/principles)). 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ètre                    | Luna       | Haiku 5.5 | DeepSeek V4 Flash     | Gemini 3.1 Flash Lite |
| ---------------------------- | ---------- | --------- | --------------------- | --------------------- |
| `temperature: 0`             | accepté    | accepté   | accepté               | accepté               |
| `reasoning_effort: 'high'`   | erreur 502 | accepté   | accepté               | accepté               |
| `parallel_tool_calls: false` | accepté    | accepté   | **erreur 400**        | **erreur 400**        |
| `tool_choice: 'required'`    | accepté    | accepté   | 503 saturé, deux fois | accepté               |

« 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](https://tchavi.com/fr/docs/errors) liste les codes, et [la référence des complétions de chat](https://tchavi.com/fr/docs/chat-completions) 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](https://tchavi.com/fr/m/openai/gpt-6-luna), [Claude Haiku 5.5](https://tchavi.com/fr/m/anthropic/claude-haiku-5-5), [DeepSeek V4 Flash](https://tchavi.com/fr/m/deepseek/deepseek-v4-flash) ou [Gemini 3.1 Flash Lite](https://tchavi.com/fr/m/google/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](https://tchavi.com/fr/register?next=%2Fdashboard%2Fbilling%3Famount%3D1000%26currency%3DXOF%26from%3Dblog-tool-calling)

> _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](https://tchavi.com/fr/docs/payments)). Plus de guides sur [le blog](https://tchavi.com/fr/blog). Pour suivre les prochains : [LinkedIn](https://www.linkedin.com/company/tchavi) et [X](https://x.com/tchaviai)._

