Tool Calling Vendor Matrix (2026)

10 min read

K7
Deep Dive · Tool & Capability Design

"Portable tool defs" was fiction; the 2026 vendor matrix has enough shared surface to look uniform and enough divergence to break every naive port.

OpenAI Chat Completions vs Responses API. Anthropic's Programmatic Tool Calling and Tool Search Tool. Gemini's OpenAPI subset. Same underlying idea — model proposes a tool call, host executes, model reads the result — packaged three different ways, with three different JSON Schema dialects, three different streaming shapes, and three different ways to name "the same" flag. Teams port a working tool spec from one vendor to another expecting the diffs to be cosmetic; the diffs are the reason your parallel-call chain silently degrades or your enum constraint quietly disappears. This essay is the matrix, the divergences that matter (JSON Schema subsets, streaming, parallel calls, custom grammars), and the small set of tool-def shapes that survive a port unchanged.

STEP 1

The shared surface: model proposes, host executes, model reads.

At the level a whiteboard drawing captures, every 2026 vendor implements the same three-move loop. The host presents a set of tool definitions alongside the user prompt; the model, at some assistant turn, chooses to emit one or more tool calls instead of (or alongside) a text reply; the host runs the calls out-of-band, packages the results as a synthetic message the model expects, and hands the loop back for another turn. The tool-calling concept works this shape at a beginner's altitude. The three-vendor divergence starts one layer down — in what a "tool definition" contains, how the model signals its choice, and how the result is threaded back — and everything below is that layer down.

Three shared conventions are worth pinning first because they are the closest thing to a portable subset. All three vendors accept JSON Schema for the argument shape (with subsets — see K10). All three name the loop primitive the same way at the user's altitude: OpenAI and Gemini call the model's emission a tool call (OpenAI's Responses API and Chat Completions both settled here after retiring "function_call"); Anthropic calls it a tool_use block, but the JSON payload has the same three fields — an id, a name, and a JSON object of arguments. All three carry the result back as a message whose role is either tool (OpenAI, Gemini's functionResponse) or a tool_result block (Anthropic) referencing the call id. A tool implementation that speaks JSON Schema args in and a JSON object out will not be the porting bottleneck. The bottleneck is everything the spec adds around that.

The tool-calling standards essay treats the loop as a protocol question; what this essay adds is the vendor-by-vendor idiom you write when you actually reach for an SDK. The structured tool I/O essay owns the return-payload half of the story; this essay owns the request-side surface — where a tool def lives, what fields it exposes, and where each vendor added a knob the other two do not have.

STEP 2

OpenAI: Chat Completions vs Responses, and where they diverge.

OpenAI ships two live surfaces in 2026 and the choice is not academic. Chat Completions is the older shape, stable, universally supported by proxies and framework glue, and is where most existing code lives. Responses API is the newer shape, aligns tool calls with a first-class item type in a typed response stream, and is where new features (Reasoning items, custom-grammar tools, computer-use tool built-ins) land first. Both accept the same tool-def shape at the top — {"type": "function", "function": {"name", "description", "parameters"}} in Chat Completions, and the flatter {"type": "function", "name", "description", "parameters"} in Responses. A port between the two flavours is mostly one level of nesting different, plus the streaming shape (see K11).

Two OpenAI-specific knobs are load-bearing and easy to miss. parallel_tool_calls defaults to true on both surfaces, letting the model emit multiple tool calls in one assistant turn — the host runs them concurrently and returns all results before the next turn. Setting it to false serializes them, which is the fix when your tools are stateful or when the model's parallelization is picking a bad order. tool_choice takes "auto" (default), "none", "required" (model must call some tool), or a named tool object (must call this one). Custom tools with a Lark or regex grammar arrived on Responses in early 2026 — the model emits text that matches your grammar, and the surface handles the constrained decoding without you writing a tool schema at all. The strict: true flag on a tool's parameters is what actually enforces the JSON Schema against the model's output; without it, the schema is a hint, not a guarantee.

# OpenAI (Chat Completions) — the tool def shape
tools = [{
    "type": "function",
    "function": {
        "name": "refund_order",
        "description": "Refund an order. Use for defective items or customer-reported issues.",
        "parameters": {
            "type": "object",
            "properties": {
                "order_id": {"type": "string"},
                "reason": {"type": "string", "enum": ["defective", "wrong_item", "other"]},
            },
            "required": ["order_id", "reason"],
            "additionalProperties": False,
        },
        "strict": True,
    },
}]
resp = client.chat.completions.create(model="gpt-5.1", messages=msgs,
    tools=tools, parallel_tool_calls=True, tool_choice="auto")

The Responses-API port removes the outer "function" nesting and moves the tool calls into the typed output array as function_call items with an arguments string, a name, and a call_id. New code should default to Responses; framework code that already speaks Chat Completions has no forced migration and will keep working. What you do not want to do is generate tool defs for both surfaces from a single template that omits the nesting difference — the SDK will silently accept the malformed shape and drop the tools.

STEP 3

Anthropic: PTC, Tool Search, and Tool Use Examples.

Anthropic's tool surface uses tool_use and tool_result content blocks inside the messages array — no separate tools field on the message role, just typed blocks the model produces and consumes. The tool def itself is flatter than OpenAI's Chat Completions shape: {"name", "description", "input_schema"}, where input_schema is JSON Schema (subset — see K10). tool_choice takes {"type": "auto"}, {"type": "any"} (must call some tool), {"type": "tool", "name": "..."}, or {"type": "none"}. Parallel tool calls are on by default and can be forced off with disable_parallel_tool_use: true inside the tool_choice object.

Three Anthropic-only primitives are the reason to reach for this vendor first when the tool surface is the hard part. Programmatic Tool Calling (PTC), shipped November 2025, changes the loop so the model writes a small Python program that runs in an Anthropic-managed sandbox, calls the tools from inside that program, and returns only the final result into context — the intermediate tool results never enter the model's context window at all. On multi-tool chains this is a large win on both cost and latency, and the tool-granularity problem softens because you can expose finer tools without paying the context tax on each call. The advanced-orchestration primitives that build on PTC are worth a separate essay of their own. Tool Search Tool is a built-in whose only job is to semantically retrieve tool definitions from a corpus you upload — you register 200 tools once, ship a single Tool Search Tool in the prompt, and the model asks the tool to fetch the three or four defs it actually needs. Anthropic's own benchmark quotes an 85% token reduction on tool-rich prompts. Tool Use Examples attach few-shot examples directly to a tool def (tool_use_examples field) — same idea as few-shot in the system prompt, but scoped to the tool, so the model only sees them when it is considering that tool.

# Anthropic — same tool, native shape
tools = [{
    "name": "refund_order",
    "description": "Refund an order. Use for defective items or customer-reported issues.",
    "input_schema": {
        "type": "object",
        "properties": {
            "order_id": {"type": "string"},
            "reason": {"type": "string", "enum": ["defective", "wrong_item", "other"]},
        },
        "required": ["order_id", "reason"],
    },
}]
resp = client.messages.create(model="claude-opus-4-7", messages=msgs,
    tools=tools, tool_choice={"type": "auto"})

Two porting-gotchas to keep in view. Anthropic's input_schema validation ignores minLength, maxLength, minimum, maximum — the K10 essay covers the full subset, and the practical consequence is that a schema that visibly enforces string length on OpenAI silently permits any length on Anthropic. And Anthropic's tool_result block is separate content, not a role, so a framework that models messages as {role, content} tuples has to allow content-block arrays or fabricate an unnatural role. Both are small friction if you know about them; both are silent bugs if you don't.

STEP 4

Gemini: OpenAPI subset and tool_choice: any.

Gemini's tool surface uses the OpenAPI 3.0 schema subset, not JSON Schema draft-2020-12 — FunctionDeclaration.parameters is a Schema object drawn from OpenAPI, and constructs that don't exist in OpenAPI 3.0 (like const, or $defs-based cross-references) don't validate. The model emits a functionCall part inside the response content, with name and args fields; the host returns a functionResponse part with the same name and a response object. Gemini's default is to allow the model to reply with either a function call or free text; tool_config.function_calling_config.mode takes AUTO (default), ANY (must call one of the exposed tools), or NONE, and an optional allowed_function_names restricts ANY to a subset.

Two Gemini-specific properties are worth budgeting for. First, Gemini natively supports multimodal function responses — the functionResponse parts can carry inline images or file-URI references, and the model reads them as part of the tool result. That is unique in the matrix and is the reason browser-agent and document-agent teams reach for Gemini even when their primary model is elsewhere. Second, Gemini exposes function-calling on both the Vertex and AI Studio surfaces with slightly different SDKs and different quota knobs; the tool-def shape is the same but the client boilerplate isn't, and template-driven codegen has to know which surface a given deployment targets.

# Gemini — OpenAPI 3.0 subset
tools = [{
    "function_declarations": [{
        "name": "refund_order",
        "description": "Refund an order. Use for defective items or customer-reported issues.",
        "parameters": {
            "type": "OBJECT",
            "properties": {
                "order_id": {"type": "STRING"},
                "reason": {"type": "STRING", "enum": ["defective", "wrong_item", "other"]},
            },
            "required": ["order_id", "reason"],
        },
    }],
}]
resp = client.models.generate_content(model="gemini-3.5-pro", contents=msgs,
    config={"tools": tools,
            "tool_config": {"function_calling_config": {"mode": "ANY"}}})

The uppercase "OBJECT" / "STRING" type names are OpenAPI-style enums, not the lowercase JSON Schema strings the other two vendors take. A tool-def template that emits lowercase will fail validation on Gemini with a message about unknown type; a template that emits uppercase will fail on the other two vendors for the same reason. The pragmatic move is a per-vendor emitter around a common intermediate representation, not a stringly-typed template.

STEP 5

Divergence that matters: parallel, streaming, structured output.

Four axes carry most of the port-breaking surprises. Parallel tool calls are on by default everywhere, but the wire shapes differ — OpenAI emits multiple tool_calls items in one assistant message, Anthropic emits multiple tool_use content blocks, Gemini emits multiple functionCall parts. Frameworks that model a tool call as "one per message" break silently on parallel calls; you get the first one, the rest disappear. A framework that iterates the list works uniformly. Streaming deltas differ enough to deserve their own essay — see K11 — and the aggregation semantics (does arguments accumulate across chunks, or is each chunk a full replacement?) are where the accumulators most commonly break. Structured output is the same constrained-decoding trick as tool calling but with a value-return contract instead of an action-proposal contract; see K9 for the port rules.

JSON Schema subsets are the fourth and most costly axis. Every vendor advertises "JSON Schema" and each enforces a different set. OpenAI's strict mode requires additionalProperties: false everywhere and marks every field required (nullable-optional workarounds required). Anthropic ignores string- and number-range keywords entirely. Gemini enforces the OpenAPI 3.0 subset and rejects constructs outside it. See the K10 essay for the full three-column table you keep next to your editor; the summary is that any schema you ported by copy-paste from one vendor's docs is a candidate bug on the other two.

One more axis is more subtle but bites in production: tool-choice semantics on the retry. When the model emits a bad tool call and you push a corrective tool result back, OpenAI's tool_choice="required" re-fires on the next turn; Anthropic's {"type": "any"} does too; Gemini's ANY mode does too — but their interaction with stop_sequences and end-of-turn detection is not uniform, and a retry harness that assumes "required forces a tool call" without checking the response can loop. The safe harness reads the response and only re-fires if the model actually produced text instead of a call.

STEP 6

The portable subset: what actually ports without editing.

The tool-def shape that survives a port unchanged is smaller than teams hope and larger than they expect. What ports: a description written as instruction (verb-first, when-and-when-not, one example), a small parameters object with a top-level "type": "object", string / integer / number / boolean primitives, a required array, and an enum constraint on strings. What does not port: string- and number-range keywords (minLength, maxLength, minimum, maximum) if Anthropic is a target; const, $defs, anyOf-with-null tricks if Gemini is a target; additionalProperties presence rules if OpenAI strict mode is on. The lowercase-vs-uppercase type names for Gemini and the extra "function" nesting for OpenAI Chat Completions are trivial rewrites; the schema-subset differences are semantic and are the ones you catch late.

Two patterns keep teams sane. The first is an intermediate representation — an internal tool spec with the union of the portable subset plus per-vendor extensions — and per-vendor emitters that render it into the right shape. The moment you have three call-sites emitting three tool-def shapes by hand, one of them will drift. The second is a per-vendor validation pass that runs your schemas through each vendor's SDK validator at CI time; every vendor ships one, and the CI cost is a few seconds per tool. If your CI does not catch a schema-subset regression, your production traffic will, and the trace will look like "the model started ignoring the enum," which is a slow bug to find.

Read the six steps together and the vendor matrix stops looking like a compatibility question and starts looking like a design one. The shared surface is real — model proposes, host executes, model reads — and every serious vendor now supports it well enough that the loop is boring. The bumps are all at the edges: how you wrap a tool def, how you stream, how you retry, and what schema keywords silently vanish. Treat the portable subset as your baseline and every extension as an explicit per-vendor opt-in, and the port is a diff you can read; treat "JSON Schema is JSON Schema" as the porting model and every new vendor is a fresh production incident waiting for a schema keyword to go quiet.