ほとんどの人は、プロジェクトを始めるときにプロバイダーの移植性について考えません。最も登録しやすいベンダーからキーを取得し、統合を書いて、出荷します。数か月後、ベンチマークを読んだり、請求書を開いたり、レート制限にぶつかったりして、別のものを試したくなります。そこで痛みが現れます。なぜなら、最初のプロバイダーに関する前提をHTTPレイヤーに焼き込んでしまっているからです。
解決策は抽象化ライブラリではありません。それはワイヤーフォーマットです。OpenAIのchat completions APIは、LLMと対話するためのデフォルトプロトコルに静かになりました。一度それに対して書けば、プロバイダーの切り替えは主に2つの文字列を変更するだけの問題になります。
みんながコピーしたAPI
OpenAIはシンプルな契約を定義しました。/v1/chat/completionsにJSONをPOSTすると、choices[0].message.content文字列が返ってきます。リクエストは次のようになります:
curl https://api.openai.com/v1/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-d '{
"model": "gpt-4o",
"messages": [{"role": "user", "content": "Explain DNS in one paragraph"}]
}'ほとんどすべての主要なモデルが今ではこの方言を話します。DeepSeek、Qwen、ERNIE、Doubao、その他はすべて、同じボディを受け入れ、同じ形状を返すエンドポイントを公開しています。つまり、クライアントコードは相手が誰であるかを気にしません。model文字列とエンドポイントを入れ替えるだけで、他には何も変わりません。
以下は、これらのモデルのいくつかを1つのベースURLの下に抱えるアグリゲーターを指した同じ呼び出しです:
curl https://api.token8341.com/v1/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKENWORKS_API_KEY" \
-d '{
"model": "deepseek-chat",
"messages": [{"role": "user", "content": "Explain DNS in one paragraph"}]
}'1つのクライアント、多数のモデル
Pythonでは、通常OpenAI SDKを一度インスタンス化し、リクエストごとにモデル名を渡します。プロバイダーが互換APIをサポートしているなら、base_urlを一度設定し、他はすべて同じに保ちます:
from openai import OpenAI
client = OpenAI(
base_url="https://api.token8341.com/v1",
api_key="sk-your-tokenworks-key",
)
def ask(model: str, prompt: str) -> str:
resp = client.chat.completions.create(
model=model,
messages=[{"role": "user", "content": prompt}],
)
return resp.choices[0].message.content
# Same function, three different model families.
print(ask("gpt-4o", "Summarize this log for me."))
print(ask("deepseek-chat", "Summarize this log for me."))
print(ask("qwen-max", "Summarize this log for me."))関数は変わりません。モデル文字列が変わります。それがすべてのトリックです。ストリーミングも同じように機能します:
stream = client.chat.completions.create(
model="deepseek-chat",
messages=[{"role": "user", "content": "Write a haiku about databases"}],
stream=True,
)
for chunk in stream:
delta = chunk.choices[0].delta.content or ""
print(delta, end="")関数呼び出し、JSONモード、埋め込みはすべて同じ互換サーフェスに乗っているので、切り替え時にプレーンなチャットに限定されることはありません。
切り替えで得られないもの
無料で変わらない部分について明確にしておきたいと思います。トランスポートは移植可能ですが、モデルはそうではありません。
異なるモデルはプロンプトに異なる反応をします。GPT-4o用に調整されたシステムプロンプトは、異なる構造を求める推論モデルでは性能を発揮しないことがあります。コンテキストウィンドウは異なり、時には大きく異なります。あるモデルは128kトークンを受け入れ、別のモデルは32kかもしれません。トークナイザーも異なるため、同じ文書が各モデルで異なる数のトークンを要します。最大出力長も、プロトコルごとではなくモデルごとのノブです。
つまり、「コードを変更せずに切り替える」は実際には「HTTPクライアントを書き直さずに切り替える」ということです。新しいモデルに本番トラフィックを向ける前に、評価実行を自分に課す必要があります。良いニュースは、互換APIがその評価を安く実行できることです。なぜなら、ハーネスはモデル名のループに過ぎないからです。
実用的なパターン
サイドプロジェクトで実際にやっていることは、コード変更ではなく小さな設定テーブルを保つことです:
MODELS = {
"fast": "deepseek-chat",
"smart": "qwen-max",
"frontier": "gpt-4o",
}
def run(task, prompt):
return ask(MODELS[task], prompt)fastレーンにもっと安いモデルを試したい?1行変更するだけ。最初のプロバイダーがレート制限されたときに2番目にフォールバックしたい?例外をキャッチして、次のモデル文字列で再試行するだけ。これらはどれもリクエスト構築に触れません。
そのフォールバックパターンこそ、マルチモデルエンドポイントがその価値を発揮するところです。1つのベースURLと1つのキーで、プロバイダー間でルーティング、再試行、A/Bテストができます。すべて、初日に書いた同じ2行のクライアントセットアップを通じて。
注意すべきレスポンスの癖
トランスポートは互換ですが、いくつかのレスポンスレベルの違いは依然として漏れ出します。推論モデルは時々、通常のメッセージと一緒にreasoning_contentのような余分なフィールドを返します。choices[0].message.contentを読むコードは関係なく動き続けますが、その推論テキストをデバッグビューに表示したいかもしれません。エラーメッセージも異なります。あるプロバイダーは役立つinsufficient_quota文字列を返し、別のプロバイダーはボディなしの429を返します。再試行を構築しているなら、ベンダー固有のテキストを解析するのではなく、任意の429または5xxを「バックオフして再試行」として扱ってください。
これはどれも1つのプロバイダーにロックされたままでいる理由にはなりません。エラー処理を汎用的に保ち、モデル選択を変数に保つ理由です。
これがSiCore TokenWorksが取る立場です:https://api.token8341.com/v1のOpenAI互換エンドポイントで、GPT-4o、Claude、Gemini、DeepSeek、Qwen、ERNIE、Doubao、Spark、Panguの間を移動するときに変わるのはモデル文字列だけです。