v3.9.3: Gemini state-continuity fix — thought signatures, guardrail, model mapping
This commit is contained in:
@@ -603,6 +603,63 @@ def _refresh_google_token(token_data, token_path):
|
|||||||
print(f"[oauth] refresh failed: {e}", file=sys.stderr)
|
print(f"[oauth] refresh failed: {e}", file=sys.stderr)
|
||||||
return token_data.get("access_token", "")
|
return token_data.get("access_token", "")
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════
|
||||||
|
# Gemini 3 thought signature preservation
|
||||||
|
# ═══════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
_gemini_sig_store = {}
|
||||||
|
_gemini_sig_lock = threading.Lock()
|
||||||
|
|
||||||
|
def _gemini_store_sig(key, signature):
|
||||||
|
if not key or not signature:
|
||||||
|
return
|
||||||
|
with _gemini_sig_lock:
|
||||||
|
_gemini_sig_store[key] = {"sig": signature, "ts": time.time()}
|
||||||
|
|
||||||
|
def _gemini_get_sig(key):
|
||||||
|
with _gemini_sig_lock:
|
||||||
|
item = _gemini_sig_store.get(key)
|
||||||
|
return item["sig"] if item else None
|
||||||
|
|
||||||
|
def _extract_gemini_sig(part):
|
||||||
|
if not isinstance(part, dict):
|
||||||
|
return None
|
||||||
|
return part.get("thoughtSignature") or part.get("thought_signature") or part.get("signature")
|
||||||
|
|
||||||
|
def _gemini_reattach_sigs(contents):
|
||||||
|
for content in contents:
|
||||||
|
for part in content.get("parts", []):
|
||||||
|
if not isinstance(part, dict):
|
||||||
|
continue
|
||||||
|
if "thoughtSignature" in part:
|
||||||
|
continue
|
||||||
|
if "functionCall" in part:
|
||||||
|
fc = part["functionCall"]
|
||||||
|
cid = fc.get("id") or fc.get("name")
|
||||||
|
if cid:
|
||||||
|
sig = _gemini_get_sig(f"fc:{cid}")
|
||||||
|
if sig:
|
||||||
|
part["thoughtSignature"] = sig
|
||||||
|
if "text" in part and content.get("role") == "model":
|
||||||
|
turn_key = content.get("_proxy_turn_key")
|
||||||
|
if turn_key:
|
||||||
|
sig = _gemini_get_sig(f"turn:{turn_key}")
|
||||||
|
if sig:
|
||||||
|
part["thoughtSignature"] = sig
|
||||||
|
return contents
|
||||||
|
|
||||||
|
# Gemini follow-through guardrail
|
||||||
|
_GEMINI_AGENT_GUARDRAIL = (
|
||||||
|
"You are running inside Codex as an autonomous coding agent. "
|
||||||
|
"When the user asks for a change to existing files, do not merely describe the previous work or summarize. "
|
||||||
|
"You must inspect the existing files, apply edits with tools, and verify the result. "
|
||||||
|
"If a file path is known from prior context, reuse it. "
|
||||||
|
"If unsure, list files first. "
|
||||||
|
"After tool results, continue until the requested change is actually implemented. "
|
||||||
|
"Never answer only with a plan such as 'I will start by...' or 'I am going to...'. "
|
||||||
|
"Always emit the actual tool call in the same response."
|
||||||
|
)
|
||||||
|
|
||||||
_LOG_FILE = None
|
_LOG_FILE = None
|
||||||
_LOG_FILE_LOCK = threading.Lock()
|
_LOG_FILE_LOCK = threading.Lock()
|
||||||
|
|
||||||
@@ -4180,44 +4237,47 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
|||||||
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()
|
||||||
|
original_model = model
|
||||||
|
|
||||||
|
_GEMINI_KEEP_RECENT = 6
|
||||||
|
_GEMINI_OLD_LIMIT = 3000
|
||||||
|
_GEMINI_RECENT_LIMIT = 20000
|
||||||
|
|
||||||
if isinstance(input_data, list) and len(input_data) > 8:
|
if isinstance(input_data, list) and len(input_data) > 8:
|
||||||
n_tool_outputs = sum(1 for it in input_data if isinstance(it, dict) and it.get("type") == "function_call_output")
|
n_tool_outputs = sum(1 for it in input_data if isinstance(it, dict) and it.get("type") == "function_call_output")
|
||||||
if n_tool_outputs > 2:
|
if n_tool_outputs > 2:
|
||||||
|
tool_indexes = [i for i, it in enumerate(input_data) if isinstance(it, dict) and it.get("type") == "function_call_output"]
|
||||||
|
recent_set = set(tool_indexes[-_GEMINI_KEEP_RECENT:])
|
||||||
compacted_data = []
|
compacted_data = []
|
||||||
last_fc_idx = None
|
|
||||||
for i in range(len(input_data) - 1, -1, -1):
|
|
||||||
if isinstance(input_data[i], dict) and input_data[i].get("type") == "function_call":
|
|
||||||
last_fc_idx = i
|
|
||||||
break
|
|
||||||
keep_from = last_fc_idx if last_fc_idx is not None else len(input_data)
|
|
||||||
for i, item in enumerate(input_data):
|
for i, item in enumerate(input_data):
|
||||||
if isinstance(item, dict) and item.get("type") == "function_call_output":
|
if isinstance(item, dict) and item.get("type") == "function_call_output":
|
||||||
o = item.get("output", "")
|
o = item.get("output", "")
|
||||||
if i < keep_from and len(o) > 1500:
|
limit = _GEMINI_RECENT_LIMIT if i in recent_set else _GEMINI_OLD_LIMIT
|
||||||
|
if len(o) > limit:
|
||||||
item = dict(item)
|
item = dict(item)
|
||||||
summary = o[:600] + f"\n... [compacted {len(o) - 600} chars - earlier tool result]"
|
item["output"] = o[:limit] + f"\n... [proxy compacted: kept {limit} of {len(o)} chars]"
|
||||||
item["output"] = summary
|
|
||||||
compacted_data.append(item)
|
compacted_data.append(item)
|
||||||
input_data = compacted_data
|
input_data = compacted_data
|
||||||
body = dict(body)
|
body = dict(body)
|
||||||
body["input"] = input_data
|
body["input"] = input_data
|
||||||
print(f"[gemini-compact] {n_tool_outputs} tool outputs, compacted earlier ones to 600 chars", file=sys.stderr)
|
print(f"[gemini-compact] {n_tool_outputs} tool outputs, recent={_GEMINI_RECENT_LIMIT} old={_GEMINI_OLD_LIMIT}", file=sys.stderr)
|
||||||
|
|
||||||
if OAUTH_PROVIDER == "google-antigravity":
|
if OAUTH_PROVIDER == "google-antigravity":
|
||||||
alias_map = {
|
alias_map = {
|
||||||
"antigravity-gemini-3-flash": "gemini-3-flash",
|
"antigravity-gemini-3-flash": "gemini-3-flash",
|
||||||
"antigravity-gemini-3-pro": "gemini-3-pro-low",
|
"antigravity-gemini-3-pro": "gemini-3-pro",
|
||||||
"antigravity-gemini-3.1-pro": "gemini-3.1-pro-low",
|
"antigravity-gemini-3.1-pro": "gemini-3.1-pro",
|
||||||
"gemini-3-flash-preview": "gemini-3-flash",
|
"gemini-3-flash-preview": "gemini-3-flash-preview",
|
||||||
"gemini-3-pro-preview": "gemini-3-pro-low",
|
"gemini-3-pro-preview": "gemini-3-pro-preview",
|
||||||
"gemini-3.1-pro-preview": "gemini-3.1-pro-low",
|
"gemini-3.1-pro-preview": "gemini-3.1-pro-preview",
|
||||||
"gemini-3-pro": "gemini-3-pro-low",
|
"gemini-3-pro": "gemini-3-pro",
|
||||||
"gemini-3.1-pro": "gemini-3.1-pro-low",
|
"gemini-3.1-pro": "gemini-3.1-pro",
|
||||||
"antigravity-claude-sonnet-4-6": "claude-sonnet-4-6",
|
"antigravity-claude-sonnet-4-6": "claude-sonnet-4-6",
|
||||||
"antigravity-claude-opus-4-6-thinking": "claude-opus-4-6-thinking",
|
"antigravity-claude-opus-4-6-thinking": "claude-opus-4-6-thinking",
|
||||||
}
|
}
|
||||||
model = alias_map.get(model, model)
|
model = alias_map.get(model, model)
|
||||||
|
if model != original_model:
|
||||||
|
print(f"[antigravity] model mapped user={original_model} upstream={model}", file=sys.stderr)
|
||||||
|
|
||||||
pair_errors = validate_tool_pairs(input_data)
|
pair_errors = validate_tool_pairs(input_data)
|
||||||
if pair_errors:
|
if pair_errors:
|
||||||
@@ -4285,7 +4345,11 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
|||||||
args = json.loads(args)
|
args = json.loads(args)
|
||||||
except Exception:
|
except Exception:
|
||||||
args = {}
|
args = {}
|
||||||
contents.append({"role": "model", "parts": [{"functionCall": {"name": fname, "args": args, "id": call_id}, "thoughtSignature": "skip_thought_signature_validator"}]})
|
fc_part = {"functionCall": {"name": fname, "args": args, "id": call_id}}
|
||||||
|
stored_sig = _gemini_get_sig(f"fc:{call_id}")
|
||||||
|
if stored_sig:
|
||||||
|
fc_part["thoughtSignature"] = stored_sig
|
||||||
|
contents.append({"role": "model", "parts": [fc_part]})
|
||||||
elif t == "function_call_output":
|
elif t == "function_call_output":
|
||||||
call_id = item.get("call_id", item.get("id", ""))
|
call_id = item.get("call_id", item.get("id", ""))
|
||||||
output = item.get("output", "")
|
output = item.get("output", "")
|
||||||
@@ -4367,6 +4431,30 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
|||||||
if func_decls:
|
if func_decls:
|
||||||
gemini_tools = [{"functionDeclarations": func_decls}]
|
gemini_tools = [{"functionDeclarations": func_decls}]
|
||||||
|
|
||||||
|
if OAUTH_PROVIDER == "google-antigravity":
|
||||||
|
contents = _gemini_reattach_sigs(contents)
|
||||||
|
|
||||||
|
if OAUTH_PROVIDER == "google-antigravity":
|
||||||
|
guardrail_found = any("autonomous coding agent" in json.dumps(c.get("parts", []), ensure_ascii=False) for c in contents[:2])
|
||||||
|
if not guardrail_found:
|
||||||
|
contents.insert(0, {"role": "user", "parts": [{"text": _GEMINI_AGENT_GUARDRAIL}]})
|
||||||
|
|
||||||
|
if OAUTH_PROVIDER == "google-antigravity" and isinstance(input_data, list):
|
||||||
|
latest_user = ""
|
||||||
|
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:
|
||||||
|
serialized = json.dumps(contents, ensure_ascii=False)
|
||||||
|
needle = latest_user.strip()[:120]
|
||||||
|
if needle and needle not in serialized:
|
||||||
|
print(f"[antigravity] WARNING: latest user instruction missing from contents! needle={needle[:60]}...", file=sys.stderr)
|
||||||
|
|
||||||
request_body = {"contents": contents}
|
request_body = {"contents": contents}
|
||||||
if system_parts:
|
if system_parts:
|
||||||
request_body["systemInstruction"] = {"parts": system_parts}
|
request_body["systemInstruction"] = {"parts": system_parts}
|
||||||
@@ -4505,6 +4593,13 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
|||||||
print(f"[{self._session_id}] finish without parts: {candidates[0].get('finishReason')}", file=sys.stderr)
|
print(f"[{self._session_id}] finish without parts: {candidates[0].get('finishReason')}", file=sys.stderr)
|
||||||
parts = candidates[0].get("content", {}).get("parts", [])
|
parts = candidates[0].get("content", {}).get("parts", [])
|
||||||
for part in parts:
|
for part in parts:
|
||||||
|
sig = _extract_gemini_sig(part)
|
||||||
|
if sig:
|
||||||
|
if part.get("functionCall"):
|
||||||
|
fc_id = part["functionCall"].get("id") or part["functionCall"].get("name")
|
||||||
|
if fc_id:
|
||||||
|
_gemini_store_sig(f"fc:{fc_id}", sig)
|
||||||
|
_gemini_store_sig(f"turn:{resp_id}", sig)
|
||||||
if part.get("thought"):
|
if part.get("thought"):
|
||||||
continue
|
continue
|
||||||
if "text" in part and not part.get("functionCall"):
|
if "text" in part and not part.get("functionCall"):
|
||||||
@@ -4530,6 +4625,12 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
|||||||
output_items.append({"tool": True})
|
output_items.append({"tool": True})
|
||||||
last_finish = candidates[0].get("finishReason", "")
|
last_finish = candidates[0].get("finishReason", "")
|
||||||
if last_finish:
|
if last_finish:
|
||||||
|
part_kinds = []
|
||||||
|
for p in parts:
|
||||||
|
if "text" in p: part_kinds.append("text")
|
||||||
|
if "functionCall" in p: part_kinds.append("functionCall")
|
||||||
|
if _extract_gemini_sig(p): part_kinds.append("thoughtSignature")
|
||||||
|
print(f"[{self._session_id}] [antigravity] finish={last_finish} parts={part_kinds} tool_calls={len(current_tool_calls)}", file=sys.stderr)
|
||||||
if OAUTH_PROVIDER == "google-antigravity" and last_finish == "MAX_TOKENS" and full_text and not current_tool_calls:
|
if OAUTH_PROVIDER == "google-antigravity" and last_finish == "MAX_TOKENS" and full_text and not current_tool_calls:
|
||||||
print(f"[{self._session_id}] MAX_TOKENS hit ({len(full_text)} chars), auto-continuing...", file=sys.stderr)
|
print(f"[{self._session_id}] MAX_TOKENS hit ({len(full_text)} chars), auto-continuing...", file=sys.stderr)
|
||||||
break
|
break
|
||||||
|
|||||||
Reference in New Issue
Block a user