Tool Calling

Let models call functions in your application with Oru-el's tool calling support.

Tool Calling#

Tool calling (also known as function calling) lets a model invoke functions defined in your application. Instead of generating a text response, the model outputs a structured function call with arguments. Your application executes the function and returns the result, and the model incorporates that result into its final response.

This is useful for:

  • Fetching real-time data (weather, stock prices, database queries)
  • Performing calculations
  • Interacting with external APIs
  • Taking actions in your application

How tool calling works#

  1. You define tools — describe available functions using JSON Schema
  2. The model decides — based on the user's message, the model chooses whether to call a tool
  3. You execute — your code runs the function with the model's arguments
  4. You return the result — send the function output back to the model
  5. The model responds — the model uses the function output to generate a final answer

Defining tools#

Tools are defined as an array of objects with type: "function" and a function descriptor:

{
  "type": "function",
  "function": {
    "name": "get_weather",
    "description": "Get the current weather for a location",
    "parameters": {
      "type": "object",
      "properties": {
        "location": {
          "type": "string",
          "description": "City name, e.g. 'San Francisco, CA'"
        },
        "unit": {
          "type": "string",
          "enum": ["celsius", "fahrenheit"],
          "description": "Temperature unit"
        }
      },
      "required": ["location"]
    }
  }
}

Tool definition fields#

FieldTypeRequiredDescription
typestringYesAlways "function"
function.namestringYesFunction name (must be unique across tools)
function.descriptionstringYesWhat the function does (helps the model decide when to use it)
function.parametersobjectYesJSON Schema defining the function's parameters

Write clear, specific descriptions — the model uses them to decide when and how to call each tool. A vague description leads to incorrect tool selection.

Complete example: weather lookup#

Python#

import json
from openai import OpenAI

client = OpenAI(
    base_url="https://api.oru-el.com/v1/inference",
    api_key="oruel_your_api_key_here",
)

# Step 1: Define your tools
tools = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "Get the current weather for a location",
            "parameters": {
                "type": "object",
                "properties": {
                    "location": {
                        "type": "string",
                        "description": "City name, e.g. 'San Francisco, CA'",
                    },
                    "unit": {
                        "type": "string",
                        "enum": ["celsius", "fahrenheit"],
                        "description": "Temperature unit",
                    },
                },
                "required": ["location"],
            },
        },
    }
]

# Step 2: Your actual function implementation
def get_weather(location: str, unit: str = "celsius") -> dict:
    # In production, call a real weather API here
    return {
        "location": location,
        "temperature": 22 if unit == "celsius" else 72,
        "unit": unit,
        "condition": "sunny",
    }

# Step 3: Send the request with tools
messages = [{"role": "user", "content": "What's the weather in Tokyo?"}]

response = client.chat.completions.create(
    model="llama-4-maverick",
    messages=messages,
    tools=tools,
    tool_choice="auto",
)

# Step 4: Check if the model wants to call a tool
message = response.choices[0].message

if message.tool_calls:
    # Add the assistant's message (with tool calls) to history
    messages.append(message)

    # Execute each tool call
    for tool_call in message.tool_calls:
        function_name = tool_call.function.name
        arguments = json.loads(tool_call.function.arguments)

        # Call the actual function
        if function_name == "get_weather":
            result = get_weather(**arguments)
        else:
            result = {"error": f"Unknown function: {function_name}"}

        # Add the tool result to messages
        messages.append({
            "role": "tool",
            "tool_call_id": tool_call.id,
            "content": json.dumps(result),
        })

    # Step 5: Get the final response with tool results
    final_response = client.chat.completions.create(
        model="llama-4-maverick",
        messages=messages,
        tools=tools,
    )

    print(final_response.choices[0].message.content)
else:
    # No tool call — model responded directly
    print(message.content)

JavaScript#

import OpenAI from "openai";

const client = new OpenAI({
  baseURL: "https://api.oru-el.com/v1/inference",
  apiKey: "oruel_your_api_key_here",
});

// Define tools
const tools = [
  {
    type: "function",
    function: {
      name: "get_weather",
      description: "Get the current weather for a location",
      parameters: {
        type: "object",
        properties: {
          location: {
            type: "string",
            description: "City name, e.g. 'San Francisco, CA'",
          },
          unit: {
            type: "string",
            enum: ["celsius", "fahrenheit"],
            description: "Temperature unit",
          },
        },
        required: ["location"],
      },
    },
  },
];

// Your function implementation
function getWeather(location, unit = "celsius") {
  return {
    location,
    temperature: unit === "celsius" ? 22 : 72,
    unit,
    condition: "sunny",
  };
}

// Function dispatch
const functionMap = {
  get_weather: ({ location, unit }) => getWeather(location, unit),
};

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

// First request
let response = await client.chat.completions.create({
  model: "llama-4-maverick",
  messages,
  tools,
  tool_choice: "auto",
});

const message = response.choices[0].message;

if (message.tool_calls) {
  messages.push(message);

  for (const toolCall of message.tool_calls) {
    const fn = functionMap[toolCall.function.name];
    const args = JSON.parse(toolCall.function.arguments);
    const result = fn ? fn(args) : { error: "Unknown function" };

    messages.push({
      role: "tool",
      tool_call_id: toolCall.id,
      content: JSON.stringify(result),
    });
  }

  // Second request with tool results
  response = await client.chat.completions.create({
    model: "llama-4-maverick",
    messages,
    tools,
  });
}

console.log(response.choices[0].message.content);

Tool call response format#

When the model decides to call a tool, the response looks like this:

{
  "id": "chatcmpl-abc123",
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": null,
        "tool_calls": [
          {
            "id": "call_abc123",
            "type": "function",
            "function": {
              "name": "get_weather",
              "arguments": "{\"location\": \"Tokyo\", \"unit\": \"celsius\"}"
            }
          }
        ]
      },
      "finish_reason": "tool_calls"
    }
  ]
}

Key details:

  • content is null when tool calls are present
  • finish_reason is "tool_calls"
  • tool_calls[].function.arguments is a JSON string (not a parsed object) — you must call JSON.parse() or json.loads() on it
  • Each tool call has a unique id that you must reference in the tool response message

tool_choice#

Controls whether and which tools the model uses:

ValueBehavior
"auto"Model decides whether to call a tool or respond with text (default when tools are provided)
"none"Model will not call any tools, even if they are defined
{"type": "function", "function": {"name": "get_weather"}}Force the model to call a specific tool

Forcing a specific tool#

response = client.chat.completions.create(
    model="llama-4-maverick",
    messages=[{"role": "user", "content": "Tokyo weather please"}],
    tools=tools,
    tool_choice={"type": "function", "function": {"name": "get_weather"}},
)

Multiple tools#

You can define up to 128 tools. The model will choose which one(s) to call based on the user's request.

tools = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "Get the current weather for a location",
            "parameters": {
                "type": "object",
                "properties": {
                    "location": {"type": "string", "description": "City name"}
                },
                "required": ["location"],
            },
        },
    },
    {
        "type": "function",
        "function": {
            "name": "search_restaurants",
            "description": "Search for restaurants near a location",
            "parameters": {
                "type": "object",
                "properties": {
                    "location": {"type": "string", "description": "City name"},
                    "cuisine": {"type": "string", "description": "Type of cuisine"},
                    "price_range": {
                        "type": "string",
                        "enum": ["$", "$$", "$$$", "$$$$"],
                        "description": "Price range",
                    },
                },
                "required": ["location"],
            },
        },
    },
    {
        "type": "function",
        "function": {
            "name": "book_flight",
            "description": "Search for flights between two cities",
            "parameters": {
                "type": "object",
                "properties": {
                    "origin": {"type": "string", "description": "Departure city"},
                    "destination": {"type": "string", "description": "Arrival city"},
                    "date": {"type": "string", "description": "Travel date (YYYY-MM-DD)"},
                },
                "required": ["origin", "destination", "date"],
            },
        },
    },
]

The model may also call multiple tools in a single response (parallel tool calls). Handle each one and return all results before making the follow-up request.

Parallel tool calls#

Some models can call multiple tools simultaneously:

{
  "message": {
    "role": "assistant",
    "content": null,
    "tool_calls": [
      {
        "id": "call_1",
        "type": "function",
        "function": {
          "name": "get_weather",
          "arguments": "{\"location\": \"Tokyo\"}"
        }
      },
      {
        "id": "call_2",
        "type": "function",
        "function": {
          "name": "get_weather",
          "arguments": "{\"location\": \"London\"}"
        }
      }
    ]
  }
}

Execute all tool calls and include all results in the follow-up request:

for tool_call in message.tool_calls:
    result = execute_function(tool_call.function.name, tool_call.function.arguments)
    messages.append({
        "role": "tool",
        "tool_call_id": tool_call.id,
        "content": json.dumps(result),
    })

Best practices#

  • Write descriptive tool names and descriptions — the model selects tools based on these. "get_current_weather_forecast" is better than "weather".
  • Use JSON Schema constraintsenum, minimum, maximum, pattern, and required help the model generate valid arguments.
  • Validate arguments — always validate the arguments the model returns before executing your function. Models can hallucinate parameter values.
  • Handle errors gracefully — if a function fails, return a structured error message as the tool result so the model can respond appropriately.
  • Limit the number of tools — while up to 128 tools are supported, model performance degrades with too many. Keep it under 20 for best results.
  • Use tool_choice: "none" to disable — if you want to pass tools for context but prevent the model from calling them in a specific turn, set tool_choice: "none".