Qu'est-ce qu'AI SDK 5 ?
Vercel AI SDK 5 est la mise à jour la plus significative apportée à cette bibliothèque depuis son lancement. Avec plus de 30 millions de téléchargements npm hebdomadaires combinés pour le package ai principal et ses packages de fournisseurs, il constitue le standard TypeScript pour la création de produits alimentés par l'IA. La version 5 introduit une refonte architecturale fondamentale qui sépare les préoccupations de l'interface utilisateur de celles du modèle, rendant les applications IA full-stack nettement plus simples à construire, déboguer et maintenir.
Les changements phares d'AI SDK 5 :
- UIMessage vs ModelMessage — deux types de messages distincts aux responsabilités différentes
- useChat basé sur le transport — remplace la gestion HTTP interne par une couche de transport explicite et interchangeable
- Streaming SSE — les Server-Sent Events remplacent le protocole de streaming binaire propriétaire, permettant le débogage natif via les DevTools du navigateur
- Contrôle de la boucle agentique —
stopWhenetprepareStepoffrent un contrôle chirurgical sur les appels d'outils multi-étapes - Messages personnalisés type-safe — déduisez des types TypeScript complets depuis vos définitions d'outils et de schémas
- Génération vocale — la primitive
generateSpeechpermet la conversion texte-vers-audio en première classe
Prérequis
Avant de commencer, assurez-vous d'avoir :
- Node.js 18 ou supérieur installé
- Un projet Next.js 15 créé avec
npx create-next-app@latest my-ai-app --typescript --app - Une clé OpenAI API stockée sous
OPENAI_API_KEYdans.env.local - Une connaissance de base de React, TypeScript et async/await
Ce que vous allez construire
À la fin de ce tutoriel, vous disposerez d'une application de chat IA full-stack qui :
- Diffuse les réponses en temps réel depuis un modèle OpenAI
- Utilise correctement l'architecture UIMessage/ModelMessage
- Appelle un outil personnalisé et gère son résultat dans le flux
- Contrôle la boucle agentique avec
stopWhenetprepareStep - Définit une forme de message personnalisée entièrement type-safe
- Convertit n'importe quel texte en parole avec
generateSpeech
Le projet terminé fonctionne avec tout fournisseur compatible AI SDK 5 — Anthropic, Google Gemini, Mistral et plus de 20 autres — en changeant seulement deux lignes de code.
Étape 1 : Installer AI SDK 5
À la racine de votre projet Next.js, installez le package principal et le fournisseur OpenAI :
npm install ai @ai-sdk/openaiVérifiez la version installée :
npm list aiVous devriez voir ai@5.x.x dans la sortie. Si vous migrez depuis AI SDK 4, consultez le guide de migration officiel — l'API useChat et la forme des messages ont considérablement changé.
Ajoutez votre clé API dans .env.local :
OPENAI_API_KEY=sk-...Étape 2 : Comprendre UIMessage vs ModelMessage
C'est le concept le plus important d'AI SDK 5. Avant la version 5, un unique type Message remplissait deux rôles — il contenait à la fois l'état de l'interface rendu dans le navigateur et la charge utile brute envoyée au LLM. Cette conception créait de la complexité dans la sérialisation et rendait la persistance des conversations sujette aux erreurs.
AI SDK 5 sépare clairement ces deux préoccupations en deux types distincts :
| Type | Vit sur | Contient |
|---|---|---|
UIMessage | Frontière client et serveur | Etat complet du message : parties texte, résultats d'outils, métadonnées |
ModelMessage | Serveur uniquement — envoyé au LLM | Charge utile allégée optimisée pour la consommation du modèle |
Le flux de données dans chaque requête :
- Le client envoie
UIMessage[]à votre route API - Le serveur appelle
convertToModelMessages(messages)pour produireModelMessage[] ModelMessage[]est transmis au LLM viastreamText- Le flux revient sous forme de
UIMessageStreamResponse useChatcôté client met à jour l'état localUIMessagedepuis le flux
Cette séparation rend la persistance simple. Le callback onFinish fournit des UIMessage[] prêtes à être stockées sans aucune conversion manuelle.
Étape 3 : Construire la route serveur
Créez la route API dans app/api/chat/route.ts :
import { openai } from '@ai-sdk/openai';
import {
convertToModelMessages,
streamText,
UIMessage,
tool,
stepCountIs,
} from 'ai';
import { z } from 'zod';
export const maxDuration = 30;
export async function POST(req: Request) {
const { messages }: { messages: UIMessage[] } = await req.json();
const result = streamText({
model: openai('gpt-4o'),
system: "Vous êtes un assistant utile. Utilisez les outils quand c'est approprié.",
messages: convertToModelMessages(messages),
stopWhen: stepCountIs(5),
tools: {
getWeather: tool({
description: "Obtenir la météo actuelle pour une ville",
inputSchema: z.object({
city: z.string().describe("Le nom de la ville"),
}),
execute: async ({ city }) => {
// Remplacer par un vrai appel API météo en production
return { city, temperature: 22, condition: 'Ensoleillé' };
},
}),
},
onFinish: async ({ messages: finalMessages }) => {
// Persistez la conversation ici — finalMessages est UIMessage[]
console.log('Conversation terminée :', finalMessages.length, 'messages');
},
});
return result.toUIMessageStreamResponse();
}Les décisions clés dans cette route :
convertToModelMessages(messages)traduit lesUIMessage[]reçus enModelMessage[]avant l'appel au LLMstopWhen: stepCountIs(5)agit comme un plafond de sécurité — la boucle agentique s'arrête après 5 étapes maximumtoUIMessageStreamResponse()emballe le flux en SSE queuseChatcomprend nativement- Le callback
onFinishest l'endroit canonique pour persister les conversations
Étape 4 : Construire l'interface chat côté client
Créez la page dans app/chat/page.tsx :
'use client';
import { useChat } from '@ai-sdk/react';
import { useState } from 'react';
export default function ChatPage() {
const [input, setInput] = useState('');
const { messages, sendMessage, status } = useChat();
return (
<div className="flex flex-col max-w-2xl mx-auto h-screen p-4">
<div className="flex-1 overflow-y-auto space-y-4 py-4">
{messages.map(message => (
<div key={message.id} className="space-y-1">
<strong className="capitalize text-sm text-gray-500">
{message.role === 'user' ? 'Vous' : 'Assistant'}
</strong>
<div>
{message.parts.map((part, i) => {
if (part.type === 'text') {
return <p key={i} className="text-gray-800">{part.text}</p>;
}
if (part.type === 'tool-getWeather') {
return (
<div key={i} className="bg-blue-50 border border-blue-200 rounded p-3 text-sm">
<span className="font-medium">Météo à {part.result.city} :</span>{' '}
{part.result.temperature}°C, {part.result.condition}
</div>
);
}
return null;
})}
</div>
</div>
))}
{status === 'streaming' && (
<div className="text-gray-400 text-sm italic">Réflexion en cours...</div>
)}
</div>
<form
onSubmit={e => {
e.preventDefault();
sendMessage({ text: input });
setInput('');
}}
className="flex gap-2 pt-4 border-t"
>
<input
value={input}
onChange={e => setInput(e.target.value)}
placeholder="Demandez la météo dans n'importe quelle ville..."
className="flex-1 border rounded-lg p-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
disabled={status === 'streaming'}
/>
<button
type="submit"
disabled={status === 'streaming'}
className="px-4 py-2 bg-blue-600 text-white rounded-lg disabled:opacity-50 hover:bg-blue-700"
>
Envoyer
</button>
</form>
</div>
);
}La différence clé avec AI SDK 4 est la façon dont on accède au contenu des messages. Chaque UIMessage possède maintenant un tableau parts où chaque élément a un discriminant type explicite :
- Contenu texte :
{ type: 'text', text: '...' } - Résultats d'outils :
{ type: 'tool-getWeather', result: { city, temperature, condition } }
Les types de parties d'outils suivent le format tool-<toolName> pour éviter les collisions de noms entre outils.
Étape 5 : Contrôler la boucle agentique
Les primitives stopWhen et prepareStep offrent un contrôle fin sur le comportement d'un agent multi-étapes — plus besoin d'implémenter votre propre logique de boucle.
Conditions d'arrêt
stopWhen accepte une condition unique ou un tableau. La boucle s'arrête quand n'importe quelle condition est satisfaite :
import { stepCountIs, toolCalls, textIncludes } from 'ai';
const result = streamText({
model: openai('gpt-4o'),
messages: convertToModelMessages(messages),
stopWhen: [
stepCountIs(10), // limite stricte : arrêt après 10 étapes
toolCalls('submitFinalAnswer'), // arrêt quand le modèle signale la fin
textIncludes('TASK_COMPLETE'), // arrêt sur une chaîne sentinelle dans la sortie
],
tools: { /* ... */ },
});Référence complète des conditions d'arrêt intégrées :
| Condition | Se déclenche quand |
|---|---|
stepCountIs(n) | Le nombre d'étapes atteint n |
toolCalls(name) | Un outil spécifique est invoqué |
toolResults(name) | Un résultat d'un outil spécifique est reçu |
textIncludes(str) | La sortie texte contient la chaîne |
textDoesNotInclude(str) | La sortie texte ne contient plus la chaîne |
custom(() => boolean) | Votre prédicat personnalisé retourne true |
Configuration par étape avec prepareStep
prepareStep s'exécute avant chaque étape et vous permet de modifier le modèle, les outils disponibles ou l'historique des messages :
const result = streamText({
model: openai('gpt-4o'),
messages: convertToModelMessages(messages),
prepareStep: async ({ stepNumber, messages }) => {
// Forcer la première étape à appeler l'outil météo immédiatement
if (stepNumber === 0) {
return {
toolChoice: { type: 'tool', toolName: 'getWeather' },
activeTools: ['getWeather'],
};
}
// Tronquer l'historique pour les agents de longue durée
if (messages.length > 20) {
return { messages: messages.slice(-10) };
}
// Retourner un objet vide utilise les paramètres par défaut pour cette étape
return {};
},
tools: { /* ... */ },
});Un modèle courant consiste à utiliser prepareStep pour basculer vers un modèle moins coûteux lors des étapes intermédiaires et revenir à un modèle plus performant uniquement pour l'étape de synthèse finale.
Étape 6 : Définir un UIMessage personnalisé type-safe
AI SDK 5 vous permet de définir la forme TypeScript exacte de vos messages grâce à Zod et au helper InferUITools. Créez lib/ai-types.ts :
import { InferUITools, ToolSet, UIMessage, tool } from 'ai';
import { z } from 'zod';
// 1. Métadonnées attachées à chaque message
const metadataSchema = z.object({
model: z.string(),
latencyMs: z.number().optional(),
tokensUsed: z.number().optional(),
});
export type MessageMetadata = z.infer<typeof metadataSchema>;
// 2. Parties de données personnalisées envoyées en milieu de réponse
const dataPartSchema = z.object({
weatherCard: z.object({
city: z.string(),
temperature: z.number(),
condition: z.string(),
}),
});
export type MessageDataPart = z.infer<typeof dataPartSchema>;
// 3. Définitions des outils partagés — importez-les aussi dans votre route API
export const appTools: ToolSet = {
getWeather: tool({
description: "Obtenir la météo actuelle pour une ville",
inputSchema: z.object({ city: z.string() }),
execute: async ({ city }) => ({
city,
temperature: 22,
condition: 'Ensoleillé',
}),
}),
};
// 4. Déduire automatiquement les types de résultats des outils
type AppTools = InferUITools<typeof appTools>;
// 5. Votre message entièrement typé — utilisez-le partout à la place de UIMessage
export type AppUIMessage = UIMessage<MessageMetadata, MessageDataPart, AppTools>;Substituez AppUIMessage à UIMessage dans toute votre base de code :
// Route API — corps de requête typé
const { messages }: { messages: AppUIMessage[] } = await req.json();
// Composant client — hook typé
const { messages } = useChat<AppUIMessage>();
// TypeScript sait maintenant que :
// message.metadata.model → string
// message.metadata.latencyMs → number | undefined
// part.type === 'tool-getWeather' → part.result.city est une stringCela élimine toute une catégorie d'erreurs à l'exécution où les formes de résultats d'outils divergent entre le serveur et la logique de rendu côté client.
Étape 7 : Ajouter la génération vocale
AI SDK 5 élève la génération vocale au rang de primitive API de première classe. Ajoutez une route dédiée dans app/api/speech/route.ts :
import { generateSpeech } from 'ai';
import { openai } from '@ai-sdk/openai';
export async function POST(req: Request) {
const { text } = await req.json();
const { audio } = await generateSpeech({
model: openai.speech('tts-1-hd'),
text,
voice: 'nova',
});
return new Response(audio.uint8Array, {
headers: {
'Content-Type': 'audio/mpeg',
'Cache-Control': 'no-store',
},
});
}Appelez-la depuis le client pour vocaliser n'importe quel message de l'assistant :
async function speakMessage(text: string) {
const response = await fetch('/api/speech', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text }),
});
const audioBlob = await response.blob();
const audioUrl = URL.createObjectURL(audioBlob);
new Audio(audioUrl).play();
}Voix disponibles pour OpenAI TTS : alloy, echo, fable, onyx, nova, shimmer. Le modèle tts-1-hd offre une qualité audio supérieure avec une latence légèrement plus élevée par rapport à tts-1.
Tester votre implémentation
Démarrez le serveur de développement :
npm run devOuvrez http://localhost:3000/chat et envoyez le message : "Quelle est la météo à Tunis ?"
Vous devriez observer :
- Le texte de l'assistant qui se diffuse mot par mot
- L'appel à l'outil
getWeatheret son résultat apparaissant dans l'interface - Le texte de suivi de l'assistant résumant les données météo
Ouvrez les DevTools réseau de votre navigateur, filtrez par EventStream et inspectez la requête /api/chat. Vous verrez les événements SSE sous forme de texte lisible — une amélioration majeure par rapport au protocole de streaming binaire précédent.
Dépannage
"messages is not iterable" — Votre route API reçoit un corps vide. Vérifiez que le client envoie Content-Type: application/json et que la forme du corps est { messages: [...] }.
Résultat d'outil absent de l'interface — Le part.type doit correspondre exactement à tool-<toolName>. Si votre outil s'appelle get_weather avec underscore, le type de partie est tool-get_weather.
Le flux se termine après une étape — Le modèle ne génère pas d'appels d'outils. Vérifiez que votre prompt system encourage l'utilisation des outils et que le message de l'utilisateur est pertinent par rapport à la description de l'outil.
Erreurs TypeScript sur les parties de messages — Importez UIMessage et InferUITools depuis 'ai' (le package principal), pas depuis '@ai-sdk/react'. Les types fondamentaux doivent provenir du package racine.
Prochaines étapes
- Ajoutez la persistance des conversations : sauvegardez
UIMessage[]depuisonFinishdans une base de données Postgres ou SQLite via Drizzle ORM - Explorez la classe
Agentlégère pour les pipelines d'agents basés sur des nœuds non streamés - Ajoutez de l'observabilité LLM avec Langfuse pour tracer chaque appel d'outil, chaque étape et le comptage des tokens
- Remplacez OpenAI par Anthropic Claude en changeant
@ai-sdk/openaipar@ai-sdk/anthropic— le reste du code reste identique - Construisez une configuration multi-fournisseurs avec
vercel-ai-gateway-unified-ai-provider-routing-nextjs-2026pour le routage par fallback et par coût
Conclusion
Vercel AI SDK 5 rend la création d'applications IA de production en TypeScript nettement plus robuste. La séparation UIMessage/ModelMessage élimine toute une classe de bugs de sérialisation, le streaming SSE rend le débogage naturel avec les outils navigateur standards, et les primitives de boucle agentique — stopWhen et prepareStep — vous donnent le contrôle dont vous avez besoin sans écrire votre propre logique de boucle. Avec plus de 30 millions de téléchargements hebdomadaires et la prise en charge de plus de 25 fournisseurs, AI SDK 5 est la base la plus pragmatique pour le développement IA full-stack dans l'écosystème JavaScript aujourd'hui.