Aller au contenu principal

Premier Appel et Streaming en TypeScript

Mis à jour le 29 juillet 2026

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 : en mode strict, TypeScript considère ces champs comme potentiellement absents et vous force à écrire un accès sûr. Vous récupérez donc undefined plutôt qu’une exception si la réponse arrive vide.

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 (content) {
      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 propre à la V2 du SDK. Et certains chunks arrivent sans contenu — d’où le test if (content) avant d’écrire, sans lequel votre sortie serait parsemée de undefined.

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 : les trois en-têtes ouvrent 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 = req.query.q as string;

  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 (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 reponse = response.choices?.[0]?.message?.content ?? '';
  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();

Observez 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é, un maxTokens calibré sur la longueur attendue et un topP légèrement resserré donnent des réponses plus prévisibles :

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,
  topP: 0.95,
});

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 son message, faute de quoi TypeScript refusera de compiler.

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 Error) {
    console.error(`Erreur API Mistral : ${error.message}`);
  }
}

Points clés à retenir

  • client.chat.complete() est asynchrone — utilisez await
  • Le streaming utilise for await...of sur le résultat de client.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[]