From 9a09e9d2b3206534984174b2ab96ddc4ad201022 Mon Sep 17 00:00:00 2001 From: niansia Date: Mon, 19 Oct 2026 22:32:52 +0800 Subject: [PATCH] fix: avoid non-string enums in Gemini tool schemas 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 #10634 --- astrbot/core/agent/tool.py | 20 ++++++++++++ tests/unit/test_tool_google_schema.py | 45 +++++++++++++++++++++++++++ 2 files changed, 65 insertions(+) diff --git a/astrbot/core/agent/tool.py b/astrbot/core/agent/tool.py index 2cf6b6747c0..9e31fcc60db 100644 --- a/astrbot/core/agent/tool.py +++ b/astrbot/core/agent/tool.py @@ -1,4 +1,5 @@ import copy +import json from collections.abc import AsyncGenerator, Awaitable, Callable from typing import Any, Generic @@ -303,6 +304,25 @@ def convert_schema(schema: dict) -> dict: } result.update({k: schema[k] for k in support_fields if k in schema}) + # Gemini only accepts string enums ("enum: only allowed for STRING + # type"). Other fields keep their type, so the model still sends + # e.g. an integer, and list the allowed values in the description. + if "enum" in result: + enum = result.pop("enum") + values = [v for v in enum if v is not None] + if None in enum: + result["nullable"] = True + if result["type"] == "string": + result["enum"] = [ + 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: properties = {} for key, value in schema["properties"].items(): diff --git a/tests/unit/test_tool_google_schema.py b/tests/unit/test_tool_google_schema.py index 0826e99e77d..6385778ec5a 100644 --- a/tests/unit/test_tool_google_schema.py +++ b/tests/unit/test_tool_google_schema.py @@ -6,6 +6,8 @@ from pathlib import Path from typing import Generic, TypeVar +from google.genai import types as genai_types + REPO_ROOT = Path(__file__).resolve().parents[2] TOOL_MODULE_PATH = REPO_ROOT / "astrbot/core/agent/tool.py" @@ -77,6 +79,49 @@ def test_google_schema_fills_missing_array_items_with_string_schema(): assert source_uuids["items"] == {"type": "string"} +def test_google_schema_keeps_enums_only_on_string_fields(): + tool_module = load_tool_module() + FunctionTool = tool_module.FunctionTool + ToolSet = tool_module.ToolSet + + tool = FunctionTool( + name="list_items", + description="List items.", + parameters={ + "type": "object", + "properties": { + "page_size": { + "type": "integer", + "description": "Items per page.", + "enum": [10, 20, 50], + }, + "strict": {"type": "boolean", "enum": [True]}, + "order": {"type": ["string", "null"], "enum": ["asc", "desc", None]}, + }, + }, + ) + + schema = ToolSet([tool]).google_schema() + properties = schema["function_declarations"][0]["parameters"]["properties"] + + assert properties["page_size"] == { + "type": "integer", + "description": "Items per page.\nAllowed values: 10, 20, 50", + } + assert properties["strict"] == { + "type": "boolean", + "description": "Allowed values: true", + } + assert properties["order"] == { + "type": "string", + "enum": ["asc", "desc"], + "nullable": True, + } + assert tool.parameters["properties"]["page_size"]["enum"] == [10, 20, 50] + # The Gemini SDK rejects non-string enum values when the request is built. + genai_types.Tool(function_declarations=schema["function_declarations"]) + + def test_openai_schema_sorts_tools_by_name_without_mutating_toolset_order(): tool_module = load_tool_module() FunctionTool = tool_module.FunctionTool