Vercel AI SDK i useChat: streaming bez klucza API

Działający czat na Vercel AI SDK 7 w Next.js: jak useChat streamuje odpowiedź, jak wyglądają status i części wiadomości, na modelu-atrapie bez klucza API.

Czas czytania: 9 min czytaniaOpublikowano:

Żeby użyć Vercel AI SDK w aplikacji Next.js, wyrenderuj useChat z @ai-sdk/react w Client Component, skieruj jego DefaultChatTransport na route handler, a w nim zwróć wynik streamText przez createUIMessageStreamResponse({ stream: toUIMessageStream(...) }). useChat daje wtedy messages, których tekst przychodzi w message.parts, oraz status, który przechodzi przez submitted, streaming i ready. Nasze demo robi dokładnie to na AI SDK 7 (ai 7.0.152, @ai-sdk/react 4.0.156) i Next.js 16.4.3, z modelem-atrapą w miejscu dostawcy: bez klucza API, bez płatnych wywołań, a osiem testów w Playwright na buildzie produkcyjnym sprawdza, co robi demo.

Wpisz wiadomość i naciśnij Send: status przechodzi przez submitted, streaming i ready, a odpowiedź rośnie słowo po słowie. Linia parts pokazuje, z czego składa się wiadomość. Przeładowanie strony czyści czat. · Next.js 16.4.3Otwórz demoZobacz kod

Czym jest Vercel AI SDK: AI SDK Core i AI SDK UI

Wprowadzenie do AI SDK (otwiera się w nowej karcie) dzieli zestaw na dwie części. AI SDK Core to „ujednolicone API do generowania tekstu, obiektów, wywołań narzędzi i budowania agentów z LLM”: działa na serwerze, a streamText to jego wywołanie ze streamingiem. AI SDK UI to „zestaw niezależnych od frameworka hooków do budowania czatów i interfejsów generatywnych”: do czatu służy useChat, który działa w przeglądarce. Aplikacja z czatem używa obu, połączonych strumieniem HTTP, a demo ma po jednym pliku na każdą stronę i osobny na model-atrapę.

Działa w W demiePakiet
AI SDK UIprzeglądarce, w Client ComponentuseChat w chat.tsx@ai-sdk/react 4.0.156
AI SDK Corena serwerze, w route handlerzestreamText w api/chat/route.tsai 7.0.152
Modeltam, gdzie dostawcaMockLanguageModelV4 w canned-model.tsai/test

useChat to hook Reacta, więc plik, który go wywołuje, zaczyna się od "use client", a wszystko, co importuje, trafia do przeglądarki: nasze demo o tym, co trafia do przeglądarki z Server i Client Components, pokazuje, gdzie przebiega ta granica. Route handler zostaje na serwerze, czyli tam, gdzie powinien być klucz API dostawcy.

Co daje useChat: status, wiadomości i części

Przewodnik po czatach (otwiera się w nowej karcie) wymienia cztery wartości status: submitted, gdy wiadomość wysłano, a strumień odpowiedzi jeszcze się nie zaczął, streaming, gdy przychodzą kolejne fragmenty, ready, gdy cała odpowiedź dotarła i można wysłać następną wiadomość, oraz error, gdy żądanie się nie powiodło. messages to rozmowa, a każda wiadomość ma parts zamiast jednego stringa content, więc tekst, wywołania narzędzi i inne typy części przychodzą na jednej liście. Wysyła się przez sendMessage({ text }). Pole tekstowe należy do ciebie: useChat już nim nie zarządza.

Cały interfejs to komponent kliencki dema:

app/demos/vercel-ai-sdk-usechat-streaming/chat.tsx
"use client"; import { useChat } from "@ai-sdk/react";import { DefaultChatTransport } from "ai";import { useState } from "react"; import styles from "./chat.module.css"; export function Chat() {  const { messages, sendMessage, status } = useChat({    // The default endpoint is /api/chat; this demo's route sits next to its page.    transport: new DefaultChatTransport({ api: "/demos/vercel-ai-sdk-usechat-streaming/api/chat" }),  });  const [input, setInput] = useState("");  // Busy only while a reply is on its way, so an "error" status does not lock the input.  const busy = status === "submitted" || status === "streaming";   return (    <section className={styles.chat} aria-label="Chat">      <p className={styles.status}>        status <output aria-label="Status">{status}</output>      </p>       <ol className={styles.messages} aria-label="Messages">        {messages.map((message) => (          <li key={message.id} className={styles.message} data-role={message.role}>            <span className={styles.role}>{message.role === "user" ? "You" : "Assistant"}</span>            <p>              {message.parts.map((part, index) =>                part.type === "text" ? (                  <span key={index} data-part="text">                    {part.text}                  </span>                ) : null,              )}            </p>            {message.role === "assistant" && (              <code className={styles.parts}>parts: {message.parts.map((part) => part.type).join(", ")}</code>            )}          </li>        ))}      </ol>       <form        className={styles.form}        onSubmit={(event) => {          event.preventDefault();          if (!input.trim()) return;          sendMessage({ text: input });          setInput("");        }}      >        <input          aria-label="Message"          value={input}          onChange={(event) => setInput(event.target.value)}          disabled={busy}          placeholder="Say something"        />        <button type="submit" disabled={busy || !input.trim()}>          Send        </button>      </form>    </section>  );}
Z labu, commit 4861ddc

Trzy szczegóły warto skopiować:

  • Pole tekstowe to zwykły stan Reacta. Formularz czyści je po wywołaniu sendMessage, a useChat od razu dodaje wiadomość użytkownika do messages: test widzi ją na liście tuż po kliknięciu, gdy status nadal pokazuje submitted albo streaming.
  • Tekst pochodzi z części. Odpowiedź powstaje z części o type === "text". Demo wypisuje pod odpowiedzią typ każdej części, a test odczytuje parts: step-start, text: część step-start oznacza początek kroku modelu, a tekst leży w jednej części text, która rośnie z każdym fragmentem.
  • Zajęty znaczy submitted albo streaming. Przykład z dokumentacji blokuje pole, gdy status !== 'ready', czyli również przy error. Demo blokuje je tylko wtedy, gdy odpowiedź jest w drodze. Test sprawdza, że pole jest zablokowane w trakcie streamingu, znów działa przy ready i że druga wiadomość przechodzi.

Jak useChat streamuje odpowiedź

useChat wysyła całą rozmowę do trasy jako POST po HTTP i czyta odpowiedź jako strumień. DefaultChatTransport domyślnie wysyła na /api/chat; trasa dema leży obok jego strony, więc podaje api. Trasa wzoruje się na trasie z dokumentacji, z atrapą zamiast identyfikatora modelu i bez instructions oraz maxDuration:

app/demos/vercel-ai-sdk-usechat-streaming/api/chat/route.ts
import { convertToModelMessages, createUIMessageStreamResponse, streamText, toUIMessageStream, type UIMessage } from "ai"; import { cannedModel } from "../../canned-model"; // useChat posts the whole conversation here and reads the reply as a UI message stream.export async function POST(req: Request) {  const { messages }: { messages: UIMessage[] } = await req.json();   const result = streamText({    model: cannedModel(), // a real provider's model goes here; nothing else in this file changes    messages: await convertToModelMessages(messages),  });   return createUIMessageStreamResponse({    stream: toUIMessageStream({ stream: result.stream }),  });}
Z labu, commit 4861ddc

convertToModelMessages zamienia wiadomości UI (z ich częściami) na to, co przyjmuje model. streamText wywołuje model i streamuje to, co zwraca. toUIMessageStream zamienia strumień modelu na fragmenty wiadomości UI, a createUIMessageStreamResponse wysyła je jako odpowiedź HTTP. Te dwa bezstanowe helpery to sposób odpowiadania w AI SDK 7: przewodnik migracji do 7.0 (otwiera się w nowej karcie) oznacza jako przestarzałe result.toUIMessageStreamResponse(), które pokazuje większość tutoriali. W v7 nadal działa, z ostrzeżeniem, i zniknie w następnej wersji głównej.

Route handlery w Next.js (otwiera się w nowej karcie) nie są cache’owane, poza handlerami GET, które się do tego włączy, więc trasa POST taka jak ta działa przy każdym żądaniu.

Co płynie w strumieniu

Strumień wiadomości UI to Server-Sent Events: jedna linia data: z JSON-em na fragment. Strona o protokole strumienia (otwiera się w nowej karcie) wymienia wszystkie typy fragmentów i mówi, że własny backend musi wysłać nagłówek x-vercel-ai-ui-message-stream: v1. Test dema wysyła jedną wiadomość prosto do trasy i czyta, co wraca:

Zdarzenie, po koleiCo oznacza (protokół strumienia)W odpowiedzi dema
startzaczyna się nowa wiadomośćraz
start-stepzaczyna się jedno wywołanie modeluraz, staje się częścią step-start
text-startzaczyna się blok tekstu, z idraz
text-deltakolejny tekst dla tego bloku, w delta17 razy, po jednym na słowo
text-endblok tekstu jest kompletnyraz
finish-step, finishkoniec wywołania modelu, potem wiadomościpo razie
[DONE]koniec strumieniana końcu

Odpowiedź przychodzi jako text/event-stream z nagłówkiem v1, a siedemnaście złączonych delt daje całą odpowiedź. W przeglądarce test zapisuje każdą wersję tekstu odpowiedzi, jaką pokazuje strona: jest ich co najmniej pięć, każda wyrasta z poprzedniej, więc tekst przychodzi w kilku fragmentach, a nie naraz. W tej samej wymianie status pokazuje kolejno ready, submitted, streaming, ready.

Domyślnie useChat publikuje aktualizację wiadomości po każdym przetworzonym fragmencie. Dla długich, szybkich odpowiedzi przewodnik po czatach proponuje throttle (na przykład throttle: 50, w milisekundach), żeby renderować rzadziej; demo go nie używa, żeby było widać każde słowo.

Jak uruchomić to bez klucza API: model-atrapa

Model to jedyna część dema, która nie jest kodem produkcyjnym:

app/demos/vercel-ai-sdk-usechat-streaming/canned-model.ts
import { simulateReadableStream } from "ai";import { MockLanguageModelV4 } from "ai/test"; const reply = "This reply is canned: a mock model streams it word by word, as a real provider would."; // A stand-in for a provider's model: no key, no network, no cost. It answers every message with// the same reply, one word per chunk, slow enough to watch. A new one per request, because a mock// keeps every call it receives (doStreamCalls), which a test wants and a server does not.export function cannedModel() {  return new MockLanguageModelV4({    doStream: async () => ({      stream: simulateReadableStream({        initialDelayInMs: 600, // the wait for the first token        chunkDelayInMs: 150, // the gap between tokens        chunks: [          { type: "text-start", id: "text-1" },          ...reply.split(/(?<= )/).map((delta) => ({ type: "text-delta" as const, id: "text-1", delta })),          { type: "text-end", id: "text-1" },          {            type: "finish",            finishReason: { unified: "stop", raw: undefined },            usage: {              inputTokens: { total: 0, noCache: 0, cacheRead: undefined, cacheWrite: undefined },              outputTokens: { total: 17, text: 17, reasoning: undefined },            },          },        ],      }),    }),  });}
Z labu, commit 4861ddc

MockLanguageModelV4 pochodzi z ai/test, czyli z narzędzi do testów (otwiera się w nowej karcie) AI SDK. Jego doStream zwraca to samo, co model dostawcy: text-start, fragmenty text-delta, text-end i finish ze zużyciem tokenów. simulateReadableStream (otwiera się w nowej karcie) wysyła je z opóźnieniami, które domyślnie wynoszą 0. Demo czeka 600 ms przed pierwszym słowem, w zastępstwie czasu do pierwszego tokena, i 150 ms między słowami, żeby dało się to obejrzeć. simulateReadableStream importuj z ai, tak jak robi to strona o testach.

Inny test sprawdza, że nic nie wychodzi na zewnątrz: każde żądanie strony podczas wysyłania idzie do jej własnego originu, a jedyny POST trafia do trasy dema.

Dlaczego route handler, a nie DirectChatTransport

AI SDK ma jeszcze dwa sposoby na ominięcie sieci. DirectChatTransport (otwiera się w nowej karcie) rozmawia z agentem „w tym samym procesie, bez HTTP” i nie umie wznowić strumienia. Strona o testach pokazuje też trasę, która zwraca ręcznie napisane stringi SSE przez simulateReadableStream. Oba sposoby testują interfejs, ale żaden nie uruchamia trasy, którą wdrożysz. Z atrapą za streamText wszystko poza modelem jest kodem, który trafi na produkcję, a test może sprawdzić, co przechodzi przez sieć.

Jak dodać czat na Vercel AI SDK do aplikacji Next.js

  1. Zainstaluj dwa pakiety

    Uruchom pnpm add ai @ai-sdk/react. Według przewodnika migracji do 7.0 AI SDK 7 wymaga Node.js 22 lub nowszego i działa tylko jako ESM.

  2. Napisz route handler

    Utwórz app/api/chat/route.ts z POST, który czyta messages, zamienia je przez convertToModelMessages, wywołuje streamText i zwraca createUIMessageStreamResponse({ stream: toUIMessageStream({ stream: result.stream }) }). Prompt systemowy podaj w instructions: AI SDK 7 zmieniło tę nazwę z system.

  3. Zacznij od modelu-atrapy

    Jako model przekaż MockLanguageModelV4 z ai/test, z fragmentami z simulateReadableStream. Interfejs da się zbudować i przetestować, zanim ktokolwiek dostanie klucz API.

  4. Zbuduj czat w Client Component

    W pliku z "use client" wywołaj useChat z DefaultChatTransport (ustaw api, jeśli trasa nie leży pod /api/chat), trzymaj pole tekstowe w useState, przy wysłaniu wywołuj sendMessage({ text }) i renderuj części wiadomości o type === "text".

  5. Steruj interfejsem przez status

    Blokuj formularz, gdy status to submitted albo streaming, pokaż spinner przy submitted i daj przycisk Stop wywołujący stop(), gdy odpowiedź jest streamowana.

  6. Przetestuj na buildzie produkcyjnym

    Uruchom next build i next start, a potem sprawdź w Playwright to, co widzi użytkownik: kolejność statusów, tekst rosnący w kilku krokach i żądania idące tylko do twojej trasy.

  7. Podmień model na prawdziwy

    Zastąp atrapę modelem dostawcy w trasie, trzymaj klucz w zmiennej środowiskowej na serwerze i ustaw maxDuration w trasie, żeby długie odpowiedzi nie były ucinane. Po stronie klienta nic się nie zmienia.

Co psuje czat na useChat

Tutoriale pisane pod AI SDK 4

Wiele tutoriali o Vercel AI SDK nadal używa input i handleSubmit z ai/react. Przewodnik migracji do 5.0 (otwiera się w nowej karcie) usunął to API, a AI SDK 7 znów zmieniło stronę serwerową:

W starszym tutorialuW AI SDK 7
import { useChat } from 'ai/react'import { useChat } from '@ai-sdk/react'
input, handleInputChangewłasny useState
handleSubmitobsługa wysłania formularza, która wywołuje sendMessage({ text })
append(message)sendMessage(message)
useChat({ api: '/api/chat' })useChat({ transport: new DefaultChatTransport({ api }) })
message.contentmessage.parts
isLoadingstatus
result.toUIMessageStreamResponse()createUIMessageStreamResponse({ stream: toUIMessageStream({ stream: result.stream }) })
system: '…'instructions: '…'
result.fullStreamresult.stream

Trzy ostatnie nadal działają w v7 jako przestarzałe aliasy. Pierwsze siedem nie działa.

Przykład streamingu w dokumentacji Next.js

Na dzień 2 listopada 2026 przykład streamingu w dokumentacji route.js w Next.js (otwiera się w nowej karcie) nadal importuje StreamingTextResponse i wywołuje result.toAIStream(). Przewodnik migracji do AI SDK 4.0 (otwiera się w nowej karcie) usunął oba: „The StreamingTextResponse function has been removed”, a toAIStream jest na jego liście metod usuniętych z wyniku streamText. Kształt trasy bierz z przewodnika po czatach AI SDK.

Trasa obok strony

route.ts nie może leżeć w tym samym segmencie trasy co page.tsx, jak mówi przewodnik po route handlerach. Daj jej własny folder (app/api/chat/route.ts albo api/chat/ pod stroną, jak w demie) i powiedz transportowi, gdzie jest.

Zablokowane pole przy błędzie

Przy disabled={status !== 'ready'} nieudane żądanie zostawia formularz zablokowany, bo error to nie ready. Blokuj tylko przy submitted i streaming, a błąd pokaż z przyciskiem Retry, który wywołuje regenerate(), jak w przewodniku po czatach.

Długie odpowiedzi na hostingu serverless

Trasa z przewodnika po czatach ustawia maxDuration = 30, żeby pozwolić na streaming odpowiedzi do 30 sekund. maxDuration (otwiera się w nowej karcie) to opcja segmentu w Next.js, w sekundach, którą platformy hostingowe mogą odczytać z wyniku builda, żeby ustawić limit czasu funkcji. Ustaw ją tak, żeby starczyła na najdłuższą odpowiedź, w granicach twojego planu.

Na jakich wersjach to sprawdziliśmy

AI SDK 7 to obecna wersja główna, a jej pakiety szybko się zmieniają: wersje w labie są tydzień za najnowszymi. Daty poniżej to czasy publikacji z wpisów w rejestrze npm dla pakietu ai (otwiera się w nowej karcie) i pakietu @ai-sdk/react (otwiera się w nowej karcie), odczytane 2 listopada 2026.

PakietPierwsze wydanie 7.x / 4.xW labieNajnowsze 2 listopada 2026
ai7.0.0, 25 czerwca 20267.0.152, 23 października 20267.0.159, 30 października 2026
@ai-sdk/react4.0.0, 25 czerwca 20264.0.156, 23 października 20264.0.163, 30 października 2026

Na 7.0.159 i 4.0.163 jeszcze dema nie uruchamialiśmy. Gdy lab się zaktualizuje, te same osiem testów przejdzie ponownie, a ten tekst zmieni się, jeśli któryś nie przejdzie.

Co to jest Vercel AI SDK?

Otwartoźródłowy zestaw narzędzi w TypeScripcie od Vercela do budowania funkcji AI. AI SDK Core działa na serwerze i wywołuje modele (streamText, generateText), a AI SDK UI to hooki takie jak useChat, które trzymają stan czatu w przeglądarce i czytają streamowaną odpowiedź.

Czy Vercel AI SDK jest darmowy?

Pakiet ai jest open source na licencji Apache-2.0, a jego instalacja nic nie kosztuje. Za wywołania API płaci się dostawcy modelu, którego podłączysz; z modelem-atrapą, jak w naszym demie, nie ma żadnych opłat.

Dlaczego useChat nie ma handleSubmit ani input?

Usunęło je AI SDK 5. useChat nie zarządza już polem tekstowym: trzymaj jego wartość we własnym useState i wywołuj sendMessage({ text }) w obsłudze wysłania formularza. append zamieniło się w sendMessage, opcja api przeszła do transportu, message.content do message.parts, a isLoading do status.

Czym różni się useChat od useCompletion?

useChat trzyma listę wiadomości złożonych z części i przez całą wymianę podaje status. useCompletion obsługuje jedno uzupełnienie tekstu: zwraca pojedynczy string completion i sam zarządza polem input, więc pasuje do jednorazowego promptu, a nie do rozmowy.

Czy Vercel AI SDK działa bez hostingu na Vercelu?

Tak. Trasa to zwykły route handler Next.js, a useChat to hook Reacta, nic w nich nie zależy od platformy Vercela. Testy naszego dema działają na buildzie produkcyjnym uruchomionym przez next start na lokalnym komputerze.

Dalsza lektura

AI SDK: Chatbot (otwiera się w nowej karcie)useChat od początku do końca: wartości status, stop i regenerate, throttle, callbacki i opcje żądania.ai-sdk.dev
AI SDK: Stream protocols (otwiera się w nowej karcie)Każdy typ fragmentu strumienia wiadomości UI, nagłówek v1 i alternatywa w czystym tekście.ai-sdk.dev
AI SDK: Testing (otwiera się w nowej karcie)MockLanguageModelV4 i inne narzędzia z ai/test, z przykładami dla streamText i strumienia UI.ai-sdk.dev
AI SDK: simulateReadableStream (otwiera się w nowej karcie)Dokumentacja: fragmenty, opóźnienie początkowe i między fragmentami oraz ich wartości domyślne.ai-sdk.dev
AI SDK: Migrate to 7.0 (otwiera się w nowej karcie)Node.js 22, tylko ESM, instructions, bezstanowe helpery strumienia i wszystkie inne zmiany nazw.ai-sdk.dev
AI SDK: Migrate to 5.0 (otwiera się w nowej karcie)Gdzie w useChat podziały się input, handleSubmit, append, content i isLoading.ai-sdk.dev
Next.js: Route Handlers (otwiera się w nowej karcie)Gdzie może leżeć route.ts, które metody są cache’owane i Cache Components dla GET.nextjs.org

Napisz, co budujemy.

Odpowiadamy w ciągu jednego dnia roboczego.