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#
| Concept | OpenAI Chat | Anthropic | Gemini |
|---|---|---|---|
| Tool list field | tools | tools | tools |
| Per-tool wrapper | {type:"function","function":{...}} | bare {...} (no wrapper) | {functionDeclarations:[...]} |
| Parameter schema field | function.parameters | input_schema | parameters |
| Tool-choice policy | tool_choice | tool_choice | toolConfig.functionCallingConfig |
| How the model's call comes back | message.tool_calls[i] | content[i] type:tool_use | parts[i].functionCall |
| How you return the result | role:"tool" + tool_call_id | content type:tool_result + tool_use_id | parts[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)
functionResponsemust be wrapped withtypes.Part.from_function_response, and itsresponsefield is a dict; (2) the model's previousfunctionCallcontent must go back intocontentsverbatim, 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
functionwrapper —name,descriptionandparameterssit flat - The model's call comes back as a
function_calloutput item (notmessage.tool_calls) - Results go back as a
function_call_outputinput item (not arole:"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#
- Tool schemas are validated strictly —
parametersmust be valid JSON Schema, and the model generates arguments against it - Arguments are a string (OpenAI) or an object (Anthropic / Gemini) — parse accordingly
- Don't return long base64 images in tool results — they count as input tokens on the next turn and the bill explodes
- The built-in tools (
web_search/image_generation) need no schema from you, but requirestream=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 toweb_search_20250305) and the Gemini line (translated togoogleSearch); GLM, DeepSeek, Grok and others don't support it by defaultimage_generation(drawing inside a conversation):gpt-5.4/gpt-5.5only. For standalone image generation use the per-image/v1/images/generations— see Image / video / music APIs
- 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 thanfunction,web_searchandimage_generation(such asfile_searchorcode_interpretercarried over from OpenAI) are rejected with 400 and a message likeTool type 'file_search' is not supported. Full list: Error codes