The Vercel AI SDK expects an OpenAI-compatible endpoint by default, but production workloads often need a gateway that handles model routing, fallback, and observability in one place. This guide walks through wiring the SDK’s baseURL option to n4n.ai so you can call 240-plus models through a single endpoint while keeping your application code unchanged. You’ll set up the client, configure routing directives, enable streaming, and verify the integration end to end.
Step 1: Install the AI SDK and dependencies
Start with a fresh or existing Node.js project. The AI SDK v4 splits providers into separate packages, so install the OpenAI-compatible client alongside the core library.
npm install ai @ai-sdk/openai zod
If you’re using pnpm or yarn, adjust the command accordingly. The zod dependency is optional but recommended for schema validation in tool calls.
Step 2: Create the gateway client with a custom baseURL
The @ai-sdk/openai package exposes a createOpenAI factory that accepts a baseURL parameter. Point it at the n4n.ai endpoint and pass your API key through the standard apiKey field.
// lib/gateway.ts
import { createOpenAI } from '@ai-sdk/openai';
export const gateway = createOpenAI({
baseURL: 'https://api.n4n.ai/v1',
apiKey: process.env.N4N_API_KEY,
});
Set N4N_API_KEY in your environment. The gateway honors the same authentication scheme as OpenAI, so no custom headers are required.
Step 3: Call a model through the gateway
With the client configured, you can invoke any supported model by its n4n.ai slug. The model identifier maps to the underlying provider automatically.
// app/chat/route.ts
import { streamText } from 'ai';
import { gateway } from '@/lib/gateway';
export async function POST(req: Request) {
const { messages } = await req.json();
const result = streamText({
model: gateway('anthropic/claude-3-5-sonnet-20241022'),
messages,
temperature: 0.3,
maxTokens: 1024,
});
return result.toDataStreamResponse();
}
The streamText helper returns a ReadableStream compatible with the AI SDK’s useChat hook on the frontend. No changes to your UI components are necessary.
Step 4: Use routing directives for model selection
n4n.ai accepts an x-n4n-route header that lets you steer requests without hardcoding model IDs in your application. Pass the directive through the headers option on the model call.
// lib/gateway.ts (extended)
import { createOpenAI } from '@ai-sdk/openai';
export const gateway = createOpenAI({
baseURL: 'https://api.n4n.ai/v1',
apiKey: process.env.N4N_API_KEY,
});
export function routedModel(slug: string, directive?: string) {
const model = gateway(slug);
if (!directive) return model;
return model.withSettings({
headers: {
'x-n4n-route': directive,
},
});
}
Now your route handler can stay generic while the directive controls fallback order, cost tier, or latency preference.
// app/chat/route.ts
import { streamText } from 'ai';
import { routedModel } from '@/lib/gateway';
export async function POST(req: Request) {
const { messages, route } = await req.json();
const result = streamText({
model: routedModel('openai/gpt-4o', route),
messages,
});
return result.toDataStreamResponse();
}
Send {"route": "prefer:cost"} to favor cheaper providers, or {"route": "require:anthropic"} to pin a specific vendor.
Step 5: Enable provider cache-control hints
When a downstream provider returns cache metadata (for example, Anthropic’s prompt caching), n4n.ai forwards the cache-control header in the response. The AI SDK surfaces this through the response.headers property on the stream result.
// app/chat/route.ts
import { streamText } from 'ai';
import { routedModel } from '@/lib/gateway';
export async function POST(req: Request) {
const { messages, route } = await req.json();
const result = streamText({
model: routedModel('anthropic/claude-3-5-sonnet-20241022', route),
messages,
onFinish: async ({ response }) => {
const cacheControl = response.headers.get('cache-control');
if (cacheControl) {
console.log('Cache hint:', cacheControl);
// Persist or metric as needed
}
},
});
return result.toDataStreamResponse();
}
This lets you build dashboards around cache hit rates without parsing provider-specific response bodies.
Step 6: Track per-token usage for cost attribution
n4n.ai includes usage metadata in the final chunk of a streaming response. The AI SDK’s onFinish callback receives a usage object with promptTokens, completionTokens, and totalTokens.
// app/chat/route.ts
import { streamText } from 'ai';
import { routedModel } from '@/lib/gateway';
export async function POST(req: Request) {
const { messages, route, userId } = await req.json();
const result = streamText({
model: routedModel('openai/gpt-4o', route),
messages,
onFinish: async ({ usage, response }) => {
await fetch('https://your-billing-api/record', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
userId,
model: response.modelId,
promptTokens: usage.promptTokens,
completionTokens: usage.completionTokens,
totalTokens: usage.totalTokens,
timestamp: new Date().toISOString(),
}),
});
},
});
return result.toDataStreamResponse();
}
The response.modelId reflects the actual provider model that served the request, which is useful when routing directives result in fallback.
Step 7: Handle automatic fallback in your error flow
When a provider returns 429 or 5xx, n4n.ai retries the request against the next provider in the routing chain. The AI SDK sees a single successful response. If the entire chain exhausts, you receive a standard APIError with a statusCode of 502.
// app/chat/route.ts
import { streamText, APIError } from 'ai';
import { routedModel } from '@/lib/gateway';
export async function POST(req: Request) {
const { messages, route } = await req.json();
try {
const result = streamText({
model: routedModel('openai/gpt-4o', route),
messages,
});
return result.toDataStreamResponse();
} catch (error) {
if (error instanceof APIError && error.statusCode === 502) {
return new Response(
JSON.stringify({ error: 'All providers unavailable' }),
{ status: 503, headers: { 'Content-Type': 'application/json' } }
);
}
throw error;
}
}
This keeps your error surface small while still surfacing actionable status codes to the client.
Step 8: Verify the integration with a curl smoke test
Before wiring the frontend, confirm the gateway responds correctly from the command line.
curl -s -X POST https://api.n4n.ai/v1/chat/completions \
-H "Authorization: Bearer $N4N_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "openai/gpt-4o-mini",
"messages": [{"role": "user", "content": "Say hello in one word"}],
"stream": false
}' | jq '.choices[0].message.content'
Expected output: a short JSON string like "Hello". If you see an authentication error, double-check that N4N_API_KEY is set and valid.
Step 9: Verify streaming from your application
Run your Next.js dev server and hit the route directly.
curl -N -X POST http://localhost:3000/api/chat \
-H "Content-Type: application/json" \
-d '{"messages": [{"role": "user", "content": "Count to three"}]}'
The -N flag disables curl’s output buffering so you see chunks as they arrive. You should observe SSE-formatted lines:
data: {"type":"text-delta","textDelta":"One"}
data: {"type":"text-delta","textDelta":" two"}
data: {"type":"text-delta","textDelta":" three"}
If the stream closes cleanly with a finish message, the integration works end to end.
Step 10: Confirm routing directives change provider behavior
Test that the x-n4n-route header influences which provider serves the request. Use a model available from multiple vendors, such as meta-llama/llama-3.1-70b-instruct.
curl -s -X POST https://api.n4n.ai/v1/chat/completions \
-H "Authorization: Bearer $N4N_API_KEY" \
-H "Content-Type: application/json" \
-H "x-n4n-route: require:together" \
-d '{
"model": "meta-llama/llama-3.1-70b-instruct",
"messages": [{"role": "user", "content": "Who are you?"}],
"stream": false
}' | jq '.model'
The response model field should reflect the Together AI variant (for example, meta-llama/llama-3.1-70b-instruct:together). Repeat with require:fireworks to see the Fireworks variant. This confirms the directive passes through your gateway client correctly.
Step 11: Add request logging for observability
Production systems need visibility into latency, token counts, and fallback events. Wrap the gateway client with a lightweight logger.
// lib/gateway.ts
import { createOpenAI } from '@ai-sdk/openai';
const base = createOpenAI({
baseURL: 'https://api.n4n.ai/v1',
apiKey: process.env.N4N_API_KEY,
});
export const gateway = new Proxy(base, {
get(target, prop) {
const original = target[prop];
if (typeof original !== 'function') return original;
return (...args: unknown[]) => {
const start = Date.now();
const modelId = args[0] as string;
console.log(`[gateway] request model=${modelId}`);
const result = original(...args);
if (result?.then) {
return result.then((r: any) => {
console.log(`[gateway] response model=${modelId} ms=${Date.now() - start} usage=${JSON.stringify(r.usage)}`);
return r;
});
}
return result;
};
},
});
This logs every model call with latency and usage without modifying your route handlers.
Step 12: Deploy and monitor
Deploy to Vercel (or your platform of choice) with the N4N_API_KEY environment variable configured. In the n4n.ai dashboard, you’ll see per-request logs showing the resolved provider, latency percentiles, and token totals. Correlate these with your application logs using the request ID that n4n.ai returns in the x-request-id response header.
// app/chat/route.ts (final)
import { streamText } from 'ai';
import { routedModel } from '@/lib/gateway';
export async function POST(req: Request) {
const { messages, route, userId } = await req.json();
const requestId = crypto.randomUUID();
const result = streamText({
model: routedModel('openai/gpt-4o', route),
messages,
headers: {
'x-request-id': requestId,
},
onFinish: async ({ usage, response }) => {
await fetch('https://your-billing-api/record', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
requestId,
userId,
model: response.modelId,
promptTokens: usage.promptTokens,
completionTokens: usage.completionTokens,
totalTokens: usage.totalTokens,
timestamp: new Date().toISOString(),
}),
});
},
});
return result.toDataStreamResponse({
headers: {
'x-request-id': requestId,
},
});
}
The x-request-id header threads through to n4n.ai and back, letting you trace a single chat turn from frontend to provider and back.
Troubleshooting checklist
- 401 Unauthorized: Verify
N4N_API_KEYis set in the deployment environment, not just locally. - 404 Model not found: Use the exact slug from the n4n.ai model catalog (for example,
anthropic/claude-3-5-sonnet-20241022, notclaude-3.5-sonnet). - Stream hangs: Ensure your platform supports streaming responses (Vercel, AWS Lambda with response streaming, Cloudflare Workers). Some serverless platforms buffer by default.
- Fallback not triggering: Confirm the routing directive allows fallback (
prefer:costvsrequire:anthropic). Arequiredirective pins to one provider and disables fallback. - Usage missing in onFinish: The
usageobject is only populated on the final chunk. If you transform the stream beforeonFinish, ensure you don’t drop the last chunk.
Next steps
You now have a production-ready Vercel AI SDK integration that routes through a single gateway endpoint. From here you can:
- Add edge caching for repeated prompts using the
cache-controlhints. - Build a model router UI that lets users pick cost, speed, or quality tiers.
- Implement per-tenant budgets by gating requests in
onFinishbefore they reach the gateway. - Explore n4n.ai’s batch endpoint for offline workloads — same baseURL, different path.
The pattern stays the same: one client, one baseURL, many models.