Chat Completions から Responses API へ移行する
OpenAI Chat Completions のコードを Responses API へ移植するためのステップバイステップガイド。入力アイテム、出力の解析、状態管理、ツール、ストリーミング、そしてロールバック手順まで。
Chat Completions から Responses API への移行は、大部分が「名前を変え、形を整える」作業です。POST /v1/chat/completions は POST /v1/responses になり、messages 配列は input になり、システムプロンプトは instructions になり、そして completion.choices[0].message.content の代わりに response.output_text を読むようになります。人がつまずくのは、名前ではなく挙動が変わる部分です。すなわち出力アイテム、ステートフルさ、ツールの紐付け、そして構造化出力のフィールドです。本ガイドでは、実際に進めるべき順序で移行をたどり、すべてを一度に書き直すのではなく、エンドポイントを一つずつ移せるようにロールバック手順も添えます。
移行は必須ではありません。Chat Completions は引き続きサポートされています。推論の連続性、サーバー管理の会話状態、ネイティブツール、あるいはエージェントループが恩恵を受けるより整った型付き出力が欲しくなったときに移行してください。まだ迷っているなら、まず Responses API と Chat Completions の比較 を読んでください。
ステップ 0: 差分を取れるベースラインを確立する
コードに手を付ける前に、現在のエンドポイントで「正しい」とはどういう状態かを記録しておきましょう。代表的なプロンプトをいくつか Chat Completions 経由で送り、応答テキスト、トークン使用量、レイテンシを記録します。それらをゲートウェイ経由で流し、トレースが使用ログに残るようにします。後で同じプロンプトを Responses に対して再実行し、比較します。このステップを飛ばした移行は、出力が食い違った瞬間に当てずっぽうへと変わってしまいます。
ステップ 1: エンドポイントをリネームし、システムプロンプトを移す
最小限の動く移行は、たった三つの編集です。SDK を Responses のメソッドに向け、system メッセージがあれば instructions へ移し、単一のユーザーターンを input に畳み込みます。
変更前:
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
変更後:
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 は、上記のようなプレーンな文字列も、複数ターンの履歴やツール結果が必要なときには型付きアイテムの配列も受け付けます。まずは文字列形式でテストをグリーンにし、構造が必要な箇所だけ後から形を整えましょう。
ステップ 2: choices[0] ではなく出力アイテムを解析する
これは挙動が変わるステップです。Chat Completions は choices を返し、インデックス 0 はほぼ常にあなたの答えでした。Responses は型付きのアイテム、つまり message、reasoning、function_call から成る output 配列を返し、答えが先頭にある保証はありません。推論モデルは message の前に reasoning アイテムを出すことがあり、ツールターンはテキストをまったく含まない function_call を出すことがあります。(ツール結果を送り返すための function_call_output は、自分で組み立てる入力アイテムであり、ここで返されるアイテムではありません。この二つを混同しないでください。)
便利なアクセサ response.output_text は、よくある「とにかくテキストが欲しい」ケースを処理します。より細かな制御が必要なときは、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)
同時に使用量の集計も更新しましょう。usage オブジェクト上で prompt_tokens/completion_tokens は input_tokens/output_tokens になります。トークン数を読む課金やダッシュボードのコードは、段階的ロールアウトの間、両方のフィールド名を扱えなければなりません。
ステップ 3: 会話状態の管理方法を決める
Chat Completions はステートレスで、呼び出しごとに全履歴を再送します。Responses はデフォルトで保存され、続けて連鎖できる id を返します。次の三つの戦略から一つを選び、一貫して適用してください。
| 戦略 | 方法 | 使うとき |
|---|---|---|
previous_response_id | 次の呼び出しで直前の応答 id を送る | サーバーに履歴を保持させたい |
| 手動アイテム | 直前の output アイテムを input に戻す | 再送する内容を完全に制御したい |
| ステートレス | store: false にして履歴を自分で再送する | ゼロ保持、またはすでにストアを自前管理している |
計画に織り込むべき事実が二つあります。第一に、instructions は previous_response_id をまたいで引き継がれないため、毎ターン再送してください。第二に、連鎖しても請求は下がりません。連鎖内の以前のすべての入力トークンは入力トークンとして課金されます。連鎖が節約するのは帯域と定型コードであって、お金ではありません。実際にコンテキストコストを削減するには、移行を プロンプトキャッシュ と組み合わせてください。
ステップ 4: ツール呼び出しと構造化出力を移植する
関数呼び出しは移行後も生き残りますが、配線が変わります。Chat Completions では tool_call_id を持つ role: "tool" メッセージを追加していました。Responses では、call_id で紐付けた function_call_output アイテムを送ります。
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,
}],
)
対応する call_id なしで結果を送るのは、最もよくある移行エラーの一つなので、テストでアサートしましょう。構造化出力も移ります。response_format は text.format になり、スキーマには strict: true を明示的に設定します。JSON Schema が厳格サブセットに対して引き続き妥当か確認してください。(ツール定義にある、strict を省略して非厳格へフォールバックする挙動を構造化出力へ持ち込まないでください。ここでは明示する必要があります。)詳細は JSON Schema による構造化出力、移植可能な定義は プロバイダー横断の移植可能なツール呼び出し にあります。
ステップ 5: ストリーミングの消費側を更新する
ストリーミングする場合、トランスポートは Server-Sent Events のままですが、イベントが変わります。Chat Completions は choices[].delta を持つ chat.completion.chunk オブジェクトをストリームし、data: [DONE] で終わっていました。Responses は response.created、response.output_text.delta、response.completed といったセマンティックイベントをストリームします。セマンティックイベントは各自が自分の名前を名乗るためルーティングが容易ですが、既存の [DONE] と delta のパーサーは変更なしにはそれらを理解できません。両方の解析パターンは SSE による LLM 応答のストリーミング にあります。
ステップ 6: フラグの背後でロールアウトし、ロールバックを保つ
すべての呼び出しを一度に切り替えないでください。エンドポイントの選択をフィーチャーフラグの背後に置くか、ゲートウェイでモデルごとにルーティングし、その背後にアップストリームを登録して、トラフィックのごく一部を Responses に送ります。ステップ 0 で捕捉したベースラインを新しいトレースと比較します。許容範囲内で同じ答え、妥当なトークン数、許容できるレイテンシを確認してください。何かが劣化した場合、クライアント側の差分は閉じ込められているため、ロールバックは再デプロイではなく、フラグを Chat Completions に戻すだけです。その一部が健全に見えたら、範囲を広げます。
移行で変わらないもの
どこで気を緩めてよいかを知っておくと役立ちます。いくつかのものはそのまま引き継がれるので、書き直すべきではありません。
- 認証。 同じ API キーと
Authorization: Bearerヘッダーが機能します。変わるのはパスだけです。 - モデル識別子。
gpt-5.6のようなモデルは両エンドポイントで同じ名前です。変えているのはリクエストの外皮であって、モデルではありません。 - プロンプトの内容。 実際の指示やユーザーテキストは同一です。単に
messagesエントリからinstructionsとinputへ移るだけです。 - 温度とサンプリングのつまみ。 一般的な生成パラメータは名前と意味を保ちます。
- ストリーミングのトランスポート。 両エンドポイントとも Server-Sent Events でストリームするので、SSE リーダーはそのままです。変わるのはそれが解析するイベントの形だけです。
これを知っておくと、移行の範囲が保たれます。本当に変わる挙動の面は小さいです。出力構造、状態管理、ツール結果のフィールド、そして構造化出力の設定です。それ以外はすべて機械的な移動です。作業を見積もるときは、この四つの領域に予算を割き、残りはコピー&ペーストとして扱いましょう。だからこそ、一括の書き直しではなく、エンドポイントごとのロールアウトが現実的なのです。
よくある移行エラーを読み解く
移行のバグの多くは、見分けのつく症状をいくつか生みます。テキストが空または欠落しているのは、たいてい output[0] を読んで message ではなく reasoning や function_call アイテムに当たったことを意味します。type で反復するか、output_text を使ってください。構造化出力でスキーマ関連の 400 が出るのは、たいてい text.format に切り替えず response_format を残したままを意味します。ツールループが止まるかエラーになるのは、たいてい function_call_output が対応する call_id なしで送られたことを意味します。そして会話がターンをまたいで役割や口調を「忘れる」のは、たいてい instructions が previous_response_id をまたいで持続すると当てにしたことを意味します。持続しないので、再送してください。この短いリストを diff の隣に置いておけば、紛らわしい失敗が素早い参照に変わります。
移行チェックリスト
- エンドポイントを
/v1/chat/completionsから/v1/responsesへ変更した。 -
systemメッセージをinstructionsへ移し、連鎖する各ターンで再送している。 - 出力を
output_text経由で、またはoutputアイテムをtypeで反復して読んでいる。 - トークン集計が
input_tokens/output_tokensを扱う。 - 状態戦略を選択した:
previous_response_id、手動アイテム、またはstore: false。 - ツール結果を、対応する
call_idを持つfunction_call_outputとして送っている。 -
response_formatをtext.formatに置き換え、スキーマを再検証した。 - ストリーミングの消費側をセマンティックイベント向けに更新した。
- ロールアウトはフラグで制御され、ワンスイッチのロールバックを備える。
FAQ
何らかの締め切りまでに移行しなければなりませんか?
Chat Completions は廃止が予定されていないため、強制的な締め切りはありません。OpenAI が廃止対象としてマークしたエンドポイントは Assistants API(2025-08-26 に非推奨化、廃止は 2026-08-26 と告知)であり、これは別の面です。Responses への移行は、その機能のためであって、切迫感からではありません。
移行後、アシスタントが時々空のテキストを返します。なぜですか?
ほぼ確実に output[0] を読んでいて、message ではなく reasoning や function_call アイテムに当たっています。output を type で反復するか、非テキストアイテムを飛ばしてくれる response.output_text を使ってください。
previous_response_id を使わず、全履歴を再送し続けてもよいですか?
はい。store: false を設定し、messages でやっていたのとまったく同じように履歴を input アイテムとして渡してください。これでエンドポイントはステートレスに保たれ、ゼロ保持の要件には正しい選択です。
まず一つのエンドポイントで検証する
重要でないエンドポイントを一つ取り、ステップ 1〜2 だけ(リネームと出力解析)を適用し、ステップ 0 のベースラインプロンプトを流してみましょう。テキストが一致し、ゲートウェイの使用ログでトークン数が正しく見えるなら、状態・ツール・ストリーミングに触れる前に、自分のトラフィックで移行の仕組みを証明できたことになります。