Skip to content

Automatic function calling never runs tools defined under from __future__ import annotations #3156

Closed

Description

@AtulJoshi1206

Description

When a tool is defined in a module that uses from __future__ import annotations (PEP 563), automatic function calling never runs it. The model's function call is answered with an error string, and no exception reaches the caller, so it fails silently:

tool calls: []
functionResponse sent to the model: {'error': 'isinstance() arg 2 must be a type, a tuple of types, or a union'}

I see this on every AFC entry point: models.generate_content / generate_content_stream and chats.send_message / send_message_stream, sync and async, on both the Gemini API and Vertex AI.

Repro (offline, mocked transport)

from __future__ import annotations  # PEP 563: annotations are stored as strings

import json

import httpx
from google import genai
from google.genai import types

calls = []


def get_weather(city: str, days: int = 3) -> str:
  """Returns the weather forecast for a city."""
  calls.append((city, days))
  return f'sunny in {city} for {days} days'


requests = []


def handler(request: httpx.Request) -> httpx.Response:
  requests.append(json.loads(request.content))
  if len(requests) == 1:  # the model asks for the tool
    part = {'functionCall': {'name': 'get_weather', 'args': {'city': 'Paris', 'days': 2}}}
  else:
    part = {'text': 'done'}
  return httpx.Response(200, json={'candidates': [{'content': {'role': 'model', 'parts': [part]}, 'finishReason': 'STOP'}]})


client = genai.Client(
    api_key='fake',
    http_options=types.HttpOptions(httpx_client=httpx.Client(transport=httpx.MockTransport(handler))),
)
client.chats.create(
    model='gemini-2.5-flash', config=types.GenerateContentConfig(tools=[get_weather])
).send_message('weather in Paris?')

print('tool calls:', calls)
print('functionResponse sent to the model:', requests[1]['contents'][-1]['parts'][0]['functionResponse']['response'])

google-genai 2.31.0 (main @ 364889a), Python 3.12.

Root cause

Under PEP 563 every annotation is a string.

  • The declaration side resolves them. parse_function_declaration_json_schema calls typing.get_type_hints(callable) (since 589b520), so the model gets the right schema and calls the tool correctly.
  • The invocation side doesn't. _extra_utils.convert_argument_from_function reads the raw inspect.signature(function) annotation, which is the string 'str'. convert_if_exist_pydantic_model then ends in isinstance(value, 'str'), which raises.
  • The error is swallowed. get_function_response_parts catches the TypeError and sends its message to the model as the tool result.

A second symptom has the same root cause. The declaration side calls get_type_hints without include_extras=True, so under PEP 563 the Annotated[..., Field(description=..., ge=..., le=...)] metadata is dropped from the declaration:

  • without the future import: "days": {"description": "Forecast length in days", "maximum": 7, "minimum": 1, "type": "integer", "default": 3}
  • with it: "days": {"type": "integer", "default": 3}

Proposed fix

--- a/google/genai/_extra_utils.py
+++ b/google/genai/_extra_utils.py
@@ def convert_argument_from_function(
   signature = inspect.signature(function)
   func_name = function.__name__
+  # `inspect.signature` returns annotations as written, which are strings under
+  # `from __future__ import annotations` (PEP 563). Resolve them the same way
+  # the function declaration was built. Without `include_extras`, this also
+  # unwraps `Annotated[T, ...]` to `T`, the runtime type the value must match.
+  type_hints: dict[str, Any]
+  try:
+    type_hints = typing.get_type_hints(function)
+  except Exception:  # pylint: disable=broad-except
+    # Unresolvable hints: keep the annotations exactly as written.
+    type_hints = {}
   converted_args = {}
   for param_name, param in signature.parameters.items():
     if param_name in args:
       converted_args[param_name] = convert_if_exist_pydantic_model(
           args[param_name],
-          param.annotation,
+          type_hints.get(param_name, param.annotation),
           param_name,
           func_name,
       )
--- a/google/genai/_automatic_function_calling_util.py
+++ b/google/genai/_automatic_function_calling_util.py
@@ def parse_function_declaration_json_schema(
-  annotation_under_future = typing.get_type_hints(callable)
+  # Keep `Annotated[T, Field(...)]` metadata (descriptions, constraints) when
+  # resolving string annotations, so they produce the same schema as eagerly
+  # evaluated ones.
+  annotation_under_future = typing.get_type_hints(callable, include_extras=True)

With this, the repro runs the tool and sends {'result': 'sunny in Paris for 2 days'}, and declarations are the same with and without the future import. Side benefit: bound methods and functools.wraps-decorated tools under PEP 563 now resolve too. If hints can't be resolved, it falls back to today's behavior.

I have regression tests for this, and they fail on main:

  • unit tests for argument conversion and declaration metadata;
  • models.generate_content and aio.chats.send_message through a mocked transport.

Happy to open a PR if that helps.

The generated types.py has the same get_type_hints call without include_extras on the use_json_schema=False path (around line 4926), which I can't patch directly.

Activity

  1. added
    priority: p2Moderately-important priority. Fix may not be included in next release.
    type: bugError or flaw in code with unintended results or allowing sub-optimal usage patterns.
    on Oct 19, 2026
  2. Venkaiahbabuneelam commented on Oct 19, 2026

    @Venkaiahbabuneelam

    Hi @AtulJoshi1206, thank you for the detailed bug report, reproduction script, and root cause analysis!
    We have reproduced this issue with from __future__ import annotations and escalated it to our engineering team.
    Thanks

  3. added 4 commits that reference this issue on Oct 21, 2026
    7da9ba4
    31c30d1
    49bcbdc
    4650ea1
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

priority: p2Moderately-important priority. Fix may not be included in next release.type: bugError or flaw in code with unintended results or allowing sub-optimal usage patterns.

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions