Engineering

Function Calling and Tool Use, Explained

DPDevon PrattMay 25, 20264 min read

Function calling (also called tool use) lets a language model decide to invoke your code with structured arguments instead of replying in prose. You describe the tools, the model returns a JSON call, you execute it, and you feed the result back. It is how LLMs touch the real world: databases, APIs, calculators, search.

Model Database passes standard OpenAI-style tools through to the underlying model, so the same code works across providers as long as the chosen model supports tool use.

Defining a tool

A tool is a name, a description, and a JSON Schema for its parameters. The description is prompt text the model reads, so write it for a reader, not a compiler.

tools = [{
    "type": "function",
    "function": {
        "name": "get_weather",
        "description": "Get current weather for a city.",
        "parameters": {
            "type": "object",
            "properties": {
                "city": {"type": "string"},
                "unit": {"type": "string", "enum": ["c", "f"]},
            },
            "required": ["city"],
        },
    },
}]

The request/response cycle

Send the tools with your messages. If the model decides to call one, the response contains tool_calls instead of (or alongside) content. You run the function and append a tool message with the result, then call again so the model can write its final answer.

from openai import OpenAI
import json

client = OpenAI(base_url="https://modeldatabase.com/v1", api_key="mdb_live_...")

messages = [{"role": "user", "content": "What's the weather in Athens?"}]

resp = client.chat.completions.create(
    model="openai/gpt-4o",
    messages=messages,
    tools=tools,
)
msg = resp.choices[0].message

if msg.tool_calls:
    messages.append(msg)
    for call in msg.tool_calls:
        args = json.loads(call.function.arguments)
        result = get_weather(**args)          # your real function
        messages.append({
            "role": "tool",
            "tool_call_id": call.id,
            "content": json.dumps(result),
        })
    final = client.chat.completions.create(
        model="openai/gpt-4o", messages=messages, tools=tools,
    )
    print(final.choices[0].message.content)

Handling multiple and parallel calls

Modern models can request several tool calls in one turn. Always loop over tool_calls rather than assuming one. Match each result back with its tool_call_id; the model relies on that ID to pair arguments with outputs. A single missing tool response will derail the next turn.

Forcing or restricting tool use

Forcing a tool is handy for deterministic flows like "always extract these fields," where you never want prose back.

Validate everything the model sends

The arguments come from a probabilistic system. Treat them like untrusted user input: validate against your schema, clamp ranges, and reject unknown enum values before executing anything with side effects. Wrap execution in try/except and return the error text as the tool result so the model can recover or apologize gracefully rather than crashing your handler.

try:
    result = get_weather(**args)
except Exception as e:
    result = {"error": str(e)}

Honest limitations

Tool support and quality vary by model. Smaller models may hallucinate function names, skip required fields, or call a tool when plain text would do. Test your specific model with GET /v1/models to confirm availability, and keep tool counts modest: dumping 40 tools into context degrades selection accuracy. Group related capabilities and pass only the tools relevant to the current step.

Build your first tool-using feature with a key from your dashboard, and see the full parameter reference in the docs.

← All articles Get your API key →