Premier Appel et Streaming en TypeScript
Sur cette page
Leçon corrigée le 3 novembre 2026 après compilation des exemples en mode strict avec le SDK
@mistralai/mistralai3.5.0 et TypeScript 7.0.2. Le SDK typecontentcommestring | ContentChunk[]: la conversation multi-tour et les boucles de streaming ne compilaient pas sans un testtypeof content === 'string'. L’exemple de réglage combinaittemperatureettopP, que la documentation de l’API conseille de ne pas modifier ensemble. La gestion d’erreurs utilise désormais la classeMistralErrordu SDK, et la route Express vérifie sa question avant d’appeler l’API. Sources : SDK TypeScript Mistral (README) · paramètres de chat completion.
Envoyer votre première requête
Maintenant que le SDK est installé, passons à la pratique. La méthode chat.complete() du SDK TypeScript est l’équivalent exact de celle du SDK Python : mêmes paramètres, même structure de réponse, à la convention de nommage près. Si vous avez suivi les leçons Python, vous êtes déjà en terrain connu ; seule la syntaxe asynchrone de JavaScript s’ajoute.
import 'dotenv/config';
import { Mistral } from '@mistralai/mistralai';
const client = new Mistral({
apiKey: process.env.MISTRAL_API_KEY,
});
async function main() {
const response = await client.chat.complete({
model: 'mistral-small-latest',
messages: [
{
role: 'user',
content: 'Expliquez le concept de closure en JavaScript en trois phrases.',
},
],
});
console.log(response.choices[0]?.message?.content);
}
main();
Les points d’interrogation dans response.choices[0]?.message?.content ne sont pas de la coquetterie : dans les types du SDK, message et content sont facultatifs, et choices[0] n’existe pas si la liste revient vide. L’accès sûr vous renvoie undefined plutôt qu’une exception. Retenez aussi que content n’est pas toujours une chaîne : le SDK le type string | ContentChunk[], car une réponse peut arriver en liste de fragments (texte, raisonnement thinking, références…).
Comprendre la réponse
La structure de la réponse est identique à celle du SDK Python, et c’est en la décortiquant que l’on comprend ce qu’on paie et pourquoi une réponse s’est arrêtée. L’exemple suivant extrait successivement le texte, la raison d’arrêt et le décompte de tokens ; en le tapant vous verrez l’éditeur proposer chaque champ, preuve que le typage fait son travail.
async function analyserReponse() {
const response = await client.chat.complete({
model: 'mistral-small-latest',
messages: [
{ role: 'user', content: 'Quelle est la capitale de la France ?' },
],
maxTokens: 100,
});
// Texte de la réponse
const texte = response.choices[0]?.message?.content;
// Raison d'arrêt
const finRaison = response.choices[0]?.finishReason;
// Statistiques de tokens
const tokensEntree = response.usage?.promptTokens;
const tokensSortie = response.usage?.completionTokens;
console.log(`Réponse : ${texte}`);
console.log(`Tokens : ${tokensEntree} entrée + ${tokensSortie} sortie`);
console.log(`Arrêt : ${finRaison}`);
}
Notez la casse : là où Python expose max_tokens, finish_reason et prompt_tokens, TypeScript emploie maxTokens, finishReason et promptTokens. C’est la source d’erreur numéro un quand on transpose un exemple Python à la main — et le compilateur vous la signalera immédiatement.
Streaming
Le streaming est essentiel pour les interfaces web : sans lui, l’utilisateur regarde un écran figé pendant plusieurs secondes ; avec lui, le texte apparaît mot à mot et la latence perçue s’effondre. Le SDK TypeScript expose le flux sous forme de générateur asynchrone, que l’on parcourt avec for await...of :
async function streamReponse() {
const stream = await client.chat.stream({
model: 'mistral-small-latest',
messages: [
{
role: 'user',
content: 'Décrivez les cinq principaux design patterns en JavaScript.',
},
],
});
for await (const chunk of stream) {
const content = chunk.data.choices[0]?.delta.content;
if (typeof content === 'string') {
process.stdout.write(content);
}
}
console.log();
}
Deux détails méritent votre attention. Le contenu se trouve dans chunk.data, pas directement dans chunk : c’est l’enveloppe d’événement du SDK depuis sa version 1.0. Et le test typeof content === 'string' écarte à la fois les fragments sans contenu, qui parsèmeraient votre sortie de undefined, et les contenus en liste de fragments, que process.stdout.write n’accepte pas : sans lui, le mode strict refuse de compiler.
Exposer le flux via Express
Un flux qui ne sort pas du terminal ne sert à personne. Voici comment le relayer vers un navigateur en Server-Sent Events depuis une API Express. La question est vérifiée avant tout appel, pour ne pas envoyer une requête vide à l’API. Les trois en-têtes ouvrent ensuite le canal, chaque fragment est renvoyé en JSON dans un événement data:, et un marqueur [DONE] signale au client qu’il peut fermer la connexion.
import express from 'express';
import { Mistral } from '@mistralai/mistralai';
const app = express();
const client = new Mistral({ apiKey: process.env.MISTRAL_API_KEY });
app.get('/api/chat', async (req, res) => {
const question = typeof req.query.q === 'string' ? req.query.q : '';
if (!question) {
res.status(400).json({ error: 'Paramètre q manquant' });
return;
}
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
const stream = await client.chat.stream({
model: 'mistral-small-latest',
messages: [{ role: 'user', content: question }],
});
for await (const chunk of stream) {
const content = chunk.data.choices[0]?.delta.content;
if (typeof content === 'string' && content) {
res.write(`data: ${JSON.stringify({ text: content })}\n\n`);
}
}
res.write('data: [DONE]\n\n');
res.end();
});
app.listen(3000, () => console.log('Serveur sur le port 3000'));
Conversations multi-tour
La gestion de l’historique suit le même principe qu’en Python : le modèle n’a aucune mémoire, c’est vous qui maintenez un tableau de messages et le renvoyez en entier à chaque tour. En TypeScript, déclarer un type Message explicite empêche d’y glisser un rôle inexistant ou un objet mal formé — une erreur que l’API vous renverrait autrement en 400.
import { Mistral } from '@mistralai/mistralai';
type Message = {
role: 'system' | 'user' | 'assistant';
content: string;
};
const client = new Mistral({ apiKey: process.env.MISTRAL_API_KEY });
const historique: Message[] = [
{
role: 'system',
content: 'Vous êtes un assistant expert en TypeScript. Répondez en français.',
},
];
async function poserQuestion(question: string): Promise<string> {
historique.push({ role: 'user', content: question });
const response = await client.chat.complete({
model: 'mistral-small-latest',
messages: historique,
});
const contenu = response.choices[0]?.message?.content;
const reponse = typeof contenu === 'string' ? contenu : '';
historique.push({ role: 'assistant', content: reponse });
return reponse;
}
async function main() {
console.log(await poserQuestion('Qu\'est-ce qu\'un type générique ?'));
console.log(await poserQuestion('Montrez-moi un exemple concret.'));
console.log(await poserQuestion('Comment ajouter des contraintes ?'));
}
main();
Le test typeof contenu === 'string' n’est pas un luxe : le champ content de la réponse peut être une liste de fragments, alors que votre type Message attend une chaîne. Observez ensuite la troisième question : « Comment ajouter des contraintes ? » n’a de sens que parce que les deux échanges précédents sont encore dans le tableau. Poussez l’exercice en supprimant la ligne qui ajoute la réponse de l’assistant à l’historique, et vous verrez le modèle perdre le fil dès le deuxième tour.
Régler la génération et gérer les erreurs
Les paramètres avancés sont identiques à ceux du SDK Python — seule la casse change. Une temperature basse pour un livrable structuré et un maxTokens calibré sur la longueur attendue donnent des réponses plus prévisibles. Réglez temperature ou topP, pas les deux : c’est la recommandation de la documentation de l’API, qui conseille une temperature entre 0 et 0,7.
const response = await client.chat.complete({
model: 'mistral-large-latest',
messages: [
{ role: 'user', content: 'Proposez une architecture pour un SaaS B2B.' },
],
temperature: 0.5,
maxTokens: 1000,
});
Enfin, tout appel réseau peut échouer : clé invalide, quota dépassé, coupure. Encadrez systématiquement l’appel d’un try/catch et vérifiez le type de l’erreur avant de lire ses propriétés, faute de quoi TypeScript refusera de compiler. Les erreurs HTTP renvoyées par l’API héritent toutes de MistralError, qui expose statusCode et body ; les erreurs réseau (ConnectionError, RequestTimeoutError…) n’en héritent pas, mais restent des Error.
import * as errors from '@mistralai/mistralai/models/errors';
try {
const response = await client.chat.complete({
model: 'mistral-small-latest',
messages: [{ role: 'user', content: 'Bonjour !' }],
});
console.log(response.choices[0]?.message?.content);
} catch (error) {
if (error instanceof errors.MistralError) {
// Réponse d'erreur de l'API : 401 clé refusée, 429 limite atteinte…
console.error(`Erreur API Mistral ${error.statusCode} : ${error.message}`);
} else if (error instanceof Error) {
// Pas de réponse de l'API : réseau coupé, délai dépassé…
console.error(`Erreur : ${error.message}`);
}
}
Points clés à retenir
client.chat.complete()est asynchrone — utilisezawait- Le streaming utilise
for await...ofsur le résultat declient.chat.stream() - Le typage TypeScript détecte les erreurs de paramètres à la compilation
- L’intégration SSE avec Express est directe pour les interfaces web temps réel
- L’historique de conversation se gère avec un tableau typé
Message[] contentvaut une chaîne ou une liste de fragments : testeztypeof content === 'string'avant de l’utiliser comme texte- Les erreurs HTTP du SDK héritent de
MistralError(statusCode,body)