javascriptstreamnuxt3.jsserver-sent-eventsmistral-ai

EventSource reader keeps repeating itself


I am working with the Mistral AI API to return a stream to the frontend but the frontend keeps repeating itself so instead of just streaming the whole text on screen it just starts again when it's done. I'm using Nuxt 3.

Backend code

import { Mistral } from '@mistralai/mistralai';
import { getQuery } from 'h3';

export default defineEventHandler(async (e) => {
    const { songs } = getQuery(e);
    const config = useRuntimeConfig();

    const messages = [{ 
        role: 'system', 
        content: `
        You are a helpful creative psychiatrist who determines peoples personalities by looking at the songs they listen to. Besides that, 
        give an in-depth analasys of who I might prefer as a romantic partner and why. Mention a few songs on which you based the analysis.
        Respond in markdown and do not ask if you can assist any further.
        `
    },{ 
        role: 'user', 
        content: `${songs}`
    }];

    try {
        const mistral = new Mistral({ apiKey: config.mistralAiApiKey });

        const stream = await mistral.chat.stream({
            model: 'mistral-small-latest',
            messages
        });

        setHeader(e, 'Content-Type', 'text/event-stream');
        setHeader(e, 'Cache-Control', 'no-cache');
        setHeader(e, 'Connection', 'keep-alive');

        for await (const chunk of stream) {
            e.node.res.write(`data: ${chunk.data.choices[0].delta.content}\n\n`);
        }

        e.node.res.end();
    } 
    catch (error) {
        console.error(error);
            
        throw createError({
            statusCode: 500,
            message: error
        });
    }
});

Frontend code

const eventSource = new EventSource(`/api/mistral/analysis?songs=${songs}`);

eventSource.onmessage = (event) => {
    console.log(event.data);
    bio.value += event.data;
};

Solution

  • The issue you’re facing is likely because the frontend keeps reopening the stream or not knowing when it’s done, which causes it to repeat the content.

    In your backend, make sure the stream ends properly and that you send an "end of stream" signal after the loop:

    for await (const chunk of stream) {
        e.node.res.write(`data: ${chunk.data.choices[0].delta.content}\n\n`);
    }
    // Optional: send an "end of stream" signal
    e.node.res.write(`data: [END OF STREAM]\n\n`);
    e.node.res.end();
    

    In your frontend, you need to make sure you handle the stream correctly and stop it once it’s done. Use the end signal and close the connection when finished:

    const eventSource = new EventSource(`/api/mistral/analysis?songs=${songs}`);
    
    eventSource.onmessage = (event) => {
        console.log(event.data);
        if (event.data !== '[END OF STREAM]') {
            bio.value += event.data;
        } else {
            console.log('Stream finished');
            eventSource.close();  // Close stream once done
        }
    };
    
    eventSource.onerror = (err) => {
        console.error('Stream error', err);
        eventSource.close();  // Close on error
    };
    

    Also, make sure you're not accidentally creating multiple EventSource instances. Only create one when needed.

    let eventSource;
    
    if (!eventSource) {
        eventSource = new EventSource(`/api/mistral/analysis?songs=${songs}`);
        eventSource.onmessage = (event) => {
            console.log(event.data);
            bio.value += event.data;
        };
    }
    

    Basically, the key is that your backend should signal the end of the stream, and your frontend should stop and close the stream properly. Make sure you're not re-opening EventSource unnecessarily.