fix(antigravity): unblock Gemini chats — schema sanitizer + UA bump (#7170)
* fix(antigravity): translate JSON Schema unions to Gemini nullable Tool parameter schemas using JSON Schema 2020-12 unions like "type": ["string", "null"] crash Gemini's function_declarations parser with HTTP 400. Two existing tools trip this: - core/framework/tasks/tools/colony_tools.py:52 (owner in _update_schema) - core/framework/tasks/tools/session_tools.py:84-87 (same shape) Add an adapter-level sanitizer that walks the schema tree and converts union-with-null to OpenAPI 3.0 "nullable": true (which Gemini accepts). Recurses into properties, items, additionalProperties, and the anyOf/oneOf/allOf combinators. Source schemas remain valid JSON Schema so OpenAI/Anthropic backends are unaffected. * fix(antigravity): bump spoofed UA past Google's deprecation cutoff Google has deprecated client version "Antigravity/1.18.3" — chats now return "This version of Antigravity is no longer supported" instead of a real model response. Bump the spoofed User-Agent to "Antigravity/1.23.2" + "Electron/39.2.3" (current desktop release) and add a comment that this needs periodic re-bumping. A more durable fix (auto-detect from the installed app's Info.plist) is a follow-up. * fix(antigravity): fail loud on multi-type non-null Gemini schema unions Per review on PR #7170: silently picking the first type from a union like ["string", "integer", "null"] changes the contract for callers that rely on the other types, and the failure is hard to diagnose at the Gemini side. Replace the silent narrowing with a ValueError that points the schema author at anyOf or a single type. A repo scan finds no current Gemini-bound schemas using multi-type non-null unions, so this branch is preventative for future authors. * chore(antigravity): drop em dash from test docstring
This commit is contained in:
@@ -61,10 +61,12 @@ _IDE_STATE_DB_KEY = "antigravityUnifiedStateSync.oauthToken"
|
||||
|
||||
_BASE_HEADERS: dict[str, str] = {
|
||||
# Mimic the Antigravity Electron app so the API accepts the request.
|
||||
# Google deprecates older client versions over time, so this needs periodic
|
||||
# bumping to match whatever the current Antigravity desktop release advertises.
|
||||
"User-Agent": (
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 "
|
||||
"(KHTML, like Gecko) Antigravity/1.18.3 Chrome/138.0.7204.235 "
|
||||
"Electron/37.3.1 Safari/537.36"
|
||||
"(KHTML, like Gecko) Antigravity/1.23.2 Chrome/138.0.7204.235 "
|
||||
"Electron/39.2.3 Safari/537.36"
|
||||
),
|
||||
"X-Goog-Api-Client": "google-cloud-sdk vscode_cloudshelleditor/0.1",
|
||||
"Client-Metadata": '{"ideType":"ANTIGRAVITY","platform":"MACOS","pluginType":"GEMINI"}',
|
||||
@@ -254,6 +256,56 @@ def _clean_tool_name(name: str) -> str:
|
||||
return name[:64]
|
||||
|
||||
|
||||
def _sanitize_schema_for_gemini(schema: Any) -> Any:
|
||||
"""Convert JSON Schema 2020-12 features to the OpenAPI 3.0 dialect Gemini accepts.
|
||||
|
||||
Gemini's function_declarations parser rejects union ``"type": ["string", "null"]``.
|
||||
Translate any such union to a single type plus ``"nullable": true``. Recurse into
|
||||
``properties``, ``items``, and the ``anyOf``/``oneOf``/``allOf`` combinators.
|
||||
"""
|
||||
if isinstance(schema, list):
|
||||
return [_sanitize_schema_for_gemini(s) for s in schema]
|
||||
if not isinstance(schema, dict):
|
||||
return schema
|
||||
|
||||
out = dict(schema)
|
||||
t = out.get("type")
|
||||
if isinstance(t, list):
|
||||
non_null = [x for x in t if x != "null"]
|
||||
has_null = "null" in t
|
||||
if len(non_null) == 1:
|
||||
out["type"] = non_null[0]
|
||||
if has_null:
|
||||
out["nullable"] = True
|
||||
elif not non_null and has_null:
|
||||
# Pure null type: fall back to string-nullable.
|
||||
out["type"] = "string"
|
||||
out["nullable"] = True
|
||||
else:
|
||||
# Multi-type non-null unions (e.g. ["string", "integer", "null"])
|
||||
# have no faithful Gemini equivalent. Silently picking one type
|
||||
# changes the contract for callers who rely on the others, so
|
||||
# fail loud and let the schema author rewrite it as anyOf or
|
||||
# narrow to a single type.
|
||||
raise ValueError(
|
||||
f"Unsupported Gemini schema union: {t!r}. "
|
||||
"Gemini accepts a single primitive type plus optional 'nullable: true'. "
|
||||
"Rewrite as anyOf or pick a single type."
|
||||
)
|
||||
|
||||
if "properties" in out and isinstance(out["properties"], dict):
|
||||
out["properties"] = {k: _sanitize_schema_for_gemini(v) for k, v in out["properties"].items()}
|
||||
if "items" in out:
|
||||
out["items"] = _sanitize_schema_for_gemini(out["items"])
|
||||
if "additionalProperties" in out and isinstance(out["additionalProperties"], dict):
|
||||
out["additionalProperties"] = _sanitize_schema_for_gemini(out["additionalProperties"])
|
||||
for combinator in ("anyOf", "oneOf", "allOf"):
|
||||
if combinator in out:
|
||||
out[combinator] = _sanitize_schema_for_gemini(out[combinator])
|
||||
|
||||
return out
|
||||
|
||||
|
||||
def _to_gemini_contents(
|
||||
messages: list[dict[str, Any]],
|
||||
thought_sigs: dict[str, str] | None = None,
|
||||
@@ -555,11 +607,13 @@ class AntigravityProvider(LLMProvider):
|
||||
{
|
||||
"name": _clean_tool_name(t.name),
|
||||
"description": t.description,
|
||||
"parameters": t.parameters
|
||||
or {
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
},
|
||||
"parameters": _sanitize_schema_for_gemini(
|
||||
t.parameters
|
||||
or {
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
}
|
||||
),
|
||||
}
|
||||
for t in tools
|
||||
]
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
"""Tests for the Antigravity Gemini schema sanitizer.
|
||||
|
||||
Run with:
|
||||
cd core
|
||||
pytest tests/test_antigravity_schema.py -v
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from framework.llm.antigravity import _sanitize_schema_for_gemini
|
||||
|
||||
|
||||
def test_union_with_null_becomes_nullable():
|
||||
assert _sanitize_schema_for_gemini({"type": ["string", "null"]}) == {
|
||||
"type": "string",
|
||||
"nullable": True,
|
||||
}
|
||||
|
||||
|
||||
def test_plain_schema_passthrough():
|
||||
assert _sanitize_schema_for_gemini({"type": "string"}) == {"type": "string"}
|
||||
|
||||
|
||||
def test_recurses_into_properties():
|
||||
out = _sanitize_schema_for_gemini(
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {"type": "integer"},
|
||||
"owner": {"type": ["string", "null"]},
|
||||
},
|
||||
"required": ["id"],
|
||||
}
|
||||
)
|
||||
assert out["properties"]["id"] == {"type": "integer"}
|
||||
assert out["properties"]["owner"] == {"type": "string", "nullable": True}
|
||||
assert out["required"] == ["id"]
|
||||
|
||||
|
||||
def test_recurses_into_items():
|
||||
assert _sanitize_schema_for_gemini({"type": "array", "items": {"type": ["integer", "null"]}}) == {
|
||||
"type": "array",
|
||||
"items": {"type": "integer", "nullable": True},
|
||||
}
|
||||
|
||||
|
||||
def test_recurses_into_combinators():
|
||||
assert _sanitize_schema_for_gemini({"anyOf": [{"type": ["string", "null"]}, {"type": "integer"}]}) == {
|
||||
"anyOf": [{"type": "string", "nullable": True}, {"type": "integer"}]
|
||||
}
|
||||
|
||||
|
||||
def test_does_not_mutate_input():
|
||||
schema = {"type": "object", "properties": {"x": {"type": ["string", "null"]}}}
|
||||
snapshot = {"type": "object", "properties": {"x": {"type": ["string", "null"]}}}
|
||||
_sanitize_schema_for_gemini(schema)
|
||||
assert schema == snapshot
|
||||
|
||||
|
||||
def test_pure_null_type_falls_back_to_string():
|
||||
assert _sanitize_schema_for_gemini({"type": ["null"]}) == {
|
||||
"type": "string",
|
||||
"nullable": True,
|
||||
}
|
||||
|
||||
|
||||
def test_multi_type_non_null_union_raises():
|
||||
"""Silently picking one type would change the contract; fail loud instead."""
|
||||
with pytest.raises(ValueError, match="Unsupported Gemini schema union"):
|
||||
_sanitize_schema_for_gemini({"type": ["string", "integer", "null"]})
|
||||
|
||||
with pytest.raises(ValueError, match="Unsupported Gemini schema union"):
|
||||
_sanitize_schema_for_gemini({"type": ["string", "integer"]})
|
||||
Reference in New Issue
Block a user