v10.13.8: FIX D force_finalize skip Gemini, FIX A status=failed, FIX B stream timeout, FIX C lock scope, threshold 8/40
This commit is contained in:
@@ -28,13 +28,15 @@ model_catalog_json = ""
|
||||
|
||||
CHANGELOG = [
|
||||
("10.13.8", "2026-05-27", [
|
||||
"Fix: force_finalize skips Gemini call entirely (was hallucinating tool calls without tools)",
|
||||
"Fix: _send_ag_finalize returns status=failed (was stored as valid history causing loops)",
|
||||
"Fix: _forward_gemini_sse wrapped in try/except for TimeoutError/BrokenPipe",
|
||||
"Fix: file tracker mutations inside lock scope (was racing in ThreadingHTTPServer)",
|
||||
"Fix: compaction summary strips raw tool outputs (was re-triggering read loops)",
|
||||
"Fix: budget cap now strips tools from request (model literally cannot call tools)",
|
||||
"Fix: detect get_goal/completion_budget null-tool loops (3+ consecutive → force finalize)",
|
||||
"Fix: post-compaction write directive when 10+ reads with 0 writes",
|
||||
"Fix: strip timestamps from loop hash (<current_date> broke cross-session tracker)",
|
||||
"Fix: strip base64 image data from tool outputs in normalizer",
|
||||
"Fix: thread-safe file tracker, response logging for finalize/budget paths",
|
||||
"Fix: detect get_goal/completion_budget null-tool loops (3+ → force finalize)",
|
||||
"Fix: read-loop threshold raised to 8 same-file / 40 total (was too aggressive at 5/30)",
|
||||
"Fix: strip timestamps from loop hash, base64 image data from normalizer",
|
||||
]),
|
||||
("3.12.1", "2026-05-27", [
|
||||
"Fix Antigravity adapter (PR #15): simplified model resolution",
|
||||
|
||||
@@ -84,13 +84,15 @@ model_catalog_json = ""
|
||||
|
||||
CHANGELOG = [
|
||||
("10.13.8", "2026-05-27", [
|
||||
"Fix: force_finalize skips Gemini call entirely (was hallucinating tool calls without tools)",
|
||||
"Fix: _send_ag_finalize returns status=failed (was stored as valid history causing loops)",
|
||||
"Fix: _forward_gemini_sse wrapped in try/except for TimeoutError/BrokenPipe",
|
||||
"Fix: file tracker mutations inside lock scope (was racing in ThreadingHTTPServer)",
|
||||
"Fix: compaction summary strips raw tool outputs (was re-triggering read loops)",
|
||||
"Fix: budget cap now strips tools from request (model literally cannot call tools)",
|
||||
"Fix: detect get_goal/completion_budget null-tool loops (3+ consecutive → force finalize)",
|
||||
"Fix: post-compaction write directive when 10+ reads with 0 writes",
|
||||
"Fix: strip timestamps from loop hash (<current_date> broke cross-session tracker)",
|
||||
"Fix: strip base64 image data from tool outputs in normalizer",
|
||||
"Fix: thread-safe file tracker, response logging for finalize/budget paths",
|
||||
"Fix: detect get_goal/completion_budget null-tool loops (3+ → force finalize)",
|
||||
"Fix: read-loop threshold raised to 8 same-file / 40 total (was too aggressive at 5/30)",
|
||||
"Fix: strip timestamps from loop hash, base64 image data from normalizer",
|
||||
]),
|
||||
("3.12.1", "2026-05-27", [
|
||||
"Fix Antigravity adapter (PR #15): simplify model resolution",
|
||||
|
||||
@@ -198,9 +198,10 @@ if [ "$RUN_TASK" = "1" ]; then
|
||||
CLI_VERSION=$(codex --version 2>/dev/null || echo "unknown")
|
||||
log_info "Codex CLI: $CLI_VERSION"
|
||||
|
||||
TASK_PROMPT='Redesign the <div class="vdb-universe" id="vectordb"> section in site/index.html. Create a bold, innovative Steve Jobs-style design: boxy approach with contrasting boxes (one side white, one black), custom art seamless background that blends the two halves, think out of the box. Use pure CSS + HTML only, no external images. Make it visually stunning with geometric precision. The section is inside the existing page so keep the outer wrapper class vdb-universe with id=vectordb. Do NOT touch anything outside that section.'
|
||||
TASK_PROMPT='Create a file /tmp/e2e-test-output.txt with the text "Hello from Codex CLI E2E test" followed by the current date. Then read it back and confirm the content is correct. This is a simple smoke test.'
|
||||
|
||||
TASK_WORKSPACE="/home/roman/Codex-Launcher-Any-AI-Provider"
|
||||
TASK_WORKSPACE="/tmp/e2e-test-workspace"
|
||||
mkdir -p "$TASK_WORKSPACE"
|
||||
|
||||
mkdir -p /tmp/antigravity-task-logs
|
||||
TASK_PROXY_LOG="/tmp/antigravity-task-logs/proxy-$(date +%s).log"
|
||||
@@ -218,26 +219,16 @@ if [ "$RUN_TASK" = "1" ]; then
|
||||
# Generate model catalog
|
||||
CATALOG_PATH="$HOME/.cache/codex-proxy/models-Antigravity-Test.json"
|
||||
python3 -c "
|
||||
import json
|
||||
import json, os
|
||||
models = ['gemini-3.5-flash-high', 'gemini-3.5-flash-medium', 'gemini-3.5-flash-low',
|
||||
'gemini-3.1-pro-high', 'gemini-3.1-pro-low',
|
||||
'claude-sonnet-4-6', 'claude-opus-4-6-thinking', 'gpt-oss-120b-medium']
|
||||
catalog = []
|
||||
for m in models:
|
||||
catalog.append({
|
||||
'slug': m, 'model': m, 'display_name': m,
|
||||
'description': f'Antigravity {m}', 'hidden': False,
|
||||
'isDefault': m == 'gemini-3.5-flash-high',
|
||||
'shell_type': 'shell_command', 'visibility': 'list',
|
||||
'default_reasoning_level': 'medium',
|
||||
'supported_reasoning_levels': [
|
||||
{'effort': 'low', 'description': 'Fast'},
|
||||
{'effort': 'medium', 'description': 'Balanced'},
|
||||
{'effort': 'high', 'description': 'Deep'},
|
||||
],
|
||||
})
|
||||
json.dump(catalog, open('$CATALOG_PATH', 'w'), indent=2)
|
||||
"
|
||||
catalog.append({'slug':m,'model':m,'display_name':m,'description':'Antigravity '+m,'hidden':False,'isDefault':m=='gemini-3.5-flash-high','shell_type':'shell_command','visibility':'list','default_reasoning_level':'medium','supported_reasoning_levels':[{'effort':'low','description':'Fast'},{'effort':'medium','description':'Balanced'},{'effort':'high','description':'Deep'}]})
|
||||
os.makedirs(os.path.dirname('$CATALOG_PATH'), exist_ok=True)
|
||||
json.dump(catalog, open('$CATALOG_PATH','w'), indent=2)
|
||||
" || log_fail "Failed to create model catalog"
|
||||
|
||||
# Write main config
|
||||
cat > "$CONFIG_FILE" <<CONFEOF
|
||||
@@ -351,16 +342,15 @@ PROFEOF
|
||||
|
||||
# ── Launch Codex CLI with the task ──
|
||||
log_info "Launching Codex CLI with real task..."
|
||||
log_info "Task: Redesign vectordb section (boxy black/white approach)"
|
||||
log_info "Task: Create and verify a simple test file"
|
||||
log_info "Monitor log: $TASK_MONITOR_LOG"
|
||||
|
||||
cd "$TASK_WORKSPACE"
|
||||
|
||||
# Run codex non-interactively with --quiet flag
|
||||
set +e
|
||||
codex --profile Antigravity-Test -c "model=gemini-3.5-flash-high" \
|
||||
-s danger-full-access -a never \
|
||||
-q "$TASK_PROMPT" \
|
||||
codex exec --profile Antigravity-Test -c "model=gemini-3.5-flash-high" \
|
||||
-c 'sandbox_permissions=["disk-full-read-access","disk-full-write-access"]' \
|
||||
"$TASK_PROMPT" \
|
||||
> "$TASK_CLI_LOG" 2>&1
|
||||
CLI_EXIT=$?
|
||||
set -e
|
||||
@@ -429,21 +419,41 @@ PROFEOF
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check if the file was actually modified
|
||||
# Check if the file was actually created
|
||||
echo ""; echo "─── Test 4d: Task Output Quality ───"
|
||||
if [ -f "$TASK_WORKSPACE/site/index.html" ]; then
|
||||
VDB_LINES=$(grep -c "vectordb\|vdb-" "$TASK_WORKSPACE/site/index.html" || echo 0)
|
||||
log_info "vectordb section has $VDB_LINES vdb-related lines"
|
||||
|
||||
# Check for common issues in the output
|
||||
MALFORMED=$(grep -c "<\|>\|&" "$TASK_WORKSPACE/site/index.html" || echo 0)
|
||||
[ "$MALFORMED" -gt 100 ] && log_fail "Possible HTML encoding issue: $MALFORMED escaped entities"
|
||||
|
||||
# Check section is still intact
|
||||
if grep -q 'id="vectordb"' "$TASK_WORKSPACE/site/index.html"; then
|
||||
log_pass "vectordb section preserved"
|
||||
if [ -f "/tmp/e2e-test-output.txt" ]; then
|
||||
CONTENT=$(cat /tmp/e2e-test-output.txt 2>/dev/null)
|
||||
if echo "$CONTENT" | grep -q "Hello from Codex CLI E2E test"; then
|
||||
log_pass "Task output file created with correct content"
|
||||
else
|
||||
log_fail "vectordb section missing or corrupted"
|
||||
log_fail "Task output file exists but content is wrong: $CONTENT"
|
||||
fi
|
||||
else
|
||||
log_fail "Task output file /tmp/e2e-test-output.txt was NOT created"
|
||||
fi
|
||||
|
||||
# Check proxy log for tool-strip events (budget cap defense)
|
||||
echo ""; echo "─── Test 4e: Anti-Loop Defense Verification ───"
|
||||
if [ -f "/tmp/antigravity-test-proxy.log" ]; then
|
||||
NULL_TOOL_LOOPS=$(grep -c "NULL-TOOL LOOP" /tmp/antigravity-test-proxy.log || echo 0)
|
||||
TOOL_STRIPPED=$(grep -c "TOOLS STRIPPED" /tmp/antigravity-test-proxy.log || echo 0)
|
||||
BUDGET_HIT=$(grep -c "HARD CAP" /tmp/antigravity-test-proxy.log || echo 0)
|
||||
READ_LOOP=$(grep -c "FILE READ LOOP" /tmp/antigravity-test-proxy.log || echo 0)
|
||||
FORCE_FINALIZE=$(grep -c "force_finalize" /tmp/antigravity-test-proxy.log || echo 0)
|
||||
|
||||
log_info "Anti-loop events: null-tool=$NULL_TOOL_LOOPS stripped=$TOOL_STRIPPED budget=$BUDGET_HIT read-loop=$READ_LOOP finalize=$FORCE_FINALIZE"
|
||||
|
||||
# For a simple task, none of these should fire
|
||||
if [ "$BUDGET_HIT" -gt 0 ]; then
|
||||
log_fail "Budget cap hit on simple task — model looping"
|
||||
else
|
||||
log_pass "No budget cap triggered (task completed cleanly)"
|
||||
fi
|
||||
|
||||
if [ "$TOOL_STRIPPED" -gt 0 ]; then
|
||||
log_fail "Tools were stripped — model hit hard limit"
|
||||
else
|
||||
log_pass "No tool stripping needed (model behaved)"
|
||||
fi
|
||||
fi
|
||||
|
||||
|
||||
@@ -5901,21 +5901,21 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
||||
if ag_key not in _ANTIGRAVITY_FILE_TRACKER:
|
||||
_ANTIGRAVITY_FILE_TRACKER[ag_key] = {"last_path": None, "path_counts": {}, "total_reads": 0}
|
||||
ft = _ANTIGRAVITY_FILE_TRACKER[ag_key]
|
||||
for item in reversed(input_data):
|
||||
if isinstance(item, dict) and item.get("type") == "function_call":
|
||||
args_str = json.dumps(item.get("arguments", {}))
|
||||
file_match = re.search(r'(/[\w/.-]+\.(?:html|py|js|ts|css|json|md|yaml|yml|xml|txt|sh))', args_str)
|
||||
if file_match:
|
||||
detected_path = file_match.group(1)
|
||||
ft["total_reads"] += 1
|
||||
ft["path_counts"][detected_path] = ft["path_counts"].get(detected_path, 0) + 1
|
||||
ft["last_path"] = detected_path
|
||||
if ft["path_counts"][detected_path] >= 5 or ft["total_reads"] > 30:
|
||||
ag_state["force_finalize"] = True
|
||||
print(f"[antigravity-loop] FILE READ LOOP: {detected_path} read "
|
||||
f"{ft['path_counts'][detected_path]}x, total={ft['total_reads']}",
|
||||
file=sys.stderr)
|
||||
break
|
||||
for item in reversed(input_data):
|
||||
if isinstance(item, dict) and item.get("type") == "function_call":
|
||||
args_str = json.dumps(item.get("arguments", {}))
|
||||
file_match = re.search(r'(/[\w/.-]+\.(?:html|py|js|ts|css|json|md|yaml|yml|xml|txt|sh))', args_str)
|
||||
if file_match:
|
||||
detected_path = file_match.group(1)
|
||||
ft["total_reads"] += 1
|
||||
ft["path_counts"][detected_path] = ft["path_counts"].get(detected_path, 0) + 1
|
||||
ft["last_path"] = detected_path
|
||||
if ft["path_counts"][detected_path] >= 8 or ft["total_reads"] > 40:
|
||||
ag_state["force_finalize"] = True
|
||||
print(f"[antigravity-loop] FILE READ LOOP: {detected_path} read "
|
||||
f"{ft['path_counts'][detected_path]}x, total={ft['total_reads']}",
|
||||
file=sys.stderr)
|
||||
break
|
||||
|
||||
null_tool_names = {"get_goal", "get_remaining_tokens", "get_completion_budget", "status"}
|
||||
consecutive_null = 0
|
||||
@@ -5947,7 +5947,10 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
||||
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."}]})
|
||||
return self._send_ag_finalize(
|
||||
"Loop detected. The proxy is forcing a stop because the model repeatedly "
|
||||
"called tools without making progress. Try a more specific or smaller request.",
|
||||
stream=body.get("stream", False))
|
||||
|
||||
if not _antigravity_is_simple_user(latest_user):
|
||||
contents.insert(0, {"role": "user", "parts": [{"text": _GEMINI_AGENT_GUARDRAIL}]})
|
||||
@@ -6730,20 +6733,20 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
||||
if ag_key not in _ANTIGRAVITY_FILE_TRACKER:
|
||||
_ANTIGRAVITY_FILE_TRACKER[ag_key] = {"last_path": None, "path_counts": {}, "total_reads": 0}
|
||||
ft = _ANTIGRAVITY_FILE_TRACKER[ag_key]
|
||||
for item in reversed(input_data):
|
||||
if isinstance(item, dict) and item.get("type") == "function_call":
|
||||
args_str = json.dumps(item.get("arguments", {}))
|
||||
file_match = re.search(r'(/[\w/.-]+\.(?:html|py|js|ts|css|json|md|yaml|yml|xml|txt|sh))', args_str)
|
||||
if file_match:
|
||||
dp = file_match.group(1)
|
||||
ft["total_reads"] += 1
|
||||
ft["path_counts"][dp] = ft["path_counts"].get(dp, 0) + 1
|
||||
ft["last_path"] = dp
|
||||
if ft["path_counts"][dp] >= 5 or ft["total_reads"] > 30:
|
||||
ag_state["force_finalize"] = True
|
||||
print(f"[antigravity-loop] FILE READ LOOP: {dp} read "
|
||||
f"{ft['path_counts'][dp]}x, total={ft['total_reads']}", file=sys.stderr)
|
||||
break
|
||||
for item in reversed(input_data):
|
||||
if isinstance(item, dict) and item.get("type") == "function_call":
|
||||
args_str = json.dumps(item.get("arguments", {}))
|
||||
file_match = re.search(r'(/[\w/.-]+\.(?:html|py|js|ts|css|json|md|yaml|yml|xml|txt|sh))', args_str)
|
||||
if file_match:
|
||||
dp = file_match.group(1)
|
||||
ft["total_reads"] += 1
|
||||
ft["path_counts"][dp] = ft["path_counts"].get(dp, 0) + 1
|
||||
ft["last_path"] = dp
|
||||
if ft["path_counts"][dp] >= 8 or ft["total_reads"] > 40:
|
||||
ag_state["force_finalize"] = True
|
||||
print(f"[antigravity-loop] FILE READ LOOP: {dp} read "
|
||||
f"{ft['path_counts'][dp]}x, total={ft['total_reads']}", file=sys.stderr)
|
||||
break
|
||||
|
||||
null_tool_names = {"get_goal", "get_remaining_tokens", "get_completion_budget", "status"}
|
||||
consecutive_null = 0
|
||||
@@ -6785,8 +6788,11 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
||||
break
|
||||
|
||||
if ag_state["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. DO NOT PRODUCE ANY MORE PLANNING TEXT. DO NOT PRODUCE ANY MORE EXPLORATORY TOOL CALLS. PRODUCE A FINAL ANSWER OR A CLEAR STATEMENT OF WHAT IS PREVENTING YOU FROM COMPLETING THE TASK."}]})
|
||||
elif latest_lower and any(w in latest_lower for w in _EDIT_WORDS) and not ag_state["nudge_injected"] and not ag_state["force_finalize"]:
|
||||
return self._send_ag_finalize(
|
||||
"Loop detected. The proxy is forcing a stop because the model repeatedly "
|
||||
"called tools without making progress. Try a more specific or smaller request.",
|
||||
stream=body.get("stream", False) if isinstance(body, dict) else False)
|
||||
elif latest_lower and any(w in latest_lower for w in _EDIT_WORDS) and not ag_state["nudge_injected"]:
|
||||
contents.append({"role": "user", "parts": [{"text": "!!! ABSOLUTELY NO PLANNING - EMIT THE TOOL CALL NOW !!! IMPORTANT: The user is requesting a modification to existing files. You MUST use tools (exec_command, read_files, write, etc.) to make the changes RIGHT NOW. Do NOT just describe what to do — actually CALL THE TOOLS IN THIS RESPONSE. IMMEDIATELY INSPECT THE FILE OR LIST FILES USING exec_command TOOL CALL."}]})
|
||||
ag_state["nudge_injected"] = True
|
||||
print(f"[antigravity] edit-intent detected; injected tool-use nudge (first time for this request)", file=sys.stderr)
|
||||
@@ -7014,82 +7020,100 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
||||
|
||||
buf = ""
|
||||
stream_finished = False
|
||||
for raw_line in _stream_with_idle_timeout(upstream, _idle_timeout_for_model(model)):
|
||||
if tracker and tracker.cancelled.is_set():
|
||||
print("[gemini-oauth] stream cancelled", file=sys.stderr)
|
||||
break
|
||||
if stream_finished:
|
||||
break
|
||||
line = raw_line.decode(errors="replace")
|
||||
if line.startswith("data: "):
|
||||
buf += line[6:]
|
||||
continue
|
||||
if not line.strip() and buf:
|
||||
try:
|
||||
chunk = json.loads(buf)
|
||||
except Exception:
|
||||
buf = ""
|
||||
continue
|
||||
buf = ""
|
||||
|
||||
candidates = chunk.get("response", chunk).get("candidates", [])
|
||||
if not candidates:
|
||||
if chunk.get("error"):
|
||||
print(f"[{self._session_id}] stream error chunk: {str(chunk.get('error'))[:300]}", file=sys.stderr)
|
||||
continue
|
||||
if candidates[0].get("finishReason") and not candidates[0].get("content", {}).get("parts"):
|
||||
print(f"[{self._session_id}] finish without parts: {candidates[0].get('finishReason')}", file=sys.stderr)
|
||||
parts = candidates[0].get("content", {}).get("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")
|
||||
fc_name = part["functionCall"].get("name")
|
||||
if fc_id:
|
||||
_gemini_store_sig(f"fc:{fc_id}", sig)
|
||||
if fc_name:
|
||||
_gemini_store_sig(f"fc:{fc_name}", sig)
|
||||
_gemini_store_sig(f"turn:{resp_id}", sig)
|
||||
if part.get("thought"):
|
||||
sig_from_thought = _extract_gemini_sig(part)
|
||||
if sig_from_thought:
|
||||
_gemini_store_sig(f"turn:{resp_id}", sig_from_thought)
|
||||
continue
|
||||
if "text" in part and not part.get("functionCall"):
|
||||
text_delta = part["text"]
|
||||
if not text_delta:
|
||||
continue
|
||||
full_text += text_delta
|
||||
if not message_started:
|
||||
flush_event("response.output_item.added", {"type": "response.output_item.added", "output_index": 0, "item": {"type": "message", "id": message_id, "role": "assistant", "content": []}})
|
||||
flush_event("response.content_part.added", {"type": "response.content_part.added", "output_index": 0, "content_index": 0, "part": {"type": "output_text", "text": ""}})
|
||||
output_items.append({"text": True})
|
||||
message_started = True
|
||||
flush_event("response.output_text.delta", {"type": "response.output_text.delta", "output_index": 0, "content_index": 0, "delta": text_delta})
|
||||
elif part.get("functionCall"):
|
||||
fc = part["functionCall"]
|
||||
call_id = f"call_{uuid.uuid4().hex[:24]}"
|
||||
args_str = json.dumps(fc.get("args", fc.get("arguments", {})))
|
||||
output_index = len(output_items)
|
||||
flush_event("response.output_item.added", {"type": "response.output_item.added", "output_index": output_index, "item": {"type": "function_call", "id": call_id, "call_id": call_id, "name": fc.get("name", ""), "arguments": ""}})
|
||||
flush_event("response.function_call_arguments.delta", {"type": "response.function_call_arguments.delta", "output_index": output_index, "item_id": call_id, "delta": args_str})
|
||||
flush_event("response.function_call_arguments.done", {"type": "response.function_call_arguments.done", "output_index": output_index, "item_id": call_id, "arguments": args_str})
|
||||
current_tool_calls[call_id] = fc
|
||||
output_items.append({"tool": True})
|
||||
last_finish = candidates[0].get("finishReason", "")
|
||||
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:
|
||||
print(f"[{self._session_id}] MAX_TOKENS hit ({len(full_text)} chars), auto-continuing...", file=sys.stderr)
|
||||
break
|
||||
stream_finished = True
|
||||
last_finish = ""
|
||||
try:
|
||||
for raw_line in _stream_with_idle_timeout(upstream, _idle_timeout_for_model(model)):
|
||||
if tracker and tracker.cancelled.is_set():
|
||||
print("[gemini-oauth] stream cancelled", file=sys.stderr)
|
||||
break
|
||||
if stream_finished:
|
||||
break
|
||||
line = raw_line.decode(errors="replace")
|
||||
if line.startswith("data: "):
|
||||
buf += line[6:]
|
||||
continue
|
||||
if not line.strip() and buf:
|
||||
try:
|
||||
chunk = json.loads(buf)
|
||||
except Exception:
|
||||
buf = ""
|
||||
continue
|
||||
buf = ""
|
||||
|
||||
candidates = chunk.get("response", chunk).get("candidates", [])
|
||||
if not candidates:
|
||||
if chunk.get("error"):
|
||||
print(f"[{self._session_id}] stream error chunk: {str(chunk.get('error'))[:300]}", file=sys.stderr)
|
||||
continue
|
||||
if candidates[0].get("finishReason") and not candidates[0].get("content", {}).get("parts"):
|
||||
print(f"[{self._session_id}] finish without parts: {candidates[0].get('finishReason')}", file=sys.stderr)
|
||||
parts = candidates[0].get("content", {}).get("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")
|
||||
fc_name = part["functionCall"].get("name")
|
||||
if fc_id:
|
||||
_gemini_store_sig(f"fc:{fc_id}", sig)
|
||||
if fc_name:
|
||||
_gemini_store_sig(f"fc:{fc_name}", sig)
|
||||
_gemini_store_sig(f"turn:{resp_id}", sig)
|
||||
if part.get("thought"):
|
||||
sig_from_thought = _extract_gemini_sig(part)
|
||||
if sig_from_thought:
|
||||
_gemini_store_sig(f"turn:{resp_id}", sig_from_thought)
|
||||
continue
|
||||
if "text" in part and not part.get("functionCall"):
|
||||
text_delta = part["text"]
|
||||
if not text_delta:
|
||||
continue
|
||||
full_text += text_delta
|
||||
if not message_started:
|
||||
flush_event("response.output_item.added", {"type": "response.output_item.added", "output_index": 0, "item": {"type": "message", "id": message_id, "role": "assistant", "content": []}})
|
||||
flush_event("response.content_part.added", {"type": "response.content_part.added", "output_index": 0, "content_index": 0, "part": {"type": "output_text", "text": ""}})
|
||||
output_items.append({"text": True})
|
||||
message_started = True
|
||||
flush_event("response.output_text.delta", {"type": "response.output_text.delta", "output_index": 0, "content_index": 0, "delta": text_delta})
|
||||
elif part.get("functionCall"):
|
||||
fc = part["functionCall"]
|
||||
call_id = f"call_{uuid.uuid4().hex[:24]}"
|
||||
args_str = json.dumps(fc.get("args", fc.get("arguments", {})))
|
||||
output_index = len(output_items)
|
||||
flush_event("response.output_item.added", {"type": "response.output_item.added", "output_index": output_index, "item": {"type": "function_call", "id": call_id, "call_id": call_id, "name": fc.get("name", ""), "arguments": ""}})
|
||||
flush_event("response.function_call_arguments.delta", {"type": "response.function_call_arguments.delta", "output_index": output_index, "item_id": call_id, "delta": args_str})
|
||||
flush_event("response.function_call_arguments.done", {"type": "response.function_call_arguments.done", "output_index": output_index, "item_id": call_id, "arguments": args_str})
|
||||
current_tool_calls[call_id] = fc
|
||||
output_items.append({"tool": True})
|
||||
last_finish = candidates[0].get("finishReason", "")
|
||||
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:
|
||||
print(f"[{self._session_id}] MAX_TOKENS hit ({len(full_text)} chars), auto-continuing...", file=sys.stderr)
|
||||
break
|
||||
stream_finished = True
|
||||
break
|
||||
else:
|
||||
if line.strip():
|
||||
buf += line
|
||||
except TimeoutError as te:
|
||||
print(f"[{self._session_id}] [antigravity-v2] STREAM TIMEOUT: {te}", file=sys.stderr)
|
||||
_log_resp(resp_id, "stream_timeout", [{"type": "error", "code": "stream_timeout", "message": str(te)}])
|
||||
try:
|
||||
flush_event("response.failed", {"type": "response.failed", "response": {"id": resp_id, "object": "response", "status": "failed", "error": {"type": "stream_timeout", "message": str(te)[:200]}}})
|
||||
except Exception:
|
||||
pass
|
||||
self.close_connection = True
|
||||
return
|
||||
except (BrokenPipeError, ConnectionResetError, ConnectionAbortedError):
|
||||
print(f"[{self._session_id}] [antigravity-v2] client disconnected during stream", file=sys.stderr)
|
||||
_log_resp(resp_id, "client_disconnect", [])
|
||||
return
|
||||
|
||||
if OAUTH_PROVIDER.startswith("google") and full_text and not current_tool_calls and last_finish == "MAX_TOKENS" and not stream_finished:
|
||||
result = _auto_continue_gemini(self, flush_event, message_id, model, gen_config, gemini_tools, system_parts, project_id, headers, endpoints, url_suffix, full_text, output_items, message_started)
|
||||
@@ -8430,12 +8454,13 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
||||
|
||||
def _send_ag_finalize(self, text, stream=False, is_responses_api=True):
|
||||
sid = getattr(self, '_session_id', 'fin')
|
||||
print(f"[{sid}] [antigravity-finalize] Sending finalize response: {text[:80]}...", file=sys.stderr)
|
||||
_log_resp(f"finalize-{sid}", "finalized", [{"type": "message", "content": [{"text": text}]}])
|
||||
print(f"[{sid}] [antigravity-finalize] Sending finalize-as-failed: {text[:80]}...", file=sys.stderr)
|
||||
_log_resp(f"finalize-{sid}", "failed", [{"type": "error", "code": "rate_limit_error", "message": text}])
|
||||
resp_id = f"resp_{uuid.uuid4().hex[:12]}"
|
||||
msg_id = f"msg_{uuid.uuid4().hex[:12]}"
|
||||
output_obj = [{"type": "message", "id": msg_id, "role": "assistant",
|
||||
"content": [{"type": "output_text", "text": text}]}]
|
||||
error_output = [{"type": "error", "code": "rate_limit_error", "message": text}]
|
||||
text_output = [{"type": "message", "id": msg_id, "role": "assistant",
|
||||
"content": [{"type": "output_text", "text": text}]}]
|
||||
if stream:
|
||||
events = [
|
||||
f"event: response.created\ndata: {json.dumps({'type':'response.created','response':{'id':resp_id,'object':'response','status':'in_progress'}})}\n\n",
|
||||
@@ -8445,7 +8470,7 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
||||
f"event: response.output_text.done\ndata: {json.dumps({'type':'response.output_text.done','output_index':0,'content_index':0,'text':text})}\n\n",
|
||||
f"event: response.content_part.done\ndata: {json.dumps({'type':'response.content_part.done','output_index':0,'content_index':0,'part':{'type':'output_text','text':text}})}\n\n",
|
||||
f"event: response.output_item.done\ndata: {json.dumps({'type':'response.output_item.done','output_index':0,'item':{'type':'message','id':msg_id,'role':'assistant','content':[{'type':'output_text','text':text}]}})}\n\n",
|
||||
f"event: response.completed\ndata: {json.dumps({'type':'response.completed','response':{'id':resp_id,'object':'response','status':'completed','output':output_obj}})}\n\n",
|
||||
f"event: response.failed\ndata: {json.dumps({'type':'response.failed','response':{'id':resp_id,'object':'response','status':'failed','output':error_output}})}\n\n",
|
||||
]
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "text/event-stream")
|
||||
@@ -8456,8 +8481,9 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
||||
self.wfile.write(evt.encode())
|
||||
self.wfile.flush()
|
||||
else:
|
||||
self.send_json(200, {"id": resp_id, "object": "response", "status": "completed",
|
||||
"output": output_obj, "model": "gemini-3-flash"})
|
||||
self.send_json(200, {"id": resp_id, "object": "response", "status": "failed",
|
||||
"output": error_output + text_output, "model": "gemini-3-flash",
|
||||
"error": {"type": "rate_limit_error", "message": text}})
|
||||
return None
|
||||
|
||||
def stream_buffered_events(self, event_iter, flush_interval=0.03, max_bytes=4096, on_event=None):
|
||||
|
||||
Reference in New Issue
Block a user