Đối với một chatbot Next.js cần nhiều nhà cung cấp mô hình, backend hữu ích nhất là backend tập trung xác thực nhưng cho phép ứng dụng chọn mô hình theo từng yêu cầu. CometAPI cung cấp mẫu này: máy chủ giữ một khóa API duy nhất và OpenAI-compatible base URL https://api.cometapi.com/v1, trong khi trường model của yêu cầu chọn một GPT, Claude, Gemini hoặc mô hình chat khác.
Hướng dẫn này xây dựng một dự án App Router hoạt động với danh sách cho phép mô hình phía máy chủ, Route Handler truyền phát, bộ chọn mô hình trên trình duyệt, biến môi trường được bảo vệ, giao diện chat hoàn chỉnh và hướng dẫn triển khai. Khóa CometAPI không bao giờ đến trình duyệt.
Ứng dụng trò chuyện AI đa mô hình là gì?
Kiến trúc chat đa mô hình đặt một backend do ứng dụng kiểm soát giữa giao diện người dùng và nhiều nhà cung cấp mô hình. Trong dự án này, trình duyệt gửi hội thoại đến /api/chat, trong khi máy chủ giữ bí mật thông tin xác thực CometAPI, xác thực ID mô hình được yêu cầu và chuyển tiếp yêu cầu qua một API tương thích.
Chuyển đổi mô hình trong Next.js hoạt động như thế nào?
Thay đổi mô hình là một quyết định định tuyến. Trình duyệt gửi một ID mô hình đã được phê duyệt cùng hội thoại đến /api/chat. Route Handler thêm thông tin xác thực CometAPI, gọi POST /v1/chat/completions, và truyền phát phản hồi của mô hình đã chọn trở lại trình duyệt.
Endpoint dùng chung không làm cho mọi mô hình giống hệt nhau. Phong cách đầu ra, hành vi công cụ, tham số hỗ trợ, giới hạn ngữ cảnh và giá có thể khác nhau. Hãy giữ các ID mô hình trong danh sách cho phép phía máy chủ và kiểm thử mỗi tuyến với cùng lời nhắc ứng dụng trước khi cung cấp cho người dùng.
Bạn sẽ xây dựng gì
Ứng dụng hoàn chỉnh có một backend Next.js và ba tuyến chat có thể chọn:
| Model ID | Vai trò ví dụ | Giá CometAPI |
|---|---|---|
| gemini-3.7-flash | Trò chuyện nhạy chi phí, khối lượng lớn | $0.60 input / $3.00 output |
| claude-opus-5 | Lập luận cao cấp tập trung | $4 input / $20 output |
| gpt-5.6 | gpt-5.6 là tuyến CometAPI chung cho GPT-5.6 và hiện ánh xạ đến hạng Sol | $3.2 input / $16 output trong hạng short-context |
Lưu ý về giá: Giá dưới đây tính theo USD trên mỗi triệu token, được kiểm tra vào ngày 21 tháng 8 năm 2026 và có thể thay đổi. Luôn xác minh mức giá hiện tại trên trang mô hình trước khi dùng trong sản xuất. Bảng là ví dụ định tuyến, không phải xếp hạng chất lượng. GPT-5.6 dùng hạng giá cao hơn trên 272.000 token. Xem các trang mô hình được liên kết Gemini 3.7 Flash, Claude Opus 5, và GPT-5.6 cho mức giá có ngày áp dụng tại đây.
CometAPI định giá mô hình minh bạch theo API chính thức của từng nhà cung cấp. Các mô hình có giá chính thức thống nhất — OpenAI, Claude, Gemini và tương tự — được tính phí theo token với tỷ lệ 0,8:1 so với giá chính thức, chiết khấu 20%; các mô hình không có API chính thức (MidJourney, Kling, Luma) được tính theo mỗi lần gọi ở mức do CometAPI đặt, cũng chiết khấu 20%. Xem Hướng dẫn định giá CometAPI để biết công thức tính, mức giá theo mô hình và đơn vị tính.
Ba tuyến bao phủ các đánh đổi khác nhau. Gemini 3.7 Flash là “workhorse” agentic hiệu quả của Google, với đầu vào đa phương thức và cửa sổ ngữ cảnh 1.048.576 token cho chat khối lượng lớn, viết code và quy trình tri thức. Claude Opus 5 là mô hình lập luận đầu bảng của Anthropic, mạnh về phân tích nhiều bước, code và viết dài cẩn trọng với giá cao cấp. GPT-5.6 là mô hình mục đích chung của OpenAI, cân bằng giữa lập luận, dùng tool và soạn thảo trong một cửa sổ ngữ cảnh rộng cho lưu lượng sản xuất hàng ngày.
Trước khi bắt đầu
Bạn cần Node.js, npm, tài khoản CometAPI và khóa API phía máy chủ từ CometAPI Quick Start. Ba ID mô hình chính xác ở trên có trong catalog trực tiếp vào ngày 21 tháng 8 năm 2026, không được đánh dấu upcoming và cung cấp POST /v1/chat/completions.
Hướng dẫn này dùng các thiết lập chung sau:
- API key:
COMETAPI_API_KEY - Base URL:
https://api.cometapi.com/v1 - Catalog endpoint:
GEThttps://api.cometapi.com/api/models
Bước 1: Tạo ứng dụng Next.js
npx create-next-app@latest multi-model-chat --ts --app --eslintcd multi-model-chatnpm run dev
Phiên bản này không cần SDK nhà cung cấp mô hình. Máy chủ dùng fetch tích hợp và chuyển tiếp luồng Server-Sent Events upstream.
Bước 2: Giữ khóa CometAPI trên máy chủ
Tạo .env.local ở thư mục gốc dự án:
COMETAPI_API_KEY=replace_with_your_cometapi_keyCOMETAPI_BASE_URL=https://api.cometapi.com/v1
Không thêm tiền tố NEXT_PUBLIC_ cho khóa. Next.js chỉ lộ biến có tiền tố đó cho bundle phía trình duyệt; khóa API phải thuộc về Route Handler phía máy chủ.
Bước 3: Định nghĩa chính sách mô hình
Đặt các ID được phép trong backend, không chỉ ở dropdown. Người dùng có thể bỏ qua điều khiển trình duyệt và gọi trực tiếp tuyến của bạn, vì vậy máy chủ phải từ chối các giá trị mô hình không xác định.
const ALLOWED_MODELS = [ "gemini-3.7-flash", "claude-opus-5", "gpt-5.6",] as const;type ModelId = (typeof ALLOWED_MODELS)[number];
Client sẽ dùng cùng ba ID cho bộ chọn của nó, trong khi máy chủ vẫn là nguồn sự thật.
Bước 4: Truyền phát CometAPI qua Route Handler
Tạo app/api/chat/route.ts. Tuyến này xác thực payload chat chỉ có văn bản, gọi CometAPI với stream: true và trả về luồng sự kiện upstream mà không lộ thông tin xác thực.
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 chưa được cấu hình." }, { status: 500 }, ); } let payload: { model?: unknown; messages?: unknown }; try { payload = await request.json(); } catch { return Response.json({ error: "Nội dung JSON không hợp lệ." }, { status: 400 }); } if (!isModelId(payload.model)) { return Response.json({ error: "ID mô hình không được hỗ trợ." }, { status: 400 }); } if ( !Array.isArray(payload.messages) || payload.messages.length === 0 || payload.messages.length > 50 || !payload.messages.every(isChatMessage) ) { return Response.json( { error: "messages phải chứa từ 1 đến 50 tin nhắn văn bản hợp lệ." }, { 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: "Yêu cầu đến mô hình đã chọn thất bại.", status: upstream.status, requestId, }, { status: upstream.status }, ); } if (!upstream.body) { return Response.json( { error: "Mô hình không trả về nội dung phản hồi." }, { 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 chuyển tiếp tín hiệu ngắt kết nối của trình duyệt upstream, vì vậy đóng yêu cầu có thể dừng việc sinh không cần thiết. Nó cũng trả về lỗi đã làm sạch cho client trong khi giữ chi tiết chẩn đoán trong nhật ký máy chủ.
Bước 5: Thêm bộ chọn mô hình và phân tích luồng
Tạo một client component trong app/page.tsx. Nó gửi mô hình đã chọn cùng hội thoại, phân tích mỗi sự kiện data: và nối văn bản delta.content tăng dần vào tin nhắn trợ lý cuối cùng.
Trình duyệt không bao giờ gọi CometAPI trực tiếp. Điểm đến duy nhất của nó là tuyến /api/chat của chính bạn, tuyến này giữ khóa ở chế độ riêng tư và thực thi danh sách cho phép.
Mã cốt lõi của dự án
Route Handler ở trên là toàn bộ tệp app/api/chat/route.ts. Thêm trang, layout và stylesheet sau để hoàn tất dự án có thể chạy.
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("Không khả dụng tính năng truyền."); 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>Trò chuyện đa mô hình</h1> <label> Mô hình <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">Chọn một mô hình và gửi tin nhắn.</p> ) : ( messages.map((message, index) => ( <article className={message.role} key={`${message.role}-${index}`}> <b>{message.role === "user" ? "Bạn" : "Trợ lý"}</b> <p>{message.content || "…"}</p> </article> )) )} </div> <form onSubmit={sendMessage}> <textarea value={input} onChange={(event) => setInput(event.target.value)} placeholder="Hãy hỏi điều gì đó…" rows={3} maxLength={20_000} /> <button disabled={loading || !input.trim()} type="submit"> {loading ? "Đang truyền…" : "Gửi"} </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: "Trò chuyện đa mô hình", description: "Ứng dụng chat Next.js truyền phát do CometAPI cung cấp.",};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; }/* Vỏ bố cục và thẻ chat */.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; }/* Danh sách tin nhắn và bong bóng */.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; }/* Các điều khiển form, nút và trạng thái lỗi theo cùng một chủ đề tối. */
Bước 6: Chạy và triển khai ứng dụng
npm run dev
Mở http://localhost:3000, chọn một mô hình và gửi tin nhắn. Đối với triển khai Node.js sản xuất, thêm COMETAPI_API_KEY và COMETAPI_BASE_URL vào thiết lập môi trường phía máy chủ của host, rồi chạy:
npm run buildnpm run start
Dùng nền tảng hosting hỗ trợ phản hồi truyền phát. Xuất tĩnh không thể chạy Route Handler /api/chat.
Kiểm thử tuyến truyền phát
Với máy chủ phát triển đang chạy, gọi trực tiếp backend của bạn:
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."} ] }'
Một yêu cầu thành công trả về Server-Sent Events. ID và văn bản chính xác có thể khác nhau, nhưng luồng có dạng như sau:
data: {"choices":[{"delta":{"content":"Model"}}]}data: {"choices":[{"delta":{"content":" routing"}}]}data: [DONE]
Trình phân tích trên trình duyệt đọc mỗi sự kiện SSE, trích xuất choices[0].delta.content và nối văn bản được truyền thành từng mảnh vào tin nhắn trợ lý khi các chunk đến.
Lỗi tích hợp thường gặp
| Triệu chứng | Nguyên nhân | Cách khắc phục |
|---|---|---|
| Lỗi xác thực 401 | Thiếu hoặc khóa phía máy chủ không hợp lệ | Đặt COMETAPI_API_KEY; không lộ với NEXT_PUBLIC_. |
| 404 hoặc sai tuyến | Base URL thiếu /v1 | Dùng https://api.cometapi.com/v1. |
| 400 mô hình không được hỗ trợ | ID không có trong danh sách cho phép backend | Dùng ID mô hình văn bản trực tiếp chính xác và cập nhật cả hai bộ chọn. |
| Câu trả lời xuất hiện cùng lúc | Host hoặc proxy đệm luồng sự kiện | Tắt biến đổi phản hồi và dùng nền tảng Node.js hỗ trợ truyền phát. |
Chuẩn bị backend chat cho sản xuất
- Xác thực người dùng của riêng bạn. Không lộ tuyến công khai tiêu tốn tín dụng cho lưu lượng ẩn danh.
- Giới hạn tốc độ theo người dùng và IP. Giới hạn số luồng đồng thời, yêu cầu mỗi phút, số lượng và độ dài tin nhắn.
- Giữ danh sách cho phép mô hình phía máy chủ. Bộ chọn trình duyệt chỉ là tiện ích, không phải ranh giới bảo mật.
- Xác thực catalog trực tiếp trong quá trình triển khai. Gọi
GEThttps://api.cometapi.com/api/modelsvà fail bản phát hành nếu ID cấu hình ở trạng thái upcoming, không khả dụng hoặc thiếu endpoint chat-completions. - Theo dõi chi phí theo tuyến. Ghi lại mô hình đã chọn, request ID, độ trễ, mức dùng token và user ID. Đặt hạn mức khóa hoặc giới hạn chi tiêu trong bảng điều khiển CometAPI khi phù hợp.
- Xử lý ngắt kết nối và timeout. Bảo toàn
request.signal, đặt timeout ứng dụng và dừng công việc khi client rời đi. - Không che giấu lỗi cấu hình bằng fallback. Bộc lộ phản hồi 400 và 401. Chỉ dùng mô hình khác cho một tập nhỏ lỗi có thể thử lại và chỉ khi schema yêu cầu tương thích; xem Hướng dẫn fallback mô hình CometAPI cho chuỗi fallback hai lớp (CometAPI chính → mô hình fallback CometAPI → nhà cung cấp chính thức).
- Rà soát nhật ký. Không đưa khóa API, toàn bộ lời nhắc và nội dung nhạy cảm của mô hình vào nhật ký lỗi sản xuất.
Một backend Next.js, nhiều lựa chọn mô hình
Chuyển đổi mô hình thuộc về chính sách máy chủ của bạn, không phải các tài khoản nhà cung cấp riêng lẻ. Một Next.js Route Handler có thể giữ một khóa CometAPI ở chế độ riêng tư, chấp nhận một ID mô hình được phê duyệt cho mỗi yêu cầu và truyền phát mô hình đã chọn qua một endpoint tương thích OpenAI. Frontend vẫn đơn giản, trong khi backend giữ quyền kiểm soát truy cập, xác thực, quan sát và chi phí. Cùng tài khoản và khóa cũng truy cập các API ảnh và video gốc của CometAPI — như Flux cho tạo ảnh và Kling cho tạo video — vì vậy backend chat này có thể mở rộng sang quy trình đa phương thức mà không cần tích hợp thứ hai.
Dùng thư mục mô hình công khai của CometAPI để khám phá mô hình và GET https://api.cometapi.com/api/models để xác thực định tuyến tự động.
Câu hỏi thường gặp
Tôi có thể dùng nhiều mô hình AI trong một ứng dụng Next.js không?
Có. Giữ một tuyến phía máy chủ và truyền một ID mô hình trong danh sách cho phép với mỗi yêu cầu. Giao diện trình duyệt có thể cung cấp các lựa chọn mô hình, trong khi máy chủ kiểm soát ID nào được chấp nhận.
Tôi chuyển giữa GPT, Claude và Gemini như thế nào?
Gửi ID mô hình đã chọn trong phần thân yêu cầu. Máy chủ xác thực nó với danh sách cho phép và chuyển tiếp cùng payload chat đến mô hình đã chọn thông qua endpoint tương thích OpenAI.
Tôi nên lưu khóa API CometAPI ở đâu?
Lưu ở biến môi trường phía máy chủ như COMETAPI_API_KEY. Không bao giờ lộ nó qua biến NEXT_PUBLIC_ hoặc mã phía client.
API tương thích OpenAI có làm mọi mô hình có thể thay thế cho nhau không?
Không. Hình dạng yêu cầu có thể dùng chung, nhưng mô hình có thể khác nhau về tham số hỗ trợ, giới hạn ngữ cảnh, hành vi công cụ, phong cách đầu ra, độ trễ và giá. Kiểm thử mọi mô hình trong danh sách cho phép với lời nhắc sản xuất của bạn.
Next.js Route Handler có thể truyền phát phản hồi AI không?
Có. Route Handler có thể trả về luồng Server-Sent Events upstream với kiểu nội dung text/event-stream, miễn là nền tảng triển khai và bất kỳ proxy nào phía trước không đệm phản hồi.
Tôi có thể triển khai ứng dụng này như một trang Next.js tĩnh không?
Không. Xuất tĩnh không thể chạy Route Handler /api/chat phía máy chủ hoặc bảo vệ khóa API. Dùng runtime Node.js hoặc máy chủ tương thích khác hỗ trợ phản hồi truyền phát.
