Vercel AI SDK to otwartoźródłowa biblioteka TypeScript do budowy aplikacji AI w Next.js, React, Svelte i Vue, ujednolicająca API różnych dostawców modeli (OpenAI, Anthropic, Google, Mistral) pod jednym interfejsem. to biblioteka TypeScript, która zdejmuje z developera większość żmudnej roboty przy dodawaniu AI do aplikacji w Next.js: ręczne zarządzanie strumieniem bajtów, parsowanie chunków, doklejanie historii rozmowy do kolejnych zapytań.
Pakiet dostarcza dwie warstwy narzędzi. Po stronie klienta to hooki jak useChat i useCompletion, które obsługują stan i aktualizacje UI. Po stronie serwera to streamText i generateText, które komunikują się z modelem i odsyłają wynik w ustandaryzowanym formacie. Biblioteka jest w pełni agnostyczna — bez zmiany architektury podepniesz OpenAI, Anthropic, Google, Mistral albo lokalne modele przez Ollamę.
Jeśli chcesz zbudować chatbota lub zautomatyzować generowanie treści w Next.js, schemat jest zawsze ten sam: useChat na froncie, streamText na zapleczu, tani model jak gpt-4o-mini na start i kilka prostych zabezpieczeń przed niekontrolowanymi kosztami. Działający prototyp zajmuje około 30 minut.
Instalacja
Instalujesz jedną paczkę rdzeniową i adapter dla wybranego dostawcy modelu:
Code
npm install ai @ai-sdk/openai @ai-sdk/react# Wolisz mądrości od Claude'a zamiast OpenAI? Proszę bardzo:npm install ai @ai-sdk/anthropic @ai-sdk/react
Chatbot w dwóch plikach
Route Handler — backend
Code
// Twój plik w app/api/chat/route.tsimport { openai } from '@ai-sdk/openai'import { convertToModelMessages, streamText, type UIMessage } from 'ai'export const maxDuration = 30 // Trzymamy lejce: ucinamy watek, gdy rozmowa potrwa powyżej 30 sekund!export async function POST(req: Request) { // Wyciągamy na stół nasze wpisane wiadomości const { messages }: { messages: UIMessage[] } = await req.json() // Wskazujemy konkretny model i zachowanie const result = streamText({ model: openai('gpt-4o'), system: 'Jesteś przemiłym i pomocnym asystentem. Nie lej wody, odpowiadaj tylko w języku polskim bardzo konkretnie i na temat.', messages: await convertToModelMessages(messages), }) // Odpowiadamy ustandaryzowanym strumieniem z pakietu return result.toUIMessageStreamResponse()}
Client Component — frontend
Code
// Twój plik w app/chat/page.tsx'use client'import { useState } from 'react'import { useChat } from '@ai-sdk/react'export default function ChatPage() { const [input, setInput] = useState('') // Cała magia SDK mieści się w wyciągnięciu tych parametrów z hooka const { messages, sendMessage, status } = useChat() const isLoading = status === 'submitted' || status === 'streaming' function handleSubmit(event: React.FormEvent<HTMLFormElement>) { event.preventDefault() if (!input.trim()) return // Wysyłamy i od razu czyścimy okienko sendMessage({ text: input }) setInput('') } return ( <div className="mx-auto max-w-2xl p-4"> <h1 className="mb-6 text-2xl font-bold">Pogawędka z AI</h1> <div className="mb-6 space-y-4"> {/* Tu "wypluwamy" rozmowę pętlą z gotowej, pilnowanej przez SDK tablicy */} {messages.map((m) => ( <div key={m.id} className={`rounded-lg p-4 ${ m.role === 'user' ? 'ml-12 bg-blue-100' : 'mr-12 bg-gray-100' }`} > <p className="mb-1 text-sm font-medium"> {m.role === 'user' ? 'Ty' : 'Sztuczna Inteligencja'} </p> {m.parts.map((part, index) => part.type === 'text' ? ( <p key={`${m.id}-${index}`} className="whitespace-pre-wrap"> {part.text} </p> ) : null, )} </div> ))} {isLoading && ( <div className="mr-12 animate-pulse rounded-lg bg-gray-100 p-4"> <p className="text-sm text-gray-500"> Robot mocno myśli nad odpowiedzią... </p> </div> )} </div> <form onSubmit={handleSubmit} className="flex gap-2"> <input value={input} onChange={(event) => setInput(event.target.value)} placeholder="Śmiało, zapytaj o coś..." className="flex-1 rounded-lg border px-4 py-3" disabled={isLoading} /> <button type="submit" disabled={isLoading} className="rounded-lg bg-blue-600 px-6 py-3 text-white disabled:opacity-50" > Start </button> </form> </div> )}
To cały core. SDK opiera się na ustandaryzowanym formacie wiadomości z obiektami parts w tablicy messages, a backend odsyła strumień przez toUIMessageStreamResponse().
Jak działa strumieniowanie
streamText() buduje odpowiedź token po tokenie i odsyła ją przez Server-Sent Events zamiast czekać na kompletny wynik. Po stronie klienta useChat odbiera kolejne fragmenty i aktualizuje tablicę wiadomości w locie — stąd efekt "pisania na żywo" znany z ChatGPT. Użytkownik widzi pierwszą odpowiedź niemal natychmiast, a nie po kilku sekundach oczekiwania na pełny tekst.
Zmiana dostawcy modelu — jedna linia kodu
Code
// app/api/chat/route.ts// Po prostu zmieniasz paczkę z której zaciągasz moduł i to wszystko!import { anthropic } from '@ai-sdk/anthropic'import { convertToModelMessages, streamText, type UIMessage } from 'ai'export async function POST(req: Request) { const { messages }: { messages: UIMessage[] } = await req.json() const result = streamText({ model: anthropic('claude-sonnet-4-6'), // Podpinasz wybranego Claude'a system: 'Wyobraź sobie, że jesteś inżynierem Next.js połączonym ze specjalistą od sprawdzania widoczności (SEO). Używaj języka polskiego i fachowych rad.', messages: await convertToModelMessages(messages), }) return result.toUIMessageStreamResponse()}
Reszta kodu pozostaje bez zmian — to praktyczny dowód agnostyczności biblioteki.
System prompt — zakres i charakter asystenta
Prompt systemowy określa, w jakich granicach działa model i jak ma się zachowywać. Dla asystenta biznesowego wygląda to tak:
Code
const result = streamText({ model: openai('gpt-4o'), system: `Jesteś oficjalnym asystentem na platformie internetowej StriveLab — potężnego hubu tworzącego witryny szyte na miarę na wdrożeniach Next.js.Pamiętaj o twardych zasadach z naszej firmy:- Wszystkie Twoje odpowiedzi muszą być wygenerowane w języku polskim.- Musisz pisać w punkt, wyrzucaj lanie wody do kosza.- Jeżeli wpadnie zapytanie z prośbą o wyceny — Twoim celem jest przekierować klienta grzecznie i profesjonalnie do podstrony [Automatyzacja AI](/uslugi/automatyzacja-procesow-ai/).- Jeżeli zaczną wypytywać o tajniki pracy dewelopera — wyjaśniaj im w punkt jakie moce skrywa praca w najnowszym Next.js.- Bądź jak głaz: nie dyskutuj absolutnie na tematy które nie dotykają inżynierii web lub tworzenia oprogramowania.Twoje podręczne suche fakty o StriveLab:- Nasz głowny konik (Specjalizacja): pisanie szybkiego Next.js, dopinanie logik SEO i dedykowane witryny konwersyjne (Landing Pages).- Baza główna biura: Miasto Kraków- Główna skrzynka pocztowa: kontakt@strivelab.pl`, messages,})
useCompletion — generowanie bez historii
Przydaje się tam, gdzie nie potrzebujesz kontekstu rozmowy: jednorazowe tłumaczenie, generowanie opisu produktu, szybka sugestia. Żadnej historii wiadomości — tylko prompt i wynik:
// app/api/generate/route.ts// Na zapleczu dla opcji Completion też ustawiasz to banalnie szybko!import { openai } from '@ai-sdk/openai'import { streamText } from 'ai'export async function POST(req: Request) { const { prompt } = await req.json() const result = streamText({ model: openai('gpt-4o-mini'), prompt: `Oto Twój cel: Stwórz chwytliwy i krótki (na 2 do maks 3 zdań) opis wdrożeniowy dla tekstu od użytkownika po haśle: "${prompt}". Skoncentruj się na tym by był w pełni optymalny dla czytających w google!`, }) return result.toUIMessageStreamResponse()}
generateText — Server Actions bez strumieniowania
Gdy nie potrzebujesz efektu pisania na żywo, tylko gotowego wyniku do dalszego przetworzenia — generowanie meta tagów, tłumaczenie wsadowe, podsumowanie — generateText zwraca kompletny tekst po zakończeniu wywołania:
Code
// actions/ai-actions.ts'use server'import { openai } from '@ai-sdk/openai'import { generateText } from 'ai'export async function generateMetaDescription(content: string) { // Pamiętaj, wyciągamy tylko sam suchy 'text' po zakończeniu przeliczania u modelu const { text } = await generateText({ model: openai('gpt-4o-mini'), prompt: `Napisz świetnie czytający się tekst z opisu wpisu dla SEO (maks 160 znaków). Tekst będzie tworzony z następującego wstępu do bloga: "${content.slice(0, 500)}"`, }) return text}export async function translateText(text: string, targetLang: string) { const { text: translated } = await generateText({ model: openai('gpt-4o-mini'), prompt: `Przetłumacz co do joty poniższy wsad dla rynku w języku ${targetLang}. Wyrzuć jakikolwiek wstęp z Twojej strony, zwróć do mojego narzędzia wyłącznie sam przetłumaczony dokument:\n\n${text}`, }) return translated}
Structured output — generateObject z walidacją Zod
Model języka domyślnie zwraca tekst — co jest problemem, gdy potrzebujesz danych do aplikacji. generateObject wymusza określoną strukturę JSON przez schemat Zod i gwarantuje, że wynik będzie typowany i gotowy do użycia:
Code
import { openai } from '@ai-sdk/openai'import { generateObject } from 'ai'import { z } from 'zod'// Określamy ścisły garnitur reguł, przez który model musi przejść by poprawnie zakończyć wynik:const productSchema = z.object({ title: z .string() .describe( 'Szykowny i dopracowany pod pozycjonowanie zarys nazwy w sklepie', ), description: z .string() .describe('Esencja produktu mieszcząca się w zgrabnych dwóch zdaniach'), tags: z .array(z.string()) .describe('Zestawienie około 3 do 5 kluczowych znaczników dla asortymentu'), category: z .string() .describe('Ogólne określenie segmentu z którego pochodzi ten towar'),})export async function generateProductData(rawDescription: string) { // Wrzucasz Zoda jako formę sztywnego odlewu do funkcji generateObject: const { object } = await generateObject({ model: openai('gpt-4o-mini'), schema: productSchema, prompt: `Spójrz na to rozlane słowotwórstwo: "${rawDescription}" — przeanalizuj je i zwróć poprawny obiekt dla moich maszyn pod ustandaryzowaną sprzedaż.`, }) return object // Wychodzi perfekcyjnie wylany obiekt typu JSON dla dalszej aplikacji na sklepie: { title: string, description: string, tags: string[], category: string }}
Zabezpieczenia przed nadużyciem i niekontrolowanymi kosztami
Bez ograniczeń jeden użytkownik może wygenerować rachunek, który zrujnuje budżet projektu. Trzy warstwy ochrony, które warto wdrożyć od pierwszego dnia produkcji:
Code
// app/api/chat/route.tsimport { rateLimiter } from '@/lib/rate-limit'import { auth } from '@/auth'import { convertToModelMessages, streamText, type UIMessage } from 'ai'import { openai } from '@ai-sdk/openai'export async function POST(req: Request) { // Po 1: Nikt przypadkowy z zewnątrz nie ma tu wstępu, wymuś w sesji zalogowanie w firmie const session = await auth() if (!session) { return new Response('O nie, nie! Najpierw się zaloguj.', { status: 401 }) } // Po 2: Ustalamy ostry kaganiec - limit na wciśnięcia wywołań np 20 chatów by uchronić Twoje portfele const { success } = await rateLimiter.limit(`chat:${session.user.id}`) if (!success) { return new Response( 'Poczekaj! Przeładowałeś silniki asystenta — wróc za chwilę ze swoimi pytaniami.', { status: 429 }, ) } const { messages }: { messages: UIMessage[] } = await req.json() // Po 3: Genialny trik inżyniera — bez sensu wysyłać modelowi ostatnie osiemset stron dyskusji przy każdym wywołaniu! Zostawiaj ogon najmłodszej pamięci np na 20 wymian zdań! const recentMessages = messages.slice(-20) const result = streamText({ model: openai('gpt-4o-mini'), // Mały model to małe spalanie gotówki na starcie! messages: await convertToModelMessages(recentMessages), maxOutputTokens: 1000, // Mocno narzucona dętka dla wielomównej odpowiedzi! }) return result.toUIMessageStreamResponse()}
Bezpieczne automatyzacje procesów i agenci AI w n8n, Make i Claude.
Maciej Sala — Product Manager i Frontend Developer z bogatym doświadczeniem w marketingu internetowym oraz SEO. Na co dzień pracuje z Reactem, Next.js i TypeScriptem, a ostatnio także z Astro i narzędziami do automatyzacji procesów AI. Sprawnie łączy perspektywę produktową z praktycznym podejściem do kodu. Przez kilka lat był związany z branżą gier wideo jako project manager i game designer. Absolwent historii na Uniwersytecie Jagiellońskim oraz studiów podyplomowych z marketingu internetowego na AGH w Krakowie. Po godzinach trenuje na siłowni, maluje figurki i rozwija własne projekty side-projecty.