Tutorial
Vercel AI SDK 4.0: Streaming Chatbot di Next.js App Router & Tool Calling
Panduan lengkap setup streaming chatbot dengan Vercel AI SDK 4.0 di Next.js App Router, implementasi tool calling multi-step, dan error handling mid-stream.

Banyak developer membuat aplikasi berbasis Large Language Model (LLM) dengan pendekatan synchronous HTTP REST API lama: kirim request ke endpoint OpenAI, tunggu 8 detik sampai seluruh teks selesai dibuat server, lalu tampilkan respons sekaligus di UI.
Hasilnya? User experience terasa lambat, patah, dan membuat pengguna menganggap aplikasi hang.
Streaming response adalah standar wajib untuk interface generative AI modern. Ketika LLM menghasilkan token demi token, UI langsung merendernya secara real-time. Di ekosistem React dan Next.js App Router, Vercel AI SDK 4.0 hadir sebagai standar baku untuk menangani mekanisme streaming, state management chat, hingga eksekusi tool calling (function calling) tanpa pusing mengurus infrastruktur Server-Sent Events (SSE) manual.
Di artikel ini, kita akan membongkar arsitektur Vercel AI SDK 4.0 dari nol. Kita akan membahas konfigurasi Route Handler di Next.js App Router, mengonsumsi data stream di client dengan hook useChat, mengeksekusi tool calling multi-step, serta menangani skenario buruk: error yang terjadi di tengah-tengah pemrosesan stream.
Arsitektur Streaming: Dari Server ke UI
Sebelum menulis kode, pahami bagaimana data bergerak dari LLM provider (seperti OpenAI, Anthropic, atau Ollama) menuju komponen UI browser Anda.
Vercel AI SDK 4.0 memperkenalkan Abstraksi Unified Provider API dan Data Stream Protocol. Dulu di versi 3.x, kita mengandalkan helper seperti OpenAIStream dan StreamingTextResponse. Di versi 4.0, arsitekturnya dibuat jauh lebih fleksibel dengan fungsi utama streamText dan helper response .toDataStreamResponse().
Proses di balik layar berjalan sebagai berikut:
- Browser mengirimkan array
messagesmelalui HTTP POST ke Next.js Route Handler. - Route Handler memanggil fungsi
streamText()yang terhubung ke provider (misalnya@ai-sdk/openai). - Stream dibuka antara Next.js dan OpenAI API via chunked HTTP transfer encoding.
- Method
toDataStreamResponse()mengemas chunk LLM, informasi tool call, dan metadata lainnya ke dalam format Data Stream Protocol. - Hook
useChat()di browser secara otomatis mendekomposisi chunk tersebut dan meng-update React State secara real-time.
Mari kita persiapkan environment proyek Next.js. Jalankan perintah instalasi paket berikut di terminal:
npm install ai @ai-sdk/openai zodPastikan Anda sudah menyimpan API Key di file .env.local:
OPENAI_API_KEY=sk-proj-your-actual-api-key-hereSetup Route Handler: Stream Text & Model Provider
Langkah awal di sisi server adalah membuat API endpoint yang menerima pesan dari pengguna dan mengembalikan aliran respons stream.
Buat file baru di app/api/chat/route.ts. Di Vercel AI SDK 4.0, kita menggunakan method streamText yang dipadukan dengan provider adapter seperti openai('gpt-4o-mini').
import { openai } from '@ai-sdk/openai';
import { streamText } from 'ai';
// Mengatur durasi maksimum eksekusi fungsi (khusus platform Serverless Vercel)
export const maxDuration = 30;
export async function POST(req: Request) {
try {
// 1. Parse JSON body yang dikirim oleh hook useChat dari client
const { messages } = await req.json();
// 2. Inisialisasi pemanggilan model dengan streamText
const result = streamText({
model: openai('gpt-4o-mini'),
system: 'Anda adalah Senior Tech Strategist di Vour Studio. Jawab pertanyaan teknis dengan praktikal, lugas, dan berbobot.',
messages,
temperature: 0.7,
});
// 3. Kembalikan respons dalam Data Stream Protocol resmi Vercel AI SDK
return result.toDataStreamResponse();
} catch (error) {
console.error('Error pada Route Handler Chat:', error);
return new Response(
JSON.stringify({ error: 'Gagal memproses permintaan AI stream' }),
{ status: 500, headers: { 'Content-Type': 'application/json' } }
);
}
}Bedah Kode Baris demi Baris
- `export const maxDuration = 30`: Pada runtime serverless seperti Vercel, Route Handler standar memiliki batas waktu timeout default 10-15 detik. Mengingat LLM streaming dapat memakan waktu lama saat menghasilkan jawaban panjang, menetapkan
maxDurationmemastikan stream tidak terputus di pertengahan eksekusi. - `openai('gpt-4o-mini')`: Menggunakan model
gpt-4o-miniyang hemat biaya dan sangat cepat untuk kebutuhan respons teks streaming dasar. - `system`: Prompt instruksi peran (system prompt) yang memaksa LLM menjaga persona dan format jawaban sesuai kebutuhan aplikasi Anda.
- `result.toDataStreamResponse()`: Method inti di AI SDK 4.0. Fungsi ini menggantikan format SSE manual dan mengatur header
Content-Type: text/plain; charset=utf-8serta headerx-vercel-ai-ui-stream: v1secara otomatis.
Client-Side Integration dengan Hook useChat
Setelah server endpoint siap mengalirkan data, sekarang kita buat antarmuka pengguna (UI) di sisi client. Hook useChat dari ai/react mengabstraksi seluruh pengelolaan state pesan, pembacaan stream reader, penanganan tombol submit, hingga status loading.
Buat komponen halaman baru pada app/chat/page.tsx:
'use client';
import { useChat } from 'ai/react';
export default function ChatPage() {
const {
messages,
input,
handleInputChange,
handleSubmit,
isLoading,
stop,
error,
reload,
} = useChat();
return (
<main className="flex flex-col w-full max-w-2xl py-12 mx-auto px-4 min-h-screen">
<h1 className="text-2xl font-bold mb-6 text-slate-100">
Vour Studio AI Assistant
</h1>
{/* Container Pesan Chat */}
<div className="flex-1 overflow-y-auto space-y-4 mb-24">
{messages.length === 0 && (
<p className="text-slate-500 italic">
Belum ada percakapan. Ketik pesan di bawah untuk memulai streaming.
</p>
)}
{messages.map((m) => (
<div
key={m.id}
className={`p-4 rounded-lg border ${
m.role === 'user'
? 'bg-slate-800 border-slate-700 text-slate-100 ml-12'
: 'bg-slate-900 border-cyan-900/50 text-slate-200 mr-12'
}`}
>
<span className="text-xs font-mono font-semibold block mb-1 text-cyan-400">
{m.role === 'user' ? 'USER' : 'AI ASSISTANT'}
</span>
<div className="whitespace-pre-wrap leading-relaxed text-sm">
{m.content}
</div>
</div>
))}
{/* Notifikasi Error jika Stream Terputus */}
{error && (
<div className="p-4 bg-red-950/80 border border-red-800 rounded-lg text-red-200 text-sm">
<p className="font-semibold">Terjadi kesalahan pada alur stream.</p>
<p className="text-xs mt-1 text-red-300">{error.message}</p>
<button
type="button"
onClick={() => reload()}
className="mt-3 px-3 py-1.5 bg-red-800 hover:bg-red-700 text-white rounded text-xs transition"
>
Coba Ulang (Reload)
</button>
</div>
)}
</div>
{/* Form Input Chat */}
<form
onSubmit={handleSubmit}
className="fixed bottom-6 left-1/2 -translate-x-1/2 w-full max-w-2xl px-4"
>
<div className="flex items-center gap-2 p-2 bg-slate-900 border border-slate-700 rounded-xl shadow-2xl">
<input
className="flex-1 bg-transparent px-3 py-2 text-sm text-slate-100 focus:outline-none placeholder:text-slate-500"
value={input}
placeholder="Tanyakan topik arsitektur perangkat lunak..."
onChange={handleInputChange}
/>
{isLoading ? (
<button
type="button"
onClick={stop}
className="px-4 py-2 bg-rose-600 hover:bg-rose-500 text-white font-medium text-xs rounded-lg transition"
>
Hentikan
</button>
) : (
<button
type="submit"
disabled={!input.trim()}
className="px-4 py-2 bg-cyan-600 hover:bg-cyan-500 disabled:bg-slate-800 text-white font-medium text-xs rounded-lg transition"
>
Kirim
</button>
)}
</div>
</form>
</main>
);
}Memahami State Utama dari useChat
- `messages`: Array berisi objek
{ id, role, content, toolInvocations }. Setiap kali chunk teks baru diterima dari server,useChatmeng-update pesan terakhir secara efisien tanpa mere-render ulang seluruh list yang tidak perlu. - `isLoading`: Boolean flag yang menandakan bahwa koneksi HTTP stream sedang terbuka dan token sedang di-stream dari server.
- `stop()`: Fungsi bawaan untuk membatalkan koneksi HTTP streaming secara instan dari sisi client (
AbortControllerdi balik layar). Ini sangat penting saat user menyadari prompt yang mereka kirimkan salah dan ingin menghentikan respons AI secara mendadak. - `reload()`: Memicu pemanggilan balik (retry) request terakhir ke server jika stream mengalami kegagalan di tengah jalan.
Implementasi Tool Calling: Kasus Nyata Weather & Database Query
Salah satu fitur paling powerful dari Vercel AI SDK 4.0 adalah dukungan native terhadap Tool Calling (Function Calling) yang terintegrasi langsung dengan eksekusi server dan pembaruan UI.
Ketika pengguna meminta informasi yang memerlukan data eksternal (misalnya cek stok barang atau data cuaca live), LLM dapat memutuskan untuk memanggil fungsi JavaScript/TypeScript yang didefinisikan di server, menerima hasilnya, dan melanjutkan streaming teks jawaban berdasarkan hasil eksekusi tersebut.
Berikut adalah arsitektur multi-step tool loop di SDK 4.0:
Mari buat Route Handler khusus tool calling di app/api/chat-tools/route.ts. Kita memvalidasi skema parameter input menggunakan library Zod.
import { openai } from '@ai-sdk/openai';
import { streamText, tool } from 'ai';
import { z } from 'zod';
export const maxDuration = 30;
export async function POST(req: Request) {
const { messages } = await req.json();
const result = streamText({
model: openai('gpt-4o'),
messages,
// maxSteps memungkinkan LLM memanggil tool, menerima hasil, dan memanggil tool lain lagi dalam 1 request
maxSteps: 5,
tools: {
getWeather: tool({
description: 'Mendapatkan data cuaca terkini untuk lokasi kota tertentu',
parameters: z.object({
city: z.string().describe('Nama kota yang akan dicek cuacanya'),
}),
execute: async ({ city }) => {
// Simulasi panggilan API Cuaca Eksternal (misal: OpenWeatherMap)
if (city.toLowerCase() === 'jakarta') {
return { city: 'Jakarta', tempC: 31, condition: 'Hujan Tropis', humidity: '82%' };
}
return { city, tempC: 25, condition: 'Cerah Berawan', humidity: '60%' };
},
}),
queryProductStock: tool({
description: 'Memeriksa jumlah stok barang gudang berdasarkan Kode SKU',
parameters: z.object({
sku: z.string().describe('Kode unik barang/SKU, contoh: LAPTOP-MAC-01'),
}),
execute: async ({ sku }) => {
// Simulasi query database PostgreSQL / Redis
const mockDb: Record<string, number> = {
'LAPTOP-MAC-01': 14,
'KEYBOARD-MECH-02': 0,
};
const stock = mockDb[sku.toUpperCase()] ?? 0;
return { sku: sku.toUpperCase(), stock, available: stock > 0 };
},
}),
},
});
return result.toDataStreamResponse();
}Mengonsumsi State Tool Call di Client UI
Sekarang buat halaman interface baru app/chat-tools/page.tsx. Di sini kita mendeteksi properti m.toolInvocations dari objek message untuk mere-render status eksekusi fungsi secara visual.
'use client';
import { useChat } from 'ai/react';
export default function ToolChatPage() {
const { messages, input, handleInputChange, handleSubmit, isLoading } = useChat({
api: '/api/chat-tools',
});
return (
<main className="flex flex-col w-full max-w-2xl py-12 mx-auto px-4 min-h-screen">
<h1 className="text-2xl font-bold mb-6 text-slate-100">
AI Tool Calling Assistant
</h1>
<div className="flex-1 overflow-y-auto space-y-4 mb-24">
{messages.map((m) => (
<div key={m.id} className="p-4 rounded-lg bg-slate-900 border border-slate-800">
<span className="text-xs font-mono font-semibold text-cyan-400 block mb-2">
{m.role.toUpperCase()}
</span>
{/* Teks biasa dari AI / User */}
{m.content && <p className="text-slate-200 text-sm whitespace-pre-wrap">{m.content}</p>}
{/* Render Komponen UI Khusus untuk Tool Invocations */}
{m.toolInvocations?.map((toolInvocation) => {
const { toolCallId, toolName, args } = toolInvocation;
const isCompleted = 'result' in toolInvocation;
return (
<div
key={toolCallId}
className="mt-3 p-3 bg-slate-950 border border-slate-700/60 rounded-md text-xs font-mono"
>
<div className="flex items-center justify-between text-slate-400 mb-1">
<span className="text-cyan-300 font-bold">🛠️ Function Call: {toolName}</span>
<span>{isCompleted ? '✅ Selesai' : '⏳ Mengeksekusi...'}</span>
</div>
<p className="text-slate-500">Argumen: {JSON.stringify(args)}</p>
{isCompleted && (
<div className="mt-2 pt-2 border-t border-slate-800 text-emerald-400">
<span className="text-slate-400 block mb-0.5">Hasil Server:</span>
<pre className="overflow-x-auto text-[11px] bg-slate-900 p-2 rounded">
{JSON.stringify(toolInvocation.result, null, 2)}
</pre>
</div>
)}
</div>
);
})}
</div>
))}
</div>
<form onSubmit={handleSubmit} className="fixed bottom-6 left-1/2 -translate-x-1/2 w-full max-w-2xl px-4">
<div className="flex items-center gap-2 p-2 bg-slate-900 border border-slate-700 rounded-xl">
<input
className="flex-1 bg-transparent px-3 py-2 text-sm text-slate-100 focus:outline-none"
value={input}
placeholder="Coba: 'Berapa cuaca di Jakarta dan stok LAPTOP-MAC-01?'"
onChange={handleInputChange}
/>
<button
type="submit"
disabled={isLoading || !input.trim()}
className="px-4 py-2 bg-cyan-600 hover:bg-cyan-500 disabled:bg-slate-800 text-white text-xs font-medium rounded-lg"
>
Kirim Request
</button>
</div>
</form>
</main>
);
}Tips Penting (`maxSteps`): Tanpa atribut maxSteps: 5 di server, pemanggilan tool hanya akan mengeksekusi fungsi di server tanpa memicu looping balik ke LLM untuk merangkum hasil eksekusi tersebut menjadi kalimat bahasa alami.Handling Error Mid-Stream dan Resilience
Salah satu tantangan terbesar aplikasi streaming berbasis serverless adalah kestabilan koneksi jaringan. Stream bisa terputus di tengah jalan karena beberapa faktor:
- Rate Limit LLM Provider: Kuota API habis atau limit TPM (Tokens Per Minute) terlampaui.
- Serverless Execution Timeout: Fungsi Next.js dipaksa berhenti oleh cloud provider.
- Network Drops: Koneksi internet seluler pengguna terputus saat stream baru berjalan setengah.
Di Vercel AI SDK 4.0, kita wajib menangani skenario ini di dua layer sekaligus: Server Side Error Callback dan Client Side Graceful Fallback.
Konfigurasi Robust Error Handling di Server
Tambahkan callback onError pada konfigurasi streamText. Callback ini bertindak sebagai tempat pencatatan telemetry (misalnya ke Datadog, Sentry, atau Logtail) tanpa merusak payload JSON yang dikirimkan ke client.
// app/api/chat/route.ts (Versi Resilient)
import { openai } from '@ai-sdk/openai';
import { streamText } from 'ai';
export const maxDuration = 30;
export async function POST(req: Request) {
const { messages } = await req.json();
const result = streamText({
model: openai('gpt-4o-mini'),
messages,
onError: ({ error }) => {
// Log error teknis ke konsol server atau layanan log eksternal
console.error('[STREAM_ERROR_SERVER]: Error terjadi di tengah alur stream:', error);
},
onFinish: ({ usage, text }) => {
// Log penggunaan token untuk pengawasan biaya API
console.log(`[USAGE_LOG]: Total Token: ${usage.totalTokens} | Prompt: ${usage.promptTokens} | Completion: ${usage.completionTokens}`);
},
});
return result.toDataStreamResponse({
getErrorMessage: (error) => {
// Saring pesan error aman yang boleh dibaca oleh client (menyembunyikan API key atau stack trace sensitif)
if (error instanceof Error && error.message.includes('rate limit')) {
return 'Sistem sedang sibuk karena lonjakan trafik. Silakan coba 10 detik lagi.';
}
return 'Terjadi gangguan komunikasi dengan model AI.';
},
});
}Handling Client State Recovery
Pada sisi client React, kita memanipulasi opsi callback pada hook useChat untuk menampilkan pesan kesalahan yang ramah pengguna:
const { messages, error, reload } = useChat({
onError: (err) => {
// Panggil pustaka notifikasi toast seperti react-hot-toast / sonner jika perlu
console.warn('Handling client error event:', err.message);
},
onResponse: (response) => {
if (!response.ok) {
console.error('Server mengembalikan HTTP Error Code:', response.status);
}
},
});Tips & Best Practices Produksi
Sebelum merilis chatbot streaming Anda ke lingkungan produksi, perhatikan 4 poin arsitektural berikut:
1. Pasang Middleware Rate Limiting
Karena endpoint streaming bersifat publik dan mengonsumsi kredit API secara real-time, pasang proteksi Rate Limit (misalnya menggunakan @upstash/ratelimit dan Redis) di Route Handler Next.js. Batasi maksimal 10-20 request per menit per IP address.
2. Hindari Penggunaan Standard HTTP Caching
Streaming response tidak boleh dicache oleh CDN atau Edge Network Vercel secara default. Pastikan Route Handler Anda menambahkan header berikut secara otomatis (sudah ditangani secara internal oleh toDataStreamResponse(), namun berhati-hatilah jika menambahkan custom middleware):
Cache-Control: no-cache, no-transform
X-Content-Type-Options: nosniff3. Gunakan gpt-4o-mini atau Claude 3 Haiku untuk Chat Biasa
Jangan gunakan model flagship seperti gpt-4o atau claude-3-5-sonnet untuk percakapan conversational umum tanpa tool calling. Model yang lebih kecil memiliki Time To First Token (TTFT) jauh lebih rendah (<300ms) yang memberikan impresi streaming luar biasa cepat di mata pengguna.
4. Kelola Otentikasi User
Jangan pernah membiarkan route /api/chat terbuka tanpa proteksi otentikasi sesi (seperti NextAuth.js, Clerk, atau Supabase Auth). Ambil userId dari session cookies di dalam Route Handler untuk membatasi kuota penggunaan token per pengguna harian.
Kesimpulan & Action Items
Vercel AI SDK 4.0 memangkas kompleksitas teknis integrasi LLM streaming di ekosistem Next.js App Router. Pola kerja lama yang mengharuskan kita menulis penanganan EventSource, ReadableStream reader manual, dan skema JSON parsial saat function calling kini diabstraksi secara elegan menjadi beberapa baris fungsi: streamText() di server dan useChat() di client.
Langkah Konkret yang Bisa Anda Lakukan Sekarang:
- Upgrade AI SDK: Update dependensi Anda ke versi terbaru via
npm install ai@latest @ai-sdk/openai@latest. - Refactor Route Handler: Ganti implementasi
OpenAIStreamlama denganstreamTextdan.toDataStreamResponse(). - Tambahkan Tool Calling: Coba definisikan 1 tool Zod di server (misalnya pencarian database sederhana) dan tampilkan indikator loading tool tersebut di UI.
- Uji Error Resilience: Matikan koneksi internet di tengah-tengah streaming untuk memastikan UI menangani status error dengan bersih via fungsi
reload().
Butuh penyesuaian khusus untuk project Anda?
Jika situasi operasional atau arsitektur sistem bisnis Anda membutuhkan solusi kustom, diskusikan langsung bersama tim engineer kami. Anda juga bisa melihat rincian layanan vour.dev atau menghitung estimasi biaya project lebih dulu.
Mulai Project