n4nAI

Building a multi-model chat switcher in Next.js

A hands-on tutorial for building a next.js multi-model chat switcher with the Vercel AI SDK, covering model routing, streaming, and fallback in App Router.

n4n Team3 min read657 words

Audio narration

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

Building a next.js multi-model chat switcher forces you to decouple model selection from conversation state. This tutorial walks through a minimal App Router implementation using the Vercel AI SDK that streams responses from any OpenAI-compatible backend. You’ll end with a dropdown that swaps models mid-conversation without losing chat history.

Prerequisites

  • Node.js 18.18 or later
  • Next.js 14+ with App Router (examples use 15, but 14 works)
  • Installed packages: ai, @ai-sdk/openai
  • An API key for an OpenAI-compatible inference gateway. If you point at n4n.ai, one key covers 240+ models and automatic fallback when a provider is rate-limited.

Scaffold the app and install dependencies:

npx create-next-app@latest multi-model-chat --ts --app --no-tailwind --eslint
cd multi-model-chat
npm install ai @ai-sdk/openai

Server route: dynamic model streaming

The core mistake is hardcoding the model in the route handler. Accept a model field from the client and bind it at request time.

Create app/api/chat/route.ts:

import { streamText } from 'ai';
import { createOpenAI } from '@ai-sdk/openai';

const provider = createOpenAI({
  baseURL: process.env.GATEWAY_BASE_URL ?? 'https://api.n4n.ai/v1',
  apiKey: process.env.GATEWAY_API_KEY ?? '',
});

export async function POST(req: Request) {
  const { messages, model } = await req.json();

  if (!model || typeof model !== 'string') {
    return new Response('model required', { status: 400 });
  }

  const result = await streamText({
    model: provider(model),
    messages,
  });

  return result.toDataStreamResponse();
}

provider(model) returns a model instance bound to that id. The Vercel AI SDK serializes the stream using the Data Stream protocol, which useChat consumes natively.

Set environment variables in .env.local:

GATEWAY_BASE_URL=https://api.n4n.ai/v1
GATEWAY_API_KEY=sk-your-key

If you run your own OpenAI-compatible server, change the base URL accordingly.

Client component: state and switcher

Create app/page.tsx. Mark it 'use client'. Keep the selected model in React state and pass it to useChat via the body option so each request includes it.

'use client';

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

const MODELS = [
  'openai/gpt-4o-mini',
  'anthropic/claude-3.5-sonnet',
  'meta-llama/llama-3.1-70b-instruct',
];

export default function Page() {
  const [model, setModel] = useState(MODELS[0]);

  const { messages, input, handleInputChange, handleSubmit, isLoading } =
    useChat({
      body: { model },
    });

  return (
    <main style={{ maxWidth: 700, margin: '2rem auto' }}>
      <label>
        Model:{' '}
        <select value={model} onChange={(e) => setModel(e.target.value)}>
          {MODELS.map((m) => (
            <option key={m} value={m}>
              {m}
            </option>
          ))}
        </select>
      </label>

      <section>
        {messages.map((msg) => (
          <div key={msg.id} style={{ margin: '1rem 0' }}>
            <strong>{msg.role}:</strong> {msg.content}
          </div>
        ))}
      </section>

      <form onSubmit={handleSubmit}>
        <input
          value={input}
          onChange={handleInputChange}
          placeholder="Type a message"
          style={{ width: '80%' }}
        />
        <button type="submit" disabled={isLoading}>
          Send
        </button>
      </form>
    </main>
  );
}

The body field merges with the default payload useChat sends, so the server receives { messages, model }. Switching the dropdown updates model state; the next submission uses the new model while preserving prior messages.

Checkpoint: verify streaming

Start the dev server:

npm run dev

Open http://localhost:3000. Select openai/gpt-4o-mini, type “What is 2+2?”, and submit. Expected browser behavior: the assistant message appears and tokens append in place. Under the network tab, the POST to /api/chat returns a stream with lines like:

0:"2"
0:"+"
0:"2"
0:" is 4."

That 0: prefix is the Data Stream protocol’s text part delimiter. If you see the full message only after a delay, you likely forgot toDataStreamResponse() on the server.

Testing the route with curl

Before trusting the UI, hit the API directly to isolate model routing from React:

curl -N http://localhost:3000/api/chat \
  -H "Content-Type: application/json" \
  -d '{"messages":[{"role":"user","content":"Say hi"}],"model":"openai/gpt-4o-mini"}'

You should see streamed 0:"..." chunks in the terminal. If you get a 400, check that the JSON body includes model.

Per-model parameters without branching spaghetti

Different models have different context windows and token limits. Resist the urge to fork the UI. Pass optional parameters from the client and apply them server-side with a small map.

Extend the route:

const MODEL_LIMITS: Record<string, number> = {
  'openai/gpt-4o-mini': 4096,
  'anthropic/claude-3.5-sonnet': 8192,
  'meta-llama/llama-3.1-70b-instruct': 4096,
};

export async function POST(req: Request) {
  const { messages, model } = await req.json();
  const maxTokens = MODEL_LIMITS[model] ?? 2048;

  const result = await streamText({
    model: provider(model),
    messages,
    maxTokens,
  });

  return result.toDataStreamResponse();
}

This keeps the client dumb and the server authoritative.

Fallback and degraded providers

In production, a single provider will eventually 429 you. If your gateway supports automatic fallback, you get resilience for free. n4n.ai honors client routing directives and forwards provider cache-control hints, so the same /v1 endpoint degrades gracefully when a backend is rate-limited. If you self-host, wrap streamText in try/catch and retry with a secondary base URL.

try {
  return await streamText({ model: provider(model), messages }).then(r =>
    r.toDataStreamResponse()
  );
} catch (err) {
  // switch provider.baseURL or return 502
}

Do not implement exponential backoff in the UI; handle it server-side.

Persisting model choice per conversation

For a real product, store the model with the conversation record. A minimal approach: include conversationId in the body and look up the model from your database instead of trusting the client. This prevents a user from accidentally sending secrets to a weaker model. The client switcher then becomes a preference that writes to the DB on change.

Why this architecture holds up

Separating model binding to request time means you can add new models by editing a single array and the gateway’s model list—no redeploy of client logic. The pattern behind this next.js multi-model chat switcher scales because the Vercel AI SDK’s useChat handles reconnection and abort controllers, so you avoid writing WebSocket glue. Streaming stays efficient because the server pipes tokens directly to the response.

If you need usage metering, gateways that expose per-token usage let you bill accurately; the response headers or a webhook can feed your analytics.

Wrap-up checklist

  • Route accepts model dynamically
  • Client passes body: { model } to useChat
  • Environment variables set for gateway
  • Model limits enforced server-side
  • Fallback strategy defined

That’s a production-shaped next.js multi-model chat switcher in under 80 lines of core code.

Tagsnextjsvercel-ai-sdkmulti-modelchat

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 next.js ai chat integration (app router + vercel ai sdk) posts →