DeepSeek Vision and Grok Imagine models are now live on CometAPI →
guide/Badania CometAPI

Jak zbudować wielomodelową aplikację czatu AI z użyciem Next.js i CometAPI

Zbuduj strumieniową aplikację czatu AI w Next.js, która przełącza się między modelami GPT, Claude i Gemini, korzystając z jednego klucza CometAPI i jednego punktu końcowego backendu.

CometAPI
AnnaZespół badań AI modeli i API
Zaktualizowano Aug 26, 2026 12 min czyt.
Jak zbudować wielomodelową aplikację czatu AI z użyciem Next.js i CometAPI
Użyj tego wzorca

Wykonaj pierwsze wywołanie 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)

Dla chatbota Next.js, który potrzebuje kilku dostawców modeli, najprzydatniejszy backend to taki, który centralizuje uwierzytelnianie, a jednocześnie pozwala aplikacji wybierać model dla każdego żądania. CometAPI realizuje ten wzorzec: serwer przechowuje jeden klucz API oraz zgodny z OpenAI adres bazowy https://api.cometapi.com/v1, a pole model w żądaniu wybiera dostępny model czatu GPT, Claude, Gemini lub inny.

Ten samouczek tworzy działający projekt App Router z serwerową listą dozwolonych modeli, Route Handlerem do streamingu, przeglądarkowym selektorem modeli, chronionymi zmiennymi środowiskowymi, kompletnym interfejsem czatu i wskazówkami wdrożeniowymi. Klucz CometAPI nigdy nie trafia do przeglądarki.

Czym jest wielomodelowa aplikacja czatu AI?

Architektura czatu AI obsługująca wiele modeli umieszcza między interfejsem użytkownika a kilkoma dostawcami modeli jeden backend kontrolowany przez aplikację. W tym projekcie przeglądarka wysyła rozmowę do /api/chat, podczas gdy serwer zachowuje poufność poświadczenia CometAPI, weryfikuje żądany identyfikator modelu i przekazuje żądanie przez jeden kompatybilny interfejs API.

Jak działa przełączanie modeli w Next.js?

Zmiana modeli to decyzja routingu. Przeglądarka wysyła zatwierdzony identyfikator modelu wraz z rozmową do /api/chat. Route Handler dodaje poświadczenie CometAPI, wywołuje POST /v1/chat/completions i streamuje odpowiedź wybranego modelu z powrotem do przeglądarki.

Wspólny endpoint nie czyni wszystkich modeli identycznymi. Styl wyjściowy, zachowanie narzędzi, obsługiwane parametry, limity kontekstu i ceny mogą się różnić. Utrzymuj identyfikatory modeli na serwerowej liście dozwolonych i testuj każdą trasę tymi samymi promtami aplikacyjnymi, zanim udostępnisz ją użytkownikom.

Co zbudujesz

Gotowa aplikacja ma jeden backend Next.js i trzy wybieralne trasy czatu:

ID modeluPrzykładowa rolaCena w CometAPI
gemini-3.7-flashKosztowo wrażliwy, wysokowolumenowy czat$0.60 input / $3.00 output
claude-opus-5Skoncentrowane, premium rozumowanie$4 input / $20 output
gpt-5.6gpt-5.6 to ogólna trasa CometAPI dla GPT-5.6 i obecnie mapuje się na poziom Sol.$3.2 input / $16 output w warstwie short-context

Uwaga cenowa: Poniższe ceny są w USD za milion tokenów, zostały sprawdzone 21 sierpnia 2026 r. i mogą ulec zmianie. Zawsze weryfikuj aktualne stawki na stronach modeli przed użyciem produkcyjnym. Tabela jest przykładem routingu, a nie rankingiem jakości. GPT-5.6 używa wyższego poziomu cen powyżej 272 000 tokenów. Zobacz podlinkowane strony modeli Gemini 3.7 Flash, Claude Opus 5 i GPT-5.6 dla stawek użytych tutaj (z datą odniesienia).

CometAPI wycenia modele w sposób przejrzysty względem oficjalnych interfejsów API dostawców. Modele z ujednoliconym oficjalnym cennikiem — OpenAI, Claude, Gemini i podobne — są rozliczane na token w stosunku 0,8:1 do oficjalnej ceny, co odpowiada 20% rabatowi; modele bez oficjalnych API (MidJourney, Kling, Luma) są rozliczane per wywołanie według stawek CometAPI, również z 20% rabatem. Zobacz Przewodnik po cenach CometAPI w celu poznania formuły marży, stawek per model i jednostek rozliczeniowych.

Trzy trasy obejmują różne kompromisy. Gemini 3.7 Flash to wydajny, agencyjny koń roboczy Google, obsługujący wejście multimodalne i okno kontekstu 1 048 576 tokenów do czatów o dużym wolumenie, kodowania i zadań wiedzochłonnych. Claude Opus 5 to czołowy model rozumowania Anthropic, mocny w wieloetapowej analizie, kodzie i uważnym długim pisaniu, w wyższej cenie. GPT-5.6 to model ogólnego przeznaczenia OpenAI, równoważący rozumowanie, użycie narzędzi i drafting w szerokim oknie kontekstu dla codziennego ruchu produkcyjnego.

Zanim zaczniesz

Potrzebujesz Node.js, npm, konta CometAPI i serwerowego klucza API z CometAPI Quick Start. Powyższe trzy dokładne identyfikatory modeli były dostępne w katalogu na żywo 21 sierpnia 2026 r., nie były oznaczone jako nadchodzące i udostępniały POST /v1/chat/completions.

Samouczek używa tych wspólnych ustawień:

Krok 1: Utwórz aplikację Next.js

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

W tej wersji nie jest wymagany żaden SDK dostawcy modeli. Serwer używa wbudowanego API fetch i przekazuje dalej strumień Server-Sent Events z upstreamu.

Krok 2: Przechowuj klucz CometAPI na serwerze

Utwórz .env.local w katalogu głównym projektu:

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

Nie poprzedzaj klucza prefiksem NEXT_PUBLIC_. Next.js eksponuje do pakietów przeglądarkowych tylko zmienne z tym prefiksem; klucz API należy do serwerowego Route Handlera.

Krok 3: Zdefiniuj politykę modeli

Umieść dozwolone identyfikatory w backendzie, a nie tylko w rozwijanym menu. Użytkownik może ominąć kontrolki przeglądarki i wywołać Twoją trasę bezpośrednio, więc serwer musi odrzucać nieznane wartości modelu.

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

Klient użyje tych samych trzech identyfikatorów dla selektora, natomiast serwer pozostaje źródłem prawdy.

Krok 4: Przekazuj strumień CometAPI przez Route Handler

Utwórz app/api/chat/route.ts. Trasa weryfikuje ładunek czatu tekstowego, wywołuje CometAPI z stream: true i zwraca strumień zdarzeń z upstreamu bez ujawniania poświadczeń.

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

Handler przekazuje w górę sygnał rozłączenia przeglądarki, więc zamknięcie żądania może zatrzymać zbędną generację. Zwraca też zanonimizowany błąd do klienta, pozostawiając szczegóły diagnostyczne w logach serwera.

Krok 5: Dodaj wybór modelu i parsowanie strumienia

Utwórz komponent kliencki w app/page.tsx. Wysyła wybrany model wraz z rozmową, parsuje każdy event data: i dodaje przyrostowy tekst delta.content do ostatniej wiadomości asystenta.

Przeglądarka nigdy nie wywołuje CometAPI bezpośrednio. Jej jedynym celem jest Twoja własna trasa /api/chat, która zachowuje poufność klucza i egzekwuje listę dozwolonych modeli.

Kluczowy kod projektu

Powyższy Route Handler to kompletny plik app/api/chat/route.ts. Dodaj poniższą stronę, layout i arkusz stylów, aby ukończyć uruchamialny 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. */

Krok 6: Uruchom i wdroż aplikację

npm run dev

Otwórz http://localhost:3000, wybierz model i wyślij wiadomość. W produkcyjnym wdrożeniu Node.js dodaj COMETAPI_API_KEY i COMETAPI_BASE_URL do serwerowych ustawień środowiskowych hosta, a następnie uruchom:

npm run buildnpm run start

Użyj hostingu, który obsługuje odpowiedzi strumieniowe. Statyczny eksport nie może uruchomić Route Handlera /api/chat.

Przetestuj trasę strumieniowania

Przy działającym serwerze deweloperskim wywołaj backend bezpośrednio:

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

Udane żądanie zwraca Server-Sent Events. Dokładne identyfikatory i tekst różnią się, ale strumień ma następujący kształt:

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

Parser w przeglądarce czyta każde zdarzenie SSE, wyodrębnia choices[0].delta.content i dołącza strumieniowany tekst do wiadomości asystenta w miarę napływania fragmentów.

Typowe błędy integracji

ObjawPrzyczynaRozwiązanie
Błąd uwierzytelnienia 401Brakujący lub nieprawidłowy klucz po stronie serweraUstaw COMETAPI_API_KEY; nie eksponuj go przez NEXT_PUBLIC_.
404 lub niewłaściwa trasaW adresie bazowym brakuje /v1Użyj https://api.cometapi.com/v1.
400 nieobsługiwany modelID nie znajduje się na serwerowej allowliścieUżyj dokładnego, aktywnego ID modelu tekstowego i zaktualizuj oba selektory.
Odpowiedź pojawia się narazHost lub proxy buforuje strumień zdarzeńWyłącz transformację odpowiedzi i użyj wdrożenia Node.js obsługującego streaming.

Przygotuj backend czatu do produkcji

  • Uwierzytelniaj własnych użytkowników. Nie eksponuj publicznej trasy, która zużywa środki dla anonimowego ruchu.
  • Ogranicz tempo na użytkownika i IP. Ogranicz równoległe strumienie, liczbę żądań na minutę, liczbę wiadomości i długość wiadomości.
  • Utrzymuj listę dozwolonych modeli po stronie serwera. Selector w przeglądarce to wygoda, nie granica bezpieczeństwa.
  • Waliduj żywy katalog podczas wdrożenia. Odpytaj GET https://api.cometapi.com/api/models i przerwij wydanie, jeśli skonfigurowany ID jest nadchodzący, niedostępny lub nie obsługuje endpointu chat-completions.
  • Śledź koszt per trasa. Loguj wybrany model, ID żądania, latencję, wykorzystanie tokenów i ID użytkownika. Ustaw limity kluczy lub limity wydatków w panelu CometAPI tam, gdzie to właściwe.
  • Obsługuj rozłączenia i timeouty. Zachowaj request.signal, ustaw timeout aplikacji i przerywaj pracę, gdy klient odchodzi.
  • Nie ukrywaj błędów konfiguracyjnych fallbackiem. Przekazuj 400 i 401. Używaj innego modelu tylko dla ograniczonego zestawu błędów możliwych do ponowienia i tylko wtedy, gdy schemat żądania jest kompatybilny; zobacz Przewodnik po fallbacku modeli w CometAPI dla dwuwarstwowego łańcucha fallbacku (CometAPI primary → CometAPI fallback model → oficjalny dostawca).
  • Redaguj logi. Nie umieszczaj kluczy API, pełnych promptów i wrażliwych wyjść modeli w produkcyjnych logach błędów.

Jeden backend Next.js, kilka wyborów modeli

Przełącznik modelu należy do polityki serwera, a nie do oddzielnych kont dostawców. Route Handler w Next.js może przechowywać prywatny klucz CometAPI, akceptować zatwierdzony identyfikator modelu na żądanie i streamować wybrany model przez jeden zgodny z OpenAI endpoint. Frontend pozostaje prosty, a backend zachowuje kontrolę nad dostępem, walidacją, obserwowalnością i kosztem. To samo konto i klucz sięgają też natywnych API obrazów i wideo CometAPI — takich jak Flux do generowania obrazów i Kling do generowania wideo — więc ten backend czatu może rozszerzyć się na przepływy multimodalne bez drugiej integracji.

Użyj publicznego katalogu modeli CometAPI do odkrywania modeli oraz GET https://api.cometapi.com/api/models do automatycznej walidacji routingu.

Najczęściej zadawane pytania

Czy mogę używać wielu modeli AI w jednej aplikacji Next.js?

Tak. Utrzymuj jedną trasę po stronie serwera i przekazuj z każdym żądaniem identyfikator modelu z listy dozwolonych. UI przeglądarki może oferować wybór modeli, podczas gdy serwer kontroluje, które ID są akceptowane.

Jak przełączać się między GPT, Claude i Gemini?

Wyślij wybrany identyfikator modelu w treści żądania. Serwer weryfikuje go względem listy dozwolonych i przekazuje ten sam ładunek czatu do wybranego modelu przez kompatybilny endpoint OpenAI.

Gdzie powinienem przechowywać klucz API CometAPI?

Przechowuj go w zmiennej środowiskowej po stronie serwera, takiej jak COMETAPI_API_KEY. Nigdy nie eksponuj go przez zmienną NEXT_PUBLIC_ ani w kodzie klienckim.

Czy zgodne z OpenAI API sprawia, że wszystkie modele są zamienne?

Nie. Kształt żądania jest przenośny, ale modele mogą różnić się obsługiwanymi parametrami, limitami kontekstu, zachowaniem narzędzi, stylem wyjścia, latencją i ceną. Testuj każdy model na liście dozwolonych Twoimi produkcyjnymi promptami.

Czy Route Handlery w Next.js mogą streamować odpowiedzi AI?

Tak. Route Handler może zwrócić strumień Server-Sent Events z text/event-stream, o ile platforma wdrożeniowa i ewentualny proxy przed nią nie buforują odpowiedzi.

Czy mogę wdrożyć tę aplikację jako statyczną stronę Next.js?

Nie. Statyczny eksport nie uruchomi serwerowej trasy /api/chat ani nie ochroni klucza API. Użyj środowiska serwerowego Node.js lub innego kompatybilnego, które obsługuje odpowiedzi strumieniowe.

Kontynuuj naukę

Połącz ten artykuł z następną decyzją.

Zobacz wszystkie tematy
Opublikowano Aug 25, 2026
Ostatnia aktualizacja Aug 26, 2026
1 wyświetleń
Sprawdzone pod kątem przejrzystości, atrybucji źródeł i aktualnej terminologii API.

Gotowy na obniżenie kosztów rozwoju AI o 20%?

Zacznij za darmo w kilka minut. Dołączone kredyty na bezpłatny okres próbny. Karta kredytowa nie jest wymagana.

Czytaj więcej