DeepSeek Vision and Grok Imagine models are now live on CometAPI →
guide/Investigación de CometAPI

Cómo crear una aplicación de chat de IA con múltiples modelos con Next.js y CometAPI

Crea una aplicación de chat de IA en Next.js con transmisión en tiempo real que alterna entre los modelos GPT, Claude y Gemini mediante una única clave de CometAPI y un único endpoint de backend.

CometAPI
AnnaEquipo de investigación de modelos de IA y API
Actualizado Aug 25, 2026 14 min de lectura
Cómo crear una aplicación de chat de IA con múltiples modelos con Next.js y CometAPI
Usa este patrón

Haz la primera llamada a la API.

from openai import OpenAI

client = OpenAI(
    api_key="YOUR_COMETAPI_KEY",
    base_url="https://api.cometapi.com/v1",
)

response = client.chat.completions.create(
    model="gpt-5-mini",
    messages=[{"role": "user", "content": "Build this workflow."}],
)

print(response.choices[0].message.content)

Para un chatbot de Next.js que necesita varios proveedores de modelos, el backend más útil es el que centraliza la autenticación y permite que la aplicación elija un modelo por solicitud. CometAPI proporciona ese patrón: el servidor mantiene una única clave de API y la URL base compatible con OpenAI https://api.cometapi.com/v1, mientras que el campo model de la solicitud selecciona un modelo de chat disponible de GPT, Claude, Gemini u otros.

Este tutorial construye un proyecto App Router funcional con una lista de permitidos de modelos en el servidor, un Route Handler con streaming, un selector de modelos en el navegador, variables de entorno protegidas, una interfaz de chat completa y guía de despliegue. La clave de CometAPI nunca llega al navegador.

¿Qué es una app de chat de IA multi‑modelo?

Una arquitectura de chat multi‑modelo coloca un backend controlado por la aplicación entre la interfaz de usuario y varios proveedores de modelos. En este proyecto, el navegador envía la conversación a /api/chat, mientras que el servidor mantiene privada la credencial de CometAPI, valida el ID del modelo solicitado y reenvía la solicitud a través de una API compatible.

¿Cómo funciona el cambio de modelo en Next.js?

Cambiar de modelo es una decisión de enrutamiento. El navegador envía un ID de modelo aprobado junto con la conversación a /api/chat. El Route Handler agrega la credencial de CometAPI, llama a POST /v1/chat/completions y transmite la respuesta del modelo seleccionado de vuelta al navegador.

El endpoint compartido no hace que todos los modelos sean idénticos. El estilo de salida, el comportamiento de herramientas, los parámetros admitidos, los límites de contexto y los precios pueden diferir. Mantén los IDs de modelos en una lista de permitidos del lado del servidor y prueba cada ruta con los mismos prompts de la aplicación antes de ponerla a disposición de usuarios.

Lo que vas a construir

La aplicación final tiene un backend único de Next.js y tres rutas de chat seleccionables:

ID de modeloRol de ejemploPrecio en CometAPI
gemini-3.7-flashChat de alto volumen sensible al costo$0.60 input / $3.00 output
claude-opus-5Razonamiento premium enfocado$4 input / $20 output
gpt-5.6gpt-5.6 es la ruta genérica de CometAPI para GPT-5.6 y actualmente se asigna al Sol tier.$3.2 input / $16 output en el nivel de contexto corto

Nota sobre precios: Los precios que se muestran a continuación están en USD por millón de tokens, fueron verificados el 21 de agosto de 2026 y pueden cambiar. Verifica siempre las tarifas actuales en las páginas de los modelos antes de uso en producción. La tabla es un ejemplo de enrutamiento, no una clasificación de calidad. GPT-5.6 usa un nivel de precio más alto por encima de 272,000 tokens. Consulta las páginas de modelos enlazadas de Gemini 3.7 Flash, Claude Opus 5 y GPT-5.6 para las tarifas con fecha utilizadas aquí.

CometAPI fija los precios de los modelos de forma transparente con respecto a la API oficial de cada proveedor. Los modelos con precios oficiales unificados — OpenAI, Claude, Gemini y similares — se facturan por token con una proporción de 0.8:1 respecto al precio oficial, un 20% de descuento; los modelos sin APIs oficiales (MidJourney, Kling, Luma) se facturan por llamada a las tarifas establecidas por CometAPI, también con un 20% de descuento. Consulta la CometAPI Pricing Guide para la fórmula de recargo, las tarifas por modelo y las unidades de facturación.

Las tres rutas cubren diferentes compromisos. Gemini 3.7 Flash es el caballo de batalla agentivo eficiente de Google, con entrada multimodal y una ventana de contexto de 1,048,576 tokens para chat de alto volumen, código y flujos de trabajo de conocimiento. Claude Opus 5 es el modelo de razonamiento de frontera de Anthropic, fuerte en análisis de múltiples pasos, código y redacción larga y cuidadosa a un precio premium. GPT-5.6 es el modelo de propósito general de OpenAI, que equilibra razonamiento, uso de herramientas y redacción a través de una ventana de contexto amplia para tráfico de producción cotidiano.

Antes de empezar

Necesitas Node.js, npm, una cuenta de CometAPI y una clave de API del lado del servidor desde el CometAPI Quick Start. Los tres IDs de modelo exactos anteriores estaban disponibles en el catálogo en vivo el 21 de agosto de 2026, no estaban marcados como “upcoming” y exponían POST /v1/chat/completions.

El tutorial usa estas configuraciones compartidas:

Paso 1: Crea la app de Next.js

npx create-next-app@latest multi-model-chat --ts --app --eslintcd multi-model-chatnpm run dev

No se requiere un SDK del proveedor de modelos para esta versión. El servidor usa la API fetch integrada y reenvía el stream de Server-Sent Events ascendente.

Paso 2: Mantén la clave de CometAPI en el servidor

Crea .env.local en la raíz del proyecto:

COMETAPI_API_KEY=replace_with_your_cometapi_keyCOMETAPI_BASE_URL=https://api.cometapi.com/v1

No antepongas la clave con NEXT_PUBLIC_. Next.js solo expone variables con ese prefijo a los bundles del navegador; la clave de API pertenece al Route Handler del lado del servidor.

Paso 3: Define la política de modelos

Coloca los IDs permitidos en el backend, no solo en el menú desplegable. Un usuario puede eludir los controles del navegador y llamar a tu ruta directamente, por lo que el servidor debe rechazar valores de modelo desconocidos.

const ALLOWED_MODELS = [  "gemini-3.7-flash",  "claude-opus-5",  "gpt-5.6",] as const;​type ModelId = (typeof ALLOWED_MODELS)[number];

El cliente usará los mismos tres IDs para su selector, mientras que el servidor sigue siendo la fuente de la verdad.

Paso 4: Transmite CometAPI a través de un Route Handler

Crea app/api/chat/route.ts. La ruta valida un payload de chat solo texto, llama a CometAPI con stream: true y devuelve el stream de eventos ascendente sin exponer la 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 no está configurada." },      { status: 500 },    );  }​  let payload: { model?: unknown; messages?: unknown };​  try {    payload = await request.json();  } catch {    return Response.json({ error: "Cuerpo JSON no válido." }, { status: 400 });  }​  if (!isModelId(payload.model)) {    return Response.json({ error: "ID de modelo no admitido." }, { status: 400 });  }​  if (    !Array.isArray(payload.messages) ||    payload.messages.length === 0 ||    payload.messages.length > 50 ||    !payload.messages.every(isChatMessage)  ) {    return Response.json(      { error: "messages debe contener entre 1 y 50 mensajes de texto válidos." },      { 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: "La solicitud del modelo seleccionado falló.",        status: upstream.status,        requestId,      },      { status: upstream.status },    );  }​  if (!upstream.body) {    return Response.json(      { error: "El modelo no devolvió cuerpo de respuesta." },      { status: 502 },    );  }​  return new Response(upstream.body, {    status: 200,    headers: {      "Content-Type": "text/event-stream; charset=utf-8",      "Cache-Control": "no-cache, no-transform",    },  });}

El manejador pasa la señal de desconexión del navegador hacia arriba, por lo que cerrar la solicitud puede detener una generación innecesaria. También devuelve un error saneado al cliente mientras mantiene detalles de diagnóstico en los logs del servidor.

Paso 5: Agrega selección de modelo y análisis del stream

Crea un componente de cliente en app/page.tsx. Envía el modelo seleccionado con la conversación, analiza cada evento data: y agrega el texto incremental delta.content al último mensaje del asistente.

El navegador nunca llama a CometAPI directamente. Su único destino es tu propia ruta /api/chat, que mantiene la clave privada y hace cumplir la lista de permitidos.

Código principal del proyecto

El Route Handler anterior es el archivo completo app/api/chat/route.ts. Agrega la siguiente página, layout y hoja de estilos para finalizar el proyecto ejecutable.

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 || `La solicitud falló con ${response.status}`);      }​      if (!response.body) throw new Error("El streaming no está disponible.");​      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 : "La solicitud ha fallado.",      );    } finally {      setLoading(false);    }  }​  return (    <main className="shell">      <section className="chat">        <header>          <p className="eyebrow">Next.js + CometAPI</p>          <h1>Chat con múltiples modelos</h1>          <label>            Modelo            <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">Elige un modelo y envía un mensaje.</p>          ) : (            messages.map((message, index) => (              <article className={message.role} key={`${message.role}-${index}`}>                <b>{message.role === "user" ? "Tú" : "Asistente"}</b>                <p>{message.content || "…"}</p>              </article>            ))          )}        </div>​        <form onSubmit={sendMessage}>          <textarea            value={input}            onChange={(event) => setInput(event.target.value)}            placeholder="Pregunta algo…"            rows={3}            maxLength={20_000}          />          <button disabled={loading || !input.trim()} type="submit">            {loading ? "Transmitiendo…" : "Enviar"}          </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: "Chat con múltiples modelos",  description: "Una app de chat en Next.js con streaming, impulsada por 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; }​/* Los controles de formulario, botones y estados de error siguen el mismo tema oscuro. */

Paso 6: Ejecuta y despliega la app

npm run dev

Abre http://localhost:3000, selecciona un modelo y envía un mensaje. Para un despliegue de Node.js en producción, agrega COMETAPI_API_KEY y COMETAPI_BASE_URL a la configuración de entorno del lado del servidor del host y luego ejecuta:

npm run buildnpm run start

Usa una plataforma de hosting que admita respuestas con streaming. Una exportación estática no puede ejecutar el Route Handler /api/chat.

Prueba la ruta de streaming

Con el servidor de desarrollo en ejecución, llama a tu backend directamente:

curl -N http://localhost:3000/api/chat \  -H "Content-Type: application/json" \  -d '{    "model": "gemini-3.7-flash",    "messages": [      {"role": "user", "content": "Explica el enrutamiento de modelos en dos oraciones."}    ]  }'

Una solicitud correcta devuelve Server-Sent Events. Los IDs y el texto exactos varían, pero el stream sigue esta forma:

data: {"choices":[{"delta":{"content":"Model"}}]}​data: {"choices":[{"delta":{"content":" routing"}}]}​data: [DONE]

El analizador del navegador lee cada evento SSE, extrae choices[0].delta.content y agrega el texto transmitido al mensaje del asistente en cuanto llegan los fragmentos.

Errores comunes de integración

SíntomaCausaSolución
Error 401 de autenticaciónClave del lado del servidor ausente o no válidaConfigura COMETAPI_API_KEY; no la expongas con NEXT_PUBLIC_.
404 o ruta incorrectaFalta /v1 en la URL baseUsa https://api.cometapi.com/v1.
400 modelo no admitidoEl ID no está en la lista de permitidos del backendUsa un ID de modelo de texto en vivo exacto y actualiza ambos selectores.
La respuesta aparece de golpeEl host o el proxy almacena en búfer el stream de eventosDesactiva la transformación de respuestas y usa un despliegue de Node.js compatible con streaming.

Prepara el backend de chat para producción

  • Autentica a tus propios usuarios. No expongas una ruta pública que gaste créditos para tráfico anónimo.
  • Limita la tasa por usuario e IP. Acota streams concurrentes, solicitudes por minuto, cantidad de mensajes y longitud de los mensajes.
  • Mantén la lista de permitidos de modelos del lado del servidor. El selector del navegador es una comodidad, no un límite de seguridad.
  • Valida el catálogo en vivo durante el despliegue. Consulta GET https://api.cometapi.com/api/models y falla la publicación si un ID configurado está como “upcoming”, no disponible o no expone el endpoint de chat-completions.
  • Lleva control de costos por ruta. Registra el modelo seleccionado, el ID de solicitud, la latencia, el uso de tokens y el ID de usuario. Establece cuotas de clave o límites de gasto en el panel de CometAPI según corresponda.
  • Maneja desconexiones y timeouts. Conserva request.signal, fija un timeout de aplicación y detén el trabajo cuando el cliente se vaya.
  • No ocultes errores de configuración con fallback. Superficia respuestas 400 y 401. Usa otro modelo solo para un conjunto acotado de fallas reintentables y solo cuando el esquema de la solicitud sea compatible; consulta la Guía de fallback de modelos de CometAPI para una cadena de fallback de dos capas (CometAPI primaria → modelo de fallback en CometAPI → proveedor oficial).
  • Redacta logs. Mantén claves de API, prompts completos y salida sensible del modelo fuera de los logs de error de producción.

Un backend de Next.js, varias opciones de modelo

El cambio de modelo pertenece a tu política de servidor, no a cuentas separadas de proveedores. Un Route Handler de Next.js puede mantener privada una única clave de CometAPI, aceptar un ID de modelo aprobado por solicitud y transmitir el modelo seleccionado a través de un único endpoint compatible con OpenAI. El frontend se mantiene simple, mientras que el backend conserva el control sobre acceso, validación, observabilidad y costo. La misma cuenta y clave también alcanzan las APIs nativas de imagen y video de CometAPI — como Flux para generación de imágenes y Kling para generación de video — por lo que este backend de chat puede extenderse a flujos de trabajo multimodales sin una segunda integración.

Usa el directorio público de modelos de CometAPI para el descubrimiento de modelos y GET https://api.cometapi.com/api/models para la validación de enrutamiento automatizada.

Preguntas frecuentes

¿Puedo usar varios modelos de IA en una app de Next.js?

Sí. Mantén una ruta del lado del servidor y pasa un ID de modelo de la lista de permitidos con cada solicitud. La UI del navegador puede ofrecer opciones de modelo, mientras el servidor controla qué IDs se aceptan.

¿Cómo cambio entre GPT, Claude y Gemini?

Envía el ID del modelo seleccionado en el cuerpo de la solicitud. El servidor lo valida contra una lista de permitidos y reenvía el mismo payload de chat al modelo elegido a través del endpoint compatible con OpenAI.

¿Dónde debo guardar mi clave de API de CometAPI?

Guárdala en una variable de entorno del lado del servidor como COMETAPI_API_KEY. Nunca la expongas mediante una variable NEXT_PUBLIC_ ni en código del cliente.

¿Una API compatible con OpenAI hace que todos los modelos sean intercambiables?

No. La forma de la solicitud es portable, pero los modelos pueden diferir en parámetros admitidos, límites de contexto, comportamiento de herramientas, estilo de salida, latencia y precio. Prueba cada modelo en la lista de permitidos con tus prompts de producción.

¿Los Route Handlers de Next.js pueden transmitir respuestas de IA?

Sí. Un Route Handler puede devolver el stream de Server-Sent Events ascendente con un tipo de contenido text/event-stream, siempre que la plataforma de despliegue y cualquier proxy delante no almacenen la respuesta en búfer.

¿Puedo desplegar esta app como sitio estático de Next.js?

No. Una exportación estática no puede ejecutar el Route Handler /api/chat ni proteger la clave de API. Usa un runtime de Node.js u otro compatible que admita respuestas con streaming.

Seguir aprendiendo

Conecta este artículo con la siguiente decisión.

Ver todos los temas
Publicado el Aug 25, 2026
Última actualización Aug 25, 2026
0 visitas
Revisado para mayor claridad, atribución de fuentes y terminología API actual.

¿Listo para reducir los costos de desarrollo de IA en un 20%?

Comienza gratis en minutos. Créditos de prueba gratuitos incluidos. No se requiere tarjeta de crédito.

Leer Más