Intelligence Artificielle angularforall.com

- Mistral AI : intégrer Le Chat et Mixtral en Node.js

Mistral-Ai Node-Js Llm Function-Calling Streaming-Sse Codestral Pixtral Mistral-Embed Rag Ia-Generative Typescript Json-Mode Api Europe-Rgpd Agents-Ia
Mistral AI : intégrer Le Chat et Mixtral en Node.js

Intégrez l'API Mistral AI en Node.js : SDK officiel, function calling parallèle, JSON mode, streaming SSE, Codestral FIM et embeddings pour le RAG européen.

Écosystème Mistral : Le Chat, La Plateforme, modèles

Mistral AI est une société française fondée en 2023 par d'anciens chercheurs de Meta et DeepMind. Elle propose une gamme complète d'API LLM ainsi que des modèles open weight redéployables. Pour un développeur européen, c'est l'alternative la plus crédible à OpenAI et Anthropic, avec des datacenters en UE et une conformité RGPD native.

Trois surfaces produits cohabitent. Le Chat est l'interface conversationnelle grand public, équivalent de ChatGPT, accessible sur chat.mistral.ai. La Plateforme (console.mistral.ai) regroupe l'API, la gestion des clés, les workspaces, le fine-tuning et le batch processing. Les modèles open weight (Mixtral, Ministral, Codestral) sont téléchargeables sur Hugging Face et exécutables via vLLM, Ollama ou Together AI.

Compatibilité OpenAI : Mistral expose un endpoint https://api.mistral.ai/v1 compatible avec le SDK OpenAI. Vous pouvez tester Mistral en changeant trois lignes dans un projet existant, puis migrer vers le SDK natif quand vous en exploitez les spécificités (codestral, embeddings, fine-tuning).

Installation du SDK et client sécurisé

Le SDK officiel @mistralai/mistralai couvre Node.js, Deno et Bun. Comme pour toute API LLM, la clé ne doit jamais transiter par le navigateur : elle reste exclusivement côté serveur.

# Installation du SDK officiel Mistral
npm install @mistralai/mistralai

# Optionnel : Zod pour valider les sorties JSON
npm install zod

# Optionnel : dotenv pour charger les variables d'environnement
npm install dotenv
// lib/mistral.ts - Client singleton cote serveur
import { Mistral } from '@mistralai/mistralai';

// La cle est lue depuis process.env, jamais commitee
if (!process.env.MISTRAL_API_KEY) {
    throw new Error('MISTRAL_API_KEY manquante - voir .env.example');
}

// Client unique reutilise dans toute l'application
const mistral = new Mistral({
    apiKey: process.env.MISTRAL_API_KEY,
    // serverURL: 'https://api.mistral.ai',  // par defaut
    // timeoutMs: 30_000,                     // 30s par appel
});

export default mistral;

Premier appel : "Hello Mistral"

// hello.ts - Premiere requete pour valider l'installation
import mistral from './lib/mistral';

const response = await mistral.chat.complete({
    model: 'mistral-large-latest',
    messages: [
        { role: 'user', content: 'Donne-moi 3 raisons d\'utiliser Mistral.' }
    ],
    maxTokens: 300,        // limite de tokens en sortie
    temperature: 0.3,      // 0 = deterministe, 1 = creatif
});

console.log(response.choices[0].message.content);
console.log('Usage :', response.usage);
// { promptTokens: 18, completionTokens: 142, totalTokens: 160 }
Endpoint OpenAI-compatible : Si vous avez deja une base de code OpenAI, vous pouvez tester Mistral sans changer de SDK : new OpenAI({ apiKey: process.env.MISTRAL_API_KEY, baseURL: 'https://api.mistral.ai/v1' }). C'est ideal pour benchmark, mais migrez vers @mistralai/mistralai ensuite pour profiter du streaming, du JSON mode strict et de Codestral.

Comparatif des modèles Mistral

L'offre Mistral couvre cinq familles : généraliste (Large/Medium/Small), code (Codestral), edge (Ministral), vision (Pixtral) et open weight (Mixtral). Choisir le bon modèle est le premier levier d'optimisation des coûts.

Modèle Contexte Input ($/1M) Output ($/1M) Usage recommandé
mistral-large-latest128 k$2,00$6,00Raisonnement complexe, agents multi-tours
mistral-small-latest32 k$0,20$0,60Classification, extraction, RAG
codestral-latest32 k$0,30$0,90Génération de code et FIM IDE
pixtral-large-latest128 k$2,00$6,00Vision : OCR, captioning, charts
ministral-8b-latest128 k$0,10$0,10Edge, on-device, fort volume
open-mixtral-8x22b64 k$2,00$6,00Open weight, self-hosted
mistral-embed8 k$0,10Embeddings 1024 dim. pour RAG
Stratégie multi-modèle : Routez les requêtes simples (extraction, intent classification) vers mistral-small ou ministral-8b, et n'utilisez mistral-large que pour le raisonnement complexe. Sur une app à 100 000 requêtes/mois, ce routage réduit la facture de 70 à 85 %.

Chat completion et conversations multi-tours

L'API chat.complete suit le même contrat que les autres LLM : un tableau de messages avec les rôles system, user, assistant et tool. Le contexte est entièrement géré côté client.

// chat.ts - Appel avec system prompt et parametres fins
import mistral from './lib/mistral';

async function ask(userMessage: string): Promise<string> {
    const response = await mistral.chat.complete({
        model: 'mistral-large-latest',
        messages: [
            {
                role: 'system',
                // System prompt - definit la personnalite et les contraintes
                content: 'Tu es un expert Angular et NestJS. Reponds en francais, ' +
                         'avec des exemples de code commentes.'
            },
            { role: 'user', content: userMessage }
        ],
        maxTokens: 1500,
        temperature: 0.3,    // 0 = factuel, 0.7 = creatif, 1 = exploratoire
        topP: 1,
        // randomSeed: 42,   // reproductibilite stricte (debug)
    });

    return response.choices[0].message.content ?? '';
}

const reponse = await ask('Explique brievement Angular Signals.');
console.log(reponse);

Gestionnaire de conversation avec troncature

// conversation.ts - Garder le contexte sans exploser la fenetre
import mistral from './lib/mistral';

type Msg = { role: 'system' | 'user' | 'assistant'; content: string };

export class Conversation {
    private history: Msg[] = [];
    private readonly maxTurns = 20;

    constructor(private systemPrompt: string) {
        this.history.push({ role: 'system', content: systemPrompt });
    }

    async send(input: string): Promise<string> {
        // Ajouter le tour utilisateur
        this.history.push({ role: 'user', content: input });

        // Garder system + N derniers echanges
        if (this.history.length > this.maxTurns + 1) {
            this.history = [
                this.history[0],
                ...this.history.slice(-this.maxTurns)
            ];
        }

        const response = await mistral.chat.complete({
            model: 'mistral-large-latest',
            messages: this.history,
        });

        const reply = response.choices[0].message.content ?? '';
        this.history.push({ role: 'assistant', content: reply });
        return reply;
    }

    reset(): void {
        this.history = [{ role: 'system', content: this.systemPrompt }];
    }
}

// Usage
const conv = new Conversation('Tu es un mentor Angular bienveillant.');
await conv.send('Quelle difference entre signal() et computed() ?');
await conv.send('Donne un exemple complet de chacun.');

Streaming SSE avec Express

Le streaming réduit la latence perçue : les premiers tokens s'affichent en 300-500 ms au lieu d'attendre la fin (3-5 s). Le SDK Mistral expose une méthode chat.stream() qui retourne un itérateur asynchrone.

// server/chat-stream.ts - Endpoint Express SSE
import express from 'express';
import mistral from './lib/mistral';

const router = express.Router();

router.post('/chat/stream', async (req, res) => {
    const { message, history = [] } = req.body;

    // Headers Server-Sent Events
    res.setHeader('Content-Type', 'text/event-stream');
    res.setHeader('Cache-Control', 'no-cache, no-transform');
    res.setHeader('Connection', 'keep-alive');
    res.flushHeaders();

    try {
        // Iterateur asynchrone : chaque chunk arrive des qu'il est genere
        const stream = await mistral.chat.stream({
            model: 'mistral-large-latest',
            messages: [
                { role: 'system', content: 'Tu es un assistant utile.' },
                ...history,
                { role: 'user', content: message }
            ],
        });

        for await (const event of stream) {
            // chaque event contient delta sur choices[0]
            const delta = event.data?.choices?.[0]?.delta?.content;
            if (delta) {
                // Envoyer le token au client en JSON
                res.write(`data: ${JSON.stringify({ delta })}\n\n`);
            }
        }

        // Signaler la fin propre du stream
        res.write('data: [DONE]\n\n');
        res.end();
    } catch (err) {
        // Toujours fermer la connexion meme en cas d'erreur
        res.write(`data: ${JSON.stringify({ error: String(err) })}\n\n`);
        res.end();
    }
});

export default router;

Consommation côté Angular avec Signals

// chat.service.ts - Service Angular standalone avec Signals
import { Injectable, signal } from '@angular/core';

@Injectable({ providedIn: 'root' })
export class MistralChatService {
    readonly response = signal('');
    readonly loading = signal(false);

    async ask(message: string): Promise<void> {
        this.response.set('');
        this.loading.set(true);

        try {
            const res = await fetch('/api/chat/stream', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ message }),
            });
            if (!res.body) return;

            const reader = res.body.getReader();
            const decoder = new TextDecoder();
            let buffer = '';

            while (true) {
                const { done, value } = await reader.read();
                if (done) break;
                buffer += decoder.decode(value, { stream: true });

                // Parser ligne par ligne (SSE = \n\n entre evenements)
                const lines = buffer.split('\n');
                buffer = lines.pop() ?? '';

                for (const line of lines) {
                    if (!line.startsWith('data: ')) continue;
                    const payload = line.slice(6);
                    if (payload === '[DONE]') return;

                    try {
                        const { delta } = JSON.parse(payload);
                        // Mise a jour reactive du signal
                        if (delta) this.response.update(r => r + delta);
                    } catch { /* ignorer chunks partiels */ }
                }
            }
        } finally {
            this.loading.set(false);
        }
    }
}

Function calling et parallel tool calls

Le function calling permet au modèle d'invoquer du code applicatif (API métier, base de données, recherche web). Mistral Large et Small supportent le parallel tool calling : le modèle peut demander plusieurs appels en une seule réponse, ce qui réduit la latence.

// agent-mistral.ts - Cycle complet avec outils metiers
import mistral from './lib/mistral';

// 1. Declaration des outils accessibles au modele
const tools = [
    {
        type: 'function' as const,
        function: {
            name: 'get_user_orders',
            description: 'Recupere les 10 dernieres commandes d\'un utilisateur',
            parameters: {
                type: 'object',
                properties: {
                    userId: { type: 'string', description: 'UUID de l\'utilisateur' },
                    status: {
                        type: 'string',
                        enum: ['paid', 'pending', 'cancelled'],
                        description: 'Filtre optionnel sur le statut'
                    }
                },
                required: ['userId']
            }
        }
    },
    {
        type: 'function' as const,
        function: {
            name: 'send_email',
            description: 'Envoie un email transactionnel a l\'utilisateur',
            parameters: {
                type: 'object',
                properties: {
                    to: { type: 'string' },
                    subject: { type: 'string' },
                    body: { type: 'string' }
                },
                required: ['to', 'subject', 'body']
            }
        }
    }
];

// 2. Implementations reelles des outils
async function executeTool(name: string, args: Record<string, any>): Promise<string> {
    switch (name) {
        case 'get_user_orders': {
            // Appel a votre repository / API metier
            const orders = await ordersRepository.findByUser(args.userId, args.status);
            return JSON.stringify(orders);
        }
        case 'send_email': {
            await mailer.send(args.to, args.subject, args.body);
            return JSON.stringify({ sent: true });
        }
        default:
            return JSON.stringify({ error: `Outil inconnu : ${name}` });
    }
}

// 3. Boucle agent : repete tant que le modele appelle des outils
async function runAgent(userMessage: string): Promise<string> {
    const messages: any[] = [
        { role: 'system', content: 'Tu es un assistant client. Utilise les outils pour repondre.' },
        { role: 'user', content: userMessage }
    ];

    // Securite : limiter les iterations pour eviter les boucles infinies
    for (let step = 0; step < 6; step++) {
        const response = await mistral.chat.complete({
            model: 'mistral-large-latest',
            messages,
            tools,
            toolChoice: 'auto',           // 'auto' | 'any' | 'none'
            parallelToolCalls: true,      // appels paralleles autorises
        });

        const message = response.choices[0].message;
        messages.push(message);

        // Si pas d'appel d'outil, on a la reponse finale
        if (!message.toolCalls || message.toolCalls.length === 0) {
            return message.content ?? '';
        }

        // Executer tous les outils en parallele
        const results = await Promise.all(
            message.toolCalls.map(async (tc: any) => {
                const args = JSON.parse(tc.function.arguments);
                const output = await executeTool(tc.function.name, args);
                return {
                    role: 'tool' as const,
                    toolCallId: tc.id,
                    name: tc.function.name,
                    content: output,
                };
            })
        );

        messages.push(...results);
    }

    throw new Error('Trop d\'iterations - boucle suspecte');
}
Garde-fous d'agent : Toujours limiter le nombre d'itérations (6-10 max), logger chaque appel d'outil avec ses arguments, et n'exposer au modèle que les outils strictement nécessaires à la tâche. Un outil delete_user ne doit jamais cohabiter avec un outil search sans validation humaine.

JSON mode et sorties structurées

Pour extraire des données métier, on veut un JSON parfaitement formé. Mistral propose deux niveaux : le json_object mode (JSON valide garanti) et la validation post-parse avec Zod pour un typage strict.

// extract.ts - Extraction structuree de donnees
import mistral from './lib/mistral';
import { z } from 'zod';

// 1. Schema metier strict
const Lead = z.object({
    fullName: z.string().min(2),
    email: z.string().email(),
    company: z.string().nullable(),
    role: z.enum(['developer', 'manager', 'cto', 'other']),
    interests: z.array(z.string()).max(5),
    score: z.number().min(0).max(100),
});

type Lead = z.infer<typeof Lead>;

async function extractLead(rawText: string): Promise<Lead> {
    const response = await mistral.chat.complete({
        model: 'mistral-large-latest',
        // Le mode json garantit un JSON valide en sortie
        responseFormat: { type: 'json_object' },
        messages: [
            {
                role: 'system',
                content: `Extrait un lead commercial au format JSON strict :
{
  "fullName": string,
  "email": string,
  "company": string | null,
  "role": "developer" | "manager" | "cto" | "other",
  "interests": string[],
  "score": number (0-100)
}
Ne retourne QUE le JSON, sans markdown, sans commentaire.`
            },
            { role: 'user', content: rawText }
        ],
        temperature: 0,   // deterministe pour extraction
    });

    const raw = response.choices[0].message.content ?? '{}';

    // 2. Validation Zod - garantit le typage en production
    const parsed = Lead.parse(JSON.parse(raw));
    return parsed;
}

// Exemple d'appel
const lead = await extractLead(`
Bonjour, je suis Sarah Martin, CTO chez Acme. Mon email : sarah@acme.io.
J'aimerais discuter de RAG et d'agents IA pour notre support client.
`);

console.log(lead);
// { fullName: 'Sarah Martin', email: 'sarah@acme.io', company: 'Acme',
//   role: 'cto', interests: ['rag', 'agents-ia'], score: 85 }
JSON mode vs prompt seul : Sans responseFormat, le modèle peut entourer la réponse de ```json … ``` ou ajouter un commentaire. Avec json_object, le SDK garantit un JSON parseable. Combiné à Zod, vous obtenez un contrat de données aussi solide qu'un endpoint REST typé.

Codestral : complétion FIM pour IDE

Codestral est le modèle code de Mistral. Outre la génération classique via chat.complete, il expose un endpoint FIM (fill-in-the-middle) spécialement conçu pour l'auto-complétion IDE : on lui passe un préfixe et un suffixe, il génère le code manquant au milieu.

// codestral-fim.ts - Auto-completion type GitHub Copilot
import mistral from './lib/mistral';

async function complete(prefix: string, suffix: string): Promise<string> {
    const response = await mistral.fim.complete({
        model: 'codestral-latest',
        prompt: prefix,        // code AVANT le curseur
        suffix: suffix,        // code APRES le curseur
        maxTokens: 200,
        temperature: 0.2,      // bas pour du code reproductible
        stop: ['\n\n'],        // stop sur double saut de ligne
    });

    return response.choices[0].message.content ?? '';
}

// Exemple : completer une fonction TypeScript
const prefix = `
function calculerTVA(montantHT: number, taux: number): number {
    // Calcule le montant TTC en arrondissant a 2 decimales
`;

const suffix = `
}

console.log(calculerTVA(100, 0.20));  // attendu : 120
`;

const completion = await complete(prefix, suffix);
console.log(completion);
// Sortie probable :
//     const ttc = montantHT * (1 + taux);
//     return Math.round(ttc * 100) / 100;

Le FIM est utilisé par les plugins IDE (extension VS Code continue.dev, intégration JetBrains) pour suggérer du code contextuel. Avec un prompt bien construit (15-30 lignes de contexte avant et après), Codestral atteint des taux d'acceptation comparables à GitHub Copilot.

Pixtral vision et embeddings

Pixtral Large traite indifféremment du texte et des images dans le même message. Les usages typiques : OCR, captioning, extraction de tableaux depuis des screenshots, lecture de tickets de caisse, modération de contenu visuel.

// pixtral-vision.ts - Analyse d'une image (OCR + extraction)
import mistral from './lib/mistral';
import fs from 'node:fs/promises';

async function readInvoice(imagePath: string) {
    // Lire l'image et l'encoder en base64
    const buffer = await fs.readFile(imagePath);
    const base64 = buffer.toString('base64');
    const dataUrl = `data:image/jpeg;base64,${base64}`;

    const response = await mistral.chat.complete({
        model: 'pixtral-large-latest',
        responseFormat: { type: 'json_object' },
        messages: [{
            role: 'user',
            content: [
                {
                    type: 'text',
                    text: 'Extrait de cette facture : { vendeur, montantTTC, devise, date }.'
                },
                {
                    type: 'image_url',
                    // Mistral accepte aussi une URL publique
                    imageUrl: dataUrl,
                }
            ]
        }],
    });

    return JSON.parse(response.choices[0].message.content ?? '{}');
}

// Usage
const data = await readInvoice('./factures/octobre.jpg');
console.log(data);  // { vendeur: '...', montantTTC: 124.50, devise: 'EUR', date: '2026-10-12' }

Embeddings pour RAG sur documentation interne

// embed-rag.ts - Vectoriser des documents pour recherche semantique
import mistral from './lib/mistral';

async function embedTexts(texts: string[]): Promise<number[][]> {
    const response = await mistral.embeddings.create({
        model: 'mistral-embed',
        inputs: texts,           // batch jusqu'a 512 textes
    });

    // Chaque embedding fait 1024 dimensions
    return response.data.map(d => d.embedding);
}

// Distance cosinus pour ranker des documents
function cosine(a: number[], b: number[]): number {
    let dot = 0, magA = 0, magB = 0;
    for (let i = 0; i < a.length; i++) {
        dot += a[i] * b[i];
        magA += a[i] * a[i];
        magB += b[i] * b[i];
    }
    return dot / (Math.sqrt(magA) * Math.sqrt(magB));
}

// Pipeline complet RAG simplifie
async function searchDocs(query: string, corpus: { id: string; text: string; vec: number[] }[]) {
    const [qVec] = await embedTexts([query]);
    return corpus
        .map(doc => ({ ...doc, score: cosine(qVec, doc.vec) }))
        .sort((a, b) => b.score - a.score)
        .slice(0, 5);
}

En production, stockez les vecteurs dans pgvector (PostgreSQL), Qdrant ou Pinecone et requêtez avec un index HNSW. mistral-embed coûte 0,10 $/M tokens, soit dix fois moins cher que text-embedding-3-large d'OpenAI pour une qualité comparable sur le français.

Production : retry, coûts, RGPD

Trois préoccupations dominent en production : la résilience (gérer les 429 et 5xx), le contrôle des coûts (monitoring + routage multi-modèle) et la conformité RGPD (zones de traitement, contrats DPA).

// production-wrapper.ts - Retry exponentiel et observabilite
import mistral from './lib/mistral';
import type { ChatCompletionRequest } from '@mistralai/mistralai';

async function callMistralRobust(params: ChatCompletionRequest, maxRetries = 3) {
    for (let attempt = 0; attempt <= maxRetries; attempt++) {
        const startedAt = Date.now();
        try {
            const response = await mistral.chat.complete(params);

            // Telemetrie : tokens et duree pour le monitoring
            logUsage({
                model: params.model,
                inputTokens: response.usage.promptTokens,
                outputTokens: response.usage.completionTokens,
                latencyMs: Date.now() - startedAt,
                attempt,
            });

            return response;
        } catch (err: any) {
            const status = err?.statusCode ?? err?.status;
            const retryable = status === 429 || (status >= 500 && status < 600);

            if (!retryable || attempt === maxRetries) throw err;

            // Backoff exponentiel : 0.5s, 1s, 2s, 4s
            const delay = 500 * Math.pow(2, attempt);
            await new Promise(r => setTimeout(r, delay));
        }
    }
    throw new Error('Retries epuises');
}

// Helper de logs (a remplacer par Datadog/OpenTelemetry en prod)
function logUsage(data: Record<string, unknown>) {
    console.log('[mistral]', JSON.stringify(data));
}
  • Proxy serveur obligatoire : Angular ou React n'appelle jamais l'API directement.
  • Rate limiting par utilisateur authentifié (ex : 30 req/min) pour éviter les abus.
  • Routage multi-modèle : ministral-8b pour le routing/classification, mistral-large pour les tâches complexes uniquement.
  • Monitoring : enregistrer tokens et latence par appel, calculer coût/utilisateur/jour.
  • Alerte budget : seuil mensuel configuré dans la console Mistral.
  • RGPD : sélectionner la région UE dans les workspaces, signer le DPA fourni par Mistral, ne pas envoyer de PII non strictement nécessaire.
  • Plan B : prévoir un fallback Mixtral self-hosted (vLLM) en cas d'incident côté API publique.
Conformité européenne : Mistral fournit un Data Processing Agreement aligné RGPD et garantit que les données ne quittent pas l'UE quand la région européenne est sélectionnée. Pour les secteurs régulés (santé, banque), c'est souvent le critère décisif face à OpenAI et Anthropic.

Conclusion

Mistral AI est un excellent choix par défaut pour un projet européen : tarification 50 à 70 % moins chère que GPT-4 sur les modèles équivalents, datacenters en UE, et une gamme couvrant le généraliste (Large/Small), le code (Codestral), la vision (Pixtral) et l'edge (Ministral). Le SDK @mistralai/mistralai est moderne, l'endpoint OpenAI-compatible facilite la migration, et le function calling parallèle simplifie l'écriture d'agents.

Pour démarrer : mistral-small-latest pour 80 % des cas, mistral-large-latest quand le raisonnement compte, codestral-latest pour l'auto-complétion IDE, mistral-embed pour la recherche sémantique. Mettez un proxy Express avec retry et monitoring devant toute production, et activez le routage multi-modèle dès que le volume dépasse quelques milliers d'appels par jour.

Partager