Tool calling

OpenAI Chat & Responses function tools, Anthropic tool use and Gemini function calling side by side, plus which models support the built-in tools

Let the model decide when to run your code — check the weather, query a database, call another API, drive some software…

All four chat protocol endpoints (OpenAI Chat, OpenAI Responses, Anthropic Messages and Gemini Native) support function/tool calling, but the field names and schema shapes differ. This page lines them up and gives the practices that work.

Concepts side by side#

ConceptOpenAI ChatAnthropicGemini
Tool list fieldtoolstoolstools
Per-tool wrapper{type:"function","function":{...}}bare {...} (no wrapper){functionDeclarations:[...]}
Parameter schema fieldfunction.parametersinput_schemaparameters
Tool-choice policytool_choicetool_choicetoolConfig.functionCallingConfig
How the model's call comes backmessage.tool_calls[i]content[i] type:tool_useparts[i].functionCall
How you return the resultrole:"tool" + tool_call_idcontent type:tool_result + tool_use_idparts[i].functionResponse

OpenAI Chat —— Function Tools#

1. Define the tool#

tools = [{
    "type": "function",
    "function": {
        "name": "get_weather",
        "description": "Get the current weather in a city",
        "parameters": {
            "type": "object",
            "properties": {
                "city": {"type": "string", "description": "City name"},
                "unit": {"type": "string", "enum": ["c", "f"], "default": "c"},
            },
            "required": ["city"],
        },
    },
}]

2. Make the call#

from openai import OpenAI
client = OpenAI(api_key="sk-gpushare-xxx", base_url="https://jiuye.zsopc.com/v1")

messages = [{"role": "user", "content": "Weather in Tokyo?"}]
resp = client.chat.completions.create(
    model="claude-sonnet-4-6",
    messages=messages,
    tools=tools,
)

3. Handle the model's tool call#

import json

choice = resp.choices[0]
if choice.finish_reason == "tool_calls":
    # append the assistant message (with all its tool_calls) exactly once.
    # putting it inside the loop produces an illegal [assistant, tool, assistant, tool]
    # sequence on multi-tool calls; the protocol requires every tool result to follow
    messages.append(choice.message)

    for tc in choice.message.tool_calls:
        args = json.loads(tc.function.arguments)
        result = get_weather(**args)  # your real function

        # one role:"tool" result per tool_call
        messages.append({
            "role": "tool",
            "tool_call_id": tc.id,
            "content": json.dumps(result),
        })

    # send another round to get the final answer
    final = client.chat.completions.create(
        model="claude-sonnet-4-6",
        messages=messages,
        tools=tools,
    )
    print(final.choices[0].message.content)

Anthropic Messages —— Tool Use#

1. Define the tool#

tools = [{
    "name": "get_weather",
    "description": "Get the current weather in a city",
    "input_schema": {
        "type": "object",
        "properties": {
            "city": {"type": "string"},
            "unit": {"type": "string", "enum": ["c", "f"]},
        },
        "required": ["city"],
    },
}]

2. Make the call#

from anthropic import Anthropic
client = Anthropic(api_key="sk-gpushare-xxx", base_url="https://jiuye.zsopc.com")

messages = [{"role": "user", "content": "Weather in Tokyo?"}]
resp = client.messages.create(
    model="claude-sonnet-4-6",
    max_tokens=1024,
    tools=tools,
    messages=messages,
)

3. Handle the model's tool call#

import json

if resp.stop_reason == "tool_use":
    tool_blocks = [b for b in resp.content if b.type == "tool_use"]
    tool_results = []
    for tb in tool_blocks:
        result = get_weather(**tb.input)
        tool_results.append({
            "type": "tool_result",
            "tool_use_id": tb.id,
            "content": json.dumps(result),
        })

    messages.append({"role": "assistant", "content": resp.content})
    messages.append({"role": "user", "content": tool_results})

    final = client.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=1024,
        tools=tools,
        messages=messages,
    )

Gemini Native —— Function Calling#

1. Define the tool#

from google.genai import types

weather_tool = types.Tool(function_declarations=[
    types.FunctionDeclaration(
        name="get_weather",
        description="Get the current weather in a city",
        parameters={
            "type": "object",
            "properties": {
                "city": {"type": "string"},
                "unit": {"type": "string", "enum": ["c", "f"]},
            },
            "required": ["city"],
        },
    )
])

2. Make the call#

from google import genai
client = genai.Client(api_key="sk-gpushare-xxx", http_options={"base_url": "https://jiuye.zsopc.com"})

response = client.models.generate_content(
    model="gemini-2.5-pro",
    contents="Weather in Tokyo?",
    config=types.GenerateContentConfig(tools=[weather_tool]),
)

3. Handle the model's tool call#

candidate = response.candidates[0]
function_calls = [p.function_call for p in candidate.content.parts if p.function_call]

if function_calls:
    # second-round contents: the original question + the model's functionCall turn (verbatim) + functionResponse parts
    contents = [
        types.Content(role="user", parts=[types.Part.from_text(text="Weather in Tokyo?")]),
        candidate.content,  # the model's previous turn (with the functionCall part) must be sent back too
    ]
    response_parts = [
        types.Part.from_function_response(
            name=fc.name,
            response={"result": get_weather(**dict(fc.args))},
        )
        for fc in function_calls
    ]
    contents.append(types.Content(role="user", parts=response_parts))

    final = client.models.generate_content(
        model="gemini-2.5-pro",
        contents=contents,
        config=types.GenerateContentConfig(tools=[weather_tool]),
    )
    print(final.text)

The two mistakes people make with Gemini: (1) functionResponse must be wrapped with types.Part.from_function_response, and its response field is a dict; (2) the model's previous functionCall content must go back into contents verbatim, or the model has no idea what it called. The Gemini Native channel is a byte pass-through, so the shape of what you send back is entirely your responsibility.

OpenAI Responses (/v1/responses) —— Function Tools#

Responses-API clients such as Codex CLI use POST /v1/responses, where function tools also work but the field shapes differ from Chat Completions:

  • Tool definitions have no function wrappername, description and parameters sit flat
  • The model's call comes back as a function_call output item (not message.tool_calls)
  • Results go back as a function_call_output input item (not a role:"tool" message)
import json
from openai import OpenAI
client = OpenAI(api_key="sk-gpushare-xxx", base_url="https://jiuye.zsopc.com/v1")

tools = [{
    "type": "function",
    "name": "get_weather",          # flat, no function:{...} wrapper
    "description": "Get the current weather in a city",
    "parameters": {
        "type": "object",
        "properties": {"city": {"type": "string"}},
        "required": ["city"],
    },
}]

resp = client.responses.create(model="gpt-5.5", input="Weather in Tokyo?", tools=tools)

input_items = [{"role": "user", "content": "Weather in Tokyo?"}]
for item in resp.output:
    if item.type == "function_call":
        result = get_weather(**json.loads(item.arguments))
        input_items.append(item)  # put the model's function_call item back verbatim
        input_items.append({
            "type": "function_call_output",
            "call_id": item.call_id,
            "output": json.dumps(result),
        })

final = client.responses.create(model="gpt-5.5", input=input_items, tools=tools)
print(final.output_text)

Tool calling across vendors#

The protocol translation layer lets you call any model with any SDK, and tools work across vendors too:

# tool calling on Claude, from the OpenAI SDK
from openai import OpenAI
client = OpenAI(api_key="sk-gpushare-xxx", base_url="https://jiuye.zsopc.com/v1")

resp = client.chat.completions.create(
    model="claude-sonnet-4-6",  # Claude goes through T1 translation
    messages=[{"role": "user", "content": "Weather in Tokyo?"}],
    tools=tools,  # the same tools definition as under "OpenAI Chat — 1. Define the tool"
)
# resp.choices[0].message.tool_calls is identical to plain OpenAI

A translated path adds X-Protocol-Translation: openai_chat_to_anthropic_messages to the response headers so you can tell which route you took. If a capability was degraded or dropped in translation (a restricted cache_control, say), an X-Protocol-Warning header spells out what — check it first when debugging "why didn't my tool fire". Note that when routing picks a native protocol channel neither header appears; their absence is normal and doesn't indicate a problem.

Advanced#

Forcing a tool call#

# OpenAI: tool_choice names a specific tool
tool_choice = {"type": "function", "function": {"name": "get_weather"}}

# Anthropic: tool_choice requires a tool to be used
tool_choice = {"type": "tool", "name": "get_weather"}

# Gemini: function_calling_config mode = ANY
config = types.GenerateContentConfig(
    tool_config=types.ToolConfig(
        function_calling_config=types.FunctionCallingConfig(mode="ANY")
    )
)

Multiple tools#

Every protocol's SDK accepts several tools in one request, and the model may return several calls at once:

# a single OpenAI response can carry multiple tool_calls
for tc in resp.choices[0].message.tool_calls:
    handle(tc)

Tools while streaming#

Tool calling does work with stream=true. The delta.tool_calls in incremental chunks accumulate field by field (function.arguments arrives in slices), so the client must concatenate every chunk to get complete JSON.

Things to know#

  1. Tool schemas are validated strictlyparameters must be valid JSON Schema, and the model generates arguments against it
  2. Arguments are a string (OpenAI) or an object (Anthropic / Gemini) — parse accordingly
  3. Don't return long base64 images in tool results — they count as input tokens on the next turn and the bill explodes
  4. The built-in tools (web_search / image_generation) need no schema from you, but require stream=true (see Streaming) and are only available on some models — check the capability columns in Models:
    • web_search: supported on GPT (gpt-5.4 / gpt-5.5), Claude (translated to web_search_20250305) and the Gemini line (translated to googleSearch); GLM, DeepSeek, Grok and others don't support it by default
    • image_generation (drawing inside a conversation): gpt-5.4 / gpt-5.5 only. For standalone image generation use the per-image /v1/images/generations — see Image / video / music APIs
  5. Unsupported tool types always 400 — a built-in tool on a model or channel that doesn't support it returns 400 tool_not_supported, and tool types other than function, web_search and image_generation (such as file_search or code_interpreter carried over from OpenAI) are rejected with 400 and a message like Tool type 'file_search' is not supported. Full list: Error codes