Function Calling: Connect AI to Your Systems
On this page
Function Calling: Connect AI to Your Systems
Function calling lets the model invoke functions you define. Keep one rule front and center, because everything else follows from it: the model does not execute them. It tells you which function to call and with what arguments; your code performs the execution and returns the result. Your database, credentials, and business logic never leave your infrastructure.
The Full Flow, Step by Step
The exchange happens in four stages:
- You declare available functions along with their parameters
- The model analyzes the request and decides to call a function
- Your code executes the function and obtains the result
- You send the result back to the model so it can formulate its response
The declaration deserves special care: the function description and each parameter’s description are the only documentation the model has to make its decision. Vague descriptions lead to approximate calls. Also note how to close the loop—the result goes back with the call_id you received, which acts as an acknowledgment and lets the API match the response to the request.
from openai import OpenAI
import json
client = OpenAI()
# Step 1: Declare available functions
tools = [{
"type": "function",
"name": "get_meteo",
"description": "Get the current weather for a city",
"parameters": {
"type": "object",
"properties": {
"ville": {
"type": "string",
"description": "The name of the city"
},
"unite": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "Temperature unit"
}
},
"required": ["ville"]
}
}]
# Step 2: The model decides to call the function
response = client.responses.create(
model="gpt-5.6-terra",
input="What's the weather like in Marseille?",
tools=tools
)
# Step 3: Execute the function
for item in response.output:
if item.type == "function_call":
print(f"Function: {item.name}")
print(f"Arguments: {item.arguments}")
args = json.loads(item.arguments)
# Your actual implementation
resultat_meteo = {"temperature": 22, "condition": "sunny"}
# Step 4: Send the result back to the model
final = client.responses.create(
model="gpt-5.6-terra",
input=[
{"role": "user", "content": "What's the weather like in Marseille?"},
{
"type": "function_call_output",
"call_id": item.call_id,
"output": json.dumps(resultat_meteo)
}
],
tools=tools
)
print(f"Response: {final.output_text}")
# Result:
# Function: get_meteo
# Arguments: {"ville": "Marseille"}
# Response: It is currently 22 C in Marseille with sunny weather.
Letting the Model Choose Among Multiple Tools
The real value shows up when you expose an entire catalog of capabilities. In an e-commerce assistant, you would declare product search, stock verification, and order placement, then let the model arbitrate. Faced with “Is the XPS gaming laptop available?”, it won’t run a search but will query stock directly: your function descriptions, not a hard-coded rule, drove that choice.
tools = [
{
"type": "function",
"name": "rechercher_produit",
"description": "Search for a product in the catalog",
"parameters": {
"type": "object",
"properties": {
"requête": {"type": "string", "description": "Search terms"},
"categorie": {"type": "string", "description": "Category (optional)"}
},
"required": ["requête"]
}
},
{
"type": "function",
"name": "verifier_stock",
"description": "Check product availability",
"parameters": {
"type": "object",
"properties": {
"produit_id": {"type": "string", "description": "Product ID"}
},
"required": ["produit_id"]
}
},
{
"type": "function",
"name": "passer_commande",
"description": "Place an order for a product",
"parameters": {
"type": "object",
"properties": {
"produit_id": {"type": "string"},
"quantite": {"type": "integer", "description": "Number of units"}
},
"required": ["produit_id", "quantite"]
}
}
]
# The model automatically chooses the right function
response = client.responses.create(
model="gpt-5.6-terra",
input="Is the XPS gaming laptop available?",
tools=tools
)
for item in response.output:
if item.type == "function_call":
print(f"Function chosen: {item.name}")
print(f"Arguments: {item.arguments}")
# Result: Function chosen: verifier_stock
# Arguments: {"produit_id": "xps-gaming"}
A single request can also trigger multiple calls at once. Comparing the weather in two cities produces two independent calls: your code must therefore iterate through response.output and handle all function_call items, never just the first one. This is also an opportunity to execute these calls in parallel and cut latency in half.
response = client.responses.create(
model="gpt-5.6-terra",
input="Compare the weather between Paris and Lyon.",
tools=[{
"type": "function",
"name": "get_meteo",
"description": "Get the weather for a city",
"parameters": {
"type": "object",
"properties": {
"ville": {"type": "string"}
},
"required": ["ville"]
}
}]
)
# The model makes two calls in parallel
function_calls = [item for item in response.output if item.type == "function_call"]
print(f"Number of calls: {len(function_calls)}")
for call in function_calls:
args = json.loads(call.arguments)
print(f" - get_meteo({args['ville']})")
# Result:
# Number of calls: 2
# - get_meteo(Paris)
# - get_meteo(Lyon)
The Agent Loop
A single round trip is rarely enough: the result of one function often leads the model to call another. So you wrap the exchange in a loop that runs as long as the response contains function calls and stops as soon as it doesn’t—a sign that the model has formulated its final answer. This is the skeleton of every agent, and you will see it under various guises in all agent frameworks.
def executer_fonction(nom: str, arguments: str) -> str:
"""Execute a function and return the result."""
args = json.loads(arguments)
if nom == "get_meteo":
# Simulation — replace with your real API
return json.dumps({"temp": 18, "condition": "cloudy"})
elif nom == "get_heure":
return json.dumps({"heure": "14:30", "timezone": "CET"})
return json.dumps({"error": "Unknown function"})
def agent_loop(question: str, tools: list) -> str:
"""Agent loop: call functions until the final response."""
input_messages = [{"role": "user", "content": question}]
while True:
response = client.responses.create(
model="gpt-5.6-terra",
input=input_messages,
tools=tools
)
# Check for function calls
function_calls = [i for i in response.output if i.type == "function_call"]
if not function_calls:
# No function calls = final response
return response.output_text
# Execute each function and add results
for call in function_calls:
resultat = executer_fonction(call.name, call.arguments)
input_messages.append({
"type": "function_call_output",
"call_id": call.call_id,
"output": resultat
})
# Usage
reponse = agent_loop("What's the weather like in Paris?", tools)
print(reponse)
A word of caution on this pattern: the while True loop has no guardrails. In production, add a maximum iteration counter; otherwise, a model that indefinitely requests the same tool will burn through your budget without ever concluding.
Lock Down Arguments with Strict Mode
Strict mode applies the same guarantee to function parameters that Structured Output applies to responses: arguments strictly adhere to the declared schema. For a function that creates a user, this prevents you from receiving an age as a string, or an extra field your code wasn’t expecting.
tools = [{
"type": "function",
"name": "creer_utilisateur",
"description": "Create a new user",
"strict": True,
"parameters": {
"type": "object",
"properties": {
"nom": {"type": "string"},
"email": {"type": "string"},
"age": {"type": "integer"}
},
"required": ["nom", "email", "age"],
"additionalProperties": False
}
}]
Key Takeaways
- Function calling connects the model to your systems without exposing your data
- The model decides which function to call; your code executes it
- Return the result using
function_call_outputand thecall_id - The model can call multiple functions in parallel
- Use an agent loop for multi-turn conversations involving functions
- Enable
strict: Trueto guarantee argument compliance