n4nAI

Building a multi-tool agent with Vercel AI SDK and n4n.ai

Hands-on tutorial to build a Vercel AI SDK multi-tool agent on n4n.ai's OpenAI-compatible gateway, with runnable TypeScript, tool calling, and step logs.

n4n Team3 min read558 words

Audio narration

Coming soon — every post will get a voice note here.

A multi-tool agent must chain function calls across reasoning steps instead of returning after a single model response. This tutorial builds a vercel ai sdk multi-tool agent n4n.ai using the Vercel AI SDK’s generateText with maxSteps, wired to an OpenAI-compatible inference endpoint for model routing.

Prerequisites

  • Node.js 20+ (native fetch and process.loadEnvFile support)
  • An API key from n4n.ai (set as N4N_API_KEY in .env)
  • Familiarity with TypeScript and Zod
  • npm or pnpm

No vector store or framework magic. We use the primitives the SDK ships.

Project setup

Create a directory and install the minimal dependency set. The Vercel AI SDK core, the OpenAI-compatible provider adapter, and Zod for tool parameter validation:

mkdir multi-tool-agent && cd multi-tool-agent
npm init -y
npm install ai@^4 @ai-sdk/openai@^1 zod dotenv

Create .env:

echo "N4N_API_KEY=sk-your-key-here" > .env

We pin ai v4 because maxSteps and the tool() helper are stable there.

Configure the n4n.ai provider

n4n.ai exposes a single OpenAI-compatible endpoint covering 240+ models, so we point the SDK at it with createOpenAI. The SDK treats it like any other OpenAI-compatible backend; we just override baseURL.

// src/client.ts
import { createOpenAI } from '@ai-sdk/openai';
import dotenv from 'dotenv';

dotenv.config();

export const n4n = createOpenAI({
  baseURL: 'https://api.n4n.ai/v1',
  apiKey: process.env.N4N_API_KEY!,
});

// Pick any model the gateway exposes. Model IDs follow provider/name convention.
export const model = n4n('anthropic/claude-3.5-sonnet');

The gateway forwards provider cache-control hints and honors routing directives, but for this tutorial we let it default to the specified model.

Define tools with Zod schemas

Tools are plain objects built with tool(). Each needs a description, a Zod parameter schema, and an execute function. The model uses the description and schema to decide when to call.

We define three intentionally simple tools: weather lookup, arithmetic eval, and character counting.

// src/tools.ts
import { tool } from 'ai';
import { z } from 'zod';

export const getWeather = tool({
  parameters: z.object({ city: z.string().describe('City name, e.g. Tokyo') }),
  execute: async ({ city }) => {
    // Mocked external call. Swap for real API + key.
    const fakeDb: Record<string, { tempC: number; conditions: string }> = {
      tokyo: { tempC: 21, conditions: 'clear' },
      berlin: { tempC: 14, conditions: 'rain' },
    };
    const data = fakeDb[city.toLowerCase()] ?? { tempC: 20, conditions: 'unknown' };
    return { city, ...data };
  },
});

export const calc = tool({
  parameters: z.object({ expression: z.string().describe('e.g. "(21*9/5)+32"') }),
  execute: async ({ expression }) => {
    // Demo-only eval. In production use a sandboxed math parser.
    const result = Function(`"use strict"; return (${expression});`)();
    return { result };
  },
});

export const countChars = tool({
  parameters: z.object({ text: z.string() }),
  execute: async ({ text }) => ({ length: text.length }),
});

Schema quality matters more than the execution logic. The model will not call a tool if the description is ambiguous.

Build the multi-tool agent loop

The Vercel AI SDK handles the reasoning loop when you pass maxSteps > 1. On each step, it sends the conversation (including prior tool results) back to the model. When the model stops emitting tool calls, the loop ends.

// src/agent.ts
import { generateText } from 'ai';
import { model } from './client';
import { getWeather, calc, countChars } from './tools';

export async function runAgent(prompt: string) {
  const { text, steps } = await generateText({
    model,
    tools: { getWeather, calc, countChars },
    maxSteps: 5,
    prompt,
    onStepFinish: (step) => {
      console.log(`\n--- Step ${step.stepNumber} ---`);
      if (step.toolCalls.length) {
        console.log('Tool calls:', JSON.stringify(step.toolCalls, null, 2));
      }
      if (step.toolResults.length) {
        console.log('Tool results:', JSON.stringify(step.toolResults, null, 2));
      }
    },
  });

  console.log('\n=== Final response ===');
  console.log(text);
  return { text, steps };
}

maxSteps: 5 caps the loop. If the task needs more hops, raise it. Each step incurs a model round-trip, so keep it tight.

Run the agent and inspect output

Write an entrypoint and execute a query that forces two tool calls in sequence:

// src/main.ts
import { runAgent } from './agent';

runAgent(
  'What is the temperature in Tokyo in Fahrenheit? ' +
  'Use the weather tool, then convert with calc.'
);

Run with tsx (install as dev dep) or compile:

npx tsx src/main.ts

Expected console output at checkpoint (abridged):

--- Step 1 ---
Tool calls: [
  {
    "toolName": "getWeather",
    "args": { "city": "Tokyo" }
  }
]
Tool results: [
  {
    "toolName": "getWeather",
    "result": { "city": "Tokyo", "tempC": 21, "conditions": "clear" }
  }
]

--- Step 2 ---
Tool calls: [
  {
    "toolName": "calc",
    "args": { "expression": "(21*9/5)+32" }
  }
]
Tool results: [
  {
    "toolName": "calc",
    "result": { "result": 69.8 }
  }
]

=== Final response ===
The temperature in Tokyo is 21°C, which is 69.8°F.

The agent called getWeather, ingested the result, then called calc on the derived expression. That is the vercel ai sdk multi-tool agent n4n.ai loop working as intended.

Handling tool errors

Real tools fail. Wrap execute in try/catch and return a structured error so the model can recover:

execute: async ({ city }) => {
  try {
    const res = await fetch(`https://api.weather.example/${city}`);
    if (!res.ok) throw new Error(`status ${res.status}`);
    return await res.json();
  } catch (err) {
    return { error: `weather lookup failed: ${String(err)}` };
  }
}

The model sees the error field and can retry with a corrected argument or call a different tool. Because n4n.ai provides automatic fallback when an upstream provider is rate-limited or degraded, a transient model-side 429 does not crash the loop—the gateway reroutes the request.

Streaming and UI integration

For a chat UI, swap generateText for streamText. Tool calls still arrive in steps, but you forward partial text via result.textStream. The tool execution semantics are identical; only the transport changes.

import { streamText } from 'ai';

const result = streamText({
  model,
  tools: { getWeather, calc, countChars },
  maxSteps: 5,
  prompt: 'Count characters in "hello world" then double it with calc.',
});

for await (const delta of result.textStream) {
  process.stdout.write(delta);
}

Extending the agent

The pattern scales to dozens of tools. Keep these rules:

  • One tool per capability. Don’t overload a single tool with branching mode args.
  • Zod schemas must be as strict as possible; describe() every field.
  • Log onStepFinish in dev. In prod, emit step traces to your observability pipe.
  • Set maxSteps based on the worst-case reasoning depth of your domain.

A vercel ai sdk multi-tool agent n4n.ai deployment is just this loop plus your tool implementations and a model ID. The gateway abstracts provider heterogeneity; the SDK abstracts the conversation mechanics.

Tagsvercel-ai-sdkmulti-tool-agentn4n-aitool-calling

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All vercel ai sdk tool & function calling posts →