Tool Calls do GLM 5.2 em Agent Loops: o que 'compatível com OpenAI' esconde
Conteúdo
Aponte um agent loop existente no estilo OpenAI para o GLM 5.2 e quase tudo funciona: você manda tools, recebe tool_calls, executa, manda os resultados. Aí ele faz algo que os exemplos do SDK nunca mostram. O assistant devolve uma linha de texto no mesmo turno das tool calls:
{
"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\"}"}}
]
}
}]
}
Duas convenções dominam, e vale manter as duas em mente. Na da OpenAI, você manda os schemas das funções, recebe tool_calls de volta e responde com uma mensagem tool por call, identificada pelo 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"})
A da Anthropic tem outro formato: as tools carregam um input_schema, o modelo emite blocos tool_use e você responde com um bloco 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"}]})
O GLM 5.2 fala o dialeto da OpenAI.
No contrato da OpenAI, message.content é null quando finish_reason é tool_calls. Muitos agent loops dependem disso: fazem branch em “content ou tool calls”, logam content como resposta final, ou assumem que está vazio. O GLM te entrega os dois ao mesmo tempo, e essa suposição é a primeira coisa que quebra.
Esse comportamento foi capturado em requests reais de tool calling para o glm-5.2, com gpt-5.5 e claude-opus-4-8 rodando a mesma tarefa como referência. Em resumo: o GLM 5.2 usa a superfície da API da OpenAI, mas em alguns aspectos se comporta mais como o Claude do que como o GPT, e é o loop treinado para a OpenAI que tropeça.
O mesmo turno, de três formas
Mesmo prompt, mesmas duas ferramentas, três modelos:
GLM (glm-5.2) | OpenAI (gpt-5.5) | Anthropic (claude-opus-4-8) | |
|---|---|---|---|
| Superfície da API | OpenAI chat-completions | OpenAI chat-completions | Anthropic messages |
| Texto no turno da tool-call | preâmbulo em content (não nulo) | content é null | um bloco text antes do tool_use |
| Reasoning nesse turno | exposto: reasoning_content + reasoning_tokens | oculto; só reasoning_tokens no usage | só como bloco thinking, se você habilitar |
| Tool calls paralelas | sim, com index | sim | sim, múltiplos blocos tool_use |
| Sinal de conclusão | finish_reason: "tool_calls" | finish_reason: "tool_calls" | stop_reason: "tool_use" |
| Prefixo do id da tool-call | call_… | call_… | toolu_… |
São duas linhas que quebram os loops: o texto no turno da tool-call e o reasoning aparecendo nesse turno. O resto é tranquilamente entediante.
Texto vem junto com a tool call
O GLM 5.2 emite com frequência um preâmbulo curto no content do assistant junto com tool_calls, com finish_reason: "tool_calls". Não é erro e não é eventual.
Aqui está o mesmo turno nos três, reduzido à parte que difere:
// 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 */ } ]
A OpenAI deixa o content nulo; o GLM o preenche; a Anthropic sempre colocou um bloco text ali. Ou seja, o GLM pega o formato de fio da OpenAI com o hábito da Anthropic de narrar antes de agir, e quem é pego de surpresa é o loop escrito para a OpenAI. O ajuste é pequeno, mas você precisa fazê-lo de propósito. Pare de tratar um turno de tool-call como se não tivesse conteúdo:
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 o seu loop renderiza o content para o usuário como a resposta do assistant, agora você vai mostrar uma linha de “deixa eu verificar isso” antes de cada tool call. Decida se você quer isso. A questão é que a decisão é sua, e não algo que o silêncio do modelo faz por você.
Ele pensa em voz alta
O GLM 5.2 é um modelo de reasoning, e isso não para para usar tools. Um turno de tool call carrega o reasoning junto, e o GLM 5.2 expõe esse reasoning como texto. Numa resposta não-streaming a contabilidade de tokens deixa isso explícito:
"usage": {
"prompt_tokens": 224,
"completion_tokens": 68,
"completion_tokens_details": { "reasoning_tokens": 30 },
"total_tokens": 292
}
Quase metade da completion foi reasoning, numa requisição cuja saída visível são duas chamadas de função curtas. É nesta linha que os três modelos divergem. O GLM 5.2 te entrega o reasoning como reasoning_content mais a contagem de tokens. O OpenAI cobra reasoning_tokens no usage mas nunca mostra o texto. O Anthropic só mostra como blocos thinking, e só quando você liga o extended thinking. Dos três, o GLM 5.2 é o mais exposto por padrão.
Duas consequências. Primeiro, custo: você paga por esses tokens de reasoning nos turnos de tool call, e um loop de agente tem muitos turnos. O reasoning effort é o botão que mexe nesse número, como vimos em GLM 5.2: Reasoning Effort Is the Cost Lever. Conte os tokens de reasoning em cada turno, não só na resposta final.
Segundo, a ordem do streaming. Ao fazer o streaming da requisição, o GLM manda primeiro o reasoning, depois o texto do preâmbulo, e só então as tool calls:
reasoning_content (many deltas)
content (a few deltas)
tool_calls (id + name, then arguments)
Um parser escrito para o chat completions padrão do OpenAI não conhece o campo reasoning_content e vai ignorar essa rajada inicial sem reclamar. Em geral, sem problema. Vira problema se a sua UI mostra um estado de “thinking…” acionado pelo primeiro delta de content, porque o que chega primeiro na conexão é reasoning, não content, e o indicador nunca dispara.
Quanto custa um turno de tool call no GLM 5.2
O comportamento é metade da história; a conta é a outra metade, e um loop de agente roda o mesmo turno muitas vezes. Com um prefixo fixo (um system prompt de cerca de 2.000 tokens mais as definições de tools) e a mensagem do usuário variando a cada chamada, medido em dez turnos quentes:
| por turno de tool call quente | GLM glm-5.2 | OpenAI gpt-5.5 | Anthropic claude-opus-4-8 |
|---|---|---|---|
| Custo | $0.0009 | $0.0042 | $0.0051 |
| Latência (mediana) | 6.6s | 1.9s | 3.1s |
| Prompt em cache | ≈96% | ≈81% | ≈97% |
| Tokens de reasoning | ≈27 | 0 | 0 |
| Custo frio → quente | 3.4× | 2.8× | 4.9× |
O GLM 5.2 é o barato: cerca de 4,5× mais barato que o GPT-5.5 e 5,4× mais barato que o Opus por turno quente. Também é o lento, com latência de duas a três vezes e meia a dos outros, porque gasta tokens de reasoning em cada turno enquanto os outros dois não gastaram nenhum nesta tarefa. É o trade: o GLM troca custo por latência, e o reasoning effort é o botão que ajusta isso.
O cache é o que torna qualquer um deles viável num loop. O system prompt e as definições de tools são a maior parte de cada prompt e são idênticos a cada turno, então, uma vez que o prefixo está em cache, o turno fica 2,8× a 4,9× mais barato. Duas coisas determinam se você vê esse ganho. GLM e OpenAI cacheiam o prefixo automaticamente; o Anthropic só cacheia o que você marca com cache_control. E o cache do GLM aquece um instante depois, então uma tarefa de três passos pode pagar preço cheio enquanto uma de trinta passos roda em cache. A mecânica está em Open-Weight LLM Caching.
Quando usar o GLM 5.2 e como rodá-lo bem
Juntando as peças: na tabela, o GLM 5.2 é o modelo barato e também o lento, e ele raciocina a cada turno. Esse perfil indica onde ele vale a pena.
Onde ele encaixa: loops de agente longos e com vários passos, onde o custo pesa mais e dá pra aceitar alguns segundos por turno. Agentes de coding em background, CI e automação em batch, jobs que rodam sem supervisão. O raciocínio que o deixa lento é também o motivo de ele se sair bem em coding e planejamento de verdade, em vez de roteamento trivial. Depois do aquecimento, o cache reforça o argumento: uma tarefa de trinta passos amortiza o prefixo e fica barata, enquanto uma de três passos pode pagar o preço cheio e engolir a latência sem retorno. Então use o GLM 5.2 nos jobs longos, e mantenha um modelo mais rápido para as chamadas interativas e de tiro único, onde os seis segundos por turno se notam.
Como rodar o GLM 5.2 bem. Cinco hábitos deixam um loop pronto pro GLM sem sair da superfície da API da OpenAI:
- Trate um turno com tool call como podendo trazer
content. Não presuma que está vazio. - Espere
reasoning_contentno fio ereasoning_tokensnousage; reserve orçamento para os dois e use o controle de reasoning-effort para trocar qualidade por custo. - No streaming, não amarre o estado da UI no primeiro delta de content, porque o reasoning chega primeiro.
- Repita o
tool_call_idexatamente como veio; trate-o como opaco, nunca o parseie nem o regere. - Acumule os
argumentsdo streaming porindexaté a chamada fechar; não assuma uma contagem de chunks.
Duas coisas contra as quais você não precisa se defender: o GLM emite tool calls paralelas com um index como os outros, e o round-trip fecha normalmente. Acrescente o turno do assistant, acrescente uma mensagem tool por chamada com o resultado dela, e termina com finish_reason: "stop". De quebra, mantenha o prefixo cacheável estável byte a byte entre os turnos; o system prompt e as definições de tools são a maior parte de qualquer prompt, e é um prefixo estável que permite ao cache do GLM carregar o custo depois que ele aquece.
Nada disso é exótico. É a diferença entre “a request teve sucesso” e “o loop do agente está correto”, e no GLM essa diferença mora basicamente em duas suposições: que um turno com tool call é silencioso, e que ele não está pensando. Largue essas duas, mantenha o prefixo estável, e um único loop atende GLM, GPT e Claude do mesmo jeito, com o GLM fazendo isso por uma fração do custo onde latência não é o que você está otimizando.
Aviso
Os números de custo, latência e cache acima foram medidos em 2026-06-30 ao longo de dez turnos quentes de tool call por modelo, com glm-5.2, gpt-5.5 e claude-opus-4-8. O custo vem do usage reportado; a latência é a mediana de wall-clock e varia com carga e reasoning effort. O comportamento dos modelos e os preços mudam, então trate os números como indicativos e meça de novo contra o seu próprio tráfego antes de depender deles.