Tool use and function calling (Gemini)

Gemini’s function-calling surface centers on tools[].function_declarations[] (JSON-Schema subset of OpenAPI) + tool_config.function_calling_config.mode (one of AUTO, ANY, VALIDATED, NONE). The Python SDK adds a powerful sugar layer — automatic function calling — where you pass a regular Python callable and the SDK both declares it (introspecting signature + type hints + docstring) and executes it on your behalf when the model calls it, feeding the result back without your code touching the loop. The biggest 2026 change is the Gemini 3 unique-id requirement: every function call now gets a unique id, and the matching functionResponse must carry it — a breaking change from 2.x semantics that needs migration attention in agent loops.

See also

1. Function declaration shape

Per ai.google.dev/gemini-api/docs/function-calling. The shape is a constrained subset of OpenAPI schema.

from google.genai import types
 
get_weather = types.FunctionDeclaration(
    name="get_weather",
    description="Get current weather conditions for a city.",
    parameters=types.Schema(
        type="OBJECT",
        properties={
            "city": types.Schema(
                type="STRING",
                description="City name, e.g. 'San Francisco'",
            ),
            "unit": types.Schema(
                type="STRING",
                enum=["celsius", "fahrenheit"],
                description="Temperature unit",
            ),
        },
        required=["city"],
    ),
)

Same shape as a JSON object:

{
  "name": "get_weather",
  "description": "Get current weather conditions for a city.",
  "parameters": {
    "type": "OBJECT",
    "properties": {
      "city": {"type": "STRING", "description": "City name, e.g. 'San Francisco'"},
      "unit": {"type": "STRING", "enum": ["celsius", "fahrenheit"], "description": "Temperature unit"}
    },
    "required": ["city"]
  }
}

Supported type values

STRING, NUMBER, INTEGER, BOOLEAN, OBJECT, ARRAY. (Note the uppercase — different from JSON Schema’s lowercase.)

Supported constraints

  • STRINGenum, format (date, date-time, time, enum), pattern (partial)
  • NUMBER / INTEGERenum, minimum, maximum
  • ARRAYitems (schema), minItems, maxItems
  • OBJECTproperties, required, additionalProperties (limited honoring)
  • All — description, title, nullable

Naming rules

  • Function name: [a-zA-Z][a-zA-Z0-9_-]*, max 64 chars
  • Parameter name: standard identifier
  • Avoid generic names like do_task or run — be descriptive

2. Modes (function_calling_config.mode)

Per the function-calling docs, four modes:

ModeBehavior
AUTODefault. Model decides whether to call a function or return text.
ANYModel must call at least one function (cannot return plain text). Optionally constrain to a subset with allowed_function_names.
VALIDATEDConstrains output to either valid function calls or text, with constrained decoding ensuring schema adherence.
NONEDisable function calling for this request even though tools are declared.
config = types.GenerateContentConfig(
    tools=[types.Tool(function_declarations=[get_weather, search_db])],
    tool_config=types.ToolConfig(
        function_calling_config=types.FunctionCallingConfig(
            mode="ANY",
            allowed_function_names=["get_weather"],  # narrows ANY to just weather
        ),
    ),
)

VALIDATED is the recommended mode when you absolutely need schema-compliant function calls (constrained decoding prevents malformed args). Tradeoff: slightly higher latency, occasional model refusal when no good function matches the prompt.

3. Calling a function (the manual flow)

The full request → call → response → final-answer cycle:

# 1. First request: model decides to call a function
resp1 = client.models.generate_content(
    model="gemini-3.5-flash",
    contents="What's the weather in Paris?",
    config=types.GenerateContentConfig(
        tools=[types.Tool(function_declarations=[get_weather])],
        automatic_function_calling=types.AutomaticFunctionCallingConfig(disable=True),
    ),
)
fc = resp1.candidates[0].content.parts[0].function_call
print(fc.name, fc.args, fc.id)        # "get_weather", {"city": "Paris"}, "fc_abc123"
 
# 2. You execute the function client-side
result = {"temp": 18, "unit": "celsius", "condition": "cloudy"}
 
# 3. Send back the result with the matching id
resp2 = client.models.generate_content(
    model="gemini-3.5-flash",
    contents=[
        types.Content(role="user", parts=[types.Part(text="What's the weather in Paris?")]),
        resp1.candidates[0].content,                       # the model's function_call turn
        types.Content(role="user", parts=[
            types.Part(function_response=types.FunctionResponse(
                id=fc.id,                                  # CRITICAL on Gemini 3 — match the id
                name="get_weather",
                response={"result": result},
            )),
        ]),
    ],
    config=types.GenerateContentConfig(
        tools=[types.Tool(function_declarations=[get_weather])],
    ),
)
print(resp2.text)

Gemini 3 change: the model emits a unique id (fc_abc123 etc.) on each function call. You must echo it in the matching FunctionResponse.id. Gemini 2.x ignores the id (matched by name only) — code that worked on 2.x will warn or error on 3.x without the id round-trip.

4. Automatic function calling (Python SDK sugar)

The SDK can do the entire loop for you. Pass a callable:

def get_weather(city: str, unit: str = "celsius") -> dict:
    """Get current weather conditions for a city.
 
    Args:
        city: City name.
        unit: Temperature unit, celsius or fahrenheit.
    """
    # ... fetch from your weather API ...
    return {"temp": 18, "unit": unit, "condition": "cloudy"}
 
resp = client.models.generate_content(
    model="gemini-3.5-flash",
    contents="What's the weather in Paris and Tokyo?",
    config=types.GenerateContentConfig(tools=[get_weather]),
)
print(resp.text)
# The SDK introspects get_weather (signature + type hints + docstring),
# declares it to the model, executes it when called (possibly multiple times
# for parallel calls), and feeds results back automatically.

To inspect the calls that happened:

for c in resp.automatic_function_calling_history:
    print(c.role, c.parts)

To disable auto-execution while still using callables:

config=types.GenerateContentConfig(
    tools=[get_weather],
    automatic_function_calling=types.AutomaticFunctionCallingConfig(disable=True),
)

Use disable when:

  • The function has side effects you want to gate (DB writes, sending email)
  • You need to inspect args before execution
  • You’re running in an async / distributed environment where the SDK loop doesn’t fit

Max function-call iterations in auto mode is configurable:

automatic_function_calling=types.AutomaticFunctionCallingConfig(
    maximum_remote_calls=10,           # default 10 — bump for deep agentic chains
)

Auto mode propagates exceptions: a KeyError in your function aborts the loop with the same exception bubbling up. Wrap risky functions in try/except returning structured errors if you want the model to recover.

5. Parallel function calling

The model can return multiple function calls in a single turn when independent. Each appears as its own function_call part:

resp.candidates[0].content.parts
# [
#   Part(function_call=FunctionCall(name="get_weather", args={"city": "Paris"}, id="fc_1")),
#   Part(function_call=FunctionCall(name="get_weather", args={"city": "Tokyo"}, id="fc_2")),
#   Part(function_call=FunctionCall(name="search_db", args={"query": "weather news"}, id="fc_3")),
# ]

Execute all three (often in parallel), then return all three FunctionResponse parts in one user-role message:

types.Content(role="user", parts=[
    types.Part(function_response=types.FunctionResponse(id="fc_1", name="get_weather", response={"result": paris_data})),
    types.Part(function_response=types.FunctionResponse(id="fc_2", name="get_weather", response={"result": tokyo_data})),
    types.Part(function_response=types.FunctionResponse(id="fc_3", name="search_db", response={"result": search_data})),
]),

The SDK auto-flow executes them sequentially by default. For true parallelism, run them yourself with asyncio.gather or a thread pool around the manual flow.

6. Compositional function calling

The model chains calls: call A, see result, then call B based on A’s output. The flow is just the manual flow repeated — each generate_content call returns either the next function call(s) or the final text answer. Common pattern:

messages = [types.Content(role="user", parts=[types.Part(text=user_query)])]
for _ in range(10):
    resp = client.models.generate_content(
        model="gemini-3.5-flash",
        contents=messages,
        config=types.GenerateContentConfig(tools=tools),
    )
    if not any(p.function_call for p in resp.candidates[0].content.parts):
        break  # model returned text, done
    messages.append(resp.candidates[0].content)
    responses = []
    for part in resp.candidates[0].content.parts:
        if part.function_call:
            result = execute(part.function_call.name, part.function_call.args)
            responses.append(types.Part(function_response=types.FunctionResponse(
                id=part.function_call.id,
                name=part.function_call.name,
                response={"result": result},
            )))
    messages.append(types.Content(role="user", parts=responses))
print(resp.text)

7. Mixing function calls with other tools

You can declare multiple Tool blocks in the same request: function declarations + Google Search + code execution + URL context.

tools = [
    types.Tool(function_declarations=[get_weather]),
    types.Tool(google_search=types.GoogleSearch()),
    types.Tool(code_execution=types.ToolCodeExecution()),
]

The model picks among them based on the prompt. Practical: declare custom functions for proprietary actions (DB write, internal API), grounding for fresh facts, code execution for math.

Note: as of 2026-05, some combinations have restrictions on certain models — see code-execution-and-grounding-gemini.

8. Schema design tips

Use descriptions aggressively

The model uses description fields to decide whether and how to call. A function named lookup with no description is much less reliable than one with description="Look up a customer record by email or customer ID. Returns name, plan, and last-login timestamp.".

Constrain with enums and required

enum is constrained-decoded — the model cannot generate a value outside the enum. This is reliable for fixed taxonomies.

"status": types.Schema(type="STRING", enum=["active", "paused", "archived"]),

required is honored — missing required fields trigger model retry / refusal. List every truly-required field.

Avoid deeply nested optional shapes

Model accuracy drops sharply past ~3 levels of nesting. For complex inputs, prefer flat objects with descriptive field names over nested hierarchies.

Avoid recursive schemas

Self-referential schemas (a node containing a list of nodes) are accepted but routinely fail. Flatten to lists.

9. Forced tool use (mode: ANY)

When you must get a function call (e.g. classifying a query into one of a fixed action set):

config=types.GenerateContentConfig(
    tools=[types.Tool(function_declarations=[classify_intent])],
    tool_config=types.ToolConfig(
        function_calling_config=types.FunctionCallingConfig(
            mode="ANY",
            allowed_function_names=["classify_intent"],
        ),
    ),
)

This forces a function call (or a refusal). Useful for routing, classification, extraction-as-function-call.

10. Tracing and debugging

response.candidates[0].content.parts gives the raw model output. For function calls:

for part in response.candidates[0].content.parts:
    if part.function_call:
        print(f"call: {part.function_call.name}({dict(part.function_call.args)}) id={part.function_call.id}")
    elif part.text:
        print(f"text: {part.text}")

For automatic mode, inspect response.automatic_function_calling_history — a list of all function calls + responses the SDK processed.

response.candidates[0].finish_reason of MALFORMED_FUNCTION_CALL means the model emitted something the parser couldn’t validate — usually means the schema is too complex or had unsupported types. Simplify and retry.

11. Schema validation gotchas

Per the docs, supported schema features:

  • OK: type, properties, required, description, title, enum, minimum, maximum, minItems, maxItems, items, nullable
  • Honored loosely: additionalProperties, pattern, format
  • Not supported: $ref / definitions, allOf / oneOf / anyOf, recursive references, not, if/then/else

If your Pydantic model has Union types or Optional[Union[...]], the SDK serializer may produce an unsupported schema. Test with client.models.count_tokens (a low-stakes call that triggers schema validation).

12. Cost considerations

Function declarations live in the request (or the cache, if cached). They count as input tokens:

  • Each function declaration ≈ 50-200 tokens depending on schema complexity
  • A request with 20 tools and verbose descriptions can hit 5,000+ tokens of tool overhead
  • Cache tools when stable across requests (see context-caching-gemini)

Function calls themselves contribute to output tokens (the call name + JSON args is generated text, billed at output rate).

13. Common pitfalls

  1. Forgetting the id round-trip on Gemini 3 — code that worked on 2.5 silently fails on 3.x without echoing function_call.id in the function_response.id.
  2. Uppercase type requirementSTRING not string. JSON-Schema-y dictionaries assembled by hand fail validation.
  3. Missing docstring in auto mode — the SDK uses the docstring as the function description. No docstring → empty description → unreliable calling.
  4. Mutable default argsdef f(items: list = []) confuses the SDK. Use Optional[list] = None.
  5. Side-effect functions in auto modesend_email runs on every model decision in auto mode. Either disable auto or gate with a confirmation parameter the model has to set explicitly.
  6. Schema too rich — Union / recursive / deeply nested → MALFORMED_FUNCTION_CALL. Flatten.
  7. Function name conflicts — declaring get and get_v2 confuses the model. Use distinct, descriptive names.
  8. tool_config set but tools not — silently ignored. Both must be present.
  9. Implicit cache invalidation when tools change — adding or removing a function declaration invalidates implicit cache; explicit cache requires re-creation.

14. Comparison to other vendors

AspectGeminiClaudeOpenAI
Declaration shapeOpenAPI subset (UPPERCASE types)JSON Schema (lowercase)JSON Schema (lowercase)
Strict modeVALIDATED (constrained decode)strict_input: truestrict: true (full schema enforcement)
Parallel callsSupported (multiple function_call parts per turn)SupportedSupported (parallel_tool_calls: true)
Forced callmode: ANY + allowed_function_namestool_choice: {type: tool, name: "..."}tool_choice: {type: "function", function: {name: "..."}}
Auto-execution sugarPython SDK only (pass callable)None (manual loop)Agents SDK helper
Unique call IDsRequired on Gemini 3tool_use_id always requiredtool_call_id always required

15. Further reading