Para um chatbot em Next.js que precisa de vários provedores de modelos, o backend mais útil é aquele que centraliza a autenticação enquanto permite que o aplicativo escolha um modelo por requisição. A CometAPI fornece esse padrão: o servidor mantém uma única chave de API e a base URL compatível com OpenAI https://api.cometapi.com/v1, enquanto o campo model da requisição seleciona um GPT, Claude, Gemini ou outro modelo de chat disponível.
Este tutorial constrói um projeto funcional com App Router contendo uma lista de permissões de modelos no servidor, um Route Handler com streaming, um seletor de modelo no navegador, variáveis de ambiente protegidas, uma interface de chat completa e orientações de deploy. A chave da CometAPI nunca chega ao navegador.
O que é um app de chat de IA multi‑modelo?
Uma arquitetura de chat de IA multi‑modelo coloca um backend controlado pela aplicação entre a interface do usuário e vários provedores de modelos. Neste projeto, o navegador envia a conversa para /api/chat, enquanto o servidor mantém a credencial da CometAPI privada, valida o ID do modelo solicitado e encaminha a requisição por uma API compatível.
Como a troca de modelos funciona no Next.js?
Mudar de modelo é uma decisão de roteamento. O navegador envia um ID de modelo aprovado com a conversa para /api/chat. O Route Handler adiciona a credencial da CometAPI, chama POST /v1/chat/completions e transmite a resposta do modelo selecionado de volta ao navegador.
O endpoint compartilhado não torna todos os modelos idênticos. Estilo de saída, comportamento de ferramentas, parâmetros suportados, limites de contexto e preços podem diferir. Mantenha os IDs de modelos em uma lista de permissões no servidor e teste cada rota com os mesmos prompts da aplicação antes de disponibilizá-la aos usuários.
O que você vai construir
O aplicativo final tem um único backend em Next.js e três rotas de chat selecionáveis:
| Model ID | Função de exemplo | Preço CometAPI |
|---|---|---|
| gemini-3.7-flash | Sensível a custo, chat de alto volume | $0.60 input / $3.00 output |
| claude-opus-5 | Raciocínio premium focado | $4 input / $20 output |
| gpt-5.6 | gpt-5.6 é a rota genérica da CometAPI para GPT-5.6 e atualmente mapeia para o tier Sol. | $3.2 input / $16 output no tier de contexto curto |
Observação sobre preços: Os preços abaixo estão em USD por milhão de tokens, foram verificados em 21 de agosto de 2026 e podem mudar. Sempre verifique as tarifas atuais nas páginas dos modelos antes do uso em produção. A tabela é um exemplo de roteamento, não um ranking de qualidade. O GPT-5.6 usa um tier de preço mais alto acima de 272.000 tokens. Veja as páginas dos modelos Gemini 3.7 Flash, Claude Opus 5 e GPT-5.6 para as tarifas datadas usadas aqui.
A CometAPI precifica os modelos de forma transparente em relação à API oficial de cada provedor. Modelos com precificação oficial unificada — OpenAI, Claude, Gemini e similares — são faturados por token em uma razão de 0.8:1 em relação ao preço oficial, um desconto de 20%; modelos sem APIs oficiais (MidJourney, Kling, Luma) são cobrados por chamada a taxas definidas pela CometAPI, também com 20% de desconto. Consulte o Guia de Preços da CometAPI para a fórmula de mark‑up, tarifas por modelo e unidades de cobrança.
As três rotas cobrem diferentes trade‑offs. O Gemini 3.7 Flash é o workhorse agentic eficiente do Google, com entrada multimodal e uma janela de contexto de 1.048.576 tokens para chat de alto volume, codificação e fluxos de trabalho de conhecimento. O Claude Opus 5 é o modelo de fronteira da Anthropic para raciocínio, forte em análise de múltiplas etapas, código e escrita longa cuidadosa, com preço premium. O GPT-5.6 é o modelo de uso geral da OpenAI, equilibrando raciocínio, uso de ferramentas e redação em uma ampla janela de contexto para tráfego de produção do dia a dia.
Antes de começar
Você precisa de Node.js, npm, uma conta na CometAPI e uma chave de API do lado do servidor a partir do Quick Start da CometAPI. Os três IDs de modelo exatos acima estavam disponíveis no catálogo ao vivo em 21 de agosto de 2026, não estavam marcados como “upcoming” e expunham POST /v1/chat/completions.
O tutorial usa estas configurações compartilhadas:
- API key:
COMETAPI_API_KEY - Base URL:
https://api.cometapi.com/v1 - Catalog endpoint:
GEThttps://api.cometapi.com/api/models
Passo 1: Crie o app Next.js
npx create-next-app@latest multi-model-chat --ts --app --eslintcd multi-model-chatnpm run dev
Nenhum SDK de provedor de modelo é necessário para esta versão. O servidor usa a API fetch embutida e encaminha o stream de Server‑Sent Events upstream.
Passo 2: Mantenha a chave da CometAPI no servidor
Crie .env.local na raiz do projeto:
COMETAPI_API_KEY=replace_with_your_cometapi_keyCOMETAPI_BASE_URL=https://api.cometapi.com/v1
Não prefixe a chave com NEXT_PUBLIC_. O Next.js só expõe variáveis com esse prefixo para bundles do navegador; a chave da API pertence ao Route Handler do lado do servidor.
Passo 3: Defina a política de modelos
Coloque os IDs permitidos no backend, não apenas no dropdown. Um usuário pode contornar os controles do navegador e chamar sua rota diretamente, portanto o servidor deve rejeitar valores de modelo desconhecidos.
const ALLOWED_MODELS = [ "gemini-3.7-flash", "claude-opus-5", "gpt-5.6",] as const;type ModelId = (typeof ALLOWED_MODELS)[number];
O cliente usará os mesmos três IDs no seu seletor, enquanto o servidor continua sendo a fonte de verdade.
Passo 4: Faça streaming da CometAPI por um Route Handler
Crie app/api/chat/route.ts. A rota valida um payload de chat somente texto, chama a CometAPI com stream: true e retorna o stream de eventos upstream sem expor a credencial.
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", }, });}
O handler repassa o sinal de desconexão do navegador para upstream, então fechar a requisição pode interromper uma geração desnecessária. Ele também retorna um erro sanitizado ao cliente enquanto mantém detalhes diagnósticos nos logs do servidor.
Passo 5: Adicione a seleção de modelo e o parsing do stream
Crie um componente cliente em app/page.tsx. Ele envia o modelo selecionado junto com a conversa, analisa cada evento data: e anexa o texto incremental delta.content à última mensagem do assistente.
O navegador nunca chama a CometAPI diretamente. Seu único destino é a sua própria rota /api/chat, que mantém a chave privada e impõe a lista de permissões.
Código principal do projeto
O Route Handler acima é o arquivo completo app/api/chat/route.ts. Adicione a página, o layout e a folha de estilo a seguir para finalizar o projeto executável.
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. */
Passo 6: Execute e faça o deploy do app
npm run dev
Abra http://localhost:3000, selecione um modelo e envie uma mensagem. Para um deploy de produção em Node.js, adicione COMETAPI_API_KEY e COMETAPI_BASE_URL às configurações de ambiente do lado do servidor do host e então execute:
npm run buildnpm run start
Use um ambiente de hospedagem que suporte respostas em streaming. Uma exportação estática não consegue executar o Route Handler /api/chat.
Teste a rota de streaming
Com o servidor de desenvolvimento em execução, chame seu backend diretamente:
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."} ] }'
Uma requisição bem-sucedida retorna Server‑Sent Events. Os IDs e o texto exatos variam, mas o stream segue este formato:
data: {"choices":[{"delta":{"content":"Model"}}]}data: {"choices":[{"delta":{"content":" routing"}}]}data: [DONE]
O parser do navegador lê cada evento SSE, extrai choices[0].delta.content e adiciona o texto transmitido à mensagem do assistente conforme os chunks chegam.
Erros comuns de integração
| Sintoma | Causa | Correção |
|---|---|---|
| Erro de autenticação 401 | Chave do lado do servidor ausente ou inválida | Configure COMETAPI_API_KEY; não a exponha com NEXT_PUBLIC_. |
| 404 ou rota incorreta | A base URL está sem /v1 | Use https://api.cometapi.com/v1. |
| 400 modelo não suportado | O ID não está na lista de permissões do backend | Use um ID de modelo de texto ao vivo exato e atualize ambos os seletores. |
| A resposta chega de uma vez | O host ou proxy está bufferizando o stream | Desative a transformação da resposta e use um deploy Node.js compatível com streaming. |
Prepare o backend de chat para produção
- Autentique seus próprios usuários. Não exponha uma rota pública que gaste créditos para tráfego anônimo.
- Faça rate limit por usuário e IP. Limite streams concorrentes, requisições por minuto, contagem de mensagens e tamanho das mensagens.
- Mantenha a lista de permissões de modelos no servidor. O seletor do navegador é uma conveniência, não uma barreira de segurança.
- Valide o catálogo ao vivo durante o deploy. Consulte
GEThttps://api.cometapi.com/api/modelse falhe a release se um ID configurado estiver como “upcoming”, indisponível ou sem o endpoint de chat‑completions. - Acompanhe custo por rota. Registre o modelo selecionado, request ID, latência, uso de tokens e ID de usuário. Defina cotas de chave ou tetos de gastos no dashboard da CometAPI quando apropriado.
- Trate desconexões e timeouts. Preserve
request.signal, defina um timeout de aplicação e interrompa o trabalho quando o cliente sair. - Não oculte erros de configuração com fallback. Superfície respostas 400 e 401. Use outro modelo apenas para um conjunto limitado de falhas retentáveis e apenas quando o esquema da requisição for compatível; veja o Guia de Fallback de Modelos da CometAPI para uma cadeia de fallback em duas camadas (CometAPI primária → modelo de fallback da CometAPI → provedor oficial).
- Reduza dados nos logs. Mantenha chaves de API, prompts completos e saídas sensíveis do modelo fora dos logs de erro de produção.
Um backend Next.js, várias escolhas de modelo
A troca de modelo pertence à sua política de servidor, não a contas separadas de provedores. Um Route Handler em Next.js pode manter uma única chave da CometAPI privada, aceitar um ID de modelo aprovado por requisição e transmitir o modelo selecionado por um único endpoint compatível com OpenAI. O frontend permanece simples, enquanto o backend retém controle sobre acesso, validação, observabilidade e custo. A mesma conta e chave também alcançam as APIs nativas de imagem e vídeo da CometAPI — como Flux para geração de imagens e Kling para geração de vídeo — então este backend de chat pode se estender a fluxos multimodais sem uma segunda integração.
Use o diretório público de modelos da CometAPI para descoberta de modelos e GET https://api.cometapi.com/api/models para validação automatizada de roteamento.
Perguntas frequentes
Posso usar vários modelos de IA em um único app Next.js?
Sim. Mantenha uma rota do lado do servidor e passe um ID de modelo presente na lista de permissões com cada requisição. A UI do navegador pode oferecer escolhas de modelo, enquanto o servidor controla quais IDs são aceitos.
Como alterno entre GPT, Claude e Gemini?
Envie o ID do modelo selecionado no corpo da requisição. O servidor o valida contra uma lista de permissões e encaminha o mesmo payload de chat para o modelo escolhido por meio do endpoint compatível com OpenAI.
Onde devo armazenar minha chave da CometAPI?
Armazene-a em uma variável de ambiente do lado do servidor, como COMETAPI_API_KEY. Nunca a exponha por uma variável NEXT_PUBLIC_ ou código do cliente.
Uma API compatível com OpenAI torna todos os modelos intercambiáveis?
Não. O formato da requisição é portável, mas os modelos podem diferir em parâmetros suportados, limites de contexto, comportamento de ferramentas, estilo de saída, latência e preço. Teste cada modelo da lista de permissões com seus prompts de produção.
Route Handlers do Next.js conseguem fazer streaming de respostas de IA?
Sim. Um Route Handler pode retornar o stream de Server‑Sent Events upstream com o content type text/event-stream, desde que a plataforma de deploy e qualquer proxy à sua frente não bufferizem a resposta.
Posso fazer o deploy deste app como um site estático do Next.js?
Não. Uma exportação estática não consegue executar o Route Handler /api/chat do lado do servidor nem proteger a chave da API. Use um runtime Node.js ou outro servidor compatível que suporte respostas em streaming.
