n4nAI

OpenAI function calling explained with a real example

A hands-on OpenAI function calling example explained step by step. Build a weather assistant with the API, including runnable code and expected outputs.

n4n Team3 min read682 words

Audio narration

Coming soon — every post will get a voice note here.

OpenAI function calling lets you give the model structured tools it can invoke, turning free-form text into typed API calls. In this openai function calling example explained, we’ll build a weather assistant that queries a mock API and returns parsed results. You’ll see exact request shapes and the model’s responses at each step.

Prerequisites

  • Python 3.10 or newer
  • openai Python package (v1.0+)
  • An API key from OpenAI or any OpenAI-compatible endpoint
  • A terminal and a code editor

Set the key in your environment:

export OPENAI_API_KEY="sk-..."

Install the SDK:

pip install openai

Step 1: Define the function schema

The model never calls your code directly. You give it a JSON Schema describing the function, its parameters, and types. Here is a minimal get_weather tool:

{
  "type": "function",
  "function": {
    "name": "get_weather",
    "description": "Get current temperature for a city",
    "parameters": {
      "type": "object",
      "properties": {
        "city": {
          "type": "string",
          "description": "City name, e.g. 'Tokyo'"
        },
        "unit": {
          "type": "string",
          "enum": ["celsius", "fahrenheit"],
          "default": "celsius"
        }
      },
      "required": ["city"]
    }
  }
}

The schema is strict. If you omit required, the model may skip the argument and your code will error. The description fields are not cosmetic—the model reads them to decide whether to call the function and how to fill arguments. A vague description yields vague calls.

Step 2: Send the first request with tools

We pass the schema in the tools parameter and ask a question that requires it. tool_choice="auto" lets the model decide; you can also force a specific function with tool_choice={"type": "function", "function": {"name": "get_weather"}}.

from openai import OpenAI
import os, json

client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])

tools = [{
    "type": "function",
    "function": {
        "name": "get_weather",
        "description": "Get current temperature for a city",
        "parameters": {
            "type": "object",
            "properties": {
                "city": {"type": "string"},
                "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}
            },
            "required": ["city"]
        }
    }
}]

resp = client.chat.completions.create(
    model="gpt-4o-mini",
    messages=[{"role": "user", "content": "What's the weather in Paris?"}],
    tools=tools,
    tool_choice="auto"
)

msg = resp.choices[0].message
print(msg)

Expected output (abbreviated):

ChatCompletionMessage(content=None, tool_calls=[ToolCall(id='call_abc', type='function', function=Function(name='get_weather', arguments='{"city": "Paris", "unit": "celsius"}'))], role='assistant')

The model returned no text. It decided to call get_weather with parsed arguments. That’s the core of this openai function calling example explained: the LLM acts as a structured argument generator.

Step 3: Execute the function locally

You own the execution. Mock the API and validate inputs before trusting them:

def get_weather(city: str, unit: str = "celsius") -> dict:
    if not isinstance(city, str) or not city.strip():
        raise ValueError("city must be a non-empty string")
    fake_db = {"paris": 21, "tokyo": 28}
    temp = fake_db.get(city.lower(), 20)
    return {"city": city, "temperature": temp, "unit": unit}

Parse the arguments the model sent:

import json

if msg.tool_calls:
    call = msg.tool_calls[0]
    args = json.loads(call.function.arguments)
    result = get_weather(**args)
    print(result)

Output:

{'city': 'Paris', 'temperature': 21, 'unit': 'celsius'}

Step 4: Feed the result back

You must append two messages: the assistant’s tool_calls message (verbatim) and a tool message carrying the result and the matching tool_call_id. Order matters—the assistant message must precede the tool message.

messages = [
    {"role": "user", "content": "What's the weather in Paris?"},
    msg,  # assistant message with tool_calls
    {
        "role": "tool",
        "tool_call_id": call.id,
        "content": json.dumps(result)
    }
]

followup = client.chat.completions.create(
    model="gpt-4o-mini",
    messages=messages,
    tools=tools
)

print(followup.choices[0].message.content)

Expected final output:

The current temperature in Paris is 21°C (celsius).

The model synthesized the tool result into a natural language answer. Our openai function calling example explained so far covers the single-call happy path.

Step 5: Handle multiple calls and loops

Production code needs a loop. The model can request several tools in one turn. Iterate over msg.tool_calls:

def run_conversation(user_input: str):
    messages = [{"role": "user", "content": user_input}]
    while True:
        resp = client.chat.completions.create(
            model="gpt-4o-mini",
            messages=messages,
            tools=tools
        )
        msg = resp.choices[0].message
        if not msg.tool_calls:
            return msg.content
        messages.append(msg)
        for call in msg.tool_calls:
            args = json.loads(call.function.arguments)
            try:
                res = get_weather(**args)
            except Exception as e:
                res = {"error": str(e)}
            messages.append({
                "role": "tool",
                "tool_call_id": call.id,
                "content": json.dumps(res)
            })

This loop terminates when the model stops emitting tool_calls. It’s the skeleton every function-calling agent needs.

Why not just prompt for JSON?

You could ask the model to output {"city": "Paris"} and parse it. That breaks constantly: the model adds markdown fences, commentary, or omits a comma. Function calling moves schema enforcement to the API boundary. The arguments arrive as a string you json.loads without regex gymnastics. You also keep the freedom to swap models—the same tools array works across any chat model that supports the feature.

Designing descriptions that work

The description on the function and each parameter is the only context the model has. “Get weather” is weaker than “Get current temperature for a city using simulated data; returns integer degrees.” Add examples inside parameter descriptions: "city": "e.g. 'Tokyo' or 'New York'". In testing, better descriptions cut spurious calls by more than half.

Common pitfalls

Forgetting tool_call_id

If you drop tool_call_id from the tool message, the API rejects the request. The ID binds the result to the specific call.

Trusting arguments blindly

The model can send "city": 123. Validate with pydantic or jsonschema before calling your backend.

Streaming

With stream=True, tool_calls arrive in fragments. You must accumulate delta.tool_calls and reconstruct the arguments string before parsing.

# Pseudo-pattern for streaming
tool_buffer = {}
for chunk in stream:
    for tc in chunk.choices[0].delta.tool_calls or []:
        tool_buffer.setdefault(tc.index, {"id": tc.id, "name": "", "args": ""})
        if tc.id: tool_buffer[tc.index]["id"] = tc.id
        if tc.function.name: tool_buffer[tc.index]["name"] += tc.function.name
        if tc.function.arguments: tool_buffer[tc.index]["args"] += tc.function.arguments

Hidden state

The assistant message with tool_calls must be sent back exactly as received. If you rebuild it from scratch, the id and type fields must match or the API errors.

Using an OpenAI-compatible gateway

If you point the client at n4n.ai’s OpenAI-compatible endpoint, the same code accesses 240+ models and gets automatic fallback when a provider is rate-limited. Set base_url="https://api.n4n.ai/v1" and keep your existing tools schema. The gateway forwards provider cache-control hints, so repeated schemas cost fewer tokens.

client = OpenAI(
    api_key=os.environ["N4N_API_KEY"],
    base_url="https://api.n4n.ai/v1"
)

No other changes required. The openai function calling example explained above runs unchanged across providers.

Wrapping up

Function calling is not magic: you expose a schema, the model emits arguments, you execute, and you return data. The openai function calling example explained here is the minimal viable loop. Build error handling and validation around it, and you have a robust tool-using system that degrades gracefully instead of hallucinating API requests.

Tagsopenaifunction-callingtutorialapi

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All function calling fundamentals posts →