Chris McKenzie

Jun 30, 2025 • 6 min read

Getting Started: Tool calling with JS and OpenAI

Tool Calling Isn’t Magic — It’s JSON

Getting Started: Tool calling with JS and OpenAI

Think of tool calling like React props: the model proposes a component + props, you control rendering and side effects.

Tool calling sounds complex, but under the hood, it’s straightforward. The model doesn’t execute anything. It doesn’t run your code. It just outputs a structured plan — something like “Call this function with these parameters.” Your job is to wire that up and make it work. That’s it. It’s a suggestion, not an action. You take that output, validate it, and then decide if and how to run the real code in your app.

👉 Sometimes you’ll see this pattern called “Function Calling,” but it’s that same thing. It’s also very similar to how MCP implements tool calling.

The Basic Pattern

Let’s walk through a minimal example: You want your app to handle math questions. Something like: “What’s 3 plus 7?”

👉 This is coded to match the OpenAI JavaScript SDK. Some providers have slightly different implementations, so you’ll want to check their docs.

You start by telling the model what tools it has access to:

const tools = [
  {
    type: "function",
    function: {
      name: "add",
      description: "Adds two numbers",
      parameters: {
        type: "object",
        properties: {
          x: { type: "number" },
          y: { type: "number" },
        },
        required: ["x", "y"],
        additionalProperties: false,
      },
      strict: true,
    },
  },
];

const systemPrompt = `You are a helpful assistant. Use tool calls when appropriate.`;

const messages = [
  { role: "system", content: systemPrompt },
  { role: "user", content: "What is 3 plus 7?" }
];

const completion = await openai.chat.completions.create({
    model: "gpt-4o",
    messages,
    tools
});

When you send this to a model like gpt-4o, you’ll often get a response like this:

// completion.choices[0].message.tool_calls
[{
    "id": "call_12345xyz",
    "type": "function",
    "function": {
      "name": "add",
      "arguments": "{\"x\":3,\"y\":7}"
    }
}]

That’s the model saying, “Looks like this is a job for your add function.”

So you run it:

function add({ x, y }) {
  return x + y;
}

const result = add({ x: 3, y: 7 });
// result = 10

And hand the result back to the model:

const toolCall = completion.choices[0].message.tool_calls[0];

messages.push(completion.choices[0].message); // append model's function call message
messages.push({                               // append result message
    role: "tool",
    tool_call_id: toolCall.id,
    content: result.toString()
});
await openai.chat.completions.create({
    model: "gpt-4o",
    messages,
    tools,
});

The model then replies: “3 plus 7 is 10.”

At no point did the LLM run your code. It just told you what to run — and you decided what was safe and appropriate to execute, then you pass the result back to the LLM so it can generate a text-based response with the answer.

Why This Pattern Matters

Tool calling shifts the burden of natural language parsing off your plate. Instead of regex spaghetti or brittle heuristics, the model interprets intent. You just implement the logic.

This makes it easier to connect LLMs to actual application behavior — whether you’re triggering API calls, updating app state, querying a database, handling business logic, or getting the weather. It’s just structured input.

Tool Definitions Are Your Interface

Tool definitions are where things break down — or scale. Be too vague, and the model guesses wrong. Be specific, and it starts acting like part of your system.

Bad:

{
  name: "searchStuff",
  description: "Searches things on the internet",
  ...
}

Better:

{
  name: "search_google",
  description: "Search Google for real-time information. Use for current events or queries requiring live data.",
  ...
}

You don’t need to over-engineer it. Just describe what the tool does and when it’s appropriate to use. That alone will save you hours of debugging weird behavior.

What Tool Calling Isn’t

Let’s be clear about what this isn’t:

  • It’s not execution. The model doesn’t run your code. You do.

  • It’s not secure by default. You need to validate inputs and handle permissions.

  • It’s not perfect. Models will occasionally invent tool names, pass the wrong types, or invoke tools unnecessarily. Be prepared to handle these edge cases.

Which means you still need to write defensively:

function safeCall(toolName, args) {
  if (toolName === "add" && typeof args.x === "number" && typeof args.y === "number") {
    return add(args);
  }
  throw new Error("Invalid tool call");
}

The model is guessing. Your job is to catch the bad guesses before they become bugs or security holes.

Put it All Together

Here’s the final working code for this example:

import OpenAI from "openai";

const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });

const tools = [
  {
    type: "function",
    function: {
      name: "add",
      description: "Adds two numbers",
      parameters: {
        type: "object",
        properties: {
          x: { type: "number" },
          y: { type: "number" },
        },
        required: ["x", "y"],
        additionalProperties: false,
      },
      strict: true,
    },
  },
];

function add({ x, y }) {
  return x + y;
}

function safeCall(toolName, args) {
  if (toolName === "add" && typeof args.x === "number" && typeof args.y === "number") {
    return add(args);
  }
  throw new Error(`Invalid tool call: ${toolName} with args ${JSON.stringify(args)}`);
}

async function main() {
  const systemPrompt = `You are a helpful assistant. Use tool calls when appropriate.`;

  const messages = [
    { role: "system", content: systemPrompt },
    { role: "user", content: "What is the capital of France?" } // try swapping this with "What's 3 plus 7?"
  ];

  const initialResponse = await openai.chat.completions.create({
    model: "gpt-4o",
    messages,
    tools
  });

  const choice = initialResponse.choices[0]?.message;

  // CASE 1: Model answered directly, no tool call
  if (!choice.tool_calls || choice.tool_calls.length === 0) {
    console.log("Model replied directly:", choice.content);
    return;
  }

  // CASE 2: Model issued a tool call
  const toolCall = choice.tool_calls[0];

  const args = JSON.parse(toolCall.function.arguments);

  const result = safeCall(toolCall.function.name, args);

  messages.push(choice); // model's tool call

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

  const finalResponse = await openai.chat.completions.create({
    model: "gpt-4o",
    messages,
    tools
  });

  const finalAnswer = finalResponse.choices[0]?.message?.content;
  console.log("Final Answer:", finalAnswer);
}

main().catch(console.error);

Final Thoughts

This is just a toy demo, but it should give you a sense of how dead simple the core idea really is. Under the hood, this same pattern drives everything from basic function calls to full agents that can chain together complex actions. Tool calls are how LLMs actually do stuff — how they move from text to execution in real systems.

If you’re just starting out, try something simple. Pick one internal function — something you’d usually trigger with a button or form — and make that callable by the model. Get that loop working first. Once you’ve got it running, you’ll see how easy it is to build on with reasoning, chaining, and more advanced flows.


To stay connected and share your journey, feel free to reach out through the following channels:

  • 👨‍💼 LinkedIn: Join me for more insights into AI development and tech innovations.

  • 🤖 JavaScript + AI: Join the JavaScript and AI group and share what you’re working on.

  • 💻 GitHub: Explore my projects and contribute to ongoing work.

  • 📚 Medium: Follow my articles for more in-depth discussions on the intersection of JavaScript and AI.

Join Chris on Peerlist!

Join amazing folks like Chris and thousands of other builders on Peerlist.

peerlist.io/

It’s available... this username is available! 😃

Claim your username before it's too late!

This username is already taken, you’re a little late.😐

0

12

0