Callable tools: optional scalar parameters (`int | None`) get `"type": "object"` in `parameters_json_schema`, so Gemini sends `{}`
メンテナーはふだん 1 日以内に返信
@Venkaiahbabuneelam がすでに取り組んでいます。
2026年10月6日 から。
評価
この issue はまだ評価されていません。
説明
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 rejectsanyOf
withouttype). - google/adk-python#3424: the same missing-
typevalidation 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-
anyOfunwrapping dropstitle/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
環境構築
- Dockerfile・Docker Compose ファイルなし
- プルリクエストのテンプレートなし
- コントリビューションガイドを読む
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
googleapis/python-genai のほかの issue
-
難易度 2/5 1〜3時間 初心者へのやさしさ 67/100
googleapis/python-genai#3105 ·
メンテナーはふだん 1 日以内に返信
-
Curated history keeps half a user turn when the model turn is invalid対応中かも @Venkaiahbabuneelam が 8 日前に担当しました。 オープンpriority: p2 status:awaiting user response type: bug
難易度 2/5 1〜3時間 初心者へのやさしさ 62/100
googleapis/python-genai#3051 · コメント 2 件 · 担当者 1 名 ·
メンテナーはふだん 1 日以内に返信
-
[Bug]: Unsubscripted typing.List and typing.Dict crash convert_if_exist_pydantic_model in AFC対応中かも @Venkaiahbabuneelam が 8 日前に担当しました。 オープンpriority: p2 type: bug
難易度 2/5 1〜3時間 初心者へのやさしさ 82/100
googleapis/python-genai#3044 · コメント 1 件 · 担当者 1 名 ·
メンテナーはふだん 1 日以内に返信
-
Name collision in google.genai.interactions: triggers.Interaction shadows response model Interaction in static type checkers対応中かも @Venkaiahbabuneelam が 15 日前に担当しました。 オープンpriority: p2 type: bug
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
googleapis/python-genai#3013 · コメント 1 件 · 担当者 1 名 ·
メンテナーはふだん 1 日以内に返信
-
Batch API: job with 446 inline image requests SUCCEEDED but batches.get returns no dest/inlinedResponses (results lost)対応中かも @Venkaiahbabuneelam が 1 日前に担当しました。 オープンpriority: p2 status:awaiting user response type: bug
難易度 4/5 3〜5日 初心者へのやさしさ 48/100
googleapis/python-genai#3100 · コメント 2 件 · 担当者 1 名 ·
メンテナーはふだん 1 日以内に返信
googleapis/python-genai の issue をすべて見る
似ている issue
-
bug difficulty:2 good first issue help wanted
難易度 2/5 1〜3時間 初心者へのやさしさ 88/100
MakazhanAlpamys/Soup#1865 ·
メンテナーはふだん 1 日以内に返信
-
area: agents area: docs good first issue
難易度 2/5 1〜3時間 初心者へのやさしさ 88/100
openapi/gknowledge#23 ·
-
useless-suppression reported for deprecated-* messages that should be exempt対応中かも @adityasinha-ghub が今日担当しました。 オープン
難易度 2/5 1〜3時間 初心者へのやさしさ 88/100
pylint-dev/pylint#11656 ·
メンテナーはふだん 1 日以内に返信
-
area:provisioning effort:S priority:P2 provider:local-libvirt risk:daytime-only status:in-progress type:bug
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
randomparity/kdive#3203 · コメント 2 件 ·
メンテナーはふだん 1 日以内に返信
-
get() for StrEnum対応中かも @FeJo5952 が今日担当しました。 オープンgood first issue
難易度 2/5 1〜3時間 初心者へのやさしさ 72/100
collective/icalendar#1921 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信