Skip to content

fix: avoid non-string enums in Gemini tool schemas - #10635

Merged
Soulter merged 1 commit into
AstrBotDevs:masterfrom
niansia:fix/10634
Oct 21, 2026
Merged

Soulter merged 1 commit into
AstrBotDevs:masterfrom
niansia:fix/10634

Conversation

@niansia

@niansia niansia commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #10634.

With the native Gemini provider, a tool parameter like {"type": "integer", "enum": [10, 20, 50]} makes every request fail before it is sent. FastMCP generates exactly this for Literal[10, 20, 50]. ToolSet.google_schema() copies enum as is, but google.genai.types.Schema.enum is list[str], so building types.Tool(...) in _prepare_query_config() raises a pydantic ValidationError. That isn't an APIError, so the request ends with "All chat models failed", and none of the other tools can be used either.

Modifications / 改动点

  • astrbot/core/agent/tool.py, in the Gemini schema conversion:

    • String fields keep their enum.
    • Other fields keep their type and drop enum, and the allowed values are added to the description (Allowed values: 10, 20, 50). Turning the values into strings isn't enough, because the Gemini API only allows enum on STRING fields and answers 400 enum: only allowed for STRING type otherwise. Turning the field into a string doesn't work either: the model would send "20", which tools expecting an integer reject (FastMCP's Literal[10, 20, 50] does). Keeping the type means the model still sends an integer, and the tool's own validation still applies.
    • A null in the enum is left out and expressed with nullable: true.
    • Values are written with json.dumps, so True shows up as true. The tool's own schema isn't modified.
  • Tests in tests/unit/test_tool_google_schema.py: test_google_schema_keeps_enums_only_on_string_fields covers an integer enum with a description, a boolean enum without one and a nullable string enum, checks that the original parameters are untouched, and builds a google.genai.types.Tool from the result, which is the step that failed before.

  • Not included: parameters that only have $ref are still converted to type: null. Fixing that means resolving $defs, which is a separate problem.

  • This is NOT a breaking change. / 这不是一个破坏性变更。

Screenshots or Test Results / 运行截图或测试结果

Verification steps:

  1. Configure the native Gemini provider (googlegenai_chat_completion).
  2. Connect an MCP tool with a Literal[10, 20, 50] parameter.
  3. Send any message.

I don't have a Gemini key here, so the provider's api_base points to a scripted Gemini endpoint. It prints the declaration it receives and, like the real API, answers 400 when a non-STRING field has an enum. The MCP tool is a small FastMCP server, and a OneBot v11 client stands in for NapCat; the rest is a normal python main.py instance. The scripts and the log before the fix are in the zip attached to #10634. The full log after the fix:

gemini_enum_live_after.log

Before, on master @ fc6b326. The request fails locally and nothing reaches the endpoint:

22:40:26 user 10001 -> AstrBot (private): 帮我查一下最近的订单,每页 20 条
[22:40:26.159] [Core] [WARN] [v4.29.1] [runners.tool_loop_agent_runner:636]: Chat Model gemini request error: 3 validation errors for Tool
function_declarations.0.parameters.properties.page_size.enum.0
  Input should be a valid string [type=string_type, input_value=10, input_type=int]
  22:40:26 AstrBot -> user: LLM 响应错误: All chat models failed: ValidationError: 3 validation errors for Tool

After. The endpoint gets an integer parameter with the allowed values in its description, and the MCP tool receives an int:

22:41:00 user 10001 -> AstrBot (private): 帮我查一下最近的订单,每页 20 条
[mock-gemini] list_orders parameters: {"properties": {"page_size": {"description": "Allowed values: 10, 20, 50", "title": "Page Size", "type": "INTEGER"}}, "required": ["page_size"], "title": "list_ordersArguments", "type": "OBJECT"}
[22:41:01.096] [Core] [INFO] [runners.tool_loop_agent_runner:1241]: 使用工具:list_orders,参数:{'page_size': 20}
[22:41:01.118] [Core] [INFO] [runners.tool_loop_agent_runner:1438]: Tool `list_orders` Result: page_size=20 (int): order #1001 paid, order #1002 shipped
  22:41:01 AstrBot -> user: 最近的订单:{"name": "list_orders", "content": "page_size=20 (int): order #1001 paid, order #1002 shipped"}

Tests (Windows 11, Python 3.13):

uv run pytest tests/unit/test_tool_google_schema.py -q
3 passed

uv run pytest tests -q \
  --deselect tests/unit/test_core_lifecycle.py::TestAstrBotCoreLifecycleRestart::test_restart_terminates_managers_and_starts_thread \
  --deselect tests/test_fastapi_v1_dashboard.py::test_config_update_revokes_only_affected_shell_sessions
4605 passed, 70 skipped, 3 deselected, 4 errors

uv run ruff format --check .
519 files already formatted

uv run ruff check .
All checks passed!

With the change to tool.py reverted, the new test fails: the integer field still has its enum and no allowed values in the description.

About the deselected tests and CI: the pytest jobs on current master time out after 30 minutes. Since #10614, restart() awaits asyncio.to_thread(close_trace_store), and test_restart_terminates_managers_and_starts_thread patches threading.Thread, so the executor can't start a worker and restart() never returns. #10617 fixes that test, so the pytest jobs here will likely be cancelled the same way until it's merged. test_config_update_revokes_only_affected_shell_sessions hangs on my machine on master as well and passes in CI. The 4 errors are the long-prompt cases in tests/unit/test_trace_store.py (also from #10614): their parameter IDs exceed Windows' 32767-character limit for environment variables. They error the same way on master, and #10617 shortens those IDs too.


Checklist / 检查清单

  • 😊 No new feature is added in this PR; it is a bug fix for [Bug] Gemini 原生提供商:工具参数含整数 enum(如 MCP 的 Literal[10, 20, 50])时所有请求在本地报 ValidationError #10634.
    / 如果 PR 中有新加入的功能,已经通过 Issue / 邮件等方式和作者讨论过。

  • 👀 My changes have been well-tested, and "Verification Steps" and "Screenshots" have been provided above.
    / 我的更改经过了良好的测试,并已在上方提供了“验证步骤”和“运行截图”。

  • 📚 No WebUI navigation, page structure, or terminology changed, so no update to docs/zh / docs/en is needed.
    / 我已对照变化后的 WebUI 入口、页面结构和术语,核对并在本 PR 中更新 docs/zh 和 docs/en 的相关操作说明与截图(或说明无需更新文档的原因)。入口改名、移动或合并时,已在文档和 changelog 中补充 旧入口 → 新入口 对照。

  • 🤓 No new dependencies are introduced.
    / 我确保没有引入新依赖库,或者引入了新依赖库的同时将其添加到 requirements.txt 和 pyproject.toml 文件相应位置。

  • 😮 My changes do not introduce malicious code.
    / 我的更改没有引入恶意代码。

Summary by Sourcery

Fix Gemini tool schema conversion so non-string enums no longer prevent requests from being built or sent.

Bug Fixes:

  • Prevent native Gemini tool requests from failing when schemas contain non-string enum values.
  • Preserve numeric and boolean parameter types while conveying their allowed values through descriptions, and represent nullable enum values correctly.

Tests:

  • Add coverage for Gemini enum conversion, schema immutability, nullable values, and compatibility with Gemini SDK tool construction.

google.genai's Schema.enum only accepts strings, and the Gemini API only
allows enum on STRING fields ("enum: only allowed for STRING type"). A
tool parameter like {"type": "integer", "enum": [10, 20, 50]}, which
FastMCP generates for Literal[10, 20, 50], made building the request fail
with a ValidationError before anything was sent.

String fields keep their enum. Other fields keep their type, so the model
still sends e.g. an integer, and list the allowed values in the
description instead. A null value is marked as nullable.

Fixes AstrBotDevs#10634

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="astrbot/core/agent/tool.py" line_range="320-324" />
<code_context>
+                        v if isinstance(v, str) else json.dumps(v) for v in values
+                    ]
+                else:
+                    allowed = ", ".join(json.dumps(v) for v in values)
+                    description = result.get("description", "")
+                    result["description"] = (
+                        f"{description}\nAllowed values: {allowed}".strip()
+                    )
+
             if "properties" in schema:
</code_context>
<issue_to_address>
**Out-of-range values reach tools**

When gemini returns a value outside a non-string enum, the tool loop filters only unknown argument names and does not validate values against the original schema, so it passes an out-of-enum value to the handler or MCP server; callers then get a tool error or the tool acts on an unintended value.

Validate tool arguments against the original parameter schema before dispatch and return validation errors to the model for correction.
</issue_to_address>

Sourcery assessment

Approval pending. 1 finding to address first.

Blocking findings: astrbot/core/agent/tool.py:324


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

Comment thread astrbot/core/agent/tool.py
@Soulter
Soulter merged commit 1b10813 into AstrBotDevs:master Oct 21, 2026
20 of 23 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] Gemini 原生提供商:工具参数含整数 enum(如 MCP 的 Literal[10, 20, 50])时所有请求在本地报 ValidationError

2 participants