Skip to main content
Lesson 10 of 22 · Responses API: OpenAI Fundamentals

Function Calling: Connect AI to Your Systems

Lesson 10 of 22 Updated July 29, 2026 Developer
On this page
  1. Function Calling: Connect AI to Your Systems
  2. The Full Flow, Step by Step
  3. Letting the Model Choose Among Multiple Tools
  4. The Agent Loop
  5. Lock Down Arguments with Strict Mode
  6. Key Takeaways

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:

  1. You declare available functions along with their parameters
  2. The model analyzes the request and decides to call a function
  3. Your code executes the function and obtains the result
  4. 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_output and the call_id
  • The model can call multiple functions in parallel
  • Use an agent loop for multi-turn conversations involving functions
  • Enable strict: True to guarantee argument compliance