Intermediate
⏱ 35 min read
© Gate of AI 2026-06-02
Architect a multi-provider LLM gateway in Next.js using App Router Route Handlers to dynamically toggle production requests between OpenAI and Anthropic architectures while maintaining state context.
Prerequisites
- Node.js 18.x or higher
- Next.js 14.x or 15.x (App Router structure)
- Valid API keys configured in the OpenAI and Anthropic developer consoles
- Familiarity with polymorphic API payloads and React state composition
What We’re Building
Production AI engineering often requires model redundancy or specialized routing—routing creative tasks to Claude and strict structural updates to GPT. In this guide, we are building a unified API abstraction layer that sanitizes, maps, and executes multi-turn conversations across both ecosystems cleanly.
Setup and Installation
Bootstrap a clean Next.js application layer and pull down both official vendor SDK packages:
npx create-next-app@latest multi-provider-chat --ts --no-tailwind --app --src-dir=false
cd multi-provider-chat
npm install openai @anthropic-ai/sdk
Populate your local environment matrix inside .env.local in your root directory:
OPENAI_API_KEY=your_openai_project_secret_key
ANTHROPIC_API_KEY=your_anthropic_live_secret_key
Step 1: Engineering the Unified Model Gateway
Create your server route handler at app/api/chat/route.js. We explicitly instantiate both clients and create an abstraction parser to handle the structural layout differences between OpenAI’s choices array and Anthropic’s content blocks.
import { NextResponse } from 'next/server';
import { OpenAI } from 'openai';
import { Anthropic } from '@anthropic-ai/sdk';
const openai = new OpenAI();
const anthropic = new Anthropic();
export async function POST(req) {
try {
const { provider, messages } = await req.json();
if (!messages || !Array.isArray(messages)) {
return NextResponse.json({ error: 'Malformed message history payload' }, { status: 400 });
}
let replyText = '';
if (provider === 'openai') {
const response = await openai.chat.completions.create({
model: 'gpt-4o-mini',
messages: messages.map(msg => ({
role: msg.role,
content: msg.content
}))
});
replyText = response.choices[0].message.content;
} else if (provider === 'anthropic') {
// Anthropic separates the 'system' message from the historical array
const systemMessage = messages.find(m => m.role === 'system')?.content || 'You are a precise assistant.';
const conversationHistory = messages.filter(m => m.role !== 'system');
const response = await anthropic.messages.create({
model: 'claude-3-5-haiku-20241022',
max_tokens: 1024,
system: systemMessage,
messages: conversationHistory.map(msg => ({
role: msg.role === 'assistant' ? 'assistant' : 'user', // Safe role mapping
content: msg.content
}))
});
replyText = response.content[0].text;
} else {
return NextResponse.json({ error: 'Unsupported provider routing request' }, { status: 400 });
}
return NextResponse.json({ reply: replyText });
} catch (error) {
console.error('Gateway Route Exception:', error);
return NextResponse.json({ error: error.message || 'Internal processing error' }, { status: 500 });
}
}
Step 2: Building the Polymorphic Interface Component
Create your frontend component architecture at app/page.js. We implement standard state updater functions to maintain conversational tracking history while providing an explicit drop-down to switch providers dynamically.
'use client';
import { useState } from 'react';
export default function HomeChatWorkspace() {
const [messages, setMessages] = useState([
{ role: 'system', content: 'You are a helpful software engineering consultant.' }
]);
const [input, setInput] = useState('');
const [provider, setProvider] = useState('openai'); // Default routing value
const [isLoading, setIsLoading] = useState(false);
const handleFormSubmit = async (e) => {
e.preventDefault();
if (!input.trim() || isLoading) return;
const incomingUserNode = { role: 'user', content: input };
setInput('');
setIsLoading(true);
// Update UI state with user message immediately
const updatedHistory = [...messages, incomingUserNode];
setMessages(updatedHistory);
try {
const response = await fetch('/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ provider, messages: updatedHistory })
});
const data = await response.json();
if (!response.ok) throw new Error(data.error || 'Server rejected request');
setMessages((prev) => [...prev, { role: 'assistant', content: data.reply }]);
} catch (err) {
console.error('Inference Error:', err);
setMessages((prev) => [...prev, { role: 'system', content: `Error: ${err.message}` }]);
} finally {
setIsLoading(false);
}
};
return (
Multi-Model Control Panel
{messages.filter(m => m.role !== 'system').map((msg, idx) => (
{msg.role === 'user' ? 'You' : 'AI'}: {msg.content}
))}
);
}
'user' followed by another 'user' node), the SDK will reject the payload with a 400 validation error. Always ensure your mapping pipeline cleanses array structure sequences before firing updates to the Claude engine.Testing Your Routing Engine
Boot your Next.js application workspace locally:
npm run devOpen http://localhost:3000. Select **GPT-4o-Mini** and ask a question. Then immediately switch the dropdown option to **Claude-3.5-Haiku** and ask follow-up questions like “Can you rewrite my last question in Rust?” Notice how the continuous context array maps safely regardless of your provider toggle swaps.
What to Build Next
- Convert the system to utilize streaming chunks concurrently across both components using
ReadableStreamwrappers. - Implement automated model comparison logging to measure execution latency variations between OpenAI and Anthropic routers.
- Add a fall-through logic block that automatically switches providers if one vendor encounters a 503 rate-limit error.