n4nAI

Writing your first function calling API request

Hands-on tutorial for your first function calling API request using the OpenAI-compatible Chat Completions API, with runnable Python code and expected outputs.

n4n Team4 min read788 words

Audio narration

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

Sending your first function calling api request is less about memorizing an endpoint and more about getting the message loop right. This tutorial builds a working example from scratch against the OpenAI-compatible Chat Completions API, showing how to define a tool, trigger a model call, execute the function, and feed the result back so the model can produce a final answer.

Prerequisites

  • Python 3.10 or newer.
  • The openai Python package: pip install openai>=1.0.0.
  • An API key from any provider that exposes the OpenAI-compatible /v1/chat/completions endpoint. If you need one URL that fronts many models, n4n.ai provides an OpenAI-compatible gateway covering 240+ models with automatic fallback when a provider is rate-limited.
  • A terminal and basic comfort with JSON and Python dictionaries.

No framework required. We use the raw SDK so you can see every field the protocol demands.

Step 1: Define the function schema

Function calling works by sending the model a JSON Schema description of functions it can call. The model does not execute code; it returns arguments shaped to that schema. The schema is passed under the tools parameter and must conform to a restricted JSON Schema dialect.

{
  "type": "function",
  "function": {
    "name": "get_weather",
    "description": "Get current temperature for a 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"]
    }
  }
}

The parameters block is a standard JSON Schema subset. Keep it strict: the model will follow required and enum closely. Avoid ambiguous descriptions—they translate directly into model behavior. If you later generate this from Python types (e.g., pydantic), you remove an entire class of drift bugs.

Step 2: Send the initial request

Configure the SDK with your base URL and key. Then call chat.completions.create with the tools argument. Use a model that supports function calling; most current ones do. The tool_choice="auto" directive lets the model decide whether to call a tool or answer directly.

import os
from openai import OpenAI

client = OpenAI(
    api_key=os.environ["API_KEY"],
    base_url="https://api.n4n.ai/v1",  # any OpenAI-compatible endpoint
)

response = client.chat.completions.create(
    model="openai/gpt-4o-mini",
    messages=[
        {"role": "user", "content": "What's the weather in Paris?"}
    ],
    tools=[{
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "Get current temperature for a location",
            "parameters": {
                "type": "object",
                "properties": {
                    "location": {"type": "string"},
                    "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}
                },
                "required": ["location"]
            }
        }
    }],
    tool_choice="auto",
)

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

Expected output (abbreviated):

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

The model returned tool_calls instead of text. That is the core of your first function calling api request: the API responded with a structured call, not prose. If content is not None and tool_calls is empty, the model chose to answer directly—handle both branches.

Step 3: Execute the function locally

You now act as the runtime. Parse the arguments and return a result. In production this hits your DB or an external API; here we stub it. Never trust the model to validate types—do it yourself.

import json

if msg.tool_calls:
    call = msg.tool_calls[0]
    try:
        args = json.loads(call.function.arguments)
    except json.JSONDecodeError:
        args = {}

    # Minimal validation
    if "location" not in args:
        raise ValueError("Model omitted required location")

    print("Model asked for:", args)

    def get_weather(location, unit="celsius"):
        # Pretend we looked it up
        return {"location": location, "temp": 21, "unit": unit}

    result = get_weather(**args)
    print("Function output:", result)

Checkpoint output:

Model asked for: {'location': 'Paris', 'unit': 'celsius'}
Function output: {'location': 'Paris', 'temp': 21, 'unit': 'celsius'}

Step 4: Feed the result back

The model needs the tool result to produce a final answer. Append the assistant message (with tool_calls) and a new message with role: "tool". The tool_call_id must match the call you executed. The content is a string—usually JSON, but any string the model can parse works.

followup = client.chat.completions.create(
    model="openai/gpt-4o-mini",
    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),
        },
    ],
    tools=[{"type": "function", "function": {
        "name": "get_weather",
        "description": "Get current temperature for a location",
        "parameters": {
            "type": "object",
            "properties": {
                "location": {"type": "string"},
                "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}
            },
            "required": ["location"]
        }
    }}],
)

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

Expected final output:

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

You have completed a full round trip. The first function calling api request triggered a tool, your code ran it, and the model synthesized the answer from the returned data.

Step 5: Handle the messy parts

Real systems deviate from the happy path. A few hard-won notes from shipping this in production:

  • Missing required args: If the model omits a required field, some providers error; others return a call with null. Validate args before calling your function and return a tool message with an error string if needed—the model can often recover.
  • Parallel calls: tool_calls is a list. Loop over it and execute each independently, then return one tool message per call with the correct tool_call_id.
  • Timeouts: Wrap external calls with asyncio.wait_for or a sync timeout. The model doesn’t know your DB is slow; a hung call blocks the whole turn.
  • Schema drift: Keep your local function signature and the JSON Schema in sync. Generate the schema from pydantic if you can; hand-written schemas rot.
  • Provider fallback: If you point at a gateway like n4n.ai, a degraded upstream gets retried automatically, but your tool_choice and cache-control hints are still forwarded—don’t assume the same model answered. Log the model field from the response.
  • Streaming: When streaming, tool_calls arrive in fragments. Accumulate function.arguments across chunks before parsing.

Why function calling beats prompt parsing

You could ask the model to emit JSON and parse it yourself. Don’t. Function calling gives you:

  1. Native required enforcement by the model training.
  2. A clean separation between conversation history and tool results.
  3. Provider-side optimizations—some run constrained decoding to guarantee valid arguments.

The cost is an extra round trip and stricter schema discipline. For any system beyond a demo, that trade is worth it.

Full runnable script

import os
import json
from openai import OpenAI

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

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

def get_weather(location, unit="celsius"):
    return {"location": location, "temp": 21, "unit": unit}

# 1. Initial request
resp = client.chat.completions.create(
    model="openai/gpt-4o-mini",
    messages=[{"role": "user", "content": "What's the weather in Paris?"}],
    tools=[TOOL],
)
assistant_msg = resp.choices[0].message

# 2. Execute
if not assistant_msg.tool_calls:
    raise SystemExit("Model didn't call a tool")

call = assistant_msg.tool_calls[0]
args = json.loads(call.function.arguments)
tool_result = get_weather(**args)

# 3. Follow-up
final = client.chat.completions.create(
    model="openai/gpt-4o-mini",
    messages=[
        {"role": "user", "content": "What's the weather in Paris?"},
        assistant_msg,
        {"role": "tool", "tool_call_id": call.id, "content": json.dumps(tool_result)},
    ],
    tools=[TOOL],
)
print(final.choices[0].message.content)

Run with API_KEY=sk-... python script.py. You should see the temperature sentence. That script is the minimal viable pattern; everything else is retry logic and schema management.

What you just built

You defined a tool, sent a first function calling api request, executed the function server-side, and closed the loop with a tool message. That pattern—model proposes, you dispose—is the backbone of every agentic integration. From here, add more tools, validate schemas with pydantic, batch parallel calls, and log the resolved model name for observability.

Tagsfunction-callingapi-requesttutorialbeginner

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 →