Tutoriel

Statut éditorial : En attente de relecture

Streamer une réponse IA dans Next.js avec le Vercel AI SDK

Créer une route App Router qui diffuse la réponse du modèle et une interface useChat qui gère envoi, affichage et erreurs.

Classification du contenu

Types

  • Modèles et API
  • Outils
Niveau
Intermédiaire
Publié le
18 août 2026
Dernière relecture
Relecture en attente
Prochaine vérification
18 novembre 2026

Le streaming affiche la réponse à mesure qu’elle arrive et réduit le temps d’attente perçu. L’appel au fournisseur reste dans une route serveur afin de protéger la clé et d’appliquer les limites.

Installer

~~~bash npm install ai @ai-sdk/openai ~~~

Configurez OPENAI_API_KEY et un modèle actif dans les variables serveur. Ne préfixez jamais la clé avec NEXT_PUBLIC_.

Créer la route

~~~typescript // app/api/chat/route.ts import { openai } from '@ai-sdk/openai'; import { streamText, convertToModelMessages, type UIMessage } from 'ai';

export const maxDuration = 30;

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

if (!Array.isArray(messages) || messages.length > 40) { return Response.json({ error: 'Conversation invalide' }, { status: 422 }); }

const result = streamText({ model: openai(process.env.OPENAI_MODEL!), system: 'Réponds clairement en français.', messages: await convertToModelMessages(messages), maxOutputTokens: 600, });

return result.toUIMessageStreamResponse(); } ~~~

Les noms précis des helpers dépendent de la version majeure du SDK. Verrouillez les dépendances après validation et suivez la documentation de cette version.

Créer l’interface

~~~tsx 'use client';

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

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

return ( <section aria-labelledby="chat-title"> <h1 id="chat-title">Assistant</h1> <div aria-live="polite"> {messages.map(message => ( <article key={message.id}> <strong>{message.role === 'user' ? 'Vous' : 'Assistant'}</strong> {message.parts.map((part, index) => part.type === 'text' ? <p key={index}>{part.text}</p> : null )} </article> ))} </div> <form onSubmit={event => { event.preventDefault(); if (!input.trim()) return; sendMessage({ text: input }); setInput(''); }}> <label htmlFor="message">Message</label> <textarea id="message" value={input} onChange={e => setInput(e.target.value)} /> <button disabled={status !== 'ready'}>Envoyer</button> {status === 'streaming' && <button type="button" onClick={stop}>Arrêter</button>} </form> {error && <p role="alert">La réponse n’a pas pu être générée.</p>} </section> ); } ~~~

Production

Authentifiez la route, limitez corps, messages, tokens et requêtes par utilisateur. Propagez l’annulation au fournisseur, filtrez les logs et affichez un état accessible. Les outils demandent une validation serveur et une confirmation pour les écritures.

FAQ

Le streaming coûte-t-il moins cher ?

Non. Il change la livraison, pas le nombre de tokens générés.

Pourquoi la réponse s’interrompt-elle sur l’hébergeur ?

Vérifiez maxDuration, limites de fonction, proxy et que tous les intermédiaires acceptent le streaming.

Sources utilisées