v3.11.12: Antigravity v2 handler rewrite (anti-api approach)

This commit is contained in:
Roman | RyzenAdvanced
2026-05-26 22:23:26 +04:00
Unverified
parent ff849e8669
commit 633e9570bb
6 changed files with 354 additions and 7 deletions

View File

@@ -1,5 +1,18 @@
# Changelog # Changelog
## v3.11.12 (2026-05-26)
**New Antigravity v2 Handler (Mimicking anti-api)**
### New Features
- **Complete rewrite of Antigravity handler** based on https://github.com/ink1ing/anti-api approach
- Safety settings (all OFF), stopSequences, sessionId, requestType: agent
- functionResponse uses `response: { result: string }` format matching anti-api
- Endpoint priority: `daily-cloudcode-pa.googleapis.com` first
- Simplified sanitizer: only deduplicates consecutive user text, never touches tool messages
## v3.11.11 (2026-05-26)
## v3.11.11 (2026-05-26) ## v3.11.11 (2026-05-26)
**Antigravity Fix: Stricter function_call/output Pairing + Gemini Sanitizer Rewrite (PR #12)** **Antigravity Fix: Stricter function_call/output Pairing + Gemini Sanitizer Rewrite (PR #12)**

Binary file not shown.

Binary file not shown.

View File

@@ -27,9 +27,9 @@ model_catalog_json = ""
""" """
CHANGELOG = [ CHANGELOG = [
("3.11.11", "2026-05-26", [ ("3.11.12", "2026-05-26", [
"Fix Antigravity: stricter function_call/output pairing (PR #12)", "New Antigravity v2 handler mimicking anti-api",
"Gemini sanitizer rewritten — tool messages always preserved", "Safety settings, stopSequences, simplified sanitizer",
]), ]),
("3.11.10", "2026-05-26", [ ("3.11.10", "2026-05-26", [
"Fix Antigravity: interleave function_call/output pairs (PR #11)", "Fix Antigravity: interleave function_call/output pairs (PR #11)",

View File

@@ -83,10 +83,14 @@ model_catalog_json = ""
""" """
CHANGELOG = [ CHANGELOG = [
("3.11.12", "2026-05-26", [
"New Antigravity v2 handler mimicking anti-api approach",
"Safety settings, stopSequences, sessionId, requestType: agent",
"Simplified sanitizer preserving functionCall/functionResponse",
"Endpoint priority: daily-cloudcode-pa first",
"functionResponse uses response.result (string) format",
]),
("3.11.11", "2026-05-26", [ ("3.11.11", "2026-05-26", [
"Fix Antigravity: stricter function_call/output pairing + Gemini sanitizer rewrite (PR #12)",
"Only pairs where BOTH call and output exist are included — no orphan calls",
"Gemini sanitizer: tool messages always preserved as-is, text merging more conservative",
"Final trimming only removes plain messages, never function_call_output", "Final trimming only removes plain messages, never function_call_output",
]), ]),
("3.11.10", "2026-05-26", [ ("3.11.10", "2026-05-26", [

View File

@@ -5212,7 +5212,10 @@ class Handler(http.server.BaseHTTPRequestHandler):
elif BACKEND in ("codebuff", "freebuff"): elif BACKEND in ("codebuff", "freebuff"):
self._handle_codebuff(body, model, stream, tracker) self._handle_codebuff(body, model, stream, tracker)
elif (BACKEND or "").startswith("gemini-oauth"): elif (BACKEND or "").startswith("gemini-oauth"):
self._handle_gemini_oauth(body, model, stream, tracker) if OAUTH_PROVIDER == "google-antigravity":
self._handle_antigravity_v2(body, model, stream, tracker)
else:
self._handle_gemini_oauth(body, model, stream, tracker)
else: else:
self._handle_openai_compat(body, model, stream, tracker) self._handle_openai_compat(body, model, stream, tracker)
update_snapshot_response(request_id, "completed", time.time() - _req_t0) update_snapshot_response(request_id, "completed", time.time() - _req_t0)
@@ -5401,6 +5404,333 @@ class Handler(http.server.BaseHTTPRequestHandler):
chat_body["reasoning_effort"] = REASONING_EFFORT chat_body["reasoning_effort"] = REASONING_EFFORT
return chat_body return chat_body
def _handle_antigravity_v2(self, body, model, stream, tracker=None):
input_data = body.get("input", "")
_schema = _load_schema(model=model)
if _schema and not _schema.supports_vision:
input_data = _preprocess_vision_input(input_data, _schema)
body = dict(body)
body["input"] = input_data
if isinstance(input_data, list) and len(input_data) > 30:
input_data = _antigravity_normalize_context(input_data, model)
body = dict(body)
body["input"] = input_data
access_token = _refresh_oauth_token()
token_path = os.path.join(os.path.expanduser("~"), ".cache", "codex-proxy", "google-antigravity-oauth-token.json")
project_id = ""
try:
with open(token_path) as f:
project_id = json.load(f).get("project_id", "")
except Exception:
pass
tool_call_names = {}
contents = []
if isinstance(input_data, list):
for item in input_data:
t = item.get("type")
if t == "message":
role = "user" if item.get("role") == "user" else "model"
content = item.get("content", "")
parts = []
if isinstance(content, list):
for c in content:
ct = c.get("type")
if ct in ("input_text", "text"):
parts.append({"text": c.get("text", "")})
elif ct in ("input_image", "image_url"):
iu = c.get("image_url") or c.get("url", {})
url = iu.get("url", iu) if isinstance(iu, dict) else iu
if isinstance(url, str) and url.startswith("data:"):
mime, _, b64 = url.partition(";base64,")
mime = mime.replace("data:", "") or "image/png"
parts.append({"inlineData": {"mimeType": mime, "data": b64}})
else:
parts.append({"text": str(url)})
elif isinstance(content, str):
parts.append({"text": content})
if parts:
contents.append({"role": role, "parts": parts})
elif t == "function_call":
call_id = item.get("call_id") or item.get("id") or f"call_{uuid.uuid4().hex[:24]}"
fname = item.get("name", "")
if call_id and fname:
tool_call_names[call_id] = fname
args = item.get("arguments", "{}")
if isinstance(args, str):
try:
args = json.loads(args)
except Exception:
args = {}
fc_part = {"functionCall": {"name": fname, "args": args, "id": call_id}}
stored_sig = _gemini_get_sig(f"fc:{call_id}") or _gemini_get_sig(f"fc:{fname}")
if stored_sig:
fc_part["thoughtSignature"] = stored_sig
fc_part["thought_signature"] = stored_sig
else:
fc_part["thought_signature"] = "skip_thought_signature_validator"
contents.append({"role": "model", "parts": [fc_part]})
elif t == "function_call_output":
call_id = item.get("call_id", item.get("id", ""))
output = item.get("output", "")
fname = item.get("name", "") or tool_call_names.get(call_id, "")
resp_part = {"functionResponse": {"name": fname or "unknown", "response": {"result": str(output)}}}
if call_id:
resp_part["functionResponse"]["id"] = call_id
contents.append({"role": "user", "parts": [resp_part]})
sanitized = []
last_user_text = None
last_role = None
for content in contents:
role = content.get("role")
parts = [p for p in content.get("parts", []) if isinstance(p, dict)]
if not parts:
continue
has_function_call = any("functionCall" in p for p in parts)
has_function_response = any("functionResponse" in p for p in parts)
text_key = "\n".join([p.get("text", "") for p in parts if "text" in p]).strip()
if has_function_call or has_function_response:
sanitized.append({"role": role, "parts": parts})
last_role = role
continue
if role == "user" and text_key and text_key == last_user_text:
continue
if role == last_role and role in ("user", "model") and sanitized:
last_parts = sanitized[-1].get("parts", [])
last_has_tool = any("functionCall" in p or "functionResponse" in p for p in last_parts)
if not last_has_tool:
sanitized[-1].setdefault("parts", []).extend(parts)
if role == "user" and text_key:
last_user_text = text_key
last_role = role
continue
sanitized.append({"role": role, "parts": parts})
if role == "user" and text_key:
last_user_text = text_key
last_role = role
while sanitized and sanitized[0].get("role") != "user":
sanitized.pop(0)
contents = sanitized
instructions = body.get("instructions", "").strip()
system_parts = []
if instructions:
system_parts.append({"text": instructions})
gen_config = {"maxOutputTokens": body.get("max_output_tokens", 64000), "stopSequences": ["\n\nHuman:", "[DONE]"]}
if body.get("temperature") is not None:
gen_config["temperature"] = body["temperature"]
if body.get("top_p") is not None:
gen_config["topP"] = body["top_p"]
_is_claude_model = "claude" in model.lower()
_is_claude_thinking = _is_claude_model and "thinking" in model.lower()
if REASONING_ENABLED and REASONING_EFFORT != "none":
if _is_claude_thinking:
budget = {"low": 8192, "medium": 16384, "high": 32768}.get(REASONING_EFFORT, 16384)
gen_config["thinkingConfig"] = {"include_thoughts": True, "thinking_budget": budget}
if gen_config.get("maxOutputTokens", 0) <= budget:
gen_config["maxOutputTokens"] = 64000
elif not _is_claude_model:
budget = {"low": 2048, "medium": 8192, "high": 24576}.get(REASONING_EFFORT, 8192)
gen_config["thinkingConfig"] = {"includeThoughts": True, "thinkingBudget": budget}
oa_tools = body.get("tools", [])
gemini_tools = []
if oa_tools:
func_decls = []
for tool in oa_tools:
ttype = tool.get("type", "function")
fname = tool.get("name", "")
if ttype == "function":
fn = tool.get("function", tool)
name = fn.get("name", fname)
desc = fn.get("description", "")
params = fn.get("parameters", fn.get("input_schema", {}))
func_decls.append({"name": name, "description": desc, "parameters": params})
elif fname:
func_decls.append({"name": fname, "description": tool.get("description", ""), "parameters": tool.get("parameters", {"type": "object", "properties": {}})})
if func_decls:
gemini_tools = [{"functionDeclarations": func_decls}]
contents = _gemini_reattach_sigs(contents)
ag_key = _antigravity_loop_key(self._session_id)
with _ANTIGRAVITY_LOOP_TRACKER_LOCK:
if ag_key not in _ANTIGRAVITY_LOOP_TRACKER:
_ANTIGRAVITY_LOOP_TRACKER[ag_key] = {
"latest_user_hash": None, "nudge_injected": False, "latest_user_appended": False,
"tool_calls_for_request": 0, "repeated_tool": False, "force_finalize": False,
"last_tool": None, "last_tool_count": 0,
}
ag_state = _ANTIGRAVITY_LOOP_TRACKER[ag_key]
latest_user = ""
if isinstance(input_data, list):
for item in reversed(input_data):
if item.get("type") == "message" and item.get("role") == "user":
c = item.get("content", "")
if isinstance(c, str):
latest_user = c
elif isinstance(c, list):
latest_user = "\n".join(p.get("text", p.get("input_text", "")) for p in c if isinstance(p, dict))
break
if latest_user:
latest_norm = " ".join(latest_user.strip().split())[:200]
latest_user_hash = hashlib.sha256(latest_norm.encode()).hexdigest()[:16]
if latest_user_hash != ag_state.get("latest_user_hash"):
ag_state["latest_user_hash"] = latest_user_hash
ag_state["nudge_injected"] = False
ag_state["latest_user_appended"] = False
ag_state["tool_calls_for_request"] = 0
ag_state["repeated_tool"] = False
ag_state["force_finalize"] = False
ag_state["last_tool"] = None
ag_state["last_tool_count"] = 0
n_tool_calls = sum(1 for it in input_data if isinstance(it, dict) and it.get("type") == "function_call")
ag_state["tool_calls_for_request"] = n_tool_calls
last_tool_key = None
for item in reversed(input_data):
if isinstance(item, dict) and item.get("type") == "function_call":
fname = item.get("name", "")
args_str = json.dumps(item.get("arguments", {}), sort_keys=True)[:100]
last_tool_key = f"{fname}:{args_str}"
break
if last_tool_key:
if last_tool_key == ag_state.get("last_tool"):
ag_state["last_tool_count"] = ag_state.get("last_tool_count", 0) + 1
if ag_state["last_tool_count"] >= 5:
ag_state["repeated_tool"] = True
ag_state["force_finalize"] = True
else:
ag_state["last_tool"] = last_tool_key
ag_state["last_tool_count"] = 1
if ag_state.get("force_finalize"):
contents.append({"role": "user", "parts": [{"text": "STOP CALLING TOOLS. APPLY THE FINAL EDIT OR SUMMARIZE WHAT BLOCKED YOU. DO NOT CALL ANY MORE TOOLS."}]})
if not _antigravity_is_simple_user(latest_user):
contents.insert(0, {"role": "user", "parts": [{"text": _GEMINI_AGENT_GUARDRAIL}]})
request_body = {"contents": contents, "safetySettings": [
{"category": "HARM_CATEGORY_HARASSMENT", "threshold": "OFF"},
{"category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "OFF"},
{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", "threshold": "OFF"},
{"category": "HARM_CATEGORY_DANGEROUS_CONTENT", "threshold": "OFF"},
{"category": "HARM_CATEGORY_CIVIC_INTEGRITY", "threshold": "OFF"},
]}
if system_parts:
request_body["systemInstruction"] = {"parts": system_parts}
if gen_config:
request_body["generationConfig"] = gen_config
if gemini_tools:
request_body["tools"] = gemini_tools
if _is_claude_model and gemini_tools:
request_body["toolConfig"] = {"functionCallingConfig": {"mode": "VALIDATED"}}
version = _ensure_antigravity_version()
import platform as _plat
_os_name = _plat.system().lower()
_os_arch = _plat.machine().lower().replace("x86_64", "x64").replace("aarch64", "arm64")
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {access_token}",
"User-Agent": f"antigravity/{version} {_os_name}/{_os_arch}",
"X-Client-Name": "antigravity",
"X-Client-Version": _ensure_antigravity_client_version(),
"x-goog-api-client": "gl-node/18.18.2 fire/0.8.6 grpc/1.10.x",
}
wrapped = {
"project": project_id,
"model": model,
"requestType": "agent",
"userAgent": f"antigravity/{version} {_os_name}/{_os_arch}",
"requestId": f"agent-{uuid.uuid4().hex[:12]}",
"request": request_body,
}
wrapped["request"]["sessionId"] = f"{uuid.uuid4().hex}{int(time.time()*1000)}"
_allow_staging = os.environ.get("ALLOW_ANTIGRAVITY_STAGING", "0") == "1"
_antigravity_endpoints = [
"https://daily-cloudcode-pa.googleapis.com",
"https://daily-cloudcode-pa.sandbox.googleapis.com",
"https://cloudcode-pa.googleapis.com",
]
if _allow_staging:
_antigravity_endpoints.append("https://autopush-cloudcode-pa.sandbox.googleapis.com")
body_b = json.dumps(wrapped).encode()
print(f"[{self._session_id}] [antigravity-v2] model={model} stream={stream} contents={len(contents)} tools={bool(gemini_tools)} project={project_id}", file=sys.stderr)
upstream = None
chosen_ep = None
global _antigravity_preferred_endpoint
with _antigravity_endpoint_lock:
_pref = _antigravity_preferred_endpoint
ordered = ([_pref] + [e for e in _antigravity_endpoints if e != _pref]) if _pref and _pref in _antigravity_endpoints else list(_antigravity_endpoints)
for ep in ordered:
action = "streamGenerateContent" if stream else "generateContent"
url_suffix = f"v1internal:{action}?alt=sse" if stream else f"v1internal:{action}"
target = f"{ep}/{url_suffix}"
req = urllib.request.Request(target, data=body_b, headers=headers)
try:
upstream = urllib.request.urlopen(req, timeout=_upstream_timeout(body, stream))
chosen_ep = ep
with _antigravity_endpoint_lock:
_antigravity_preferred_endpoint = ep
break
except urllib.error.HTTPError as e:
err_body = e.read().decode()
err_class = _classify_antigravity_error(e.code, err_body)
print(f"[{self._session_id}] [antigravity-v2] {ep.replace('https://','')} {e.code} class={err_class}", file=sys.stderr)
if e.code == 400:
try:
debug_path = os.path.join(_LOG_DIR, "antigravity-v2-400.json")
with open(debug_path, "w") as dbg:
json.dump({"endpoint": ep, "model": model, "wrapped": wrapped, "error": err_body}, dbg, indent=2)
except Exception:
pass
return self.send_json(e.code, {"error": {"type": "upstream_error", "message": _sanitize_err_body(err_body)}})
if err_class in ("auth_permanent", "service_disabled", "forbidden", "account_banned", "validation_required"):
return self.send_json(e.code, {"error": {"type": "upstream_error", "message": _sanitize_err_body(err_body)}})
if err_class in ("quota_exhausted", "rate_limited"):
pool = _google_antigravity_pool
_, acct = _get_google_account(OAUTH_PROVIDER)
if acct:
reset_s = _parse_rate_limit_reset(err_body)
cooldown = reset_s if reset_s and reset_s > 10 else 60
pool.mark_rate_limited(acct, cooldown)
return self.send_json(e.code, {"error": {"type": "upstream_error", "message": _sanitize_err_body(err_body)}})
if ep == ordered[-1]:
return self.send_json(e.code, {"error": {"type": "upstream_error", "message": _sanitize_err_body(err_body)}})
continue
except Exception as e:
print(f"[{self._session_id}] [antigravity-v2] {ep.replace('https://','')} conn failed: {e}", file=sys.stderr)
if ep == ordered[-1]:
return self.send_json(502, {"error": {"type": "proxy_error", "message": str(e)}})
continue
if upstream is None:
return self.send_json(502, {"error": {"type": "proxy_error", "message": "All endpoints failed"}})
if stream:
self._forward_gemini_sse(upstream, model, body, input_data, tracker)
else:
self._forward_gemini_json(upstream, model, body, input_data)
def _handle_gemini_oauth(self, body, model, stream, tracker=None): def _handle_gemini_oauth(self, body, model, stream, tracker=None):
input_data = body.get("input", "") input_data = body.get("input", "")
policy = provider_policy() policy = provider_policy()