MacLustr API

Documentation de l'API MacLustr

Deux grands modèles de langage servis localement sur le cluster MacLustr via une API compatible OpenAI, avec streaming token-par-token.

Llama 3.3 70B · 4-bit Qwen2.5 72B · 4-bit MLX · Apple Silicon SSE streaming

Choisis le modèle : les exemples de code ci-dessous s'adaptent automatiquement.

Base URLs

ModèleBase URLNom du modèle (champ model)
Llama 3.3 70Bhttps://llama.maclustr.ioLlama-3.3-70B-Instruct-4bit
Qwen2.5 72Bhttps://qwen.maclustr.ioQwen2.5-72B-Instruct-4bit

Les deux serveurs exposent exactement la même surface d'API. Le champ model dans le corps de requête est optionnel (chaque base URL ne sert qu'un seul modèle).

Authentification

Aucune clé requise pour l'instant — les endpoints sont publics derrière ngrok (TLS terminé par ngrok). N'envoie pas de données sensibles. Un en-tête Authorization est accepté mais ignoré, ce qui permet d'utiliser les SDK OpenAI sans modification.

GET /health

Vérifie que le serveur et le modèle sont chargés.

GET__BASE__/health
curl __BASE__/health

Réponse

{ "status": "ok", "model": "__MODELNAME__" }

GET /v1/models

Liste le(s) modèle(s) disponible(s), au format OpenAI.

GET__BASE__/v1/models
{
  "object": "list",
  "data": [{ "id": "__MODELNAME__", "object": "model", "owned_by": "maclustr" }]
}

POST /v1/chat/completions

Génération conversationnelle. Le modèle applique son chat template aux messages (rôles system, user, assistant).

POST__BASE__/v1/chat/completions

Requête (streaming)

curl -N __BASE__/v1/chat/completions \
  -H 'Content-Type: application/json' \
  -d '{
    "messages": [
      {"role": "system", "content": "Tu es un assistant utile."},
      {"role": "user", "content": "Explique la mémoire unifiée en 2 phrases."}
    ],
    "max_tokens": 512,
    "temperature": 0.7,
    "top_p": 1.0,
    "stream": true
  }'

Réponse en streaming

Suite d'événements text/event-stream, chacun préfixé par data: , terminée par data: [DONE].

data: {"id":"chatcmpl-…","object":"chat.completion.chunk","model":"__MODELNAME__",
       "choices":[{"index":0,"delta":{"role":"assistant"},"finish_reason":null}]}

data: {"id":"chatcmpl-…","object":"chat.completion.chunk","model":"__MODELNAME__",
       "choices":[{"index":0,"delta":{"content":"La"},"finish_reason":null}]}

data: {"id":"chatcmpl-…","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}

data: [DONE]

Réponse non-streaming ("stream": false)

{
  "id": "chatcmpl-…",
  "object": "chat.completion",
  "model": "__MODELNAME__",
  "choices": [{
    "index": 0,
    "message": { "role": "assistant", "content": "La mémoire unifiée…" },
    "finish_reason": "stop"
  }]
}

POST /v1/completions

Complétion brute à partir d'un prompt texte (sans chat template).

POST__BASE__/v1/completions
curl -N __BASE__/v1/completions \
  -H 'Content-Type: application/json' \
  -d '{ "prompt": "La capitale de la France est", "max_tokens": 16, "stream": true }'

Chaque chunk contient choices[0].text (au lieu de delta.content) ; objet "text_completion".

Paramètres

ChampTypeDéfautDescription
messagesarrayChat uniquement. Liste {role, content}.
promptstringComplétion uniquement. Texte d'entrée.
max_tokensint1024Nombre max de tokens générés.
temperaturefloat0.70 = déterministe, >1 = plus créatif.
top_pfloat1.0Nucleus sampling.
top_kint0Garde les k tokens les plus probables (0 = désactivé).
min_pfloat0.0Seuil de probabilité minimale relative.
repetition_penaltyfloat1.0>1 pénalise la répétition.
presence_penaltyfloatPénalise les tokens déjà apparus.
frequency_penaltyfloatPénalise selon la fréquence.
seedintGénération reproductible.
stopstring / arraySéquence(s) d'arrêt — coupe la génération.
streamboolfalseSi true, renvoie un flux SSE.
modelstringautoOptionnel — ignoré (un modèle par base URL).

Streaming (SSE)

Format text/event-stream. Parser : découper sur \n\n, retirer le préfixe data: , ignorer [DONE], parser le JSON, lire choices[0].delta.content (chat) ou choices[0].text (complétion).

Erreurs

CodeSignification
200OK (y compris flux SSE).
422Corps JSON invalide / champ manquant.
502/504 (ngrok)Le serveur du modèle est hors ligne ou redémarre.
Astuce : vérifie __BASE__/health avant de débugger un client.

Exemple — SDK OpenAI (Python)

L'API étant compatible OpenAI, il suffit de pointer base_url vers la base URL du modèle.

from openai import OpenAI

client = OpenAI(base_url="__BASE__/v1", api_key="not-needed")

stream = client.chat.completions.create(
    model="__MODELID__",
    messages=[{"role": "user", "content": "Salut !"}],
    stream=True,
)
for chunk in stream:
    print(chunk.choices[0].delta.content or "", end="", flush=True)

Exemple — Python (requests, streaming)

import json, requests

r = requests.post(
    "__BASE__/v1/chat/completions",
    json={"messages": [{"role": "user", "content": "Salut !"}], "stream": True},
    stream=True,
)
for line in r.iter_lines():
    if not line or not line.startswith(b"data: "):
        continue
    data = line[6:]
    if data == b"[DONE]":
        break
    delta = json.loads(data)["choices"][0]["delta"]
    print(delta.get("content", ""), end="", flush=True)

Exemple — JavaScript (fetch, navigateur/Node)

const res = await fetch("__BASE__/v1/chat/completions", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    messages: [{ role: "user", content: "Salut !" }],
    stream: true,
  }),
});
const reader = res.body.getReader();
const dec = new TextDecoder();
let buf = "";
while (true) {
  const { value, done } = await reader.read();
  if (done) break;
  buf += dec.decode(value, { stream: true });
  let i;
  while ((i = buf.indexOf("\n\n")) >= 0) {
    const line = buf.slice(0, i).trim(); buf = buf.slice(i + 2);
    if (!line.startsWith("data:")) continue;
    const d = line.slice(5).trim();
    if (d === "[DONE]") continue;
    const j = JSON.parse(d);
    process.stdout.write(j.choices[0].delta.content || "");
  }
}

Passerelle web (chat / playground)

L'interface unifiée chat.maclustr.io / playground.maclustr.io proxifie ces deux API sur une même origine. Endpoints internes :

EndpointRôle
POST /api/chatProxy chat — body {model:"llama"|"qwen", messages, system?, max_tokens, temperature, top_p}, toujours en streaming.
POST /api/completeProxy complétion brute — body {model, prompt, …}.
GET /api/modelsListe des modèles de la passerelle.
GET /api/healthÉtat agrégé des deux modèles.