Appel de fonctions
L'aller-retour complet des outils (requête, tool_calls, tour de résultat, réponse finale) et la façon dont les outils sont facturés.
tools et tool_choice sont honorés, et la boucle entière l'est aussi : un modèle qui
peut demander un appel d'outil, et un appelant qui peut lui renvoyer le résultat. Une
demi-boucle serait un endpoint utilisable une seule fois.
L'aller-retour
import type OpenAI from "openai";
const tools: OpenAI.ChatCompletionTool[] = [
{
type: "function",
function: {
name: "get_weather",
description: "Météo actuelle pour une ville.",
parameters: {
type: "object",
properties: { city: { type: "string" } },
required: ["city"],
},
},
},
];
const messages: OpenAI.ChatCompletionMessageParam[] = [
{ role: "user", content: "Quel temps fait-il à Lyon ?" },
];
// 1. Demander, en proposant l'outil.
const first = await client.chat.completions.create({
model: "nova-lite",
messages,
tools,
});
const message = first.choices[0]!.message;
if (first.choices[0]!.finish_reason === "tool_calls") {
// 2. Rejouer le tour assistant tel quel : il porte les tool_calls.
messages.push(message);
for (const call of message.tool_calls ?? []) {
// `arguments` est une *chaîne* JSON, comme sur le protocole OpenAI.
const args = JSON.parse(call.function.arguments) as { city: string };
const result = await getWeather(args.city);
// 3. Un message tool par appel, reprenant son id en écho.
messages.push({
role: "tool",
tool_call_id: call.id,
content: JSON.stringify(result),
});
}
// 4. Redemander, cette fois avec les résultats dans la conversation.
const second = await client.chat.completions.create({
model: "nova-lite",
messages,
tools,
});
console.log(second.choices[0]?.message.content);
}Ce qui revient
finish_reason: "tool_calls", etchoices[0].message.tool_calls[] = { id, type: "function", function: { name, arguments } }.argumentsest une chaîne JSON, pas un objet. C'est la forme du protocole OpenAI : passez-la àJSON.parse.contentvautnullquand le modèle n'a produit aucun texte à côté de l'appel. C'est ce que renvoie OpenAI, et c'est ce que vous rejouez tel quel.
Ce que vous renvoyez
Le tour assistant qui a demandé l'appel est rejoué avec ses tool_calls, et chaque
résultat est un message tool. Les deux sont validés, et une conversation malformée donne
un 400 plutôt qu'un 502 venu du fournisseur :
| Règle | |
|---|---|
tool_calls | Uniquement sur un message assistant. |
tool_call_id | Obligatoire sur un message tool, interdit partout ailleurs. |
content | Obligatoire, sauf sur un message assistant porteur de tool_calls. |
Chaque tool_call_id | Doit correspondre à un id déclaré par un message assistant antérieur. |
Un outil déclaré sans parameters est transmis avec le schéma signifiant « ne prend
aucun argument », ce qui est bien ce qui a été demandé.
tool_choice
| Valeur | |
|---|---|
'auto' | Le modèle décide. |
'required' | Le modèle doit appeler un outil. |
{ type: 'function', function: { name } } | Cet outil-là. |
'none' | Un 400. Il n'existe aucun moyen d'exposer les outils tout en en interdisant l'usage, et les deux approximations sont indétectables : retirer les outils change la réponse et les tokens d'entrée facturés, ignorer le champ laisse le modèle appeler un outil que vous aviez interdit. Envoyez plutôt la requête sans tools. |
tool_choice sans tools est aussi un 400.
Streaming : les arguments arrivent en fragments
Avec stream: true, un appel d'outil est livré sous forme de fragments
delta.tool_calls, dans la forme propre à OpenAI :
- le premier fragment d'un appel porte
index,id,typeet le nom de la fonction, avecarguments: ""; - chaque fragment suivant ne porte qu'un morceau du JSON des arguments, sous le même
index.
Concaténer les morceaux d'un même index redonne exactement la chaîne qu'aurait portée un
corps non diffusé. Accumulez donc par index et parsez une seule fois à la fin : un
fragment isolé n'est pas du JSON valide.
const pending = new Map<number, { name: string; args: string }>();
for await (const chunk of stream) {
for (const fragment of chunk.choices[0]?.delta?.tool_calls ?? []) {
const entry = pending.get(fragment.index) ?? { name: "", args: "" };
if (fragment.function?.name) entry.name = fragment.function.name;
entry.args += fragment.function?.arguments ?? "";
pending.set(fragment.index, entry);
}
}
for (const [, { name, args }] of pending) {
console.log(name, JSON.parse(args)); // parser seulement une fois complet
}Les outils coûtent des tokens d'entrée
C'est le point qui surprend les intégrateurs : les schémas d'outils et les résultats d'outils sont du contenu de prompt. Ils comptent dans l'estimation pessimiste qui dimensionne la réservation sur le portefeuille, et ils font partie des tokens qui vous sont facturés.
- Le
name, ladescriptionet les octets sérialisés du schémaparametersde chaque outil déclaré. - Le nom d'outil dans un
tool_choicede forme{ type: 'function' }. - Le contenu et le
tool_call_idde chaque messagetool, ainsi que l'id, lenameet lesargumentssérialisés de chaque entréetool_callsrejouée.
Un schéma de 16 Ko représente des milliers de tokens d'entrée à chaque appel de la boucle,
et un aller-retour en deux temps paie le schéma deux fois. Un schéma trop gros se
manifeste donc par un 402 sur un portefeuille qui aurait couvert la conversation, avant
même d'apparaître sur une facture. Raccourcissez les descriptions, et n'envoyez pas des
outils que le modèle ne peut pas utiliser à ce tour.
Les arguments d'un appel d'outil sont de la sortie générée et sont facturés comme telle : une réponse composée uniquement d'un appel d'outil n'est pas gratuite.
Limites
Toutes donnent un 400. Voir le
tableau complet des limites.
| Limite | |
|---|---|
tools | 128 au maximum |
function.name | 64 caractères au maximum, sur la grammaire [A-Za-z0-9_-] d'OpenAI |
function.description | 4 096 caractères au maximum |
function.parameters | un objet JSON, 16 384 caractères sérialisés au maximum, 10 niveaux de profondeur au maximum |
tool_calls | 32 par message au maximum ; chaque arguments 32 768 caractères au maximum, et doit se parser en objet |
tool_call_id | 128 caractères au maximum |
function.strict, comme tout autre sous-champ non déclaré d'un outil, donne un 400 qui
le nomme.
Ce qui est stocké
Rien du contenu. Les appels d'outils sont de la sortie et les résultats d'outils de l'entrée ; l'événement d'usage enregistre le décompte des tokens, le modèle et le coût, jamais un message, un appel d'outil ou un résultat d'outil.