Hacktoberfest 2026:メンテナが10月に向けて印を付けた、オープンで初心者向けの issue。 Hacktoberfest の issue を見る

Callable tools: optional scalar parameters (`int | None`) get `"type": "object"` in `parameters_json_schema`, so Gemini sends `{}`

オープン
#3,060 コメント 1 件 リアクション 0 件 担当者 1 名 GitHub で見る

メンテナーはふだん 1 日以内に返信

@Venkaiahbabuneelam がすでに取り組んでいます。

2026年10月6日 から。

  • #3074 @chauvuusvn による — オープン
  • #3093 @DavidLeeGarrett による — オープン

評価

この issue はまだ評価されていません。

説明

priority: p2 type: bug
Environment
  • google-genai 2.25.0 (latest on PyPI as of 2026-10-06), pydantic 2.13.5
  • Python 3.13.5, Gemini Developer API, model gemini-pro-latest
Summary

When a plain Python function is passed in tools=[...], an optional scalar
parameter such as start_line: int | None = None is declared as:

{"anyOf": [{"type": "integer"}, {"type": "null"}], "type": "object", "default": null}

anyOf already allows only an integer or null. The extra "type": "object"
adds a requirement that the value also be an object, which no integer meets.
The model follows the object part and fills the argument with {}. Calling
the function with the returned args then fails, e.g. max(1, {}) raises
TypeError: '>' not supported between instances of 'dict' and 'int'.

Cause

t_tool in _transformers.py builds callable tools with
FunctionDeclaration.from_callable(..., use_json_schema=True). That path,
parse_function_declaration_json_schema in _automatic_function_calling_util.py,
does:

# pydantic doesn't assign the `type` field when the schema has 'anyOf'.
if not 'type' in json_schema_dict and 'anyOf' in json_schema_dict:
  json_schema_dict['type'] = 'object'

pydantic's schema for int | None is already complete without type. Setting
it to 'object' is only correct for unions of object types, and it breaks
every optional scalar (int | None, str | None, bool | None, ...).

Reproduction
# /// script
# dependencies = ["google-genai"]
# ///
import json
import os

from google import genai
from google.genai import types


def read_file(path: str, start_line: int | None = None, end_line: int | None = None) -> dict:
    """Reads a file, optionally only lines start_line to end_line (1-indexed)."""
    return {}


client = genai.Client(api_key=os.environ["GEMINI_API_KEY"])
config = types.GenerateContentConfig(
    tools=[read_file],
    automatic_function_calling=types.AutomaticFunctionCallingConfig(disable=True),
    tool_config=types.ToolConfig(
        function_calling_config=types.FunctionCallingConfig(mode="ANY")
    ),
)
for i in range(3):
    response = client.models.generate_content(
        model="gemini-pro-latest",
        contents="Read lines 100 to 120 of CONCEPT.md.",
        config=config,
    )
    for call in response.function_calls or []:
        print(json.dumps(call.args))

Output, even though the prompt names the lines explicitly:

{"start_line": {}, "end_line": {}, "path": "CONCEPT.md"}
{"path": "CONCEPT.md", "end_line": {}, "start_line": {}}
{"end_line": {}, "path": "CONCEPT.md", "start_line": {}}

The generated schema can be inspected without an API call:

from google.genai import _transformers
print(_transformers.t_tool(client._api_client, read_file).model_dump(mode="json", exclude_none=True))
Expected behavior

The model sends integers for start_line/end_line, or leaves them out.
Building the declaration through the non-JSON-schema path does exactly that:

types.Tool(function_declarations=[
    types.FunctionDeclaration.from_callable_with_api_option(callable=read_file)
])

This yields {"type": "INTEGER", "nullable": true}. Over 8 trials of a similar
prompt, the model sent integers every time with that schema, and {} every
time with the anyOf + object schema.

Suggested fix

The type: 'object' default exists for a reason: some backends reject an
anyOf schema without a top-level type. See #1807, where Vertex AI fails
with "schema didn't specify the schema type field", and
google/adk-python#3424. So the fix should keep a top-level type but derive
it from the union's non-null members, falling back to 'object' only when
they are objects:

if 'type' not in json_schema_dict and 'anyOf' in json_schema_dict:
  member_types = {
      m.get('type') for m in json_schema_dict['anyOf'] if m.get('type') != 'null'
  }
  json_schema_dict['type'] = (
      member_types.pop() if len(member_types) == 1 and None not in member_types
      else 'object'
  )

I tested the candidate schemas against the Gemini Developer API
(gemini-pro-latest, prompt "Read lines 100 to 120 of CONCEPT.md.", 3 calls
each):

Schema for start_line / end_line Args returned
Current: anyOf[integer, null] + "type": "object" {} 3/3
anyOf[integer, null], no type 100/120 3/3
anyOf[integer, null] + "type": "integer" (proposed) 100/120 3/3
Collapsed: "type": ["integer", "null"] 100/120 3/3

Leaving type unset also works on the Developer API, but it would likely
bring back the Vertex errors in #1807. Collapsing anyOf: [X, null] into a
type list is an alternative if Vertex accepts type lists. I couldn't test
either option on Vertex.

Related
  • #1807: why the type: 'object' default was added (Vertex AI rejects anyOf
    without type).
  • google/adk-python#3424: the same missing-type validation failure for MCP
    tools.
  • mlflow/mlflow#26359: the same symptom (Gemini returns empty arguments for
    anyOf: [T, null]) from a different source, mlflow's gateway forwarding
    OpenAI schemas.
  • #1992 and #2917: nullable-anyOf unwrapping drops title/description. A
    different code path (Schema.from_json_schema), in the same area.
Workaround

Pass explicit declarations built with
FunctionDeclaration.from_callable_with_api_option(callable=fn) instead of
bare callables.

主要言語
Python
スター
4k
フォーク
1k
平均マージ
1日 8時間
マージ済み PR(30日)
73

環境構築

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

googleapis/python-genai のほかの issue

googleapis/python-genai の issue をすべて見る

似ている issue

Python の issue をもっと見る

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。