n4nAI

Function calling with the Vercel AI SDK and n4n.ai

Hands-on tutorial for vercel ai sdk function calling n4n.ai: connect the OpenAI-compatible gateway to TypeScript tools with runnable code and output.

n4n Team2 min read533 words

Audio narration

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

Function calling turns a language model into a controller for your code, and the vercel ai sdk function calling n4n.ai approach gives you a single OpenAI-compatible endpoint that fronts 240+ models without changing your tool definitions. This tutorial builds a Node.js script that registers a weather tool, calls a model through that gateway, and prints the resolved answer with real tool execution.

Prerequisites

  • Node.js 18 or newer (native fetch, ES modules, global crypto).
  • An API key from the gateway (used below as N4N_API_KEY); the endpoint is OpenAI-compatible.
  • Basic TypeScript and a shell.

Scaffold the project:

mkdir vcs-fn-demo && cd vcs-fn-demo
npm init -y
npm install ai @ai-sdk/openai zod dotenv

Edit package.json to add "type": "module". Create a minimal tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "strict": true
  }
}

Configure the OpenAI-compatible provider

The Vercel AI SDK abstracts vendors behind one interface. Because the gateway speaks the OpenAI API shape, we point @ai-sdk/openai at its base URL and use the gateway’s model routing strings.

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

dotenv.config();

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

Model identifiers follow the gateway convention: 'openai/gpt-4o-mini', 'anthropic/claude-3.5-sonnet', etc. Swapping models later requires no tool code changes.

Define a tool with Zod

Tools are typed contracts. The SDK uses Zod to validate arguments and infer TypeScript types end to end.

// 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. "Berlin"'),
  }),
  execute: async ({ city }) => {
    // Stand-in for a real downstream API
    const mock: Record<string, number> = { Berlin: 21, Paris: 18, 'New York': 24 };
    const tempC = mock[city] ?? 20;
    return { city, tempC, conditions: 'clear' };
  },
});

execute must return JSON-serializable data. The model only sees description and the parameter schema, not your implementation.

Run a single function call

Use generateText with tools and maxSteps to let the SDK run the tool and feed the result back for a final answer.

// src/run.ts
import { generateText } from 'ai';
import { gateway } from './client';
import { getWeather } from './tools';

const { text, toolCalls, toolResults, usage } = await generateText({
  model: gateway('openai/gpt-4o-mini'),
  prompt: 'What is the temperature in Berlin right now?',
  tools: { getWeather },
  maxSteps: 2,
});

console.log('Final text:', text);
console.log('Tool calls:', toolCalls);
console.log('Tool results:', toolResults);
console.log('Usage:', usage);

Expected output

Final text: The temperature in Berlin is currently 21°C with clear conditions.
Tool calls: [ { toolName: 'getWeather', args: { city: 'Berlin' } } ]
Tool results: [ { toolName: 'getWeather', result: { city: 'Berlin', tempC: 21, conditions: 'clear' } } ]
Usage: { promptTokens: 68, completionTokens: 14, totalTokens: 82 }

The model emitted a tool call, the SDK executed getWeather, and the model synthesized the final sentence from the returned JSON.

Designing tool parameters

Good tool schemas reduce failed calls. Use .describe() on every field, prefer enums over free strings, and keep nested objects shallow.

const searchDocs = tool({
  parameters: z.object({
    query: z.string().describe('Natural language search query'),
    topK: z.number().int().min(1).max(10).default(3),
  }),
  execute: async ({ query, topK }) => ({ hits: [{ title: 'Onboarding', score: 0.9 }] }),
});

The SDK validates arguments before execute runs. If validation fails, the model receives a structured error and can retry within the maxSteps budget.

Handling tool errors

Never let execute throw without a plan. Return an error-shaped object so the model can recover or explain.

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: `Failed to fetch weather: ${(err as Error).message}` };
  }
}

The model sees { "error": "..." } and can either call a different tool or tell the user the lookup failed.

Using multiple tools

Register several tools at once. The model chooses based on the prompt.

const { text } = await generateText({
  model: gateway('anthropic/claude-3.5-sonnet'),
  prompt: 'What is the weather in Paris and the latest sales figure?',
  tools: { getWeather, getSales },
  maxSteps: 4,
});

Set maxSteps high enough for parallel or sequential calls. The SDK executes independent tools in the same step when the model requests them together.

Streaming with intermediate steps

For chat UIs, streamText exposes partial text and tool events with the same tool objects.

import { streamText } from 'ai';

const result = streamText({
  model: gateway('openai/gpt-4o-mini'),
  prompt: 'Compare weather in Berlin and Paris.',
  tools: { getWeather },
  maxSteps: 3,
});

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

The gateway forwards provider cache-control hints, so repeated tool schemas or system prompts can hit provider prompt caches when the underlying model supports them.

Production considerations

The vercel ai sdk function calling n4n.ai setup stays portable: because the gateway honors client routing directives and provides automatic fallback when a provider is rate-limited or degraded, the same tools object works across model swaps without code changes. Per-token usage metering is returned on every response, so you can pipe usage into your own cost tracker or rate limiter.

const { usage } = await generateText({
  model: gateway('openai/gpt-4o-mini'),
  prompt: 'Ping',
  maxSteps: 1,
});
console.log(usage); // { promptTokens, completionTokens, totalTokens }

Keep execute functions idempotent where possible. Tool calls may be retried by the SDK only if you enable experimental_repairToolCalls or handle them in a wrapper. For long-running tools, return a job id and poll instead of blocking the model turn.

Full script for reference

import { createOpenAI } from '@ai-sdk/openai';
import { generateText, tool } from 'ai';
import { z } from 'zod';
import dotenv from 'dotenv';

dotenv.config();

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

const getWeather = tool({
  parameters: z.object({ city: z.string() }),
  execute: async ({ city }) => {
    const mock: Record<string, number> = { Berlin: 21, Paris: 18 };
    return { city, tempC: mock[city] ?? 20, conditions: 'clear' };
  },
});

const { text, toolResults } = await generateText({
  model: gateway('openai/gpt-4o-mini'),
  prompt: 'Weather in Berlin?',
  tools: { getWeather },
  maxSteps: 2,
});

console.log(text, toolResults);

Run it with npx tsx src/run.ts. The process exits after one resolved turn. Extend maxSteps, add more tools, and swap the model string to build a provider-agnostic agent against one endpoint.

Tagsvercel-ai-sdkn4n-aifunction-callingtypescript

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 function calling in typescript/node.js posts →