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

Decision brief
TL;DR
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.
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 schema mode improves argument shape. Your server must still validate values, authorize the caller, control side effects, and treat tool output as untrusted.
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.
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.
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
| Concern | OpenAI | Anthropic | Google Gemini |
|---|---|---|---|
| Tool definition | type: function + function schema | name, description, input_schema | type: function + parameters in Interactions |
| Call output | function_call | tool_use content block | function_call step |
| Correlation key | call_id | tool_use_id | call_id; preserve signature/state where required |
| Return result | function_call_output | tool_result in the next user message | function_result |
| Strict schema | strict: true; strict object requirements | strict: true supported | Supported schema subset varies by API/model |
| Parallel calls | Supported; can disable | Multiple tool_use blocks; can disable | Parallel and compositional calling supported |
| Stateful continuation | Use response/conversation state or send prior items | Send the next user message with result blocks | previous_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.
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.
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.
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.
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: falseand 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
| Control | Use it when | Production caution |
|---|---|---|
| Auto | The model may answer or call tools. | Best default for mixed conversations. |
| Required / forced | A workflow step must obtain structured data. | Do not force irreversible actions. |
| Strict | You need dependable argument shape. | Still enforce business rules server-side. |
| Parallel | Calls 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
Schema validation answers “is this shaped correctly?” Authorization answers “may this identity perform this action on this resource?” Both are required.
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.
Persist a deterministic idempotency key before side effects. Retries must return the previous outcome instead of repeating an order, message, payment, or mutation.
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
| Symptom | Likely cause | Fix |
|---|---|---|
| Provider rejects the next turn | Missing, reordered, or wrong result ID | Persist raw calls and generate results through one adapter |
| Tool runs twice | Network retry without idempotency | Deduplicate before execution and cache outcome |
| Arguments fail randomly | Parsing streamed fragments or vague schema | Aggregate to completion; simplify and tighten schema |
| Parallel results are mismatched | Matching by array position | Match only by provider call identifier |
| Model follows text inside a result | Indirect prompt injection | Minimize, 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.
Migration checklist between providers
- Freeze a canonical tool registry and version every schema.
- Map definition fields, tool-choice controls, call IDs, result envelopes, and conversation state.
- Remove unsupported schema features before translation.
- Replay production traces in shadow mode without side effects.
- Compare call selection, argument validity, recovery, latency, and cost.
- 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.
Find the capability and inspect its contract before a prompt exposes it.
Route validated arguments to the selected provider or data source.
Return provenance, retrieval time, units, and validation status with the result.
FAQ
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.
There is no universal winner. Test your models and workloads for schema compliance, streaming, parallel behavior, recovery, observability, cost, and migration effort.
No. The model proposes a call. Your application validates, authorizes, executes, and returns the result.
Use it where supported, but still validate server-side. Strict mode does not grant permission or guarantee business correctness.
Collect all calls, run only independent work concurrently, and correlate each result by its original call identifier.
Persist an idempotency key derived from the turn, call ID, tool name, and normalized arguments before execution.
No. MCP standardizes client-to-tool-server interaction; provider APIs still define the model-facing call-and-result messages.
QVeris adds discovery, inspection, routing, and evidence for real-world capabilities while the adapter manages provider-native messages.
