How to Use Function Calling With AI APIs: A Complete Python Tutorial
Function calling (also called tool use) is the feature that turns a chatbot into an agent. Instead of just answering in text, the model can ask your code to run a function — look up an order in a database, call a weather API, send an email — and then use the result to compose its answer.
In this tutorial you'll learn how function calling works under the hood, and build a working weather assistant in Python using an OpenAI-compatible API. Everything here works with GPT, Claude, Gemini, GLM, DeepSeek, and any other model exposed through the Qubax AI gateway.
How Function Calling Actually Works
A common misconception: the model does not execute your functions. The flow is a loop between the model and your code:
- You send the user's message plus a list of available tools (names, descriptions, JSON schemas).
- The model replies with a special
tool_callsmessage instead of normal text — it picks a tool and generates JSON arguments. - Your code executes the real function (query the DB, hit the API).
- You send the result back to the model as a
toolrole message. - The model answers the user — or requests another tool call (that's the loop).
The model provides intelligence about what to call and with what arguments; your code provides the actual capabilities. Keeping that separation clear prevents most confusion.
Prerequisites
pip install openai # works with any OpenAI-compatible endpointYou'll need an API key from a provider, or from Qubax AI if you want to test the same code against multiple model families by changing one string.
Step 1: Define Your Tools
A tool definition has three parts: a name, a natural-language description (this is what the model reads to decide when to use the tool — make it precise), and a JSON Schema for the arguments.
tools = [
{
"type": "function",
"function": {
"name": "get_current_weather",
"description": "Get the current weather for a given city. Use this whenever the user asks about weather conditions, temperature, or forecasts.",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "City name, e.g. 'San Francisco'",
},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "Temperature unit. Defaults to celsius.",
},
},
"required": ["city"],
},
},
}
]Tips that dramatically improve reliability:
- *Describe when to use the tool*, not just what it does. The description is the model's only guidance.
- Use enums wherever possible to constrain outputs.
- Keep required fields minimal — let the model infer optional parameters when reasonable.
Step 2: Implement the Actual Functions
import json
def get_current_weather(city: str, unit: str = "celsius") -> str:
# Real implementation: call OpenWeather, WeatherAPI, etc.
# Here we return mock data for the demo.
return json.dumps({
"city": city,
"temperature": 22 if unit == "celsius" else 72,
"unit": unit,
"condition": "Partly cloudy",
})
AVAILABLE_TOOLS = {
"get_current_weather": get_current_weather,
}Two production rules:
- Never trust model-generated arguments blindly. Validate them (Pydantic is great) before executing — especially for tools that write, pay, or delete.
- Return errors as strings, not exceptions. If the tool fails, telling the model why lets it apologize, retry, or try a different approach.
Step 3: Build the Agent Loop
This is the part everyone gets wrong. You must handle the possibility of multiple tool calls in one response, and loop until the model produces a final answer.
from openai import OpenAI
client = OpenAI(
base_url="https://api.qubax.ai/v1",
api_key="YOUR_QUBAX_KEY",
)
def run_agent(user_message: str, model: str = "gpt-5.6-luna", max_turns: int = 5):
messages = [{"role": "user", "content": user_message}]
for _ in range(max_turns):
response = client.chat.completions.create(
model=model,
messages=messages,
tools=tools,
)
msg = response.choices[0].message
# No tool calls? The model is done — return the final answer.
if not msg.tool_calls:
return msg.content
messages.append(msg) # include the assistant's tool_calls message
# Execute every requested tool and feed results back
for tc in msg.tool_calls:
fn = AVAILABLE_TOOLS[tc.function.name]
args = json.loads(tc.function.arguments)
result = fn(**args)
messages.append({
"role": "tool",
"tool_call_id": tc.id,
"content": result,
})
return "Agent stopped: too many tool turns."Try it:
print(run_agent("What's the weather in Tokyo right now? Should I bring a jacket?"))
# -> "It's currently 22°C and partly cloudy in Tokyo. A light jacket is a good idea..."Notice what happened: the model never "knew" the weather. It decided on its own that it needed the tool, extracted city="Tokyo" from the user's message, waited for your code to run, and folded the result into a natural answer.
Step 4: Add Streaming (Production-Grade UX)
For real apps you'll want tokens streaming to the UI. Streaming with tools works the same way — you just accumulate tool-call deltas until the call is complete. We covered the full pattern in our streaming tutorial — the key addition here is buffering delta.tool_calls chunks and stitching their argument fragments together before executing.
Common Pitfalls (and Fixes)
- Infinite tool loops. The model keeps calling the same tool. Fix: cap turns (
max_turnsabove) and include turn count in the system prompt. - Hallucinated arguments. The model invents values for missing info. Fix: tighten schemas, mark required fields, and validate with Pydantic before executing.
- Wrong tool chosen. Usually a description problem. Rewrite descriptions as "Use this when X, not when Y."
- Parallel calls mishandled. Modern models often request several tools at once — always loop over
msg.tool_calls(as above), never assume exactly one. - Leaking secrets into tool results. Tool outputs go into the model's context. Sanitize API keys and PII before appending results.
Choosing Models for Function Calling
Tool-calling quality varies more between models than raw chat quality does. As a rule of thumb for 2026:
- Frontier models (GPT-5.6 Sol, Claude Opus 5, Gemini 3.7 Flash) handle complex multi-tool chains reliably.
- Flash-tier and distilled models (GPT-5.6 Luna, GLM 5.3 Flash, DeepSeek V4 Flash) are excellent for single-tool routing and high-volume agents at 5–20× lower cost.
- Budget tip: the same agent code runs unchanged on all of them through Qubax AI — benchmark your real workload across several models and pick the cheapest one that passes.
FAQ
What is function calling in AI APIs?
It's a feature where the model outputs structured JSON indicating which of your functions to run and with what arguments, then incorporates your results into its answer.
Does the model execute my code?
No. The model only requests a function call. Your application executes it and returns the result to the model.
Is function calling the same across providers?
The concept is universal, and OpenAI-compatible gateways like Qubax AI normalize the request/response format so one codebase works with GPT, Claude, Gemini, GLM, DeepSeek, and more.
How do I stop the model from calling tools when it shouldn't?
Sharpen tool descriptions ("Use this only when..."), and state in the system prompt: "If the question is answerable from context, answer directly without tools."
Next steps: grab an API key at Qubax AI, swap the model string, and test your agent against a dozen models in minutes. Full API reference lives in the Qubax docs.