mirror of
https://github.com/andrewyng/openworker.git
synced 2026-09-11 14:50:14 +00:00
Coerce union-typed JSON Schema for Gemini tool declarations
Vendor MCP schemas with type: [a, b] failed SDK validation and killed every Gemini turn. Null unions become nullable; multi-type unions become anyOf.
This commit is contained in:
@@ -257,7 +257,11 @@ def convert_messages(
|
|||||||
|
|
||||||
|
|
||||||
def _sanitize_schema(schema: Any) -> Any:
|
def _sanitize_schema(schema: Any) -> Any:
|
||||||
"""Strip JSON Schema keys Gemini's OpenAPI subset rejects (recursively)."""
|
"""Strip JSON Schema keys Gemini's OpenAPI subset rejects (recursively), and coerce
|
||||||
|
list-valued `type` (JSON Schema union, e.g. ["string", "number"] — common in vendor
|
||||||
|
MCP tool schemas) into shapes the API accepts: null joins as `nullable`, a single
|
||||||
|
remaining type stays `type`, several become `anyOf` (owner-hit 2026-07-23: monday's
|
||||||
|
compareValue union 400'd every Gemini turn in sessions with MCP tools)."""
|
||||||
if not isinstance(schema, dict):
|
if not isinstance(schema, dict):
|
||||||
return schema
|
return schema
|
||||||
cleaned: dict[str, Any] = {}
|
cleaned: dict[str, Any] = {}
|
||||||
@@ -270,6 +274,14 @@ def _sanitize_schema(schema: Any) -> Any:
|
|||||||
cleaned[key] = _sanitize_schema(value)
|
cleaned[key] = _sanitize_schema(value)
|
||||||
elif key == "anyOf" and isinstance(value, list):
|
elif key == "anyOf" and isinstance(value, list):
|
||||||
cleaned[key] = [_sanitize_schema(sub) for sub in value]
|
cleaned[key] = [_sanitize_schema(sub) for sub in value]
|
||||||
|
elif key == "type" and isinstance(value, list):
|
||||||
|
types = [t for t in value if t != "null"]
|
||||||
|
if len(value) != len(types):
|
||||||
|
cleaned["nullable"] = True
|
||||||
|
if len(types) == 1:
|
||||||
|
cleaned["type"] = types[0]
|
||||||
|
elif types:
|
||||||
|
cleaned["anyOf"] = [{"type": t} for t in types]
|
||||||
else:
|
else:
|
||||||
cleaned[key] = value
|
cleaned[key] = value
|
||||||
return cleaned
|
return cleaned
|
||||||
|
|||||||
@@ -634,3 +634,66 @@ def test_stream_yields_reasoning_deltas_for_thought_parts():
|
|||||||
assert [c.reasoning_delta for c in out if c.reasoning_delta] == ["mull ", "it over"]
|
assert [c.reasoning_delta for c in out if c.reasoning_delta] == ["mull ", "it over"]
|
||||||
final = out[-1].turn
|
final = out[-1].turn
|
||||||
assert final.text == "done" and final.reasoning == "mull it over"
|
assert final.text == "done" and final.reasoning == "mull it over"
|
||||||
|
|
||||||
|
|
||||||
|
def test_sanitize_coerces_union_types():
|
||||||
|
"""Vendor MCP schemas use JSON-Schema union types (owner-hit 2026-07-23: monday's
|
||||||
|
compareValue `type: ['string','number']` 400'd every Gemini turn). Nullable unions
|
||||||
|
become `nullable`, multi-type unions become anyOf."""
|
||||||
|
cleaned = _sanitize_schema(
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"compareValue": {
|
||||||
|
"anyOf": [
|
||||||
|
{"type": "string"},
|
||||||
|
{"type": "array", "items": {"type": ["string", "number"]}},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"maybe": {"type": ["string", "null"]},
|
||||||
|
"nothing": {"type": ["null"]},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
items = cleaned["properties"]["compareValue"]["anyOf"][1]["items"]
|
||||||
|
assert items == {"anyOf": [{"type": "string"}, {"type": "number"}]}
|
||||||
|
assert cleaned["properties"]["maybe"] == {"type": "string", "nullable": True}
|
||||||
|
assert cleaned["properties"]["nothing"] == {"nullable": True}
|
||||||
|
|
||||||
|
|
||||||
|
def test_union_type_schema_validates_as_sdk_config():
|
||||||
|
"""The exact failing shape must pass the SDK's GenerateContentConfig validation."""
|
||||||
|
types_mod = pytest.importorskip("google.genai.types")
|
||||||
|
tools = convert_tools(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": "mcp__monday__search",
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"filters": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"compareValue": {
|
||||||
|
"anyOf": [
|
||||||
|
{"type": "string"},
|
||||||
|
{"type": "number"},
|
||||||
|
{"type": "array", "items": {"type": "string"}},
|
||||||
|
{"type": "array", "items": {"type": ["string", "number"]}},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
]
|
||||||
|
)
|
||||||
|
config = types_mod.GenerateContentConfig.model_validate({"tools": tools})
|
||||||
|
assert config.tools
|
||||||
|
|||||||
Reference in New Issue
Block a user