Free developer tools and practical guides for SQL, data workflows, and debugging.
AAskDBSQL & Data Toolkit

Function Calling Deep Dive: OpenAI, Anthropic, and Gemini Patterns

·11 min read

Function calling (also called tool use) enables LLMs to trigger structured actions in your application. It is the foundation of AI agents, data extraction pipelines, and any workflow where the model needs to interact with external systems.

Core concept

// Without function calling: hope the model formats a string correctly
// With function calling: the model returns structured data guaranteed by schema

// Model receives: user question + tool definitions
// Model returns:  { tool_name: "get_weather", arguments: { city: "Tokyo" } }
// You execute:    getWeather("Tokyo")
// You return:     { temp: 22, condition: "cloudy" }
// Model answers:  "It's 22°C and cloudy in Tokyo right now."

OpenAI function calling

import OpenAI from 'openai';

const openai = new OpenAI();

const tools: OpenAI.ChatCompletionTool[] = [
  {
    type: 'function',
    function: {
      name: 'get_stock_price',
      description: 'Get the current stock price for a ticker symbol',
      parameters: {
        type: 'object',
        properties: {
          ticker: { type: 'string', description: 'Stock ticker symbol, e.g. AAPL' },
          currency: { type: 'string', enum: ['USD', 'EUR', 'GBP'], default: 'USD' },
        },
        required: ['ticker'],
        additionalProperties: false,
      },
      strict: true,  // enforces schema exactly
    },
  },
];

const response = await openai.chat.completions.create({
  model: 'gpt-4o',
  messages: [{ role: 'user', content: 'What is Apple's stock price?' }],
  tools,
  tool_choice: 'auto',  // auto | none | required | { type: 'function', function: { name } }
});

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

if (msg.tool_calls) {
  for (const call of msg.tool_calls) {
    const args = JSON.parse(call.function.arguments);
    const result = await getStockPrice(args.ticker, args.currency ?? 'USD');

    // Return result to model
    const finalResponse = await openai.chat.completions.create({
      model: 'gpt-4o',
      messages: [
        { role: 'user', content: 'What is Apple's stock price?' },
        msg,  // assistant message with tool call
        { role: 'tool', tool_call_id: call.id, content: JSON.stringify(result) },
      ],
      tools,
    });

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

Parallel function calls

GPT-4o can call multiple tools simultaneously when they are independent:

// "What are the prices of AAPL, MSFT, and GOOGL?" 
// → model generates 3 parallel tool calls in a single response

const response = await openai.chat.completions.create({
  model: 'gpt-4o',
  messages: [{ role: 'user', content: 'Compare AAPL, MSFT, and GOOGL stock prices' }],
  tools,
  parallel_tool_calls: true,  // default true
});

// Execute all tool calls in parallel
const toolCallPromises = response.choices[0].message.tool_calls!.map(async call => {
  const args = JSON.parse(call.function.arguments);
  const result = await getStockPrice(args.ticker);
  return { role: 'tool' as const, tool_call_id: call.id, content: JSON.stringify(result) };
});

const toolResults = await Promise.all(toolCallPromises);

const finalResponse = await openai.chat.completions.create({
  model: 'gpt-4o',
  messages: [
    { role: 'user', content: 'Compare AAPL, MSFT, and GOOGL stock prices' },
    response.choices[0].message,
    ...toolResults,
  ],
  tools,
});

Anthropic Claude tool use

import Anthropic from '@anthropic-ai/sdk';

const anthropic = new Anthropic();

const tools: Anthropic.Tool[] = [
  {
    name: 'search_database',
    description: 'Search the product database for items matching a query',
    input_schema: {
      type: 'object',
      properties: {
        query:    { type: 'string' },
        category: { type: 'string', enum: ['electronics', 'clothing', 'books'] },
        limit:    { type: 'number', default: 10 },
      },
      required: ['query'],
    },
  },
];

const response = await anthropic.messages.create({
  model: 'claude-sonnet-4-5',
  max_tokens: 1024,
  tools,
  messages: [{ role: 'user', content: 'Find me the top 5 laptops under $1000' }],
});

// Claude may return tool_use blocks
for (const block of response.content) {
  if (block.type === 'tool_use') {
    const result = await searchDatabase(block.input as { query: string; category?: string; limit?: number });

    // Continue conversation with tool result
    const finalResponse = await anthropic.messages.create({
      model: 'claude-sonnet-4-5',
      max_tokens: 1024,
      tools,
      messages: [
        { role: 'user', content: 'Find me the top 5 laptops under $1000' },
        { role: 'assistant', content: response.content },
        { role: 'user', content: [{ type: 'tool_result', tool_use_id: block.id, content: JSON.stringify(result) }] },
      ],
    });

    console.log(finalResponse.content[0].type === 'text' ? finalResponse.content[0].text : '');
  }
}

Google Gemini function calling

import { GoogleGenerativeAI, FunctionDeclarationSchemaType } from '@google/generative-ai';

const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY!);

const tools = [{
  functionDeclarations: [{
    name: 'get_weather',
    description: 'Get the current weather for a location',
    parameters: {
      type: FunctionDeclarationSchemaType.OBJECT,
      properties: {
        location: { type: FunctionDeclarationSchemaType.STRING, description: 'City name' },
        unit:     { type: FunctionDeclarationSchemaType.STRING, enum: ['celsius', 'fahrenheit'] },
      },
      required: ['location'],
    },
  }],
}];

const model = genAI.getGenerativeModel({ model: 'gemini-1.5-pro', tools });
const chat  = model.startChat();

const result = await chat.sendMessage('What's the weather in London?');
const response = result.response;

// Handle function call
const functionCall = response.candidates?.[0].content.parts.find(p => p.functionCall);
if (functionCall?.functionCall) {
  const { name, args } = functionCall.functionCall;
  const weatherData = await getWeather(args as { location: string; unit?: string });

  // Return result
  const finalResult = await chat.sendMessage([{
    functionResponse: { name, response: weatherData },
  }]);
  console.log(finalResult.response.text());
}

Tool call patterns comparison

FeatureOpenAIAnthropicGemini
Parallel calls✅ Native✅ Native✅ Native
Strict schema✅ strict: truePartialPartial
Forced call✅ tool_choice: required✅ tool_choice✅ toolConfig
No tooltool_choice: nonetool_choice: noneNONE mode

Production patterns

// 1. Always validate tool arguments before execution
function safeParseTool<T>(name: string, rawArgs: string, schema: z.ZodType<T>): T {
  const parsed = schema.safeParse(JSON.parse(rawArgs));
  if (!parsed.success) {
    throw new Error(`Invalid args for ${name}: ${parsed.error.message}`);
  }
  return parsed.data;
}

// 2. Timeout tool executions
async function timedTool<T>(fn: () => Promise<T>, timeoutMs = 10_000): Promise<T> {
  const timeout = new Promise<never>((_, reject) =>
    setTimeout(() => reject(new Error('Tool timeout')), timeoutMs)
  );
  return Promise.race([fn(), timeout]);
}

// 3. Return structured errors back to the model
try {
  const result = await executeTool(name, args);
  return { role: 'tool', tool_call_id: id, content: JSON.stringify({ success: true, data: result }) };
} catch (err) {
  return { role: 'tool', tool_call_id: id, content: JSON.stringify({ success: false, error: String(err) }) };
  // The model can then tell the user about the error or try a different approach
}

Takeaway

Use OpenAI with strict: true for guaranteed argument schemas. Enable parallel tool calls for independent lookups. Always validate arguments with Zod before execution, timeout long-running tools, and return structured error objects so the model can recover gracefully.