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

Sådan bygger du en AI-chatapp med flere modeller ved hjælp af Next.js og CometAPI

Byg en streaming Next.js AI-chatapp, der kan skifte mellem GPT-, Claude- og Gemini-modeller via én CometAPI-nøgle og ét backend-endpoint.

CometAPI
AnnaForskningshold for AI-modeller og API
Opdateret Sep 3, 2026 12 min. læsning
Sådan bygger du en AI-chatapp med flere modeller ved hjælp af Next.js og CometAPI
Brug dette mønster

Lav det første API-kald.

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, der har brug for flere modeludbydere, er den mest nyttige backend en, der centraliserer godkendelse, samtidig med at applikationen kan vælge en model pr. anmodning. CometAPI tilbyder dette mønster: serveren beholder én API-nøgle og den OpenAI-kompatible basis-URL https://api.cometapi.com/v1, mens anmodningens feltet model vælger en tilgængelig GPT-, Claude-, Gemini- eller anden chatmodel.

Denne vejledning bygger et fungerende App Router-projekt med en serverside-tilladelsesliste for modeller, en streaming-Route Handler, en modelvælger i browseren, beskyttede miljøvariabler, en komplet chatgrænseflade og implementeringsvejledning. CometAPI-nøglen når aldrig browseren.

Hvad er en AI-chatapp med flere modeller?

En AI-chatarkitektur med flere modeller placerer én applikationskontrolleret backend mellem brugergrænsefladen og flere modeludbydere. I dette projekt sender browseren samtalen til /api/chat, mens serveren holder CometAPI-legitimationsoplysningerne private, validerer den ønskede model-ID og videresender anmodningen gennem ét kompatibelt API.

Hvordan fungerer modelskift i Next.js?

At skifte model er en ruteringsbeslutning. Browseren sender en godkendt model-ID med samtalen til /api/chat. Route Handleren tilføjer CometAPI-legitimationsoplysningen, kalder POST /v1/chat/completions og streamer den valgte models svar tilbage til browseren.

Det delte endpoint gør ikke alle modeller identiske. Outputstil, værktøjsadfærd, understøttede parametre, kontekstgrænser og priser kan variere. Hold model-ID'er i en serverside-tilladelsesliste og test hver rute med de samme applikationsprompter, før du gør dem tilgængelige for brugere.

Hvad du bygger

Den færdige applikation har én Next.js-backend og tre valgbare chatruter:

Model-IDEksempelrolleCometAPI-pris
gemini-3.7-flashOmkostningsfølsom chat med høj volumen$0.60 input / $3.00 output
claude-opus-5Fokuseret premium-ræsonnement$4 input / $20 output
gpt-5.6gpt-5.6 er den generiske CometAPI-rute for GPT-5.6 og peger i øjeblikket på Sol-tieret.$3.2 input / $16 output i short-context-tieret

Prisnote: De viste priser nedenfor er i USD pr. million tokens, blev kontrolleret den 21. august 2026 og kan ændre sig. Verificér altid aktuelle satser på modelsiderne, før du bruger dem i produktion. Tabellen er et ruterings-eksempel, ikke en kvalitetsrangering. GPT-5.6 anvender et højere pristrin over 272.000 tokens. Se de linkede modelsider for Gemini 3.7 Flash, Claude Opus 5 og GPT-5.6 for de daterede satser, der er brugt her.

CometAPI prissætter modeller gennemsigtigt i forhold til hver udbyders officielle API. Modeller med samlet officiel prissætning — OpenAI, Claude, Gemini og lignende — faktureres pr. token med et forhold på 0.8:1 til den officielle pris, en rabat på 20 %; modeller uden officielle API'er (MidJourney, Kling, Luma) faktureres pr. kald til CometAPIs fastsatte satser, også med 20 % rabat. Se CometAPI-prisguide for mark-up-formlen, takster pr. model og afregningsenheder.

De tre ruter dækker forskellige kompromiser. Gemini 3.7 Flash er Googles effektive agentiske arbejdshest med multimodal input og et kontekstvindue på 1.048.576 tokens til chat med høj volumen, kodning og vidensarbejde. Claude Opus 5 er Anthropics frontier-ræsonnementsmodel, stærk i flertrinsanalyse, kode og omhyggelig langformstekst til en premiumpris. GPT-5.6 er OpenAIs generelle model, der balancerer ræsonnement, værktøjsbrug og udkast på tværs af et bredt kontekstvindue til daglig produktionstrafik.

Før du begynder

Du skal bruge Node.js, npm, en CometAPI-konto og en serverside API-nøgle fra CometAPI Quick Start. De tre præcise model-ID'er ovenfor var tilgængelige i det live katalog den 21. august 2026, var ikke markeret som kommende og eksponerede POST /v1/chat/completions.

Vejledningen bruger disse fælles indstillinger:

Trin 1: Opret Next.js-appen

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

Der kræves ikke nogen modeludbyder-SDK til denne version. Serveren bruger den indbyggede fetch-API og videresender den opstrøms Server-Sent Events-strøm.

Trin 2: Hold CometAPI-nøglen på serveren

Opret .env.local i projektroden:

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

Præfiks ikke nøglen med NEXT_PUBLIC_. Next.js eksponerer kun variabler med det præfiks til browserbundter; API-nøglen hører hjemme i den serverside Route Handler.

Trin 3: Definér modelpolitikken

Læg de tilladte ID'er i backend, ikke kun i dropdown-menuen. En bruger kan omgå browserkontrollerne og kalde din rute direkte, så serveren skal afvise ukendte modelværdier.

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

Klienten bruger de samme tre ID'er til sin vælger, mens serveren forbliver sandhedskilden.

Trin 4: Stream CometAPI gennem en Route Handler

Opret app/api/chat/route.ts. Ruten validerer en tekstbaseret chat-payload, kalder CometAPI med stream: true og returnerer den opstrøms event-strøm uden at eksponere legitimationsoplysningen.

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 videresender browserens afbrydningssignal opstrøms, så lukning af anmodningen kan stoppe unødvendig generering. Den returnerer også en saniteret fejl til klienten, samtidig med at diagnostiske detaljer forbliver i serverloggene.

Trin 5: Tilføj modelvalg og stream-parsing

Opret en klientkomponent i app/page.tsx. Den sender den valgte model med samtalen, parser hver data:-event og føjer den inkrementelle delta.content-tekst til den seneste assistentbesked.

Browseren kalder aldrig CometAPI direkte. Dens eneste destination er din egen /api/chat-rute, som holder nøglen privat og håndhæver tilladelseslisten.

Kerneprojektkode

Route Handleren ovenfor er hele filen app/api/chat/route.ts. Tilføj følgende side, layout og stylesheet for at fuldende det kørbare projekt.

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

Trin 6: Kør og implementér appen

npm run dev

Åbn http://localhost:3000, vælg en model, og send en besked. For en produktions-Node.js-implementering skal du tilføje COMETAPI_API_KEY og COMETAPI_BASE_URL til værtens serverside-miljøindstillinger og derefter køre:

npm run buildnpm run start

Brug en hostingløsning, der understøtter streaming-svar. En statisk eksport kan ikke køre Route Handleren for /api/chat.

Test streamingruten

Med udviklingsserveren kørende kan du kalde din backend 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 succesfuld anmodning returnerer Server-Sent Events. De præcise ID'er og teksten varierer, men strømmen følger denne form:

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

Browserparseren læser hver SSE-event, udtrækker choices[0].delta.content og føjer den streamede tekst til assistentbeskeden, efterhånden som stykkerne ankommer.

Almindelige integrationsfejl

SymptomÅrsagLøsning
401-godkendelsesfejlManglende eller ugyldig serverside-nøgleSet COMETAPI_API_KEY; do not expose it with NEXT_PUBLIC_.
404 eller forkert ruteBasis-URL mangler /v1Brug https://api.cometapi.com/v1.
400 ikke-understøttet modelID'et er ikke i backendens tilladelseslisteBrug et nøjagtigt live tekstmodel-ID og opdatér begge vælgere.
Svaret vises på én gangVærten eller proxyen buffer event-strømmenDeaktivér responstransformation og brug en Node.js-implementering, der kan streame.

Gør chat-backenden klar til produktion

  • Autentificér dine egne brugere. Eksponér ikke en offentlig rute, der bruger credits til anonym trafik.
  • Rate-limitér pr. bruger og IP. Begræns samtidige streams, anmodninger pr. minut, beskedantal og beskedlængde.
  • Behold model-tilladelseslisten på serveren. Browservælgeren er en bekvemmelighed, ikke en sikkerhedsgrænse.
  • Valider livekataloget under deployment. Forespørg GET https://api.cometapi.com/api/models og fejl udgivelsen, hvis en konfigureret ID er kommende, utilgængelig eller mangler chat-completions-endpointet.
  • Spor omkostninger pr. rute. Log den valgte model, request ID, latenstid, tokenforbrug og bruger-ID. Sæt nøglekvoter eller forbrugslofter i CometAPI-dashboardet, hvor det er relevant.
  • Håndtér afbrydelser og timeouts. Bevar request.signal, sæt en applikationstimeout, og stop arbejdet, når klienten forlader.
  • Skjul ikke konfigurationsfejl med fallback. Vis 400- og 401-svar. Brug kun en anden model for et afgrænset sæt genprøvelige fejl og kun når anmodningsskemaet er kompatibelt; se CometAPI Model Fallback Guide for en tolags fallback-kæde (CometAPI primær → CometAPI fallback-model → officiel udbyder).
  • Redigér logs. Hold API-nøgler, fulde prompter og følsomme modeluddata ude af produktionsfejllogs.

Én Next.js-backend, flere modelvalg

Modelskiftet hører hjemme i din serverpolitik, ikke i separate udbyderkonti. En Next.js Route Handler kan holde én CometAPI-nøgle privat, acceptere en godkendt model-ID pr. anmodning og streame den valgte model gennem ét OpenAI-kompatibelt endpoint. Frontenden forbliver enkel, mens backenden bevarer kontrol over adgang, validering, observabilitet og omkostninger. Den samme konto og nøgle rækker også til CometAPIs native billed- og video-API'er — såsom Flux til billedgenerering og Kling til videogenerering — så denne chat-backend kan udvides til multimodale arbejdsgange uden en anden integration.

Brug CometAPIs offentlige modelkatalog til modelopdagelse og GET https://api.cometapi.com/api/models til automatiseret ruteringsvalidering.

Ofte stillede spørgsmål

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

Ja. Behold én serverside-rute, og send en model-ID fra tilladelseslisten med hver anmodning. Browser-UI'et kan tilbyde modelvalg, mens serveren styrer, hvilke ID'er der accepteres.

Hvordan skifter jeg mellem GPT, Claude og Gemini?

Send den valgte model-ID i anmodningskroppen. Serveren validerer den mod en tilladelsesliste og videresender den samme chat-payload til den valgte model gennem det OpenAI-kompatible endpoint.

Hvor skal jeg opbevare min CometAPI API-nøgle?

Opbevar den i en serverside-miljøvariabel som COMETAPI_API_KEY. Eksponér den aldrig via en NEXT_PUBLIC_-variabel eller klientkode.

Gør et OpenAI-kompatibelt API alle modeller udskiftelige?

Nej. Anmodningsformen er portabel, men modeller kan variere i understøttede parametre, kontekstgrænser, værktøjsadfærd, outputstil, latenstid og pris. Test hver model på tilladelseslisten med dine produktionsprompter.

Kan Next.js Route Handlers streame AI-svar?

Ja. En Route Handler kan returnere den opstrøms Server-Sent Events-strøm med en text/event-stream content type, forudsat at deploymentsplatformen og enhver proxy foran den ikke buffer svaret.

Kan jeg implementere denne app som et statisk Next.js-site?

Nej. En statisk eksport kan ikke køre den serverside Route Handler for /api/chat eller beskytte API-nøglen. Brug en Node.js- eller anden kompatibel serverruntime, der understøtter streamingsvar.

Fortsæt læring

Knyt denne artikel til den næste beslutning.

Se alle emner
Udgivet den Aug 25, 2026
Sidst opdateret Sep 3, 2026
11 visninger
Gennemgået for klarhed, kildeangivelse og aktuel API-terminologi.

Læs mere