cd ../back to blog
$Guide//June 4, 2026//7 min read

Claude と Gemini をまたぐツール使用 — 1 つの base_url で動く関数呼び出し

OpenAI 互換の tools スキーマで Claude と Gemini の両方を 1 つの base_url から動かす実践ガイド。ツール定義、tool_calls の読み取り、マルチターンのループ、並列呼び出しまでを解説します。

ツール使用 — 関数呼び出し(function calling)とも呼ばれます — は、チャット モデルを実際に物事をこなせる存在に変えるものです。レコードを参照し、 API を叩き、計算を実行し、データベースに問い合わせる。モデル自身がコードを 実行するわけではありません。どの関数をどんな引数で呼ぶべきかを伝えてくるので、 あなたがそれを実行し、結果を返すと、モデルが回答を仕上げます。 朗報は、OpenAI の tools の形が事実上の標準であり、Brievio を 通せば、まったく同じコードが 1 つの base_url の背後で Claude と Gemini の両方を動かすことです。model 文字列を変えるだけで、ほかはすべてそのままにしておけます。

この記事は実践版です。ツールを定義し、tool_calls を読み取り、 マルチターンのループを最初から最後まで回し、並列呼び出しを処理する。 どのスニペットも OpenAI Python SDK を使って https://api.brievio.com/v1 に対してそのまま実行できます。 本番で驚かずにすむよう、モデルファミリー間で挙動が実際に異なる数少ない箇所には 印を付けておきます。

頭の中のモデル: 魔法の呼び出しではなく、ループ

関数呼び出しは一発勝負ではなく、会話です。常に同じ 4 拍子をたどります。

  • あなたが送る — ユーザーのメッセージと、モデルが使ってよい ツールの一覧。
  • モデルが判断する。 文章で答えるか、あるいは 1 つ以上の tool_calls — 関数名と引数の JSON 文字列 — を返して止まる
  • あなたが関数を実行する — 自分のコードの中で実行し、その 結果を tool メッセージとしてメッセージ一覧に追加する。
  • あなたが再びモデルを呼ぶ — より長くなった履歴とともに。 モデルは結果を読み、回答するか、別のツールを求める。ツール呼び出しが なくなるまで繰り返す。

モデルはあなたのシステムに触れることは決してありません。提案するだけで、 実行の判断はあなたのコードが下します。この境界こそがツール使用のセキュリティの すべてです — モデルが送ってくる引数はどれも信頼できない入力として扱い、 フォームの入力欄と同じように検証してください。

ステップ 1 — ツールを定義し、呼び出しを読む

ツールとは {"type": "function", ...} で包んだ JSON Schema です。description フィールドは飾りではありません — モデルがいつ・ どのように呼ぶかを判断するために読む、唯一の手がかりです。新人エンジニア向けの docstring を書くつもりで書いてください。

define_tool.py
# 標準的な OpenAI の "function" スキーマでツールを定義し、モデルが返す
# tool_calls を読み取る。Claude でも Gemini でもまったく同じ形。
from openai import OpenAI
import json

client = OpenAI(
    api_key="sk-brievio-...",
    base_url="https://api.brievio.com/v1",
)

tools = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "Get the current weather for a city.",
            "parameters": {
                "type": "object",
                "properties": {
                    "city": {"type": "string", "description": "e.g. 'Tokyo'"},
                    "unit": {
                        "type": "string",
                        "enum": ["celsius", "fahrenheit"],
                        "description": "Temperature unit.",
                    },
                },
                "required": ["city"],
            },
        },
    }
]

messages = [{"role": "user", "content": "What's the weather in Tokyo?"}]

resp = client.chat.completions.create(
    model="claude-sonnet-4-6",   # "gemini-2.5-pro" に差し替えても以下のコードは同じ
    messages=messages,
    tools=tools,
    tool_choice="auto",          # 呼び出すかどうかはモデルに委ねる
)

msg = resp.choices[0].message

# モデルは文章で答えず、関数を実行するよう求めてきた。
if msg.tool_calls:
    for call in msg.tool_calls:
        print(call.function.name)             # "get_weather"
        print(call.function.arguments)         # '{"city": "Tokyo", "unit": "celsius"}'
        args = json.loads(call.function.arguments)  # 常に JSON 文字列 — パースすること
else:
    print(msg.content)           # ツール不要の通常の回答

ここで人がつまずく点が 2 つあります。第一に、 function.arguments は dict ではなく JSON 文字列 です — 必ず json.loads してください。第二に、モデルはツールを 呼ばないことを選ぶ場合があり、そのときは tool_calls が空で、content に通常の回答が入ります。両方に分岐させましょう。 これは modelclaude-sonnet-4-6 に設定しても gemini-2.5-pro に設定しても同じです。Brievio はリクエストを 本物のファーストパーティのモデルに渡し、ネイティブのツール呼び出しを 返します — 作り替えたり偽装したりはしません。

ステップ 2 — マルチターンのループ

では往復をつなぎましょう。肝心なのはその形です。アシスタントのメッセージを返ってきたとおり厳密に追加し(呼び出しの id を保持しているため)、 続いて呼び出しごとに 1 つの tool メッセージを追加して、それぞれが 自分の tool_call_id を返すようにします。id を取り違えると次の リクエストは 400 を返します。これが両プロバイダーに対して 1 つの関数で動く ループの全体です。

tool_loop.py
# マルチターンのループ: モデルが求める -> あなたが関数を実行する ->
# その結果を返す -> モデルが最終的な回答を書く。
def run_get_weather(city: str, unit: str = "celsius") -> dict:
    # 実際の実装: HTTP 呼び出し、DB 参照、なんでも。
    return {"city": city, "temp": 18, "unit": unit, "sky": "clear"}

TOOL_IMPLS = {"get_weather": run_get_weather}

def answer(question: str, model: str) -> str:
    messages = [{"role": "user", "content": question}]

    while True:
        resp = client.chat.completions.create(
            model=model,
            messages=messages,
            tools=tools,
            tool_choice="auto",
        )
        msg = resp.choices[0].message

        # ツールの要求なし -> これが最終回答。完了。
        if not msg.tool_calls:
            return msg.content

        # 1. アシスタントのターンを返ってきたとおり厳密に追加する(次の
        #    メッセージが参照すべき tool_call の id を保持しているため)。
        messages.append(msg)

        # 2. 要求された各関数を実行し、呼び出しごとに 1 つの tool メッセージを
        #    追加して、対応する tool_call_id を返す。
        for call in msg.tool_calls:
            fn = TOOL_IMPLS[call.function.name]
            args = json.loads(call.function.arguments)
            result = fn(**args)
            messages.append({
                "role": "tool",
                "tool_call_id": call.id,          # 呼び出しの id と必ず一致させる
                "content": json.dumps(result),    # 結果を文字列化する
            })
        # 3. ループ: モデルはツールの出力を見て続きを進める。

# 同じ関数が 1 つの base_url の背後で両プロバイダーに対して動作する:
print(answer("What's the weather in Tokyo?", "claude-sonnet-4-6"))
print(answer("What's the weather in Tokyo?", "gemini-2.5-pro"))

この while True こそ、あなたがこれまで使ってきたあらゆる エージェントのエンジンです。モデルはツールを連鎖させられます — search を呼び、結果を読み、上位ヒットに対して get_details を呼び、それから答える — そしてこのループは特別な 場合分けなしに任意の深さを処理します。混乱したモデルが永遠に回り続けない よう、ガードレールとしてターンカウンタを追加してください。ほとんどの アプリでは 8〜10 ラウンドが妥当な上限です。

移植性について正直な注意点が 1 つ。プロトコルは Claude と Gemini で 同一ですが、挙動はクローンではありません。モデルファミリーによって 選ぶツールが違い、引数の表現が異なり、既存の知識から答えるのとツールを呼ぶのと どちらに積極的かもばらつきます。コードは変わりません。変わるのは判断です。 一方が他方に完璧に移ると決めつけず、本番投入する予定の各モデルに対して プロンプトをテストしてください。

ステップ 3 — 並列ツール呼び出し

質問が複数の独立した参照を必要とするとき — 3 都市の天気、5 つの SKU の 在庫 — 能力の高いモデルは 1 回のアシスタントターンですべての呼び出しを 返せます。あなたはそれらを(I/O バウンドな処理なら並行して)実行し、 再び問い合わせる前に id ごとに 1 つの tool メッセージを返します。

parallel_calls.py
# 並列ツール呼び出し: 1 回のアシスタントターンで複数の関数を同時に
# 要求できる。あなたはそれらを(必要なら並行して)実行し、呼び出しの id ごとに
# 1 つの tool メッセージを返す。モデルが呼び出しをまとめるかどうかはまちまち —
# だから常にリストを反復処理し、ちょうど 1 件だと決めつけないこと。
messages = [{"role": "user",
             "content": "Compare the weather in Tokyo, Paris and Cairo."}]

resp = client.chat.completions.create(
    model="claude-sonnet-4-6",
    messages=messages,
    tools=tools,
    tool_choice="auto",
)
msg = resp.choices[0].message
messages.append(msg)

# msg.tool_calls には今や別々の id を持つ get_weather 呼び出しが 3 件あるかもしれない。
from concurrent.futures import ThreadPoolExecutor

def handle(call):
    args = json.loads(call.function.arguments)
    result = TOOL_IMPLS[call.function.name](**args)
    return {
        "role": "tool",
        "tool_call_id": call.id,
        "content": json.dumps(result),
    }

with ThreadPoolExecutor() as pool:
    tool_msgs = list(pool.map(handle, msg.tool_calls or []))

messages.extend(tool_msgs)   # 次のリクエストの前にすべての結果を追加する

final = client.chat.completions.create(
    model="claude-sonnet-4-6", messages=messages, tools=tools,
)
print(final.choices[0].message.content)

# 注: モデルがまとめてではなく 1 件ずつ呼び出しを返す場合でも、前のスニペットの
# ループがそのまま処理してくれる — ただ実行ラウンドが増えるだけ。

ここがモデルファミリー間でもっとも差が出る部分なので、決めつけを ハードコードしないでください。あるモデルが 1 ターンで並列に 呼び出しを出すか、それとも複数ターンにわたって 1 件ずつたどるかは、 ファミリーによって、ときにはリクエストによっても異なります。対策は単純で、 すでに上のコードに入っています。tool_calls を 反復処理し、必要ならループにラウンドを増やして回させることです。 返ってきたリストをループするコードはどちらの場合も正しく、ちょうど 1 件の 呼び出しだと決めつけるコードがバグです。同様に、厳格なスキーマの強制 (JSON が必ず妥当で、余分なキーは拒否される)も一律ではありません — どのモデルが生成したかにかかわらず、引数はサーバー側で検証し続けてください。

1 つの base_url こそが本当の勝ち筋

ゲートウェイがなければ、Claude Gemini をサポートするとは、 2 つの SDK、2 つの認証方式、2 つのペイロード形、そして 2 通りの ツール結果の配管を意味します — 一方は Anthropic の tool_use/tool_result のコンテンツブロック、 もう一方は Google の function-call パーツ。Brievio の OpenAI 互換 エンドポイントの背後では、どちらも上で見た Chat Completions の tools 方言を話すので、モデル間の A/B テストは 1 行の差分で済み、 ツール層は一度書けば済みます。ツールのフィールドを含む完全な リクエスト/レスポンス仕様は Chat Completions のドキュメントにあり、 正確な id を載せた最新のモデル一覧は モデルページにあります。

はっきり言っておく価値があります。この価値が成り立つのは、向こう側の モデルが本物である場合に限られます。ツール呼び出しは実のところ有用な 本物らしさのシグナルです — 本物のフラッグシップは、込み入ったスキーマに 対しても妥当な引数を伴う整った tool_calls を確実に生成しますが、 安価な身代わりは JSON をしくじったりツールを無視したりしがちです。Brievio は 本物のファーストパーティのモデル(Claude Sonnet 4.6、Opus 4.7、 Gemini 2.5 Pro/Flash など)を提供し、ネイティブのツール呼び出しを尊重し、 正直なトークン数を報告します。自分で確かめたいなら、 あなたの Claude が本当に Claude かを確かめる方法をご覧ください。

現場の短いチェックリスト

  • 引数は必ずパースする。 function.arguments は文字列です。json.loads してから検証し、使ってください。
  • id を返す。 アシスタントのメッセージをそのまま追加し、 続いて呼び出しごとに 1 つの tool メッセージを、一致する tool_call_id とともに追加する。すべて次のリクエストの前に。
  • リストをループする。 1 ターンに 1 件の呼び出しと決して 決めつけず、0 件・1 件・複数件を処理する。この習慣 1 つで、並列型でも 逐次型でも、どちらのモデルもそのまま動きます。
  • ラウンドに上限を設ける。 ターンカウンタが無限の ツール呼び出しスパイラルを防ぎ、コストを抑えます。
  • 何も信用しない。 引数はモデルの出力です。型・範囲・ 権限を、ユーザー入力とまったく同じように検証してください。

この 5 つを正しく押さえれば、Claude と Gemini をまたいで変更なしに動く ツール使用エージェントが手に入り、リクエストごとにコストや能力で ルーティングする選択肢も得られます。なお、失敗した 4xx/5xx 呼び出しは Brievio では課金されないので、 ツール定義を仕上げる過程で避けられないスキーマ調整の反復は無料です。 ツールの背後にどのモデルを置くか決める準備ができたら、 ゲートウェイ選定ガイド が本番で本当に効いてくるトレードオフを順に解説しています。