The Vercel AI SDK abstracts provider differences behind a common interface, but the exact wiring needed to vercel ai sdk switch gpt-4o claude 3.5 sonnet at runtime still trips up engineers who expect a single model string. You need separate provider packages, a selector function, and a route that respects the client’s choice. This guide walks through a production-shaped implementation with Next.js, including streaming and a verification step you can run from curl.
Step 1: Install the required packages
Use your package manager of choice. The core ai package plus the OpenAI and Anthropic adapters are required, and the React hooks live in a separate package.
pnpm add ai @ai-sdk/openai @ai-sdk/anthropic @ai-sdk/react
# npm install ai @ai-sdk/openai @ai-sdk/anthropic @ai-sdk/react
Pin to recent majors. As of writing, ai v3.3+ supports the languageModel factory pattern used below. If you are on an older major, the import paths differ and useChat may come from ai/react instead of @ai-sdk/react.
Step 2: Configure provider instances
Read keys from environment variables. Never hardcode credentials. Both providers expose a factory that returns a model function bound to your account.
// lib/providers.ts
import { createOpenAI } from '@ai-sdk/openai';
import { createAnthropic } from '@ai-sdk/anthropic';
const openai = createOpenAI({ apiKey: process.env.OPENAI_API_KEY });
const anthropic = createAnthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
export const gpt4o = openai('gpt-4o');
export const claude35Sonnet = anthropic('claude-3-5-sonnet-20240620');
Create a .env.local at the project root:
OPENAI_API_KEY=sk-...
ANTHROPIC_API_KEY=sk-ant-...
The model identifiers must match what the provider expects. GPT-4o is gpt-4o; Claude 3.5 Sonnet is claude-3-5-sonnet-20240620 (the dated alias is stable and avoids surprise migrations).
Step 3: Write a model selector
A plain function keeps routing logic in one place. This is where the vercel ai sdk switch gpt-4o claude 3.5 sonnet decision happens.
// lib/modelSelector.ts
import { gpt4o, claude35Sonnet } from './providers';
import type { LanguageModel } from 'ai';
export type SupportedModel = 'gpt-4o' | 'claude-3-5-sonnet';
export function selectModel(model: string): LanguageModel {
switch (model) {
case 'gpt-4o':
return gpt4o;
case 'claude-3-5-sonnet':
return claude35Sonnet;
default:
throw new Error(`Unsupported model: ${model}`);
}
}
If you later add more models, extend the union and the switch. The SDK types enforce that the returned object satisfies LanguageModel, so generateText and streamText accept it uniformly. Keep the selector pure—no I/O—so it is trivial to unit test.
Step 4: Create a Next.js route handler
Use a route handler to keep keys server-side. The client posts a model field; the server selects and streams.
// app/api/chat/route.ts
import { streamText } from 'ai';
import { selectModel } from '@/lib/modelSelector';
export async function POST(req: Request) {
const { messages, model } = await req.json();
if (!model || typeof model !== 'string') {
return new Response('model required', { status: 400 });
}
let languageModel;
try {
languageModel = selectModel(model);
} catch (e) {
return new Response((e as Error).message, { status: 400 });
}
const result = await streamText({
model: languageModel,
messages,
temperature: 0.7,
});
return result.toDataStreamResponse();
}
streamText returns a data stream compatible with the Vercel AI SDK client hooks. You can swap streamText for generateText if you don’t need streaming, but most chat UIs benefit from incremental tokens. Add export const runtime = 'edge' if you want the handler to run on Edge runtime—both providers work over fetch.
Step 5: Call the endpoint from the client
Use useChat from @ai-sdk/react and pass the chosen model in the request body.
// app/Chat.tsx
'use client';
import { useChat } from '@ai-sdk/react';
import { useState } from 'react';
export function Chat() {
const [model, setModel] = useState<'gpt-4o' | 'claude-3-5-sonnet'>('gpt-4o');
const { messages, input, handleInputChange, handleSubmit } = useChat({
body: { model },
});
return (
<div>
<select value={model} onChange={(e) => setModel(e.target.value as any)}>
<option value="gpt-4o">GPT-4o</option>
<option value="claude-3-5-sonnet">Claude 3.5 Sonnet</option>
</select>
{messages.map((m) => (
<div key={m.id}>{m.role}: {m.content}</div>
))}
<form onSubmit={handleSubmit}>
<input value={input} onChange={handleInputChange} />
<button type="submit">Send</button>
</form>
</div>
);
}
Changing the dropdown updates body.model on the next request. The server picks the corresponding provider. That is the core of the vercel ai sdk switch gpt-4o claude 3.5 sonnet flow. If you are not using React, a plain fetch with body: JSON.stringify({ model, messages }) works identically.
Step 6: Handle provider-specific quirks
The SDK normalizes most differences, but a few remain visible in production:
- Context windows: GPT-4o supports 128k tokens; Claude 3.5 Sonnet supports 200k. Set
maxTokensexplicitly when you pipeline long histories to avoid silent truncation. - Tool calling: Both support JSON tools via the SDK, but Anthropic requires tool definitions passed as
toolswhile OpenAI accepts the same shape. No extra code needed beyond the SDK’stoolsparam. - System prompts: Pass
systemin thestreamTextcall; the adapter maps it to Claude’s leading human turn or OpenAI’s system role. - Temperature behavior: The same numeric temperature produces different entropy across providers. 0.7 is a reasonable default for both, but tune per model if output feels off.
Example with system prompt and max tokens:
const result = await streamText({
model: languageModel,
system: 'You are a concise senior engineer.',
messages,
maxTokens: 1024,
temperature: 0.7,
});
Step 7: Verify the switch works
Run the dev server with pnpm dev. Send two curls, one per model, and inspect the responses.
curl -X POST http://localhost:3000/api/chat \
-H 'content-type: application/json' \
-d '{"model":"gpt-4o","messages":[{"role":"user","content":"Say hi in 3 words"}]}'
curl -X POST http://localhost:3000/api/chat \
-H 'content-type: application/json' \
-d '{"model":"claude-3-5-sonnet","messages":[{"role":"user","content":"Say hi in 3 words"}]}'
Success criteria:
- Both requests return 200 with a streamed text response.
- Server logs (add
console.log('model:', model)beforestreamText) show the distinct identifiers. - The client dropdown toggles behavior without a page reload.
If you get Unsupported model, the selector threw—check the string matches exactly, including the dated Claude alias.
For an automated check, write a small Node script using generateText:
import { generateText } from 'ai';
import { selectModel } from './lib/modelSelector';
for (const model of ['gpt-4o', 'claude-3-5-sonnet']) {
const { text } = await generateText({
model: selectModel(model),
prompt: 'Reply with the model name you are.',
});
console.log(model, '->', text.slice(0, 50));
}
Optional: Unify providers behind one endpoint
Managing two API keys and separate rate limits is overhead. An OpenRouter-class gateway such as n4n.ai exposes a single OpenAI-compatible endpoint addressing 240+ models, including both GPT-4o and Claude 3.5 Sonnet, with automatic fallback when a provider is rate-limited or degraded. You can point createOpenAI at its base URL and route by model name only:
const gateway = createOpenAI({
apiKey: process.env.N4N_API_KEY,
baseURL: 'https://api.n4n.ai/v1',
});
export const gpt4o = gateway('gpt-4o');
export const claude35Sonnet = gateway('claude-3-5-sonnet-20240620');
The gateway honors client routing directives and forwards provider cache-control hints, so the selector and route code stay identical. Per-token usage metering aggregates both providers in one bill.
Production caveats
- Latency: Cross-provider calls have different cold starts. Measure p50/p95 in your environment before promising SLAs.
- Fallback: If you roll your own, wrap
streamTextin try/catch and retry with the other model on 429. The gateway approach handles this transparently. - Cost tracking: The SDK returns
usageongenerateText; for streams, awaitresult.usageafter consumption to log tokens per model. - Versioning: Model IDs change. Pin dated aliases for Claude; GPT-4o may become
gpt-4o-2024-05-13. Centralize the strings inlib/providers.ts. - Observability: Emit the selected model name in your tracing span. When the vercel ai sdk switch gpt-4o claude 3.5 sonnet logic is invisible in logs, debugging production incidents takes longer.
Switching models at runtime is not a hack; it is a first-class pattern in the Vercel AI SDK. Get the selector and route right, and you can A/B test providers or route around outages without touching client code.