Claude Haiku 5.5 and Nano Banana 2.1 are now live on CometAPI →
guide/CometAPI-forskning

Slik bygger du en multimodell AI-chatapp med Next.js og CometAPI

Bygg en Next.js-basert AI-chatapp med streaming som kan bytte mellom GPT-, Claude- og Gemini-modeller via én CometAPI-nøkkel og ett backend-endepunkt.

CometAPI
AnnaForskerteam for AI-modeller og API
Oppdatert Sep 3, 2026 12 min lesetid
Slik bygger du en multimodell AI-chatapp med Next.js og CometAPI
Bruk dette mønsteret

Gjør det første API-kallet.

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)

For en Next.js-chatbot som trenger flere modellleverandører, er den mest nyttige backend-løsningen en som sentraliserer autentisering og samtidig lar applikasjonen velge modell per forespørsel. CometAPI tilbyr det mønsteret: serveren beholder én API-nøkkel og den OpenAI-kompatible base-URL-en https://api.cometapi.com/v1, mens feltet model i forespørselen velger en tilgjengelig GPT-, Claude-, Gemini- eller annen chatmodell.

Denne veiledningen bygger et fungerende App Router-prosjekt med en server-side modell-tillatelsesliste, en strømmende Route Handler, en modellvelger i nettleseren, beskyttede miljøvariabler, et komplett chatgrensesnitt og distribusjonsveiledning. CometAPI-nøkkelen når aldri nettleseren.

Hva er en multimodell AI-chatapp?

En multimodell AI-chatarkitektur plasserer én applikasjonskontrollert backend mellom brukergrensesnittet og flere modellleverandører. I dette prosjektet sender nettleseren samtalen til /api/chat, mens serveren holder CometAPI-legitimasjonen privat, validerer den forespurte modell-ID-en og videresender forespørselen via ett kompatibelt API.

Hvordan fungerer modellbytte i Next.js?

Å bytte modell er en rutingbeslutning. Nettleseren sender en godkjent modell-ID sammen med samtalen til /api/chat. Route Handleren legger til CometAPI-legitimasjonen, kaller POST /v1/chat/completions, og strømmer den valgte modellens respons tilbake til nettleseren.

Det delte endepunktet gjør ikke alle modeller identiske. Utskriftsstil, verktøyadferd, støttede parametere, kontekstgrenser og prising kan variere. Hold modell-ID-er i en serverside tillatelsesliste og test hver rute med de samme applikasjonsproptene før du gjør den tilgjengelig for brukere.

Dette skal du bygge

Den ferdige applikasjonen har én Next.js-backend og tre valgbare chatruter:

Modell-IDEksempelrolleCometAPI-pris
gemini-3.7-flashKostnadssensitiv, høyvolum chat$0.60 input / $3.00 output
claude-opus-5Fokusert premium-resonnering$4 input / $20 output
gpt-5.6gpt-5.6 er den generiske CometAPI-ruten for GPT-5.6 og er for tiden koblet til Sol tier.$3.2 input / $16 output i short-context tier

Prismerknad: Prisene nedenfor er i USD per million tokens, ble kontrollert 21. august 2026, og kan endres. Verifiser alltid gjeldende satser på modellsidene før produksjonsbruk. Tabellen er et rutingeksempel, ikke en kvalitetsrangering. GPT-5.6 bruker et høyere prisnivå over 272,000 tokens. Se de lenkede modellsidene for Gemini 3.7 Flash, Claude Opus 5 og GPT-5.6 for de daterte satsene som brukes her.

CometAPI priser modeller transparent mot hver leverandørs offisielle API. Modeller med enhetlig offisiell prising — OpenAI, Claude, Gemini og lignende — faktureres per token til et forhold på 0,8:1 mot den offisielle prisen, en rabatt på 20 %; modeller uten offisielle API-er (MidJourney, Kling, Luma) faktureres per kall til CometAPIs fastsatte satser, også rabattert 20 %. Se CometAPI Pricing Guide for påslagsformelen, per-modell-satser og faktureringsenheter.

De tre rutene dekker ulike avveininger. Gemini 3.7 Flash er Googles effektive agentiske arbeidshest, med multimodal input og et kontekstvindu på 1,048,576 tokens for høyvolumchat, koding og kunnskapsarbeid. Claude Opus 5 er Anthropics spydspiss for resonnering, sterk på flerstegs analyse, kode og nøye langformskriving til en premiumpris. GPT-5.6 er OpenAIs allroundmodell, som balanserer resonnering, verktøybruk og utkast på tvers av et bredt kontekstvindu for daglig produksjonstrafikk.

Før du begynner

Du trenger Node.js, npm, en CometAPI-konto og en serverside API-nøkkel fra CometAPI Quick Start. De tre eksakte modell-ID-ene ovenfor var tilgjengelige i den levende katalogen 21. august 2026, var ikke merket som kommende og eksponerte POST /v1/chat/completions.

Veiledningen bruker disse delte innstillingene:

Trinn 1: Opprett Next.js-appen

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

Ingen SDK for modellleverandør er nødvendig for denne versjonen. Serveren bruker den innebygde fetch-API-en og videresender den oppstrøms Server-Sent Events-strømmen.

Trinn 2: Hold CometAPI-nøkkelen på serveren

Opprett .env.local i prosjektroten:

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

Ikke prefiks nøkkelen med NEXT_PUBLIC_. Next.js eksponerer bare variabler med det prefikset til nettleserbundler; API-nøkkelen hører hjemme i serversidens Route Handler.

Trinn 3: Definer modellpolicyen

Legg de tillatte ID-ene i backend, ikke bare i nedtrekksmenyen. En bruker kan omgå nettleserkontroller og kalle ruten din direkte, så serveren må avvise ukjente modellverdier.

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

Klienten vil bruke de samme tre ID-ene for sin velger, mens serveren forblir sannhetskilden.

Trinn 4: Strøm CometAPI gjennom en Route Handler

Opprett app/api/chat/route.ts. Ruten validerer en tekst-only chat-payload, kaller CometAPI med stream: true, og returnerer den oppstrøms hendelsesstrømmen uten å eksponere legitimasjonen.

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",    },  });}

Handleren sender nettleserens frakoblingssignal oppstrøms, så lukking av forespørselen kan stoppe unødvendig generering. Den returnerer også en sanitisert feil til klienten mens diagnostiske detaljer holdes i serverlogger.

Trinn 5: Legg til modellvalg og strømparsing

Opprett en klientkomponent i app/page.tsx. Den sender den valgte modellen med samtalen, parser hver data:-hendelse og legger til den inkrementelle delta.content-teksten til den siste assistentmeldingen.

Nettleseren kaller aldri CometAPI direkte. Den har kun din egen /api/chat-rute som destinasjon, som holder nøkkelen privat og håndhever tillatelseslisten.

Kjerneprosjektkode

Route Handleren ovenfor er hele app/api/chat/route.ts-filen. Legg til følgende side, layout og stilark for å fullføre det kjørbare prosjektet.

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. */

Trinn 6: Kjør og distribuer appen

npm run dev

Åpne http://localhost:3000, velg en modell og send en melding. For en produksjonsdistribusjon på Node.js, legg til COMETAPI_API_KEY og COMETAPI_BASE_URL i vertens serverside miljøinnstillinger, og kjør deretter:

npm run buildnpm run start

Bruk en hostingløsning som støtter strømmende responser. Et statisk eksportert bygg kan ikke kjøre /api/chat-Route Handleren.

Test den strømmende ruten

Med utviklingsserveren kjørende, kall backend-en din direkte:

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."}    ]  }'

En vellykket forespørsel returnerer Server-Sent Events. De eksakte ID-ene og tekstene varierer, men strømmen følger denne formen:

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

Nettleserens parser leser hver SSE-hendelse, henter ut choices[0].delta.content, og legger til den strømmende teksten til assistentmeldingen etter hvert som deler kommer.

Vanlige integrasjonsfeil

SymptomÅrsakLøsning
401-autentiseringsfeilManglende eller ugyldig serverside-nøkkelSett COMETAPI_API_KEY; ikke eksponer den med NEXT_PUBLIC_.
404 eller feil ruteBasis-URL mangler /v1Bruk https://api.cometapi.com/v1.
400 ikke støttet modell-IDID-en er ikke i tillatelseslisten på backendBruk en nøyaktig, tilgjengelig tekstmodell-ID og oppdater begge selektorene.
Svaret vises på én gangVert eller proxy bufferer hendelsesstrømmenDeaktiver responstransformasjon og bruk en streaming-kompatibel Node.js-deployering.

Gjør chat-backenden klar for produksjon

  • Autentiser dine egne brukere. Ikke eksponer en offentlig rute som bruker kreditter for anonym trafikk.
  • Ratebegrens per bruker og IP. Begrens samtidige strømmer, forespørsler per minutt, meldingsantall og meldingslengde.
  • Hold modell-tillatelseslisten på serversiden. Nettleserens velger er en bekvemmelighet, ikke en sikkerhetsgrense.
  • Valider den levende katalogen under distribusjon. Spørr GET https://api.cometapi.com/api/models og avbryt releasen hvis en konfigurert ID er kommende, utilgjengelig eller mangler chat-completions-endepunktet.
  • Spor kostnad per rute. Logg valgt modell, request ID, latens, tokenbruk og bruker-ID. Sett nøkkelkvoter eller forbruksgrenser i CometAPI-dashbordet der det er hensiktsmessig.
  • Håndter frakoblinger og tidsavbrudd. Bevar request.signal, sett en applikasjonstimeout, og stopp arbeidet når klienten forlater.
  • Ikke skjul konfigurasjonsfeil med fallback. Vis 400- og 401-responser. Bruk en annen modell kun for et avgrenset sett av prøvbare feil og bare når forespørselsskjemaet er kompatibelt; se CometAPI Model Fallback Guide for en tolags fallback-kjede (CometAPI primær → CometAPI fallback-modell → offisiell leverandør).
  • Rediger logger. Hold API-nøkler, fulle prompter og sensitiv modellutdata ute av produksjonsfeillogger.

Én Next.js-backend, flere modellvalg

Modellbyttet hører hjemme i serverpolicyen din, ikke i separate leverandørkontoer. En Next.js Route Handler kan holde én CometAPI-nøkkel privat, akseptere en godkjent modell-ID per forespørsel og strømme den valgte modellen gjennom ett OpenAI-kompatibelt endepunkt. Frontenden forblir enkel, mens backenden beholder kontroll over tilgang, validering, observabilitet og kostnader. Den samme kontoen og nøkkelen når også CometAPIs egne bilde- og video-API-er — som Flux for bildegenerering og Kling for videogenerering — slik at denne chat-backenden kan utvides til multimodale arbeidsflyter uten en andre integrasjon.

Bruk CometAPI public model directory for modelldiscovery og GET https://api.cometapi.com/api/models for automatisert rutingsvalidering.

Ofte stilte spørsmål

Kan jeg bruke flere AI-modeller i én Next.js-app?

Ja. Behold én serverside-rute og send en modell-ID fra tillatelseslisten med hver forespørsel. Nettleser-UI-et kan tilby modellvalg, mens serveren kontrollerer hvilke ID-er som aksepteres.

Hvordan bytter jeg mellom GPT, Claude og Gemini?

Send den valgte modell-ID-en i forespørselens body. Serveren validerer den mot en tillatelsesliste og videresender den samme chat-payloaden til den valgte modellen via det OpenAI-kompatible endepunktet.

Hvor bør jeg lagre CometAPI API-nøkkelen min?

Lagre den i en serverside miljøvariabel som COMETAPI_API_KEY. Eksponer den aldri via en NEXT_PUBLIC_-variabel eller klientkode.

Gjør et OpenAI-kompatibelt API alle modeller utskiftbare?

Nei. Forespørselsformen er portabel, men modeller kan variere i støttede parametere, kontekstgrenser, verktøyadferd, utskriftsstil, latenstid og pris. Test hver modell på tillatelseslisten med produksjonsproptene dine.

Kan Next.js Route Handlers strømme AI-responser?

Ja. En Route Handler kan returnere den oppstrøms Server-Sent Events-strømmen med innholdstypen text/event-stream, forutsatt at plattformen for distribusjon og eventuelle proxyer foran ikke bufferer responsen.

Kan jeg distribuere denne appen som et statisk Next.js-nettsted?

Nei. En statisk eksport kan ikke kjøre den serversidige /api/chat-Route Handleren eller beskytte API-nøkkelen. Bruk en Node.js- eller annen kompatibel server-runtime som støtter strømmende responser.

Fortsett å lære

Koble denne artikkelen til neste beslutning.

Se alle temaer
Publisert Aug 25, 2026
Sist oppdatert Sep 3, 2026
11 visninger
Gjennomgått for klarhet, kildeangivelse og gjeldende API-terminologi.

Les mer