AI & MCP

Streaming UI con l'AI SDK: route handler, useChat e parti

Lo streaming con l'AI SDK è un route handler che restituisce parti e un componente che le legge: il testo compare mentre arriva, la tool call come parte.

In sintesi

  • Il modello e la chiave restano nel route handler; il browser riceve solo uno stream di parti.
  • useChat espone messages e sendMessage: ogni messaggio ha un array parts ordinato.
  • Una tool call è una parte chiamata tool-, non testo da interpretare.

Lo streaming con l'AI SDK è un route handler che restituisce uno stream di parti e un componente che legge message.parts: il testo compare mentre arriva, e una chiamata a un tool compare come parte separata. Non serve gestire a mano il protocollo.

Tre file, un percorso

  • app/api/chat/route.ts: riceve i messaggi, chiama il modello, restituisce lo stream.
  • app/page.tsx: usa useChat e disegna le parti di ogni messaggio.
  • .env.local: tiene la chiave del provider, che non esce mai dal server.
import {
  streamText,
  convertToModelMessages,
  createUIMessageStreamResponse,
  toUIMessageStream,
  type UIMessage,
} from 'ai';

export async function POST(req: Request) {
  const { messages }: { messages: UIMessage[] } = await req.json();

  const result = streamText({
    model: 'anthropic/claude-sonnet-5.5',
    messages: await convertToModelMessages(messages),
  });

  return createUIMessageStreamResponse({
    stream: toUIMessageStream({ stream: result.stream }),
  });
}

Il route handler non aspetta la risposta completa: restituisce un oggetto stream e il framework lo invia al browser mentre il modello scrive. convertToModelMessages toglie dai messaggi della UI i campi che il modello non deve vedere, come identificativi e tempi.

La chiave resta sul server

Il modello si sceglie nel route handler, non nel componente. Una chiave in una variabile NEXT_PUBLIC_ finisce nel bundle e chiunque la legge dal browser. Se il modello deve cambiare per utente o per piano, la decisione si prende sul server dopo aver letto la sessione: il client manda i messaggi, non il nome del modello.

Le parti sono l'interfaccia

'use client';

import { useChat } from '@ai-sdk/react';
import { useState } from 'react';

export default function Chat() {
  const [input, setInput] = useState('');
  const { messages, sendMessage } = useChat();

  return (
    
      {messages.map((m) =>
        m.parts.map((part, i) => {
          if (part.type === 'text') return {part.text};
          if (part.type === 'tool-search_components')
            return {JSON.stringify(part, null, 2)};
          return null;
        }),
      )}
       {
          e.preventDefault();
          sendMessage({ text: input });
          setInput('');
        }}
      >
         setInput(e.target.value)} />
      
    
  );
}

message.parts è un array ordinato: contiene testo, ragionamento e tool call nello stesso ordine in cui il modello li ha prodotti. Una tool call si chiama tool- più il nome che hai dato al tool, quindi tool-search_components. Non c'è HTML da interpretare e non c'è un formato da indovinare.

Multi-step: alzare stopWhen

Con il default isStepCount(1) la generazione si ferma alla prima tool call: il risultato viene eseguito, ma il modello non lo usa per rispondere. Per un agente che cerca, legge e poi scrive serve stopWhen: isStepCount(5), con un tetto esplicito. Il numero è una scelta di prodotto: ogni passo in più costa un prompt intero.

Cosa non fa lo streaming

  • Non nasconde la latenza: il primo token arriva quando arriva; lo streaming cambia l'attesa, non il tempo.
  • Non mette in sicurezza il route handler: autenticazione, rate limit e quota restano da scrivere.
  • Non valida l'output del tool: se il server restituisce testo con istruzioni, il client lo mostra.

La limitazione

Il pacchetto ai 7.0.159 occupa 8,4 MB scompattati in 714 file, richiede Node 22 e zod come peer dependency: è una dipendenza che pesa, non un hook da due righe. La major 7 ha cambiato l'API rispetto alle versioni precedenti, quindi un tutorial vecchio produce errori di tipo invece di una UI: blocca la versione nel lockfile e leggi la guida della major che usi.

Dove continuare

Lo streaming lato server senza SDK è in streaming e Suspense in Next.js, la differenza fra route handler e server action in route handler o server action, il rendering sospeso in Suspense in React. Le parti pronte per una chat sono in 111.985 componenti e 1.556 blocchi; un esempio con licenza dichiarata a monte è chatgpt prompt input di Jahed.

Domande frequenti

Devo usare Next.js per lo streaming con l'AI SDK?

No. Serve un endpoint che restituisca uno stream e un client che lo legga; la guida ufficiale usa Next.js App Router perché è il percorso più breve. Su Vite, Express o un altro framework cambi il trasporto, non il modello di dati.

Dove metto la chiave del provider?

In una variabile d'ambiente letta solo dal server, come AI_GATEWAY_API_KEY in .env.local. Una chiave con prefisso pubblico finisce nel bundle e chiunque la può leggere dal browser.

Come mostro una tool call invece del testo?

Leggendo message.parts e trattando la parte il cui type è tool- più il nome del tool, per esempio tool-search_components. La parte porta input e risultato, quindi puoi mostrarla come JSON senza interpretare stringhe.

Perché il modello non risponde dopo la tool call?

Perché stopWhen vale isStepCount(1) per default: la generazione finisce alla prima chiamata. Alza il limite, per esempio a 5, se vuoi che il modello usi il risultato del tool per chiudere la risposta.

Lo streaming riduce i token o il costo?

No. I token sono gli stessi, cambia solo quando l'utente li vede. Il costo si riduce tagliando contesto e passi, non cambiando il modo in cui il testo arriva al browser.