OpenAI Function Calling Guide 2025: Tool Use with GPT-4o
Function calling lets GPT-4o interact with your APIs, databases, and code by returning structured JSON instead of plain text. This guide covers everything from basic tool definitions to parallel calls, strict mode, streaming, and full agent loops — with complete Python examples.
What Is Function Calling?
OpenAI function calling (now called tool use in the API) is a mechanism where GPT-4o returns a structured JSON object specifying which function to call and with what arguments — rather than generating free-form text. Your application code receives this JSON, executes the actual function, and sends the result back to the model so it can continue the conversation with real data.
This pattern enables AI to reliably interact with external systems: REST APIs, SQL databases, calculators, file systems, or any custom Python function. The model never executes code directly — it only produces the JSON call specification. You control the execution boundary.
Conceptual flow
User: "What's the weather in Paris?"
1. GPT-4o returns: tool_calls: [{ name: "get_weather", args: {"city": "Paris"} }]
2. Your code runs: get_weather(city="Paris") → {"temp": 22, "condition": "sunny"}
3. You send back: tool result message with the JSON
4. GPT-4o replies: "It's 22°C and sunny in Paris right now." Basic Example: Define a Tool and Call It
Tools are described as JSON Schema objects inside the tools parameter. Each tool has a name, a description the model reads to decide when to use it, and a parameters schema defining the arguments:
basic_tool_call.py
import json
from openai import OpenAI
client = OpenAI()
tools = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get current weather for a city",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "City name, e.g. 'Paris'"
},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "Temperature unit"
}
},
"required": ["city"]
}
}
}
]
response = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": "What's the weather in Tokyo?"}],
tools=tools,
tool_choice="auto"
)
message = response.choices[0].message
# Check if model wants to call a tool
if message.tool_calls:
tool_call = message.tool_calls[0]
args = json.loads(tool_call.function.arguments)
print(f"Function: {tool_call.function.name}")
print(f"Args: {args}")
# → Function: get_weather
# → Args: {'city': 'Tokyo'}
After executing the function, send the result back as a tool role message and make a second API call to get the model's final reply:
Send result back to model
tool_result = {"temp": 18, "condition": "cloudy", "unit": "celsius"}
final_response = client.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "user", "content": "What's the weather in Tokyo?"},
message, # assistant's tool_calls message
{
"role": "tool",
"tool_call_id": tool_call.id,
"content": json.dumps(tool_result)
}
],
tools=tools
)
print(final_response.choices[0].message.content)
# → "The weather in Tokyo is currently 18°C and cloudy." Parallel Function Calling
GPT-4o can call multiple tools in a single response when the user's request requires several lookups simultaneously. The tool_calls list will contain multiple entries — iterate over all of them, execute each, and return all results before making the next API call:
parallel_calls.py
response = client.chat.completions.create(
model="gpt-4o",
messages=[{
"role": "user",
"content": "Compare weather in Paris and Tokyo, and also tell me the EUR/JPY rate."
}],
tools=tools # includes get_weather + get_fx_rate
)
message = response.choices[0].message
# GPT-4o may return 3 tool calls in one shot
follow_up_messages = [message]
for tool_call in message.tool_calls:
args = json.loads(tool_call.function.arguments)
result = dispatch_tool(tool_call.function.name, args)
follow_up_messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": json.dumps(result)
})
# Second call with all tool results
final = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": "..."}] + follow_up_messages,
tools=tools
) Parallel calls cut round-trip latency significantly — three sequential tool calls at 300 ms each becomes one 300 ms batch.
tool_choice Parameter
The tool_choice parameter controls whether and which tool the model must use:
tool_choice options
# "auto" — model decides (default)
# Use when: general assistant that sometimes needs tools
response = client.chat.completions.create(
model="gpt-4o",
messages=[...],
tools=tools,
tool_choice="auto"
)
# "required" — must call at least one tool
# Use when: you always need structured output
response = client.chat.completions.create(
model="gpt-4o",
messages=[...],
tools=tools,
tool_choice="required"
)
# Force a specific function
# Use when: deterministic routing in agent pipelines
response = client.chat.completions.create(
model="gpt-4o",
messages=[...],
tools=tools,
tool_choice={
"type": "function",
"function": {"name": "get_weather"}
}
)
# "none" — no tool calls, plain text reply only
response = client.chat.completions.create(
model="gpt-4o",
messages=[...],
tools=tools,
tool_choice="none"
) Structured Outputs with strict: true
Adding strict: true inside the function definition activates Structured Outputs — GPT-4o is guaranteed to produce arguments that exactly match your JSON schema. No extra keys, no missing required fields, no type coercions. This eliminates defensive parsing code:
strict_mode.py
tools = [
{
"type": "function",
"function": {
"name": "create_calendar_event",
"description": "Create a calendar event",
"strict": True, # Enforce exact schema adherence
"parameters": {
"type": "object",
"properties": {
"title": {"type": "string"},
"date": {
"type": "string",
"description": "ISO 8601 date string"
},
"duration_minutes": {"type": "integer"},
"attendees": {
"type": "array",
"items": {"type": "string"}
}
},
"required": ["title", "date", "duration_minutes", "attendees"],
"additionalProperties": False # Required for strict mode
}
}
}
]
# With strict=True, tool_call.function.arguments is ALWAYS valid JSON
# that matches the schema — no try/except needed for parsing
args = json.loads(tool_call.function.arguments)
event_title = args["title"] # guaranteed to exist and be a string
Strict mode requires additionalProperties: false at every object level and all properties listed in required. Optional fields must use anyOf: [type, null].
Complete End-to-End Example: Weather + Calculator Agent
A full agent loop with two tools, parallel calls, and conversation history management:
agent_loop.py
import json
from openai import OpenAI
client = OpenAI()
# --- Tool definitions ---
tools = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get current weather for a city",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string"}
},
"required": ["city"]
}
}
},
{
"type": "function",
"function": {
"name": "calculate",
"description": "Evaluate a math expression",
"parameters": {
"type": "object",
"properties": {
"expression": {
"type": "string",
"description": "Math expression, e.g. '(22 + 18) / 2'"
}
},
"required": ["expression"]
}
}
}
]
# --- Tool implementations ---
def get_weather(city: str) -> dict:
# Real code would call a weather API
mock_data = {
"Paris": {"temp": 22, "condition": "sunny"},
"Tokyo": {"temp": 18, "condition": "cloudy"},
}
return mock_data.get(city, {"temp": 20, "condition": "unknown"})
def calculate(expression: str) -> dict:
try:
result = eval(expression, {"__builtins__": {}})
return {"result": result}
except Exception as e:
return {"error": str(e)}
def dispatch(name: str, args: dict) -> dict:
if name == "get_weather":
return get_weather(**args)
elif name == "calculate":
return calculate(**args)
raise ValueError(f"Unknown tool: {name}")
# --- Agent loop ---
messages = [
{"role": "user", "content": "What's the average temperature between Paris and Tokyo?"}
]
while True:
response = client.chat.completions.create(
model="gpt-4o",
messages=messages,
tools=tools,
tool_choice="auto"
)
msg = response.choices[0].message
messages.append(msg)
if not msg.tool_calls:
# No more tool calls — final answer
print(msg.content)
break
# Execute all tool calls (may be parallel)
for tc in msg.tool_calls:
args = json.loads(tc.function.arguments)
result = dispatch(tc.function.name, args)
messages.append({
"role": "tool",
"tool_call_id": tc.id,
"content": json.dumps(result)
})
# Loop continues → model sees results and may call more tools or reply Streaming Function Calls
When stream=True, tool call data arrives in chunks. You need to accumulate the arguments string across delta events before parsing it as JSON:
streaming_tools.py
from collections import defaultdict
stream = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": "Weather in Berlin?"}],
tools=tools,
stream=True
)
# Accumulate tool call chunks
tool_calls_acc = defaultdict(lambda: {"name": "", "arguments": ""})
for chunk in stream:
delta = chunk.choices[0].delta
if delta.tool_calls:
for tc_chunk in delta.tool_calls:
idx = tc_chunk.index
if tc_chunk.function.name:
tool_calls_acc[idx]["name"] += tc_chunk.function.name
if tc_chunk.function.arguments:
tool_calls_acc[idx]["arguments"] += tc_chunk.function.arguments
# After stream ends, parse accumulated args
for idx, tc in tool_calls_acc.items():
args = json.loads(tc["arguments"])
result = dispatch(tc["name"], args)
print(f"Called {tc['name']}({args}) → {result}") Function Calling vs Assistants API vs Responses API
OpenAI offers three surfaces for tool use. Understanding when to use each saves significant refactoring:
| Surface | State managed by | Best for | Streaming |
|---|---|---|---|
| Chat Completions + tools | Your code (messages array) | Custom agents, full control, existing apps | Yes |
| Assistants API | OpenAI (threads) | Persistent conversations, file search, code interpreter | Yes |
| Responses API | OpenAI (built-in tools) | Web search, file search without custom code | Yes |
Chat Completions with tools= is the right choice when you need full control over tool execution, custom Python functions, and no OpenAI-managed state. Use the Assistants API when you want persistent threads and built-in file search/code interpreter. Use the Responses API for one-shot tasks with OpenAI-hosted tools.
Common Patterns
Production function calling patterns that combine well with the agent loop:
Pattern 1: RAG with function calls
# Expose vector search as a tool so the model retrieves only what it needs
tools = [{
"type": "function",
"function": {
"name": "search_knowledge_base",
"description": "Search internal docs for relevant passages",
"parameters": {
"type": "object",
"properties": {
"query": {"type": "string"},
"top_k": {"type": "integer", "default": 3}
},
"required": ["query"]
}
}
}]
# Model calls search_knowledge_base(query="...") when it needs context
# Avoids stuffing the entire corpus into the system prompt Pattern 2: Type-safe responses with Pydantic
from pydantic import BaseModel
import json
class WeatherArgs(BaseModel):
city: str
unit: str = "celsius"
# Parse and validate tool call arguments with Pydantic
args_raw = json.loads(tool_call.function.arguments)
args = WeatherArgs.model_validate(args_raw)
result = get_weather(city=args.city, unit=args.unit)
# ValidationError raised if schema doesn't match — fast failure Pattern 3: Max-iterations safety limit
MAX_ITERATIONS = 10
iterations = 0
while iterations < MAX_ITERATIONS:
response = client.chat.completions.create(...)
msg = response.choices[0].message
messages.append(msg)
iterations += 1
if not msg.tool_calls:
break # Final answer reached
for tc in msg.tool_calls:
# execute tools...
pass
else:
raise RuntimeError("Agent exceeded max iterations — possible loop") Monitor OpenAI API Status in Real Time
Building GPT-4o function calling pipelines? Prismix monitors OpenAI's API status in real time — get alerted before your tools break.
Monitor OpenAI Status →