A vercel ai sdk model picker dropdown lets users switch the underlying LLM without restarting the app or rewriting your chat logic. This guide walks through a production-shaped implementation: a React select bound to Vercel AI SDK’s useChat, a Next.js route that maps the selection to a provider, and streaming that just works.
Step 1: Scaffold the Next.js app and install dependencies
Start with a clean Next.js App Router project. If you already have one, skip the create step.
npx create-next-app@latest model-picker --ts --app --no-tailwind
cd model-picker
npm install ai @ai-sdk/openai @ai-sdk/anthropic zod
The @ai-sdk/openai and @ai-sdk/anthropic packages are the provider bindings. zod is optional but useful for validating the request body in the route.
Create a .env.local with your provider keys:
OPENAI_API_KEY=sk-...
ANTHROPIC_API_KEY=sk-ant-...
You now have a app/ directory. We’ll add a client component and a route handler.
Step 2: Define a model catalog
Hard-coding model strings in the UI is fine for a demo, but a real vercel ai sdk model picker dropdown needs a single source of truth that both the client and server can read. Put it in lib/models.ts.
export type ModelEntry = {
id: string;
label: string;
provider: 'openai' | 'anthropic';
};
export const modelCatalog: ModelEntry[] = [
{ id: 'gpt-4o', label: 'GPT-4o', provider: 'openai' },
{ id: 'gpt-3.5-turbo', label: 'GPT-3.5 Turbo', provider: 'openai' },
{ id: 'claude-3-5-sonnet', label: 'Claude 3.5 Sonnet', provider: 'anthropic' },
];
export const defaultModelId = modelCatalog[0].id;
If you want 240+ models behind one OpenAI-compatible endpoint, a gateway like n4n.ai collapses the provider branching: one endpoint addresses all of them and forwards cache-control hints, so you can call a single factory with any id from the catalog.
Step 3: Build the dropdown UI
Create app/components/chat.tsx. Use the useChat hook from ai/react and bind the selected model to the request body.
'use client';
import { useChat } from 'ai/react';
import { useState } from 'react';
import { modelCatalog, defaultModelId } from '../lib/models';
export function Chat() {
const [modelId, setModelId] = useState(defaultModelId);
const { messages, input, handleInputChange, handleSubmit, error } = useChat({
body: { model: modelId },
});
return (
<div style={{ maxWidth: 700, margin: '0 auto' }}>
<select
value={modelId}
onChange={(e) => setModelId(e.target.value)}
style={{ marginBottom: 12, padding: 6 }}
>
{modelCatalog.map((m) => (
<option key={m.id} value={m.id}>
{m.label}
</option>
))}
</select>
{messages.map((msg) => (
<div key={msg.id} style={{ margin: '8px 0' }}>
<strong>{msg.role}: </strong>
{msg.content}
</div>
))}
<form onSubmit={handleSubmit} style={{ marginTop: 12 }}>
<input
value={input}
onChange={handleInputChange}
placeholder="Type a message…"
style={{ width: '80%', padding: 6 }}
/>
<button type="submit" style={{ padding: '6px 12px' }}>
Send
</button>
</form>
{error && <p style={{ color: 'red' }}>Error: {error.message}</p>}
</div>
);
}
The critical line is body: { model: modelId }. useChat sends that JSON to your route on every request, so the server always knows which model the user picked.
Step 4: Write the dynamic route handler
Create app/api/chat/route.ts. It reads model from the body, resolves the correct provider model object, and streams the response.
import { openai } from '@ai-sdk/openai';
import { anthropic } from '@ai-sdk/anthropic';
import { streamText } from 'ai';
import { modelCatalog } from '../../../lib/models';
export async function POST(req: Request) {
const { messages, model } = await req.json();
const entry = modelCatalog.find((m) => m.id === model);
if (!entry) {
return new Response('Unknown model', { status: 400 });
}
const lm =
entry.provider === 'openai' ? openai(entry.id) : anthropic(entry.id);
const result = await streamText({ model: lm, messages });
return result.toAIStreamResponse();
}
If you used a gateway in Step 2, replace both provider imports with a single createOpenAI({ baseURL: 'https://api.n4n.ai/v1' }) instance and call gateway(entry.id) for every entry. The rest of the code stays identical.
Step 5: Mount the component and stream
Edit app/page.tsx to render the chat client-side.
import { Chat } from './components/chat';
export default function Page() {
return (
<main style={{ padding: 24 }}>
<h2>Model Picker Demo</h2>
<Chat />
</main>
);
}
Vercel AI SDK’s toAIStreamResponse() emits the standard AI stream protocol that useChat consumes out of the box. No manual ReadableStream wiring required. When you submit a message, the dropdown value is posted, the server picks the model, and tokens render incrementally.
Step 6: Persist selection and handle provider errors
A vercel ai sdk model picker dropdown is only useful if the choice survives a refresh. Persist to localStorage and restore on mount.
'use client';
import { useChat } from 'ai/react';
import { useEffect, useState } from 'react';
import { modelCatalog, defaultModelId } from '../lib/models';
const STORAGE_KEY = 'selected-model';
export function Chat() {
const [modelId, setModelId] = useState(defaultModelId);
useEffect(() => {
const saved = localStorage.getItem(STORAGE_KEY);
if (saved) setModelId(saved);
}, []);
const onSelect = (id: string) => {
setModelId(id);
localStorage.setItem(STORAGE_KEY, id);
};
const { messages, input, handleInputChange, handleSubmit, error } = useChat({
body: { model: modelId },
});
// …render select with onSelect instead of setModelId…
}
Provider errors (rate limits, bad keys) surface as error in useChat. Render it, and consider a retry that falls back to a cheaper model if error.message includes 429. The SDK does not auto-fallback; that logic is yours unless your gateway provides it.
Step 7: Verify the integration
Run the dev server and exercise the dropdown end to end.
npm run dev
Open http://localhost:3000. Perform these checks:
- Select GPT-4o, send “What model are you?”, confirm a coherent answer streams in.
- Switch to Claude 3.5 Sonnet, send the same prompt, confirm the response style changes and the network request payload in DevTools shows
"model":"claude-3-5-sonnet". - Refresh the page; the dropdown should reflect your last selection from
localStorage. - Temporarily set a wrong API key in
.env.localfor one provider; the matching dropdown entry should produce a visible error, while the other provider still works.
If all four pass, your vercel ai sdk model picker dropdown is wired correctly. The pattern scales to any number of models: add an entry to modelCatalog, ensure the server can resolve it, and the UI updates with zero further changes.
Where to go next
For multi-user apps, move the catalog to a shared module and validate the incoming model field with Zod to avoid clients requesting arbitrary strings. If you need usage metering per token, capture result.usage on the server before returning the stream, or rely on a gateway that emits per-token metrics. The dropdown itself stays dumb—it only ships an ID, and the server owns the mapping.