0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

1つのコードベースから4つのLLM APIを呼んで学んだこと

0
Posted at

qiita-llm-api-thumbnail-v2.png

ベンチマークのスコア比較記事は多いですが、この記事はベンチマークではわからない実務的な差異を扱います。レスポンス形式の違い、ストリーミングの実装、コスト構造など、複数のLLMプロバイダーをサポートするツールを作りながら実際に経験したことをまとめました。

※本記事は公開情報をもとにした個人的なまとめであり、各企業の公式見解ではありません。

なぜ1つのコードベースで4つのAPIなのか

ブラウザベースの開発ツールを作っています。あるプロジェクトで、ユーザーが好きなLLMプロバイダーを選択できるようにする必要がありました。ユーザーがプロバイダーを選び、自分のAPIキーを入力すると、ツールが残りを処理する構造です。

シンプルに見えますが、考慮すべき点は想像以上に多かったです。

OpenAI、Google Gemini、Anthropic Claude、そしてOpenAI互換エンドポイント(OllamaやLM Studioなどのローカルツール含む)を1つのコードベースから支えることで、各APIの実務的な違いを多く学びました。

レスポンス形式の違い

プロバイダーごとにレスポンスの構造が異なります。

OpenAI(Chat Completions API):

{
  "choices": [{"message": {"content": "..."}}]
}

OpenAIの新しいResponses API(/v1/responses)は別のフォーマットを使います:

{
  "output": [{"type": "message", "content": [{"type": "output_text", "text": "..."}]}]
}

Claude:

{
  "content": [{"type": "text", "text": "..."}]
}

Gemini:

{
  "candidates": [{"content": {"parts": [{"text": "..."}]}}]
}

些細に見えますが、パイプライン全体がこのテキスト抽出に依存します。ノーマライザー関数を早い段階で書いておくことをお勧めします:

function extractText(provider, response) {
  switch (provider) {
    case 'openai':
      // Chat Completions API形式
      return response.choices?.[0]?.message?.content ?? '';
    case 'openai-responses':
      // Responses API形式
      return response.output
        ?.filter(b => b.type === 'message')
        .flatMap(b => b.content)
        .filter(c => c.type === 'output_text')
        .map(c => c.text)
        .join('\n') ?? '';
    case 'claude':
      return response.content
        ?.filter(b => b.type === 'text')
        .map(b => b.text)
        .join('\n') ?? '';
    case 'gemini':
      return response.candidates?.[0]?.content?.parts
        ?.map(p => p.text)
        .join('\n') ?? '';
    default:
      // OpenAI互換フォールバック
      return response.choices?.[0]?.message?.content ?? '';
  }
}

他の何よりも先に、このノーマライザーを書くことをお勧めします。

ストリーミングの実装

4つのプロバイダーすべてがストリーミングに対応していますが、それぞれ実装方式が異なります。

OpenAIと互換エンドポイントはdata: [DONE]でストリーム終了を通知します。Claudeはevent: message_stopを使います。Geminiは独自のSSE形式を持っています。

チャンク構造も異なります。OpenAIはdelta.contentを送り、Claudeはcontent_block_deltaイベント内のdelta.textを送ります。Geminiはcandidates[0].content.parts内の部分的なtextを送ります。

ストリーミングテキストを表示するUIを作る場合、各プロバイダー用のパーサーが必要になります。

システムプロンプトの扱い

OpenAI Chat Completions APIはメッセージ配列でsystemロールを受け取ります:

{"role": "system", "content": "You are a helpful assistant."}

OpenAIの新しいResponses APIはトップレベルのinstructionsパラメータを代わりに使います:

{
  "instructions": "You are a helpful assistant.",
  "input": [...]
}

Claudeはシステムプロンプトを別のトップレベルパラメータとして受け取ります:

{
  "system": "You are a helpful assistant.",
  "messages": [...]
}

Geminiはsystem_instructionを別フィールドとして使います:

{
  "system_instruction": {"parts": [{"text": "You are a helpful assistant."}]},
  "contents": [...]
}

単一インターフェースの背後でこれを抽象化する場合、システムメッセージをインターセプトして各プロバイダーの正しい場所にルーティングする必要があります。これにより、プロバイダーに関係なくモデルがシステムプロンプトを正しく受け取れるようになります。

トークンカウントとコスト

各プロバイダーの課金体系が異なるため、違いを理解しておくことをお勧めします。

OpenAIは入力トークンと出力トークンを別々に課金します。Claudeも同様ですが、モデルごとに価格帯が異なります。Geminiにはレート制限付きの無料枠と有料枠があります。

興味深い点として、同じプロンプトでもプロバイダーによって出力の長さが変わることがあります。各モデルにはデフォルトの出力量の傾向があります。入力が同一でもリクエストごとのコストが変動する可能性があります。

ユーザーが自分のAPIキーを使う構造であれば、リクエスト前に予想トークン数を表示することを検討してみてください。

エラーハンドリングの違い

各APIのエラー返却方式も異なります。

OpenAIはerror.messageを標準的なHTTPステータスコードとともに返します(429はレート制限、401は不正なキー)。

Claudeはerror.typeerror.message構造でエラーを返します。レート制限はrate_limit_errorとして返ってきます。

Geminiは場合によって200 OKを返しつつレスポンスボディ内にエラーを含むことがあるため、HTTPステータスコードだけでなくレスポンス内容も確認することが重要です。

// ステータスコードとレスポンスボディの両方を確認
if (response.ok) {
  const data = await response.json();
  // Geminiは200でもエラーを含む場合がある
  if (data.error) {
    throw new Error(data.error.message);
  }
}

もう一度最初からやるなら

最初からやり直すとしたら、こうすると思います:

  1. 即座にノーマライズする。 プロバイダー固有のレスポンス形式をアダプター層に隔離し、アプリケーションロジックをクリーンに保つ。

  2. 各プロバイダーの最安モデルでテストする。 開発中はGPT-4o-mini(または最新のGPT-5シリーズミニモデル)、Claude Haiku、Gemini Flashを使ってトークン予算を節約。

  3. OpenAI互換フォーマットを活用する。 OpenAIフォーマットをサポートするプロバイダーが多いため(OllamaやLM Studioなどのローカルツール含む)、1つの統合で80%のプロバイダーをカバー可能。

  4. 最初からストリーミングで設計する。 同期アーキテクチャに後からストリーミングを追加するのはかなりのリファクタリングが必要。当面不要でも最初からストリーミング前提で設計することをお勧めします。

  5. 開発中は生レスポンスをログに残す。 生のAPIレスポンスを保存しておくと、予期しない動作を調査する際にデバッグが格段に速くなります。

まとめ

2026年のLLM APIランドスケープは2024年と比較して大きく進化しました。

ほとんどのプロバイダーがファンクションコーリング、構造化出力、ビジョンをサポートしています。ベースラインの品質が十分に高いため、「最良の」モデルはベンチマーク順位よりも具体的なユースケースに依存します。OpenAIはChat Completions APIと新しいResponses APIの両方を提供しており、統合時に考慮すべき要素が1つ増えました。

同時に、各プロバイダーが独自の実装方式で機能を追加し続けています。MCP、ツール使用、マルチモーダル入力、構造化出力のすべてにプロバイダー間の差異があり、良い抽象化レイヤーの価値がますます高まっています。

この環境における重要なポイントは、「最良の」LLMを選ぶことではありません。アプリケーションを書き直すことなくプロバイダー間を切り替えられる、クリーンな抽象化を構築することです。

参考

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?