Serveur compatible OpenAI¶
vllm-mlx fournit un serveur FastAPI avec une compatibilité complète avec l'API OpenAI.
Par défaut, le serveur n'écoute que sur 127.0.0.1. Utilisez --host 0.0.0.0 uniquement si vous souhaitez délibérément l'exposer au-delà de la machine locale.
Démarrage du serveur¶
Mode simple (par défaut)¶
Débit maximal pour un utilisateur unique :
Mode continuous batching¶
Pour plusieurs utilisateurs simultanés :
Avec paged cache¶
Mise en cache efficace en mémoire pour la production :
vllm-mlx serve mlx-community/Llama-3.2-3B-Instruct-4bit --port 8000 --continuous-batching --use-paged-cache
Options du serveur¶
| Option | Description | Défaut |
|---|---|---|
--port |
Port du serveur | 8000 |
--host |
Hôte du serveur | 127.0.0.1 |
--api-key |
Clé API pour l'authentification | None |
--rate-limit |
Requêtes par minute par client (0 = désactivé) | 0 |
--timeout |
Délai d'expiration des requêtes en secondes | 300 |
--enable-metrics |
Expose les métriques Prometheus sur /metrics |
False |
--continuous-batching |
Active le batching pour plusieurs utilisateurs | False |
--use-paged-cache |
Active le paged KV cache | False |
--cache-memory-mb |
Limite mémoire du cache en Mo | Auto |
--cache-memory-percent |
Fraction de la RAM réservée au cache | 0.20 |
--max-tokens |
Nombre maximal de tokens par défaut | 32768 |
--max-request-tokens |
Valeur maximale de max_tokens acceptée des clients API |
32768 |
--default-temperature |
Température par défaut si non spécifiée | None |
--default-top-p |
Valeur top_p par défaut si non spécifiée | None |
--stream-interval |
Tokens par fragment de streaming | 1 |
--mcp-config |
Chemin vers le fichier de configuration MCP | None |
--reasoning-parser |
Parser pour les modèles reasoning (qwen3, deepseek_r1) |
None |
--embedding-model |
Précharge un modèle d'embeddings au démarrage | None |
--enable-auto-tool-choice |
Active le tool calling automatique | False |
--tool-call-parser |
Parser de tool calling (voir Tool Calling) | None |
Points de terminaison de l'API¶
Chat Completions¶
from openai import OpenAI
client = OpenAI(base_url="http://localhost:8000/v1", api_key="not-needed")
# Non-streaming
response = client.chat.completions.create(
model="default",
messages=[{"role": "user", "content": "Hello!"}],
max_tokens=100
)
# Streaming
stream = client.chat.completions.create(
model="default",
messages=[{"role": "user", "content": "Tell me a story"}],
stream=True
)
for chunk in stream:
if chunk.choices[0].delta.content:
print(chunk.choices[0].delta.content, end="")
Completions¶
response = client.completions.create(
model="default",
prompt="The capital of France is",
max_tokens=50
)
Modèles¶
Retourne les modèles disponibles.
Embeddings¶
response = client.embeddings.create(
model="mlx-community/multilingual-e5-small-mlx",
input="Hello world"
)
print(response.data[0].embedding[:5]) # First 5 dimensions
Voir le Guide des embeddings pour plus de détails.
Vérification de l'état¶
Retourne l'état du serveur.
Métriques¶
Point de terminaison de collecte Prometheus pour les métriques du serveur, du cache, du scheduler et des requêtes.
Le point de terminaison est désactivé par défaut et s'active avec --enable-metrics.
/metrics est intentionnellement non authentifié. Exposez-le uniquement sur un réseau de confiance ou derrière un reverse proxy ou un pare-feu qui limite les accès.
API Anthropic Messages¶
Point de terminaison compatible Anthropic qui permet à des outils comme Claude Code et OpenCode de se connecter directement à vllm-mlx. En interne, il traduit les requêtes Anthropic au format OpenAI, exécute l'inférence via le moteur, puis convertit la réponse au format Anthropic.
Fonctionnalités :
- Réponses non-streaming et streaming (SSE)
- Messages système (chaîne simple ou liste de blocs de contenu)
- Conversations multi-tours avec messages utilisateur et assistant
- Tool calling avec blocs de contenu tool_use et tool_result
- Comptage de tokens pour le suivi du budget
- Contenu multimodal (images via blocs source)
- Détection de déconnexion client (retourne HTTP 499)
- Filtrage automatique des tokens spéciaux dans la sortie en streaming
Non-streaming¶
from anthropic import Anthropic
client = Anthropic(base_url="http://localhost:8000", api_key="not-needed")
response = client.messages.create(
model="default",
max_tokens=256,
messages=[{"role": "user", "content": "Hello!"}]
)
print(response.content[0].text)
# Response includes: response.id, response.model, response.stop_reason,
# response.usage.input_tokens, response.usage.output_tokens
Streaming¶
Le streaming suit le protocole d'événements SSE d'Anthropic. Les événements sont émis dans cet ordre :
message_start -> content_block_start -> content_block_delta (répété) -> content_block_stop -> message_delta -> message_stop
with client.messages.stream(
model="default",
max_tokens=256,
messages=[{"role": "user", "content": "Tell me a story"}]
) as stream:
for text in stream.text_stream:
print(text, end="")
Messages système¶
Les messages système peuvent être une chaîne simple ou une liste de blocs de contenu :
# Plain string
response = client.messages.create(
model="default",
max_tokens=256,
system="You are a helpful coding assistant.",
messages=[{"role": "user", "content": "Write a hello world in Python"}]
)
# List of content blocks
response = client.messages.create(
model="default",
max_tokens=256,
system=[
{"type": "text", "text": "You are a helpful assistant."},
{"type": "text", "text": "Be concise in your answers."},
],
messages=[{"role": "user", "content": "What is 2+2?"}]
)
Tool calling¶
Définissez les outils avec name, description et input_schema. Le modèle retourne des blocs de contenu tool_use lorsqu'il souhaite appeler un outil. Renvoyez les résultats sous forme de blocs tool_result.
# Step 1: Send request with tools
response = client.messages.create(
model="default",
max_tokens=1024,
messages=[{"role": "user", "content": "What's the weather in Paris?"}],
tools=[{
"name": "get_weather",
"description": "Get weather for a city",
"input_schema": {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"]
}
}]
)
# Step 2: Check if model wants to use tools
for block in response.content:
if block.type == "tool_use":
print(f"Tool: {block.name}, Input: {block.input}, ID: {block.id}")
# response.stop_reason will be "tool_use"
# Step 3: Send tool result back
response = client.messages.create(
model="default",
max_tokens=1024,
messages=[
{"role": "user", "content": "What's the weather in Paris?"},
{"role": "assistant", "content": response.content},
{"role": "user", "content": [
{
"type": "tool_result",
"tool_use_id": block.id,
"content": "Sunny, 22C"
}
]}
],
tools=[{
"name": "get_weather",
"description": "Get weather for a city",
"input_schema": {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"]
}
}]
)
print(response.content[0].text) # "The weather in Paris is sunny, 22C."
Modes de sélection d'outil :
tool_choice |
Comportement |
|---|---|
{"type": "auto"} |
Le modèle décide d'appeler ou non des outils (par défaut) |
{"type": "any"} |
Le modèle doit appeler au moins un outil |
{"type": "tool", "name": "get_weather"} |
Le modèle doit appeler l'outil spécifié |
{"type": "none"} |
Le modèle n'appellera aucun outil |
Conversations multi-tours¶
messages = [
{"role": "user", "content": "My name is Alice."},
{"role": "assistant", "content": "Nice to meet you, Alice!"},
{"role": "user", "content": "What's my name?"},
]
response = client.messages.create(
model="default",
max_tokens=100,
messages=messages
)
Comptage de tokens¶
Compte les tokens d'entrée d'une requête Anthropic en utilisant le tokenizer du modèle. Utile pour le suivi du budget avant d'envoyer une requête. Comptabilise les tokens des messages système, des messages de conversation, des entrées tool_use, du contenu tool_result et des définitions d'outils (name, description, input_schema).
import requests
resp = requests.post("http://localhost:8000/v1/messages/count_tokens", json={
"model": "default",
"messages": [{"role": "user", "content": "Hello, how are you?"}],
"system": "You are helpful.",
"tools": [{
"name": "search",
"description": "Search the web",
"input_schema": {"type": "object", "properties": {"q": {"type": "string"}}}
}]
})
print(resp.json()) # {"input_tokens": 42}
Exemples curl¶
Non-streaming :
curl http://localhost:8000/v1/messages \
-H "Content-Type: application/json" \
-d '{
"model": "default",
"max_tokens": 256,
"messages": [{"role": "user", "content": "Hello!"}]
}'
Streaming :
curl http://localhost:8000/v1/messages \
-H "Content-Type: application/json" \
-d '{
"model": "default",
"max_tokens": 256,
"stream": true,
"messages": [{"role": "user", "content": "Tell me a joke"}]
}'
Comptage de tokens :
curl http://localhost:8000/v1/messages/count_tokens \
-H "Content-Type: application/json" \
-d '{
"model": "default",
"messages": [{"role": "user", "content": "Hello!"}]
}'
# {"input_tokens": 12}
Champs de la requête¶
| Champ | Type | Requis | Défaut | Description |
|---|---|---|---|---|
model |
string | oui | - | Nom du modèle (utilisez "default" pour le modèle chargé) |
messages |
list | oui | - | Messages de conversation avec role et content |
max_tokens |
int | oui | - | Nombre maximal de tokens à générer |
system |
string or list | non | null | Invite système (chaîne ou liste de blocs {"type": "text", "text": "..."}) |
stream |
bool | non | false | Active le streaming SSE |
temperature |
float | non | 0.7 | Température d'échantillonnage (0.0 = déterministe, 1.0 = créatif) |
top_p |
float | non | 0.9 | Seuil d'échantillonnage nucleus |
top_k |
int | non | null | Échantillonnage top-k |
stop_sequences |
list | non | null | Séquences qui arrêtent la génération |
tools |
list | non | null | Définitions d'outils avec name, description, input_schema |
tool_choice |
dict | non | null | Mode de sélection d'outil (auto, any, tool, none) |
metadata |
dict | non | null | Métadonnées arbitraires (transmises telles quelles, non utilisées par le serveur) |
Format de réponse¶
Réponse non-streaming :
{
"id": "msg_abc123...",
"type": "message",
"role": "assistant",
"model": "default",
"content": [
{"type": "text", "text": "Hello! How can I help?"}
],
"stop_reason": "end_turn",
"stop_sequence": null,
"usage": {
"input_tokens": 12,
"output_tokens": 8
}
}
Lorsque des outils sont appelés, content inclut des blocs tool_use et stop_reason vaut "tool_use" :
{
"content": [
{"type": "text", "text": "Let me check the weather."},
{
"type": "tool_use",
"id": "call_abc123",
"name": "get_weather",
"input": {"city": "Paris"}
}
],
"stop_reason": "tool_use"
}
Raisons d'arrêt :
stop_reason |
Signification |
|---|---|
end_turn |
Le modèle a terminé naturellement |
tool_use |
Le modèle souhaite appeler un outil |
max_tokens |
La limite max_tokens a été atteinte |
Utilisation avec Claude Code¶
Pointez Claude Code directement vers votre serveur vllm-mlx :
# Start the server
vllm-mlx serve mlx-community/Qwen3-Coder-Next-235B-A22B-4bit \
--continuous-batching \
--enable-auto-tool-choice \
--tool-call-parser hermes
# In another terminal, configure Claude Code
export ANTHROPIC_BASE_URL=http://localhost:8000
export ANTHROPIC_API_KEY=not-needed
claude
État du serveur¶
Point de terminaison de surveillance en temps réel qui retourne des statistiques globales du serveur et des détails par requête. Utile pour déboguer les performances, suivre l'efficacité du cache et surveiller la mémoire Metal GPU.
Exemple de réponse :
{
"status": "running",
"model": "mlx-community/Qwen3-8B-4bit",
"uptime_s": 342.5,
"steps_executed": 1247,
"num_running": 1,
"num_waiting": 0,
"total_requests_processed": 15,
"total_prompt_tokens": 28450,
"total_completion_tokens": 3200,
"metal": {
"active_memory_gb": 5.2,
"peak_memory_gb": 8.1,
"cache_memory_gb": 2.3
},
"cache": {
"type": "memory_aware_cache",
"entries": 5,
"hit_rate": 0.87,
"memory_mb": 2350
},
"requests": [
{
"request_id": "req_abc123",
"phase": "generation",
"tokens_per_second": 45.2,
"ttft_s": 0.8,
"progress": 0.35,
"cache_hit_type": "prefix",
"cached_tokens": 1200,
"generated_tokens": 85,
"max_tokens": 256
}
]
}
Champs de la réponse :
| Champ | Description |
|---|---|
status |
État du serveur : running, stopped ou not_loaded |
model |
Nom du modèle chargé |
uptime_s |
Secondes écoulées depuis le démarrage du serveur |
steps_executed |
Nombre total d'étapes d'inférence exécutées |
num_running |
Nombre de requêtes en cours de génération de tokens |
num_waiting |
Nombre de requêtes en attente de prefill |
total_requests_processed |
Total des requêtes traitées depuis le démarrage |
total_prompt_tokens |
Total des tokens de prompt traités depuis le démarrage |
total_completion_tokens |
Total des tokens de complétion générés depuis le démarrage |
metal.active_memory_gb |
Mémoire Metal GPU actuellement utilisée (Go) |
metal.peak_memory_gb |
Pic d'utilisation de la mémoire Metal GPU (Go) |
metal.cache_memory_gb |
Utilisation de la mémoire cache Metal (Go) |
cache |
Statistiques du cache (type, entrées, taux de hit, utilisation mémoire) |
requests |
Liste des requêtes actives avec détails par requête |
Champs par requête dans requests :
| Champ | Description |
|---|---|
request_id |
Identifiant unique de la requête |
phase |
Phase actuelle : queued, prefill ou generation |
tokens_per_second |
Débit de génération pour cette requête |
ttft_s |
TTFT (secondes) |
progress |
Pourcentage de complétion (0.0 à 1.0) |
cache_hit_type |
Type de correspondance dans le cache : exact, prefix, supersequence, lcp ou miss |
cached_tokens |
Nombre de tokens servis depuis le cache |
generated_tokens |
Tokens générés jusqu'à présent |
max_tokens |
Nombre maximal de tokens demandés |
Tool Calling¶
Activez le tool calling compatible OpenAI avec --enable-auto-tool-choice :
vllm-mlx serve mlx-community/Devstral-Small-2507-4bit \
--enable-auto-tool-choice \
--tool-call-parser mistral
Utilisez l'option --tool-call-parser pour sélectionner le parser adapté à votre modèle :
| Parser | Modèles |
|---|---|
auto |
Détection automatique (essaie tous les parsers) |
mistral |
Mistral, Devstral |
qwen |
Qwen, Qwen3 |
llama |
Llama 3.x, 4.x |
hermes |
Hermes, NousResearch |
deepseek |
DeepSeek V3, R1 |
kimi |
Kimi K2, Moonshot |
granite |
IBM Granite 3.x, 4.x |
nemotron |
NVIDIA Nemotron |
xlam |
Salesforce xLAM |
functionary |
MeetKai Functionary |
glm47 |
GLM-4.7, GLM-4.7-Flash |
response = client.chat.completions.create(
model="default",
messages=[{"role": "user", "content": "What's the weather in Paris?"}],
tools=[{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get weather for a city",
"parameters": {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"]
}
}
}]
)
if response.choices[0].message.tool_calls:
for tc in response.choices[0].message.tool_calls:
print(f"{tc.function.name}: {tc.function.arguments}")
Voir le Guide du tool calling pour la documentation complète.
Modèles reasoning¶
Pour les modèles qui exposent leur processus de réflexion (Qwen3, DeepSeek-R1), utilisez --reasoning-parser pour séparer le reasoning de la réponse finale :
# Qwen3 models
vllm-mlx serve mlx-community/Qwen3-8B-4bit --reasoning-parser qwen3
# DeepSeek-R1 models
vllm-mlx serve mlx-community/DeepSeek-R1-Distill-Qwen-7B-4bit --reasoning-parser deepseek_r1
La réponse de l'API inclut un champ reasoning avec le processus de réflexion du modèle :
response = client.chat.completions.create(
model="default",
messages=[{"role": "user", "content": "What is 17 × 23?"}]
)
print(response.choices[0].message.reasoning) # Step-by-step thinking
print(response.choices[0].message.content) # Final answer
En streaming, les fragments de reasoning arrivent en premier, suivis des fragments de contenu :
for chunk in stream:
delta = chunk.choices[0].delta
if delta.reasoning:
print(f"[Thinking] {delta.reasoning}")
if delta.content:
print(delta.content, end="")
Voir le Guide des modèles reasoning pour tous les détails.
Sortie structurée (mode JSON)¶
Forcez le modèle à retourner du JSON valide en utilisant response_format :
Mode JSON Object¶
Retourne n'importe quel JSON valide :
response = client.chat.completions.create(
model="default",
messages=[{"role": "user", "content": "List 3 colors"}],
response_format={"type": "json_object"}
)
# Output: {"colors": ["red", "blue", "green"]}
Mode JSON Schema¶
Retourne du JSON conforme à un schéma spécifique :
response = client.chat.completions.create(
model="default",
messages=[{"role": "user", "content": "List 3 colors"}],
response_format={
"type": "json_schema",
"json_schema": {
"name": "colors",
"schema": {
"type": "object",
"properties": {
"colors": {
"type": "array",
"items": {"type": "string"}
}
},
"required": ["colors"]
}
}
}
)
# Output validated against schema
data = json.loads(response.choices[0].message.content)
assert "colors" in data
Exemple curl¶
curl http://localhost:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "default",
"messages": [{"role": "user", "content": "List 3 colors"}],
"response_format": {"type": "json_object"}
}'
Exemples curl¶
Chat¶
curl http://localhost:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "default",
"messages": [{"role": "user", "content": "Hello!"}],
"max_tokens": 100
}'
Streaming¶
curl http://localhost:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "default",
"messages": [{"role": "user", "content": "Hello!"}],
"stream": true
}'
Configuration du streaming¶
Contrôlez le comportement du streaming avec --stream-interval :
| Valeur | Comportement |
|---|---|
1 (par défaut) |
Envoie chaque token immédiatement |
2-5 |
Regroupe les tokens avant envoi |
10+ |
Débit maximal, sortie plus fragmentée |
# Smooth streaming
vllm-mlx serve model --continuous-batching --stream-interval 1
# Batched streaming (better for high-latency networks)
vllm-mlx serve model --continuous-batching --stream-interval 5
Intégration Open WebUI¶
# 1. Start vllm-mlx server
vllm-mlx serve mlx-community/Llama-3.2-3B-Instruct-4bit --port 8000
# 2. Start Open WebUI
docker run -d -p 3000:8080 \
-e OPENAI_API_BASE_URL=http://host.docker.internal:8000/v1 \
-e OPENAI_API_KEY=not-needed \
--name open-webui \
ghcr.io/open-webui/open-webui:main
# 3. Open http://localhost:3000
Déploiement en production¶
Avec systemd¶
Créez /etc/systemd/system/vllm-mlx.service :
[Unit]
Description=vLLM-MLX Server
After=network.target
[Service]
Type=simple
ExecStart=/usr/local/bin/vllm-mlx serve mlx-community/Qwen3-0.6B-8bit \
--continuous-batching --use-paged-cache --port 8000
Restart=always
[Install]
WantedBy=multi-user.target
Paramètres recommandés¶
Pour une production avec 50 utilisateurs simultanés ou plus :