Skip to main content

Tool Use (Function Calling)

What is Tool Use?

Tool use lets the model call functions you define on your server. This is how you build LLM-powered features that can read from your database, call external APIs, or perform computations the model cannot do on its own.

The flow is always a loop:

1. You send messages + tool definitions
2. Model decides to call a tool → returns stop_reason: "tool_calls"
3. You execute the function with the model's arguments
4. You send the result back to the model
5. Model uses the result to form a final answer

OpenAI Tool Use

Define your tools

src/services/tools.js
const tools = [
{
type: 'function',
function: {
name: 'get_user_orders',
description: 'Fetch the recent orders for a user by their user ID',
parameters: {
type: 'object',
properties: {
userId: { type: 'string', description: 'The user UUID' },
limit: { type: 'number', description: 'Max number of orders to return (default 5)' },
},
required: ['userId'],
},
},
},
{
type: 'function',
function: {
name: 'cancel_order',
description: 'Cancel an order by its order ID',
parameters: {
type: 'object',
properties: {
orderId: { type: 'string', description: 'The order UUID to cancel' },
reason: { type: 'string', description: 'Reason for cancellation' },
},
required: ['orderId'],
},
},
},
];

export default tools;

Implement the tool handlers

src/services/toolExecutor.js
import db from '../db.js';

const toolHandlers = {
async get_user_orders({ userId, limit = 5 }) {
return db.orders.findMany({
where: { userId },
take: limit,
orderBy: { createdAt: 'desc' },
});
},

async cancel_order({ orderId, reason }) {
await db.orders.update({
where: { id: orderId },
data: { status: 'cancelled', cancelReason: reason },
});
return { success: true, orderId };
},
};

export async function executeTool(name, args) {
const handler = toolHandlers[name];
if (!handler) throw new Error(`Unknown tool: ${name}`);
return handler(args);
}

The tool loop

src/services/openai.agent.js
import openai from '../lib/openai.js';
import tools from './tools.js';
import { executeTool } from './toolExecutor.js';

export async function agentChat(messages, maxIterations = 5) {
let iteration = 0;

while (iteration < maxIterations) {
iteration++;

const response = await openai.chat.completions.create({
model: 'gpt-4o',
messages,
tools,
tool_choice: 'auto', // 'auto' lets model decide, 'required' forces a tool call
});

const choice = response.choices[0];

// No tool call — model produced a final answer
if (choice.finish_reason !== 'tool_calls') {
return choice.message.content;
}

// Append the assistant's tool call message to history
messages = [...messages, choice.message];

// Execute all tool calls (the model may call multiple tools at once)
const toolResults = await Promise.all(
choice.message.tool_calls.map(async (toolCall) => {
const args = JSON.parse(toolCall.function.arguments);
const result = await executeTool(toolCall.function.name, args);
return {
role: 'tool',
tool_call_id: toolCall.id,
content: JSON.stringify(result),
};
})
);

messages = [...messages, ...toolResults];
}

throw new Error('Agent exceeded max iterations');
}

Anthropic Tool Use

The Anthropic version uses a slightly different schema format (input_schema instead of parameters) and a different message structure for tool results.

src/services/anthropic.agent.js
import anthropic from '../lib/anthropic.js';
import { executeTool } from './toolExecutor.js';

const tools = [
{
name: 'get_user_orders',
description: 'Fetch the recent orders for a user by their user ID',
input_schema: {
type: 'object',
properties: {
userId: { type: 'string', description: 'The user UUID' },
limit: { type: 'number', description: 'Max number of orders to return' },
},
required: ['userId'],
},
},
{
name: 'cancel_order',
description: 'Cancel an order by its order ID',
input_schema: {
type: 'object',
properties: {
orderId: { type: 'string', description: 'The order UUID to cancel' },
reason: { type: 'string', description: 'Reason for cancellation' },
},
required: ['orderId'],
},
},
];

export async function agentChat(messages, systemPrompt = '', maxIterations = 5) {
let iteration = 0;

while (iteration < maxIterations) {
iteration++;

const response = await anthropic.messages.create({
model: 'claude-sonnet-4-6',
max_tokens: 1024,
system: systemPrompt,
tools,
messages,
});

// No tool call — model produced a final answer
if (response.stop_reason !== 'tool_use') {
return response.content.find((b) => b.type === 'text')?.text;
}

// Append the assistant's full content block to history
messages = [...messages, { role: 'assistant', content: response.content }];

// Execute all tool use blocks
const toolResults = await Promise.all(
response.content
.filter((block) => block.type === 'tool_use')
.map(async (toolUse) => {
const result = await executeTool(toolUse.name, toolUse.input);
return {
type: 'tool_result',
tool_use_id: toolUse.id,
content: JSON.stringify(result),
};
})
);

// Tool results go back as a user message with structured content
messages = [...messages, { role: 'user', content: toolResults }];
}

throw new Error('Agent exceeded max iterations');
}

Schema Differences at a Glance

OpenAIAnthropic
Parameters fieldparametersinput_schema
Tool result role"tool" message"user" message with tool_result content
Finish signalfinish_reason: "tool_calls"stop_reason: "tool_use"
Multiple toolstool_calls arrayFilter content blocks by type === "tool_use"