對於需要多個模型供應商的 Next.js 聊天機器人,最有用的後端是集中管理驗證,並允許應用在每次請求中選擇模型的架構。CometAPI 提供了這種模式:伺服器只需保管一組 API 金鑰與 OpenAI 相容的基底 URL https://api.cometapi.com/v1,而請求的 model 欄位則用於選擇可用的 GPT、Claude、Gemini 或其他聊天模型。
本教學將建立一個可運作的 App Router 專案,包含伺服器端模型允許清單、可串流的 Route Handler、瀏覽器端模型選擇器、受保護的環境變數、完整的聊天介面與部署指引。CometAPI 金鑰永不會出現在瀏覽器。
什麼是多模型 AI 聊天應用?
多模型 AI 聊天架構在使用者介面與多個模型供應商之間放置一個由應用程式控制的後端。在此專案中,瀏覽器將對話送至 /api/chat,伺服器則將 CometAPI 憑證保密、驗證所請求的模型 ID,並透過一個相容的 API 轉發請求。
在 Next.js 中模型切換如何運作?
切換模型是一項路由決策。瀏覽器將經核准的模型 ID 與對話一起送至 /api/chat。Route Handler 會加入 CometAPI 憑證、呼叫 POST /v1/chat/completions,並把所選模型的回應以串流方式回傳到瀏覽器。
共用端點不會讓所有模型變得完全一致。輸出風格、工具行為、支援的參數、上下文限制與定價都可能不同。請在伺服器端維護模型 ID 的允許清單,並在開放給使用者前,使用相同的應用提示語對每條路由進行測試。
你將建立什麼
完成的應用具有一個 Next.js 後端與三條可選聊天路由:
| 模型 ID | 範例定位 | CometAPI 價格 |
|---|---|---|
| gemini-3.7-flash | 注重成本的高量聊天 | $0.60 輸入 / $3.00 輸出 |
| claude-opus-5 | 高階推理,聚焦精準 | $4 輸入 / $20 輸出 |
| gpt-5.6 | gpt-5.6 是 CometAPI 對 GPT-5.6 的通用路由,目前對應到 Sol 階層。 | $3.2 輸入 / $16 輸出(短上下文階層) |
定價說明:下述價格以每百萬代幣的美元計價,查核日期為 2026 年 8 月 21 日,並可能變動。上線前請務必在各模型頁面核對最新價格。此表為路由範例,並非品質排名。GPT-5.6 在超過 272,000 個代幣時會使用更高的價目等級。上述日期所用之費率可見連結的 Gemini 3.7 Flash、Claude Opus 5 與 GPT-5.6 模型頁面。
CometAPI 以各供應商的官方 API 作為透明對照來定價。具有統一官方定價的模型——如 OpenAI、Claude、Gemini 等——按代幣計費,費率為官方價格的 0.8:1(八折);沒有官方 API 的模型(MidJourney、Kling、Luma)則按次計費,依 CometAPI 設定費率,同樣提供八折折扣。請參見 CometAPI 定價指南 了解加價公式、各模型費率與計費單位。
這三條路由代表不同的取捨。Gemini 3.7 Flash 是 Google 高效率的代理型主力,支援多模態輸入與 1,048,576 代幣的上下文視窗,適合高量聊天、程式與知識工作流程。Claude Opus 5 是 Anthropic 的前沿推理模型,擅長多步分析、程式與謹慎的長文寫作,但價格較高。GPT-5.6 是 OpenAI 的通用模型,在寬上下文中平衡推理、工具使用與撰寫,適合日常生產流量。
開始之前
你需要 Node.js、npm、一個 CometAPI 帳號,以及從 CometAPI 快速開始 取得的伺服器端 API 金鑰。上述三個精確的模型 ID 在 2026 年 8 月 21 日時於即時型錄中可用,未標示為即將推出,並且提供 POST /v1/chat/completions。
本教學使用以下共用設定:
- API 金鑰:
COMETAPI_API_KEY - 基底 URL:
https://api.cometapi.com/v1 - 目錄端點:
GEThttps://api.cometapi.com/api/models
步驟 1:建立 Next.js 應用程式
npx create-next-app@latest multi-model-chat --ts --app --eslintcd multi-model-chatnpm run dev
此版本不需要模型供應商的 SDK。伺服器使用內建的 fetch API,並轉發上游的 Server-Sent Events 串流。
步驟 2:將 CometAPI 金鑰留在伺服器
在專案根目錄建立 .env.local:
COMETAPI_API_KEY=replace_with_your_cometapi_keyCOMETAPI_BASE_URL=https://api.cometapi.com/v1
不要以 NEXT_PUBLIC_ 作為金鑰的前綴。Next.js 只會將帶此前綴的變數暴露到瀏覽器端組件;API 金鑰應留在伺服器端 Route Handler。
步驟 3:定義模型政策
將允許的 ID 放在後端,而非只在下拉選單。使用者可以繞過瀏覽器控制直接呼叫你的路由,因此伺服器必須拒絕未知的模型值。
const ALLOWED_MODELS = [ "gemini-3.7-flash", "claude-opus-5", "gpt-5.6",] as const;type ModelId = (typeof ALLOWED_MODELS)[number];
用戶端會使用相同的三個 ID 作為選擇器,但伺服器端才是最終準則。
步驟 4:透過 Route Handler 串流 CometAPI
建立 app/api/chat/route.ts。此路由會驗證僅含文字的聊天負載、以 stream: true 呼叫 CometAPI,並在不暴露憑證的情況下回傳上游事件串流。
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", }, });}
此處理器會將瀏覽器的中斷訊號向上傳遞,因此關閉請求即可停止不必要的生成。它也會向用戶端回傳已淨化的錯誤,同時將診斷細節保留在伺服器日誌中。
步驟 5:加入模型選擇與串流解析
在 app/page.tsx 建立用戶端元件。它會把所選模型與對話一起送出、解析每一個 data: 事件,並將逐步到達的 delta.content 文字附加至最後一則助理訊息。
瀏覽器永遠不會直接呼叫 CometAPI。它唯一的目的地是你自己的 /api/chat 路由,該路由會保護金鑰並強制執行允許清單。
核心專案程式碼
上面的 Route Handler 即是完整的 app/api/chat/route.ts 檔案。再加入以下頁面、版面配置與樣式表,即可完成可執行的專案。
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. */
步驟 6:執行並部署應用
npm run dev
開啟 http://localhost:3000,選擇一個模型並送出訊息。針對生產環境的 Node.js 部署,請將 COMETAPI_API_KEY 與 COMETAPI_BASE_URL 加入主機的伺服器端環境變數設定,然後執行:
npm run buildnpm run start
請使用支援串流回應的託管環境。靜態匯出無法執行 /api/chat Route Handler。
測試串流路由
在開發伺服器運作時,直接呼叫你的後端:
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."} ] }'
成功的請求會回傳 Server-Sent Events。確切的 ID 與文字會有所不同,但串流形態如下:
data: {"choices":[{"delta":{"content":"Model"}}]}data: {"choices":[{"delta":{"content":" routing"}}]}data: [DONE]
瀏覽器的解析器會讀取每個 SSE 事件、擷取 choices[0].delta.content,並在資料塊到達時把串流文字附加到助理訊息。
常見整合錯誤
| 症狀 | 原因 | 修正 |
|---|---|---|
| 401 驗證錯誤 | 缺少或無效的伺服器端金鑰 | 設定 COMETAPI_API_KEY;不要以 NEXT_PUBLIC_ 暴露到前端。 |
| 404 或路由錯誤 | 基底 URL 缺少 /v1 | 使用 https://api.cometapi.com/v1。 |
| 400 不支援的模型 | ID 不在後端允許清單中 | 使用精確的即時文字模型 ID,並同步更新兩邊的選擇器。 |
| 回答一次性全部出現 | 主機或代理緩衝了事件串流 | 停用回應轉換,並使用支援串流的 Node.js 部署環境。 |
為生產環境準備聊天後端
- 驗證你的自有使用者。不要暴露可為匿名流量花費點數的公開路由。
- 針對使用者與 IP 進行速率限制。限制並發串流、每分鐘請求數、訊息數量與訊息長度。
- 將模型允許清單維持在伺服器端。瀏覽器選單只是便利性,非安全邊界。
- 在部署時驗證即時型錄。查詢
GEThttps://api.cometapi.com/api/models,若設定的 ID 標示為即將推出、不可用,或缺少 chat-completions 端點,則使發佈失敗。 - 依路由追蹤成本。記錄所選模型、request ID、延遲、代幣用量與使用者 ID;在 CometAPI 控制台設定金鑰配額或支出上限(視情況而定)。
- 處理斷線與逾時。保留
request.signal,設定應用層逾時,並在用戶端離開時停止工作。 - 不要用回退掩蓋設定錯誤。對 400 與 401 直接回報。僅對有限且可重試的失敗使用其他模型,且僅在請求結構相容時;請參見 CometAPI 模型回退指南 以建立雙層回退鏈(CometAPI 主模型 → CometAPI 回退模型 → 官方供應商)。
- 編輯日誌。避免在生產錯誤日誌中留下 API 金鑰、完整提示與敏感的模型輸出。
單一 Next.js 後端,對應多個模型選擇
模型切換應屬於你的伺服器政策,而非分散在不同供應商帳號中。Next.js 的 Route Handler 能保護一組 CometAPI 金鑰、接受每個請求附帶的核准模型 ID,並透過單一 OpenAI 相容端點串流所選模型。前端可保持簡潔,而後端保有對存取、驗證、可觀測性與成本的控制。相同帳號與金鑰也能使用 CometAPI 的原生影像與影片 API——例如用於圖像生成的 Flux 與用於影片生成的 Kling——因此此聊天後端可拓展至多模態工作流程,而無需第二套整合。
用於模型探索的 CometAPI 公開模型目錄,以及自動化路由驗證的 GET https://api.cometapi.com/api/models。
常見問題
我可以在同一個 Next.js 應用中使用多個 AI 模型嗎?
可以。保留單一伺服器端路由,並在每次請求中傳入允許清單中的模型 ID。瀏覽器 UI 可提供模型選擇,伺服器端則控制可接受的 ID。
我如何在 GPT、Claude 與 Gemini 之間切換?
在請求本文中傳送所選的模型 ID。伺服器會針對允許清單驗證之後,透過相容端點將相同的聊天負載轉發至所選模型。
我應該把 CometAPI API 金鑰存在哪裡?
將它存於伺服器端的環境變數,例如 COMETAPI_API_KEY。切勿透過 NEXT_PUBLIC_ 變數或用戶端程式碼暴露。
OpenAI 相容的 API 是否讓所有模型可互換?
否。雖然請求結構可移植,但模型在支援參數、上下文限制、工具行為、輸出風格、延遲與價格等方面均可能不同。請用生產提示語對每個允許的模型進行測試。
Next.js 的 Route Handler 能串流 AI 回應嗎?
可以。Route Handler 可在前端與任何前置代理不會緩衝回應的情況下,以 text/event-stream 內容型別回傳上游的 Server-Sent Events 串流。
我能將此應用部署為靜態的 Next.js 網站嗎?
不能。靜態匯出無法執行伺服器端的 /api/chat Route Handler,也無法保護 API 金鑰。請使用支援串流回應的 Node.js 或其他相容伺服器執行環境。
