import { OpenAIStream, StreamingTextResponse } from 'ai';
import OpenAI from 'openai';
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
export const runtime = 'edge';
export async function POST(req: Request) {
const { messages } = await req.json();
const response = await openai.chat.completions.create({
model: 'gpt-4',
stream: true,
messages,
});
const stream = OpenAIStream(response);
return new StreamingTextResponse(stream);
}Salutare tuturor. Am avut recent de implementat o interfață de chat cu AI pentru un proiect intern folosit de vreo 400 de operatori din call-center. La început am mers pe varianta clasică, simplă: trimit request, aștept ca LLM-ul să genereze tot textul, apoi trimit răspunsul înapoi în UI. Rezultatul? Userii au crezut că s-a blocat aplicația când au văzut un spinner timp de 12 secunde.
Soluția evidentă a fost streaming-ul. În loc să aștepți tot blocul de text, afișezi cuvintele pe măsură ce sunt generate. Utilizatorul simte că aplicația reacționează instant, chiar dacă timpul total de generare rămâne același.
De ce am renunțat la implementarea manuală de SSE
La un proiect mai vechi, am scris manual toată logica de Server-Sent Events (SSE). Am configurat headerele HTTP cu text/event-stream, am parsat chunk-urile venite de la OpenAI pe client și am gestionat starea manual. A fost un coșmar de debugging, mai ales când conexiunea se întrerupea la jumătate și trebuia să fac manual reconnect.
De data asta, am zis să testez Vercel AI SDK (ai package). Trade-off-ul e destul de clar aici. Pe de o parte, te leagă destul de mult de ecosistemul lor de librării și asumpțiile pe care le fac ei despre cum ar trebui să arate starea unui chat. Pe de altă parte, am economisit cel puțin trei zile de muncă și am scăpat de vreo 200 de linii de cod de boilerplate pe client. Dacă proiectul tău folosește Next.js App Router, integrarea e aproape instantă.
Cum arată implementarea în Route Handler
Pe backend, în Next.js, tot ce trebuie să faci este să expui un endpoint POST care returnează un stream. Folosim StreamingTextResponse din Vercel AI SDK împreună cu providerul OpenAI. Acesta se ocupă automat de setarea headerelor corecte și de transformarea stream-ului într-un format pe care clientul îl poate digera ușor.
Codul pe server devine extrem de curat, fără să mai configurezi manual encodere de text sau stream-uri Node.js native.
Partea de client: magia lui useChat
Pe client, hook-ul useChat face toată magia. Îți oferă un array de messages, starea de input, plus funcțiile de submit și change handler. Nu trebuie să-ți bați capul cu stările de loading, cu append-ul textului primit sau cu trimiterea istoricului de conversație înapoi la server — hook-ul trimite automat tot array-ul de mesaje la fiecare request pentru a păstra contextul.
Singurul minus de care m-am lovit a fost auto-scroll-ul. Când textul curge repede pe ecran, vrei ca pagina să facă scroll automat în jos. useChat nu se ocupă de asta direct, așa că a trebuit să folosesc un IntersectionObserver combinat cu un useEffect destul de fin acordat ca să nu stric experiența de citire dacă userul încearcă să facă scroll în sus în timp ce se generează răspunsul.
Probleme reale și optimizări de performanță
După ce am dat drumul în producție, am observat că pe mașinile mai vechi ale operatorilor, browserul începea să agațe când răspunsurile depășeau 1000 de cuvinte. Fiecare chunk nou primit declanșa un re-render masiv în listă. Am rezolvat asta destul de simplu: am extras componenta care randează fiecare mesaj individual în parte și am izolat-o folosind React.memo. Astfel, doar ultimul mesaj care primește stream-ul activ face re-render, restul istoric rămâne intact în DOM.
O altă chestie de reținut: dacă folosiți Vercel serverless pe free sau pro tier, aveți grijă la timeout-ul funcțiilor (10 secunde pe serverless normal). Dacă LLM-ul are o zi mai proastă și răspunde greu, funcția voastră va fi omorâtă la jumătatea stream-ului. Soluția este să rulați route handler-ul pe Edge runtime (export const runtime = 'edge').
Voi cum gestionați streaming-ul în aplicațiile voastre? Preferați să mergeți pe o soluție nativă cu SSE ca să nu depindeți de librăriile Vercel sau folosiți SDK-ul lor fără rețineri?