Skip to content

Commit b22495d

Browse files
Strip the full set of Gemini-rejected JSON-Schema keywords from tool definitions
Expanding the earlier $schema/additionalProperties/title fix: the default MCP tools carry more validation keywords Gemini's function_declarations reject (exclusiveMinimum/ Maximum, multipleOf, patternProperties, …), each 400-ing a tool-bound turn. Strip the full validation/metadata keyword set (structural keys kept). Verified end-to-end: the live brain replies on gemini-2.5-flash-lite with all 28 default MCP tools loaded. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 0c8da5a commit b22495d

2 files changed

Lines changed: 62 additions & 17 deletions

File tree

aai_cli/code_agent/model.py

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -158,10 +158,32 @@ def _is_empty_arguments(arguments: object) -> bool:
158158
return isinstance(parsed, dict) and not parsed
159159

160160

161-
# JSON-Schema metadata keys some gateway-routed models reject on tool definitions. OpenAI
162-
# ignores them, but Gemini's ``function_declarations`` 400 on ``$schema`` (and friends), which
163-
# kills any tool-bound turn — so strip them from every tool's parameter schema before sending.
164-
_UNSUPPORTED_SCHEMA_KEYS = ("$schema", "additionalProperties", "title")
161+
# JSON-Schema keywords some gateway-routed models reject on tool definitions. OpenAI ignores
162+
# them, but Gemini's ``function_declarations`` 400 on them ("Unknown name …"), which kills any
163+
# tool-bound turn. These are all validation/metadata keywords — stripping them leaves the
164+
# structural schema (type/properties/items/required/enum/anyOf/description/…) the model needs
165+
# to call the tool, so the call still works; only the unenforced constraints are dropped.
166+
_UNSUPPORTED_SCHEMA_KEYS = (
167+
"$schema",
168+
"$id",
169+
"$comment",
170+
"title",
171+
"default",
172+
"examples",
173+
"const",
174+
"additionalProperties",
175+
"unevaluatedProperties",
176+
"patternProperties",
177+
"minProperties",
178+
"maxProperties",
179+
"propertyNames",
180+
"exclusiveMinimum",
181+
"exclusiveMaximum",
182+
"multipleOf",
183+
"additionalItems",
184+
"unevaluatedItems",
185+
"contains",
186+
)
165187

166188

167189
def _sanitize_tool_schemas(payload: object) -> None:

tests/test_code_model.py

Lines changed: 36 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -174,17 +174,40 @@ def test_ensure_tool_call_arguments_guards() -> None:
174174

175175

176176
def test_sanitize_tool_schemas_strips_model_incompatible_keys() -> None:
177-
# Gemini's function_declarations 400 on $schema/additionalProperties/title; strip them
178-
# recursively from each tool's parameters so a tool-bound request works on every model.
179-
city: dict[str, object] = {"type": "string", "title": "City"} # held ref (nested dict)
180-
any_of: list[object] = [{"$schema": "x", "type": "string"}] # held ref (nested list)
177+
# Gemini's function_declarations 400 on these validation/metadata keywords; strip every
178+
# one (recursively) while keeping structural keys, so a tool-bound request works.
179+
denied = [
180+
"$schema",
181+
"$id",
182+
"$comment",
183+
"title",
184+
"default",
185+
"examples",
186+
"const",
187+
"additionalProperties",
188+
"unevaluatedProperties",
189+
"patternProperties",
190+
"minProperties",
191+
"maxProperties",
192+
"propertyNames",
193+
"exclusiveMinimum",
194+
"exclusiveMaximum",
195+
"multipleOf",
196+
"additionalItems",
197+
"unevaluatedItems",
198+
"contains",
199+
]
200+
# Pin the shipped denylist against this list: a renamed/dropped key would silently leak an
201+
# unsupported keyword to Gemini (and break a tool-bound turn).
202+
assert set(model_mod._UNSUPPORTED_SCHEMA_KEYS) == set(denied)
203+
204+
nested: dict[str, object] = {"type": "string", **dict.fromkeys(denied, "x")}
205+
inside_list: dict[str, object] = {"type": "number", **dict.fromkeys(denied, "x")}
181206
params: dict[str, object] = {
182-
"$schema": "https://json-schema.org/draft/2020-12/schema",
183207
"type": "object",
184-
"additionalProperties": False,
185-
"title": "Args",
186-
"properties": {"city": city},
187-
"anyOf": any_of,
208+
"properties": {"city": nested}, # nested dict
209+
"anyOf": [inside_list], # nested list
210+
**dict.fromkeys(denied, "x"),
188211
}
189212
payload: dict[str, object] = {
190213
"tools": [
@@ -194,10 +217,10 @@ def test_sanitize_tool_schemas_strips_model_incompatible_keys() -> None:
194217
]
195218
}
196219
model_mod._sanitize_tool_schemas(payload)
197-
assert not ({"$schema", "additionalProperties", "title"} & set(params)) # top-level stripped
198-
assert params["type"] == "object" # real schema keys preserved
199-
assert city == {"type": "string"} # nested dict stripped
200-
assert any_of == [{"type": "string"}] # nested list stripped
220+
assert not (set(denied) & set(params)) # every denied key stripped at the top level
221+
assert params["type"] == "object" # structural keys preserved
222+
assert nested == {"type": "string"} # nested dict fully stripped
223+
assert inside_list == {"type": "number"} # nested-in-list fully stripped
201224

202225

203226
def test_sanitize_tool_schemas_guards() -> None:

0 commit comments

Comments
 (0)