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
openaipackage 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 value | Behavior |
|---|---|
"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
| Property | Value |
|---|---|
| Context window | 1,048,576 tokens (1M) |
| Speed | 158 tokens/second |
| TTFT | 1.54 s |
| Terminal-Bench v2.1 | 78% |
| SWE-bench Pro | 62.1% |
| Input price | $1.40 / M tokens |
| Output price | $4.40 / M tokens |
| Cache hit price | $0.26 / M tokens |
| Parallel tool calls | Yes |
| Tool choice modes | auto, required, none, forced |
| Model ID | glm-5.2 |
| Base URL | https://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
| Problem | Likely Cause | Fix |
|---|---|---|
tool_calls is None even with tools defined | Model chose to answer directly | Check your prompt; use tool_choice="required" if you always need a call |
json.JSONDecodeError on function.arguments | Model produced malformed JSON | Add a try/except; log and skip the bad call; report the error back as a tool result |
| Tool result not matched to call | Missing or wrong tool_call_id | Copy tool_call.id exactly; do not generate your own |
| Loop never terminates | Agent keeps calling tools | Add max_iterations; check if tool results contain errors the model is trying to fix |
| Context window exceeded | Long history | GLM 5.2's 1M context makes this rare; if it happens, summarize earlier tool results |
| Model ignores a tool | Poor description | Rewrite 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.

