TAI BUI
Back to curriculum
BuildPython

Function Calling Deep Dive — OpenAI, Anthropic, Gemini

OpenAI, Anthropic, and Gemini all settled on the same tool-call loop and then went their own ways on the details. OpenAI uses `tools` and `tool_calls`. Anthropic uses `tool_use` and `tool_result` blocks. Gemini uses `functionDeclarations` and per-call ids. This lesson puts the three side by side so code written for one provider survives a port to another.

TIME: ~75 minutesPREREQUISITES: Phase 13 · 01 (the tool interface)

Learning Objectives

  • State the three shape differences between OpenAI, Anthropic, and Gemini function-calling payloads (declaration, call, result).
  • Translate one tool declaration across all three provider formats and predict where strict-mode constraints will differ.
  • Use `tool_choice` in each provider to force, forbid, or auto-pick tool calls.
  • Know the per-provider hard limits (tool count, schema depth, argument length) and the error signatures each one emits when limits are violated.

The Problem

The shape of a function-calling request depends on the provider. Three examples from 2026 production stacks:

OpenAI Chat Completions / Responses API. You pass tools: [{type: "function", function: {name, description, parameters, strict}}]. The response carries choices[0].message.tool_calls: [{id, type: "function", function: {name, arguments}}], and arguments is a JSON string you have to parse. Strict mode (strict: true) enforces schema compliance with constrained decoding.

Anthropic Messages API. You pass tools: [{name, description, input_schema}]. The response comes back as content: [{type: "text"}, {type: "tool_use", id, name, input}]. input is already an object, not a string. You answer with a new user message holding a {type: "tool_result", tool_use_id, content} block.

Google Gemini API. You pass tools: [{functionDeclarations: [{name, description, parameters}]}], nested under functionDeclarations. The response arrives as candidates[0].content.parts: [{functionCall: {name, args, id}}], where id is unique from Gemini 3 on so parallel calls can be correlated. You answer with {functionResponse: {name, id, response}}.

Same loop, different field names, different nesting, different string-versus-object conventions, different correlation mechanisms. A team that writes a weather agent on OpenAI pays two days to port it to Anthropic and another day for Gemini, and all of it is plumbing.

This lesson builds a translator that turns the three formats into one canonical tool declaration and routes at the edge. Phase 13 · 17 generalizes the same pattern into an LLM gateway.

Warm-up0/3
1. In the tool interface from Phase 13 · 01, each tool has a name and a description. What else does the model need to call it correctly?Warm-up
A tool card with a name, get_weather, and a two-line description about current conditions in a named city, next to a call the model has to write with its arguments left as question marks.
Name and description are the lesson's weather tool (main.py).
2. A model proposes a call whose arguments do not match the tool's declared schema. What should the host do before running the tool?Warm-up
The weather schema says city is a string. The model proposes get_weather with city set to the number 42. The call goes from the model to the host, and the host's next step is a question mark.
Typed illustration, not captured output. The value 42 is made up to show a mismatch.
3. In JSON Schema, how do you restrict a string parameter to a fixed set of allowed values?Warm-up
A parameter called units that must be celsius or fahrenheit, next to a JSON Schema property with type string and one line left as question marks.
Values are the lesson's weather tool; the property is a simplified typed version.
Check: The Problem0/7
1. What type is the `arguments` field on an OpenAI tool call response?Define
An OpenAI chat completion message with role assistant and one tool call with id call_abc123 and function name get_weather. The value of the arguments field is hidden behind question marks.
Values come from the lesson's main.py.
2. What is function calling?Define
A request with a user question about the weather in Bengaluru and one tool, get_weather, goes to a provider's model. The response box is left as a question mark.
Typed illustration, not captured output.
3. What is a `tool_use` block in Anthropic's Messages API?Define
An Anthropic response message with role assistant and a content list holding a text block that says Looking that up and a tool_use entry with id toolu_xyz789, name get_weather and an input object.
Values come from the lesson's main.py.
4. What is a `functionCall` part in a Gemini response?Define
A Gemini response content with role model and a parts list holding one functionCall entry with id fc-9a3d, name get_weather and an args object with city Bengaluru and units celsius.
Values come from the lesson's main.py.
5. A weather agent written for OpenAI costs days to port to Anthropic and Gemini even though all three run the same loop. Why?Explain
A weather agent on OpenAI is ported to Anthropic, taking about two days, and then to Gemini, taking another day. A dashed box asks what takes the days.
Day counts come from the lesson's Problem section.
6. Your agent works on Anthropic. After a port to OpenAI, every tool call crashes when you pass the call's arguments straight to the tool function. What is the most likely cause and fix?Analyze
Two code snippets with the same line, get_weather called with double-star args. Before the port, args are the arguments of the Anthropic call and it works. After the port, args are the arguments of the OpenAI call and it crashes on every call.
Typed illustration, not captured output.
7. Team A keeps three copies of its agent loop, one per provider. Team B keeps one loop over a canonical `Tool` and `Call`, with translators at the edge. Why does B port to a new provider faster?Analyze
Team A has three copies of the agent loop, one per provider. Team B has one loop over a canonical Tool and Call with translators at the edge. A dashed box asks what must be written when a new provider appears.
Typed illustration, not captured output.

The Concept

The common structure

Every provider needs five things:

  1. Tool list. Per-tool name, description, and input schema.
  2. Tool choice. Force a specific tool, forbid tools, or let the model decide.
  3. Call emission. Structured output naming the tool and arguments.
  4. Call id. Correlate the response to the right call (matters for parallel).
  5. Result injection. A message or block that ties the result back to the call.

Shape diffs, field by field

AspectOpenAIAnthropicGemini
Declaration envelope{type: "function", function: {...}}{name, description, input_schema}{functionDeclarations: [{...}]}
Schema fieldparametersinput_schemaparameters
Response containertool_calls[] on assistant messagecontent[] of type tool_useparts[] of type functionCall
Arguments typestringified JSONparsed objectparsed object
Id formatcall_... (OpenAI generates)toolu_... (Anthropic)UUID (Gemini 3+)
Result blockrole tool, tool_call_iduser with tool_result, tool_use_idfunctionResponse with matching id
Force-a-tooltool_choice: {type: "function", function: {name}}tool_choice: {type: "tool", name}tool_config: {function_calling_config: {mode: "ANY"}}
Forbid toolstool_choice: "none"tool_choice: {type: "none"}mode: "NONE"
Strict schemastrict: trueschema-is-schema (always enforced)responseSchema at request level

Limits you will actually hit

  • OpenAI. 128 tools per request. Schema depth 5. Argument string <= 8192 bytes. Strict mode requires no $ref, no oneOf/anyOf/allOf with overlap, every property listed in required.
  • Anthropic. 64 tools per request. Schema depth effectively unbounded but practical limit 10. No strict-mode flag; schema is a contract and the model tends to comply.
  • Gemini. 64 functions per request. Schema types are OpenAPI 3.0 subset (slight divergence from JSON Schema 2020-12). Parallel calls unique-id since Gemini 3.

tool_choice behavior

Three modes everyone supports, named differently.

  • Auto. Model picks tool or text. Default.
  • Required / Any. Model must call at least one tool.
  • None. Model must not call tools.

Plus one mode unique to each provider:

  • OpenAI. Force a specific tool by name.
  • Anthropic. Force a specific tool by name; disable_parallel_tool_use flag separates single vs multi.
  • Gemini. mode: "VALIDATED" routes every response through a schema validator regardless of model intent.

Parallel calls

OpenAI's parallel_tool_calls: true (default) emits multiple calls in one assistant message. You run them all and reply with a batched tool-role message containing one entry per tool_call_id. Anthropic historically did single-call; disable_parallel_tool_use: false (default as of Claude 3.5) enables multi. Gemini 2 allowed parallel calls but did not give stable ids; Gemini 3 adds UUIDs so out-of-order responses correlate cleanly.

Streaming

All three support streamed tool calls. The wire format differs:

  • OpenAI. Delta chunks of tool_calls[i].function.arguments arrive incrementally. You accumulate until finish_reason: "tool_calls".
  • Anthropic. Block-start / block-delta / block-stop events. input_json_delta chunks carry partial arguments.
  • Gemini. streamFunctionCallArguments (new in Gemini 3) emits chunks with a functionCallId so multiple parallel calls can interleave.

Phase 13 · 03 goes deep on parallel + streaming reassembly. This lesson focuses on the declaration and single-call shapes.

Errors and repair

Invalid-argument errors look different too.

  • OpenAI (non-strict). Model returns arguments: "{bad json}", your JSON parse fails, you inject an error message and re-call.
  • OpenAI (strict). Validation happens during decoding; invalid JSON is impossible but refusal can appear.
  • Anthropic. input may contain unexpected fields; schema is advisory. Validate server-side.
  • Gemini. OpenAPI 3.0 quirk: enum on object fields silently ignored; validate yourself.

The translator pattern

A canonical tool declaration in your code looks like this (you pick the shape):

python
Tool(
    name="get_weather",
    description="Use when ...",
    input_schema={"type": "object", "properties": {...}, "required": [...]},
    strict=True,
)

Three small functions translate it to the three provider shapes. code/main.py does exactly this, then parses a hand-written response in each provider's shape back into one Call object. No network is involved, because the point is the shapes and not the HTTP.

Production teams wrap this translator in AbstractToolset (Pydantic AI), UniversalToolNode (LangGraph), or BaseTool (LlamaIndex). Phase 13 · 17 ships a gateway that exposes an OpenAI-shaped API in front of any of the three.

Check: The Concept0/18
1. Which three `tool_choice` modes do all three providers support, under different names?Define
Three request boxes for OpenAI, Anthropic and Gemini, each offering the tool get_weather with the tool choice setting left as question marks. Each box points to a dashed box asking for its values.
Typed illustration, not captured output.
2. What does a tool declaration contain?Define
A request body skeleton with a model, one user message asking about weather in Bengaluru, and a tools list whose single entry is hidden behind question marks.
Typed illustration, not captured output. Field layout is simplified.
3. What does OpenAI's strict mode do?Define
The OpenAI form of the weather tool with name, shortened description, collapsed parameters and strict set to true highlighted.
Shape from main.py's to_openai; schema and description collapsed.
4. What are parallel tool calls?Define
A user asks for the weather in Bengaluru and in Mumbai with one tool, get_weather, available. Below it, a dashed box asks what the assistant reply contains.
Typed illustration, not captured output. Mumbai and the wording are made up.
5. In OpenAI strict mode, what does a `refusal` mean when you expected a tool call?Define
An OpenAI strict-mode assistant message with role assistant, content null and a refusal field whose text is elided. There is no tool_calls field.
Typed illustration, not captured output. The refusal text is elided.
6. What is the OpenAPI 3.0 subset in Gemini's function calling?Define
The Gemini form of the weather tool: functionDeclarations holding a name, a shortened description, and a collapsed parameters field highlighted. A note names the schema types as an OpenAPI 3.0 subset.
Shape from main.py's to_gemini; schema and description collapsed.
7. What field does Anthropic's Messages API use to tie a `tool_result` block back to its originating call?Define
An Anthropic assistant message with a tool_use block that has id toolu_xyz789, and below it a user message with a tool_result block whose linking field and value are question marks.
First panel from main.py; second panel is a typed skeleton.
8. Which OpenAPI 3.0 quirk affects Gemini's schema handling?Explain
Three boxes in a row: a declaration labelled OpenAPI 3.0 subset, the Gemini model that reads the schema, and call arguments that reach your code. A dashed box asks which constraint is treated differently.
Typed illustration, not captured output.
9. What changed about Gemini's parallel function calls between Gemini 2 and Gemini 3?Explain
A parts list with two functionCall entries for get_weather, and a dashed box asking what changed between Gemini 2 and Gemini 3.
Typed illustration, not captured output. Layout simplified from main.py's response.
10. Which set of requirements does OpenAI strict mode add on top of plain JSON Schema?Explain
A plain JSON Schema for an object with city and units string properties, an arrow labelled strict, and a dashed box asking which requirements strict mode adds.
Typed illustration, not captured output.
11. What wire-format detail is unique to Gemini 3 streaming?Explain
Four stream chunks labelled chunk 1 to chunk 4, each a streamed piece. A dashed box asks for the wire-format detail unique to Gemini 3 streaming.
Typed illustration, not captured output.
12. How many tools can a single OpenAI request include at most?Apply
Five boxes labelled tool 1, tool 2, tool 3, an ellipsis and tool N, as entries of one OpenAI request's tools list. A dashed box asks for the largest N allowed.
Typed illustration, not captured output.
13. How does Anthropic force the model to call a tool named "x"?Apply
An Anthropic request skeleton with a tools list that includes a tool named x and a tool_choice field hidden behind question marks. The goal is to make the model call x.
Typed illustration, not captured output.
14. What are Anthropic's per-request tool cap and schema depth limit?Apply
On the left, a row of tool boxes tool 1, tool 2, ellipsis and tool N, with a dashed box asking for the largest N. On the right, nested rectangles labelled schema level 1, schema level 2 and schema level ellipsis, and a dashed box asking for the deepest level allowed.
Typed illustration, not captured output.
15. Your Gemini tool declares an `enum` on a field inside an object. The model sometimes sends a value outside the enum, and nothing errors. What is the likely cause and fix?Analyze
A Gemini declaration with a units enum of celsius and fahrenheit, and a call that arrives with units kelvin and no error from Gemini.
Typed illustration, not captured output. The value kelvin is made up.
16. An Anthropic tool call arrives with an extra `verbose` field you never declared, and your tool function throws on it. What explains this?Analyze
The weather tool declares city and units. The call that arrived also has a verbose field. The tool function takes only city and units and throws when called with all three fields.
Typed illustration, not captured output. The verbose field is from the stem.
17. After you turn on OpenAI strict mode, some responses hold no tool call and no malformed JSON, only a refusal. Which assumption in your code is wrong?Analyze
Four responses after enabling strict mode: three hold a tool call and one, response 2, holds only a refusal. A note says no response holds malformed JSON.
Typed illustration, not captured output. The order and count are made up.
18. You must add an optional `units` field to a tool on OpenAI strict mode, which requires every property in `required`. What is the design?Design
Two cards: the tool needs units to be optional, and strict mode demands every property in required. Both point to a dashed box asking for the schema design of units.
Typed illustration, not captured output.

Use It

code/main.py defines one canonical Tool dataclass and three translators (to_openai, to_anthropic, to_gemini) that emit each provider's declaration JSON. It also converts a canonical ToolChoice into each provider's tool-choice shape (tool_choice for OpenAI and Anthropic, function_calling_config for Gemini). Then it parses a hand-written response in each provider's format into the same Call object (parse_openai, parse_anthropic, parse_gemini), which shows the semantics match underneath. Run it and compare the three declarations side by side.

What to look at:

  • The three declarations differ only in envelope and field names. The Gemini one also uppercases the schema type values and drops additionalProperties.
  • The three responses differ in where the call lives: tool_calls on the message, a tool_use entry in content[], or a functionCall entry in parts[].
  • The final equivalence check confirms all three parse to the same name and the same args.
Check: Use It0/7
1. Which provider's translator in `main.py` has to rewrite the schema itself, instead of only wrapping or renaming it?Explain
A canonical Tool with name, description and input_schema feeds three translator functions, to_openai, to_anthropic and to_gemini. Each output is a dashed question box.
Values come from the lesson's main.py.
2. What do `parse_openai`, `parse_anthropic`, and `parse_gemini` in `main.py` do?Apply
Three rows naming parse_openai, parse_anthropic and parse_gemini, each taking that provider's response. Each row points to a dashed box asking what the function returns.
Values come from the lesson's main.py.
3. What does `tool_choice_anthropic(ToolChoice(mode="required"))` return in `main.py`?Apply
The ToolChoice dataclass from main.py with mode and tool_name fields, and the call tool_choice_anthropic with mode required whose return value is hidden behind question marks.
Values come from the lesson's main.py.
4. What is the `city` property after `to_gemini(WEATHER)` runs on the lesson's weather tool?Apply
In the lesson's weather tool, city has type string. After to_gemini runs, the city property in the output is hidden behind question marks.
Values come from the lesson's main.py.
5. What happens to the `units` property's type `["string", "null"]` in the Gemini output of `main.py`?Apply
In the lesson's weather tool, units has a type list of string and null plus an enum. After to_gemini runs, the type of units in the output is hidden behind question marks.
Values come from the lesson's main.py. The enum values are elided.
6. What does `tool_choice_gemini(ToolChoice(mode="force", tool_name="get_weather"))` return in `main.py`?Apply
The call tool_choice_gemini with mode force and tool name get_weather, whose return value is hidden behind question marks.
Values come from the lesson's main.py.
7. Your agent must be able to force one named tool on OpenAI, Anthropic, and Gemini from a single setting. Which design fits?Design
An agent setting, force get_weather, points to OpenAI, Anthropic and Gemini. Each provider has a dashed box asking for the request field.
Typed illustration, not captured output.

Ship It

This lesson produces outputs/skill-provider-portability-audit.md. Point it at a function-calling integration written for one provider and it returns a portability audit: which provider limits the code relies on, which fields need renaming, and what breaks on each of the other two.

Check: Ship It0/1
1. What does the provider-portability-audit skill report for an integration built against one provider?Explain
A snippet of code that calls an OpenAI chat completion with tools goes into the provider-portability audit skill, which produces a report shown as a question mark.
Typed illustration, not captured output.
Author's Note

I'd run main.py first and read the three declarations before the field-by-field table. The canonical Tool carries one input_schema, and the translators barely touch it. OpenAI wraps it in a function envelope and adds strict. Anthropic renames the field. Gemini is the only one that rewrites the schema itself, uppercasing every type and dropping additionalProperties.

The weather tool's units field is worth a closer look. Its type is ["string", "null"] and it is listed in required, which is how the toy satisfies strict mode's every-property-required rule while still letting the value be absent. The Gemini translator only uppercases a type that is a plain string, so that list passes through untouched. A real Gemini integration would need its own handling for nullable fields.

The code stops at shapes. Nothing here sends a request, so it cannot tell you which provider rejects a schema or how it words the error. Treat the limits and error behavior in the body as claims to check against each provider's docs before you depend on them. If you pick one thing to build on, I'd pick the ToolChoice mapping, since it is the piece where a canonical type pays for itself immediately.

Exercises

  1. Run code/main.py and verify that the three provider declaration JSONs all serialize the same underlying Tool object. Modify the canonical tool to add an enum parameter and confirm only the Gemini translator needs to handle the OpenAPI quirk.

  2. Add a ListToolsResponse parser for each provider that extracts the tool list a model returns after a list_tools or discovery call. OpenAI does not have one natively; note this asymmetry.

  3. Implement tool_choice conversion: map a canonical ToolChoice(mode="force", tool_name="x") into all three provider shapes. Then map mode="any" and mode="none". Check the shape-diff table in The Concept.

  4. Pick one of the three providers and read its function-calling guide end to end. Find one field in its schema spec that the other two do not support. Candidates: OpenAI strict, Anthropic disable_parallel_tool_use, Gemini function_calling_config.allowed_function_names.

  5. Write a test vector: a tool call whose arguments violate the declared schema. Run it through each provider's validator (the stdlib one in Lesson 01 will do as a proxy) and record which errors fire. Document which provider you would use in production for strictness.

Key Terms

TermWhat people sayWhat it actually means
Function calling"Tool use"Provider-level API for structured tool-call emission
Tool declaration"Tool spec"Name + description + JSON Schema input payload
tool_choice"Force / forbid"Auto / required / none / specific-name modes
Strict mode"Schema enforcement"OpenAI flag that constrains decoding to match schema
tool_use block"Anthropic's call shape"Inline content block with id, name, input
functionCall part"Gemini's call shape"A parts[] entry containing name, args, and id
Arguments-as-string"Stringified JSON"OpenAI returns args as a JSON string, not an object
Parallel tool calls"Fan-out in one turn"Multiple tool calls in one assistant message
Refusal"Model declines"Strict-mode-only refusal block instead of a call
OpenAPI 3.0 subset"Gemini schema quirk"Gemini uses a JSON-Schema-like dialect with minor differences

Further Reading

Code

Ships With