Per un chatbot Next.js che necessita di più provider di modelli, il backend più utile è quello che centralizza l’autenticazione lasciando all’applicazione la scelta del modello per richiesta. CometAPI offre questo schema: il server mantiene un’unica chiave API e la base URL compatibile con OpenAI https://api.cometapi.com/v1, mentre il campo model della richiesta seleziona un modello di chat disponibile tra GPT, Claude, Gemini o altri.
Questo tutorial realizza un progetto App Router funzionante con una allowlist dei modelli lato server, un Route Handler con streaming, un selettore di modelli nel browser, variabili d’ambiente protette, un’interfaccia chat completa e indicazioni per il deployment. La chiave CometAPI non raggiunge mai il browser.
Che cos’è un’app di chat AI multi-modello?
Un’architettura di chat AI multi-modello colloca un unico backend controllato dall’applicazione tra l’interfaccia utente e diversi provider di modelli. In questo progetto, il browser invia la conversazione a /api/chat, mentre il server mantiene privata la credenziale CometAPI, valida l’ID del modello richiesto e inoltra la richiesta attraverso un’unica API compatibile.
Come funziona il cambio modello in Next.js?
Cambiare modello è una decisione di instradamento. Il browser invia un ID di modello approvato con la conversazione a /api/chat. Il Route Handler aggiunge la credenziale CometAPI, chiama POST /v1/chat/completions e restituisce in streaming la risposta del modello selezionato al browser.
L’endpoint condiviso non rende tutti i modelli identici. Stile di output, comportamento degli strumenti, parametri supportati, limiti di contesto e prezzi possono differire. Mantieni gli ID dei modelli in una allowlist lato server e testa ogni percorso con gli stessi prompt applicativi prima di renderlo disponibile agli utenti.
Cosa costruirai
L’applicazione finale ha un unico backend Next.js e tre percorsi di chat selezionabili:
| Model ID | Ruolo di esempio | Prezzo CometAPI |
|---|---|---|
| gemini-3.7-flash | Chat ad alto volume attenta ai costi | $0.60 input / $3.00 output |
| claude-opus-5 | Ragionamento premium focalizzato | $4 input / $20 output |
| gpt-5.6 | gpt-5.6 è il percorso generico CometAPI per GPT-5.6 e attualmente mappa al tier Sol. | $3.2 input / $16 output nel tier a contesto breve |
Nota sui prezzi: I prezzi mostrati di seguito sono in USD per milione di token, verificati il 21 agosto 2026, e possono variare. Verifica sempre le tariffe correnti sulle pagine dei modelli prima dell’uso in produzione. La tabella è un esempio di instradamento, non una classifica di qualità. GPT-5.6 utilizza un tier di prezzo più alto oltre 272.000 token. Consulta le pagine dei modelli collegate Gemini 3.7 Flash, Claude Opus 5 e GPT-5.6 per le tariffe datate utilizzate qui.
CometAPI prezza i modelli in modo trasparente rispetto all’API ufficiale di ciascun provider. I modelli con pricing ufficiale unificato — OpenAI, Claude, Gemini e simili — vengono fatturati per token a un rapporto 0,8:1 rispetto al prezzo ufficiale, uno sconto del 20%; i modelli senza API ufficiali (MidJourney, Kling, Luma) sono fatturati per chiamata alle tariffe stabilite da CometAPI, anch’esse scontate del 20%. Consulta la Guida ai prezzi di CometAPI per la formula di markup, le tariffe per modello e le unità di fatturazione.
Le tre route coprono compromessi diversi. Gemini 3.7 Flash è il cavallo di battaglia agentico efficiente di Google, con input multimodale e una finestra di contesto da 1.048.576 token per chat ad alto volume, coding e workflow di conoscenza. Claude Opus 5 è il modello di frontiera di Anthropic per il ragionamento, forte su analisi multi-step, codice e scrittura attenta di lunga durata a un prezzo premium. GPT-5.6 è il modello generalista di OpenAI, che bilancia ragionamento, uso di strumenti e drafting su un’ampia finestra di contesto per il traffico quotidiano in produzione.
Prima di iniziare
Ti servono Node.js, npm, un account CometAPI e una chiave API lato server dalla Quick Start di CometAPI. I tre ID modello esatti sopra erano disponibili nel catalogo live il 21 agosto 2026, non contrassegnati come upcoming e esponevano POST /v1/chat/completions.
Il tutorial utilizza queste impostazioni condivise:
- API key:
COMETAPI_API_KEY - Base URL:
https://api.cometapi.com/v1 - Catalog endpoint:
GEThttps://api.cometapi.com/api/models
Step 1: Crea l’app Next.js
npx create-next-app@latest multi-model-chat --ts --app --eslintcd multi-model-chatnpm run dev
Per questa versione non è richiesto alcun SDK del provider di modelli. Il server usa l’API fetch integrata e inoltra lo stream Server-Sent Events a monte.
Step 2: Mantieni la chiave CometAPI sul server
Crea .env.local nella radice del progetto:
COMETAPI_API_KEY=replace_with_your_cometapi_keyCOMETAPI_BASE_URL=https://api.cometapi.com/v1
Non prefissare la chiave con NEXT_PUBLIC_. Next.js espone ai bundle del browser solo le variabili con quel prefisso; la chiave API appartiene al Route Handler lato server.
Step 3: Definisci la policy dei modelli
Inserisci gli ID consentiti nel backend, non solo nel menu a tendina. Un utente può bypassare i controlli del browser e chiamare direttamente la tua route, quindi il server deve rifiutare i valori di modello sconosciuti.
const ALLOWED_MODELS = [ "gemini-3.7-flash", "claude-opus-5", "gpt-5.6",] as const;type ModelId = (typeof ALLOWED_MODELS)[number];
Il client utilizzerà gli stessi tre ID per il selettore, mentre il server rimane la fonte di verità.
Step 4: Metti in streaming CometAPI tramite un Route Handler
Crea app/api/chat/route.ts. La route valida un payload di chat solo testo, chiama CometAPI con stream: true e restituisce lo stream di eventi a monte senza esporre la credenziale.
export const runtime = "nodejs";export const dynamic = "force-dynamic";const ALLOWED_MODELS = [ "gemini-3.7-flash", "claude-opus-5", "gpt-5.6",] as const;type ModelId = (typeof ALLOWED_MODELS)[number];type ChatMessage = { role: "system" | "user" | "assistant"; content: string;};function isModelId(value: unknown): value is ModelId { return ( typeof value === "string" && (ALLOWED_MODELS as readonly string[]).includes(value) );}function isChatMessage(value: unknown): value is ChatMessage { if (typeof value !== "object" || value === null) return false; const message = value as Record<string, unknown>; return ( ["system", "user", "assistant"].includes(String(message.role)) && typeof message.content === "string" && message.content.length > 0 && message.content.length <= 20_000 );}export async function POST(request: Request) { const apiKey = process.env.COMETAPI_API_KEY; const baseUrl = ( process.env.COMETAPI_BASE_URL || "https://api.cometapi.com/v1" ).replace(/\/$/, ""); if (!apiKey) { return Response.json( { error: "COMETAPI_API_KEY is not configured." }, { status: 500 }, ); } let payload: { model?: unknown; messages?: unknown }; try { payload = await request.json(); } catch { return Response.json({ error: "Invalid JSON body." }, { status: 400 }); } if (!isModelId(payload.model)) { return Response.json({ error: "Unsupported model ID." }, { status: 400 }); } if ( !Array.isArray(payload.messages) || payload.messages.length === 0 || payload.messages.length > 50 || !payload.messages.every(isChatMessage) ) { return Response.json( { error: "messages must contain 1 to 50 valid text messages." }, { status: 400 }, ); } const upstream = await fetch(`${baseUrl}/chat/completions`, { method: "POST", headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json", }, body: JSON.stringify({ model: payload.model, messages: payload.messages, stream: true, }), cache: "no-store", signal: request.signal, }); if (!upstream.ok) { const requestId = upstream.headers.get("x-request-id"); console.error("CometAPI request failed", { status: upstream.status, requestId, }); return Response.json( { error: "The selected model request failed.", status: upstream.status, requestId, }, { status: upstream.status }, ); } if (!upstream.body) { return Response.json( { error: "The model returned no response body." }, { status: 502 }, ); } return new Response(upstream.body, { status: 200, headers: { "Content-Type": "text/event-stream; charset=utf-8", "Cache-Control": "no-cache, no-transform", }, });}
L’handler passa a monte il segnale di disconnessione del browser, quindi la chiusura della richiesta può interrompere generazioni non necessarie. Restituisce inoltre un errore sanificato al client mantenendo i dettagli diagnostici nei log del server.
Step 5: Aggiungi selezione del modello e parsing dello stream
Crea un componente client in app/page.tsx. Invia il modello selezionato con la conversazione, analizza ogni evento data: e aggiunge il testo incrementale delta.content all’ultimo messaggio dell’assistente.
Il browser non chiama mai direttamente CometAPI. La sua unica destinazione è la tua route /api/chat, che mantiene la chiave privata e applica la allowlist.
Codice principale del progetto
Il Route Handler sopra è l’intero file app/api/chat/route.ts. Aggiungi la seguente pagina, layout e stylesheet per completare il progetto eseguibile.
app/page.tsx
"use client";import { FormEvent, useState } from "react";const MODEL_OPTIONS = [ { id: "gemini-3.7-flash", label: "Gemini 3.7 Flash" }, { id: "claude-opus-5", label: "Claude Opus 5" }, { id: "gpt-5.6", label: "GPT-5.6" },] as const;type Role = "user" | "assistant";type Message = { role: Role; content: string };function textFromSseLine(line: string): string { const trimmed = line.trim(); if (!trimmed.startsWith("data:")) return ""; const data = trimmed.slice(5).trim(); if (!data || data === "[DONE]") return ""; try { const event = JSON.parse(data); return event.choices?.[0]?.delta?.content ?? ""; } catch { return ""; }}export default function Home() { const [model, setModel] = useState("gemini-3.7-flash"); const [messages, setMessages] = useState<Message[]>([]); const [input, setInput] = useState(""); const [loading, setLoading] = useState(false); const [error, setError] = useState(""); function appendAssistantText(text: string) { if (!text) return; setMessages((current) => { const next = [...current]; const lastIndex = next.length - 1; if (lastIndex >= 0 && next[lastIndex].role === "assistant") { next[lastIndex] = { ...next[lastIndex], content: next[lastIndex].content + text, }; } return next; }); } async function sendMessage(event: FormEvent<HTMLFormElement>) { event.preventDefault(); const content = input.trim(); if (!content || loading) return; const outgoing: Message[] = [...messages, { role: "user", content }]; setMessages([...outgoing, { role: "assistant", content: "" }]); setInput(""); setError(""); setLoading(true); try { const response = await fetch("/api/chat", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ model, messages: outgoing }), }); if (!response.ok) { const body = await response.json().catch(() => ({})); throw new Error(body.error || `Request failed with ${response.status}`); } if (!response.body) throw new Error("Streaming is not available."); const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ""; while (true) { const { value, done } = await reader.read(); buffer += decoder.decode(value, { stream: !done }); const lines = buffer.split("\n"); buffer = lines.pop() ?? ""; for (const line of lines) { appendAssistantText(textFromSseLine(line)); } if (done) { appendAssistantText(textFromSseLine(buffer)); break; } } } catch (requestError) { setError( requestError instanceof Error ? requestError.message : "Request failed.", ); } finally { setLoading(false); } } return ( <main className="shell"> <section className="chat"> <header> <p className="eyebrow">Next.js + CometAPI</p> <h1>Multi-model chat</h1> <label> Model <select value={model} onChange={(event) => setModel(event.target.value)} disabled={loading} > {MODEL_OPTIONS.map((option) => ( <option key={option.id} value={option.id}> {option.label} </option> ))} </select> </label> </header> <div className="messages" aria-live="polite"> {messages.length === 0 ? ( <p className="empty">Choose a model and send a message.</p> ) : ( messages.map((message, index) => ( <article className={message.role} key={`${message.role}-${index}`}> <b>{message.role === "user" ? "You" : "Assistant"}</b> <p>{message.content || "…"}</p> </article> )) )} </div> <form onSubmit={sendMessage}> <textarea value={input} onChange={(event) => setInput(event.target.value)} placeholder="Ask something…" rows={3} maxLength={20_000} /> <button disabled={loading || !input.trim()} type="submit"> {loading ? "Streaming…" : "Send"} </button> </form> {error ? <p className="error">{error}</p> : null} </section> </main> );}
app/layout.tsx
import type { Metadata } from "next";import "./globals.css";export const metadata: Metadata = { title: "Multi-Model Chat", description: "A streaming Next.js chat app powered by CometAPI.",};export default function RootLayout({ children,}: Readonly<{ children: React.ReactNode }>) { return ( <html lang="en"> <body>{children}</body> </html> );}
app/globals.css
:root { color-scheme: dark; font-family: Arial, sans-serif; background: #07111f; color: #eef4ff;}* { box-sizing: border-box; }body { margin: 0; }button, select, textarea { font: inherit; }/* Layout shell and chat card */.shell { min-height: 100vh; display: grid; place-items: center; padding: 32px 16px; }.chat { width: min(820px, 100%); background: #0d1b2e; border: 1px solid #223957; border-radius: 20px; padding: 24px; }/* Message list and bubbles */.messages { min-height: 360px; display: grid; align-content: start; gap: 12px; margin: 24px 0; }.messages article { max-width: 85%; padding: 12px 14px; border-radius: 14px; white-space: pre-wrap; }.user { justify-self: end; background: #164f8f; }.assistant { justify-self: start; background: #182a42; }/* Form controls, buttons, and error states follow the same dark theme. */
Step 6: Esegui e distribuisci l’app
npm run dev
Apri http://localhost:3000, seleziona un modello e invia un messaggio. Per un deployment Node.js di produzione, aggiungi COMETAPI_API_KEY e COMETAPI_BASE_URL alle impostazioni d’ambiente lato server dell’host, quindi esegui:
npm run buildnpm run start
Usa un ambiente di hosting che supporti risposte in streaming. Un export statico non può eseguire il Route Handler /api/chat.
Testa la route di streaming
Con il server di sviluppo in esecuzione, chiama direttamente il tuo backend:
curl -N http://localhost:3000/api/chat \ -H "Content-Type: application/json" \ -d '{ "model": "gemini-3.7-flash", "messages": [ {"role": "user", "content": "Explain model routing in two sentences."} ] }'
Una richiesta riuscita restituisce Server-Sent Events. Gli ID esatti e il testo variano, ma lo stream segue questa forma:
data: {"choices":[{"delta":{"content":"Model"}}]}data: {"choices":[{"delta":{"content":" routing"}}]}data: [DONE]
Il parser del browser legge ogni evento SSE, estrae choices[0].delta.content e aggiunge il testo in streaming al messaggio dell’assistente man mano che arrivano i chunk.
Errori di integrazione comuni
| Sintomo | Causa | Correzione |
|---|---|---|
| Errore di autenticazione 401 | Chiave lato server mancante o non valida | Imposta COMETAPI_API_KEY; non esporla con NEXT_PUBLIC_. |
| 404 o route errata | La base URL manca di /v1 | Usa https://api.cometapi.com/v1. |
| 400 modello non supportato | L’ID non è nella allowlist del backend | Usa un ID di modello testuale live esatto e aggiorna entrambi i selettori. |
| La risposta arriva tutta insieme | L’host o il proxy bufferizza lo stream | Disabilita la trasformazione della risposta e usa un deployment Node.js compatibile con lo streaming. |
Prepara il backend della chat per la produzione
- Autentica i tuoi utenti. Non esporre una route pubblica che consumi crediti per traffico anonimo.
- Applica rate limit per utente e IP. Limita stream concorrenti, richieste al minuto, conteggio dei messaggi e lunghezza dei messaggi.
- Mantieni la allowlist dei modelli lato server. Il selettore del browser è una comodità, non un confine di sicurezza.
- Valida il catalogo live durante il deployment. Interroga
GEThttps://api.cometapi.com/api/modelse fallisci il rilascio se un ID configurato è upcoming, indisponibile o privo dell’endpoint chat-completions. - Traccia i costi per route. Registra il modello selezionato, l’ID richiesta, la latenza, l’uso di token e l’ID utente. Imposta quote della chiave o limiti di spesa nella dashboard CometAPI dove opportuno.
- Gestisci disconnessioni e timeout. Preserva
request.signal, imposta un timeout applicativo e interrompi il lavoro quando il client abbandona. - Non nascondere errori di configurazione con fallback. Superficia risposte 400 e 401. Usa un altro modello solo per un insieme limitato di errori ripetibili e solo quando lo schema della richiesta è compatibile; vedi la Guida al fallback del modello CometAPI per una catena di fallback a due livelli (CometAPI primaria → modello di fallback CometAPI → provider ufficiale).
- Redigi i log. Tieni chiavi API, prompt completi e output di modelli sensibili fuori dai log di errore in produzione.
Un unico backend Next.js, più scelte di modelli
Lo switch del modello appartiene alla policy del tuo server, non ad account separati dei provider. Un Route Handler Next.js può mantenere privata un’unica chiave CometAPI, accettare un ID di modello approvato per richiesta e mettere in streaming il modello selezionato tramite un unico endpoint compatibile con OpenAI. Il frontend rimane semplice, mentre il backend conserva controllo su accesso, validazione, osservabilità e costi. Lo stesso account e la stessa chiave raggiungono anche le API native di immagini e video di CometAPI — come Flux per la generazione di immagini e Kling per la generazione di video — quindi questo backend di chat può estendersi a workflow multimodali senza una seconda integrazione.
Usa la directory pubblica dei modelli di CometAPI per la scoperta dei modelli e GET https://api.cometapi.com/api/models per la validazione automatizzata dell’instradamento.
Domande frequenti
Posso usare più modelli AI in un’unica app Next.js?
Sì. Mantieni un’unica route lato server e passa un ID di modello in allowlist con ogni richiesta. L’interfaccia del browser può offrire scelte di modelli, mentre il server controlla quali ID sono accettati.
Come passo tra GPT, Claude e Gemini?
Invia l’ID del modello selezionato nel corpo della richiesta. Il server lo valida rispetto a una allowlist e inoltra lo stesso payload di chat al modello scelto tramite l’endpoint compatibile con OpenAI.
Dove devo archiviare la mia chiave API di CometAPI?
Conservala in una variabile d’ambiente lato server come COMETAPI_API_KEY. Non esporla tramite una variabile NEXT_PUBLIC_ o codice lato client.
Un’API compatibile con OpenAI rende tutti i modelli intercambiabili?
No. La forma della richiesta è portabile, ma i modelli possono differire per parametri supportati, limiti di contesto, comportamento degli strumenti, stile di output, latenza e prezzo. Testa ogni modello in allowlist con i tuoi prompt di produzione.
I Route Handler di Next.js possono mettere in streaming risposte AI?
Sì. Un Route Handler può restituire lo stream di eventi a monte con un content type text/event-stream, a condizione che la piattaforma di deployment e qualsiasi proxy a monte non bufferizzino la risposta.
Posso distribuire questa app come sito statico Next.js?
No. Un export statico non può eseguire il Route Handler /api/chat né proteggere la chiave API. Usa un runtime server compatibile, come Node.js, che supporti risposte in streaming.
