Appels d'outils GLM 5.2 : ce que masque la compatibilité OpenAI
Sommaire
Branchez une boucle d’agent existante, conçue pour OpenAI, sur GLM 5.2 : presque tout fonctionne immédiatement. Vous envoyez tools, vous recevez tool_calls, vous les exécutez, puis vous renvoyez les résultats. Mais le modèle adopte ensuite un comportement absent des exemples des SDK. L’assistant renvoie une ligne de texte pendant le même tour que les appels d’outils :
{
"choices": [{
"finish_reason": "tool_calls",
"message": {
"role": "assistant",
"content": "I'll look up both pieces of information for you at the same time!",
"tool_calls": [
{"id": "call_…", "type": "function",
"function": {"name": "get_weather", "arguments": "{\"city\":\"Paris\"}"}},
{"id": "call_…", "type": "function",
"function": {"name": "get_time", "arguments": "{\"city\":\"Tokyo\"}"}}
]
}
}]
}
TL;DR
- Un tour d’appel d’outils GLM 5.2 avec cache chaud coûtait $0.0009, contre $0.0042 pour gpt-5.5 et $0.0051 pour claude-opus-4-8 (mesures du 2026-06-30).
- La latence médiane d’un tour à chaud atteignait 6.6s sur GLM 5.2, contre 1.9s sur gpt-5.5 et 3.1s sur opus-4-8 : le tour le moins cher est aussi le plus lent.
- GLM 5.2 renvoie du texte visible pendant le même tour que
tool_calls, avecfinish_reason: "tool_calls"; dans le contrat d’OpenAI,contentreste null dans ce cas. - Chaque tour à chaud de GLM 5.2 comportait environ 27 tokens de raisonnement ; gpt-5.5 et claude-opus-4-8 en avaient 0 sur la même tâche.
Deux conventions dominent, et il faut garder les deux en tête. Avec celle d’OpenAI, vous envoyez des schémas de fonctions, récupérez tool_calls, puis répondez par un message tool pour chaque appel, associé via tool_call_id :
resp = openai.chat.completions.create(model="…", tools=tools, tool_choice="auto", messages=messages)
# assistant.tool_calls → [{"id": "call_…", "function": {"name": "get_weather", "arguments": "{\"city\":\"Paris\"}"}}]
messages.append(resp.choices[0].message)
messages.append({"role": "tool", "tool_call_id": "call_…", "content": "18C, clear"})
Chez Anthropic, la structure diffère : les outils contiennent un input_schema, le modèle émet des blocs tool_use, puis vous répondez avec un bloc tool_result :
resp = anthropic.messages.create(model="…", tools=tools, messages=messages)
# resp.content → [{"type": "tool_use", "id": "toolu_…", "name": "get_weather", "input": {"city": "Paris"}}]
messages.append({"role": "assistant", "content": resp.content})
messages.append({"role": "user", "content": [
{"type": "tool_result", "tool_use_id": "toolu_…", "content": "18C, clear"}]})
GLM 5.2 parle le dialecte OpenAI.
Dans le contrat d’OpenAI, message.content vaut null lorsque finish_reason vaut tool_calls. Beaucoup de boucles d’agents reposent sur ce principe : elles choisissent entre « contenu ou appels d’outils », journalisent content comme réponse finale ou vérifient qu’il est vide. GLM renvoie les deux à la fois. C’est la première hypothèse qui casse.
Ce comportement a été observé sur des requêtes réelles d’appel d’outils envoyées à glm-5.2, avec gpt-5.5 et claude-opus-4-8 exécutés sur la même tâche à titre de comparaison. En bref, GLM 5.2 reprend l’interface API d’OpenAI, mais se comporte davantage comme Claude que comme GPT sur certains points. Ce sont donc les boucles conçues pour OpenAI qui trébuchent.
Un même tour, trois comportements
Même prompt, mêmes deux outils, trois modèles :
GLM (glm-5.2) | OpenAI (gpt-5.5) | Anthropic (claude-opus-4-8) | |
|---|---|---|---|
| Interface API | chat-completions d’OpenAI | chat-completions d’OpenAI | messages d’Anthropic |
| Texte pendant le tour d’appel d’outils | préambule dans content (non null) | content vaut null | un bloc text avant tool_use |
| Raisonnement pendant ce tour | exposé : reasoning_content + reasoning_tokens | masqué ; uniquement reasoning_tokens dans usage | uniquement sous forme de bloc thinking, si l’option est activée |
| Appels d’outils parallèles | oui, avec index | oui | oui, plusieurs blocs tool_use |
| Signal de fin | finish_reason: "tool_calls" | finish_reason: "tool_calls" | stop_reason: "tool_use" |
| Préfixe des identifiants d’appel d’outil | call_… | call_… | toolu_… |
Deux lignes peuvent casser les boucles : le texte présent pendant le tour d’appel d’outils et le raisonnement exposé pendant ce même tour. Le reste est heureusement sans surprise.
Le texte accompagne l’appel d’outil
GLM 5.2 émet régulièrement un court préambule dans le content de l’assistant, en même temps que tool_calls, avec finish_reason: "tool_calls". Ce n’est ni une erreur ni un comportement occasionnel.
Voici le même tour pour les trois modèles, réduit aux parties qui diffèrent :
// OpenAI gpt-5.5: content is null on a tool-call turn
"message": { "content": null,
"tool_calls": [ {/* get_weather */}, {/* get_time */} ] }
// GLM glm-5.2: content carries a preamble
"message": { "content": "I'll look up both pieces of information for you at the same time!",
"tool_calls": [ {/* get_weather */}, {/* get_time */} ] }
// Anthropic claude-opus-4-8: a text block sits before the tool_use blocks
"content": [ { "type": "text", "text": "I'll get both pieces of information for you." },
{ "type": "tool_use", /* get_weather */ },
{ "type": "tool_use", /* get_time */ } ]
OpenAI laisse content à null ; GLM le renseigne ; Anthropic place depuis toujours un bloc text à cet endroit. GLM combine donc le format d’échange d’OpenAI avec l’habitude d’Anthropic qui consiste à annoncer l’action avant de l’exécuter. Une boucle écrite pour OpenAI est prise au dépourvu. La correction est simple, mais doit être explicite : ne considérez plus qu’un tour d’appel d’outils ne contient jamais de texte.
resp = client.chat.completions.create(model="glm-5.2", messages=msgs, tools=tools)
msg = resp.choices[0].message
# GLM may return assistant text in the same turn as the tool calls.
if msg.content:
log.debug("preamble: %s", msg.content) # keep or drop, but don't assume it's empty
msgs.append(msg)
for call in msg.tool_calls:
result = dispatch(call.function.name, json.loads(call.function.arguments))
msgs.append({"role": "tool", "tool_call_id": call.id, "content": result})
Si votre boucle affiche content à l’utilisateur comme réponse de l’assistant, elle montrera désormais un message du type « je vérifie » avant chaque appel d’outil. À vous de décider si vous voulez le conserver. Ce choix ne doit pas dépendre du silence du modèle.
Il raisonne à voix haute
GLM 5.2 est un modèle de raisonnement, y compris lorsqu’il utilise des outils. Un tour d’appel d’outils contient donc aussi du raisonnement, que GLM 5.2 expose sous forme de texte. Dans une réponse non streamée, le décompte des tokens le montre clairement :
"usage": {
"prompt_tokens": 224,
"completion_tokens": 68,
"completion_tokens_details": { "reasoning_tokens": 30 },
"total_tokens": 292
}
Près de la moitié de la complétion correspondait au raisonnement, alors que la sortie visible ne contenait que deux courts appels de fonctions. C’est sur ce point que les trois modèles divergent. GLM 5.2 fournit le raisonnement dans reasoning_content, accompagné du nombre de tokens. OpenAI facture les reasoning_tokens indiqués dans usage, mais n’en montre jamais le texte. Anthropic ne l’expose que dans des blocs thinking, et uniquement lorsque le raisonnement étendu est activé. Par défaut, GLM 5.2 est le plus transparent des trois.
Cela entraîne deux conséquences. D’abord, le coût : vous payez ces tokens de raisonnement à chaque tour d’appel d’outils, et une boucle d’agent en compte beaucoup. Le reasoning effort est le réglage qui fait varier ce nombre, comme expliqué dans GLM 5.2 : le reasoning effort est le principal levier de coût. Comptez les tokens de raisonnement à chaque tour, pas seulement dans la réponse finale.
Ensuite, l’ordre des événements en streaming. Lorsque la requête est streamée, GLM envoie d’abord le raisonnement, puis le texte du préambule et enfin les appels d’outils :
reasoning_content (many deltas)
content (a few deltas)
tool_calls (id + name, then arguments)
Un parseur écrit pour les chat completions OpenAI classiques ne connaît pas le champ reasoning_content et ignorera silencieusement ce premier flux. Ce n’est généralement pas un problème. Cela le devient si votre UI affiche un état « réflexion en cours… » déclenché par le premier delta de contenu : le raisonnement arrive avant content, et l’indicateur ne change jamais d’état.
Coût d’un tour d’appel d’outils avec GLM 5.2
Le comportement ne représente que la moitié du sujet ; l’autre moitié, c’est la facture. Or une boucle d’agent répète ce même tour de nombreuses fois. Avec un préfixe fixe, composé d’un system prompt d’environ 2,000 tokens et des définitions d’outils, et un message utilisateur modifié à chaque appel, voici les mesures relevées sur dix tours à chaud :
| par tour d’appel d’outils à chaud | GLM glm-5.2 | OpenAI gpt-5.5 | Anthropic claude-opus-4-8 |
|---|---|---|---|
| Coût | $0.0009 | $0.0042 | $0.0051 |
| Latence (médiane) | 6.6s | 1.9s | 3.1s |
| Prompt mis en cache | ≈96% | ≈81% | ≈97% |
| Tokens de raisonnement | ≈27 | 0 | 0 |
| Coût à froid → à chaud | 3.4× | 2.8× | 4.9× |
GLM 5.2 est le moins cher : environ 4.5× moins cher que GPT-5.5 et 5.4× moins cher qu’Opus par tour à chaud. C’est aussi le plus lent, avec une latence deux à trois fois et demie supérieure. Il consomme des tokens de raisonnement à chaque tour, alors que les deux autres n’en ont utilisé aucun sur cette tâche. Le compromis est clair : GLM échange de la latence contre une baisse des coûts, et le reasoning effort permet d’ajuster cet équilibre.
Le cache rend ces boucles financièrement viables. Le system prompt et les définitions d’outils constituent l’essentiel de chaque prompt et ne changent pas d’un tour à l’autre. Une fois le préfixe en cache, le coût d’un tour diminue donc d’un facteur de 2.8× à 4.9×. Deux éléments déterminent si vous en bénéficiez. GLM et OpenAI mettent automatiquement le préfixe en cache ; Anthropic ne met en cache que les éléments marqués avec cache_control. Par ailleurs, le cache de GLM chauffe avec un léger retard : une tâche en trois étapes peut être facturée au tarif plein, tandis qu’une tâche en trente étapes profite du cache. Le fonctionnement détaillé est décrit dans Mise en cache des LLM à poids ouverts.
Quand choisir GLM 5.2 et comment bien l’exploiter
Le tableau est cohérent : GLM 5.2 est le modèle le moins cher, mais aussi le plus lent, et il raisonne à chaque tour. Ce profil détermine les cas où il est pertinent.
Il convient aux longues boucles d’agents en plusieurs étapes, lorsque le coût prime et que quelques secondes par tour restent acceptables : agents de développement en arrière-plan, CI, traitements batch et tâches exécutées sans supervision. Le raisonnement qui le ralentit lui permet aussi de rester efficace sur de vrais problèmes de code et de planification, plutôt que sur du simple routage. Après la phase de chauffe, le cache renforce cet avantage : une tâche en trente étapes amortit le préfixe et reste économique, alors qu’une tâche en trois étapes peut être facturée au tarif plein tout en subissant la latence. Réservez donc GLM 5.2 aux tâches longues et utilisez un modèle plus rapide pour les appels interactifs et ponctuels, où une latence de six secondes par tour se ressent.
Cinq pratiques permettent d’adapter une boucle à GLM 5.2 tout en conservant l’interface API d’OpenAI :
- Considérez qu’un tour d’appel d’outils peut contenir
content. Ne vérifiez pas qu’il est vide. - Attendez-vous à recevoir
reasoning_contentdans le flux etreasoning_tokensdansusage; prévoyez leur coût et utilisez le réglage du reasoning effort pour arbitrer entre qualité et coût. - En streaming, ne déclenchez pas l’état de l’UI sur le premier delta de contenu, car le raisonnement arrive avant.
- Renvoyez
tool_call_idà l’identique ; considérez-le comme opaque, sans jamais l’analyser ni le régénérer. - Accumulez les
argumentsstreamés parindexjusqu’à la fin de l’appel ; ne supposez pas un nombre fixe de chunks.
Deux points ne nécessitent aucune précaution particulière : comme les autres modèles, GLM émet les appels d’outils parallèles avec un index, et le cycle se termine normalement. Ajoutez le tour de l’assistant, puis un message tool par appel avec son résultat ; le modèle termine avec finish_reason: "stop". Veillez aussi à conserver un préfixe strictement identique d’un tour à l’autre. Le system prompt et les définitions d’outils représentent l’essentiel de chaque prompt, et cette stabilité permet au cache de GLM de réduire les coûts une fois chaud.
Rien de tout cela n’est inhabituel. C’est simplement l’écart entre « la requête aboutit » et « la boucle d’agent est correcte ». Avec GLM, cet écart tient surtout à deux hypothèses erronées : un tour d’appel d’outils serait silencieux et ne comporterait aucun raisonnement. Supprimez ces deux hypothèses et gardez le préfixe stable. Une seule boucle pourra alors prendre en charge GLM, GPT et Claude, GLM coûtant bien moins cher partout où la latence n’est pas le critère principal.
Avertissement
Les coûts, latences et taux de cache ci-dessus ont été mesurés le 2026-06-30 sur dix tours d’appel d’outils à chaud par modèle, avec glm-5.2, gpt-5.5 et claude-opus-4-8. Le coût provient de l’usage indiqué par les API ; la latence correspond à la médiane du temps écoulé et varie avec la charge et le reasoning effort. Le comportement et les tarifs des modèles évoluent. Ces chiffres sont donc indicatifs : refaites les mesures avec votre propre trafic avant de vous appuyer dessus.