Claude Opus 5 is now live on CometAPI →

単一プロバイダーにロックインされないAIアプリの構築方法

CometAPI
AnnaJun 7, 2026
単一プロバイダーにロックインされないAIアプリの構築方法

AIアプリにおけるベンダーロックインは、たいてい一度に起こるものではありません。少しずつ入り込んできます。ここで直接 import openai し、そこでモデル名をハードコードし、別プロバイダーが同じものを返すか確認せずにレスポンスフィールドをパースする。6か月後、プロバイダーを切り替えるにはバックエンドの半分を書き直すことになります。

ロックインが起こる4つのパターン

多くの開発者は、ロックインとは「OpenAI SDK を使っていること」だと考えます。それは最も危険性の低い種類です。本当の落とし穴はもっと微妙です。

ロックインの種類起こり方結果
SDK ロックインfrom openai import OpenAI everywhereSDK を切り替えるには全ファイルを触る必要がある
モデル名ロックインmodel="gpt-4o" hardcoded in business logicモデル変更のたびにコード変更が必要になる
パラメータロックインUsing logprobs, n>1, or reasoning_effortClaude や Gemini には存在しない
レスポンス形式ロックインParsing provider-specific response fieldsプロバイダーごとに返す構造が異なる

目的は、これらをすべて排除することではありません。一部は許容できるトレードオフです。目的は、自分がどれを引き受けているのかを把握することです。

抽象化レイヤーとして OpenAI 互換エンドポイントを使う

SDK ロックインを避ける最もクリーンな方法は、複数のプロバイダーへルーティングできる単一の OpenAI 互換エンドポイントを使うことです。OpenAI SDK はそのまま使いながら、バックエンドは任意のプロバイダーにできます。

CometAPI はこれを実現します。1つのエンドポイント、1つのキーで、OpenAI、Anthropic、Google、DeepSeek、xAI などにまたがる500以上のモデルを利用できます。

import osfrom openai import OpenAIfrom dotenv import load_dotenv​load_dotenv()​api_key = os.environ.get("AI_API_KEY")if not api_key:    raise ValueError("AI_API_KEY environment variable is not set")​client = OpenAI(    base_url=os.environ.get("AI_BASE_URL", "https://api.cometapi.com/v1"),    api_key=api_key,)

GPT から Claude、Gemini への切り替えは1行の変更です。

# Beforeresponse = client.chat.completions.create(model="gpt-5.4", messages=[...])​# After — same code, different modelresponse = client.chat.completions.create(model="claude-sonnet-4-6", messages=[...])

注: gpt-5.4claude-sonnet-4-6 のようなモデル名は CometAPI のプラットフォーム識別子です。これらは https://api.cometapi.com/v1 経由でのみ機能し、OpenAI や Anthropic の API で直接使うことはできません。完全なカタログと料金については、モデル一覧全体を参照してください。

モデル名をビジネスロジックから切り離す

コード全体に散らばったモデル名は、最も一般的なロックインの形です。解決策は、環境変数から読み込む中央設定を用意することです。

# config.py — one place to change model assignmentsimport os​MODEL_CONFIG = {    "summarize": os.environ.get("MODEL_SUMMARIZE", "claude-opus-4-7"),    "code":      os.environ.get("MODEL_CODE",      "gpt-5.4"),    "classify":  os.environ.get("MODEL_CLASSIFY",  "claude-haiku-4-5"),    "chat":      os.environ.get("MODEL_CHAT",       "gpt-5.4-mini"),}​# Validate at startup — fail fast rather than getting mysterious API errorsfor task, model in MODEL_CONFIG.items():    if not model:        raise ValueError(f"Model config for '{task}' is not set")

ビジネスロジックでは、モデル名を直接参照しません。

from config import MODEL_CONFIG​def summarize(text: str) -> str:    response = client.chat.completions.create(        model=MODEL_CONFIG["summarize"],        messages=[{"role": "user", "content": f"Summarize: {text}"}],        max_tokens=300  # move to config in production    )    return response.choices[0].message.content

アプリ全体で要約モデルを切り替えるには、環境変数を1つ変更するだけです。grep も検索置換も不要です。

コードがプロバイダー固有フィールドに依存しないようレスポンスをラップする

プロバイダーによってレスポンスの構造は少しずつ異なります。コードベース全体で生の API レスポンスをパースしていると、そのプロバイダーの形式にロックインされます。

正規化された dataclass にラップします。

from dataclasses import dataclassfrom typing import Optionalfrom openai import OpenAI, APIStatusError, APIConnectionError, APITimeoutErrorfrom openai.types.chat import ChatCompletionimport logging​@dataclassclass AIResponse:    content: str    model: str    input_tokens: int    output_tokens: int​def call_model(task: str, messages: list, **kwargs) -> AIResponse:    """    Single entry point for all LLM calls.    Returns a normalized AIResponse regardless of which model handled it.    Raises on 4xx (client errors). Logs and re-raises on 5xx/network errors.    """    model = MODEL_CONFIG.get(task, "gpt-5.4-mini")    if not model:        raise ValueError(f"No model configured for task '{task}'")​    try:        response: ChatCompletion = client.chat.completions.create(            model=model,            messages=messages,            **kwargs        )    except APIStatusError as e:        logging.error(f"API error for task={task} model={model}: {e.status_code} {e.message}")        raise    except (APIConnectionError, APITimeoutError) as e:        logging.error(f"Network error for task={task} model={model}: {e}")        raise​    # content is None when the model triggers a tool call instead of returning text    content = response.choices[0].message.content or ""​    # usage is None in streaming mode — default to 0 if not available    usage = response.usage    input_tokens = usage.prompt_tokens if usage else 0    output_tokens = usage.completion_tokens if usage else 0​    logging.info(        f"task={task} model={model} "        f"input_tokens={input_tokens} output_tokens={output_tokens}"    )​    return AIResponse(        content=content,        model=response.model,        input_tokens=input_tokens,        output_tokens=output_tokens,    )

これでビジネスロジックは、生の API レスポンスではなく AIResponse オブジェクトを扱います。プロバイダーがレスポンス形式を変更しても、修正箇所は1か所です。

ラッパーにストリーミング対応を追加する

チャットインターフェースではストリーミングが必要になるでしょう。ラッパーでは別経路として扱います。

from typing import Iterator​def stream_model(task: str, messages: list, **kwargs) -> Iterator[str]:    """    Stream tokens from the routed model.    Note: streaming doesn't return usage data.    Fallback is not supported in streaming mode — you've already    started yielding tokens before you know if the full request succeeds.    """    model = MODEL_CONFIG.get(task, "gpt-5.4-mini")    if not model:        raise ValueError(f"No model configured for task '{task}'")​    stream = client.chat.completions.create(        model=model,        messages=messages,        stream=True,        **kwargs    )​    for chunk in stream:        delta = chunk.choices[0].delta.content        if delta:            yield delta​# Usagefor token in stream_model("chat", [{"role": "user", "content": "Hello"}]):    print(token, end="", flush=True)

どのパラメータがロックインを生むかを把握する

一部のパラメータは特定のプロバイダーにしか存在しません。使うこと自体は問題ありません。ただし、意図的な選択をしていることを理解しておく必要があります。

パラメータ対応しているものロックインリスク
logprobsGPT のみ高 — Claude や Gemini に同等機能がない
n > 1GPT、Gemini(Claude は非対応)中 — Claude ではループが必要
reasoning_effortGPT o-series のみ高 — 他には同等機能がない
temperature > 1.0GPT、Gemini(Claude は非対応)低 — Claude は 1.0 が上限
tools主要プロバイダーすべてなし — 安全に使用可能
response_format主要プロバイダーすべて低 — スキーマ差異は軽微

信頼度スコアリングに logprobs を使っているなら、その機能については GPT にロックインされています。それは妥当なトレードオフです。ただし、次の開発者が理由を理解できるように文書化しておきましょう。

プロバイダーエンドポイントを設定可能にする

base_url="https://api.cometapi.com/v1" をハードコードすることも、依然としてロックインの一種です。環境変数にしましょう。

# .env — using CometAPIAI_BASE_URL=https://api.cometapi.com/v1AI_API_KEY=your_cometapi_key​# To switch to OpenAI directly, change two lines:# AI_BASE_URL=https://api.openai.com/v1# AI_API_KEY=your_openai_key

Step 1 のクライアント初期化は、すでにこれらの変数から読み込むようになっています。CometAPI とプロバイダーへの直接接続の切り替えは、コード変更ではなく設定変更になります。

Node.js 版

import OpenAI from 'openai';​const apiKey = process.env.AI_API_KEY;if (!apiKey) throw new Error('AI_API_KEY is not set');​const client = new OpenAI({  baseURL: process.env.AI_BASE_URL ?? 'https://api.cometapi.com/v1',  apiKey,});​// Model IDs are CometAPI platform identifiers — see cometapi.com/modelsconst MODEL_CONFIG = {  summarize: process.env.MODEL_SUMMARIZE ?? 'claude-opus-4-7',  code:      process.env.MODEL_CODE      ?? 'gpt-5.4',  classify:  process.env.MODEL_CLASSIFY  ?? 'claude-haiku-4-5',  chat:      process.env.MODEL_CHAT      ?? 'gpt-5.4-mini',};​// Validate at startupfor (const [task, model] of Object.entries(MODEL_CONFIG)) {  if (!model) throw new Error(`Model config for '${task}' is not set`);}​/** * Single entry point for all LLM calls. * Returns normalized response. Raises on 4xx, logs and re-raises on 5xx/network. */async function callModel(task, messages, options = {}) {  const model = MODEL_CONFIG[task] ?? 'gpt-5.4-mini';​  let response;  try {    response = await client.chat.completions.create({      model,      messages,      ...options,    });  } catch (err) {    // Don't swallow errors — log and re-raise    console.error(`API error task=${task} model=${model}:`, err.message);    throw err;  }​  // content is null when model triggers a tool call  const content = response.choices[0].message.content ?? '';​  // usage may be absent in some configurations  const inputTokens  = response.usage?.prompt_tokens     ?? 0;  const outputTokens = response.usage?.completion_tokens ?? 0;​  console.log(`task=${task} model=${model} input=${inputTokens} output=${outputTokens}`);​  return { content, model: response.model, inputTokens, outputTokens };}​/** * Stream tokens from the routed model. * Usage data is not available in streaming mode. */async function* streamModel(task, messages, options = {}) {  const model = MODEL_CONFIG[task] ?? 'gpt-5.4-mini';​  const stream = await client.chat.completions.create({    model,    messages,    stream: true,    ...options,  });​  for await (const chunk of stream) {    const delta = chunk.choices[0]?.delta?.content;    if (delta) yield delta;  }}​// Usage — blockingconst result = await callModel('classify', [  { role: 'user', content: 'Positive or negative? "Loved it!"' }]);console.log(result.content);​// Usage — streamingfor await (const token of streamModel('chat', [  { role: 'user', content: 'Hello' }])) {  process.stdout.write(token);}

許容できるロックイン

すべてのロックインに対抗する価値があるわけではありません。意味のあるトレードオフもあります。

  • OpenAI SDK を使うこと — 事実上の標準です。ほとんどのプロバイダーが対応しています。低リスクのロックインです。
  • 実際に必要なプロバイダー固有機能logprobs が必要なら使いましょう。そのコードを分離しておけば、後で見つけて置き換えやすくなります。
  • ファインチューニング済みモデル — ファインチューニング済みモデルは本質的に1つのプロバイダーに結び付きます。それは想定どおりです。

避けるべきロックインは、偶発的なものです。ビジネスロジック内のモデル名、複数ファイルに散らばった生レスポンスのパース、ソース内にハードコードされた API キーです。

次の内容

これで、プロバイダーの詳細をビジネスロジックから切り離す抽象化レイヤーができました。このシリーズの最後の記事では、問題が起きたときの対処を扱います。失敗した生成のデバッグ方法、エラーコードの解釈、実際に何が壊れたのかを伝えるエラーハンドリングの構築方法です。

次回: 失敗した AI API 生成をデバッグする方法

FAQ

Q: SDK ロックインとモデルロックインの違いは何ですか?

SDK ロックインとは、コードが特定のライブラリをインポートしており、SDK を切り替える場合に変更が必要になることです。モデルロックインとは、モデル名がビジネスロジック全体に散らばっていることです。現在は多くのプロバイダーが OpenAI SDK 形式に対応しているため、SDK ロックインの危険性は比較的低いです。モデルロックインは、見つけて修正するのが難しいため、より厄介です。

Q: CometAPI を使うと、単に OpenAI ロックインを CometAPI ロックインに置き換えているだけではありませんか?

部分的にはそうです。直接プロバイダーへのロックインを、プロキシレイヤーへの依存に置き換えています。利点は、1つのキー、1つのエンドポイント、簡単なモデル切り替えです。リスクは、CometAPI に障害が発生すると、すべてのプロバイダーがまとめて使えなくなることです。緩和策は上のコードにすでに入っています。AI_BASE_URL は環境変数です。CometAPI を迂回してプロバイダーを直接呼び出す必要がある場合、それはコード変更ではなく設定変更です。

Q: このパターンで Claude の extended thinking や OpenAI の reasoning_effort を使えますか?

はい、call_model**kwargs として渡せます。ただし、そのタスクを別のモデルにルーティングした場合、それらのパラメータは無視されるかエラーになります。どのタスクがプロバイダー固有機能を使っているのかを文書化し、次の開発者が理由を理解できるようにしてください。

Q: Claude と GPT** の間でルーティングする場合、Claude の temperature 上限 1.0 にはどう対応すればよいですか?**

両方で安全な範囲に収めるには、temperature を 1.0 以下に保ちます。創造的なタスクで GPT 専用により高い temperature が必要な場合は、汎用ルーターに任せるのではなく、MODEL_CONFIG でそれらのタスクを明示的に GPT にルーティングしてください。

Q: 画像生成 API や動画生成 API も同じように抽象化すべきですか?

同じ原則が適用されます。中央設定、正規化されたレスポンスラッパー、ビジネスロジックにプロバイダー固有フィールドを持ち込まないことです。画像 API や動画 API は構造上の違いが大きいため(非同期と同期、異なるパラメータセットなど)、抽象化レイヤーにはより多くの作業が必要です。まずテキストから始め、構造が実証されてからパターンを拡張してください。

Q: モデル間の context window の違いはどう扱えばよいですか?

これはルーティング時の実際のリスクです。GPT-5.5 は 1M トークンの context window を持ち、Claude モデルは最大 200K、Gemini 3.5 Flash は最大 1M をサポートします。長文ドキュメントのタスクをより短い context window のモデルにルーティングすると、入力が黙って切り詰められます。タスクが長い入力を扱う場合は、ルーティング前にコンテキスト長チェックを追加してください。あるいは、長いコンテキストを必要とするタスクは、デフォルトに流すのではなく MODEL_CONFIG で特定のモデルに常にルーティングしてください。

AI開発コストを20%削減する準備はできていますか?

数分で無料スタート。無料トライアルクレジット付き。クレジットカード不要。

もっと読む