Migrer Chat Completions vers l'API Responses
Un guide pas à pas pour porter du code OpenAI Chat Completions vers l'API Responses : éléments d'entrée, analyse de sortie, état, outils, streaming et plan de retour arrière.
Migrer de Chat Completions vers l'API Responses est surtout un travail de renommage et de remise en forme : POST /v1/chat/completions devient POST /v1/responses, le tableau messages devient input, le prompt système devient instructions, et vous lisez response.output_text au lieu de completion.choices[0].message.content. Les parties qui font trébucher sont celles qui changent le comportement plutôt que les noms — les éléments de sortie, la gestion d'état, la liaison des outils et les champs de sortie structurée. Ce guide parcourt la migration dans l'ordre où vous devriez réellement la mener, avec un plan de retour arrière afin que vous puissiez déplacer un endpoint à la fois plutôt que de tout réécrire d'un coup.
Vous n'êtes pas obligé de migrer. Chat Completions reste pris en charge. Migrez lorsque vous voulez une continuité de raisonnement, un état de conversation géré par le serveur, des outils natifs, ou la sortie typée plus propre dont bénéficient les boucles d'agent. Si vous hésitez encore, lisez d'abord API Responses ou Chat Completions.
Étape 0 : établir une base de référence contre laquelle comparer
Avant de toucher au code, capturez ce à quoi ressemble le « correct » sur votre endpoint actuel. Envoyez quelques prompts représentatifs via Chat Completions et enregistrez le texte de réponse, l'usage de jetons et la latence. Faites-les transiter par votre passerelle afin que les traces atterrissent dans ses journaux d'usage. Vous rejouerez les mêmes prompts contre Responses et comparerez. Les migrations qui sautent cette étape se transforment en devinettes dès que la sortie diffère.
Étape 1 : renommer l'endpoint et déplacer le prompt système
La plus petite migration fonctionnelle tient en trois modifications. Pointez le SDK vers la méthode Responses, déplacez tout message system dans instructions, et repliez un unique tour utilisateur dans input.
Avant :
completion = client.chat.completions.create(
model="gpt-5.6",
messages=[
{"role": "system", "content": "You are a release-notes summarizer."},
{"role": "user", "content": "Summarize the changelog below:\n" + changelog},
],
)
text = completion.choices[0].message.content
Après :
response = client.responses.create(
model="gpt-5.6",
instructions="You are a release-notes summarizer.",
input="Summarize the changelog below:\n" + changelog,
)
text = response.output_text
input accepte soit une simple chaîne (comme ci-dessus), soit un tableau d'éléments typés lorsque vous avez besoin d'un historique multi-tours ou de résultats d'outils. Commencez par la forme chaîne pour obtenir un test au vert, puis remodelez uniquement là où vous avez besoin de structure.
Étape 2 : analyser les éléments output, pas choices[0]
C'est l'étape qui change le comportement. Chat Completions vous donnait choices, et l'indice 0 était presque toujours votre réponse. Responses renvoie un tableau output d'éléments typés — message, reasoning et function_call — et la réponse n'est pas garantie d'être en première position. Un modèle de raisonnement peut émettre un élément reasoning devant le message, et un tour d'outil peut émettre un function_call sans aucun texte. (Le function_call_output avec lequel vous renvoyez un résultat d'outil est un élément d'entrée que vous construisez, pas l'un de ces éléments renvoyés — gardez les deux distincts.)
L'accesseur pratique response.output_text gère le cas courant du « donne-moi juste le texte ». Quand vous avez besoin de plus de contrôle, parcourez par type :
answer, tool_calls = None, []
for item in response.output:
if item.type == "message":
answer = item.content[0].text
elif item.type == "function_call":
tool_calls.append(item)
Mettez à jour votre comptabilité d'usage en même temps : prompt_tokens/completion_tokens deviennent input_tokens/output_tokens sur l'objet usage. Tout code de facturation ou de tableau de bord qui lit le décompte de jetons doit gérer les deux noms de champs pendant un déploiement progressif.
Étape 3 : décider comment vous gérez l'état de conversation
Chat Completions est sans état — vous renvoyez l'historique complet à chaque appel. Les réponses sont stockées par défaut et renvoient un id à partir duquel vous pouvez chaîner. Choisissez l'une des trois stratégies et appliquez-la de façon cohérente :
| Stratégie | Comment | À utiliser quand |
|---|---|---|
previous_response_id | Envoyez l'id de la réponse précédente sur l'appel suivant | Vous voulez que le serveur conserve l'historique |
| Éléments manuels | Repassez les éléments output antérieurs dans input | Vous voulez un contrôle total de ce qui est renvoyé |
| Sans état | store: false et renvoyez l'historique vous-même | Zéro conservation ou vous gérez déjà un stockage |
Deux faits à anticiper. Premièrement, les instructions ne sont pas reportées à travers previous_response_id, alors renvoyez-les à chaque tour. Deuxièmement, le chaînage ne réduit pas votre facture : tous les jetons d'entrée antérieurs de la chaîne sont facturés comme des jetons d'entrée. Le chaînage économise de la bande passante et du code répétitif, pas de l'argent. Pour une vraie réduction du coût du contexte, combinez la migration avec le prompt caching.
Étape 4 : porter les appels d'outils et la sortie structurée
L'appel de fonctions survit à la migration, mais le câblage change. Dans Chat Completions, vous ajoutiez un message role: "tool" avec un tool_call_id. Dans Responses, vous envoyez un élément function_call_output lié par call_id :
client.responses.create(
model="gpt-5.6",
previous_response_id=response.id,
input=[{
"type": "function_call_output",
"call_id": call.call_id,
"output": tool_result,
}],
)
Envoyer le résultat sans le call_id correspondant est l'une des erreurs de migration les plus courantes, alors vérifiez-le par assertion dans les tests. La sortie structurée bouge aussi : response_format devient text.format, et vous définissez strict: true explicitement sur le schéma — vérifiez que votre JSON Schema valide toujours par rapport au sous-ensemble strict. (Ne reportez pas le comportement d'omission de strict avec dégradation en non-strict issu des définitions d'outils ; pour les sorties structurées, soyez explicite.) Les détails sont dans les sorties structurées avec JSON Schema, et les définitions portables dans l'appel d'outils portable entre fournisseurs.
Étape 5 : mettre à jour les consommateurs de streaming
Si vous diffusez, le transport reste Server-Sent Events mais les événements changent. Chat Completions diffusait des objets chat.completion.chunk avec choices[].delta et se terminait par data: [DONE]. Responses diffuse des événements sémantiques tels que response.created, response.output_text.delta et response.completed. Les événements sémantiques sont plus faciles à router parce que chacun se nomme lui-même, mais votre analyseur existant fondé sur [DONE] et delta ne les comprendra pas sans modifications. Les schémas d'analyse des deux sont dans diffuser les réponses de LLM avec SSE.
Étape 6 : déployer derrière un drapeau, garder un retour arrière
Ne basculez pas tous les appels d'un coup. Placez le choix d'endpoint derrière un feature flag ou routez-le par modèle au niveau de votre passerelle, enregistrez les fournisseurs en amont derrière lui, et envoyez une petite tranche de trafic vers Responses. Comparez la base de référence capturée à l'étape 0 aux nouvelles traces : mêmes réponses dans la tolérance, décomptes de jetons sensés, latence acceptable. Si quelque chose régresse, le retour arrière consiste à rebasculer le drapeau vers Chat Completions — pas un redéploiement — parce que la différence côté client est contenue. Quand la tranche paraît saine, élargissez-la.
Ce que la migration ne change pas
Il est utile de savoir où vous pouvez vous détendre. Plusieurs choses sont reportées sans changement, vous ne devriez donc pas les réécrire :
- L'authentification. La même clé d'API et le même en-tête
Authorization: Bearerfonctionnent ; seul le chemin change. - Les identifiants de modèle. Un modèle comme
gpt-5.6porte le même nom sur les deux endpoints — vous changez l'enveloppe de la requête, pas le modèle. - Le contenu des prompts. Vos instructions réelles et votre texte utilisateur sont identiques ; ils se déplacent simplement des entrées
messagesversinstructionsetinput. - La température et les réglages d'échantillonnage. Les paramètres de génération courants conservent leurs noms et leurs significations.
- Le transport pour le streaming. Les deux endpoints diffusent via Server-Sent Events, votre lecteur SSE reste donc en place ; seules les formes d'événements qu'il analyse diffèrent.
Savoir cela maintient la migration circonscrite. La surface comportementale qui change réellement est réduite : structure de sortie, gestion d'état, champ de résultat d'outil et configuration de sortie structurée. Tout le reste est un déplacement mécanique. Quand vous estimez le travail, budgétez pour ces quatre zones et traitez le reste comme du copier-coller, ce qui explique pourquoi un déploiement endpoint par endpoint est réaliste plutôt qu'une réécriture d'un seul tenant.
Lire les erreurs de migration courantes
La plupart des bugs de migration produisent une poignée de symptômes reconnaissables. Un texte vide ou manquant signifie généralement que vous avez lu output[0] et êtes tombé sur un élément reasoning ou function_call au lieu d'un message — parcourez par type ou utilisez output_text. Une erreur 400 liée au schéma sur la sortie structurée signifie généralement que vous avez laissé response_format en place au lieu de passer à text.format. Une boucle d'outils qui cale ou échoue signifie généralement qu'un function_call_output est parti sans le call_id correspondant. Et une conversation qui « oublie » son rôle ou son ton d'un tour à l'autre signifie généralement que vous avez compté sur la persistance des instructions à travers previous_response_id — ce n'est pas le cas, alors renvoyez-les. Garder cette courte liste à côté de votre diff transforme des échecs déroutants en recherches rapides.
Liste de vérification de migration
- Endpoint changé de
/v1/chat/completionsà/v1/responses. - Message
systemdéplacé versinstructionset renvoyé à chaque tour chaîné. - Sortie lue via
output_textou en parcourant les élémentsoutputpartype. - La comptabilité des jetons gère
input_tokens/output_tokens. - Stratégie d'état choisie :
previous_response_id, éléments manuels oustore: false. - Résultats d'outils envoyés en
function_call_outputavec uncall_idcorrespondant. -
response_formatremplacé partext.format; schéma revalidé. - Consommateurs de streaming mis à jour pour les événements sémantiques.
- Déploiement protégé par un drapeau avec un retour arrière en un seul basculement.
FAQ
Dois-je migrer avant une échéance ?
Chat Completions n'est pas programmé pour un retrait, il n'y a donc aucune échéance forcée le concernant. L'endpoint qu'OpenAI a marqué pour dépréciation est l'API Assistants (dépréciée le 2025-08-26, avec un retrait annoncé pour le 2026-08-26), qui est une surface différente. Migrez vers Responses pour ses capacités, non par urgence.
Mon assistant renvoie parfois un texte vide après la migration. Pourquoi ?
Vous lisez presque certainement output[0] et tombez sur un élément reasoning ou function_call au lieu d'un message. Parcourez output par type, ou utilisez response.output_text, qui saute les éléments non textuels pour vous.
Puis-je continuer à renvoyer l'historique complet au lieu d'utiliser previous_response_id ?
Oui. Définissez store: false et passez votre historique comme éléments input exactement comme vous le faisiez avec messages. Cela garde l'endpoint sans état et constitue le bon choix pour les exigences de zéro conservation.
Vérifiez d'abord sur un seul endpoint
Prenez un endpoint non critique, appliquez uniquement les étapes 1 et 2 (renommage plus analyse de sortie), et faites passer par lui vos prompts de base de référence de l'étape 0. Si le texte correspond et que les décomptes de jetons paraissent corrects dans les journaux d'usage de votre passerelle, vous avez prouvé la mécanique de la migration sur votre propre trafic avant de toucher à l'état, aux outils ou au streaming.