複数社のLLM APIを切り替えたい時:自前の抽象化層は作るべきか
AIを使いこなすClaudeで執筆し、GPTで自動校閲・修正しています。

結論から言うと、作るのは「自分のアプリが実際に使う機能だけを覆う薄いラッパー」までにして、全社共通の万能な抽象化層は作らないほうが安全です。テキスト生成と関数呼び出し(ツール呼び出し)が中心なら、薄い層で足ります。思考(thinking)、プロンプトキャッシュ、厳密なスキーマ保証のような各社固有の機能に依存し始めたら、その部分はラッパーの外に出し、各社のネイティブSDKを直接使うほうが現実的です。 この記事では、公式ドキュメントで確認できた差分を一覧にし、「作る・作らない」を分ける判断基準と、薄いラッパーの最小例を示します。確認したのは2026年10月28日時点で閲覧できたOpenAI、Anthropic、Googleの公式ページです。モデル名や対応状況は変わるため、実装前に各社の最新ページを開いてください。
この記事の目次
まず「切り替え」の目的を決める
抽象化層が必要かどうかは、何のために切り替えるかで変わります。
- 障害時のフォールバック:同じ入力を別社に回す。本番で動くので、差分の吸収が必須です。
- コストや品質の比較:評価用に両社へ同じ入力を流す。本番コードに入れる必要はなく、評価スクリプト側で変換すれば済みます。
- 将来の乗り換えに備える:今すぐ切り替える予定はない。この場合は抽象化層を作り込まず、SDK呼び出しを1つのモジュールに閉じ込めるだけで十分です。
目的がフォールバックでない限り、凝った層は先行投資になりがちです。
差分が出やすい項目の一覧
次の表は、公式ドキュメントに書かれている内容をもとに、ラッパーで吸収が必要になる項目をまとめたものです。Geminiの列は、取得できたGemini API関数呼び出しのページの記述に基づきます。同社の他の呼び出し形式まで同じとは限らないため、使う方式のページで確認してください。
- ツール定義、OpenAI:type: "function"、name、description、parameters、strict を持つ(公式)、Claude:name、description、input_schema(公式)、Gemini:type: "function"、name、description、parameters(type/properties/required)
- モデルのツール呼び出し、OpenAI:出力内の function_call 項目。call_id、name、JSON文字列の arguments、Claude:tool_use ブロック。id、name、input(オブジェクト)、Gemini:function_call ステップ。name、arguments
- 結果の返し方、OpenAI:function_call_output に call_id と output、Claude:user メッセージ内の tool_result に tool_use_id、Gemini:function_result ステップで返す
- システム指示、OpenAI:Chat Completionsではメッセージ列に置ける、Claude:トップレベルの system パラメータ(メッセージ列とは別)、Gemini:今回は未確認
- 出力上限の指定、OpenAI:今回の確認範囲では必須の記述なし、Claude:max_tokens が必須、Gemini:今回は未確認
- 会話履歴、OpenAI:自分で保持するか、サーバー側の仕組みを使う、Claude:user/assistant が交互になる想定。連続する同役割は結合される、Gemini:既定はステートフル(previous_interaction_id)、store=false で履歴を自前管理
特に事故になりやすいのは次の3点です。
- 引数の型:OpenAIは arguments がJSON文字列、Claudeは input が既にオブジェクトです。変換を忘れるとパースエラーか二重エンコードになります。
- ツール結果の置き場所:Claudeでは結果が user ロールのメッセージ内に入り、OpenAIでは独立した項目です。履歴の保存形式を片方に寄せると、もう片方へ戻せなくなります。
- 履歴の管理主体:Geminiの説明ページでは、サーバー側に履歴を持たせる方式が既定でした。履歴を自前DBで持つ設計と混ぜると、二重に履歴が積まれます。
「OpenAI互換エンドポイント」は抽象化層の代わりになるか
OpenAIのSDKのまま接続先だけ変える方法が、AnthropicとGoogleの両社から提供されています。ただし、互換対応には機能上の制約があります。
- Anthropicは、互換レイヤーをOpenAI SDKでClaude APIを試し、モデル能力を評価するための機能として説明しています。同ページによれば、strict は無視され、ツール呼び出しのJSONがスキーマに従う保証はありません。response_format も無視され、プロンプトキャッシュは使えません。システム/デベロッパーメッセージは会話中のどこにあっても先頭の system に連結されます。未対応の項目の多くはエラーにならず黙って無視されます。
- GoogleのOpenAI互換ページは、OpenAIライブラリのサポートが「まだベータ」であると明記し、リストにないパラメータは黙って無視されるとしています。
「黙って無視される」点が肝心です。切り替えは成功したように見えて、構造化出力の厳密さなど期待した挙動が静かに失われる可能性があります。互換エンドポイントは、評価やプロトタイプで両社を素早く比べる用途に向いています。
ラッパー導入の判断基準
次の表は編集上の提案です。上から当てはめて、最初に該当した行を採用してください。
- 切り替えの予定も要件もない、判断:作らない。SDK呼び出しを1ファイルに閉じるだけ
- 評価・比較が目的、判断:本番には入れず、評価スクリプトに変換関数を置く
- フォールバックが要件で、使う機能がテキスト生成とツール呼び出しのみ、判断:薄いラッパーを作る
- 厳密なスキーマ保証が業務要件、判断:各社ネイティブの機能を使い、その経路はラッパーから外す
- 思考、キャッシュ、PDF・引用などに依存、判断:該当機能は社別の実装にし、共通層は持たない
- 社数が3以上で、機能も増え続けている、判断:既存の汎用ライブラリの採用を比較検討(導入前にライセンスと保守状況を確認)
薄いラッパーの設計では、次の3つを守ると肥大化を防げます。
- 共通インターフェースは自分のアプリの語彙で作る。「OpenAI互換」ではなく「うちの機能が必要とする入力と出力」を型にします。
- 履歴は自社形式で保存し、送信時に社別へ変換する。保存形式を特定社のものにしない。
- サポートしない機能は例外にする。無視して通すと、互換エンドポイントと同じ静かな劣化を自分で作ることになります。
薄いラッパーの最小例
説明用の例で、実行して検証したものではありません。ツール定義と結果の変換だけを書いています。公式ページに書かれたフィールド名に合わせていますが、使用SDKのバージョンに応じて確認してください。
from dataclasses import dataclass
@dataclass
class Tool: # 自社の共通表現
name: str
description: str
schema: dict # JSON Schema (type: object)
def to_openai(t: Tool) -> dict:
return {"type": "function", "name": t.name,
"description": t.description,
"parameters": t.schema, "strict": False}
def to_claude(t: Tool) -> dict:
return {"name": t.name, "description": t.description,
"input_schema": t.schema}
def result_to_openai(call_id: str, text: str) -> dict:
return {"type": "function_call_output",
"call_id": call_id, "output": text}
def result_to_claude(tool_use_id: str, text: str) -> dict:
# Claudeでは user メッセージの content 配列に入れる
return {"role": "user", "content": [
{"type": "tool_result",
"tool_use_id": tool_use_id, "content": text}]}
def parse_args_openai(raw: str) -> dict:
import json
return json.loads(raw) # 文字列で届く
def parse_args_claude(obj: dict) -> dict:
return obj # すでにオブジェクト
UNSUPPORTED = {"strict_schema", "thinking", "prompt_cache"}
def require(features: set[str], provider: str):
bad = features & UNSUPPORTED
if provider == "fallback" and bad:
raise NotImplementedError(f"未対応: {bad}")strict を False にしているのは、Claude側へ切り替えたときに厳密保証がない前提を揃えるためです。厳密保証が必要な経路は、前節の表のとおりラッパーの外に出してください。
差分一覧を自分のアプリ用に作る依頼文
自分の使い方に絞った差分表は、生成AIに下書きさせて、人が公式ページで検証する流れが安全です。次の依頼文は、機密を含まない説明用データに置き換えて使ってください。
あなたはLLM API連携のレビュー担当です。以下の「自分のアプリが使う機能」だけを対象に、
OpenAI・Claude・Geminiの差分表を作ってください。
- 使う機能:{例:テキスト生成、関数呼び出し2個、ストリーミング}
- 履歴の保持方法:{例:自社DBに保存}
- 切り替えの目的:{例:障害時のフォールバック}
出力は「項目/各社の仕様/吸収方法/確認すべき公式ページ名」の表にしてください。
確信のない箇所は「要確認」と書き、推測で埋めないでください。返ってきた表は、「要確認」の行と、引数の型・結果の返し方・履歴の持ち方の3行を、必ず各社の公式ページで照合してください。AIの表をそのまま実装の根拠にしないことが、この作業の確認ポイントです。

