n4nAI

Function calling tutorial: from schema to executed result

Hands-on function calling tutorial: define JSON schemas, invoke a model via OpenAI-compatible API, execute the call locally, and return results to the model.

n4n Team3 min read616 words

Audio narration

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

This function calling tutorial schema to result shows how to take a JSON Schema, send it to an LLM, and run the returned call against real code. We build a working Python pipeline that talks to an OpenAI-compatible chat endpoint and executes the tool locally, then feeds the result back for a natural language answer.

Prerequisites

  • Python 3.10 or newer
  • openai Python package (pip install openai)
  • An API key for an OpenAI-compatible endpoint, exported as OPENAI_API_KEY. If you use a gateway, set OPENAI_BASE_URL to its OpenAI-compatible base URL.
  • jsonschema or pydantic for validation (optional but recommended)
  • Basic comfort with JSON Schema draft-07

Define the function schema

Function calling starts with a precise contract. The model sees a tools array where each entry is a function definition. The parameters field is a standard JSON Schema object. Be explicit: describe every property and mark required fields.

{
  "type": "function",
  "function": {
    "name": "get_weather",
    "description": "Get current temperature for a given location",
    "parameters": {
      "type": "object",
      "properties": {
        "location": {
          "type": "string",
          "description": "City and state, e.g. San Francisco, CA"
        },
        "unit": {
          "type": "string",
          "enum": ["celsius", "fahrenheit"],
          "default": "celsius"
        }
      },
      "required": ["location"]
    }
  }
}

A vague description breeds bad arguments. Spend time on the description strings; they are part of the prompt.

Send the schema to the model

We use the official openai client. Point it at any compatible endpoint. The tools parameter carries our schema; tool_choice="auto" lets the model decide.

import os
from openai import OpenAI

client = OpenAI(
    api_key=os.environ["OPENAI_API_KEY"],
    base_url=os.environ.get("OPENAI_BASE_URL", "https://api.openai.com/v1")
)

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

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.tool_calls)

Expected output (id truncated):

[ChatCompletionMessageToolCall(id='call_123', type='function', function=Function(arguments='{"location":"Paris","unit":"celsius"}', name='get_weather'))]

If msg.tool_calls is None, the model answered directly. In this function calling tutorial schema to result we focus on the tool path.

Parse and validate arguments

The model returns a JSON string inside arguments. Parse it, then validate. Never assume the shape is correct.

import json

def parse_tool_call(tool_call):
    if tool_call.function.name != "get_weather":
        raise ValueError(f"unexpected tool: {tool_call.function.name}")
    args = json.loads(tool_call.function.arguments)
    if "location" not in args:
        raise ValueError("missing required field: location")
    if args.get("unit") not in (None, "celsius", "fahrenheit"):
        raise ValueError("invalid unit")
    args.setdefault("unit", "celsius")
    return args

args = parse_tool_call(msg.tool_calls[0])
print(args)

Output:

{'location': 'Paris', 'unit': 'celsius'}

For production, replace the manual checks with a pydantic model. It gives clearer errors and serializes back to JSON easily.

Execute the function locally

The whole point is to run real code. Here is a mock that stands in for an HTTP weather API.

import random

def get_weather(location: str, unit: str) -> float:
    # Pretend we called an external service
    temp_c = random.uniform(10.0, 25.0)
    if unit == "fahrenheit":
        return temp_c * 9/5 + 32
    return temp_c

result = get_weather(**args)
print(f"{result:.1f} {args['unit']}")

Sample output:

18.3 celsius

Swap the mock for requests.get to your internal service. Keep the function side-effect free where possible; pass all context via arguments.

Return the result to the model

The model needs the tool result to compose a final answer. Append three messages: the original user turn, the assistant message containing tool_calls, and a role: "tool" message referencing the call id.

messages = [
    {"role": "user", "content": "What's the weather in Paris?"},
    msg,
    {
        "role": "tool",
        "tool_call_id": msg.tool_calls[0].id,
        "content": json.dumps({"temperature": result, "unit": args["unit"]})
    }
]

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

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

Expected final answer:

The current temperature in Paris is 18.3°C.

This closes the loop. The function calling tutorial schema to result is now a round trip.

Handle errors and fallback

Models hallucinate. Wrap execution so a bad call returns structured error instead of crashing the loop.

try:
    args = parse_tool_call(msg.tool_calls[0])
    result = get_weather(**args)
    tool_content = json.dumps({"temperature": result, "unit": args["unit"]})
except Exception as e:
    tool_content = json.dumps({"error": str(e)})

messages.append({
    "role": "tool",
    "tool_call_id": msg.tool_calls[0].id,
    "content": tool_content
})

If you route through a gateway that provides automatic fallback when a provider is rate-limited or degraded, the second create call will still succeed without code changes. n4n.ai does this on its OpenAI-compatible endpoint, but the client code stays identical.

Multiple tools and parallel calls

Real apps expose several functions. Add more entries to tools. The model can emit multiple tool_calls in one message. Execute them concurrently:

import concurrent.futures

def dispatch(tool_call):
    args = parse_tool_call(tool_call)
    return tool_call.id, json.dumps({"temperature": get_weather(**args)})

with concurrent.futures.ThreadPoolExecutor() as ex:
    results = list(ex.map(dispatch, msg.tool_calls))

for call_id, content in results:
    messages.append({"role": "tool", "tool_call_id": call_id, "content": content})

Always match each result to its tool_call_id. Mismatches cause API errors.

Production considerations

  • Schema caching: Build tools once at startup. Rebuilding per request wastes tokens and CPU.
  • Strict validation: Use pydantic v2 with model_validate_json for speed.
  • Streaming: Pass stream=True on the final call to render the answer token by token.
  • Metering: If your gateway does per-token usage metering, log resp.usage.total_tokens to track cost per tool loop.
  • Routing hints: Some gateways, including n4n.ai, honor client routing directives and forward provider cache-control hints. You can pin a model or reuse a cached prompt prefix without altering your schema code.

Function calling is not magic: it is a strict request/response protocol with JSON Schema as the contract. Get the schema right, validate every argument, and execute locally with the same discipline you apply to any API boundary.

Test the loop

A tiny test guards against schema drift:

def test_parse():
    tc = type("TC", (), {"function": type("F", (), {
        "name": "get_weather",
        "arguments": '{"location":"Berlin"}'
    })})()
    args = parse_tool_call(tc)
    assert args == {"location": "Berlin", "unit": "celsius"}

Run pytest -q. Green means your function calling tutorial schema to result pipeline is reproducible and safe to ship.

That’s the entire path from schema to executed result.

Tagsfunction-callingtutorialjson-schemaexecution

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 →