関数呼び出し:Responses APIとChat Completionsの比較

関数定義、呼び出しID、結果メッセージ、引数のストリーミング、認可、冪等性、ルート互換性を通信プロトコル単位で比較します。

関数呼び出し:Responses APIとChat Completionsの比較

ResponsesとChat Completionsは、どちらもアプリケーションに関数の実行を要求できます。ただし、ツールループの表現方法は異なります。Chat Completionsはツール定義をtools[].functionに置き、message.tool_callsで呼び出しを返し、role: "tool"のメッセージで結果を受け取ります。Responsesはフラットな関数定義を使い、型付きのfunction_call出力項目を返し、call_idでfunction_call_outputと関連付けます。

モデルがアプリケーションの関数を実行するわけではありません。アプリケーションコードが、提案された引数の検証、操作の認可、意図どおりの実行、結果の返却を担い、リクエストが再試行されても副作用が重複しないようにする必要があります。

ツールループの4つのステップ

エンドポイントごとにJSONは異なりますが、アプリケーションのステートマシンは同じです。

1. 許可する関数と引数スキーマを宣言する
2. モデルが提案した1つ以上の関数呼び出しを受け取る
3. アプリケーション側で各呼び出しを検証・認可・実行する
4. 呼び出しごとの相関 ID を使って結果を返す

4番目のステップが終わって初めて、モデルはツール結果に基づく回答を生成できます。モデルが別のツールを要求した場合は、新しい呼び出しIDでループを繰り返します。

OpenAI Function Callingガイドでは、これを複数ステップのやり取りとして説明しています。最初のツール呼び出しを最終回答として扱うことは、よくある実装ミスです。

2つの通信プロトコルを比較する

主な違いは概念ではなく構造です。

観点 Responses API Chat Completions
関数定義 name、description、parameters、strictを持つフラットなtools[]項目 同じフィールドをfunction内にネストしたtools[]項目
モデルが提案する呼び出し 型付きのfunction_call出力項目 アシスタントメッセージのtool_calls[]項目
関連付けID call_id ツール呼び出しのid。結果ではtool_call_idとして返す
関数名 function_call.name tool_calls[].function.name
引数 function_call.arguments内のJSON文字列 tool_calls[].function.arguments内のJSON文字列
ツール結果 function_call_output入力項目 role: "tool"のメッセージ
一般的なストリーミング引数 型付きの関数引数増分イベント インデックス付きのdelta.tool_calls[]断片
最終テキスト 型付き出力項目と出力テキスト用ヘルパー choices[0].message.content

配列位置で呼び出しを関連付けないでください。並列呼び出しやストリーミング断片の到着順は、アプリケーション側で処理が完了する順序と異なる場合があります。明示的な呼び出しIDが安定した関連付けキーです。

1つの厳格な関数スキーマを定義する

次の例では、読み取り専用のget_delivery_status関数だけを公開します。モデルは注文状況を問い合わせられますが、注文の変更、返金、任意ストレージへのアクセスはできません。

引数スキーマは意図的に狭くしています。

{
  "type": "object",
  "properties": {
    "order_id": {
      "type": "string",
      "pattern": "^ORDER-[0-9]{4}$"
    }
  },
  "required": ["order_id"],
  "additionalProperties": false
}

strict: trueは、対応モデルに関数引数スキーマへの準拠を求めます。それでも、実行前にアプリケーションでJSONを解析・検証する必要があります。patternなどのJSON Schema機能はモデルやプロバイダーによって対応状況が異なるため、関数引数にもStructured Outputsと同じ互換性ゲートを適用します。

Responses APIでツールループを実装する

次のJavaScript例は、モデルの出力項目と関数結果をまとめて返し、やり取り全体を明示します。別の外部APIを追加せずに制御フローを確認できるよう、決定的なローカル関数を使っています。

import OpenAI from "openai";

const client = new OpenAI({
  apiKey: process.env.MODELFLARE_API_KEY,
  baseURL: "https://modelflare.dev/v1",
});

const model = process.env.MODEL_ID;
if (!model) throw new Error("MODEL_ID が必要です");

const orders = new Map([
  ["ORDER-1001", { status: "in_transit", eta: "2026-08-06" }],
]);

const tools = [
  {
    type: "function",
    name: "get_delivery_status",
    description: "注文の現在の配送状況を返します。",
    parameters: {
      type: "object",
      properties: {
        order_id: { type: "string", pattern: "^ORDER-[0-9]{4}$" },
      },
      required: ["order_id"],
      additionalProperties: false,
    },
    strict: true,
  },
];

function executeTool(name, rawArguments) {
  if (name !== "get_delivery_status") {
    throw new Error(`許可されていないツールです:${name}`);
  }

  const args = JSON.parse(rawArguments);
  if (!/^ORDER-[0-9]{4}$/.test(args.order_id)) {
    throw new Error("order_id が不正です");
  }

  return orders.get(args.order_id) ?? { status: "not_found" };
}

const first = await client.responses.create({
  model,
  input: "ORDER-1001 はどこまで届いていますか?",
  tools,
  parallel_tool_calls: false,
});

const calls = first.output.filter((item) => item.type === "function_call");
if (calls.length === 0) {
  console.log(first.output_text);
  process.exit(0);
}

const toolOutputs = calls.map((call) => ({
  type: "function_call_output",
  call_id: call.call_id,
  output: JSON.stringify(executeTool(call.name, call.arguments)),
}));

const final = await client.responses.create({
  model,
  input: [...first.output, ...toolOutputs],
  tools,
  parallel_tool_calls: false,
});

console.log(final.output_text);

ステートフルな統合では、ルートと保存ポリシーが許可する場合、Responsesは以前のレスポンスから継続する方法も提供します。この例で会話履歴全体を明示しているのは、すべてのOpenAI互換ルートがレスポンス状態を保存すると仮定しないためです。

重要なのは、モデル出力のcall.call_idと、function_call_outputに設定する一致したcall_idです。誤ったIDで結果を返すと、そのデータをモデルの要求へ関連付けられません。

Chat Completionsでツールループを実装する

Chat Completionsでも同じアプリケーション関数を利用できます。ツール定義と結果メッセージにはChat Completionsのラッパーを使います。

import OpenAI from "openai";

const client = new OpenAI({
  apiKey: process.env.MODELFLARE_API_KEY,
  baseURL: "https://modelflare.dev/v1",
});

const model = process.env.MODEL_ID;
if (!model) throw new Error("MODEL_ID が必要です");

const orders = new Map([
  ["ORDER-1001", { status: "in_transit", eta: "2026-08-06" }],
]);

const tools = [
  {
    type: "function",
    function: {
      name: "get_delivery_status",
      description: "注文の現在の配送状況を返します。",
      parameters: {
        type: "object",
        properties: {
          order_id: { type: "string", pattern: "^ORDER-[0-9]{4}$" },
        },
        required: ["order_id"],
        additionalProperties: false,
      },
      strict: true,
    },
  },
];

function executeTool(name, rawArguments) {
  if (name !== "get_delivery_status") {
    throw new Error(`許可されていないツールです:${name}`);
  }

  const args = JSON.parse(rawArguments);
  if (!/^ORDER-[0-9]{4}$/.test(args.order_id)) {
    throw new Error("order_id が不正です");
  }

  return orders.get(args.order_id) ?? { status: "not_found" };
}

const messages = [
  { role: "user", content: "ORDER-1001 はどこまで届いていますか?" },
];

const first = await client.chat.completions.create({
  model,
  messages,
  tools,
  parallel_tool_calls: false,
});

const assistant = first.choices[0].message;
const calls = assistant.tool_calls ?? [];
if (calls.length === 0) {
  console.log(assistant.content);
  process.exit(0);
}

messages.push(assistant);
for (const call of calls) {
  const result = executeTool(call.function.name, call.function.arguments);
  messages.push({
    role: "tool",
    tool_call_id: call.id,
    content: JSON.stringify(result),
  });
}

const final = await client.chat.completions.create({
  model,
  messages,
  tools,
  parallel_tool_calls: false,
});

console.log(final.choices[0].message.content);

最初のアシスタントメッセージは、ツール結果より先にmessagesへ追加する必要があります。各結果はtool_call_id: call.idを使います。アシスタントのツール呼び出しメッセージを省略したり、一致しないIDを返したりすると、無効な会話履歴になります。

ストリーミング引数を完全なJSONではなく断片として扱う

ストリーミングでは、関数引数が複数の断片に分かれて到着する場合があります。{"orderだけを含む断片は不正なJSONではなく、未完成のJSONです。

安全なパーサーは、呼び出しIDごとに独立したバッファーを維持します。

  1. イベントから呼び出しIDまたはインデックス付きの呼び出しスロットを特定する。
  2. 各引数の増分を、その呼び出しのバッファーへ追加する。
  3. エンドポイントの引数終了イベントまたは応答完了イベントを待つ。
  4. 完成した文字列を一度だけ解析する。
  5. 完成後に検証し、実行する。

Responsesは、関数引数と出力項目に型付きイベントを使います。Chat Completionsはchoices[].delta.tool_calls[]で断片を返し、インデックスが作成中の項目を識別し、最終的なツール呼び出しには安定したIDが含まれます。最初の断片が届いた時点で関数を実行してはいけません。

AI APIストリーミングガイドでは、接続、最初のイベント、最初の有効な出力、アイドルタイムアウト、完了を分けて測定すべき理由を説明しています。

ツール実行を安全かつ冪等にする

厳格な引数は構造を改善しますが、認可を提供するものではありません。呼び出しを実行する前に、次を行います。

  • 登録済みの関数名だけを許可する。
  • 引数にサイズ上限を設けてから解析する。
  • アプリケーションコードで完全なスキーマを検証する。
  • 現在のユーザーまたはワークロードに対象リソースへの権限があるか確認する。
  • 読み取り専用ツールと副作用を持つツールを分離する。
  • モデルへ返す前に機密情報や機密出力を除去する。
  • 期限と制限付きの下流再試行を設定する。
  • 機密情報や非公開コンテンツではなく、安全な操作参照を記録する。

副作用を持つツールには、安定したアプリケーションリクエストと呼び出し識別子から導出した冪等キーを使います。モデルへ返す前に完了結果を保存してください。ネットワーク再試行で同じ呼び出しが再生された場合は、返金、メッセージ送信、デプロイ、データベース変更を再実行せず、保存済み結果を返します。

モデルが生成した関数名と引数は信頼できない入力です。有効なスキーマは、権限や所有権を証明せず、トランザクション境界の代わりにもなりません。

複数呼び出しの扱いを決める

例ではステートマシンを確認しやすくするため、parallel_tool_calls: falseを設定しています。並列呼び出しを有効にする場合は、次を守ります。

  • 完了順ではなく呼び出しIDで各結果を関連付ける。
  • 1つのレスポンスで許可する呼び出し数に上限を設ける。
  • アプリケーション側に同時実行数の制限を設ける。
  • 1つのツールが失敗したとき、他をキャンセル、ブロック、または成功として扱うかを定義する。
  • 受け入れたすべての呼び出しに結果を返す。
  • 副作用の順序が重要な操作は直列に実行する。

並列処理は独立した読み取りの待ち時間を減らせますが、認可、順序、再試行、部分失敗を複雑にします。パラメーターが存在するという理由ではなく、ワークロードに効果がある場合に有効化してください。

Modelflareの互換性境界を理解する

ModelflareのOpenAI互換コードは、厳格な関数定義を保持し、対象となるOpenAI/Codexルートで、対応しているResponsesとChat Completionsの関数呼び出し形式を変換します。このマッピングは、Responsesの全ツール機能より意図的に狭く設計されています。

アプリケーション定義の関数ツールは、両方の形式で表現できます。一方、プロバイダーが実行する検索、コード実行、その他の内蔵ツールなど、Responses専用のホスト型ツールはアプリケーション関数呼び出しと同じではなく、Chat Completionsへ変換できるとは限りません。

その他のOpenAI互換モデルファミリーは、別途検証されていない限り、元のChat Completions形式でそのまま転送されます。これらのルートでtools、strict、並列呼び出し、ストリーミング引数の断片、特定のtool_choice形式が使えるかは、アップストリームプロバイダーによって決まります。

ルートを有効にする前に、最初のリクエストが200を返すかだけでなく、ループ全体をテストします。

テスト 必要な証拠
1回の読み取り専用呼び出し 関数名、解析済み引数、呼び出しID、結果の関連付け、最終テキスト
不正な引数 ツールを実行せず、明示的に検証失敗
未知の関数 アプリケーションの許可リストで拒否
ツール不要 架空の呼び出しを作らず、通常の最終テキストを返す
ストリーミング呼び出し 引数を完全に再構築し、正しい呼び出しIDを使用
リクエストの再実行 副作用は1回、または保存済み結果を1回返し、重複実行しない
複数呼び出し 完了順に依存せず正しく関連付ける
ルートフォールバック 同じモデル、プロトコル、ツールスキーマ、結果契約を維持

信頼性の高いAI APIルーティングを使ってルート変更を明示し、AI APIキーのセキュリティガイドを使ってワークロードと権限を分離します。

アプリケーション契約に合わせて形式を選ぶ

型付きの出力項目、Responsesのイベントモデル、状態継続オプション、または検証済みのResponses機能が必要ならResponsesを選びます。アプリケーションが安定したメッセージ履歴をすでに管理し、選択したプロバイダーのツール契約を検証済みならChat Completionsを選べます。より広いエンドポイント選択はResponses APIとChat Completionsで説明しています。

実装基準はどちらも同じです。明示的なツール許可リスト、厳格な引数スキーマ、アプリケーション側の検証、実行前の認可、呼び出しIDによる関連付け、副作用の冪等性、モデルへの完全な結果返却が必要です。関数呼び出しのワークフローを信頼できるものにするのは、エンドポイント名ではなく、これらの制御です。