Function Calling with OpenAI API
Function calling (also called tool use) enables language models to invoke external functions and integrate their outputs into responses. Instead of the model hallucinating answers, it can request data from a weather API, database query, calculation, or custom function. The model describes what it wants to do, you execute it, and the model uses the result. This capability transforms LLMs from passive text generators into active agents that interact with the real world, check facts, perform computations, and take actions on behalf of users.
Defining Tools and Schemas
You define tools as JSON schemas that describe what each function does, what parameters it accepts, and what type of data it returns. The OpenAI API uses these schemas to understand when and how to call functions:
from openai import OpenAI
client = OpenAI()
# Define a simple tool: get current weather
tools = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get the current weather for a location",
"parameters": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "The city and state, e.g. San Francisco, CA"
},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "The temperature unit (optional, defaults to fahrenheit)"
}
},
"required": ["location"]
}
}
}
]
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "user", "content": "What is the weather in Boston, MA?"}
],
tools=tools
)
print(response.choices[0].message)
The schema defines a function named get_weather with parameters location (required string) and unit (optional enum). The description helps the model understand when to use this tool.
Handling Tool Calls in Responses
When the model decides to call a function, it returns a tool_call message instead of text. You must detect this, execute the function, and pass the result back:
from openai import OpenAI
import json
client = OpenAI()
def get_weather(location, unit="fahrenheit"):
"""Simulated weather function."""
# In production, this would call a real weather API
weather_data = {
"Boston, MA": {"temp": 68, "condition": "Sunny"},
"New York, NY": {"temp": 72, "condition": "Cloudy"}
}
if location in weather_data:
data = weather_data[location]
temp_value = data["temp"]
# Convert if needed
if unit == "celsius":
temp_value = (temp_value - 32) * 5 / 9
return json.dumps({
"location": location,
"temperature": round(temp_value, 1),
"unit": unit,
"condition": data["condition"]
})
else:
return json.dumps({"error": "Location not found"})
tools = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get the current weather for a location",
"parameters": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "The city and state"
},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"]
}
},
"required": ["location"]
}
}
}
]
messages = [
{"role": "user", "content": "What is the weather in Boston and New York?"}
]
# First API call: model decides to use tools
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=messages,
tools=tools
)
# Check if model wants to call tools
if response.choices[0].message.tool_calls:
print("Model decided to call tools:")
# Add the assistant's response to messages
messages.append(response.choices[0].message)
# Process each tool call
for tool_call in response.choices[0].message.tool_calls:
func_name = tool_call.function.name
func_args = json.loads(tool_call.function.arguments)
print(f" Calling {func_name}({func_args})")
# Execute the function
if func_name == "get_weather":
result = get_weather(**func_args)
else:
result = json.dumps({"error": "Unknown function"})
# Add result to messages
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": result
})
# Second API call: model uses tool results to generate final response
final_response = client.chat.completions.create(
model="gpt-4o-mini",
messages=messages,
tools=tools
)
print("\nFinal response:")
print(final_response.choices[0].message.content)
else:
print("Model did not call any tools")
print(response.choices[0].message.content)
The workflow is: (1) send user message with tools defined, (2) model returns tool_calls, (3) you execute each function and collect results, (4) append results as tool role messages, (5) send messages back to the API so the model can synthesize a final response using the data. This back-and-forth loop allows the model to gather information before answering.
Building Multi-Tool Agents
Agents can call multiple tools in sequence and reason about which tool to use next. Here is a simple agent loop:
from openai import OpenAI
import json
client = OpenAI()
def get_stock_price(ticker):
"""Simulated stock price lookup."""
prices = {"AAPL": 180.25, "MSFT": 420.50, "GOOG": 140.75}
if ticker in prices:
return json.dumps({"ticker": ticker, "price": prices[ticker]})
return json.dumps({"error": f"Ticker {ticker} not found"})
def calculate_portfolio_value(holdings):
"""Calculate total portfolio value from holdings dict."""
total = sum(holdings.values())
return json.dumps({"total_value": total})
tools = [
{
"type": "function",
"function": {
"name": "get_stock_price",
"description": "Get the current price of a stock by ticker",
"parameters": {
"type": "object",
"properties": {
"ticker": {"type": "string"}
},
"required": ["ticker"]
}
}
},
{
"type": "function",
"function": {
"name": "calculate_portfolio_value",
"description": "Calculate total portfolio value",
"parameters": {
"type": "object",
"properties": {
"holdings": {
"type": "object",
"description": "Dict of stock_ticker: value_in_dollars"
}
},
"required": ["holdings"]
}
}
}
]
messages = [
{"role": "user", "content": "I own 10 shares of AAPL and 5 shares of MSFT. What is my portfolio worth?"}
]
# Agent loop: keep calling until no more tools are needed
max_iterations = 10
for i in range(max_iterations):
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=messages,
tools=tools
)
if response.choices[0].message.tool_calls:
messages.append(response.choices[0].message)
for tool_call in response.choices[0].message.tool_calls:
func_name = tool_call.function.name
func_args = json.loads(tool_call.function.arguments)
if func_name == "get_stock_price":
result = get_stock_price(func_args["ticker"])
elif func_name == "calculate_portfolio_value":
result = calculate_portfolio_value(func_args["holdings"])
else:
result = json.dumps({"error": "Unknown function"})
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": result
})
else:
# No more tools to call; break the loop
break
print("Final answer:")
print(response.choices[0].message.content)
The loop continues until the model decides no more tools are needed. It can call get_stock_price multiple times to fetch data, then call calculate_portfolio_value to synthesize a final answer.
Best Practices for Tool Design
Design tools with a single, clear responsibility. A tool that does too much ("analyze_everything") confuses the model; instead, break it into focused tools ("get_price", "calculate_return", "check_portfolio_limit"). Always return results as JSON strings; the model parses them. Include error messages in results (e.g., "ticker not found") so the model knows what went wrong and can adjust.
Validate tool parameters before execution. Never blindly execute user-provided arguments; a malicious user could request delete_all_files() if you exposed that as a tool. Always validate that parameters are within safe ranges and match expected types.
Key Takeaways
- Define tools as JSON schemas describing function names, descriptions, parameters, and return types.
- Send tools with the initial API request using the
toolsparameter. - Check if the response contains
tool_calls; if so, execute each function and return results astoolrole messages. - Build agent loops that repeatedly call the API with tool results until the model generates a final response.
- Design tools with single responsibilities; always validate parameters and return results as JSON.
Frequently Asked Questions
Can the model refuse to call a tool?
Yes. If the model determines that no tool is necessary, it will return a regular text response without tool_calls. Always check if response.choices[0].message.tool_calls before attempting to iterate.
What if a tool call has invalid parameters?
Catch the error, format it as a JSON error message, and append it as a tool role message. The model sees the error and can try again with corrected parameters or adapt its approach.
Can I force the model to use a specific tool?
Yes, using tool_choice="get_weather" in the API request. This is useful when you know which tool the model should use. Omit tool_choice to let the model decide.
Do tool definitions consume tokens?
Yes, every tool definition is included in the API request as context. For efficiency, define only the tools relevant to the current task. Remove unused tools to reduce token consumption and cost.