Invitez et gagnez

Fonctionnement des récompenses

Partagez votre lien. Lorsqu’un ami s’inscrit avec ce lien et recharge son solde, vous recevez la récompense affichée sur ses recharges ultérieures.

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 :

  1. Le format est accepté : le serveur analyse la requête et renvoie un statut de succès.
  2. 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.
  3. 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énarioPoint de départ le plus adaptéPourquoi
Une application existante utilise déjà de façon stable le SDK OpenAI et messagesChat CompletionsLes 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é serveurResponsesOpenAI 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 à ClaudeAnthropic MessagesLes 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èleConserver un adaptateur distinct pour chaque protocole upstreamUn « 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.

DimensionOpenAI Chat CompletionsOpenAI ResponsesAnthropic Messages
Endpoint/v1/chat/completions/v1/responses/v1/messages
Entrée principalemessagesItems dans input, avec prise en charge d’un message simplemessages, généralement avec un champ system distinct au niveau supérieur
Sortie principalechoices[].messageItems typés dans output[]Blocs de contenu dans content[]
Définition des outilstools[].functionname et parameters directement dans tools[]input_schema dans tools[]
Arguments d’outilfunction.arguments, chaîne JSONarguments, chaîne JSONtool_use.input, objet JSON
ID de corrélationtool_calls[].idcall_idtool_use.id
Retour du résultatrole: "tool" + tool_call_idfunction_call_output + call_idtool_result + tool_use_id dans un message user
État multi-tourL’application rejoue l’historique des messagesRelecture des Items, previous_response_id ou ConversationsL’application rejoue les messages et blocs de contenu
Sortie structurée finaleresponse_formattext.formatoutput_config.format
Streamingchoices[].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 team et 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 dans tool_call_id lors 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 conversionTraitement correct
Texte utilisateur ordinaireSe mappe généralement directementConserver le texte, l’ordre et les types multimodaux, pas uniquement les chaînes visibles
Schema de fonction de basePeut être remodeléeConvertir entre function.parameters, parameters de Responses et input_schema de Messages, puis revalider le sous-ensemble de JSON Schema pris en charge
Arguments d’outilLe type doit être convertiLes 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’appelPréserver la sémantique sans réutiliser les namespacesConserver un canonical call ID interne avec l’ID upstream original, puis renvoyer le champ propre au protocole
Appels parallèlesPris en charge en principe, mais jamais corrélés par position dans un tableauAssocier chaque résultat par tool_call_id, call_id ou tool_use_id
Instructions system/developerConversion potentiellement avec pertesDistinguer 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 finaleChamps non interchangeables mécaniquementChat utilise response_format, Responses text.format et Messages output_config.format
Arguments d’outil en streamingParser propre au protocole nécessaireAccumuler les fragments par événement et ID d’appel, puis analyser le JSON uniquement après l’événement de fin
État serveur multi-tourAucun équivalent universelLes 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/reasoningGénéralement impossible à convertir sans perteConserver les Items opaques, thinking blocks, signatures ou contenus chiffrés exactement comme l’exige le protocole natif ; ne jamais les fabriquer
Outils hébergésSouvent sans équivalent directDé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 candidatsPeut ne pas avoir d’équivalentNe 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.

ObjectifChat CompletionsResponsesAnthropic Messages
Contraindre les arguments d’appeltools[].function.stricttools[].stricttools[].strict
Contraindre la sortie finaleresponse_formattext.formatoutput_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 strict est 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 afficher strict: false dans 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_calls depuis choices[].delta.
  • Responses émet des événements typés comme response.output_text.delta, response.function_call_arguments.delta, response.function_call_arguments.done, response.completed et error.
  • Messages utilise message_start, content_block_start, content_block_delta, content_block_stop, message_delta et message_stop ; les arguments arrivent par fragments via input_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 :

  1. Sticky routing : envoyer les requêtes suivantes au même upstream que celui ayant créé l’état.
  2. Relecture complète : renvoyer tous les messages, appels, résultats et éléments d’état natif pouvant légalement être rejoués.
  3. 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" avec budget_tokens est 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 le tool_call_id correspondant.
  • Responses crée un Item function_call_output par résultat avec le call_id correspondant.
  • Messages place les blocs tool_result correspondants dans le tour user immédiatement suivant et renseigne chaque tool_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 :

TestCritère de réussite
Texte ordinaireLe contenu est lisible et la portée system/developer se comporte comme prévu
Appel d’outil uniqueNom, arguments, ID, résultat et réponse finale forment un cycle complet
Appels parallèlesChaque résultat est associé par ID, sans croisement ni perte
Arguments en streamingLes fragments sont assemblés en entier et le JSON est analysable après la fin
Schema stricte d’outilLes champs et types invalides sont rejetés comme prévu ou dégradés explicitement
Sortie structurée finaleLa réponse respecte la Schema, et ne se contente pas de « ressembler à du JSON »
Erreur d’exécutionLe modèle reçoit une erreur structurée et ne boucle pas ni n’invente une réussite
Continuité multi-tourLe deuxième tour peut citer les faits du premier, avec des règles d’état explicites
reasoning/thinkingLe mode déclaré fonctionne et l’état natif n’est ni supprimé ni fabriqué
Erreur dans le flux et déconnexionLe client distingue fin normale, échec et interruption de connexion
Erreur API contrôléeType 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ômeCause fréquenteDiagnostic et correction
La requête renvoie 200, mais le modèle n’appelle jamais d’outilLa 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 insuffisantAfficher 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 pasL’application lit encore un ancien champ, par exemple uniquement message.contentLire tool_calls, l’Item function_call ou le bloc tool_use selon le protocole
Le JSON des arguments ne s’analyse pasUn fragment de streaming a été traité comme un JSON complet, ou un objet a été analysé une seconde fois comme chaîneAttendre 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é strictLa couche compatible l’ignore, la Schema ne respecte pas le mode strict ou la requête ne cible pas l’endpoint natifVé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 manqueL’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_resulttool_result ne suit pas immédiatement l’appel ou du texte ordinaire a été inséré avantPlacer 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’argumentsLe client n’attend que la fin du texte et ne traite pas les états terminaux des arguments et erreursImplémenter une machine d’événements distincte par protocole et distinguer completed, failed, error et disconnected
Le deuxième tour oublie le premierL’historique, les appels ou result Items ont été omis ; ou previous_response_id appartient à un autre upstreamRejouer 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’interfaceLa couche compatible a remonté une instruction system/developer intermédiaire au débutModé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 foisRetry de requête, relecture après déconnexion ou absence d’idempotenceUtiliser 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 parfoisLe prompt demande seulement « renvoyer du JSON » sans activer la sortie structuréeUtiliser response_format, text.format ou output_config.format selon l’interface, puis revalider dans l’application

Séquence de migration plus sûre

  1. 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.
  2. Listez les comportements à préserver. Au minimum : outils, appels parallèles, Schema stricte, sortie structurée finale, état multi-tour, streaming et thinking/reasoning.
  3. 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.
  4. 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.
  5. 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.
  6. 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.
  7. 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éralement https://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" et wire_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.

Prêt à optimiser votre workflow LLM ?

Connectez vos modèles via une API unique, gérez les clés et maîtrisez vos dépenses d’IA.

Commencer gratuitement