Building a Conversational AI with Claude and ChatGPT APIs: A Practical Guide

Share:
Tutorial
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}
))}
setInput(e.target.value)} placeholder="Ask a technical question..." disabled={isLoading} style={{ flex: 1, padding: '10px', borderRadius: '4px', border: '1px solid #ccc' }} />
); }
⚠️ Advanced Ingestion Warning: Anthropic’s Messages API enforces strict array alternation. If you pass consecutive messages with the same role (e.g., '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 dev

Open 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 ReadableStream wrappers.
  • 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.

Share:

Was this tutorial helpful?