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
- Auto (default): the model decides whether to call a tool.
- Required: set
tool_choice="required"to force some tool call. - Specific: name a function in
tool_choiceto force exactly that one. - None: disable tools for a turn while keeping them in scope.
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.