Repository navigation
Description
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_schemacallstyping.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_functionreads the rawinspect.signature(function)annotation, which is the string'str'.convert_if_exist_pydantic_modelthen ends inisinstance(value, 'str'), which raises. - The error is swallowed.
get_function_response_partscatches theTypeErrorand 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_contentandaio.chats.send_messagethrough 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
Venkaiahbabuneelam commentedon Oct 19, 2026
Hi @AtulJoshi1206, thank you for the detailed bug report, reproduction script, and root cause analysis!
We have reproduced this issue with
from __future__ import annotationsand escalated it to our engineering team.Thanks
fix: Resolve PEP 563 string annotations when invoking AFC tools
fix: Resolve PEP 563 string annotations when invoking AFC tools
fix: Resolve PEP 563 string annotations when invoking AFC tools
fix: Resolve PEP 563 string annotations when invoking AFC tools