From 250f13e4cf02b9ee28049627cb6cf1bbc4ae9bcc Mon Sep 17 00:00:00 2001 From: Bryan Subotnick Date: Wed, 22 Apr 2026 11:32:27 -0700 Subject: [PATCH] fix(gemini): stringify integer enum values in tool schemas MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Gemini's Schema.enum field strictly requires TYPE_STRING values. When a Hermes tool declares an integer enum (e.g. discord_server's auto_archive_duration=[60, 1440, 4320, 10080]), Gemini rejects the entire request with HTTP 400: INVALID_ARGUMENT: Invalid value at 'tools[0].function_declarations[N].parameters.properties[M].value.enum[0]' (TYPE_STRING), 60 This happens even when the schema's type is 'integer' — Gemini doesn't coerce. The result is that any subagent routed to a Gemini model with the discord toolset (or any other tool with integer enums) fails every API call, spamming gateway logs with non-retryable 400s. Fix: in agent/gemini_schema.sanitize_gemini_schema, coerce every enum value to its str() representation before passing through. This is lossless for downstream consumers — Gemini echoes back the string form, and the tool dispatcher parses arguments from the model's JSON output where integer coercion already happens. Tests: - 6 new unit tests in tests/agent/test_gemini_schema.py covering integer, string, mixed, nested, items, and non-list enum cases - All 95 existing gemini adapter tests still pass --- agent/gemini_schema.py | 12 ++++++ tests/agent/test_gemini_schema.py | 66 +++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+) create mode 100644 tests/agent/test_gemini_schema.py diff --git a/agent/gemini_schema.py b/agent/gemini_schema.py index 904c99d31b8..1f179fb2af4 100644 --- a/agent/gemini_schema.py +++ b/agent/gemini_schema.py @@ -72,6 +72,18 @@ def sanitize_gemini_schema(schema: Any) -> Dict[str, Any]: if isinstance(item, dict) ] continue + if key == "enum": + # Gemini's Schema.enum requires TYPE_STRING values — it rejects + # integer/float/bool enum entries even when the schema's ``type`` + # is ``integer`` or ``number``. Stringify every value so tools + # like discord_server's auto_archive_duration=[60, 1440, ...] + # don't trigger "Invalid value at enum[0] (TYPE_STRING)" 400s. + if isinstance(value, list): + cleaned[key] = [ + str(v) if not isinstance(v, str) else v + for v in value + ] + continue cleaned[key] = value return cleaned diff --git a/tests/agent/test_gemini_schema.py b/tests/agent/test_gemini_schema.py new file mode 100644 index 00000000000..94f4538b72f --- /dev/null +++ b/tests/agent/test_gemini_schema.py @@ -0,0 +1,66 @@ +"""Tests for agent.gemini_schema.sanitize_gemini_schema.""" + +from agent.gemini_schema import ( + sanitize_gemini_schema, + sanitize_gemini_tool_parameters, +) + + +def test_integer_enum_is_stringified(): + """Gemini's Schema.enum only accepts TYPE_STRING values. + + Tools like discord_server's auto_archive_duration declare integer enums + ([60, 1440, 4320, 10080]). Those would raise Gemini HTTP 400 + 'Invalid value at enum[0] (TYPE_STRING)' if passed through unchanged. + """ + schema = { + "type": "integer", + "enum": [60, 1440, 4320, 10080], + "description": "Thread archive duration in minutes.", + } + cleaned = sanitize_gemini_schema(schema) + assert cleaned["enum"] == ["60", "1440", "4320", "10080"] + assert cleaned["type"] == "integer" + + +def test_string_enum_is_preserved(): + schema = {"type": "string", "enum": ["alpha", "beta", "gamma"]} + cleaned = sanitize_gemini_schema(schema) + assert cleaned["enum"] == ["alpha", "beta", "gamma"] + + +def test_mixed_enum_is_stringified(): + schema = {"enum": ["a", 1, 2.5, True]} + cleaned = sanitize_gemini_schema(schema) + assert cleaned["enum"] == ["a", "1", "2.5", "True"] + + +def test_nested_enum_inside_properties_is_stringified(): + schema = { + "type": "object", + "properties": { + "auto_archive_duration": { + "type": "integer", + "enum": [60, 1440], + }, + "name": {"type": "string"}, + }, + } + cleaned = sanitize_gemini_tool_parameters(schema) + assert cleaned["properties"]["auto_archive_duration"]["enum"] == ["60", "1440"] + assert "enum" not in cleaned["properties"]["name"] + + +def test_enum_in_items_is_stringified(): + schema = { + "type": "array", + "items": {"type": "integer", "enum": [1, 2, 3]}, + } + cleaned = sanitize_gemini_schema(schema) + assert cleaned["items"]["enum"] == ["1", "2", "3"] + + +def test_non_list_enum_is_dropped(): + schema = {"enum": "not-a-list"} + cleaned = sanitize_gemini_schema(schema) + assert "enum" not in cleaned