Chiamate a tool con GLM 5.2 negli agent loop: cosa nasconde 'OpenAI-compatible'
Indice
Prendi un agent loop già scritto in stile OpenAI, puntalo su GLM 5.2 e quasi tutto funziona: mandi i tools, ricevi i tool_calls, li esegui, rimandi i risultati. Poi succede una cosa che gli esempi dell’SDK non mostrano mai. L’assistant restituisce una riga di testo nello stesso turno delle chiamate ai tool:
{
"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 turno di tool-call a caldo su GLM 5.2 è costato $0.0009 contro $0.0042 su gpt-5.5 e $0.0051 su claude-opus-4-8 (misurato il 2026-06-30).
- La latenza mediana del turno a caldo di GLM 5.2 era di 6.6s contro 1.9s su gpt-5.5 e 3.1s su opus-4-8: il turno economico è anche quello lento.
- GLM 5.2 restituisce testo visibile nello stesso turno di
tool_callsconfinish_reason: "tool_calls"; il contratto di OpenAI lasciacontentnull in quel caso. - Ogni turno a caldo di GLM 5.2 trasportava circa 27 reasoning token; gpt-5.5 e claude-opus-4-8 ne trasportavano 0 sullo stesso task.
Dominano due convenzioni, e conviene tenerle entrambe presenti. In quella di OpenAI mandi gli schemi delle funzioni, ricevi i tool_calls e rispondi con un messaggio tool per ogni chiamata, agganciato tramite 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"})
Quella di Anthropic ha una forma diversa: i tool portano un input_schema, il modello emette blocchi tool_use e tu rispondi con un blocco 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 parla il dialetto di OpenAI.
Nel contratto di OpenAI, message.content è null quando finish_reason è tool_calls. Molti agent loop ci si appoggiano: ramificano su “contenuto oppure chiamate ai tool”, loggano content come risposta finale, oppure verificano che sia vuoto. GLM ti consegna entrambi in una volta, ed è proprio questo assunto a saltare per primo.
Il comportamento qui descritto è stato registrato da richieste di tool calling reali verso glm-5.2, con gpt-5.5 e claude-opus-4-8 fatti girare sullo stesso task come riferimento. In breve: GLM 5.2 usa la superficie dell’API di OpenAI, ma su un paio di aspetti si comporta più come Claude che come GPT, e a inciampare è proprio il loop pensato per OpenAI.
Lo stesso turno, in tre modi
Stesso prompt, stessi due tool, tre modelli:
GLM (glm-5.2) | OpenAI (gpt-5.5) | Anthropic (claude-opus-4-8) | |
|---|---|---|---|
| Superficie API | OpenAI chat-completions | OpenAI chat-completions | Anthropic messages |
| Testo nel turno della tool-call | preambolo in content (non nullo) | content è null | un blocco text prima di tool_use |
| Reasoning in quel turno | esposto: reasoning_content + reasoning_tokens | nascosto; solo reasoning_tokens in usage | solo come blocco thinking, se lo abiliti |
| Tool call parallele | sì, con index | sì | sì, più blocchi tool_use |
| Segnale di completamento | finish_reason: "tool_calls" | finish_reason: "tool_calls" | stop_reason: "tool_use" |
| Prefisso id della tool-call | call_… | call_… | toolu_… |
I loop si rompono su due righe: il testo nel turno della tool-call e il reasoning che compare in quel turno. Il resto è rassicurantemente noioso.
Il testo viaggia insieme alla tool call
GLM 5.2 emette abitualmente un breve preambolo nel content dell’assistant insieme alle tool_calls, con finish_reason: "tool_calls". Non è un errore e non capita ogni tanto.
Ecco lo stesso turno dai tre modelli, ridotto alla parte che cambia:
// 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 lascia content a null; GLM lo riempie; Anthropic ci ha sempre messo un blocco text. Quindi GLM prende il formato di trasporto di OpenAI e ci aggiunge l’abitudine di Anthropic di raccontare prima di agire, e a farsi cogliere impreparato è il loop scritto per OpenAI. La correzione è piccola, ma va fatta di proposito. Smetti di considerare il turno di una tool-call come privo di contenuto:
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})
Se il tuo loop mostra content all’utente come risposta dell’assistant, ora vedrai una riga tipo “let me check that” prima di ogni tool call. Decidi se la vuoi. Il punto è che la decisione è tua, non qualcosa che il silenzio del modello prende al posto tuo.
Ragiona ad alta voce
GLM 5.2 è un modello di reasoning, e questo non si ferma durante l’uso dei tool. Un turno con chiamata a un tool porta con sé il reasoning, e GLM 5.2 lo espone come testo. In una risposta non-streaming il conteggio dei token lo rende evidente:
"usage": {
"prompt_tokens": 224,
"completion_tokens": 68,
"completion_tokens_details": { "reasoning_tokens": 30 },
"total_tokens": 292
}
Quasi metà del completion era reasoning, su una richiesta il cui output visibile sono due brevi function call. Qui è dove i tre modelli divergono. GLM 5.2 ti dà il reasoning come reasoning_content più un conteggio di token. OpenAI fattura i reasoning_tokens nel campo usage ma non mostra mai il testo. Anthropic lo mostra solo come blocchi thinking, e solo se abiliti l’extended thinking. GLM 5.2 è il più esposto dei tre per default.
Due conseguenze. La prima è il costo: paghi quei reasoning token sui turni con chiamata a un tool, e un loop di un agente è fatto di molti turni. Il reasoning effort è la manopola che sposta quel numero, come abbiamo visto in GLM 5.2: Reasoning Effort Is the Cost Lever. Conta i reasoning token a ogni turno, non solo sulla risposta finale.
La seconda è l’ordine nello streaming. Quando fai streaming della richiesta, GLM invia prima il reasoning, poi il testo di preambolo, poi le chiamate ai tool:
reasoning_content (many deltas)
content (a few deltas)
tool_calls (id + name, then arguments)
Un parser scritto per le chat completions standard di OpenAI non conosce il campo reasoning_content e ignora silenziosamente quella raffica iniziale. Di solito va bene. Smette di andare bene se la tua UI mostra uno stato “thinking…” agganciato al primo content delta: la prima cosa che arriva sul filo è reasoning, non content, e l’indicatore non scatta mai.
Quanto costa un turno con chiamata a un tool su GLM 5.2
Il comportamento è metà della storia; l’altra metà è il conto, e un loop di un agente ripete lo stesso turno molte volte. Con un prefisso fisso (un system prompt di circa 2.000 token più le definizioni dei tool) e il messaggio utente variato a ogni chiamata, misurato su dieci turni caldi:
| per turno caldo con chiamata a un tool | GLM glm-5.2 | OpenAI gpt-5.5 | Anthropic claude-opus-4-8 |
|---|---|---|---|
| Costo | $0.0009 | $0.0042 | $0.0051 |
| Latenza (mediana) | 6.6s | 1.9s | 3.1s |
| Prompt in cache | ≈96% | ≈81% | ≈97% |
| Reasoning token | ≈27 | 0 | 0 |
| Costo cold → warm | 3.4× | 2.8× | 4.9× |
GLM 5.2 è quello economico: circa 4,5× più economico di GPT-5.5 e 5,4× più economico di Opus per turno caldo. È anche quello lento, da due a tre volte e mezzo la loro latenza, perché spende reasoning token a ogni turno mentre gli altri due non ne hanno spesi su questo compito. È questo il compromesso: GLM compra costo con la latenza, e il reasoning effort è la manopola che lo regola.
È il caching a rendere sostenibile qualunque di questi in un loop. Il system prompt e le definizioni dei tool sono la maggior parte di ogni prompt e sono identici a ogni turno, quindi una volta che il prefisso è in cache il turno costa da 2,8× a 4,9× meno. Due cose determinano se lo ottieni. GLM e OpenAI mettono in cache il prefisso automaticamente; Anthropic mette in cache solo ciò che marchi con cache_control. E la cache di GLM si scalda con un attimo di ritardo, quindi un compito di tre passi può pagare il prezzo pieno mentre uno di trenta gira in cache. I dettagli sono in Open-Weight LLM Caching.
Quando conviene usare GLM 5.2 e come farlo bene
Mettiamo insieme i pezzi. Nella tabella GLM 5.2 è il modello economico e quello lento, e ragiona a ogni turno. Da questo profilo si capisce dove dà il meglio.
Dove ha senso usarlo: loop agentici lunghi e multi-step, dove il costo pesa più di tutto e qualche secondo per turno è accettabile. Agenti di coding in background, CI e automazioni batch, job che girano senza supervisione. Il ragionamento che lo rende lento è anche il motivo per cui regge su coding e planning reali, invece di limitarsi al routing banale. Il caching rafforza l’argomento una volta superato il warm-up: un task da trenta step ammortizza il prefisso e costa poco, mentre uno da tre step rischia di pagare il prezzo pieno e di subire la latenza per niente. Quindi usa GLM 5.2 sui job lunghi, e tieni un modello più veloce per le chiamate interattive one-shot, dove sei secondi a turno si sentono.
Come far girare bene GLM 5.2. Cinque abitudini rendono un loop pronto per GLM senza uscire dalla superficie dell’API OpenAI:
- Considera che un turno con tool call può contenere
content. Non dare per scontato che sia vuoto. - Aspettati
reasoning_contentsul filo ereasoning_tokensinusage; prevedili entrambi, e usa la manopola del reasoning-effort per bilanciare qualità e costo. - In streaming, non legare lo stato della UI al primo delta di content: il reasoning arriva prima.
- Rimanda indietro
tool_call_ididentico; trattalo come opaco, non fare parsing e non rigenerarlo. - Accumula gli
argumentsin streaming perindexfinché la call non si chiude; non assumere un numero fisso di chunk.
Due cose contro cui non devi difenderti: GLM emette tool call parallele con un index come gli altri, e il round-trip si chiude normalmente. Aggiungi il turno dell’assistant, aggiungi un messaggio tool per ogni call con il suo risultato, e finisce con finish_reason: "stop". Nel frattempo mantieni stabile a livello di byte il prefisso cacheabile tra un turno e l’altro: il system prompt e le definizioni dei tool sono la parte più grossa di ogni prompt, ed è proprio un prefisso stabile a permettere alla cache di GLM di abbattere il costo una volta che si scalda.
Niente di esotico. È la differenza tra “la richiesta va a buon fine” e “il loop dell’agente è corretto”, e su GLM questa differenza sta quasi tutta in due assunzioni: che un turno con tool call sia silenzioso, e che non stia pensando. Elimina queste due, tieni stabile il prefisso, e un unico loop regge GLM, GPT e Claude allo stesso modo, con GLM che fa il lavoro a una frazione del costo ovunque la latenza non sia ciò che stai ottimizzando.
Disclaimer
I dati su costo, latenza e cache riportati sopra sono stati misurati il 2026-06-30 su dieci turni con tool call a caldo per ogni modello, usando glm-5.2, gpt-5.5 e claude-opus-4-8. Il costo è preso dall’usage riportato; la latenza è la mediana wall-clock e varia con il carico e il reasoning effort. Il comportamento dei modelli e i prezzi cambiano nel tempo, quindi prendi questi numeri come indicativi e rimisurali sul tuo traffico prima di farci affidamento.