Anthropic Messages, Chat Completions et Responses : choisir, convertir et comprendre les limites de compatibilité
Un HTTP 200 ne signifie pas qu’un agent est compatible. Trois cycles complets d’appel d’outil montrent les différences réelles entre Messages, Chat Completions et Responses, avec une méthode de migration, de test et de diagnostic.
Sommaire
Un même agent peut continuer à recevoir 200 après le remplacement de l’endpoint et des noms de champs, tout en cessant d’exécuter les outils, en renvoyant un JSON non conforme à la Schema ou en perdant soudain le contexte entre plusieurs tours. Cela ne signifie généralement pas que le modèle est « devenu moins performant », mais que l’application a traité trois protocoles distincts comme une seule interface.
Une migration réussie doit être vérifiée sur au moins trois niveaux :
- Le format est accepté : le serveur analyse la requête et renvoie un statut de succès.
- Le comportement est équivalent : les outils sont appelés, les résultats sont renvoyés correctement, le flux se termine complètement et le contexte multi-tour reste cohérent.
- Les capacités sont préservées : Schema stricte, état de reasoning natif, outils hébergés, sorties structurées et fonctions similaires ne sont ni ignorés ni dégradés silencieusement.
HTTP 200 ne prouve que le premier niveau. Une conversion simple suffit parfois pour une requête qui produit un seul bloc de texte. Dès qu’un agent utilise des outils, des arguments en streaming, un état multi-tour ou un modèle de reasoning, il faut valider toute la chaîne d’interaction.
Conclusion pratique : choisissez le protocole selon le client et les capacités requises, pas selon le nom du modèle
| Scénario | Point de départ le plus adapté | Pourquoi |
|---|---|---|
Une application existante utilise déjà de façon stable le SDK OpenAI et messages | Chat Completions | Les changements sont minimes et la boucle actuelle de messages et d’outils peut être conservée |
| Un nouvel agent OpenAI a besoin d’outils hébergés, d’Items typés ou d’une continuité d’état côté serveur | Responses | OpenAI le recommande actuellement pour les nouveaux projets et son éventail de fonctions d’agent est plus large |
| Claude Code, une application Claude native ou un flux dépendant de fonctions propres à Claude | Anthropic Messages | Les blocs de contenu, résultats d’outils, thinking et autres comportements suivent le contrat natif d’Anthropic |
| Une passerelle maison ou un routeur multi-modèle | Conserver un adaptateur distinct pour chaque protocole upstream | Un « JSON universel » unique ne peut pas représenter toutes les capacités natives sans perte |
OpenAI continue de prendre en charge Chat Completions. Une application stable n’a donc pas besoin d’être immédiatement réécrite uniquement parce qu’une interface plus récente existe. La migration est plus pertinente pour les nouveaux projets ou lorsque des capacités natives de Responses sont nécessaires. Anthropic Messages n’est pas non plus une interface OpenAI dont un champ aurait simplement été renommé messages : ses blocs de contenu, son transfert des résultats d’outils, ses événements de streaming et ses règles d’état constituent un contrat indépendant.
Différences essentielles entre les trois API
Dans cet article, Completions désigne Chat Completions, et non l’ancien endpoint /v1/completions.
| Dimension | OpenAI Chat Completions | OpenAI Responses | Anthropic Messages |
|---|---|---|---|
| Endpoint | /v1/chat/completions | /v1/responses | /v1/messages |
| Entrée principale | messages | Items dans input, avec prise en charge d’un message simple | messages, généralement avec un champ system distinct au niveau supérieur |
| Sortie principale | choices[].message | Items typés dans output[] | Blocs de contenu dans content[] |
| Définition des outils | tools[].function | name et parameters directement dans tools[] | input_schema dans tools[] |
| Arguments d’outil | function.arguments, chaîne JSON | arguments, chaîne JSON | tool_use.input, objet JSON |
| ID de corrélation | tool_calls[].id | call_id | tool_use.id |
| Retour du résultat | role: "tool" + tool_call_id | function_call_output + call_id | tool_result + tool_use_id dans un message user |
| État multi-tour | L’application rejoue l’historique des messages | Relecture des Items, previous_response_id ou Conversations | L’application rejoue les messages et blocs de contenu |
| Sortie structurée finale | response_format | text.format | output_config.format |
| Streaming | choices[].delta | Événements typés de Responses | Événements message/content block |
Le tableau peut donner l’impression qu’il suffit de renommer des champs. Les erreurs se produisent surtout lors de la deuxième requête : une fois l’appel d’outil produit par le modèle, comment l’application l’exécute-t-elle, quel ID doit-elle conserver et avec quel rôle et dans quel ordre renvoie-t-elle le résultat ? Les sections suivantes déroulent la même tâche sans effet secondaire dans les trois protocoles.
Exemple commun : consulter une offre de test
L’utilisateur demande :
Consultez l’offre
teamet indiquez si le dépassement peut être facturé à l’usage.
L’outil s’appelle get_plan_info. Il lit uniquement des données locales fixes et ne produit aucun effet secondaire externe, ce qui le rend adapté aux tests de migration de protocole.
Les données d’offre ci-dessous sont synthétiques et destinées à l’apprentissage. Elles ne représentent pas les offres, tarifs ou droits réels d’OpenAI, Anthropic ou BetterToken. Les trois séquences de requêtes et réponses illustrent la structure des protocoles et ne sont pas des traces d’exécutions réelles d’API.
L’outil côté application peut être écrit comme une fonction indépendante du protocole :
from __future__ import annotations
import json
from typing import Any
PLAN_FIXTURES: dict[str, dict[str, Any]] = {
"team": {
"plan_code": "team",
"display_name": "Team",
"billing_mode": "usage_based",
"included_requests": 10_000,
"overage_allowed": True,
"source_version": "fixture-2026-09-01",
}
}
def execute_tool(name: str, raw_arguments: str | dict[str, Any]) -> str:
"""Exécute l’outil pédagogique en lecture seule et renvoie une chaîne JSON directement transmissible au modèle."""
if isinstance(raw_arguments, str):
arguments = json.loads(raw_arguments)
elif isinstance(raw_arguments, dict):
arguments = raw_arguments
else:
raise TypeError("les arguments de l’outil doivent être une chaîne JSON ou un objet")
if name != "get_plan_info":
raise ValueError(f"outil inconnu : {name}")
if set(arguments) != {"plan_code"}:
raise ValueError("get_plan_info accepte uniquement plan_code")
plan_code = arguments["plan_code"]
if not isinstance(plan_code, str):
raise TypeError("plan_code doit être une chaîne")
plan = PLAN_FIXTURES.get(plan_code)
if plan is None:
return json.dumps(
{"ok": False, "error": "plan_not_found", "plan_code": plan_code},
ensure_ascii=False,
)
return json.dumps({"ok": True, "data": plan}, ensure_ascii=False)
Même si la requête active une Schema stricte, l’application doit conserver ses propres validations d’entrée. Le mode strict encadre les arguments générés par le modèle ; il ne remplace ni l’autorisation, ni la validation des valeurs, ni l’idempotence, ni les contrôles de sécurité métier.
Chat Completions : cycle complet d’appel d’outil
Première requête : demander au modèle de produire un appel d’outil
Les exemples ci-dessous utilisent l’endpoint officiel d’OpenAI pour illustrer le protocole. Avec un service compatible, remplacez Base URL, la méthode d’authentification et le Model ID conformément à la documentation du fournisseur.
curl https://api.openai.com/v1/chat/completions \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "YOUR_OPENAI_MODEL",
"messages": [
{
"role": "system",
"content": "Vous êtes un assistant chargé des offres. Répondez uniquement à partir des données renvoyées par l’outil ; ne faites aucune supposition."
},
{
"role": "user",
"content": "Consultez l’offre team et indiquez si le dépassement peut être facturé à l’usage."
}
],
"tools": [
{
"type": "function",
"function": {
"name": "get_plan_info",
"description": "Consulter des données de test fixes à partir du code de l’offre",
"strict": true,
"parameters": {
"type": "object",
"properties": {
"plan_code": {
"type": "string",
"enum": ["team"]
}
},
"required": ["plan_code"],
"additionalProperties": false
}
}
}
],
"tool_choice": "required",
"parallel_tool_calls": false
}'
L’application doit lire tool_calls dans le message assistant. La réponse suivante ne conserve que les champs nécessaires à la suite du cycle :
{
"choices": [
{
"message": {
"role": "assistant",
"content": null,
"tool_calls": [
{
"id": "call_plan_001",
"type": "function",
"function": {
"name": "get_plan_info",
"arguments": "{\"plan_code\":\"team\"}"
}
}
]
},
"finish_reason": "tool_calls"
}
]
}
Deux valeurs ne doivent pas être perdues :
tool_calls[0].id: renvoyez-le sans modification danstool_call_idlors de la deuxième requête.function.arguments: il s’agit d’une chaîne JSON. Analysez-la d’abord, puis appliquez votre propre validation de Schema et de règles métier.
Exécutez l’outil :
tool_result = execute_tool(
"get_plan_info",
"{\"plan_code\":\"team\"}",
)
Deuxième requête : renvoyer le résultat de l’outil au modèle
Chat Completions exige de conserver dans l’historique le message assistant contenant l’appel d’outil initial, puis d’ajouter un message de résultat avec role: "tool".
curl https://api.openai.com/v1/chat/completions \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "YOUR_OPENAI_MODEL",
"messages": [
{
"role": "system",
"content": "Vous êtes un assistant chargé des offres. Répondez uniquement à partir des données renvoyées par l’outil ; ne faites aucune supposition."
},
{
"role": "user",
"content": "Consultez l’offre team et indiquez si le dépassement peut être facturé à l’usage."
},
{
"role": "assistant",
"content": null,
"tool_calls": [
{
"id": "call_plan_001",
"type": "function",
"function": {
"name": "get_plan_info",
"arguments": "{\"plan_code\":\"team\"}"
}
}
]
},
{
"role": "tool",
"tool_call_id": "call_plan_001",
"content": "{\"ok\":true,\"data\":{\"plan_code\":\"team\",\"display_name\":\"Team\",\"billing_mode\":\"usage_based\",\"included_requests\":10000,\"overage_allowed\":true,\"source_version\":\"fixture-2026-09-01\"}}"
}
],
"tools": [
{
"type": "function",
"function": {
"name": "get_plan_info",
"description": "Consulter des données de test fixes à partir du code de l’offre",
"strict": true,
"parameters": {
"type": "object",
"properties": {
"plan_code": {
"type": "string",
"enum": ["team"]
}
},
"required": ["plan_code"],
"additionalProperties": false
}
}
}
]
}'
Exemple de message final représentatif :
{
"choices": [
{
"message": {
"role": "assistant",
"content": "L’offre Team permet la facturation à l’usage après dépassement. Les données de test incluent 10 000 requêtes et overage_allowed vaut true."
},
"finish_reason": "stop"
}
]
}
Si l’adaptateur convertit uniquement le premier tour utilisateur sans conserver les tool_calls du message assistant, ou place un mauvais ID dans tool_call_id, la deuxième requête ne prolonge plus le même appel d’outil.
Responses : cycle complet d’appel d’outil
Responses représente les messages, le reasoning, les appels et les résultats comme différents types d’Item. Il ne faut pas supposer que output[0] est toujours le texte final : le traitement doit dépendre du type de chaque Item.
Première requête : demander au modèle un Item function_call
curl https://api.openai.com/v1/responses \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "YOUR_OPENAI_MODEL",
"instructions": "Vous êtes un assistant chargé des offres. Répondez uniquement à partir des données renvoyées par l’outil ; ne faites aucune supposition.",
"input": "Consultez l’offre team et indiquez si le dépassement peut être facturé à l’usage.",
"tools": [
{
"type": "function",
"name": "get_plan_info",
"description": "Consulter des données de test fixes à partir du code de l’offre",
"strict": true,
"parameters": {
"type": "object",
"properties": {
"plan_code": {
"type": "string",
"enum": ["team"]
}
},
"required": ["plan_code"],
"additionalProperties": false
}
}
],
"tool_choice": "required",
"parallel_tool_calls": false,
"store": false
}'
Exemple représentatif d’Item d’appel d’outil :
{
"id": "resp_plan_001",
"object": "response",
"output": [
{
"type": "function_call",
"id": "fc_plan_001",
"call_id": "call_plan_001",
"name": "get_plan_info",
"arguments": "{\"plan_code\":\"team\"}",
"status": "completed"
}
]
}
Utilisez call_id pour corréler le résultat. id: "fc_plan_001" est l’ID de l’Item lui-même et ne doit pas remplacer call_id.
Exécutez l’outil :
tool_result = execute_tool(
"get_plan_info",
"{\"plan_code\":\"team\"}",
)
Deuxième requête : renvoyer un function_call_output
L’exemple suivant utilise une relecture manuelle sans état : instructions, l’entrée utilisateur initiale, l’appel d’outil et son résultat sont donc envoyés de nouveau.
curl https://api.openai.com/v1/responses \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "YOUR_OPENAI_MODEL",
"instructions": "Vous êtes un assistant chargé des offres. Répondez uniquement à partir des données renvoyées par l’outil ; ne faites aucune supposition.",
"input": [
{
"role": "user",
"content": "Consultez l’offre team et indiquez si le dépassement peut être facturé à l’usage."
},
{
"type": "function_call",
"call_id": "call_plan_001",
"name": "get_plan_info",
"arguments": "{\"plan_code\":\"team\"}"
},
{
"type": "function_call_output",
"call_id": "call_plan_001",
"output": "{\"ok\":true,\"data\":{\"plan_code\":\"team\",\"display_name\":\"Team\",\"billing_mode\":\"usage_based\",\"included_requests\":10000,\"overage_allowed\":true,\"source_version\":\"fixture-2026-09-01\"}}"
}
],
"tools": [
{
"type": "function",
"name": "get_plan_info",
"description": "Consulter des données de test fixes à partir du code de l’offre",
"strict": true,
"parameters": {
"type": "object",
"properties": {
"plan_code": {
"type": "string",
"enum": ["team"]
}
},
"required": ["plan_code"],
"additionalProperties": false
}
}
],
"store": false
}'
Exemple représentatif d’Item de sortie final :
{
"id": "resp_plan_002",
"object": "response",
"output": [
{
"type": "message",
"role": "assistant",
"content": [
{
"type": "output_text",
"text": "L’offre Team permet la facturation à l’usage après dépassement. Les données de test incluent 10 000 requêtes et overage_allowed vaut true."
}
]
}
]
}
Si vous choisissez la continuité d’état côté serveur, vous pouvez autoriser l’enregistrement de la première réponse puis utiliser la forme suivante dans la deuxième requête :
{
"model": "YOUR_OPENAI_MODEL",
"previous_response_id": "resp_plan_001",
"input": [
{
"type": "function_call_output",
"call_id": "call_plan_001",
"output": "{\"ok\":true,\"data\":{\"plan_code\":\"team\",\"overage_allowed\":true}}"
}
]
}
Un previous_response_id appartient au service upstream qui a créé la réponse. Il ne peut pas être transmis à un autre fournisseur pour poursuivre la conversation. Il ne rend pas non plus l’historique gratuit : la documentation actuelle d’OpenAI précise que les token d’entrée antérieurs de la chaîne restent facturés comme input.
Lorsqu’une réponse contient un reasoning Item, la relecture sans état doit aussi conserver l’Item correspondant comme l’exige la documentation. Il n’est pas possible de le supprimer au nom d’un « format uniforme » puis d’affirmer que le contexte de reasoning est resté équivalent.
Anthropic Messages : cycle complet d’appel d’outil
Messages représente l’appel par un bloc tool_use dans le contenu assistant et renvoie le résultat par un bloc tool_result dans le message user suivant. Les arguments sont déjà un objet, et non une chaîne JSON qui reste à analyser.
Première requête : demander à Claude de renvoyer tool_use
curl https://api.anthropic.com/v1/messages \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "content-type: application/json" \
-d '{
"model": "YOUR_CLAUDE_MODEL",
"max_tokens": 512,
"system": "Vous êtes un assistant chargé des offres. Répondez uniquement à partir des données renvoyées par l’outil ; ne faites aucune supposition.",
"messages": [
{
"role": "user",
"content": "Consultez l’offre team et indiquez si le dépassement peut être facturé à l’usage."
}
],
"tools": [
{
"name": "get_plan_info",
"description": "Consulter des données de test fixes à partir du code de l’offre",
"strict": true,
"input_schema": {
"type": "object",
"properties": {
"plan_code": {
"type": "string",
"enum": ["team"]
}
},
"required": ["plan_code"],
"additionalProperties": false
}
}
],
"tool_choice": {
"type": "tool",
"name": "get_plan_info"
}
}'
Forcer un outil précis dépend de la prise en charge par le modèle et la configuration choisis. Si le modèle cible ne le permet pas, utilisez auto et vérifiez dans l’application qu’un appel d’outil a réellement été renvoyé.
Exemple de réponse représentative :
{
"id": "msg_plan_001",
"type": "message",
"role": "assistant",
"content": [
{
"type": "tool_use",
"id": "toolu_plan_001",
"name": "get_plan_info",
"input": {
"plan_code": "team"
}
}
],
"stop_reason": "tool_use"
}
L’objet input peut être transmis directement à l’exécuteur de l’outil :
tool_result = execute_tool(
"get_plan_info",
{"plan_code": "team"},
)
Deuxième requête : placer tool_result dans le message user immédiatement suivant
curl https://api.anthropic.com/v1/messages \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "content-type: application/json" \
-d '{
"model": "YOUR_CLAUDE_MODEL",
"max_tokens": 512,
"system": "Vous êtes un assistant chargé des offres. Répondez uniquement à partir des données renvoyées par l’outil ; ne faites aucune supposition.",
"messages": [
{
"role": "user",
"content": "Consultez l’offre team et indiquez si le dépassement peut être facturé à l’usage."
},
{
"role": "assistant",
"content": [
{
"type": "tool_use",
"id": "toolu_plan_001",
"name": "get_plan_info",
"input": {
"plan_code": "team"
}
}
]
},
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": "toolu_plan_001",
"content": "{\"ok\":true,\"data\":{\"plan_code\":\"team\",\"display_name\":\"Team\",\"billing_mode\":\"usage_based\",\"included_requests\":10000,\"overage_allowed\":true,\"source_version\":\"fixture-2026-09-01\"}}"
}
]
}
],
"tools": [
{
"name": "get_plan_info",
"description": "Consulter des données de test fixes à partir du code de l’offre",
"strict": true,
"input_schema": {
"type": "object",
"properties": {
"plan_code": {
"type": "string",
"enum": ["team"]
}
},
"required": ["plan_code"],
"additionalProperties": false
}
}
]
}'
Exemple de réponse finale représentative :
{
"id": "msg_plan_002",
"type": "message",
"role": "assistant",
"content": [
{
"type": "text",
"text": "L’offre Team permet la facturation à l’usage après dépassement. Les données de test incluent 10 000 requêtes et overage_allowed vaut true."
}
],
"stop_reason": "end_turn"
}
Messages impose un ordre précis : tool_result doit suivre immédiatement le message assistant contenant le tool_use correspondant. Si un tour assistant produit plusieurs appels d’outils côté client, tous les blocs de résultat doivent être renvoyés dans le message user suivant et chacun doit être corrélé par tool_use_id. Si ce même message user contient aussi du texte normal, les blocs de résultat doivent précéder le texte.
Ce qui se mappe directement et ce qui entraîne nécessairement des pertes
| Capacité | Évaluation de la conversion | Traitement correct |
|---|---|---|
| Texte utilisateur ordinaire | Se mappe généralement directement | Conserver le texte, l’ordre et les types multimodaux, pas uniquement les chaînes visibles |
| Schema de fonction de base | Peut être remodelée | Convertir entre function.parameters, parameters de Responses et input_schema de Messages, puis revalider le sous-ensemble de JSON Schema pris en charge |
| Arguments d’outil | Le type doit être converti | Les deux interfaces OpenAI renvoient généralement des chaînes JSON ; Messages renvoie un objet. Normaliser, analyser et valider avant la logique métier |
| ID d’appel | Préserver la sémantique sans réutiliser les namespaces | Conserver un canonical call ID interne avec l’ID upstream original, puis renvoyer le champ propre au protocole |
| Appels parallèles | Pris en charge en principe, mais jamais corrélés par position dans un tableau | Associer chaque résultat par tool_call_id, call_id ou tool_use_id |
| Instructions system/developer | Conversion potentiellement avec pertes | Distinguer portée globale, phase de conversation et tour unique ; dégrader ou refuser explicitement si le protocole cible ne peut pas exprimer la portée d’origine |
| Sortie structurée finale | Champs non interchangeables mécaniquement | Chat utilise response_format, Responses text.format et Messages output_config.format |
| Arguments d’outil en streaming | Parser propre au protocole nécessaire | Accumuler les fragments par événement et ID d’appel, puis analyser le JSON uniquement après l’événement de fin |
| État serveur multi-tour | Aucun équivalent universel | Les ID comme previous_response_id sont liés à l’upstream d’origine ; entre upstreams, rejouer le contexte visible ou utiliser un sticky routing |
| État thinking/reasoning | Généralement impossible à convertir sans perte | Conserver les Items opaques, thinking blocks, signatures ou contenus chiffrés exactement comme l’exige le protocole natif ; ne jamais les fabriquer |
| Outils hébergés | Souvent sans équivalent direct | Déclarer séparément la prise en charge et les alternatives pour web search, file search, computer use, server tools et fonctions similaires |
| Génération de plusieurs candidats | Peut ne pas avoir d’équivalent | Ne pas supposer que n de Chat Completions se mappe directement à Responses ; lancer plusieurs requêtes côté application ou changer le comportement produit |
L’abstraction interne la plus fiable pour une passerelle n’est donc pas un immense objet regroupant tous les champs possibles. Il faut modéliser séparément les messages, la portée des instructions, les définitions d’outils, les appels, les résultats, les handles d’état, les événements de streaming et l’état natif opaque. Lorsqu’une capacité ne peut pas être exprimée, renvoyez explicitement « non pris en charge » ou « conversion avec pertes » au lieu de supprimer silencieusement le champ.
strict, response_format et text.format ne résolvent pas le même problème
Une confusion fréquente consiste à traiter « des arguments d’outil valides » et « une réponse finale dans une forme JSON imposée » comme une seule fonction.
| Objectif | Chat Completions | Responses | Anthropic Messages |
|---|---|---|---|
| Contraindre les arguments d’appel | tools[].function.strict | tools[].strict | tools[].strict |
| Contraindre la sortie finale | response_format | text.format | output_config.format |
Le strict d’un outil encadre la manière dont le modèle appelle la fonction. La sortie structurée finale encadre le contenu remis à l’utilisateur. Un agent peut avoir besoin des deux : appeler d’abord un outil avec des arguments stricts, puis renvoyer le résultat final selon une JSON Schema fixe.
La documentation actuelle d’OpenAI contient également une différence de valeur par défaut facile à manquer :
- Les appels de fonction dans Chat Completions sont non stricts par défaut.
- Lorsque
strictest omis dans Responses, le service tente de normaliser la Schema en mode strict. En cas d’incompatibilité, il peut revenir au mode non strict et afficherstrict: falsedans la définition d’outil analysée.
Pour rendre l’intention explicite et éviter de dépendre des valeurs par défaut propres à chaque interface, les requêtes de production doivent définir volontairement strict: true ou strict: false. Une Schema stricte doit aussi respecter les exigences correspondantes, par exemple interdire les propriétés supplémentaires et déclarer tous les champs obligatoires.
Plus important encore : une couche de compatibilité peut accepter un champ sans appliquer sa contrainte. La documentation officielle d’Anthropic sur la compatibilité avec le SDK OpenAI indique que, dans cette couche précise, des champs comme function strict, response_format et reasoning_effort sont ignorés, et que la plupart des champs non pris en charge ne déclenchent pas d’erreur. Une requête peut donc renvoyer 200 alors que la Schema ou le réglage de reasoning n’a jamais été appliqué.
Cela ne signifie pas que Messages natif ne possède pas de capacités équivalentes. Messages natif prend en charge les entrées d’outil strictes et utilise output_config.format pour le JSON final. Le diagnostic doit commencer par une question : appelez-vous Messages natif ou une couche compatible OpenAI ?
system, developer et la portée des instructions ne se préservent pas par simple concaténation
Les interfaces de style OpenAI autorisent plusieurs rôles dans l’historique, tandis que Responses expose également instructions. Anthropic Messages utilise historiquement un champ system au niveau supérieur. En septembre 2026, certains modèles actuels acceptent aussi role: "system" au milieu d’une conversation, mais pas tous, et des contraintes de position et d’ordre avec les outils s’appliquent.
Parallèlement, la couche de compatibilité d’Anthropic avec le SDK OpenAI collecte les messages system/developer, les joint avec des sauts de ligne et les remonte en une seule instruction system au début. La requête devient utilisable, mais son calendrier et sa portée changent. Une instruction developer destinée à ne s’appliquer qu’à partir du huitième tour peut alors modifier la sémantique des sept premiers tours.
Un adaptateur plus sûr distingue d’abord trois portées en interne :
- Instructions globales : valables pendant toute la conversation.
- Instructions de phase : prennent effet à partir d’un tour déterminé.
- Instructions de tour unique : contrôlent uniquement la tâche actuelle.
Ne mappez une instruction que si le protocole cible peut exprimer la même portée. Sinon, choisissez une stratégie explicite : conserver la requête sur un modèle qui prend en charge la fonction, dégrader l’instruction tout en enregistrant l’écart, ou refuser la migration. La concaténation silencieuse demande peu de code, mais elle produit souvent « la requête a réussi, le comportement a changé ».
Le streaming nécessite une machine à états, pas une simple concaténation de token texte
Les trois interfaces prennent en charge le streaming, mais leurs événements ne sont pas équivalents :
- Chat Completions accumule généralement le texte et les fragments de
tool_callsdepuischoices[].delta. - Responses émet des événements typés comme
response.output_text.delta,response.function_call_arguments.delta,response.function_call_arguments.done,response.completedeterror. - Messages utilise
message_start,content_block_start,content_block_delta,content_block_stop,message_deltaetmessage_stop; les arguments arrivent par fragments viainput_json_delta.partial_json.
Les arguments peuvent être découpés ainsi :
{"plan_
code":"te
am"}
Aucun de ces fragments n’est un JSON valide à lui seul. Accumulez-les par ID d’appel ou indice de bloc de contenu, puis analysez-les uniquement après l’événement de fin des arguments :
from __future__ import annotations
import json
from collections import defaultdict
from typing import Any
class ToolArgumentAssembler:
def __init__(self) -> None:
self._buffers: dict[str, list[str]] = defaultdict(list)
def add_delta(self, call_id: str, fragment: str) -> None:
self._buffers[call_id].append(fragment)
def finish(self, call_id: str) -> dict[str, Any]:
if call_id not in self._buffers:
raise KeyError(f"call_id inconnu : {call_id}")
raw = "".join(self._buffers.pop(call_id))
value = json.loads(raw)
if not isinstance(value, dict):
raise TypeError("les arguments de l’outil doivent être décodés sous forme d’objet")
return value
def discard(self, call_id: str) -> None:
self._buffers.pop(call_id, None)
L’adaptateur doit également enregistrer un état terminal explicite :
created -> receiving -> completed
\-> failed
\-> disconnected
disconnected n’est pas completed. Anthropic Messages peut émettre un event: error dans le flux après que la connexion HTTP a déjà réussi ; Responses possède également des événements d’erreur séparés. Regarder uniquement le statut HTTP initial ou traiter la fermeture de connexion comme une fin normale peut tronquer les arguments d’outil ou la réponse finale.
Le parser d’événements doit aussi tolérer les types inconnus : les journaliser et ignorer ceux qui n’affectent pas la capacité actuelle, plutôt que de faire échouer tout le client lorsqu’un serveur ajoute un événement.
L’état multi-tour et l’état de reasoning ne peuvent pas être fabriqués
Chat Completions et les flux Messages traditionnels reposent généralement sur la relecture de l’historique par l’application. Responses peut aussi maintenir un état serveur via previous_response_id ou Conversations. Leur notion du « tour précédent » n’est pas interchangeable.
Lorsqu’une passerelle reçoit un ID d’état, elle ne dispose que de trois stratégies valides :
- Sticky routing : envoyer les requêtes suivantes au même upstream que celui ayant créé l’état.
- Relecture complète : renvoyer tous les messages, appels, résultats et éléments d’état natif pouvant légalement être rejoués.
- Refus explicite : si l’upstream cible ne peut pas continuer, renvoyer une erreur diagnosticable et permettre au client de recommencer la conversation.
Ne transmettez pas un previous_response_id OpenAI à Anthropic et ne présentez pas l’ID interne d’une conversation de passerelle comme un handle d’état compris par un autre fournisseur.
L’état de reasoning ne se résout pas non plus en renommant des champs :
- En mode sans état ou avec certaines politiques de conservation, Responses peut renvoyer des reasoning Items chiffrés qui doivent être réinjectés dans une requête ultérieure.
- Les flux thinking d’Anthropic peuvent contenir des thinking blocks, des signatures ou d’autres états opaques. Avec les outils et les conversations multi-tours, ils doivent être préservés selon la documentation native.
- En septembre 2026, le mode manuel Anthropic
thinking.type: "enabled"avecbudget_tokensest obsolète sur les modèles de génération 4.6 et refusé par les générations 4.7 et ultérieures ; les modèles plus récents utilisent adaptive thinking et le contrôle effort correspondant.
Il est donc impossible d’établir une règle permanente qui assimile OpenAI reasoning_effort à Anthropic budget_tokens. Une description correcte des capacités doit préciser le modèle cible, son mode thinking actuel et le comportement de repli lorsque ce mode n’est pas disponible.
Appels d’outils parallèles : corréler par ID, jamais par position
Un modèle peut demander plusieurs outils au cours d’un même tour. Les durées d’exécution varient et les résultats peuvent revenir dans un ordre différent. L’adaptateur doit maintenir une relation de ce type :
canonical_call_id
-> provider
-> provider_call_id
-> tool_name
-> validated_arguments
-> execution_status
-> result
Lors du retour des résultats :
- Chat Completions crée un message
role: "tool"par résultat avec letool_call_idcorrespondant. - Responses crée un Item
function_call_outputpar résultat avec lecall_idcorrespondant. - Messages place les blocs
tool_resultcorrespondants dans le tour user immédiatement suivant et renseigne chaquetool_use_id.
Pendant les tests de migration, commencez avec parallel_tool_calls: false, validez le chemin à un seul outil, puis activez le parallélisme. En production, les outils à effets de bord — envoi d’e-mail, facturation ou création de ressources — nécessitent également des clés d’idempotence. Un retry réseau, une rupture de flux ou une relecture upstream peut livrer de nouveau le même appel sémantique ; le texte généré par le modèle ne suffit pas à savoir s’il a déjà été exécuté.
Pourquoi HTTP 200 ne suffit pas à valider la compatibilité
Un test de migration utile doit couvrir au moins les parcours suivants :
| Test | Critère de réussite |
|---|---|
| Texte ordinaire | Le contenu est lisible et la portée system/developer se comporte comme prévu |
| Appel d’outil unique | Nom, arguments, ID, résultat et réponse finale forment un cycle complet |
| Appels parallèles | Chaque résultat est associé par ID, sans croisement ni perte |
| Arguments en streaming | Les fragments sont assemblés en entier et le JSON est analysable après la fin |
| Schema stricte d’outil | Les champs et types invalides sont rejetés comme prévu ou dégradés explicitement |
| Sortie structurée finale | La réponse respecte la Schema, et ne se contente pas de « ressembler à du JSON » |
| Erreur d’exécution | Le modèle reçoit une erreur structurée et ne boucle pas ni n’invente une réussite |
| Continuité multi-tour | Le deuxième tour peut citer les faits du premier, avec des règles d’état explicites |
| reasoning/thinking | Le mode déclaré fonctionne et l’état natif n’est ni supprimé ni fabriqué |
| Erreur dans le flux et déconnexion | Le client distingue fin normale, échec et interruption de connexion |
| Erreur API contrôlée | Type d’erreur, request ID et politique de retry restent diagnosticables |
Utilisez des entrées et fixtures fixes, et enregistrez séparément pour chaque protocole :
- si le résultat métier final est équivalent ;
- si l’appel d’outil et le retour de résultat sont complets ;
- les latences P50 et P95 ;
- les usages input, output et cache ;
- le type d’erreur, le request ID et l’état terminal ;
- les capacités explicitement dégradées.
Ne journalisez pas les API Keys, les prompts sensibles complets ni les sorties privées. Au minimum, les logs d’erreur doivent conserver HTTP status, upstream error type/code, un message court, request ID, endpoint, protocole, Model ID et état terminal du flux. Sinon, model_not_found, une absence de droits et un chemin incompatible peuvent tous devenir un 400 impossible à diagnostiquer.
Diagnostic par symptôme : où l’agent s’est-il cassé ?
| Symptôme | Cause fréquente | Diagnostic et correction |
|---|---|---|
La requête renvoie 200, mais le modèle n’appelle jamais d’outil | La définition n’a pas été envoyée, tool_choice est ignoré, le modèle ne prend pas en charge les outils ou le prompt est insuffisant | Afficher la requête sortante finale ; vérifier le modèle et la couche de compatibilité ; n’exposer qu’un outil en lecture seule et forcer ou demander explicitement son utilisation |
| Le modèle renvoie un appel, mais l’application ne l’exécute pas | L’application lit encore un ancien champ, par exemple uniquement message.content | Lire tool_calls, l’Item function_call ou le bloc tool_use selon le protocole |
| Le JSON des arguments ne s’analyse pas | Un fragment de streaming a été traité comme un JSON complet, ou un objet a été analysé une seconde fois comme chaîne | Attendre l’événement de fin ; déterminer d’abord s’il s’agit d’une chaîne ou d’un objet |
Des champs supplémentaires apparaissent malgré strict | La couche compatible l’ignore, la Schema ne respecte pas le mode strict ou la requête ne cible pas l’endpoint natif | Vérifier l’endpoint final et la documentation ; définir strict explicitement ; ajouter un test de régression violant volontairement la Schema |
| La deuxième requête indique qu’un résultat manque | L’ID ne correspond pas ou le premier Item assistant/tool n’a pas été conservé | Stocker l’appel upstream et son ID sans modification ; renvoyer le résultat immédiatement à l’endroit imposé par le protocole |
Messages renvoie tool_use ids ... without tool_result | tool_result ne suit pas immédiatement l’appel ou du texte ordinaire a été inséré avant | Placer tous les blocs tool_result correspondants dans le message user suivant et avant le texte facultatif |
| Le streaming se bloque ou ne renvoie qu’une moitié d’arguments | Le client n’attend que la fin du texte et ne traite pas les états terminaux des arguments et erreurs | Implémenter une machine d’événements distincte par protocole et distinguer completed, failed, error et disconnected |
| Le deuxième tour oublie le premier | L’historique, les appels ou result Items ont été omis ; ou previous_response_id appartient à un autre upstream | Rejouer tout le contexte visible ou maintenir le sticky routing ; ne jamais transférer d’ID d’état entre fournisseurs |
| Une instruction system s’applique trop tôt après un changement d’interface | La couche compatible a remonté une instruction system/developer intermédiaire au début | Modéliser la portée ; dégrader explicitement ou conserver le protocole natif lorsque le mappage sans perte est impossible |
| Un outil s’exécute deux fois | Retry de requête, relecture après déconnexion ou absence d’idempotence | Utiliser des outils en lecture seule pendant les tests ; dériver la clé d’idempotence des outils à effets de bord d’un canonical call ID |
| Le résultat final est en JSON, mais des champs manquent parfois | Le prompt demande seulement « renvoyer du JSON » sans activer la sortie structurée | Utiliser response_format, text.format ou output_config.format selon l’interface, puis revalider dans l’application |
Séquence de migration plus sûre
- Identifiez le protocole réellement envoyé par le client. Ne le déduisez pas du nom du modèle. Relevez l’endpoint complet, la méthode SDK, les champs de premier niveau et les types d’événements du flux.
- Listez les comportements à préserver. Au minimum : outils, appels parallèles, Schema stricte, sortie structurée finale, état multi-tour, streaming et thinking/reasoning.
- Privilégiez le protocole natif. Lorsqu’une fonction peut être réalisée directement par Messages ou Responses natif, évitez une couche compatible supplémentaire.
- Construisez une matrice de capacités de conversion. Classez chaque fonction comme totalement prise en charge, prise en charge avec pertes ou non prise en charge, et exposez le résultat au caller.
- Exécutez un cycle complet de deux requêtes avec un fixture sans effet secondaire. Ne vous arrêtez pas à une requête qui renvoie du texte : exécutez l’outil et renvoyez son résultat.
- Testez ensuite le parallélisme, le streaming et les erreurs. N’activez les outils à effets de bord et le trafic réel qu’après validation du chemin normal.
- Augmentez progressivement le trafic et comparez les métriques. Surveillez exactitude, latence, usage, erreurs et doubles exécutions, pas uniquement le taux de succès HTTP.
Choisir le point d’entrée BetterToken correspondant
BetterToken propose des chemins de connexion différents selon les clients. Le protocole reste déterminé par le wire contract réellement utilisé :
- Chat Completions : l’URL complète est
https://www.bettertoken.ai/v1/chat/completions. Pour les SDK ou outils qui ajoutent automatiquement le chemin, Base URL est généralementhttps://www.bettertoken.ai/v1. Consultez la référence Chat Completions API. - Codex / Responses : la documentation Codex actuelle utilise
base_url = "https://www.bettertoken.ai/v1"etwire_api = "responses"; Codex ajoute lui-même/responses. Consultez le guide Codex. - Claude Code / Messages : la documentation actuelle utilise
ANTHROPIC_BASE_URL=https://bettertoken.ai, sans ajouter/v1à Base URL ; le client ajoute/v1/messages. Consultez le guide Claude Code.
Le même Dashboard, la même API Key ou le même nom de modèle ne transforment pas trois protocoles en un format unique. Pour un outil existant, choisissez le protocole qu’il attend. Pour un agent personnalisé, validez les capacités requises avec les cycles complets et la matrice d’acceptation de cet article.
Questions fréquentes
OpenAI-compatible signifie-t-il une copie complète de l’API OpenAI ?
Non. Cela signifie généralement que certains endpoints et structures peuvent être appelés par des clients de style OpenAI. Modèles, paramètres, événements de streaming, outils, sorties structurées, outils hébergés et sémantique des erreurs doivent toujours être vérifiés séparément.
Suffit-il de remplacer Base URL et API Key ?
Parfois, pour des requêtes texte simples utilisant déjà le même wire contract. Un agent à outils exige encore de vérifier les définitions, le retour du résultat dans la deuxième requête, les événements de streaming, la Schema stricte, l’état et les erreurs. Si le client attend Responses, /chat/completions seul ne suffit pas ; s’il attend Messages, un endpoint de style OpenAI ne s’adapte pas automatiquement.
Un adaptateur universel peut-il convertir les trois protocoles ?
Il peut couvrir le texte ordinaire et une partie de la boucle de fonctions, mais ne doit pas promettre une prise en charge intégrale sans perte. État géré par le fournisseur, outils hébergés, état thinking/reasoning opaque, certaines portées system et capacités propres au modèle n’ont souvent aucun équivalent universel. L’adaptateur doit exposer une matrice de capacités et les dégradations.
Pourquoi les tests unitaires passent-ils alors que l’agent réel échoue ?
De nombreux tests simulent uniquement la première réponse du modèle. Ils ne vérifient ni le retour de résultat dans la deuxième requête, ni les appels parallèles, ni les fragments en streaming, ni la continuité d’état. Étendez le test à « requête utilisateur → appel d’outil par le modèle → exécution dans l’application → retour du résultat → réponse finale » pour révéler les vrais problèmes de protocole.
Faut-il migrer d’abord Chat Completions ou Responses ?
Une application stable sur Chat Completions peut continuer à fonctionner et migrer fonction par fonction selon la valeur métier. Un nouvel agent OpenAI, ou un agent ayant explicitement besoin d’Items typés, d’outils hébergés ou de l’état Responses, gagne à démarrer directement sur Responses. Les critères sont les capacités et le coût de migration, pas l’impression de nouveauté donnée par le nom de l’interface.