If you’re building an agent or structured-output pipeline, the choice between Gemini function calling vs OpenAI function calling isn’t academic — it shapes your schema design, error handling, and latency profile. Both providers now support parallel invocation, streaming tool calls, and forced tool selection, but the implementation details diverge in ways that bite you in production. This breakdown covers the concrete differences so you can pick without guessing.
Schema definition and validation
OpenAI uses standard JSON Schema (Draft 2020-12 subset) for function parameters. Gemini uses its own FunctionDeclaration schema, which looks similar but enforces a stricter type system and lacks a few JSON Schema keywords.
OpenAI function definition:
{
"name": "search_products",
"description": "Search the product catalog",
"parameters": {
"type": "object",
"properties": {
"query": { "type": "string" },
"filters": {
"type": "object",
"properties": {
"category": { "type": "string", "enum": ["electronics", "apparel", "home"] },
"price_max": { "type": "number" }
},
"additionalProperties": false
},
"limit": { "type": "integer", "minimum": 1, "maximum": 50 }
},
"required": ["query"],
"additionalProperties": false
}
}
Gemini function declaration:
{
"name": "search_products",
"description": "Search the product catalog",
"parameters": {
"type": "OBJECT",
"properties": {
"query": { "type": "STRING" },
"filters": {
"type": "OBJECT",
"properties": {
"category": { "type": "STRING", "enum": ["electronics", "apparel", "home"] },
"price_max": { "type": "NUMBER" }
}
},
"limit": { "type": "INTEGER" }
},
"required": ["query"]
}
}
Key differences:
- Gemini types are uppercase (
STRING,NUMBER,INTEGER,BOOLEAN,ARRAY,OBJECT) not lowercase - No
additionalPropertieskeyword in Gemini — extra keys are silently ignored at validation time - No
minimum/maximum/pattern/formatconstraints in Gemini; you validate in your handler - Gemini requires
requiredarray but doesn’t enforce it as strictly during model generation
If you share schemas across providers, write a canonical JSON Schema and transform at the boundary. Don’t hand-maintain both.
Parallel and composite calls
Both providers now return multiple tool calls in a single response. The wire format differs.
OpenAI response (non-streaming):
{
"choices": [{
"message": {
"role": "assistant",
"tool_calls": [
{ "id": "call_abc123", "type": "function", "function": { "name": "search_products", "arguments": "{\"query\": \"wireless headphones\"}" }},
{ "id": "call_def456", "type": "function", "function": { "name": "get_inventory", "arguments": "{\"sku\": \"WH-1000XM5\"}" }}
]
}
}]
}
Gemini response (non-streaming):
{
"candidates": [{
"content": {
"parts": [
{ "functionCall": { "name": "search_products", "args": { "query": "wireless headphones" }}},
{ "functionCall": { "name": "get_inventory", "args": { "sku": "WH-1000XM5" }}}
]
}
}]
}
OpenAI includes a tool_call_id you must echo back in the tool result message. Gemini uses the function name as the implicit key — you return a functionResponse part with the same name. This matters for your message builder: OpenAI requires a 1:1 mapping between tool_calls and tool role messages; Gemini matches by name and order.
Both support parallel execution in your handler. The latency win is real — two 200ms API calls in parallel beat 400ms sequential every time.
Streaming and partial results
Streaming tool calls arrive incrementally. OpenAI streams tool_calls as an array where each entry accumulates function.arguments as a JSON string fragment. Gemini streams functionCall parts where args arrives as a progressively complete JSON object.
OpenAI streaming chunk:
{
"choices": [{
"delta": {
"tool_calls": [{
"index": 0,
"id": "call_abc123",
"type": "function",
"function": { "name": "search_products", "arguments": "{\"query\": \"wireless" }}
}]
}
}]
}
Gemini streaming chunk:
{
"candidates": [{
"content": {
"parts": [{ "functionCall": { "name": "search_products", "args": { "query": "wireless" }}}]
}
}]
}
Practical impact: with OpenAI you buffer string fragments and parse JSON only when finish_reason == "tool_calls". With Gemini you can validate args as a real object on each chunk — but beware that intermediate states may be invalid (missing required fields). Don’t execute until the final chunk.
Both providers let you stream the model’s text response after tool results are submitted. The pattern is identical: submit tool results, then stream the final answer.
Tool choice and forced invocation
OpenAI’s tool_choice parameter accepts "auto" (default), "none", "required", or a specific function object {"type": "function", "function": {"name": "my_func"}}. "required" forces some tool call but lets the model pick which.
Gemini uses tool_config.function_calling_config with mode: AUTO, ANY, NONE, and allowed_function_names to restrict the set.
// OpenAI: force a specific function
{ "tool_choice": { "type": "function", "function": { "name": "search_products" }} }
// Gemini: force any from allowed set
{
"tool_config": {
"function_calling_config": {
"mode": "ANY",
"allowed_function_names": ["search_products"]
}
}
}
Gemini’s ANY mode with a single allowed function is the closest equivalent to OpenAI’s forced function. OpenAI’s "required" (force any tool) maps to Gemini’s ANY without allowed_function_names.
One gotcha: OpenAI returns finish_reason: "tool_calls" when a tool is invoked. Gemini returns finishReason: "STOP" even when function calls are present — check parts for functionCall. Your routing logic must handle both.
Context handling and token accounting
OpenAI counts function definitions in the input token budget. The tools array adds ~200-500 tokens per function depending on schema complexity. Tool results (role: "tool" messages) also count against input tokens on the next turn.
Gemini counts function_declarations in the tools array similarly. However, Gemini’s functionResponse parts are not billed as input tokens on subsequent turns — only the initial declaration is. This can save 10-20% token spend on long agent loops with many tool rounds.
Both providers include tool call arguments and results in the context window. If you’re running multi-step agents, compress or summarize tool results before feeding them back. Neither provider does this automatically.
Provider-specific quirks and limits
| Dimension | OpenAI | Gemini |
|---|---|---|
| Max parallel calls | 128 (gpt-4o) | 16 (gemini-1.5-pro), 8 (gemini-1.5-flash) |
| Max function schema depth | Unlimited (practical ~10) | 15 levels |
| Enum values per parameter | 500 | 100 |
| Function name length | 64 chars | 64 chars |
| Argument size limit | 2 MB (gpt-4o) | 1 MB |
| Streaming tool calls | Yes (all models) | Yes (1.5 Pro/Flash) |
| Structured output (JSON mode) | response_format: {type: "json_object"} |
generation_config.response_mime_type: "application/json" |
| Vision + tools same request | Yes (gpt-4o) | Yes (1.5 Pro/Flash) |
| Audio + tools | No | No |
Gemini’s lower parallel call limit matters for batch-heavy workloads (e.g., “fetch details for these 50 SKUs”). OpenAI’s higher ceiling lets you fan out more aggressively. On the flip side, Gemini Flash is significantly cheaper per token for high-volume tool-use workloads.
Both providers occasionally return malformed arguments (missing required fields, wrong types). Your handler must validate against the original schema before executing — don’t trust the model’s output blindly.
Comparison table
| Capability | OpenAI | Gemini |
|---|---|---|
| Schema format | JSON Schema (Draft 2020-12) | FunctionDeclaration (proprietary) |
| Parallel calls | Up to 128 | Up to 16 (Pro), 8 (Flash) |
| Forced tool selection | tool_choice: {function: {name}} |
mode: ANY, allowed_function_names: [...] |
| Force any tool | tool_choice: "required" |
mode: ANY |
| Streaming tool calls | String fragments (arguments) |
Object fragments (args) |
| Tool result token billing | Counted every turn | Only initial declaration counted |
| Enum limit | 500 values | 100 values |
| Max schema depth | ~10 practical | 15 enforced |
| JSON structured output | response_format |
generation_config.response_mime_type |
| Vision + tools | gpt-4o, gpt-4o-mini | gemini-1.5-pro, gemini-1.5-flash |
| Typical latency (tool call) | 150-300ms (gpt-4o) | 200-400ms (1.5 Flash) |
| Cost per 1M input tokens | $2.50 (gpt-4o), $0.15 (gpt-4o-mini) | $1.25 (1.5 Pro), $0.075 (1.5 Flash) |
Which to choose
Choose OpenAI when:
- You need maximum parallel fan-out (dozens of simultaneous calls)
- Your schemas rely on JSON Schema keywords (
pattern,format,minimum,additionalProperties) - You’re already invested in the OpenAI SDK ecosystem and want minimal migration
- You need the lowest latency on complex reasoning + tool use (gpt-4o edges out 1.5 Pro)
- You require
response_format: {type: "json_schema"}for strict structured output (Gemini’sresponse_mime_typeis looser)
Choose Gemini when:
- Cost per token is the primary driver — Flash is ~5x cheaper than gpt-4o-mini for tool-heavy workloads
- You run long agent loops where tool result token billing adds up (Gemini doesn’t re-bill responses)
- You need native multimodal (image/video/audio) input with tools in a single request
- Your schemas are simple and you don’t need JSON Schema validation keywords
- You’re building on Vertex AI and want unified billing, VPC-SC, and data residency controls
Hybrid approach (what we see in production):
Route by task. Use gpt-4o-mini for high-fan-out, low-latency tool orchestration. Use Gemini 1.5 Flash for high-volume, cost-sensitive extraction and summarization pipelines where tool loops run 10+ turns. Keep a single abstraction layer in your code — normalize both providers to a common ToolCall / ToolResult interface — so you can swap or mix without rewriting business logic.
If you’re running a gateway that sits in front of both (like n4n.ai does), you can even implement automatic fallback: try the primary provider, and on rate limit or 5xx, retry the equivalent call on the secondary with the normalized schema. The schema translation is the only non-trivial part — everything else maps cleanly.