nAIxus Docs
Guides pratiques

Recevoir les réponses en SSE

Guide pratique - consommer le flux Server-Sent Events du canal API depuis un backend Node.js ou un client navigateur.

Utilisez ce guide lorsque vous souhaitez afficher la réponse IA de manière incrémentale au fur et à mesure de sa génération, plutôt que d'attendre la réponse complète.

Résultat attendu : votre client lit le flux SSE et traite chaque delta de texte à sa réception.


Quand utiliser le streaming

Définissez "stream": true dans le corps de la requête lorsque :

  • Vous construisez une interface de chat qui affiche le texte au fur et à mesure de son apparition.
  • Votre flow produit des réponses longues et vous souhaitez réduire la latence perçue.
  • Vous avez besoin de run_id ou conversation_id avant la fin de la réponse (ils arrivent dans le premier événement data-run).

Pour les automatisations courtes et synchrones (traitement en lot, webhooks vers d'autres systèmes), utilisez "stream": false et lisez la réponse JSON.


Effectuer une requête en streaming

POST /public/v1/api/execute
Authorization: Bearer <clé>
Content-Type: application/json

{"payload":"Expliquez l'impact de ce changement de code.","stream":true}

Le serveur répond avec Content-Type: text/event-stream; charset=utf-8 et commence à émettre des événements.


Séquence des événements

Chaque réponse en streaming suit cet ordre :

data: {"type":"data-run","data":{"run_id":"9c1d…","conversation_id":"b1f2…"}}
data: {"type":"text-start","id":"9c1d…"}
data: {"type":"text-delta","id":"9c1d…","delta":"Le changement "}
data: {"type":"text-delta","id":"9c1d…","delta":"introduit une rupture d'API."}
data: {"type":"text-end","id":"9c1d…"}
data: {"type":"finish","finishReason":"stop"}

Si l'exécution échoue après le début du streaming :

data: {"type":"error","errorText":"Echec d'exécution : timeout"}

Aucun événement finish ne suit un error. Le statut HTTP est déjà 200 à ce stade ; l'événement error est le signal d'échec.


Analyser le flux en Node.js

async function streamApiChannel(
  payload: string,
  apiKey: string,
  onDelta: (delta: string) => void
): Promise<{ runId: string; conversationId: string }> {
  const body = JSON.stringify({ payload, stream: true });

  const response = await fetch('https://api.example.com/public/v1/api/execute', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${apiKey}`,
      'Content-Type': 'application/json',
    },
    body,
  });

  if (!response.ok) {
    const err = await response.json();
    throw new Error(`${err.error?.code}: ${err.error?.message}`);
  }

  const reader = response.body!.getReader();
  const decoder = new TextDecoder();
  let buffer = '';
  let runId = '';
  let conversationId = '';

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;

    buffer += decoder.decode(value, { stream: true });
    const chunks = buffer.split('\n\n');
    buffer = chunks.pop() ?? '';

    for (const chunk of chunks) {
      if (!chunk.startsWith('data: ')) continue;
      const event = JSON.parse(chunk.slice(6));

      switch (event.type) {
        case 'data-run':
          runId = event.data.run_id;
          conversationId = event.data.conversation_id;
          break;
        case 'text-delta':
          onDelta(event.delta);
          break;
        case 'error':
          throw new Error(`Erreur de flux : ${event.errorText}`);
        case 'finish':
          return { runId, conversationId };
      }
    }
  }

  return { runId, conversationId };
}

Analyser le flux dans un navigateur

const response = await fetch('/public/v1/api/execute', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer ' + publicKey,
    'Content-Type': 'application/json',
    Origin: window.location.origin,
  },
  body: JSON.stringify({ payload: userMessage, stream: true }),
});

const reader = response.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 });

  for (const chunk of buffer.split('\n\n')) {
    if (!chunk.startsWith('data: ')) continue;
    const event = JSON.parse(chunk.slice(6));
    if (event.type === 'text-delta') {
      outputDiv.textContent += event.delta;
    }
  }
  buffer = buffer.endsWith('\n\n') ? '' : (buffer.split('\n\n').pop() ?? '');
}

Les appelants navigateur doivent utiliser une clé nx_pub_ et le canal doit avoir l'origine de la page dans sa liste d'origines autorisées.


Gestion de la déconnexion client

Lorsque le client ferme la connexion, le serveur annule la tâche d'exécution en arrière-plan. Aucune donnée n'est perdue côté serveur ; l'entrée de conversation n'est pas corrompue.


Mise en mémoire tampon par proxy

Si vous routez les requêtes via nginx ou un autre proxy inverse, assurez-vous que la mise en mémoire tampon des réponses est désactivée pour le point d'accès de streaming :

location /public/v1/api/ {
    proxy_buffering off;
    proxy_pass http://core_api;
}

Le serveur définit X-Accel-Buffering: no automatiquement, mais tous les proxys ne respectent pas cet en-tête.


Référence

Pour les définitions complètes des types d'événements et la sémantique des erreurs, voir la référence d'API - section Réponse SSE en streaming.

On this page