Implementing langchain.js tool calling gpt-4o n4n.ai gives you a single OpenAI-compatible surface for GPT-4o while keeping LangChain’s agent primitives intact. This guide builds a minimal Node + TypeScript project that binds a custom tool to GPT-4o and runs a multi-turn tool-calling loop. You’ll end with a runnable script that resolves a user query by invoking your function and returning a final answer.
Step 1: Scaffold a TypeScript project
Create a directory and install the required packages. LangChain’s OpenAI integration lives in @langchain/openai; tool definitions use @langchain/core/tools and zod for schema validation.
mkdir lc-tool-demo && cd lc-tool-demo
npm init -y
npm install langchain@^0.2 @langchain/openai@^0.2 @langchain/core@^0.2 zod@^3
npm install -D typescript@^5 tsx@^4
Add a tsconfig.json with module: "ESNext" and target: "ES2022" to support top-level await.
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true,
"esModuleInterop": true
},
"include": ["*.ts"]
}
Set your gateway key as an environment variable. The script reads OPENAI_API_KEY because @langchain/openai expects that name by default, but the base URL will point elsewhere.
export OPENAI_API_KEY="sk-your-n4n-key"
Step 2: Point ChatOpenAI at the OpenAI-compatible endpoint
GPT-4o is available through the n4n.ai OpenAI-compatible endpoint, so we configure ChatOpenAI with a custom configuration.baseURL. No LangChain code changes when you swap providers; only the connection details differ.
import { ChatOpenAI } from "@langchain/openai";
const model = new ChatOpenAI({
model: "gpt-4o",
temperature: 0,
apiKey: process.env.OPENAI_API_KEY,
configuration: {
baseURL: "https://api.n4n.ai/v1",
},
});
The model string must match the exact identifier the gateway exposes. For GPT-4o that is typically gpt-4o or a dated snapshot like gpt-4o-2024-08-06; check your gateway’s model list. Because the endpoint speaks the OpenAI chat completions protocol, tool calling works identically to OpenAI’s official SDK.
Step 3: Define a tool with a Zod schema
LangChain tools are functions wrapped with tool() from @langchain/core/tools. The Zod schema defines the JSON parameters the model will emit. Keep schemas tight—GPT-4o respects them strictly, but loose definitions cause silent argument coercion.
import { tool } from "@langchain/core/tools";
import { z } from "zod";
const getWeather = tool(
async ({ city }: { city: string }) => {
// Mock implementation; replace with real API call.
const temps: Record<string, number> = { "San Francisco": 15, "New York": 22 };
return `${city} is ${temps[city] ?? 20}°C`;
},
{
name: "get_weather",
schema: z.object({
city: z.string().describe("The city name, e.g. 'San Francisco'"),
}),
}
);
const calculator = tool(
async ({ expression }: { expression: string }) => {
// Safe-ish eval for demo only.
const result = Function(`"use strict"; return (${expression})`)();
return String(result);
},
{
name: "calculator",
schema: z.object({
expression: z.string().describe("Arithmetic like '2 * (3 + 4)'"),
}),
}
);
const tools = [getWeather, calculator];
Each tool returns a string. LangChain serializes that into the message history so the model can consume it in the next turn.
Step 4: Bind tools to the model
Binding converts your tool schemas into the tools parameter of the chat completion request. The returned model object intercepts AIMessage.tool_calls.
const modelWithTools = model.bindTools(tools);
You can inspect the bound schema by calling modelWithTools.asToolCallSchema?() in newer versions, but typically you trust the binding. If you need to force a specific tool, pass { tool_choice: { type: "function", function: { name: "get_weather" } } } to bindTools options.
Step 5: Run the tool-calling loop
LangChain does not auto-execute tools unless you use an agent executor. For a transparent how-to, write the loop manually. It appends the user message, calls the model, checks for tool_calls, invokes the matching tool, and feeds results back until the model returns plain text.
import { HumanMessage, AIMessage, ToolMessage } from "@langchain/core/messages";
async function runAgent(query: string) {
const messages = [new HumanMessage(query)];
for (let i = 0; i < 5; i++) {
const aiMsg = await modelWithTools.invoke(messages);
messages.push(aiMsg);
if (!aiMsg.tool_calls || aiMsg.tool_calls.length === 0) {
return aiMsg.content;
}
for (const call of aiMsg.tool_calls) {
const matched = tools.find(t => t.name === call.name);
if (!matched) throw new Error(`Unknown tool: ${call.name}`);
const output = await matched.invoke(call.args);
messages.push(
new ToolMessage({
content: output,
tool_call_id: call.id!,
})
);
}
}
throw new Error("Exceeded max iterations");
}
const answer = await runAgent("What is 2 * (3 + 4) in New York's temperature units?");
console.log(answer);
The loop handles parallel tool calls because aiMsg.tool_calls is an array. Each ToolMessage must carry the tool_call_id from the corresponding call, or the API rejects the request.
Handling errors and timeouts
Wrap matched.invoke in try/catch and return a error string as tool content. GPT-4o will reason about the failure and retry or apologize. Set a hard timeout on the whole runAgent with Promise.race if you deploy this in a request path.
const withTimeout = (p: Promise<any>, ms: number) =>
Promise.race([p, new Promise((_, rej) => setTimeout(() => rej(new Error("timeout")), ms))]);
Step 6: Verify success
Run the script with tsx:
npx tsx index.ts
Expected output is a natural language string that combines both tools, e.g. "2 * (3 + 4) is 14, and New York is 22°C, so 14 in New York's temperature units is 14 (temperature units are arbitrary here)." The exact phrasing varies.
To confirm tool calling actually fired, add a console log inside the tool functions or inspect messages before the final return. You should see an AIMessage with tool_calls containing calculator and get_weather, followed by two ToolMessage entries.
If you see a 401, verify the OPENAI_API_KEY and base URL. A 404 on the model name means the gateway does not expose gpt-4o under that ID. A schema validation error from Zod indicates the model emitted a parameter that violates your description—tighten the schema or add examples.
Why this pattern holds up in production
Binding tools at the model level keeps your business logic in plain TypeScript functions. You can swap the baseURL to any OpenAI-compatible gateway without touching tool code. The manual loop shows exactly what tokens flow where; when you outgrow it, replace runAgent with createToolCallingAgent and AgentExecutor from @langchain/community or @langchain/core without changing tool definitions.
Streaming works the same way: use modelWithTools.stream and accumulate tool_calls deltas, but the execution boundary remains the ToolMessage handshake. Keep tool latency under a few seconds; models do not handle long waits gracefully without explicit instruction.
That’s the complete path for langchain.js tool calling gpt-4o n4n.ai. You have a runnable Node script, clear verification steps, and a structure that scales to real agent workflows.