TanStack AI 1.0 ou Vercel AI SDK 7 : le protocole se choisit d'abord
TanStack AI est passé en 1.0 le 22 octobre. Le flux réel de chaque SDK capturé dans un essai, le poids des clients et pourquoi le protocole décide.
Lundi matin, une équipe de quatre personnes ouvre le ticket « assistant dans l'application ». Le prototype tourne depuis trois semaines sur le Vercel AI SDK. Son bouton « Approuver » dépend d'un détail précis : une partie de message nommée tool-envoyerEmail, dans l'état approval-requested. Ce nom sort du protocole de flux de Vercel. Un serveur TanStack AI n'envoie rien de tel. La question du jour est donc concrète : garder ce qu'on connaît ou regarder le SDK dont tout le monde parle depuis une semaine.
Ce SDK, c'est TanStack AI, passé en 1.0. Le paquet @tanstack/ai@1.0.0 est sorti le 22 octobre 2026, d'après la liste des versions du dépôt1 et sa note de version2. Le billet d'annonce3 est daté du 23.
Les deux SDK couvrent le même terrain. Ce qui les sépare est le protocole de flux que lisent vos composants : ce choix se défait mal, donc il se protège par une couche d'adaptation côté client. Le poids du client, lui, dépend surtout de la façon d'importer les définitions d'outils.
J'ai monté hors du dépôt un petit essai dérivé des exemples de la documentation des deux SDK, avec un modèle factice et sans appel réseau, pour compiler le code et capturer le flux réel de chacun. L'article en rend compte, puis dit comment trancher cette semaine. C'est le genre de décision qu'on prend au démarrage d'une prestation d'agents et enchaînements pilotés par des modèles de langage, avant que l'interface ne dépende du flux.
Une réserve d'entrée : l'essai est minimal, un seul cas de support, aucun appel à Anthropic. Je n'ai pas fait tourner TanStack AI en production et je n'ai trouvé aucun retour de production tiers. Le reste vient de la documentation, des annonces et du dépôt de TanStack. Je signale quand un chiffre est le leur.
Trois mots avant de commencer
- un adaptateur, c'est le module qui parle à un fournisseur de modèle. Chez TanStack il y en a un par activité et par fournisseur. Le billet d'annonce3 en compte 23 ;
- un protocole de flux, c'est le format des messages que le serveur envoie à l'interface pendant que le modèle répond : morceaux de texte, appels d'outils, demandes d'approbation. AG-UI est un protocole ouvert de ce type. Le Vercel AI SDK en a un autre, qu'il appelle UI Message Stream ;
- un outil isomorphe, c'est un outil défini une seule fois, avec ses schémas, puis rattaché à une exécution côté serveur ou côté client.
Le deuxième mot compte le plus. Adaptateurs et outils se remplacent à la marge, tandis que le protocole traverse votre code jusqu'aux composants.
La 1.0 de TanStack AI engage les API cœur et laisse le harness expérimental
La 1.0 n'ajoute aucun concept : TanStack a supprimé les API que la 0.x marquait comme dépréciées, chaque remplaçant existant déjà3. Le numéro engage les API cœur. Les phases alpha, bêta et candidate ont été publiées en 0.x, la dernière étant la 0.71.0 du 21 octobre1.
Pour qui vient de la 0.x, les changements cassants se lisent dans la note de version2 et le guide de migration4 :
threadIddevient obligatoire côté client et côté serveur,conversationIdest renomméthreadIdetbodydevientforwardedProps;addToolApprovalResponse()disparaît, l'interruption d'approbation se résout avecresolveInterrupt;- les anciens formats de fil sont supprimés, dont la ligne SSE
[DONE]: un client 1.0 exige un serveur 1.0.
Seul @tanstack/ai-harness, la couche de sessions d'agents durables publiée en 0.1.0, reste expérimental : sa documentation dit qu'il peut changer dans une version mineure5. La suite ne parle que du cœur.
Outils, route et approbation tiennent en peu de code
Le cas de l'essai tient en deux outils, une route et un hook. Tout compile en mode strict. C'est celui de l'article sur Jev : un assistant de support qui classe un ticket, avec chercherTicket exécuté côté serveur et envoyerEmail soumis à approbation, sur claude-sonnet-5-5. Chaque extrait part de la documentation officielle. Les versions : les paquets @tanstack/* en 1.0.0, ai 7.0.156, @ai-sdk/react 4.0.160 et zod 4.6.7.
L'extrait suivant définit les deux outils. .server() donne une exécution côté serveur et needsApproval: true suspend l'outil jusqu'à l'accord de l'utilisateur6 :
export const chercherTicketDef = toolDefinition({
name: 'chercherTicket',
description: 'Cherche un ticket de support par son identifiant',
inputSchema: z.object({ identifiant: z.string() }),
outputSchema: z.object({
identifiant: z.string(),
objet: z.string(),
urgence: z.enum(['urgent', 'normal', 'basse']),
}),
})
export const envoyerEmailDef = toolDefinition({
name: 'envoyerEmail',
description: 'Envoie un e-mail au client',
inputSchema: z.object({
destinataire: z.string(),
objet: z.string(),
corps: z.string(),
}),
outputSchema: z.object({ envoye: z.boolean() }),
needsApproval: true,
})
// Exécuté côté serveur, uniquement après l'approbation de l'utilisateur
export const envoyerEmail = envoyerEmailDef.server(
async ({ destinataire, objet }) => {
console.log(`Envoi de « ${objet} » à ${destinataire}`)
return { envoye: true }
},
)La route appelle chat() et renvoie le flux en SSE. L'adaptateur est un paramètre, ce qui laisse brancher un faux modèle :
import {
chat,
chatParamsFromRequest,
toServerSentEventsResponse,
} from '@tanstack/ai'
import { anthropicText } from '@tanstack/ai-anthropic'
import { chercherTicket, envoyerEmail } from './tanstack-outils'
type AdaptateurTexte = Parameters<typeof chat>[0]['adapter']
export function creerRoute(adapter: AdaptateurTexte) {
return async function POST(request: Request) {
const params = await chatParamsFromRequest(request)
const flux = chat({
adapter,
messages: params.messages,
threadId: params.threadId,
runId: params.runId,
parentRunId: params.parentRunId,
...(params.resume ? { resume: params.resume } : {}),
systemPrompts: ['Tu classes les tickets et proposes une réponse.'],
tools: [chercherTicket, envoyerEmail],
})
return toServerSentEventsResponse(flux)
}
}
export const POST = (request: Request) =>
creerRoute(anthropicText('claude-sonnet-5-5'))(request)Côté interface, le hook se branche sur la route et l'approbation arrive dans interrupts7 :
const { messages, sendMessage, interrupts, resuming } = useChat({
connection: fetchServerSentEvents('/api/chat'),
threadId: 'support-chat',
tools: [envoyerEmailDef],
})
// …
{interrupts.map((interruption) =>
interruption.kind === 'tool-approval' ? (
<div key={interruption.id}>
<p>Envoyer cet e-mail au client ?</p>
<pre>{JSON.stringify(interruption.originalArgs, null, 2)}</pre>
<button
disabled={!interruption.canResolve || resuming}
onClick={() => interruption.resolveInterrupt(true)}
>
Approuver
</button>
</div>
) : null,
)}Une validation humaine tient donc en une ligne de configuration et un bouton.
Les modèles factices viennent de la documentation : fakeText() côté TanStack8 et MockLanguageModelV4 côté Vercel9. Les deux renvoient un peu de texte, un appel à chercherTicket, puis une phrase de conclusion.
Deux exemples de Vercel demandent une correction. Le client de la page d'approbation ne compile pas tant que useChat n'est pas typé avec InferUITools10. Et streamText() s'arrête par défaut après une étape11 : ma première capture finissait sur l'outil exécuté, sans seconde réponse, jusqu'à stopWhen: isStepCount(5). ToolLoopAgent va jusqu'à vingt étapes d'après le guide agentique et chat() s'arrête après cinq tours12 : un agent qui enchaîne dix appels d'outils s'arrête au cinquième chez TanStack et va au bout avec ToolLoopAgent.
Le poids du client tient à un import. J'ai compilé le composant de chaque côté avec esbuild (bundle minifié, react et react-dom externes, plateforme navigateur). Avec le hook ci-dessus, le client TanStack pèse 679 733 octets, 154 170 en gzip, contre 201 230 et 55 329 chez Vercel. Cet écart de 2,8 fois vient de import { envoyerEmailDef }, qui fait entrer zod en entier dans le navigateur : 453 883 octets de sortie. Sans cet import, le client TanStack pèse 224 224 octets, 60 665 en gzip, soit 5 336 de plus que le client Vercel.
Retirer l'import et tools: [envoyerEmailDef] casse le typage : interrupts n'est plus reconnu comme tool-approval. La documentation passe toujours les outils à useChat et ne décrit pas d'alternative. Les types du paquet en offrent une, qui compile et donne le même bundle de 60 665 octets : un import de type seul, passé en paramètre de type.
import type { envoyerEmailDef } from './tanstack-outils'
const { messages, sendMessage, interrupts, resuming } = useChat<
readonly [typeof envoyerEmailDef]
>({
connection: fetchServerSentEvents('/api/chat'),
threadId: 'support-chat',
})Je n'ai que compilé cette variante, sans la faire tourner contre un flux réel. Le poids se joue donc dans la façon de partager la définition d'outil avec le client.
Le protocole de flux est le choix qui ne se défait pas
Les deux SDK diffusent des flux incompatibles : c'est le critère décisif, avant la liste de fonctionnalités. La page de comparaison de TanStack l'écrit : « TanStack AI streams the open AG-UI protocol. »13 Elle ajoute que le Vercel AI SDK diffuse son propre protocole et que TanStack AI ne lit ni n'écrit celui-là.
Voici le fil, pour la même réponse du modèle factice. J'ai retiré horodatages, métadonnées et compteurs de jetons, abrégé le texte et mis sur plusieurs lignes les évènements de plus de 80 caractères. Le premier extrait est le corps SSE de toServerSentEventsResponse, qui suit les évènements d'AG-UI14 :
{"type":"RUN_STARTED","runId":"run-1","threadId":"support-chat"}
{"type":"TEXT_MESSAGE_START","messageId":"run-1-text","role":"assistant"}
{"type":"TEXT_MESSAGE_CONTENT","messageId":"run-1-text","delta":"Je r"}
: …
{"type":"TEXT_MESSAGE_END","messageId":"run-1-text"}
{"type":"TOOL_CALL_START","toolCallId":"c1","toolCallName":"chercherTicket"}
{
"type": "TOOL_CALL_ARGS",
"toolCallId": "c1",
"delta": "{\"identifiant\":\"T-1042\"}"
}
{"type":"TOOL_CALL_END","toolCallId":"c1"}
{"type":"RUN_FINISHED","runId":"run-1","threadId":"support-chat"}
: TOOL_CALL_RESULT, puis la réponse finaleLe second est la réponse de flux de Vercel, décrite dans la page du protocole15 :
{"type":"start"}
{"type":"start-step"}
{"type":"text-start","id":"t1"}
{"type":"text-delta","id":"t1","delta":"Je regarde le ticket "}
: …
{"type":"text-end","id":"t1"}
{
"type": "tool-input-available",
"toolCallId": "c1",
"toolName": "chercherTicket",
"input": {
"identifiant": "T-1042"
}
}
: tool-output-available, puis la réponse finale
{"type":"finish-step"}Un composant TanStack lit des évènements qui s'ouvrent et se ferment par messageId ou toolCallId. Un composant Vercel lit des types en minuscules et reçoit l'entrée de l'outil d'un bloc. L'approbation n'a aucun point commun : Vercel envoie un évènement tool-approval-request au milieu du message, TanStack termine le tour par un RUN_FINISHED dont outcome vaut interrupt, avec reason: "tool_call" et le toolCallId concerné, comme la page des interruptions d'AG-UI le décrit16. Vercel ferme son flux par data: [DONE] et un en-tête x-vercel-ai-ui-message-stream: v1, que TanStack n'a pas.
Le flux de TanStack s'écarte de la documentation d'AG-UI sur deux points, observés avec fakeText() et non avec l'adaptateur Anthropic : deux RUN_FINISHED pour un RUN_STARTED puis un messageId réutilisé par la réponse finale. Je ne sais pas si cela vient du faux adaptateur ou de chat().
Les composants bâtis sur le protocole de Vercel, AI Elements et le hook useChat de @ai-sdk/react, lisent le second flux seulement. Si votre application affiche déjà un fil avec ces briques, passer à TanStack AI veut dire réécrire l'interface en plus de l'appel au modèle. TanStack conseille de rester sur Vercel dans ce cas13. C'est la position d'un éditeur concerné, mais le flux capturé la confirme.
Le reste de la migration est mécanique, d'après la table de correspondance de TanStack13 :
| Vercel AI SDK | TanStack AI |
|---|---|
streamText() | chat() |
generateObject() | chat({ outputSchema }) |
stopWhen: stepCountIs(n) | agentLoopStrategy: maxIterations(n) |
tool() | toolDefinition() puis .server() ou .client() |
Le bon choix dépend de l'interface que vous avez déjà
Pour une nouvelle application, TanStack AI est un bon candidat. Pour une application qui parle déjà le protocole de Vercel, il vaut mieux rester. Mon tableau mélange ce que dit TanStack, ce que dit le guide agentique de ce blog, ce que je livre et les mesures de l'essai.
| Situation | TanStack AI 1.0 | Vercel AI SDK 7 | Mastra |
|---|---|---|---|
| Nouvelle application, interface à construire | Bon candidat, protocole ouvert | Bon candidat, écosystème d'interface le plus répandu | Pas d'avis |
| Application qui parle déjà le protocole Vercel | Réécriture de l'interface | Rester | Une autre décision, côté serveur |
| Cadre complet : mémoire, RAG, évaluation | À assembler | À assembler avec des tiers | Le cas où il se justifie, voir l'article sur Mastra 1.0 |
| Poids du client dans l'essai (gzip) | 154 170 octets avec la définition d'outil, 60 665 sans | 55 329 octets | Pas mesuré |
Le guide classe la maturité du Vercel AI SDK 7 en haute et celle de Mastra en moyenne : un numéro de version ne place pas TanStack AI plus haut. Son conseil de prendre Vercel pour Next.js tient aussi, car mon essai n'a pas touché à Next.js.
TanStack annonce un client et un serveur MCP intégrés, comme le décrit l'article sur MCP et A2A. Il annonce aussi des sous-agents, que détaille l'architecture multi-agent à écriture mono-thread.
Ce que ça ne règle pas
L'essai prouve peu : un composant, deux outils et un modèle factice. Il ne dit rien d'une vraie réponse d'Anthropic, d'une reprise après coupure réseau, d'une sortie structurée en streaming, de MCP, des sous-agents, de decide(), de Next.js ni de la compatibilité d'AI Elements avec un serveur TanStack. Je n'ai pas non plus vérifié l'affirmation de TanStack sur le typage par modèle, que Vercel n'aurait que par fournisseur.
Le billet décrit une équipe de trois mainteneurs et 143 contributeurs3 : dépendre de si peu de personnes est un risque réel quand le SDK porte votre fonctionnalité centrale. Le projet, sous licence MIT, compte 3 275 étoiles sur GitHub17.
L'adoption est bien plus faible. Sur la semaine du 21 au 27 octobre, @tanstack/ai a été téléchargé 973 955 fois sur npm et le paquet ai de Vercel 34 285 429 fois, soit environ 35 fois plus, d'après npm-stat18. Ces compteurs incluent miroirs et intégration continue. La fenêtre contient la sortie de la 1.0 : seul l'ordre de grandeur compte.
Le harness reste expérimental, avec des limites de reprise que sa documentation signale5 : si votre produit en dépend, prévoyez de réécrire. Le tableau comparatif de TanStack donne environ 38 paquets de fournisseurs côté Vercel13. Il est écrit par TanStack, donc partial. Je ne l'ai pas recoupé. Je n'ai trouvé aucun benchmark de performance publié par une équipe tierce.
Isolez le protocole derrière une couche d'adaptation dès cette semaine
Quel que soit le SDK, isolez-le derrière une interface à vous, comme le recommande le playbook MVP : votre code métier appelle repondre(messages) et seul un petit module connaît chat() ou streamText(). Pour une nouvelle application, essayez ensuite TanStack AI deux jours sur votre écran le plus exigeant : outil avec approbation, sortie structurée en streaming, reprise après coupure réseau. S'il le tient sans contorsion, son protocole ouvert est un vrai argument. Pour une application qui parle déjà le protocole de Vercel, n'y touchez pas parce que la 1.0 vient de sortir : réécrire l'interface coûte presque toujours plus que ce que vous y gagnez.
Le protocole de flux reste le point qui déborde, parce qu'il atteint les composants. Limitez-le à une couche côté client : un seul fichier qui transforme les parties de message en types à vous, sur le modèle du contrat MoteurDeDecision de l'article sur Jev. Voici celle de l'essai :
import type { UIMessage as MessageTanstack } from '@tanstack/ai-react'
import type { UIMessage as MessageVercel } from 'ai'
// Les types de l'interface : les composants ne connaissent que ceux-là
type Appel = { identifiant: string; nom: string; entree: unknown }
export type PartieDeMessage =
| { type: 'texte'; texte: string }
| ({ type: 'outil'; etat: 'en-cours' | 'termine' } & Appel)
| ({ type: 'approbation' } & Appel)
export function depuisTanstack(
parties: MessageTanstack['parts'],
): PartieDeMessage[] {
return parties.flatMap((p): PartieDeMessage[] => {
if (p.type === 'text') return [{ type: 'texte', texte: p.content }]
if (p.type !== 'tool-call') return []
const appel = { identifiant: p.id, nom: p.name, entree: p.input }
if (p.approval?.needsApproval && p.approval.approved === undefined)
return [{ type: 'approbation', ...appel }]
const etat = p.output === undefined ? 'en-cours' : 'termine'
return [{ type: 'outil', etat, ...appel }]
})
}
export function depuisVercel(
parties: MessageVercel['parts'],
): PartieDeMessage[] {
return parties.flatMap((p): PartieDeMessage[] => {
if (p.type === 'text') return [{ type: 'texte', texte: p.text }]
if (!p.type.startsWith('tool-')) return []
const o = p as { toolCallId: string; state: string; input?: unknown }
const nom = p.type.slice(5)
const appel = { identifiant: o.toolCallId, nom, entree: o.input }
if (o.state === 'approval-requested')
return [{ type: 'approbation', ...appel }]
const etat = o.state === 'output-available' ? 'termine' : 'en-cours'
return [{ type: 'outil', etat, ...appel }]
})
}MessageTanstack et MessageVercel sont les UIMessage de chaque SDK. Sur des parties construites à la main, un texte suivi d'une demande d'approbation, les deux fonctions rendent la même liste. La réponse de l'utilisateur passe par resolveInterrupt d'un côté et addToolApprovalResponse de l'autre. Elle se range dans le même fichier.
Ce qu'il faut retenir
- La 1.0 de TanStack AI n'ajoute aucun concept : elle supprime les dépréciations de la 0.x et engage les API cœur, sauf le harness, qui reste expérimental. Client et serveur doivent être tous deux en 1.0.
- Le choix entre TanStack AI et le Vercel AI SDK 7 porte sur le protocole de flux : dans l'essai, l'approbation d'un outil est une interruption dans
RUN_FINISHEDd'un côté et un évènementtool-approval-requestde l'autre. - Dans l'essai, les deux clients pèsent 60 665 et 55 329 octets gzip. Importer la définition d'outil côté client fait monter TanStack à 154 170 octets, parce qu'elle embarque
zod. Un import de type seul l'évite. - Les chiffres de TanStack, tableau comparatif compris, sont ceux de TanStack. Je n'ai ni mis le SDK en production ni trouvé de retour tiers.
- Isolez le SDK derrière une interface à vous et confinez le protocole de flux à une seule couche d'adaptation.
Je prends le choix d'un SDK avec mes clients au cadrage, avant que l'interface ne s'écrive. Il se prolonge dans la construction d'un produit web IA-first.
Sources
- TanStack, liste des versions de TanStack AI, consulté le 29/10/2026.
- TanStack, note de version 1.0.0, 22 octobre 2026.
- TanStack, billet « TanStack AI 1.0 », 23 octobre 2026.
- TanStack, guide de migration, consulté le 29/10/2026.
- TanStack, harness, consulté le 29/10/2026.
- TanStack, outils, consulté le 29/10/2026.
- TanStack, approbation d'outil, consulté le 29/10/2026.
- TanStack, tests avec fakeText, consulté le 29/10/2026.
- Vercel, AI SDK, tests, consulté le 29/10/2026.
- Vercel, AI SDK, outils et approbation, consulté le 29/10/2026.
- Vercel, AI SDK, référence de streamText, consulté le 29/10/2026.
- TanStack, cycle agentique, consulté le 29/10/2026.
- TanStack, comparaison avec Vercel, consulté le 29/10/2026.
- AG-UI, évènements, consulté le 29/10/2026.
- Vercel, AI SDK, protocole de flux, consulté le 29/10/2026.
- AG-UI, interruptions, consulté le 29/10/2026.
- TanStack, dépôt GitHub, consulté le 29/10/2026.
- npm-stat, téléchargements npm de @tanstack/ai et de ai, 21 au 27 octobre 2026.