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
modeldynamically - Client passes
body: { model }touseChat - 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.