🎁 Nouveau Inscription gratuite, 10 appels offerts. Jusqu'à 1 $, sans carte.
Appels d'outils GLM 5.2 dans les boucles d'agent : ce que cache la « compatibilité OpenAI »

Appels d'outils GLM 5.2 dans les boucles d'agent : ce que cache la « compatibilité OpenAI »

Sommaire
  1. Le même tour, de trois façons
  2. Le texte voyage avec l’appel d’outil
  3. Il réfléchit à voix haute
  4. Combien coûte un tour avec appel d’outil sur GLM 5.2
  5. Quand sortir GLM 5.2, et comment le faire tourner correctement
  6. Avertissement
  7. Sources

Branchez une boucle d’agent existante de style OpenAI sur GLM 5.2 et l’essentiel fonctionne tel quel : vous envoyez tools, vous récupérez tool_calls, vous les exécutez, vous renvoyez les résultats. Puis il fait quelque chose que les exemples de SDK ne montrent jamais. L’assistant renvoie une ligne de texte dans 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\"}"}}
      ]
    }
  }]
}

Deux conventions dominent, et il vaut mieux garder les deux en tête. Chez OpenAI, vous envoyez des schémas de fonctions, vous récupérez des tool_calls, et vous répondez avec un message tool par appel, identifié par 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"})

Celle d’Anthropic a une forme différente : les outils portent un input_schema, le modèle émet des blocs tool_use, et 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 OpenAI, message.content vaut null quand finish_reason est tool_calls. Beaucoup de boucles d’agent s’appuient là-dessus : elles branchent sur « contenu ou appels d’outils », loguent content comme réponse finale, ou vérifient qu’il est vide. GLM vous donne les deux à la fois, et c’est cette hypothèse qui saute en premier.

Le comportement décrit ici a été observé sur des requêtes tool-calling réelles vers glm-5.2, avec gpt-5.5 et claude-opus-4-8 exécutés sur la même tâche comme points de comparaison. En résumé : GLM 5.2 utilise la surface d’API d’OpenAI, mais sur deux ou trois aspects il se comporte plus comme Claude que comme GPT, et c’est la boucle entraînée sur OpenAI qui trébuche.

Le même tour, de trois façons

Même prompt, deux mêmes outils, trois modèles :

GLM (glm-5.2)OpenAI (gpt-5.5)Anthropic (claude-opus-4-8)
Surface d’APIOpenAI chat-completionsOpenAI chat-completionsAnthropic messages
Texte dans le tour d’appel d’outilpréambule dans content (non null)content vaut nullun bloc text avant tool_use
Reasoning sur ce tourexposé : reasoning_content + reasoning_tokensmasqué ; uniquement reasoning_tokens dans usageuniquement via un bloc thinking, si vous l’activez
Appels d’outils parallèlesoui, avec indexouioui, plusieurs blocs tool_use
Signal de finfinish_reason: "tool_calls"finish_reason: "tool_calls"stop_reason: "tool_use"
Préfixe d’id d’appel d’outilcall_…call_…toolu_…

Deux lignes cassent les boucles : le texte dans le tour d’appel d’outil, et le reasoning qui apparaît sur ce tour. Le reste est rassurant de banalité.

Le texte voyage avec 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 les tool_calls, avec finish_reason: "tool_calls". Ce n’est pas une erreur et ce n’est pas occasionnel.

Voici le même tour pour les trois modèles, réduit à ce qui diffère :

// 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 remplit ; Anthropic y a toujours placé un bloc text. GLM reprend donc le format de OpenAI avec l’habitude d’Anthropic de commenter avant d’agir, et c’est une boucle écrite pour OpenAI qui se fait surprendre. Le correctif est léger, mais il faut le faire volontairement. Arrêtez de considérer qu’un tour d’appel d’outil est dépourvu de contenu :

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 le content à l’utilisateur comme réponse de l’assistant, vous montrerez désormais un « laissez-moi vérifier » avant chaque appel d’outil. À vous de décider si vous le voulez. Ce qui compte, c’est que la décision vous revienne, et qu’elle ne soit pas imposée par le silence du modèle.

Il réfléchit à voix haute

GLM 5.2 est un modèle de raisonnement, et il ne s’arrête pas pour utiliser un outil. Un tour avec appel d’outil embarque son raisonnement, et GLM 5.2 l’expose sous forme de texte. Dans une réponse non-streaming, le décompte de tokens le rend explicite :

"usage": {
  "prompt_tokens": 224,
  "completion_tokens": 68,
  "completion_tokens_details": { "reasoning_tokens": 30 },
  "total_tokens": 292
}

Presque la moitié de la complétion était du raisonnement, pour une requête dont la sortie visible se résume à deux appels de fonction courts. C’est la ligne où les trois modèles divergent. GLM 5.2 fournit le raisonnement dans reasoning_content, avec un décompte de tokens. OpenAI facture les reasoning_tokens dans usage mais ne montre jamais le texte. Anthropic ne le montre que sous forme de blocs thinking, et uniquement si vous activez le extended thinking. Par défaut, GLM 5.2 est le plus exposé des trois.

Deux conséquences. D’abord le coût : vous payez ces reasoning tokens à chaque tour avec appel d’outil, et une boucle d’agent compte beaucoup de tours. Le reasoning effort est le curseur qui fait bouger ce chiffre, comme nous l’avons vu dans GLM 5.2 : le reasoning effort est le levier de coût. Comptez les reasoning tokens à chaque tour, pas seulement sur la réponse finale.

Ensuite, l’ordre en streaming. Quand vous streamez la requête, GLM envoie d’abord le raisonnement, puis le texte de préambule, puis les appels d’outils :

reasoning_content  (many deltas)
content            (a few deltas)
tool_calls         (id + name, then arguments)

Un parser écrit pour les chat completions OpenAI classiques ne connaît pas le champ reasoning_content et ignorera silencieusement cette première salve. En général ça ne pose pas de problème. Mais ça en devient un si votre UI affiche un état « thinking… » déclenché par le premier delta de content : la première chose qui arrive sur le fil est du raisonnement, pas du content, et l’indicateur ne s’allume jamais.

Combien coûte un tour avec appel d’outil sur GLM 5.2

Le comportement, c’est la moitié de l’histoire ; la facture est l’autre moitié, et une boucle d’agent rejoue le même tour de nombreuses fois. Avec un préfixe fixe (un system prompt d’environ 2 000 tokens plus les définitions d’outils) et un message utilisateur qui change à chaque appel, mesuré sur dix tours à chaud :

par tour à chaud avec appel d’outilGLM glm-5.2OpenAI gpt-5.5Anthropic claude-opus-4-8
Coût$0.0009$0.0042$0.0051
Latence (médiane)6.6s1.9s3.1s
Prompt mis en cache≈96%≈81%≈97%
Reasoning tokens≈2700
Coût froid → chaud3.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, deux à trois fois et demie leur latence, parce qu’il dépense des reasoning tokens à chaque tour alors que les deux autres n’en ont consommé aucun sur cette tâche. C’est le compromis : GLM achète son coût avec de la latence, et le reasoning effort est le curseur qui le règle.

Le cache est ce qui rend tout ça abordable dans une boucle. Le system prompt et les définitions d’outils représentent l’essentiel de chaque prompt et sont identiques à chaque tour ; une fois le préfixe en cache, le tour devient 2,8× à 4,9× moins cher. Deux éléments décident si vous en profitez. GLM et OpenAI mettent le préfixe en cache automatiquement ; Anthropic ne met en cache que ce que vous marquez avec cache_control. Et le cache de GLM se réchauffe avec un léger décalage : une tâche en trois étapes peut payer plein tarif là où une tâche en trente étapes tourne en cache. Le détail du mécanisme est dans Le caching des LLM open-weight.

Quand sortir GLM 5.2, et comment le faire tourner correctement

Mettons les morceaux bout à bout. Dans ce tableau, GLM 5.2 est le modèle pas cher, le lent, et celui qui raisonne à chaque tour. Ce profil indique où il a sa place.

Là où il convient : les boucles d’agent longues et multi-étapes, où le coût domine et où quelques secondes par tour passent. Agents de codage en arrière-plan, CI et automatisation par lots, jobs qui tournent sans surveillance. Le raisonnement qui le rend lent est aussi ce qui lui permet de tenir sur du vrai codage et de la planification, plutôt que sur du routage trivial. Le cache renforce l’argument une fois la phase de chauffe passée : une tâche de trente étapes amortit le préfixe et coûte peu, alors qu’une tâche de trois étapes peut payer plein tarif et encaisser la latence pour rien. Donc on sort GLM 5.2 sur les jobs longs, et on garde un modèle plus rapide pour les appels interactifs et uniques, où six secondes par tour se ressentent.

Comment faire tourner GLM 5.2 correctement. Cinq habitudes rendent une boucle compatible GLM sans quitter la surface de l’API OpenAI :

  • Considérer qu’un tour avec appel d’outil peut transporter du content. Ne pas partir du principe qu’il est vide.
  • S’attendre à reasoning_content sur le fil et à reasoning_tokens dans usage ; prévoir les deux, et jouer sur le reasoning-effort pour arbitrer entre qualité et coût.
  • En streaming, ne pas baser l’état de l’UI sur le premier delta de contenu, puisque le raisonnement arrive d’abord.
  • Renvoyer tool_call_id à l’identique ; le traiter comme opaque, ne jamais le parser ni le régénérer.
  • Accumuler les arguments en streaming par index jusqu’à la clôture de l’appel ; ne pas présumer d’un nombre de chunks.

Deux choses contre lesquelles vous n’avez pas à vous prémunir : GLM émet des appels d’outils parallèles avec un index comme les autres, et l’aller-retour se clôt normalement. Ajoutez le tour de l’assistant, ajoutez un message tool par appel avec son résultat, et ça se termine sur finish_reason: "stop". Au passage, gardez le préfixe cacheable stable au octet près d’un tour à l’autre ; le system prompt et les définitions d’outils constituent l’essentiel de chaque prompt, et c’est un préfixe stable qui permet au cache de GLM d’absorber le coût une fois chaud.

Rien de tout ça n’est exotique. C’est l’écart entre « la requête réussit » et « la boucle d’agent est correcte », et sur GLM cet écart tient surtout à deux hypothèses : qu’un tour avec appel d’outil est silencieux, et qu’il ne réfléchit pas. Abandonnez ces deux-là, gardez le préfixe stable, et une seule boucle fait tourner GLM, GPT et Claude de la même manière, GLM le faisant pour une fraction du coût partout où la latence n’est pas ce que vous optimisez.

Avertissement

Les chiffres de coût, de latence et de cache ci-dessus ont été mesurés le 2026-06-30 sur dix tours d’appel d’outil à chaud par modèle, avec glm-5.2, gpt-5.5 et claude-opus-4-8. Le coût provient de l’usage rapporté ; la latence est la médiane en temps réel et varie selon la charge et le reasoning effort. Le comportement des modèles et les prix bougent, donc traitez ces chiffres comme indicatifs et remesurez sur votre propre trafic avant d’en dépendre.

Sources

← Retour au blog