From b78ff50d8d59ece8868af7d65d28888055dcb370 Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Mon, 13 Jul 2026 17:28:07 -0700 Subject: [PATCH] fix(gemini): prune required entries missing from properties in tool schemas MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Port from Kilo-Org/kilocode#11955: Gemini validates every object schema's required list strictly against the same node's properties and fails the ENTIRE GenerateContentRequest with HTTP 400 'required[0]: property is not defined' when a name has no matching property. MCP servers (e.g. the GitHub remote MCP) routinely emit array item schemas carrying required without properties, which made every request on the native Gemini path fail before any model output. sanitize_gemini_schema() now filters required to names present in the node's properties and drops the keyword when nothing valid remains. Applies recursively (properties / items / anyOf). Tool handlers still validate required fields at execution time, so nothing the model could actually use is lost. Scoped to the Gemini-facing sanitizer only — the universal tools/schema_sanitizer.py already prunes typed object nodes, and its remaining gap (untyped nodes) is contested by open PR #20151. --- agent/gemini_schema.py | 24 +++++++++ tests/agent/test_gemini_schema.py | 87 +++++++++++++++++++++++++++++++ 2 files changed, 111 insertions(+) diff --git a/agent/gemini_schema.py b/agent/gemini_schema.py index 7d5385063ec0..b0985422fbb1 100644 --- a/agent/gemini_schema.py +++ b/agent/gemini_schema.py @@ -87,6 +87,30 @@ def sanitize_gemini_schema(schema: Any) -> Dict[str, Any]: if any(not isinstance(item, str) for item in enum_val): cleaned.pop("enum", None) + # Gemini validates ``required`` strictly against the same node's + # ``properties`` — GenerateContentRequest fails with HTTP 400 + # "...items.required[0]: property is not defined" when a required name + # has no matching property in that node. MCP servers routinely emit + # this shape (e.g. the GitHub remote MCP's array item schemas carry + # ``required`` without ``properties``), and one bad tool schema fails + # the ENTIRE request before any model output. Filter ``required`` to + # names that exist in this node's ``properties`` and drop it when + # nothing valid remains. The tool handler still validates required + # fields at execution time, so this only removes what Gemini couldn't + # accept anyway. (Port of Kilo-Org/kilocode#11955.) + required_val = cleaned.get("required") + if isinstance(required_val, list): + props_val = cleaned.get("properties") + prop_names = set(props_val.keys()) if isinstance(props_val, dict) else set() + valid_required = [ + name for name in required_val + if isinstance(name, str) and name in prop_names + ] + if not valid_required: + cleaned.pop("required", None) + elif len(valid_required) != len(required_val): + cleaned["required"] = valid_required + return cleaned diff --git a/tests/agent/test_gemini_schema.py b/tests/agent/test_gemini_schema.py index 069c99a21809..88bd163eb8d2 100644 --- a/tests/agent/test_gemini_schema.py +++ b/tests/agent/test_gemini_schema.py @@ -108,6 +108,93 @@ class TestSanitizeGeminiSchema: assert sanitize_gemini_schema([1, 2, 3]) == {} +class TestRequiredPropertyPruning: + """Gemini rejects ``required`` names missing from the node's ``properties``. + + Regression for the Kilo-Org/kilocode#11955 bug class: MCP servers (e.g. + the GitHub remote MCP) emit array item schemas whose ``required`` lists + reference properties that don't exist in the same node — Google fails the + entire GenerateContentRequest with HTTP 400 "property is not defined". + """ + + def test_drops_required_when_node_has_no_properties(self): + schema = {"type": "object", "required": ["a", "b"]} + cleaned = sanitize_gemini_schema(schema) + assert "required" not in cleaned + + def test_filters_ghost_required_entries(self): + schema = { + "type": "object", + "properties": {"x": {"type": "string"}}, + "required": ["x", "ghost"], + } + cleaned = sanitize_gemini_schema(schema) + assert cleaned["required"] == ["x"] + + def test_prunes_inside_array_items(self): + """The exact shape from the GitHub MCP report — nested in items.""" + schema = { + "type": "object", + "properties": { + "issue_fields": { + "type": "array", + "items": { + "type": "object", + "required": ["field_id", "value"], + }, + }, + }, + "required": ["issue_fields"], + } + cleaned = sanitize_gemini_schema(schema) + items = cleaned["properties"]["issue_fields"]["items"] + assert "required" not in items + # Top-level required is valid and survives. + assert cleaned["required"] == ["issue_fields"] + + def test_prunes_node_without_explicit_type(self): + """Nodes carrying properties+required but no ``type`` key still prune.""" + schema = { + "properties": {"x": {"type": "string"}}, + "required": ["x", "ghost"], + } + cleaned = sanitize_gemini_schema(schema) + assert cleaned["required"] == ["x"] + + def test_valid_required_untouched(self): + schema = { + "type": "object", + "properties": {"a": {"type": "string"}, "b": {"type": "integer"}}, + "required": ["a", "b"], + } + cleaned = sanitize_gemini_schema(schema) + assert cleaned["required"] == ["a", "b"] + + def test_drops_non_string_required_entries(self): + schema = { + "type": "object", + "properties": {"a": {"type": "string"}}, + "required": ["a", 42, None], + } + cleaned = sanitize_gemini_schema(schema) + assert cleaned["required"] == ["a"] + + def test_prunes_inside_anyof_branches(self): + schema = { + "anyOf": [ + { + "type": "object", + "properties": {"x": {"type": "string"}}, + "required": ["x", "ghost"], + }, + {"type": "object", "required": ["orphan"]}, + ] + } + cleaned = sanitize_gemini_schema(schema) + assert cleaned["anyOf"][0]["required"] == ["x"] + assert "required" not in cleaned["anyOf"][1] + + class TestSanitizeGeminiToolParameters: def test_empty_parameters_return_valid_object_schema(self): """Gemini requires ``parameters`` to be a valid object schema."""