Protocoles des API LLM Architecture v200

, par Bertrand Degoy

Ce document décrit les deux modes d’appel aux API utilisés par les modèles LLM dans l’architecture v200 : synchrone et streaming.

Protocoles d’appel LLM : synchrone vs streaming

Ce document décrit les deux modes d’appel utilisés par les modèles LLM dans l’architecture v200 :

  • appel synchrone (generate)
  • appel en streaming (stream_generate)

Les deux modes utilisent HTTP(S), mais pas le même protocole.

Dans la v200, les InternalLLM sont les seules classes responsables :

  • de la configuration backend (client, modèle, URL si HTTP)
  • de l’appel synchrone
  • de l’appel streaming
  • de la normalisation des réponses

1. Appel synchrone : HTTP POST classique

L’appel synchrone (generate) utilise un POST HTTP standard.

Caractéristiques

  • Une seule requête
  • Une seule réponse
  • Format JSON complet
  • Pas de flux
  • Pas de tokens intermédiaires

Exemple (OpenAI/Mistral-like)

POST /v1/chat/completions
Content-Type: application/json

{
  "model": "mistral-large",
  "messages": [...]
}

Réponse :

200 OK
Content-Type: application/json

{
  "id": "...",
  "choices": [
    { "message": { "content": "réponse complète" } }
  ]
}

2. Appel streaming : HTTP POST + protocole de flux

L’appel stream_generate utilise également un POST HTTP, mais la réponse n’est pas un JSON unique.
Le serveur envoie un flux d’événements ou de chunks.

Deux protocoles sont utilisés selon les backends :

2.1. SSE (Server-Sent Events)

C’est le protocole utilisé par :

  • OpenAI
  • Mistral
  • Anthropic
  • Groq
  • LM Studio (mode OpenAI)

Caractéristiques

  • Content-Type: text/event-stream
  • Transfer-Encoding: chunked
  • Chaque ligne commence par data:
  • Chaque événement contient un fragment (delta)
  • Le flux se termine par data: [DONE]

Exemple

data: {"choices":[{"delta":{"content":"Bon"}}]}
data: {"choices":[{"delta":{"content":"jour"}}]}
data: {"choices":[{"delta":{"content":" !"}}]}
data: [DONE]

2.2. Chunked Transfer Encoding (Ollama)

Ollama n’utilise pas SSE mais un flux de JSON successifs, un par chunk.

Exemple

{"response": "Bon"}
{"response": "jour"}
{"response": " Bertrand"}
{"done": true}

Caractéristiques

  • Pas de data:
  • Pas de SSE
  • Chaque chunk est un JSON indépendant
  • Le client doit lire le flux chunk par chunk

3. Pourquoi deux protocoles différents ?

Parce que :

  • un appel synchrone renvoie une réponse complète
  • un streaming renvoie un flux de tokens

HTTP classique ne permet pas d’envoyer plusieurs réponses successives.
Les serveurs utilisent donc :

  • SSE → flux d’événements textuels
  • chunked transfer → flux de JSON partiels
  • WebSocket (rare) → flux bidirectionnel

4. Conséquences pour l’architecture v200

4.1. generate()

  • utilise un appel HTTP simple
  • renvoie un JSON complet
  • normalisation effectuée dans la classe LLM

4.2. stream_generate()

  • utilise SSE ou chunked transfer
  • lit les fragments au fur et à mesure
  • normalise chaque fragment en format v200 :
{ "delta": "texte" }
...
{ "delta": "", "meta": {...} }

Retourne un flux de chunks représentant la réponse en streaming. Contrat de l'interface :

  • La méthode doit produire un itérable de chunks.
Convention v200 (obligatoire pour toutes les implémentations) :
- Les chunks intermédiaires sont des dicts contenant :
    { "delta": "<texte incrémental>" }
- Le dernier chunk contient en plus des métadonnées :
    { "delta": "", "meta": {...} }

Cette convention garantit un format standardisé pour toutes les
implémentations InternalLLM, indépendamment du backend utilisé.

5. Résumé

| Mode | Protocole | Format | Usage | | generate() | HTTP POST classique | JSON complet | réponse unique | | stream_generate | SSE ou chunked transfer | flux de tokens | streaming |

6. Exemple

  1. from commons.models.internal.base import BaseGenerativeModel
  2. from commons.models.response import Response
  3.  
  4. class MistralAPIInternalLLM(BaseGenerativeModel):
  5.     """
  6.    Implémentation Mistral API pour v200.
  7.  
  8.    Cette classe contient :
  9.    - la configuration backend (client, modèle)
  10.    - generate() : appel synchrone
  11.    - stream_generate() : appel SSE
  12.    - normalisation des réponses
  13.    """
  14.  
  15.     def __init__(self, client, model_name: str):
  16.         self.client = client
  17.         self.model_name = model_name
  18.  
  19.     def generate(self, prompt: str, **kwargs) -> Response:
  20.         """
  21.        Appel synchrone : requête HTTP complète, pas de streaming.
  22.        Le format exact de la réponse dépend du backend.
  23.        """
  24.         raw = self.client.chat.completions.create(
  25.             model=self.model_name,
  26.             messages=[{"role": "user", "content": prompt}],
  27.             stream=False,
  28.             **kwargs,
  29.         )
  30.  
  31.         try:
  32.             text = raw["choices"][0]["message"]["content"]
  33.         except Exception:
  34.             text = str(raw)
  35.  
  36.         return Response(
  37.             response=text,
  38.             raw_response=raw,
  39.             source_nodes=None,
  40.             metadata={},
  41.         )
  42.  
  43.     def stream_generate(self, prompt: str, **kwargs):
  44.         """
  45.        Retourne un flux de chunks.
  46.        Le format exact dépend de l'implémentation.
  47.  
  48.        Convention interne v200 :
  49.        - chunks intermédiaires : {"delta": "..."}
  50.        - chunk final : {"delta": "", "meta": {...}}
  51.        """
  52.         stream = self.client.chat.completions.create(
  53.             model=self.model_name,
  54.             messages=[{"role": "user", "content": prompt}],
  55.             stream=True,
  56.             **kwargs,
  57.         )
  58.  
  59.         final_message = None
  60.  
  61.         for chunk in stream:
  62.             delta = ""
  63.  
  64.             if chunk.choices:
  65.                 c = chunk.choices[0]
  66.  
  67.                 if getattr(c, "delta", None) and getattr(c.delta, "content", None):
  68.                     delta = c.delta.content or ""
  69.  
  70.                 if getattr(c, "message", None):
  71.                     final_message = c.message
  72.  
  73.             yield {"delta": delta}
  74.  
  75.         yield {
  76.             "delta": "",
  77.             "meta": {
  78.                 "raw_response": final_message,
  79.                 "nodes": None,
  80.                 "metadata": {},
  81.             },
  82.         }

Télécharger