Function Calling Deep Dive — OpenAI, Anthropic, Gemini

> The three frontier providers converged on the same tool-call loop in 2024 and then diverged on everything else. OpenAI uses tools and tool_calls. Anthropic uses tool_use and tool_result blocks. Gemini uses functionDeclarations and unique-id correlation. This lesson diffs the three side by side so code that ships on one provider does not break when you port it.

Type: Build

Languages: Python (stdlib, schema translators)

Prerequisites: Phase 13 · 01 (the tool interface)

Time: ~75 minutes

Learning Objectives

The Problem

The shape of a function-calling request differs by provider. Three concrete examples from 2026 production stacks:

OpenAI Chat Completions / Responses API. You pass tools: [{type: "function", function: {name, description, parameters, strict}}]. The model's response contains choices[0].message.tool_calls: [{id, type: "function", function: {name, arguments}}] where arguments is a JSON string you must parse. Strict mode (strict: true) enforces schema compliance via 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 parsed (an object, not a string). You reply with a new user message containing 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 in Gemini 3 and up for parallel-call correlation. You reply with {functionResponse: {name, id, response}}.

Same loop. Different field names, different nesting, different string-vs-object conventions, different correlation mechanisms. A team that writes a weather agent on OpenAI pays a two-day port to Anthropic and another day to Gemini just for the plumbing.

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

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

Aspect OpenAI Anthropic Gemini
Declaration envelope {type: "function", function: {...}} {name, description, input_schema} {functionDeclarations: [{...}]}
Schema field parameters input_schema parameters
Response container tool_calls[] on assistant message content[] of type tool_use parts[] of type functionCall
Arguments type stringified JSON parsed object parsed object
Id format call_... (OpenAI generates) toolu_... (Anthropic) UUID (Gemini 3+)
Result block role tool, tool_call_id user with tool_result, tool_use_id functionResponse with matching id
Force-a-tool tool_choice: {type: "function", function: {name}} tool_choice: {type: "tool", name} tool_config: {function_calling_config: {mode: "ANY"}}
Forbid tools tool_choice: "none" tool_choice: {type: "none"} mode: "NONE"
Strict schema strict: true schema-is-schema (always enforced) responseSchema at request level

Limits you will actually hit

tool_choice behavior

Three modes everyone supports, named differently.

Plus one mode unique to each provider:

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:

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.

The translator pattern

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

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

Three tiny functions translate it to the three provider shapes. The harness in code/main.py does exactly this, then round-trips a fake tool call through each provider's response shape. No network required — this lesson teaches the shapes, 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.

Use It

code/main.py defines one canonical Tool dataclass and three translators that emit the OpenAI, Anthropic, and Gemini declaration JSON. It then parses a hand-crafted provider response of each shape into the same canonical call object, demonstrating that the semantics are identical under the skin. Run it and diff the three declarations side by side.

What to look at:

Ship It

This lesson produces outputs/skill-provider-portability-audit.md. Given a function-calling integration against one provider, the skill produces a portability audit: which provider limits it relies on, which fields need renaming, and what breaks when ported to each other provider.

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.
  1. 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.
  1. 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 lesson's diff table.
  1. 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.
  1. 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

Term What people say What 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