In the last lesson you built an agent with the Claude API. A fair question is whether everything you learned applies to other providers. The short answer is yes. Claude, OpenAI and Gemini all let a model ask your code to run a function, and the idea is the same on each. What changes is the spelling: field names, where the arguments appear, and how results go back. This lesson gives you the shared pattern, shows each provider's version, and ends with a way to write your tools once so you can move between providers without a rewrite.
One pattern, three spellings
Whatever the provider calls it, "tool use" and "function calling" mean the same thing. Anthropic's documentation puts it plainly: you specify what operations exist and what shape their inputs take, and the model decides when to call them. The model never runs anything. It returns a structured request, and your program does the work.
Every provider follows these five steps:

- Declare. Send a list of tools, each with a name, a description and a JSON Schema for its inputs.
- Request. The model replies, either with a final answer or with one or more tool calls.
- Run. Your code runs the function. This is your code, in your environment.
- Return. You send the result back, tagged with the id of the call it answers.
- Continue. The model reads the result and decides again. The loop from The agent loop repeats until it answers without a tool call.
If you understand these five steps, you can read any provider's documentation in a few minutes. The rest of this lesson is about what each step looks like in code.
Claude
You have seen this one already, so here is a quick reminder. Tools carry an input_schema. A tool request arrives as a tool_use block in response.content, with the stop reason tool_use. The arguments are already a parsed object in block.input. You return a tool_result block inside a user message, with tool_use_id set to the id of the request.
A few controls worth knowing from Anthropic's documentation:
tool_choicedecides whether the model may call tools. The default isauto, andnoneturns tool use off. The optionsany(call one of your tools) andtool(call a named tool) force a call, but not every model accepts them. Anthropic's documentation says they return a 400 error on Claude Opus 5.5, Sonnet 5.5, Fable 5.1 and Mythos 5.1, and also when manual extended thinking is on. On those models, useautowith strict tool use instead, and check the page for the model you use.disable_parallel_tool_useinsidetool_choiceasks for at most one tool call per turn.strict: trueon a tool definition makes the model's calls match your schema exactly.- Server tools such as web search run on Anthropic's side, so you never handle their results yourself.
OpenAI
OpenAI's current recommended interface is the Responses API. Here is the full round trip, following the pattern in OpenAI's function calling guide. The model name is a placeholder, because model names change often. Use the current tool-capable one from OpenAI's model page.
1import json
2from openai import OpenAI
3
4client = OpenAI()
5MODEL = "your-model-name"
6
7tools = [
8 {
9 "type": "function",
10 "name": "get_weather",
11 "description": "Get the current weather for a city.",
12 "parameters": {
13 "type": "object",
14 "properties": {"city": {"type": "string"}},
15 "required": ["city"],
16 },
17 }
18]
19
20input_list = [{"role": "user", "content": "What's the weather in Pune?"}]
21response = client.responses.create(model=MODEL, tools=tools, input=input_list)
22input_list += response.output
23
24for item in response.output:
25 if item.type == "function_call":
26 args = json.loads(item.arguments)
27 result = {"city": args["city"], "temperature_c": 31}
28 input_list.append(
29 {
30 "type": "function_call_output",
31 "call_id": item.call_id,
32 "output": json.dumps(result),
33 }
34 )
35
36final = client.responses.create(model=MODEL, tools=tools, input=input_list)
37print(final.output_text)Compare it with Claude, piece by piece:
- A tool is
{"type": "function", "name", "description", "parameters"}. The schema key isparameters, where Claude usesinput_schema. - A request is an item in
response.outputwith typefunction_call. It carriesname,argumentsand acall_id. argumentsis a JSON string, not an object. You must runjson.loadson it. This trips up many people moving from Claude.- The result is an item of type
function_call_outputwith the samecall_idand anoutputstring. - You add the model's output to your input list before adding the result, much as you append the assistant reply with Claude.
Controls from the same guide: tool_choice accepts auto, required, a named function, allowed_tools to restrict the set, or none. Setting parallel_tool_calls to false limits the model to zero or one call per turn. Strict mode ("strict": true) makes calls follow your schema, and it requires additionalProperties to be false and every property to be listed in required. OpenAI recommends turning it on.
The older Chat Completions format
You will still see the Chat Completions API in many tutorials and codebases, and it spells the same idea differently. Tools are wrapped as {"type": "function", "function": {...}}, requests appear in a tool_calls list on the assistant message, and each result goes back as a message with the role tool and a tool_call_id. The five steps are unchanged. If you meet this format, map the names using the table below and check the current documentation, since details differ between versions.
Gemini
Google's current documentation for Gemini function calling uses the Interactions API. Here is the round trip in that style:
1import json
2from google import genai
3
4client = genai.Client()
5MODEL = "your-model-name"
6
7declaration = {
8 "type": "function",
9 "name": "get_weather",
10 "description": "Get the current weather for a city.",
11 "parameters": {
12 "type": "object",
13 "properties": {"city": {"type": "string"}},
14 "required": ["city"],
15 },
16}
17
18interaction = client.interactions.create(
19 model=MODEL,
20 input="What's the weather in Pune?",
21 tools=[declaration],
22)
23
24call = next(s for s in interaction.steps if s.type == "function_call")
25result = {"city": call.arguments["city"], "temperature_c": 31}
26
27final = client.interactions.create(
28 model=MODEL,
29 previous_interaction_id=interaction.id,
30 tools=[declaration],
31 input=[
32 {
33 "type": "function_result",
34 "name": call.name,
35 "call_id": call.id,
36 "result": [{"type": "text", "text": json.dumps(result)}],
37 }
38 ],
39)
40print(final.output_text)What stands out:
- The tool declaration looks just like OpenAI's:
type,name,descriptionandparameters. - A request is a
function_callstep ininteraction.steps, withname,argumentsand anid. - The result goes back as a
function_resultstep whosecall_idis the step'sid. - State lives on the server. Instead of resending the whole history, you pass
previous_interaction_idand the service remembers the earlier turns. Claude and OpenAI, as shown above, have you resend the conversation yourself.
Gemini also has a tool_choice setting with modes such as auto, any (the model must call a function), none and validated, plus allowed_tools to restrict the set. Independent functions can be called in the same turn, and chained calls work too.
One note on versions. Google says the older generateContent API remains fully supported, while recommending the Interactions API for new work. Some features, such as automatic function calling in Python, were listed as available in generateContent but not yet in Interactions. If you read a Gemini tutorial that looks different from the code above, it probably uses the older API. The five steps still hold.
Side by side

| Claude | OpenAI (Responses) | Gemini (Interactions) | |
|---|---|---|---|
| Schema key | input_schema | parameters | parameters |
| Request is a | tool_use block | function_call item | function_call step |
| Arguments | input, a parsed object | arguments, a JSON string | arguments, an object |
| Result is a | tool_result block | function_call_output item | function_result step |
| Id that links them | tool_use_id | call_id | call_id |
| Conversation state | You resend messages | You resend the input list | Server keeps it, use previous_interaction_id |
| Force a tool call | tool_choice: any or a named tool, on models that allow it | tool_choice: required or a named function | tool_choice: any |
The names differ, but the ideas line up one to one. If you can do it on one provider, you can do it on the others.
Hosted tools and hosted agents
Providers also offer tools that run on their side, so you write no handler.
- Claude server tools include web search, web fetch and code execution. Anthropic runs them and returns the results in the same response, as you saw in The agent loop.
- OpenAI built-in tools are run by OpenAI. Its guide notes that built-in tools cannot be part of a parallel batch of function calls.
- Hosted agent services manage the loop and the conversation state for you. OpenAI's older Assistants API worked this way, but forum posts quoting OpenAI's notice say it was scheduled to shut down on 26 August 2026, with the Responses API as its replacement. Check OpenAI's own pages for the current status before you rely on any of it.
Hosted options save code, and they tie you to one vendor. The trade is the same one as with frameworks: convenience now against control and portability later. For learning, and for any agent where you want to see every step, keep your own loop.
Write your tools once
Because the three formats map so closely, you can describe each tool once in your own neutral shape and convert it per provider. This small adapter is the idea:
1import json
2
3TOOL = {
4 "name": "get_weather",
5 "description": "Get the current weather for a city.",
6 "schema": {
7 "type": "object",
8 "properties": {"city": {"type": "string", "description": "City name, for example Pune"}},
9 "required": ["city"],
10 },
11}
12
13
14def for_claude(t):
15 return {"name": t["name"], "description": t["description"], "input_schema": t["schema"]}
16
17
18def for_openai(t):
19 return {"type": "function", "name": t["name"], "description": t["description"], "parameters": t["schema"]}
20
21
22for_gemini = for_openai
23
24
25def claude_calls(response):
26 return [(b.id, b.name, b.input) for b in response.content if b.type == "tool_use"]
27
28
29def openai_calls(response):
30 return [(i.call_id, i.name, json.loads(i.arguments)) for i in response.output if i.type == "function_call"]
31
32
33def gemini_calls(interaction):
34 return [(s.id, s.name, s.arguments) for s in interaction.steps if s.type == "function_call"]Each *_calls function turns a provider's reply into the same list of (id, name, args) tuples. Your run_tool(name, args) from the last lesson then works unchanged, and only the small glue code around it differs. The result-building step is the other half: a tool_result block for Claude, a function_call_output item for OpenAI, a function_result step for Gemini.
Two cautions. These adapters cover the common case only. Provider features such as strict mode, caching and extra content types need their own handling. And frameworks such as LiteLLM or the Vercel AI SDK do this translation for you, which Frameworks compared weighs against writing it yourself.
What changes when you switch
The tool concept moves across easily. These things do not, so test them again on any new provider:
- Tool choice quality. Models differ in when they call a tool and how well they fill in the arguments. A description that works on one may need rewording on another.
- Schema limits. Strict modes put rules on your schema, such as listing every property as required. A schema that is valid on one provider can be rejected by another.
- Parallel calls. Support and defaults differ, so your loop must handle one call or several.
- Error handling. Rate limits and error shapes differ. Errors, retries and rate limits covers handling them.
- Cost and speed. Each provider counts and bills tokens differently, so measure with the model you pick.
Mistakes beginners make
- Forgetting that OpenAI's
argumentsis a string. Parse it withjson.loadsbefore use. - Mixing up the schema key. It is
input_schemafor Claude andparametersfor OpenAI and Gemini. - Returning a result with the wrong id. Use
tool_use_idfor Claude andcall_idfor the others. - Not adding the model's own output back before the result, on providers where you resend history.
- Copying a tutorial that uses an older API and wondering why the field names differ. Check the date and the API name.
- Assuming a schema is portable. Strict modes and parallel behavior vary.
- Hard-coding model names. They change often. Keep one
MODELsetting. - Locking into a hosted agent service before you know you need it.
Quick recap
- Claude, OpenAI and Gemini share one pattern: declare tools, get a call, run it, return the result with its id, repeat.
- Names differ:
input_schemaversusparameters,tool_useversusfunction_call,tool_use_idversuscall_id. - OpenAI sends arguments as a JSON string. Claude and Gemini send them as objects.
- Claude and OpenAI have you resend the history. Gemini's Interactions API can keep it on the server.
- Hosted tools save code but tie you to a vendor.
- Describe tools once in a neutral shape and convert per provider, then retest the behavior on each.
Try it yourself
Take the notes agent from the last lesson and write down, field by field, what its get_weather-style round trip would look like on OpenAI and on Gemini, using the table above. Then copy the adapter into a file, run the three *_calls functions on small fake replies of your own, and confirm they return the same tuple. If you have a key for a second provider, port the loop and compare how many steps the same question takes.
Next lesson: Errors, retries and rate limits.