GLM 5.2 Function Calling: Tool Use, Parallel Calls, and Agentic Workflows
Jul 20, 2026

GLM 5.2 Function Calling: Tool Use, Parallel Calls, and Agentic Workflows

GLM 5.2 supports OpenAI-compatible function calling with parallel tool calls. Here is how to define tools, handle responses, build multi-step agents, and run real agentic loops.

Most language model APIs make you choose between a chatbot and a workflow engine. GLM 5.2 sidesteps that trade-off: its OpenAI-compatible function calling interface lets you hand the model a set of tools, then sit back while it decides which ones to invoke, in what order, and with what arguments—including calling multiple tools in a single turn. This tutorial walks through everything, from defining your first tool to running a full multi-step agentic loop that terminates when the model has no more work to do.

Prerequisites

  • A Z.ai API key (sign up at z.ai)
  • Python 3.9+ with the openai package installed (pip install openai)
  • Basic familiarity with the OpenAI chat completions API shape

GLM 5.2's function calling uses the same wire format as OpenAI, so any existing tool-calling code you have can be pointed at Z.ai's endpoint with two environment variable changes and zero refactoring.

Step 1: Set Up the Client

Because GLM 5.2 speaks the OpenAI protocol, the openai Python SDK works out of the box. Point it at Z.ai's base URL and provide your API key:

from openai import OpenAI

client = OpenAI(
    api_key="YOUR_Z_AI_KEY",
    base_url="https://api.z.ai/v1",
)

MODEL = "glm-5.2"

That is the only configuration required. Every snippet below uses this client and MODEL constant.

Step 2: Define Your Tools

Tools are described using JSON Schema inside a tools list. Each entry has type: "function" and a nested function object that specifies the name, a plain-English description the model reads at inference time, and a parameters schema that validates what the model is allowed to pass back.

tools = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": (
                "Returns current weather conditions for a given city. "
                "Use this when the user asks about weather or temperature."
            ),
            "parameters": {
                "type": "object",
                "properties": {
                    "city": {
                        "type": "string",
                        "description": "City name, e.g. 'Tokyo' or 'San Francisco'",
                    },
                    "units": {
                        "type": "string",
                        "enum": ["celsius", "fahrenheit"],
                        "description": "Temperature unit. Defaults to celsius.",
                    },
                },
                "required": ["city"],
            },
        },
    },
    {
        "type": "function",
        "function": {
            "name": "search_web",
            "description": "Fetches top search results for a query. Use for factual lookups.",
            "parameters": {
                "type": "object",
                "properties": {
                    "query": {
                        "type": "string",
                        "description": "The search query string.",
                    },
                    "num_results": {
                        "type": "integer",
                        "description": "Number of results to return. Default 5.",
                    },
                },
                "required": ["query"],
            },
        },
    },
]

Write descriptions the way you would write a docstring for a junior colleague. The model reads them verbatim to decide which tool fits each situation, so precision matters more than brevity.

Step 3: Make the Initial API Call

Pass the tools list and set tool_choice to control how the model approaches tool selection:

tool_choice valueBehavior
"auto"Model calls tools when useful; may respond directly
"required"Model must call at least one tool
{"type":"function","function":{"name":"X"}}Force a specific function
"none"Disable tools for this call

For most agent loops, "auto" is the right default:

messages = [
    {"role": "user", "content": "What is the weather in Tokyo and Berlin right now?"},
]

response = client.chat.completions.create(
    model=MODEL,
    messages=messages,
    tools=tools,
    tool_choice="auto",
)

message = response.choices[0].message
print(message)

If the model decides to call tools, message.content will be None or empty, and message.tool_calls will contain a list of call objects.

Step 4: Handle Tool Calls in the Response

Inspect message.tool_calls. Each item has three fields: id, function.name, and function.arguments (a JSON string, not a dict—always parse it):

import json

def run_tool(name: str, arguments: dict) -> str:
    """Dispatch to the real implementation and return a string result."""
    if name == "get_weather":
        city = arguments["city"]
        units = arguments.get("units", "celsius")
        # Replace with a real weather API call
        return json.dumps({"city": city, "temp": 22, "units": units, "condition": "sunny"})
    elif name == "search_web":
        query = arguments["query"]
        num = arguments.get("num_results", 5)
        # Replace with a real search API call
        return json.dumps({"query": query, "results": [f"Result {i} for {query}" for i in range(num)]})
    else:
        return json.dumps({"error": f"Unknown tool: {name}"})


# Add the assistant's message to history first
messages.append(message)

# Execute each tool call and append a tool-role message for each
if message.tool_calls:
    for tool_call in message.tool_calls:
        name = tool_call.function.name
        args = json.loads(tool_call.function.arguments)
        result = run_tool(name, args)

        messages.append({
            "role": "tool",
            "tool_call_id": tool_call.id,
            "content": result,
        })

Two things are mandatory here: the assistant message must be appended before the tool results, and each tool result message must reference the exact tool_call_id from the call that triggered it. The model uses these IDs to match results to calls, especially when parallel calls are involved.

Step 5: Parallel Tool Calls

Notice that the Tokyo/Berlin weather question above can be answered with two simultaneous calls. GLM 5.2 will emit both in a single response rather than asking for one, waiting, then asking for the other. Your loop already handles this correctly because it iterates over message.tool_calls—parallel calls are simply multiple entries in that list.

# The model may return something like:
# tool_calls = [
#   ToolCall(id="call_abc", function=Function(name="get_weather", arguments='{"city":"Tokyo"}')),
#   ToolCall(id="call_xyz", function=Function(name="get_weather", arguments='{"city":"Berlin"}')),
# ]

You can execute these in parallel using concurrent.futures.ThreadPoolExecutor if each tool call hits a slow external API:

from concurrent.futures import ThreadPoolExecutor

def execute_tool_call(tc):
    name = tc.function.name
    args = json.loads(tc.function.arguments)
    result = run_tool(name, args)
    return tc.id, result

if message.tool_calls:
    with ThreadPoolExecutor() as executor:
        futures = {executor.submit(execute_tool_call, tc): tc for tc in message.tool_calls}
        for future in futures:
            call_id, result = future.result()
            messages.append({
                "role": "tool",
                "tool_call_id": call_id,
                "content": result,
            })

This pattern cuts latency proportionally to the number of parallel calls—two 500 ms weather fetches become a single 500 ms wait instead of 1000 ms serial.

Step 6: Build a Complete Agentic Loop

A full agent loop runs until the model returns a response with no tool calls, meaning it has gathered everything it needs and is ready to answer the user:

import json
from concurrent.futures import ThreadPoolExecutor
from openai import OpenAI

client = OpenAI(api_key="YOUR_Z_AI_KEY", base_url="https://api.z.ai/v1")
MODEL = "glm-5.2"

def agent_loop(user_message: str, tools: list, max_iterations: int = 10) -> str:
    messages = [{"role": "user", "content": user_message}]

    for iteration in range(max_iterations):
        response = client.chat.completions.create(
            model=MODEL,
            messages=messages,
            tools=tools,
            tool_choice="auto",
        )
        message = response.choices[0].message
        messages.append(message)

        # No tool calls → model is done
        if not message.tool_calls:
            return message.content

        # Execute tool calls (parallel)
        def execute(tc):
            name = tc.function.name
            args = json.loads(tc.function.arguments)
            return tc.id, run_tool(name, args)

        with ThreadPoolExecutor() as ex:
            for call_id, result in ex.map(execute, message.tool_calls):
                messages.append({
                    "role": "tool",
                    "tool_call_id": call_id,
                    "content": result,
                })

    raise RuntimeError(f"Agent did not finish within {max_iterations} iterations")


answer = agent_loop(
    "Compare the current weather in Tokyo and Berlin, then search for the best time to visit each city.",
    tools=tools,
)
print(answer)

The max_iterations guard prevents runaway loops. In practice, GLM 5.2's 78% score on Terminal-Bench v2.1—a benchmark of real multi-step shell tasks—means it converges quickly and avoids unnecessary extra calls. The 1M token context window (1,048,576 tokens) means even deep agent histories with hundreds of tool-call/result pairs will not overflow and force a truncation mid-task.

Step 7: Structured Output from Tool Results

Tool results are plain strings in the protocol, but the model handles JSON naturally. Returning structured JSON from your tool implementations gives the model rich data to reason over:

# Instead of: return "The temperature is 22 degrees"
# Return:
return json.dumps({
    "city": "Tokyo",
    "temperature": 22,
    "units": "celsius",
    "humidity": 68,
    "condition": "partly cloudy",
    "wind_kph": 14,
})

For database queries or API responses that return large payloads, GLM 5.2's 1M context means you can return the full response rather than truncating it, letting the model pick out the relevant fields itself rather than requiring pre-filtering in your tool wrapper.

GLM 5.2 Specs Relevant to Agentic Use

PropertyValue
Context window1,048,576 tokens (1M)
Speed158 tokens/second
TTFT1.54 s
Terminal-Bench v2.178%
SWE-bench Pro62.1%
Input price$1.40 / M tokens
Output price$4.40 / M tokens
Cache hit price$0.26 / M tokens
Parallel tool callsYes
Tool choice modesauto, required, none, forced
Model IDglm-5.2
Base URLhttps://api.z.ai/v1

At 158 tokens per second, GLM 5.2 ranks third among frontier models by throughput on Artificial Analysis. In an agent loop, speed compounds: if each model turn takes 0.3–0.5 seconds instead of 1–2 seconds, a ten-step agent finishes in under five seconds rather than twenty. Combine that with the $1.40/M input price and caching at $0.26/M for repeated system prompts and tool schemas, and the cost per agent run stays predictable even for high-volume production workloads.

For a detailed breakdown of how to optimize spend on long agentic sessions, see GLM 5.2 pricing.

Common Issues

ProblemLikely CauseFix
tool_calls is None even with tools definedModel chose to answer directlyCheck your prompt; use tool_choice="required" if you always need a call
json.JSONDecodeError on function.argumentsModel produced malformed JSONAdd a try/except; log and skip the bad call; report the error back as a tool result
Tool result not matched to callMissing or wrong tool_call_idCopy tool_call.id exactly; do not generate your own
Loop never terminatesAgent keeps calling toolsAdd max_iterations; check if tool results contain errors the model is trying to fix
Context window exceededLong historyGLM 5.2's 1M context makes this rare; if it happens, summarize earlier tool results
Model ignores a toolPoor descriptionRewrite the description field to be more specific about when to use it

FAQ

Does GLM 5.2 function calling work with the OpenAI Python SDK?

Yes. Set base_url="https://api.z.ai/v1" and api_key to your Z.ai key, then use the SDK exactly as you would for OpenAI. No other changes are required. The same applies to TypeScript, cURL, and any HTTP client that can set a base URL.

Can the model call the same function multiple times in one response?

Yes. If the model needs to fetch weather for five cities, it can emit five calls to get_weather in a single tool_calls list. Execute them and return all five tool-role messages before the next model turn.

What happens if my tool raises an exception?

Wrap your tool implementation in a try/except and return a descriptive error string as the tool result. The model will read the error and decide whether to retry with different arguments, call a different tool, or explain to the user that it could not complete the task.

How do I prevent the model from calling tools I have not defined?

GLM 5.2 can only call functions that appear in the tools list you pass to the API. It cannot invent new function names. If you want to limit it further—for example, to exactly one function—use the forced tool_choice format: {"type": "function", "function": {"name": "your_function"}}.

Is there a latency cost to parallel tool calls?

The model itself does not add latency for returning multiple calls in one response. Your execution latency depends on how you run the tools: serial execution adds up the individual times, while parallel execution caps at the slowest individual call. For I/O-bound tools (HTTP requests, database queries), always run in parallel with ThreadPoolExecutor or asyncio.

How does the 1M context window affect agent design?

Most agent frameworks add truncation logic to avoid exceeding the context limit. With GLM 5.2's 1,048,576-token window, a conversation history with hundreds of tool-call/result pairs fits without truncation. This simplifies your agent code and ensures the model always has the full history available when reasoning about what to do next.

Can I use function calling with streaming?

Yes. Set stream=True in the create call. Tool call chunks arrive incrementally with delta.tool_calls; accumulate them until the stream ends, then dispatch and loop exactly as in the non-streaming case. The wire format matches OpenAI's streaming tool call protocol.

Next Steps

  • Read the GLM 5.2 overview for full benchmark context and architecture details
  • See GLM 5.2 pricing for how to estimate cost across different agent workloads
  • Explore OpenRouter as an alternative endpoint if you want a single API key for multiple models

Try GLM 5.2 — no API key needed: glm5.app/chat.

Sources

Commencez à utiliser GLM 5 dès aujourd'hui

Essayez GLM 5 gratuitement — raisonnement, codage, agents et génération d'images sur une seule plateforme.