Function Calling:Responses APIとChat Completionsの比較

関数定義、Call ID、結果メッセージ、引数ストリーミング、認可、冪等性、ルート互換性をWire Contract単位で比較します。

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

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

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

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

1. Declare an allowed function and its argument schema
2. Receive one or more model-proposed function calls
3. Validate, authorize, and execute each call in application code
4. Return each result using the call's correlation ID

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

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

2つのWire Contractを比較する

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

観点 Responses API Chat Completions
関数定義 namedescriptionparametersstrictを持つフラットなtools[] Item 同じフィールドをfunction内にネストしたtools[] Item
モデルが提案する呼び出し 型付きのfunction_call Output Item Assistant Messageのtool_calls[]エントリー
関連付けID call_id Tool Callのid。結果ではtool_call_idとして返す
関数名 function_call.name tool_calls[].function.name
引数 function_call.arguments内のJSON文字列 tool_calls[].function.arguments内のJSON文字列
ツール結果 function_call_output Input Item role: "tool"のメッセージ
一般的なストリーミング引数 型付きの関数引数Delta Event インデックス付きのdelta.tool_calls[]断片
最終テキスト 型付きOutput ItemとOutput Text Helper choices[0].message.content

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

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例は、モデルのOutput Itemと関数結果をまとめて返し、やり取り全体を明示します。別の外部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 is required");

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

const tools = [
  {
    type: "function",
    name: "get_delivery_status",
    description: "Return the current delivery status for one order.",
    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(`Tool is not allowed: ${name}`);
  }

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

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

const first = await client.responses.create({
  model,
  input: "Where is 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は以前のResponseから継続する方法も提供します。この例でTranscript全体を明示しているのは、すべてのOpenAI互換ルートがResponse Stateを保存すると仮定しないためです。

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

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

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

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 is required");

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

const tools = [
  {
    type: "function",
    function: {
      name: "get_delivery_status",
      description: "Return the current delivery status for one order.",
      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(`Tool is not allowed: ${name}`);
  }

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

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

const messages = [
  { role: "user", content: "Where is 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);

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

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

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

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

  1. EventからCall IDまたはインデックス付きの呼び出しスロットを特定する。
  2. 各Argument Deltaを、その呼び出しのBufferへ追加する。
  3. エンドポイントのArgument DoneまたはCompletedシグナルを待つ。
  4. 完成した文字列を一度だけ解析する。
  5. 完成後に検証し、実行する。

Responsesは、関数引数とOutput Itemに型付きEventを使います。Chat Completionsはchoices[].delta.tool_calls[]で断片を返し、Indexが作成中のエントリーを識別し、最終的なTool Callには安定したIDが含まれます。最初の断片が届いた時点で関数を実行してはいけません。

AI API Streaming Guideでは、接続、最初のEvent、最初の有効な出力、Idle Timeout、完了を分けて測定すべき理由を説明しています。

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Reliable AI API Routingを使ってルート変更を明示し、AI API Key Securityを使ってワークロードと権限を分離します。

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

型付きのOutput Item、Responses Event Model、状態継続オプション、または検証済みのResponses機能が必要ならResponsesを選びます。アプリケーションが安定したMessage Transcriptをすでに管理し、選択したプロバイダーのTool Contractを検証済みならChat Completionsを選べます。より広いエンドポイント選択はResponses API vs Chat Completionsで説明しています。

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