n4nAI

Python function calling with n4n.ai's chat completions API

Hands-on Python tutorial for function calling with the n4n.ai API: define tools, parse model responses, execute local functions, and handle errors.

n4n Team4 min read849 words

Audio narration

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

Function calling turns an LLM from a text generator into a controller for your code. This tutorial walks through python function calling n4n.ai api using the OpenAI-compatible chat completions endpoint, so you can wire model-selected tools into real Python functions without rewriting your stack. You will define a tool, send a request, parse the model’s call, execute local code, and feed the result back for a final answer.

Prerequisites

  • Python 3.10 or newer
  • openai Python SDK v1.0 or later
  • A gateway API key exported as N4N_API_KEY
  • python-dotenv if you load env vars from a file
pip install openai python-dotenv

Create a .env file in your project root:

echo "N4N_API_KEY=sk-your-key" > .env

You should already know basic Python and the shape of a chat completion request. No prior experience with function calling is required.

Configure the OpenAI-compatible client

The gateway exposes a single OpenAI-compatible base URL. You point the standard OpenAI client at it and keep the rest of your code unchanged.

from openai import OpenAI
from dotenv import load_dotenv
import os

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

The chat.completions.create method behaves exactly as the OpenAI SDK documents. The only difference is that the model field accepts any of the 240+ models routed by the gateway, and you can pass provider-specific hints through the same parameters.

Define a tool schema

Tools are declared as JSON Schema objects inside the tools parameter. The model sees the name, description, and parameter definitions, but never your implementation. Write tight descriptions—they directly affect whether the model calls the tool correctly.

tools = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "Fetch current temperature for a given city",
            "parameters": {
                "type": "object",
                "properties": {
                    "city": {
                        "type": "string",
                        "description": "City name, e.g. 'Berlin' or 'Paris'",
                    }
                },
                "required": ["city"],
            },
        },
    }
]

Avoid ambiguous parameter names. If a tool needs units, encode them as an enum rather than free text.

Send the first request

Set tool_choice="auto" to let the model decide whether to call a tool. Use a fast model for the demo loop.

response = client.chat.completions.create(
    model="gpt-3.5-turbo",
    messages=[{"role": "user", "content": "What's the temperature in Paris?"}],
    tools=tools,
    tool_choice="auto",
)
message = response.choices[0].message
print(message)

Expected checkpoint output (abridged):

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

When content is None and tool_calls is populated, the model is asking your code to run something. If the model instead returns content with text, it chose not to use a tool.

Parse and dispatch

Extract the function name and arguments, then map to a local Python callable. Never use eval on the arguments—always json.loads.

import json

def get_weather(city: str) -> str:
    # Stub: replace with a real HTTP request to a weather API
    return f"12°C and overcast in {city}"

if message.tool_calls:
    for call in message.tool_calls:
        if call.function.name == "get_weather":
            args = json.loads(call.function.arguments)
            result = get_weather(args["city"])
            print("TOOL RESULT:", result)

Output:

TOOL RESULT: 12°C and overcast in Paris

In production, validate args against your own schema before calling the function. Models occasionally omit required fields or emit malformed JSON.

Feed the result back

The model needs the tool result to produce a natural language answer. Append the assistant message (which contains tool_calls) and a role: "tool" message carrying the result and the matching tool_call_id.

follow_up = client.chat.completions.create(
    model="gpt-3.5-turbo",
    messages=[
        {"role": "user", "content": "What's the temperature in Paris?"},
        message,
        {
            "role": "tool",
            "tool_call_id": message.tool_calls[0].id,
            "content": result,
        },
    ],
    tools=tools,
)
print(follow_up.choices[0].message.content)

Expected final output:

The temperature in Paris is currently 12°C and overcast.

The tool role is mandatory for the second turn. Omitting it raises a validation error from the API.

Build a reusable conversation loop

Wrap the exchange in a loop so the model can chain multiple tools across turns. Stop when the response contains no tool_calls.

def run_conversation(user_prompt: str, model: str = "gpt-3.5-turbo", max_steps: int = 5):
    msgs = [{"role": "user", "content": user_prompt}]
    for _ in range(max_steps):
        resp = client.chat.completions.create(
            model=model, messages=msgs, tools=tools, tool_choice="auto"
        )
        msg = resp.choices[0].message
        msgs.append(msg)
        if not msg.tool_calls:
            return msg.content
        for call in msg.tool_calls:
            try:
                args = json.loads(call.function.arguments)
            except json.JSONDecodeError:
                args = {}
            if call.function.name == "get_weather":
                tool_out = get_weather(args.get("city", "unknown"))
            else:
                tool_out = "unknown tool"
            msgs.append({
                "role": "tool",
                "tool_call_id": call.id,
                "content": tool_out,
            })
    return "Reached max steps without a final answer"

print(run_conversation("Compare weather in Paris and Berlin."))

This loop handles multi-step plans. The model may call get_weather twice, receive both results, then synthesize a comparison in the final text response.

Designing safe tool schemas

Treat every tool as a privileged entry point. Follow these rules:

  • Keep descriptions declarative and explicit about side effects.
  • Use enums for constrained inputs instead of free strings.
  • Never expose filesystem or shell primitives directly; wrap them with allowlists.
  • Return strings from local functions; the API expects content as text.

A well-shaped schema reduces the chance the model hallucinates parameters and reduces parsing errors in your dispatch code.

Inspecting usage metadata

Every completion response includes a usage object with token counts. Because the gateway meters per-token usage, you can log response.usage to track cost across model switches.

print(response.usage)
# CompletionUsage(prompt_tokens=54, completion_tokens=12, total_tokens=66)

If you set stream=True, usage arrives in the final chunk. For function calling, streaming adds complexity because tool-call arguments arrive in fragments; buffer them before parsing.

Provider resilience without code changes

The python function calling n4n.ai api sits in front of multiple upstream providers. If the primary model is rate-limited or degraded, the gateway performs automatic fallback to a healthy equivalent and forwards your provider cache-control hints. Your tools schema, parsing logic, and conversation loop stay identical; you only swap the model string if you want to force a specific route.

This matters when you ship a tool-using agent to production: a single vendor outage should not require a code deploy.

Error handling notes

  • Validate arguments with pydantic or jsonschema before calling local functions.
  • Catch json.JSONDecodeError—models occasionally emit trailing commas or truncated JSON.
  • Set a max iteration count in the loop to avoid infinite tool chains.
  • Use client.with_options(timeout=10) for strict latency budgets on each call.
try:
    args = json.loads(call.function.arguments)
except json.JSONDecodeError:
    args = {}

Also handle openai.APIStatusError to detect 4xx/5xx responses and retry with a different model if your logic requires it.

Wrapping up

You now have a minimal, robust pattern: declare tools as JSON Schema, let the model emit tool_calls, execute them locally, and return results via role: "tool" messages. The OpenAI-compatible surface means the same code works against the gateway or directly against a provider, and the routing fallback reduces operational toil when a single vendor hiccups.

Checkpoint summary:

  1. Client configured with gateway base URL.
  2. Tool schema defined and passed in tools.
  3. Model returns tool_calls with structured arguments.
  4. Local function executes and result fed back with tool_call_id.
  5. Final assistant message contains the synthesized answer.

Extend get_weather with a real HTTP call, add more tools, and you have a working agent. The mechanics do not change as you scale to dozens of functions—only your dispatch table and schema quality need attention.

Tagspythonn4n-aifunction-callingchat-completions

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 in python posts →