mirror of
https://github.com/andrewyng/openworker.git
synced 2026-09-13 15:50:02 +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:
|
||||
"""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):
|
||||
return schema
|
||||
cleaned: dict[str, Any] = {}
|
||||
@@ -270,6 +274,14 @@ def _sanitize_schema(schema: Any) -> Any:
|
||||
cleaned[key] = _sanitize_schema(value)
|
||||
elif key == "anyOf" and isinstance(value, list):
|
||||
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:
|
||||
cleaned[key] = value
|
||||
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"]
|
||||
final = out[-1].turn
|
||||
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