n4nAI

TypeScript setup for Vercel AI SDK and n4n.ai

A step-by-step guide to configuring TypeScript with Vercel AI SDK and n4n.ai for production-ready LLM inference with automatic fallback and usage metering.

n4n Team4 min read875 words

Audio narration

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

The vercel ai sdk typescript setup n4n.ai combination gives you a type-safe streaming interface backed by 240+ models behind a single OpenAI-compatible endpoint. This guide walks through a minimal, production-ready configuration you can drop into a new or existing Next.js project. You’ll end up with a working chat route that streams tokens, handles provider failures automatically, and surfaces per-token usage for observability.

Step 1: Initialize the project and install dependencies

Create a Next.js app with TypeScript and the App Router if you don’t have one already:

npx create-next-app@latest ai-chat --typescript --tailwind --eslint --app --src-dir --import-alias "@/*"
cd ai-chat

Install the Vercel AI SDK core and the OpenAI-compatible provider. The SDK’s openai package works directly with n4n.ai because n4n.ai exposes an OpenAI-compatible REST surface:

npm install ai @ai-sdk/openai zod

Install the dev dependencies for type checking and local development:

npm install -D @types/node typescript

Step 2: Configure TypeScript for strict mode

Open tsconfig.json and ensure strict is enabled. The AI SDK relies on discriminated unions and generic inference that work best under strict mode. Add these compiler options if they’re missing:

{
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
    "moduleResolution": "bundler",
    "target": "ES2022",
    "lib": ["ES2022", "DOM", "DOM.Iterable"],
    "jsx": "preserve",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "allowSyntheticDefaultImports": true,
    "forceConsistentCasingInFileNames": true,
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"]
    }
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
  "exclude": ["node_modules"]
}

Run npx tsc --noEmit to verify the configuration compiles cleanly.

Step 3: Add environment variables

Create .env.local at the repository root. Use the n4n.ai base URL and your API key. The key format follows the standard sk- prefix:

# .env.local
N4N_BASE_URL="https://api.n4n.ai/v1"
N4N_API_KEY="sk-your-key-here"

Add .env.local to .gitignore if it isn’t already there. Never commit API keys.

Step 4: Create a typed client wrapper

Create src/lib/n4n-client.ts. This wrapper centralizes the OpenAI-compatible client configuration and adds runtime validation for the environment variables. It also re-exports the model IDs you plan to use so the rest of your codebase stays typed.

// src/lib/n4n-client.ts
import { createOpenAI } from "@ai-sdk/openai";
import { z } from "zod";

const envSchema = z.object({
  N4N_BASE_URL: z.string().url(),
  N4N_API_KEY: z.string().min(1),
});

const env = envSchema.parse(process.env);

export const n4n = createOpenAI({
  baseURL: env.N4N_BASE_URL,
  apiKey: env.N4N_API_KEY,
});

// Model identifiers you intend to route to. Extend as needed.
export const models = {
  "gpt-4o-mini": n4n("gpt-4o-mini"),
  "claude-3-5-sonnet": n4n("claude-3-5-sonnet-20241022"),
  "llama-3.1-70b": n4n("meta-llama/llama-3.1-70b-instruct"),
} as const;

export type ModelId = keyof typeof models;

The as const assertion preserves literal types so TypeScript knows exactly which strings are valid model IDs. The createOpenAI factory returns a provider that implements the Vercel AI SDK’s LanguageModelV1 interface.

Step 5: Build a streaming chat route

Create src/app/api/chat/route.ts. This route accepts a JSON body with messages and an optional model field, validates the payload, and streams the response using the SDK’s streamText function. The route also demonstrates how to read the provider’s usage metadata, which n4n.ai forwards from the upstream provider.

// src/app/api/chat/route.ts
import { streamText } from "ai";
import { models, ModelId } from "@/lib/n4n-client";
import { z } from "zod";

const bodySchema = z.object({
  messages: z.array(
    z.object({
      role: z.enum(["user", "assistant", "system", "tool"]),
      content: z.string(),
    })
  ),
  model: z.enum(["gpt-4o-mini", "claude-3-5-sonnet", "llama-3.1-70b"]).optional(),
  temperature: z.number().min(0).max(2).optional(),
  maxTokens: z.number().int().positive().optional(),
});

export async function POST(req: Request) {
  const json = await req.json();
  const parseResult = bodySchema.safeParse(json);

  if (!parseResult.success) {
    return Response.json(
      { error: "Invalid request body", issues: parseResult.error.flatten() },
      { status: 400 }
    );
  }

  const { messages, model = "gpt-4o-mini", temperature, maxTokens } = parseResult.data;

  const result = await streamText({
    model: models[model],
    messages,
    temperature,
    maxTokens,
    onFinish: ({ usage, finishReason }) => {
      // Log or emit metrics. n4n.ai forwards provider usage fields.
      console.log("chat completion", {
        model,
        finishReason,
        promptTokens: usage.promptTokens,
        completionTokens: usage.completionTokens,
        totalTokens: usage.totalTokens,
      });
    },
  });

  return result.toDataStreamResponse({
    // Send usage and finish reason in the final chunk for client-side telemetry
    sendUsage: true,
    sendFinishReason: true,
  });
}

The toDataStreamResponse helper returns a ReadableStream formatted for the AI SDK’s useChat hook. The onFinish callback fires after the stream completes and gives you access to token counts without buffering the entire response.

Step 6: Add a minimal chat UI

Replace src/app/page.tsx with a client component that uses the useChat hook. This keeps the example self-contained so you can verify the end-to-end flow in the browser.

// src/app/page.tsx
"use client";

import { useChat } from "ai/react";
import { useState } from "react";

export default function ChatPage() {
  const [model, setModel] = useState<"gpt-4o-mini" | "claude-3-5-sonnet" | "llama-3.1-70b">("gpt-4o-mini");
  const { messages, input, handleInputChange, handleSubmit, isLoading, error, status } = useChat({
    api: "/api/chat",
    body: { model },
    onFinish: (message, { usage, finishReason }) => {
      console.log("client finish", { finishReason, usage });
    },
    onError: (err) => {
      console.error("chat error", err);
    },
  });

  return (
    <main className="flex min-h-screen flex-col items-center p-8 gap-4">
      <header className="w-full max-w-2xl">
        <h1 className="text-3xl font-semibold">Vercel AI SDK + n4n.ai</h1>
        <p className="text-muted-foreground mt-1">
          Streaming chat with automatic provider fallback
        </p>
      </header>

      <div className="w-full max-w-2xl flex flex-col gap-4">
        <div className="flex gap-2">
          <select
            value={model}
            onChange={(e) => setModel(e.target.value as typeof model)}
            className="border rounded px-2 py-1"
          >
            <option value="gpt-4o-mini">GPT-4o Mini</option>
            <option value="claude-3-5-sonnet">Claude 3.5 Sonnet</option>
            <option value="llama-3.1-70b">Llama 3.1 70B</option>
          </select>
          <span className="text-sm text-muted-foreground self-center">
            Status: {status}
          </span>
        </div>

        <div className="flex flex-col gap-2 max-h-[50vh] overflow-auto border rounded p-4">
          {messages.map((m) => (
            <div key={m.id} className={`flex gap-2 ${m.role === "user" ? "justify-end" : ""}`}>
              <div
                className={`max-w-[80%] rounded-lg px-4 py-2 ${
                  m.role === "user" ? "bg-blue-600 text-white" : "bg-gray-100"
                }`}
              >
                {m.content}
              </div>
            </div>
          ))}
          {isLoading && (
            <div className="flex justify-start">
              <div className="bg-gray-100 rounded-lg px-4 py-2 animate-pulse">…</div>
            </div>
          )}
        </div>

        <form onSubmit={handleSubmit} className="flex gap-2">
          <input
            value={input}
            onChange={handleInputChange}
            placeholder="Type a message…"
            disabled={isLoading}
            className="flex-1 border rounded px-3 py-2 disabled:opacity-50"
          />
          <button type="submit" disabled={isLoading || !input.trim()} className="px-4 py-2 bg-blue-600 text-white rounded disabled:opacity-50">
            Send
          </button>
        </form>

        {error && (
          <p className="text-red-600 text-sm">Error: {error.message}</p>
        )}
      </div>
    </main>
  );
}

The useChat hook handles the streaming protocol, message history, and optimistic updates. Passing body: { model } sends the selected model ID to the route on every request.

Step 7: Verify the integration locally

Start the development server:

npm run dev

Open http://localhost:3000. Select a model, type a message, and press Send. You should see tokens stream in real time. Open the browser console and the terminal running next dev — both will log the onFinish payload with promptTokens, completionTokens, and totalTokens.

If the stream stalls or returns an error, check the terminal for the route’s console output. Common issues:

  • 401 Unauthorized: Verify N4N_API_KEY in .env.local matches the key issued in your n4n.ai dashboard.
  • 404 Model not found: Ensure the model ID in models matches an identifier n4n.ai exposes. The gateway normalizes provider-specific IDs (for example, meta-llama/llama-3.1-70b-instruct).
  • Rate limit / provider degraded: n4n.ai automatically fails over to a healthy provider for the same model family. The stream will continue without client-side intervention.

Step 8: Add production-grade error handling

The minimal route above returns a generic 500 for upstream failures. In production you want to surface retryable errors and respect the Retry-After header when the gateway signals backpressure. Update src/app/api/chat/route.ts:

// src/app/api/chat/route.ts (updated)
import { streamText, StreamTextOnFinishCallback } from "ai";
import { models, ModelId } from "@/lib/n4n-client";
import { z } from "zod";

const bodySchema = z.object({
  messages: z.array(
    z.object({
      role: z.enum(["user", "assistant", "system", "tool"]),
      content: z.string(),
    })
  ),
  model: z.enum(["gpt-4o-mini", "claude-3-5-sonnet", "llama-3.1-70b"]).optional(),
  temperature: z.number().min(0).max(2).optional(),
  maxTokens: z.number().int().positive().optional(),
});

function isRetryableError(error: unknown): error is { status: number; headers: Headers } {
  return (
    typeof error === "object" &&
    error !== null &&
    "status" in error &&
    typeof (error as Record<string, unknown>).status === "number" &&
    "headers" in error
  );
}

export async function POST(req: Request) {
  const json = await req.json();
  const parseResult = bodySchema.safeParse(json);

  if (!parseResult.success) {
    return Response.json(
      { error: "Invalid request body", issues: parseResult.error.flatten() },
      { status: 400 }
    );
  }

  const { messages, model = "gpt-4o-mini", temperature, maxTokens } = parseResult.data;

  try {
    const result = await streamText({
      model: models[model],
      messages,
      temperature,
      maxTokens,
      onFinish: ((result) => {
        console.log("chat completion", {
          model,
          finishReason: result.finishReason,
          promptTokens: result.usage.promptTokens,
          completionTokens: result.usage.completionTokens,
          totalTokens: result.usage.totalTokens,
        });
      }) satisfies StreamTextOnFinishCallback<typeof models[ModelId]>,
    });

    return result.toDataStreamResponse({
      sendUsage: true,
      sendFinishReason: true,
    });
  } catch (err) {
    if (isRetryableError(err) && err.status === 429) {
      const retryAfter = err.headers.get("Retry-After");
      return Response.json(
        { error: "Rate limited", retryAfter: retryAfter ? Number(retryAfter) : undefined },
        { status: 429, headers: retryAfter ? { "Retry-After": retryAfter } : {} }
      );
    }

    if (isRetryableError(err) && err.status >= 500) {
      return Response.json(
        { error: "Upstream provider error", status: err.status },
        { status: 502 }
      );
    }

    console.error("Unexpected chat error", err);
    return Response.json({ error: "Internal server error" }, { status: 500 });
  }
}

The isRetryableError guard narrows the caught error to the shape the AI SDK throws for HTTP failures. Returning 429 with Retry-After lets the client implement exponential backoff. Returning 502 for upstream 5xx distinguishes gateway issues from application bugs.

Step 9: Enable edge runtime for lower latency

If you deploy to Vercel, move the route to the Edge runtime to reduce cold-start latency for streaming responses. Add the runtime export at the top of src/app/api/chat/route.ts:

// src/app/api/chat/route.ts
export const runtime = "edge";
// ... rest of the file

The Edge runtime supports ReadableStream and the AI SDK’s streaming helpers natively. Note that console.log in onFinish writes to Vercel’s Edge logs, which you can view in the Vercel dashboard under Functions → Logs.

Step 10: Run type checks and lint before commit

Add a precommit script or CI step that runs the full type check and lint pass:

// package.json
{
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint",
    "typecheck": "tsc --noEmit",
    "check": "npm run typecheck && npm run lint"
  }
}

Run npm run check locally before pushing. The build will fail on type errors, preventing runtime surprises in production.

Verification checklist

  • npm run dev starts without TypeScript errors.
  • http://localhost:3000 loads the chat UI.
  • Sending a message streams tokens incrementally (not a single block after delay).
  • Switching the model dropdown changes the model used for the next request.
  • Terminal shows chat completion log with promptTokens, completionTokens, totalTokens after each response.
  • Browser console shows client finish with matching usage numbers.
  • npm run check exits with code 0.

What you have now

A typed, streaming chat endpoint backed by n4n.ai that:

  • Validates requests and environment at runtime with Zod.
  • Streams tokens to the client with useChat and toDataStreamResponse.
  • Exposes per-completion token usage for cost tracking and observability.
  • Handles rate limits and upstream failures with appropriate HTTP codes.
  • Runs on the Edge runtime for minimal latency.
  • Compiles cleanly under strict TypeScript.

From here you can add tool calling, multi-step agents, or persistent conversation storage — the foundation is solid and typed end to end.

Tagsvercel-ai-sdktypescriptn4n-aisetup

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 getting started with n4n.ai posts →