Define Tools in JSON
On this page
From theory to practice
You now understand the concept of function calling and the structure of JSON schemas. It’s time to move to concrete implementation. In this lesson, you will learn how to build a complete request with tool definitions and handle the model’s response.
Request structure with tools
Here is a complete request to the Grok API with a defined tool:
from openai import OpenAI
client = OpenAI(
api_key="your-api-key",
base_url="https://api.x.ai/v1"
)
tools = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get the current weather for a city",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "City name"
},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"default": "celsius"
}
},
"required": ["city"]
}
}
}
]
response = client.chat.completions.create(
model="grok-4.5",
messages=[
{"role": "user", "content": "What's the weather like in Paris?"}
],
tools=tools
)
Note the nested structure: the tools object contains an array, where each element has a type and a function object with the details.
Implementation key figures
Handle the tool_call response
When the model decides to call a function, the response contains a tool_calls array:
message = response.choices[0].message
if message.tool_calls:
for tool_call in message.tool_calls:
print(f"Function: {tool_call.function.name}")
print(f"Arguments: {tool_call.function.arguments}")
print(f"ID: {tool_call.id}")
The tool_call.function.arguments field is a JSON string (not a Python object). You must parse it:
import json
args = json.loads(tool_call.function.arguments)
city = args["city"]
unit = args.get("unit", "celsius")
Execute and return the result
Once the function is executed, you send the result back to the model in a second call:
import json
# 1. Execute the function
weather_data = get_weather(city="Paris", unit="celsius")
# 2. Build the messages with the result
messages = [
{"role": "user", "content": "What's the weather like in Paris?"},
message, # The model response with tool_calls
{
"role": "tool",
"tool_call_id": tool_call.id,
"content": json.dumps(weather_data)
}
]
# 3. Call the model again with the result
final_response = client.chat.completions.create(
model="grok-4.5",
messages=messages,
tools=tools
)
print(final_response.choices[0].message.content)
The message with role tool must contain:
tool_call_id: the unique identifier of the tool_call (to link it to the request)content: the result as a JSON string
Define multiple tools
You can declare as many tools as needed in the tools array:
tools = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get the current weather for a city",
"parameters": { ... }
}
},
{
"type": "function",
"function": {
"name": "search_restaurants",
"description": "Search for restaurants by city and cuisine type",
"parameters": { ... }
}
},
{
"type": "function",
"function": {
"name": "book_table",
"description": "Book a table at a restaurant",
"parameters": { ... }
}
}
]
The model will automatically choose the relevant tool(s) based on the user request.
Check finish_reason
The finish_reason field in the response indicates whether the model finished its response or is waiting for a tool result:
finish_reason = response.choices[0].finish_reason
if finish_reason == "tool_calls":
# The model is waiting for the result of one or more tools
handle_tool_calls(response)
elif finish_reason == "stop":
# The model has finished its response
print(response.choices[0].message.content)
Key takeaways
- The request contains
messagesandtools(definitions) - tool_call arguments are a JSON string to parse
- Send the result in a message with role
toolincluding thetool_call_id - Check
finish_reasonto know if the model is waiting for a result - You can declare multiple tools—the model chooses automatically