Skip to main content
Function calling (a.k.a. tool use) is when the model, instead of replying with text, returns a structured request to call one of your functions: its name and its arguments. You run the actual code on your side and feed the result back. The model then turns that result into a normal, human-readable answer. It sounds modest, but this is the mechanism every agent is built on. A model can’t query your database, fetch the weather, or hit a third-party API on its own — but it can ask you to do it and reason about whatever you hand back. The flow is always the same:
  1. You describe the available functions (their schema) and send a normal request with tools=.
  2. The model replies not with text but with a tool_calls array — what to call and with which arguments.
  3. You run the function yourself and send a second request, appending the model’s reply and the function result to the history.
  4. The model returns the final text.

Complete Python example

Let’s use the classic get_weather(city). In real life it would hit a weather service; here it returns a stub so the flow stays clear.
import json
from openai import OpenAI

client = OpenAI(
    api_key="sk-YOUR_KEY",
    base_url="https://www.ruapi.ai/v1",
)

# This is your real code. The model does NOT run it — it only asks you to.
def get_weather(city: str) -> str:
    # a real weather API call would go here
    return f"It's currently +18°C and partly cloudy in {city}"

# Function description for the model: what it does and what arguments it takes
tools = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "Returns the current weather in the given city",
            "parameters": {
                "type": "object",
                "properties": {
                    "city": {
                        "type": "string",
                        "description": "City name, e.g. \"London\"",
                    }
                },
                "required": ["city"],
            },
        },
    }
]

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

# Step 1. First request — hand the model the question and the tool list
response = client.chat.completions.create(
    model="claude-opus-4-8",  # exact names are on the main site's pricing page
    messages=messages,
    tools=tools,
)

message = response.choices[0].message

# Step 2. Check whether the model wants to call a function
if message.tool_calls:
    # IMPORTANT: first append the model's own reply (the one with tool_calls)
    messages.append(message)

    # Step 3. Handle each call (there may be more than one)
    for tool_call in message.tool_calls:
        # Arguments arrive as a JSON STRING — you have to parse it
        args = json.loads(tool_call.function.arguments)

        if tool_call.function.name == "get_weather":
            result = get_weather(args["city"])
        else:
            result = "Unknown function"

        # Send the call result back as a separate role="tool" message
        messages.append(
            {
                "role": "tool",
                "tool_call_id": tool_call.id,  # ties the result to this call
                "content": result,
            }
        )

    # Step 4. Second request — now the model sees the result and gives the final answer
    final = client.chat.completions.create(
        model="claude-opus-4-8",
        messages=messages,
        tools=tools,
    )
    print(final.choices[0].message.content)
else:
    # The model chose to answer directly, without calling a function
    print(message.content)
Tool use is only supported on capable models — the Claude and GPT families, for example. Older or lightweight models may ignore tools= entirely. To see which model supports what, check the capability badges on the main site’s Pricing page.

Gotchas

A single reply’s message.tool_calls is an array and may hold more than one call — the weather in two cities, say. Loop over it and add a separate role="tool" message with its own tool_call_id for every tool_call. Leave even one call unanswered and the next request will fail.
The order in messages is strict: append the assistant’s own reply (the message object carrying tool_calls) first, and only after it the role="tool" messages with the results. Send a result without the preceding assistant reply and the API rejects the history as inconsistent.
tool_call.function.arguments is a JSON string, not a ready-made dict. Parse it before use with json.loads(...). Indexing the raw string by key will raise.

What’s next