QVeris
MCP & TOOL CALLING

Function Calling APIs: Provider Comparison

Compare function-calling APIs by schemas, call IDs, results, strict mode, parallel calls and streaming; plan adapters and validation.

Published: April 2, 2026 · By the QVeris Team
Diagram of an AI model selecting a tool, validating and correcting structured arguments, executing a call and receiving a result.
Tool-call validation workflow: check structured arguments, return invalid inputs for correction, then execute the tool and collect its result.

Decision brief

TL;DR

Same loop, different wire formats

All three APIs describe tools, emit structured calls, wait for your application to execute them, and consume correlated results. The field names and conversation rules differ.

Normalize at your boundary

Keep one internal tool contract and translate provider requests and responses in thin adapters. Do not let provider message shapes leak into business logic.

Strict is not safe

Strict schema mode improves argument shape. Your server must still validate values, authorize the caller, control side effects, and treat tool output as untrusted.

Test the loop, not one happy call

Evaluate malformed arguments, multiple calls, partial streams, timeouts, retries, duplicate side effects, result ordering, and model recovery.

Who this comparison is for

Use this guide if you are selecting a provider, migrating an existing agent, or building one orchestration layer across multiple models. It assumes you can make API requests, define JSON Schema, and execute tools in a trusted backend.

Prerequisite: server-side tool executor
Prerequisite: JSON Schema validation
Prerequisite: secrets outside prompts
Prerequisite: structured logs and traces

Start with one canonical tool contract

The durable unit is not a provider payload. It is a provider-neutral definition, a normalized call, and a normalized result. Store the provider call ID separately so the adapter can reconstruct the required reply.

TypeScript · internal contract
type ToolDefinition = {
  name: string;
  description: string;
  inputSchema: JsonSchema;
  sideEffect: "read" | "write";
};

type ToolCall = {
  provider: "openai" | "anthropic" | "google";
  providerCallId: string;
  name: string;
  arguments: unknown;
  raw: unknown;
};

type ToolResult = {
  providerCallId: string;
  ok: boolean;
  content: unknown;
  evidence?: { source: string; retrievedAt: string }[];
};

OpenAI vs Anthropic vs Google: protocol comparison

ConcernOpenAIAnthropicGoogle Gemini
Tool definitiontype: function + function schemaname, description, input_schematype: function + parameters in Interactions
Call outputfunction_calltool_use content blockfunction_call step
Correlation keycall_idtool_use_idcall_id; preserve signature/state where required
Return resultfunction_call_outputtool_result in the next user messagefunction_result
Strict schemastrict: true; strict object requirementsstrict: true supportedSupported schema subset varies by API/model
Parallel callsSupported; can disableMultiple tool_use blocks; can disableParallel and compositional calling supported
Stateful continuationUse response/conversation state or send prior itemsSend the next user message with result blocksprevious_interaction_id in Interactions

Version boundary: Google has both the newer Interactions API vocabulary and the legacy generateContent vocabulary (functionCall/functionResponse). Pick one surface per adapter and document it.

OpenAI implementation: correlate with call_id

Send function tools with the request. When the model emits a function_call, parse its arguments, execute the approved tool, then append a function_call_output carrying the same call_id. Use tool_choice deliberately instead of forcing tools by default.

OpenAI Responses API · abbreviated
const first = await client.responses.create({
  model, input: userInput,
  tools: [{ type: "function", name: "get_quote", description,
    parameters: quoteSchema, strict: true }]
});

const call = first.output.find(x => x.type === "function_call");
const result = await executeSafely(call.name, JSON.parse(call.arguments));

const final = await client.responses.create({
  model, previous_response_id: first.id,
  input: [{ type: "function_call_output", call_id: call.call_id,
    output: JSON.stringify(result) }]
});

Anthropic implementation: preserve tool_use ordering

Claude returns one or more tool_use blocks, commonly with stop_reason: tool_use. The next user message must contain corresponding tool_result blocks. Put result blocks before any additional user text and mark execution failures with is_error: true.

Anthropic Messages API · abbreviated
const calls = response.content.filter(x => x.type === "tool_use");
const toolResults = await Promise.all(calls.map(async call => ({
  type: "tool_result",
  tool_use_id: call.id,
  content: JSON.stringify(await executeSafely(call.name, call.input))
})));

messages.push({ role: "assistant", content: response.content });
messages.push({ role: "user", content: toolResults });

Google implementation: preserve state and signatures

In the Interactions API, a function call is a step with id, name, and arguments; return a function_result referencing its call ID. Stateful continuation uses previous_interaction_id. In stateless flows, replay prior steps exactly, including required thought/signature metadata.

Google Interactions API · abbreviated
const call = interaction.outputs.find(x => x.type === "function_call");
const result = await executeSafely(call.name, call.arguments);

const next = await client.interactions.create({
  model,
  previous_interaction_id: interaction.id,
  input: [{ type: "function_result", call_id: call.id,
    result }]
});

Build a thin provider adapter

Each adapter should do four things only: serialize definitions, extract calls, serialize results, and preserve provider state. Validation, authorization, execution, retries, and evidence belong to shared infrastructure.

VALIDATE
AUTHORIZE
EXECUTE
CORRELATE RESULT
ANSWER
Provider-neutral loop
const turn = await adapter.ask({ messages, tools: canonicalTools });
const calls = adapter.extractCalls(turn);

const results = [];
for (const call of calls) {
  const args = validate(call.name, call.arguments);
  await authorize(identity, call.name, args);
  results.push(await executeIdempotently(call, args));
}

const answer = await adapter.continue({
  state: turn.state,
  toolResults: results
});

Schema design that survives all three APIs

  • Use small objects with explicit properties and predictable primitive types.
  • Keep enums short and explain units, formats, time zones, and defaults in descriptions.
  • Avoid deeply recursive schemas, ambiguous unions, and provider-specific extensions in the canonical layer.
  • For OpenAI strict mode, objects need additionalProperties: false and required fields; represent optional values explicitly where needed.
  • Validate again after decoding. Schema compliance does not prove a ticker exists, a date range is allowed, or a transfer is authorized.

Tool choice, strict mode, and parallel calls

ControlUse it whenProduction caution
AutoThe model may answer or call tools.Best default for mixed conversations.
Required / forcedA workflow step must obtain structured data.Do not force irreversible actions.
StrictYou need dependable argument shape.Still enforce business rules server-side.
ParallelCalls are independent and read-only.Serialize dependent or side-effecting calls.

Streaming and multi-turn state

Never execute a tool from an incomplete argument fragment. Buffer deltas until the provider marks the call complete, decode once, validate once, and persist the provider call ID before execution. For stateless continuation, retain every provider-required message item and opaque signature exactly.

Rule: parse only complete JSON; execute only persisted calls; return exactly one result for every accepted call.

Security, cost, and latency controls

Validate and authorize separately

Schema validation answers “is this shaped correctly?” Authorization answers “may this identity perform this action on this resource?” Both are required.

Treat tool output as untrusted

Fetched pages, documents, and API text can contain indirect prompt injection. Preserve provenance, limit returned fields, and keep results inside the provider's designated tool-result structure.

Make writes idempotent

Persist a deterministic idempotency key before side effects. Retries must return the previous outcome instead of repeating an order, message, payment, or mutation.

Budget every stage

Set deadlines, maximum calls per turn, concurrency, result-size limits, and token budgets. Cancel downstream work when the user request is abandoned.

Common failure modes and fixes

SymptomLikely causeFix
Provider rejects the next turnMissing, reordered, or wrong result IDPersist raw calls and generate results through one adapter
Tool runs twiceNetwork retry without idempotencyDeduplicate before execution and cache outcome
Arguments fail randomlyParsing streamed fragments or vague schemaAggregate to completion; simplify and tighten schema
Parallel results are mismatchedMatching by array positionMatch only by provider call identifier
Model follows text inside a resultIndirect prompt injectionMinimize, label, sanitize, and policy-check untrusted content

A provider-neutral validation benchmark

Do not publish a single “accuracy” score from a handful of prompts. Run the same versioned corpus against every provider and report success by failure class.

Selection: correct tool vs no tool
Arguments: schema and semantic validity
Correlation: every call gets the right result
Recovery: timeout, error, empty result, retry
Safety: unauthorized and injected requests blocked
Operations: p50/p95 latency, cost, call count

Migration checklist between providers

  1. Freeze a canonical tool registry and version every schema.
  2. Map definition fields, tool-choice controls, call IDs, result envelopes, and conversation state.
  3. Remove unsupported schema features before translation.
  4. Replay production traces in shadow mode without side effects.
  5. Compare call selection, argument validity, recovery, latency, and cost.
  6. Canary read-only tools first; enable write tools only after idempotency and authorization tests pass.

QVeris implementation pattern

Provider-native function calling controls the model-facing message loop. MCP can standardize how a client discovers and invokes tool servers. QVeris sits at the capability layer: it helps an agent discover, inspect, route, call, and audit real-world tools and data, while your provider adapter preserves the required OpenAI, Anthropic, or Google protocol.

Discover

Find the capability and inspect its contract before a prompt exposes it.

Call

Route validated arguments to the selected provider or data source.

Evidence

Return provenance, retrieval time, units, and validation status with the result.

FAQ

Is function calling the same as tool use?

Usually. The names differ, but the application loop is equivalent: describe tools, receive a structured call, execute outside the model, and return a correlated result.

Which provider has the best function calling API?

There is no universal winner. Test your models and workloads for schema compliance, streaming, parallel behavior, recovery, observability, cost, and migration effort.

Does function calling execute code?

No. The model proposes a call. Your application validates, authorizes, executes, and returns the result.

Should strict mode always be enabled?

Use it where supported, but still validate server-side. Strict mode does not grant permission or guarantee business correctness.

How should parallel calls be handled?

Collect all calls, run only independent work concurrently, and correlate each result by its original call identifier.

How do I prevent duplicate side effects?

Persist an idempotency key derived from the turn, call ID, tool name, and normalized arguments before execution.

Does MCP replace provider function calling?

No. MCP standardizes client-to-tool-server interaction; provider APIs still define the model-facing call-and-result messages.

What does QVeris add?

QVeris adds discovery, inspection, routing, and evidence for real-world capabilities while the adapter manages provider-native messages.

Official sources