Lompat ke konten utama
vourdev
Kembali ke Blog

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.

Gambar Cover Vercel AI SDK 4.0: Streaming Chatbot di Next.js App Router & Tool Calling
Ditulis olehvourdev12 menit baca

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().

Alur Streaming Response Vercel AI SDK dari Server ke Client Client (Browser) useChat() Hook POST /api/chat Next.js App Router streamText() API LLM Stream LLM Provider OpenAI / Anthropic Data Stream Protocol (Chunks / SSE)
Alur Streaming Response Vercel AI SDK dari Server ke Client

Proses di balik layar berjalan sebagai berikut:

  1. Browser mengirimkan array messages melalui HTTP POST ke Next.js Route Handler.
  2. Route Handler memanggil fungsi streamText() yang terhubung ke provider (misalnya @ai-sdk/openai).
  3. Stream dibuka antara Next.js dan OpenAI API via chunked HTTP transfer encoding.
  4. Method toDataStreamResponse() mengemas chunk LLM, informasi tool call, dan metadata lainnya ke dalam format Data Stream Protocol.
  5. 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:

bash
npm install ai @ai-sdk/openai zod

Pastikan Anda sudah menyimpan API Key di file .env.local:

bash
OPENAI_API_KEY=sk-proj-your-actual-api-key-here

Setup 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').

typescript
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 maxDuration memastikan stream tidak terputus di pertengahan eksekusi.
  • `openai('gpt-4o-mini')`: Menggunakan model gpt-4o-mini yang 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-8 serta header x-vercel-ai-ui-stream: v1 secara 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:

typescript
'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

  1. `messages`: Array berisi objek { id, role, content, toolInvocations }. Setiap kali chunk teks baru diterima dari server, useChat meng-update pesan terakhir secara efisien tanpa mere-render ulang seluruh list yang tidak perlu.
  2. `isLoading`: Boolean flag yang menandakan bahwa koneksi HTTP stream sedang terbuka dan token sedang di-stream dari server.
  3. `stop()`: Fungsi bawaan untuk membatalkan koneksi HTTP streaming secara instan dari sisi client (AbortController di balik layar). Ini sangat penting saat user menyadari prompt yang mereka kirimkan salah dan ingin menghentikan respons AI secara mendadak.
  4. `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:

Alur Eksekusi Tool Calling Multi-Step Vercel AI SDK 4.0 1. User Prompt 2. streamText() Menganalisis Intent 3. LLM Request Tool Call getWeather({ city: "Jakarta" }) 4. Server Execute Function Fetch API External / DB Query 5. Send Tool Result maxSteps Loop > 1 6. Stream UI Render Output Final
Alur Eksekusi Tool Calling Multi-Step Vercel AI 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.

typescript
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.

typescript
'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:

  1. Rate Limit LLM Provider: Kuota API habis atau limit TPM (Tokens Per Minute) terlampaui.
  2. Serverless Execution Timeout: Fungsi Next.js dipaksa berhenti oleh cloud provider.
  3. 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.

Mekanisme Catch & Recover Error Mid-Stream Vercel AI SDK 4.0 Stream Active Tokens streaming... Stream Interrupted Network / Rate Limit / Timeout Server onError() Log Telemetry & Audit Client reload() / retry() Preserve Stream Context Graceful Fallback UI & Resume Stream
Mekanisme Catch & Recover Error Mid-Stream Vercel AI SDK 4.0

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.

typescript
// 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:

typescript
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):

text
Cache-Control: no-cache, no-transform
X-Content-Type-Options: nosniff

3. 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:

  1. Upgrade AI SDK: Update dependensi Anda ke versi terbaru via npm install ai@latest @ai-sdk/openai@latest.
  2. Refactor Route Handler: Ganti implementasi OpenAIStream lama dengan streamText dan .toDataStreamResponse().
  3. Tambahkan Tool Calling: Coba definisikan 1 tool Zod di server (misalnya pencarian database sederhana) dan tampilkan indikator loading tool tersebut di UI.
  4. 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