DEV Community

Cover image for Building a Conversational AI with Claude and ChatGPT APIs: A Practical Guide
Gate of AI
Gate of AI

Posted on • Originally published at gateofai.com

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

๐Ÿš€ Technical Briefing: This tutorial is part of our deep-dive series on Agentic Workflows at Gate of AI. For the full technical breakdown, interactive code sandbox, and the native Arabic translation, visit the original article here.

<span>Tutorial</span>
<span>Intermediate</span>
<span>โฑ 35 min read</span>
<span>ยฉ Gate of AI 2026-06-02</span>
Enter fullscreen mode Exit fullscreen mode

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 =&gt; ({
                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 =&gt; m.role === 'system')?.content || 'You are a precise assistant.';
        const conversationHistory = messages.filter(m =&gt; m.role !== 'system');

        const response = await anthropic.messages.create({
            model: 'claude-3-5-haiku-20241022',
            max_tokens: 1024,
            system: systemMessage,
            messages: conversationHistory.map(msg =&gt; ({
                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 });
}
Enter fullscreen mode Exit fullscreen mode

}

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) =&gt; {
    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) =&gt; [...prev, { role: 'assistant', content: data.reply }]);
    } catch (err) {
        console.error('Inference Error:', err);
        setMessages((prev) =&gt; [...prev, { role: 'system', content: `Error: ${err.message}` }]);
    } finally {
        setIsLoading(false);
    }
};

return (


            <h2>Multi-Model Control Panel</h2>
             setProvider(e.target.value)} disabled={isLoading}&gt;
                Engine: GPT-4o-Mini
                Engine: Claude-3.5-Haiku




            {messages.filter(m =&gt; m.role !== 'system').map((msg, idx) =&gt; (

                    <span>
                        <strong>{msg.role === 'user' ? 'You' : 'AI'}:</strong> {msg.content}
                    </span>

            ))}



             setInput(e.target.value)} 
                placeholder="Ask a technical question..." 
                disabled={isLoading}
                style={{ flex: 1, padding: '10px', borderRadius: '4px', border: '1px solid #ccc' }}
            /&gt;

                {isLoading ? 'Processing...' : 'Send'}



);
Enter fullscreen mode Exit fullscreen mode

}

โš ๏ธ 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.

Top comments (0)