Untuk chatbot Next.js yang memerlukan beberapa penyedia model, backend yang paling berguna adalah yang memusatkan autentikasi sekaligus membiarkan aplikasi memilih model per permintaan. CometAPI menyediakan pola tersebut: server menyimpan satu kunci API dan OpenAI-compatible base URL https://api.cometapi.com/v1, sementara field model pada permintaan memilih GPT, Claude, Gemini, atau model chat lainnya yang tersedia.
Tutorial ini membangun proyek App Router yang berfungsi dengan daftar-izinkan model di sisi server, Route Handler streaming, pemilih model di browser, variabel lingkungan terlindungi, antarmuka chat lengkap, dan panduan deployment. Kunci CometAPI tidak pernah mencapai browser.
Apa Itu Aplikasi Chat AI Multi-Model?
Arsitektur chat AI multi-model menempatkan satu backend yang dikontrol aplikasi di antara antarmuka pengguna dan beberapa penyedia model. Dalam proyek ini, browser mengirim percakapan ke /api/chat, sementara server menyimpan kredensial CometAPI secara privat, memvalidasi ID model yang diminta, dan meneruskan permintaan melalui satu API yang kompatibel.
Bagaimana Pergantian Model Bekerja di Next.js?
Mengganti model adalah keputusan routing. Browser mengirimkan ID model yang disetujui bersama percakapan ke /api/chat. Route Handler menambahkan kredensial CometAPI, memanggil POST /v1/chat/completions, dan melakukan streaming respons model terpilih kembali ke browser.
Endpoint bersama tidak membuat semua model menjadi identik. Gaya output, perilaku tool, parameter yang didukung, batas konteks, dan harga dapat berbeda. Simpan ID model dalam daftar-izinkan sisi server dan uji setiap rute dengan prompt aplikasi yang sama sebelum membuatnya tersedia bagi pengguna.
Apa yang Akan Anda Bangun
Aplikasi selesai memiliki satu backend Next.js dan tiga rute chat yang dapat dipilih:
| ID Model | Peran contoh | Harga CometAPI |
|---|---|---|
| gemini-3.7-flash | Sensitif biaya, chat volume tinggi | $0.60 input / $3.00 output |
| claude-opus-5 | Penalaran premium yang fokus | $4 input / $20 output |
| gpt-5.6 | gpt-5.6 adalah rute umum CometAPI untuk GPT-5.6 dan saat ini memetakan ke tier Sol. | $3.2 input / $16 output di tier konteks pendek |
Catatan harga: Harga di bawah dalam USD per satu juta token, diperiksa pada 21 Agustus 2026, dan dapat berubah. Selalu verifikasi tarif saat ini di halaman model sebelum penggunaan produksi. Tabel ini adalah contoh routing, bukan peringkat kualitas. GPT-5.6 menggunakan tier harga yang lebih tinggi di atas 272.000 token. Lihat halaman model tertaut Gemini 3.7 Flash, Claude Opus 5, dan GPT-5.6 untuk tarif ber-tanggal yang digunakan di sini.
CometAPI memberi harga model secara transparan terhadap API resmi tiap penyedia. Model dengan harga resmi yang disatukan — OpenAI, Claude, Gemini, dan sejenisnya — ditagih per token dengan rasio 0,8:1 terhadap harga resmi (diskon 20%); model tanpa API resmi (MidJourney, Kling, Luma) ditagih per panggilan dengan tarif yang ditetapkan CometAPI, juga didiskon 20%. Lihat Panduan Harga CometAPI untuk formula markup, tarif per model, dan unit penagihan.
Ketiga rute mencakup trade-off yang berbeda. Gemini 3.7 Flash adalah andalan agentic Google yang efisien, dengan input multimodal dan jendela konteks 1.048.576 token untuk chat volume tinggi, pengodean, dan alur kerja pengetahuan. Claude Opus 5 adalah model penalaran frontier dari Anthropic, kuat dalam analisis multi-langkah, kode, dan penulisan panjang yang hati-hati dengan harga premium. GPT-5.6 adalah model serbaguna dari OpenAI, menyeimbangkan penalaran, penggunaan tool, dan drafting di jendela konteks lebar untuk trafik produksi sehari-hari.
Sebelum Memulai
Anda memerlukan Node.js, npm, akun CometAPI, dan kunci API sisi server dari CometAPI Quick Start. Tiga ID model persis di atas tersedia di katalog live pada 21 Agustus 2026, tidak ditandai sebagai upcoming, dan mengekspos POST /v1/chat/completions.
Tutorial ini menggunakan pengaturan bersama berikut:
- API key:
COMETAPI_API_KEY - Base URL:
https://api.cometapi.com/v1 - Catalog endpoint: GET
https://api.cometapi.com/api/models
Langkah 1: Buat Aplikasi Next.js
npx create-next-app@latest multi-model-chat --ts --app --eslintcd multi-model-chatnpm run dev
Tidak diperlukan SDK penyedia model untuk versi ini. Server menggunakan fetch bawaan dan meneruskan stream Server-Sent Events hulu.
Langkah 2: Simpan Kunci CometAPI di Server
Buat .env.local di root proyek:
COMETAPI_API_KEY=replace_with_your_cometapi_keyCOMETAPI_BASE_URL=https://api.cometapi.com/v1
Jangan beri awalan kunci dengan NEXT_PUBLIC_. Next.js hanya mengekspos variabel dengan awalan tersebut ke bundel browser; kunci API berada di Route Handler sisi server.
Langkah 3: Definisikan Kebijakan Model
Tempatkan ID yang diizinkan di backend, bukan hanya di dropdown. Pengguna dapat melewati kontrol browser dan memanggil rute Anda secara langsung, sehingga server harus menolak nilai model yang tidak dikenal.
const ALLOWED_MODELS = [ "gemini-3.7-flash", "claude-opus-5", "gpt-5.6",] as const;type ModelId = (typeof ALLOWED_MODELS)[number];
Klien akan menggunakan tiga ID yang sama untuk selektornya, sementara server tetap menjadi sumber kebenaran.
Langkah 4: Streaming CometAPI Melalui Route Handler
Buat app/api/chat/route.ts. Rute ini memvalidasi payload chat teks-saja, memanggil CometAPI dengan stream: true, dan mengembalikan stream event hulu tanpa mengekspos kredensial.
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 meneruskan sinyal putus-koneksi dari browser ke hulu, sehingga menutup permintaan dapat menghentikan generasi yang tidak perlu. Ini juga mengembalikan error yang disanitasi ke klien sambil menjaga detail diagnostik di log server.
Langkah 5: Tambahkan Pemilihan Model dan Parsing Stream
Buat komponen klien di app/page.tsx. Komponen ini mengirim model terpilih bersama percakapan, mem-parsing setiap event data:, dan menambahkan teks delta.content inkremental ke pesan asisten terakhir.
Browser tidak pernah memanggil CometAPI secara langsung. Satu-satunya tujuan adalah rute /api/chat milik Anda, yang menjaga kunci tetap privat dan menegakkan daftar-izinkan.
Kode Proyek Inti
Route Handler di atas adalah seluruh isi file app/api/chat/route.ts. Tambahkan halaman, layout, dan stylesheet berikut untuk menyelesaikan proyek yang dapat dijalankan.
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. */
Langkah 6: Jalankan dan Deploy Aplikasi
npm run dev
Buka http://localhost:3000, pilih model, dan kirim pesan. Untuk deployment Node.js produksi, tambahkan COMETAPI_API_KEY dan COMETAPI_BASE_URL ke pengaturan variabel lingkungan sisi server pada host, lalu jalankan:
npm run buildnpm run start
Gunakan platform hosting yang mendukung respons streaming. Ekspor statis tidak dapat menjalankan Route Handler /api/chat.
Uji Rute Streaming
Dengan server pengembangan berjalan, panggil backend Anda secara langsung:
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."} ] }'
Permintaan yang berhasil mengembalikan Server-Sent Events. ID dan teks tepatnya bervariasi, tetapi stream mengikuti bentuk ini:
data: {"choices":[{"delta":{"content":"Model"}}]}data: {"choices":[{"delta":{"content":" routing"}}]}data: [DONE]
Parser browser membaca setiap event SSE, mengekstrak choices[0].delta.content, dan menambahkan teks yang di-stream ke pesan asisten saat potongan tiba.
Error Integrasi Umum
| Gejala | Penyebab | Perbaikan |
|---|---|---|
| Error autentikasi 401 | Kunci sisi server hilang atau tidak valid | Setel COMETAPI_API_KEY; jangan mengeksposnya dengan NEXT_PUBLIC_. |
| 404 atau rute salah | Base URL tidak menyertakan /v1 | Gunakan https://api.cometapi.com/v1. |
| 400 model tidak didukung | ID tidak ada di daftar-izinkan backend | Gunakan ID model teks live yang tepat dan perbarui kedua selector. |
| Jawaban muncul sekaligus | Host atau proxy melakukan buffer stream | Nonaktifkan transformasi respons dan gunakan deployment Node.js yang mendukung streaming. |
Siapkan Backend Chat untuk Produksi
- Autentikasi pengguna Anda sendiri. Jangan mengekspos rute publik yang menghabiskan kredit untuk trafik anonim.
- Batasi laju berdasarkan pengguna dan IP. Batasi stream konkuren, permintaan per menit, jumlah pesan, dan panjang pesan.
- Pertahankan daftar-izinkan model di sisi server. Selector di browser adalah kemudahan, bukan batasan keamanan.
- Validasi katalog live saat deployment. Query
GEThttps://api.cometapi.com/api/modelsdan gagal-kan rilis bila ID yang dikonfigurasi berstatus upcoming, tidak tersedia, atau tidak memiliki endpoint chat-completions. - Lacak biaya per rute. Log model yang dipilih, ID permintaan, latensi, penggunaan token, dan ID pengguna. Setel kuota kunci atau batas pengeluaran di dashboard CometAPI bila perlu.
- Tangani disconnect dan timeout. Pertahankan
request.signal, tetapkan timeout aplikasi, dan hentikan kerja saat klien keluar. - Jangan menyembunyikan error konfigurasi dengan fallback. Permukaan-kan respons 400 dan 401. Gunakan model lain hanya untuk kumpulan kegagalan yang dapat di-retry dan hanya ketika skema permintaan kompatibel; lihat Panduan Model Fallback CometAPI untuk rantai fallback dua-lapis (CometAPI primer → model fallback CometAPI → penyedia resmi).
- Sensor log. Jangan menyimpan kunci API, prompt penuh, dan output model sensitif di log error produksi.
Satu Backend Next.js, Beberapa Pilihan Model
Saklar model berada dalam kebijakan server Anda, bukan di akun penyedia terpisah. Route Handler Next.js dapat menyimpan satu kunci CometAPI secara privat, menerima ID model yang disetujui per permintaan, dan melakukan streaming model terpilih melalui satu endpoint yang kompatibel dengan OpenAI. Frontend tetap sederhana, sementara backend mempertahankan kontrol atas akses, validasi, observabilitas, dan biaya. Akun dan kunci yang sama juga menjangkau API gambar dan video native CometAPI — seperti Flux untuk pembuatan gambar dan Kling untuk pembuatan video — sehingga backend chat ini dapat diperluas ke alur kerja multimodal tanpa integrasi kedua.
Gunakan direktori model publik CometAPI untuk penemuan model dan GET https://api.cometapi.com/api/models untuk validasi routing otomatis.
Pertanyaan yang Sering Diajukan
Bisakah saya menggunakan beberapa model AI dalam satu aplikasi Next.js?
Bisa. Pertahankan satu rute sisi server dan kirim ID model yang ada di daftar-izinkan dengan setiap permintaan. UI browser dapat menawarkan pilihan model, sementara server mengontrol ID mana yang diterima.
Bagaimana cara beralih antara GPT, Claude, dan Gemini?
Kirim ID model yang dipilih di body permintaan. Server memvalidasinya terhadap daftar-izinkan dan meneruskan payload chat yang sama ke model yang dipilih melalui endpoint yang kompatibel dengan OpenAI.
Di mana sebaiknya saya menyimpan kunci API CometAPI?
Simpan di variabel lingkungan sisi server seperti COMETAPI_API_KEY. Jangan pernah mengeksposnya melalui variabel NEXT_PUBLIC_ atau kode sisi klien.
Apakah API yang kompatibel dengan OpenAI membuat semua model saling dapat dipertukarkan?
Tidak. Bentuk permintaan portabel, tetapi model dapat berbeda dalam parameter yang didukung, batas konteks, perilaku tool, gaya output, latensi, dan harga. Uji setiap model dalam daftar-izinkan dengan prompt produksi Anda.
Bisakah Route Handler Next.js melakukan streaming respons AI?
Bisa. Route Handler dapat mengembalikan stream Server-Sent Events hulu dengan tipe konten text/event-stream, selama platform deployment dan proxy di depannya tidak melakukan buffering respons.
Bisakah saya mendepoy aplikasi ini sebagai situs Next.js statis?
Tidak. Ekspor statis tidak dapat menjalankan Route Handler /api/chat sisi server atau melindungi kunci API. Gunakan runtime Node.js atau server kompatibel lain yang mendukung respons streaming.
