feat: Implement JSON Schema sanitization for Gemini API compatibility and enhance OpenClaw setup script

This commit is contained in:
hlohaus
2026-04-01 22:29:22 +02:00
parent 91ccb19661
commit 71ed6243f9
6 changed files with 95 additions and 21 deletions
+27 -1
View File
@@ -40,6 +40,32 @@ from ..helper import get_connector, get_system_prompt, format_media_prompt
from ... import debug
# JSON Schema keywords not supported by the Gemini API
_UNSUPPORTED_SCHEMA_KEYS = {
"patternProperties", "$schema", "$id", "$defs", "definitions",
"if", "then", "else", "not", "allOf", "anyOf", "oneOf",
"default", "examples", "readOnly", "writeOnly",
"contentEncoding", "contentMediaType", "additionalProperties",
}
def _sanitize_schema(schema: dict) -> dict:
"""Recursively remove JSON Schema keywords unsupported by the Gemini API."""
if not isinstance(schema, dict):
return schema
result = {}
for k, v in schema.items():
if k in _UNSUPPORTED_SCHEMA_KEYS:
continue
if isinstance(v, dict):
result[k] = _sanitize_schema(v)
elif isinstance(v, list):
result[k] = [_sanitize_schema(i) if isinstance(i, dict) else i for i in v]
else:
result[k] = v
return result
def get_antigravity_oauth_creds_path():
"""Get the default path for Antigravity OAuth credentials."""
return Path.home() / ".antigravity" / "oauth_creds.json"
@@ -1079,7 +1105,7 @@ class AntigravityProvider:
function_declarations.append({
"name": func.get("name"),
"description": func.get("description", ""),
"parameters": func.get("parameters", {})
"parameters": _sanitize_schema(func.get("parameters", {}))
})
if function_declarations:
gemini_tools = [{"functionDeclarations": function_declarations}]
+27 -1
View File
@@ -29,6 +29,32 @@ from ..helper import get_connector, get_system_prompt, format_media_prompt
from ... import debug
# JSON Schema keywords not supported by the Gemini API
_UNSUPPORTED_SCHEMA_KEYS = {
"patternProperties", "$schema", "$id", "$defs", "definitions",
"if", "then", "else", "not", "allOf", "anyOf", "oneOf",
"default", "examples", "readOnly", "writeOnly",
"contentEncoding", "contentMediaType", "additionalProperties",
}
def _sanitize_schema(schema: dict) -> dict:
"""Recursively remove JSON Schema keywords unsupported by the Gemini API."""
if not isinstance(schema, dict):
return schema
result = {}
for k, v in schema.items():
if k in _UNSUPPORTED_SCHEMA_KEYS:
continue
if isinstance(v, dict):
result[k] = _sanitize_schema(v)
elif isinstance(v, list):
result[k] = [_sanitize_schema(i) if isinstance(i, dict) else i for i in v]
else:
result[k] = v
return result
def get_oauth_creds_path():
return Path.home() / ".gemini" / "oauth_creds.json"
@@ -674,7 +700,7 @@ class GeminiCLIProvider():
function_declarations.append({
"name": func.get("name"),
"description": func.get("description", ""),
"parameters": func.get("parameters", {})
"parameters": _sanitize_schema(func.get("parameters", {}))
})
if function_declarations:
gemini_tools = [{"functionDeclarations": function_declarations}]
+1
View File
@@ -298,6 +298,7 @@ class Api:
details = exc.errors()
modified_details = []
for error in details:
debug.log(f"Validation error: {error['loc']} - {error['msg']} ({error['type']})")
modified_details.append({
"loc": error["loc"],
"message": error["msg"],
+13 -5
View File
@@ -9,11 +9,19 @@ from typing import Optional
@lru_cache(maxsize=1)
def get_config_dir() -> Path:
"""Get platform-appropriate config directory."""
if sys.platform == "win32":
return Path(os.environ.get("APPDATA", Path.home() / "AppData" / "Roaming"))
elif sys.platform == "darwin":
return Path.home() / "Library" / "Application Support"
return Path.home() / ".config"
def get_fallback_config_dir() -> Path:
if sys.platform == "win32":
return Path(os.environ.get("APPDATA", Path.home() / "AppData" / "Roaming"))
elif sys.platform == "darwin":
return Path.home() / "Library" / "Application Support"
return Path.home() / ".config"
config_dir = Path.home() / ".config"
if not config_dir.exists():
config_dir = get_fallback_config_dir()
if not config_dir.exists():
config_dir = Path.home() / ".g4f"
config_dir.mkdir(parents=True, exist_ok=True)
return config_dir
DEFAULT_PORT = 1337
DEFAULT_TIMEOUT = 600
+15 -2
View File
@@ -54,9 +54,20 @@ class ContentPart(TypedDict, total=False):
bucket_id: str
name: str
class Message(TypedDict):
class ToolCallFunction(TypedDict, total=False):
name: str
arguments: str # JSON-encoded arguments string
class ToolCall(TypedDict, total=False):
id: str
type: str # e.g., "function"
function: ToolCallFunction
class Message(TypedDict, total=False):
role: str
content: Union[str, List[ContentPart]]
content: Optional[Union[str, List[ContentPart]]]
tool_calls: Optional[List[ToolCall]]
tool_call_id: str # present on "tool" role messages
Messages = List[Message]
@@ -86,6 +97,8 @@ __all__ = [
"Messages",
"Message",
"ContentPart",
"ToolCall",
"ToolCallFunction",
"Cookies",
"PILImage", # Changed from "Image" to "PILImage" to match the actual class name
"ImageType",
+12 -12
View File
@@ -7,7 +7,7 @@
set -e
if [ -z "$1" ]; then
echo "No API key provided. Proceeding without a Pollinations API key."
echo "No API key provided. Proceeding without an API key."
API_KEY=""
else
API_KEY="$1"
@@ -46,7 +46,7 @@ EOF
cat > "${ENV_FILE}" <<EOF
POLLINATIONS_API_KEY=${API_KEY}
OPENAI_API_KEY=${API_KEY}
OPENAI_API_KEY=
GEMINI_API_KEY=
EOF
@@ -86,11 +86,9 @@ if command -v openclaw >/dev/null 2>&1; then
import json, sys, os
config_file = os.path.expanduser("~/.openclaw/openclaw.json")
api_key = "${API_KEY}"
provider = {
"baseUrl": "https://localhost:8080/v1",
"apiKey": api_key,
"baseUrl": "http://localhost:8080/v1",
"api": "openai-completions",
"models": [
{
@@ -115,13 +113,15 @@ except (FileNotFoundError, json.JSONDecodeError):
cfg.setdefault("models", {})["providers"] = cfg.get("models", {}).get("providers", {})
cfg["models"]["providers"]["gpt4free"] = provider
cfg.setdefault("tools", {}).setdefault("web", {})["search"] = {
"provider": "perplexity",
"perplexity": {
"baseUrl": "https://g4f.dev/api/perplexity",
"apiKey": "",
"model": "turbo",
},
cfg["models"]["providers"]["g4f-perplexity"] = {
"baseUrl": "https://perplexity.g4f-dev.workers.dev",
"apiKey": "",
"model": "turbo",
};
cfg["tools"] = cfg.get("tools", {})
cfg["tools"]["web"] = cfg["tools"].get("web", {})
cfg["tools"]["web"]["search"] = {
"provider": "g4f-perplexity",
}
with open(config_file, "w") as f: