Symptôme : le premier function calling aboutit, puis le tour suivant renvoie une erreur 400 DeepSeek V4 multi-tour.
Solution la plus rapide : créez deux reproductions HTTP indépendantes. Pour l’API officielle, conservez reasoning_content dans chaque message assistant qui a déclenché un outil. Pour vLLM, vérifiez le champ reasoning réellement produit par votre version et ne le renommez pas automatiquement.
Cet article s’adresse aux développeurs d’Agents qui doivent fournir une preuve reproductible à l’équipe modèle ou au mainteneur du framework, aux ingénieurs backend qui cherchent à localiser la panne entre client, passerelle et endpoint, ainsi qu’aux équipes plateforme qui préparent une régression sur deux runtimes.
Dernière mise à jour : 18 août 2026. Les points de contrat ont été vérifiés dans la documentation DeepSeek du thinking mode et des Chat Completions, dans la documentation vLLM sur les sorties de raisonnement, le parseur DeepSeek V4 et la recette de déploiement DeepSeek V4 Flash. (api-docs.deepseek.com)
Le même nom de modèle ne garantit pas le même contrat
Avant de modifier votre boucle Agent, notez le base_url, le modèle demandé, le proxy éventuel et la version du serveur. Un nom comme deepseek-v4-pro ou DeepSeek-V4-Flash ne suffit pas à déterminer le protocole réellement appliqué.
Vous devez traiter séparément :
| Élément à vérifier | API officielle DeepSeek | Endpoint vLLM auto-hébergé |
|---|---|---|
| Adresse cible | URL officielle configurée dans votre client | Adresse du serveur vLLM ou de votre passerelle |
| Champ de raisonnement à observer | reasoning_content |
reasoning dans la documentation actuelle vLLM |
| Appel d’outil | tool_calls au format Chat Completions |
Format OpenAI compatible, dépendant du parseur activé |
| Rejeu après outil | reasoning_content conservé dans l’assistant |
Contrat défini par la version et le template utilisés |
| Paramètres critiques | thinking mode, tools, messages | --reasoning-parser, --tool-call-parser, auto tool choice |
| Preuve acceptable | réponse et erreur de l’API officielle | réponse réelle et configuration du déploiement |
La documentation DeepSeek indique que, lorsqu’un tour en mode réflexion effectue un appel d’outil, le champ reasoning_content doit être renvoyé dans les requêtes suivantes. Une omission peut provoquer une réponse 400. (api-docs.deepseek.com)
vLLM suit une autre convention de sortie. Sa documentation de raisonnement décrit le champ reasoning, et son parseur DeepSeek V4 reconstruit notamment content, reasoning et tool_calls. Cette différence ne prouve pas que l’API DeepSeek acceptera reasoning en entrée. (docs.vllm.ai)
Les coûts cachés sont généralement ailleurs :
- un SDK peut convertir un objet assistant en dictionnaire incomplet ;
- un journal de conversation peut supprimer les champs considérés comme sensibles ;
- une passerelle OpenAI compatible peut normaliser
reasoningetreasoning_contentsans connaître la cible ; - un serveur vLLM peut utiliser un parseur ou un template différent de celui attendu ;
- un test qui vérifie uniquement le statut 200 ne prouve pas que les identifiants d’appel et l’ordre des messages sont corrects.
Ne comparez donc pas deux payloads après passage dans le même adaptateur. Comparez d’abord les requêtes HTTP réellement envoyées.
Première étape : préparer deux reproductions qui ne se contaminent pas
Utilisez un seul outil sans effet de bord. Par exemple, l’outil get_build_status peut retourner une valeur fixe et ne doit ni modifier une base ni lancer une tâche réelle.
Préparez exactement les mêmes éléments fonctionnels pour les deux tests :
{
"tools": [
{
"type": "function",
"function": {
"name": "get_build_status",
"description": "Retourne l'état d'une compilation.",
"parameters": {
"type": "object",
"properties": {
"job_id": { "type": "string" }
},
"required": ["job_id"],
"additionalProperties": false
}
}
}
],
"messages": [
{
"role": "user",
"content": "Vérifiez l'état de la compilation job-17."
}
]
}
Conservez séparément les variables suivantes :
export BASE_URL="https://api.example.test"
export MODEL="deepseek-v4-pro"
export API_KEY="redacted"
Dans votre dossier de reproduction, enregistrez :
01-request-first.json
02-response-first.json
03-request-with-tool.json
04-response-second.json
05-request-without-reasoning.json
06-error.json
Ne consignez jamais la clé, le texte complet du raisonnement, les données utilisateur ou le résultat réel d’un outil. Remplacez ces valeurs par des marqueurs de même type. L’objectif est de conserver la structure, pas de publier une conversation privée.
Votre première table de décision doit rester factuelle :
| Si vous observez… | Hypothèse prioritaire | Vérification immédiate |
|---|---|---|
| 400 après le premier outil uniquement sur l’API officielle | reasoning_content absent ou altéré |
Comparer le message assistant brut et le message rejoué |
reasoning dans la réponse vLLM |
Contrat vLLM appliqué | Lire la version, le parseur et le template |
tool_call_id différent entre assistant et tool |
Association de messages rompue | Vérifier les identifiants avant sérialisation |
200 sans tool_calls côté vLLM |
Parseur ou auto tool choice incomplet | Vérifier les options de service et le format généré |
| 400 sur les deux endpoints | Payload commun invalide | Retirer le proxy et tester directement chaque endpoint |
Deuxième étape : déclencher un appel d’outil avec HTTP brut
Pour l’API officielle, adaptez l’URL et les paramètres à votre contrat documenté. Le point important est de ne pas laisser un framework ajouter automatiquement tool_choice, des champs de raisonnement ou une structure de message que vous ne contrôlez pas.
Exemple volontairement minimal :
curl "$BASE_URL/v1/chat/completions" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
--data-binary @01-request-first.json \
| tee 02-response-first.json
Dans le fichier de réponse, conservez uniquement les éléments nécessaires à la reconstruction :
{
"id": "redacted-response-id",
"choices": [
{
"finish_reason": "tool_calls",
"message": {
"role": "assistant",
"content": "Je vérifie l'état de la compilation.",
"reasoning_content": "[raisonnement supprimé]",
"tool_calls": [
{
"id": "call_redacted",
"type": "function",
"function": {
"name": "get_build_status",
"arguments": "{\"job_id\":\"job-17\"}"
}
}
]
}
}
]
}
Le finish_reason, l’identifiant de réponse et l’identifiant tool_calls sont utiles pour l’enquête. Ils ne remplacent pas le contrat du message assistant. La documentation DeepSeek décrit reasoning_content, content et tool_calls comme des champs distincts du message assistant. (api-docs.deepseek.com)
Pour vLLM, envoyez le même scénario à votre endpoint local ou privé, mais archivez également la commande de lancement. La recette DeepSeek V4 Flash montre notamment l’usage d’un parseur d’outil DeepSeek V4 et d’un parseur de raisonnement DeepSeek V4 ; ces options appartiennent au déploiement vLLM, pas à l’API officielle. (recipes.vllm.ai)
Réponse assistant : conserver la structure, pas le texte sensible
Le point le plus souvent mal diagnostiqué est la phrase « nous avons bien conservé la réponse ». Il faut distinguer trois niveaux :
- la réponse JSON reçue sur le réseau ;
- l’objet interne après désérialisation ;
- le message ajouté à l’historique avant le second appel.
Pour l’API officielle, le message assistant du tour d’outil doit être reconstruit avec ses champs pertinents :
{
"role": "assistant",
"content": "Je vérifie l'état de la compilation.",
"reasoning_content": "[raisonnement supprimé]",
"tool_calls": [
{
"id": "call_redacted",
"type": "function",
"function": {
"name": "get_build_status",
"arguments": "{\"job_id\":\"job-17\"}"
}
}
]
}
Ajoutez ensuite le résultat minimal :
{
"role": "tool",
"tool_call_id": "call_redacted",
"content": "{\"status\":\"success\"}"
}
Ne placez pas reasoning_content dans le message tool. Ne le transformez pas en content. Ne le recopiez pas sous le nom reasoning si la destination est l’API DeepSeek officielle.
Ajoutez des assertions avant l’envoi :
assert assistant["role"] == "assistant"
assert assistant.get("tool_calls")
assert assistant["tool_calls"][0]["id"] == tool_result["tool_call_id"]
assert "reasoning_content" in assistant
assert "content" in assistant
Si vous masquez le raisonnement dans les journaux, faites-le uniquement dans la copie destinée à la journalisation. La copie envoyée au serveur doit conserver la structure exigée par le endpoint. Le masquage des logs, la conversion en dictionnaire et la logique de confidentialité sont des couches applicatives ; ils ne modifient pas le contrat de l’API.
Troisième étape : fabriquer l’échec, puis le succès
Construisez deux demandes presque identiques. Cette paire est plus probante qu’un simple « cela fonctionne maintenant ».
Échantillon d’échec
{
"messages": [
{
"role": "user",
"content": "Vérifiez l'état de la compilation job-17."
},
{
"role": "assistant",
"content": "Je vérifie l'état de la compilation.",
"tool_calls": [
{
"id": "call_redacted",
"type": "function",
"function": {
"name": "get_build_status",
"arguments": "{\"job_id\":\"job-17\"}"
}
}
]
},
{
"role": "tool",
"tool_call_id": "call_redacted",
"content": "{\"status\":\"success\"}"
}
]
}
Échantillon conforme pour l’API officielle
{
"messages": [
{
"role": "user",
"content": "Vérifiez l'état de la compilation job-17."
},
{
"role": "assistant",
"content": "Je vérifie l'état de la compilation.",
"reasoning_content": "[raisonnement supprimé]",
"tool_calls": [
{
"id": "call_redacted",
"type": "function",
"function": {
"name": "get_build_status",
"arguments": "{\"job_id\":\"job-17\"}"
}
}
]
},
{
"role": "tool",
"tool_call_id": "call_redacted",
"content": "{\"status\":\"success\"}"
}
]
}
Envoyez d’abord l’échantillon d’échec, capturez le corps JSON complet de l’erreur, puis envoyez l’échantillon conforme. Si les deux résultats diffèrent, vous avez isolé un problème de contrat. Si les deux échouent, vérifiez le modèle, le mode de réflexion, les paramètres non pris en charge et le base_url.
Cette méthode explique pourquoi un premier appel réussi ne suffit pas. Le premier appel ne teste pas encore la capacité de votre client à rejouer l’assistant après un appel d’outil. C’est le second POST qui révèle la perte de champ, le mauvais nom ou le mauvais ordre.
API officielle et vLLM : comparaison sans règle de remplacement globale
La comparaison suivante sert à localiser la frontière d’adaptation. Elle ne constitue pas une règle selon laquelle un champ pourrait toujours remplacer l’autre.
| Question | API officielle DeepSeek | vLLM actuel |
|---|---|---|
| Champ de raisonnement documenté | reasoning_content |
reasoning |
| Message à rejouer après un outil | Assistant complet, dont reasoning_content pour les tours concernés |
Dépend de la version, du parseur et du template |
| Parsing des appels | Contrat API DeepSeek | Parseur activé au lancement |
| Compatibilité d’entrée | À vérifier dans le schéma DeepSeek ciblé | À vérifier dans le protocole OpenAI compatible et le code de version |
| Risque principal | Omission de reasoning_content entraînant 400 |
Confusion entre sortie structurée et champ d’entrée accepté |
La documentation vLLM actuelle précise que le parseur DeepSeek V4 extrait un champ reasoning et des appels d’outil au format structuré. Le code d’encodage vLLM traite également reasoning comme une donnée de message interne. Cela ne permet pas d’inférer que DeepSeek acceptera ce même nom dans un historique destiné à son API. (docs.vllm.ai)
Concevez donc un modèle interne neutre :
{
"role": "assistant",
"summary": "...",
"private_reasoning": "[redacted]",
"tool_calls": []
}
À la sortie :
- pour DeepSeek, mappez
private_reasoningversreasoning_contentseulement lorsque le contrat l’exige ; - pour vLLM, mappez-le vers
reasoninguniquement si la version et le template le prévoient ; - pour un endpoint sans raisonnement rejouable, refusez la conversion silencieuse et produisez une erreur explicite.
Une substitution globale de reasoning_content par reasoning peut réparer un test vLLM tout en cassant l’API officielle. L’adaptateur doit être unidirectionnel, associé à un base_url connu et couvert par des tests.
Questions fréquentes sur le diagnostic HTTP
La première erreur apparaît-elle toujours au deuxième tour ?
Non. Le deuxième tour est le cas le plus visible, mais une passerelle peut supprimer le champ dès la première désérialisation, puis l’erreur n’apparaître qu’après un nouvel appel. Journalisez donc la réponse réseau, l’objet interne et le JSON final séparément.
Faut-il conserver le raisonnement d’un tour sans outil ?
Ne généralisez pas la règle des tours sans outil aux tours avec outil. La documentation DeepSeek distingue ces deux situations : pour un tour qui effectue un appel d’outil, le raisonnement doit être renvoyé dans les requêtes suivantes. (api-docs.deepseek.com)
Que faire si vLLM renvoie une liste vide de tool_calls ?
Vérifiez le parseur d’appel, le parseur de raisonnement, l’activation de l’auto-sélection d’outil, le template de conversation et le format réellement généré par le modèle. Une réponse textuelle contenant une pseudo-invocation n’est pas équivalente à un tableau tool_calls structuré.
Dernière étape : transformer la reproduction en régression
Une fois l’origine identifiée, ne supprimez pas le payload d’échec. Il doit devenir un test négatif stable. Votre suite minimale doit contenir quatre scénarios :
- conversation sans outil ;
- appel d’un outil unique ;
- deux appels d’outil consécutifs dans le même tour ;
- bascule d’un endpoint à l’autre avec adaptation explicite.
Pour chaque scénario, vérifiez :
[ ] L’URL de base correspond à l’endpoint attendu
[ ] Le modèle envoyé correspond à la cible réellement testée
[ ] Le message assistant conserve les champs requis
[ ] Chaque tool_call_id correspond au message tool associé
[ ] L’ordre user → assistant → tool est conservé
[ ] Le corps d’erreur est archivé lorsqu’un 400 est attendu
[ ] Le payload final est comparé après sérialisation
[ ] L’échantillon ne contient aucune clé ni donnée privée
[ ] Les tests API officielle et vLLM restent indépendants
[ ] Une modification de parseur déclenche la suite complète
Le critère de correction n’est pas « tous les tests retournent 200 ». Il est plus précis : le cas conforme doit passer sur chaque endpoint prévu, le cas volontairement incomplet doit échouer de manière stable, et les journaux doivent montrer le champ qui a changé.
Pour vos tickets d’incident, joignez le base_url anonymisé, la version du serveur, les options de lancement, les cinq champs de réponse utiles et les deux payloads comparés. La documentation d’aide MacHTML peut servir de point de départ si vous devez préparer un environnement de test isolé, tandis que la console MacHTML convient à une exécution où le script HTTP, les journaux et les versions doivent rester séparés de votre poste de développement.
Quand remplacer le poste local par un environnement Mac isolé
Un poste local est acceptable pour une reproduction ponctuelle. Il devient moins fiable lorsque vous devez conserver deux endpoints, plusieurs versions de vLLM, des fichiers de payload anonymisés et une procédure de retour arrière. Les limites habituelles sont concrètes : environnement CUDA difficile à reproduire sur macOS, dépendances globales qui contaminent les essais et absence de séparation entre les tests d’un développeur et ceux de la plateforme.
Dans ce cas, louer un environnement Mac auprès de MacHTML peut être plus pertinent pour les tests d’intégration, les scripts HTTP, les projets audio ou vidéo qui doivent rester proches de l’écosystème Apple, et les validations qui nécessitent un poste propre pendant quelques jours. Ce n’est pas le meilleur choix pour un service de production à charge lourde et permanente, ni pour une équipe qui exige directement des accélérateurs NVIDIA ou des interfaces matérielles spécifiques. Pour un besoin temporaire de reproduction, de comparaison et de retour arrière, l’environnement isolé évite surtout de confondre un problème de contrat API avec un problème de machine.
Commencez par conserver vos deux chaînes HTTP indépendantes. Si votre machine ne peut pas garder cet état propre assez longtemps, utilisez un environnement Mac distant dédié, puis rejouez exactement le même paquet de tests avant et après chaque changement de framework, de parseur ou de modèle.
FAQ
Reproduisez vos tests API dans un environnement Mac fiable
Louez un Mac à distance avec MacHTML pour isoler vos scénarios HTTP et reproduire vos erreurs dans des conditions maîtrisées. Accédez à une machine dédiée depuis votre navigateur afin de vérifier chaque requête, chaque réponse et chaque étape d’un échange multitour. Profitez d’un environnement Mac prêt à l’emploi pour vos tests techniques, sans installer de SDK ni de framework superflu. Choisissez la formule MacHTML adaptée à vos besoins et lancez rapidement vos expérimentations, validations et débogages à distance.