OpenAI Function Calling Tool Use 9 min read

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 →