Compare commits
7 Commits
37
CHANGELOG.md
37
CHANGELOG.md
@@ -1,5 +1,42 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## v3.10.12 (2026-05-26)
|
||||||
|
|
||||||
|
**Sticky Endpoint, Claude Fixes, Guardrail Skip, Anti-Stall**
|
||||||
|
|
||||||
|
### New Features
|
||||||
|
- **Sticky endpoint caching**: remembers which endpoint last succeeded, reuses it on every subsequent request (zero overhead)
|
||||||
|
- **Sequential fallback**: if sticky endpoint fails (429/502/503), tries next endpoint in order — no parallel probing, no wasted requests
|
||||||
|
- **Endpoint order**: `cloudcode-pa.googleapis.com` first (matches agy CLI), `daily-cloudcode-pa.googleapis.com` as fallback
|
||||||
|
- **Anti-stall engine**: kills stale proxy processes and clears `__pycache__` on every new session start
|
||||||
|
- **Smart error classification**: distinguishes `quota_exhausted` vs `capacity_exhausted` vs `account_banned` vs `validation_required` vs `service_disabled` vs `auth_permanent`
|
||||||
|
- **Rate limit reset time parsing**: extracts cooldown from error body (`quotaResetDelay`, `Resets in ~1h27m`, etc.) for accurate cooldown
|
||||||
|
- **Missing Antigravity headers**: `X-Client-Name`, `X-Client-Version`, `x-goog-api-client`, platform-aware `User-Agent`
|
||||||
|
- **Session ID**: added `sessionId` to request wrapper for proper session tracking
|
||||||
|
|
||||||
|
### Bug Fixes (TRAE Agent)
|
||||||
|
- **Guardrail skip for simple messages**: when user sends simple messages (e.g. "hi"), skip injecting `_GEMINI_AGENT_GUARDRAIL` — prevents model from aggressively calling tools and looping `ls -la` 50+ times
|
||||||
|
- **Claude tool preservation**: Claude models through Antigravity now keep ALL tool outputs in normalizer (no summarization/truncation) — prevents context loss that broke Claude sessions
|
||||||
|
- **Claude compaction guard**: `_adaptive_compact` skipped for Claude models — Claude handles its own context, no forced compaction
|
||||||
|
- **Claude normalizer guard**: `_antigravity_normalize_context` skipped for Claude models — avoids stripping Claude-specific message structure
|
||||||
|
- **Claude sanitization guard**: Google content sanitization loop skipped for Claude models — prevents mangling Claude's response format
|
||||||
|
- **Normalizer model parameter**: `_antigravity_normalize_context` now receives `model` param to distinguish Claude vs Gemini behavior
|
||||||
|
|
||||||
|
## v3.10.11 (2026-05-26)
|
||||||
|
|
||||||
|
**Hybrid Endpoint Fallback — Redundant Antigravity Endpoints**
|
||||||
|
|
||||||
|
### New Features
|
||||||
|
- Hybrid endpoint fallback: tries `cloudcode-pa.googleapis.com` then `daily-cloudcode-pa.googleapis.com` on 429
|
||||||
|
- `daily-cloudcode-pa.googleapis.com` is the same production endpoint agy-core uses (separate rate limit bucket)
|
||||||
|
- 429 errors now log full response body for debugging
|
||||||
|
- SERVICE_DISABLED (403) still falls through to next endpoint
|
||||||
|
- Rate-limit marking only happens after ALL endpoints fail
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
- Fixed 429 on one endpoint immediately failing — now tries fallback before giving up
|
||||||
|
- Restored SERVICE_DISABLED fallthrough (was accidentally removed)
|
||||||
|
|
||||||
## v3.10.10 (2026-05-25)
|
## v3.10.10 (2026-05-25)
|
||||||
|
|
||||||
**Context Normalizer Fix — Compaction Summary Preservation**
|
**Context Normalizer Fix — Compaction Summary Preservation**
|
||||||
|
|||||||
@@ -554,6 +554,7 @@ The launcher generates model catalog JSON with dual field naming to satisfy both
|
|||||||
|
|
||||||
Codex Launcher includes special handling for Gemini 3 / Antigravity OAuth:
|
Codex Launcher includes special handling for Gemini 3 / Antigravity OAuth:
|
||||||
|
|
||||||
|
- **Sticky endpoint with parallel discovery**: First request probes `cloudcode-pa.googleapis.com` and `daily-cloudcode-pa.googleapis.com` simultaneously — first 200 wins and is cached. All subsequent requests go straight to the cached endpoint. If it fails (429/502/503), cache is cleared and all endpoints are re-probed in parallel. Zero wasted time on rate-limited endpoints.
|
||||||
- **Thought signature preservation**: Captures `thoughtSignature` from Gemini responses
|
- **Thought signature preservation**: Captures `thoughtSignature` from Gemini responses
|
||||||
and reattaches them on follow-up requests to maintain tool-call continuity.
|
and reattaches them on follow-up requests to maintain tool-call continuity.
|
||||||
- **Edit-intent detection**: When follow-up requests contain edit keywords, a tool-use
|
- **Edit-intent detection**: When follow-up requests contain edit keywords, a tool-use
|
||||||
@@ -561,7 +562,7 @@ Codex Launcher includes special handling for Gemini 3 / Antigravity OAuth:
|
|||||||
- **User instruction enforcement**: The latest user message is guaranteed to be the
|
- **User instruction enforcement**: The latest user message is guaranteed to be the
|
||||||
final content turn sent to Gemini, even after compaction.
|
final content turn sent to Gemini, even after compaction.
|
||||||
- **Smart compaction**: Old tool outputs capped at 3000 chars, recent 6 at 20000 chars.
|
- **Smart compaction**: Old tool outputs capped at 3000 chars, recent 6 at 20000 chars.
|
||||||
- **Context compaction**: Aggressive auto-trimming when approaching 60% of model context
|
- **Context compaction**: Aggressive auto-trimming when approaching 80% of model context
|
||||||
limit (1M tokens Gemini, 200K Claude, 128K GPT-OSS). Prevents token limit errors.
|
limit (1M tokens Gemini, 200K Claude, 128K GPT-OSS). Prevents token limit errors.
|
||||||
- **Model ID mapping**: Display names (e.g. `Gemini 3.5 Flash (High)`) mapped to REST API
|
- **Model ID mapping**: Display names (e.g. `Gemini 3.5 Flash (High)`) mapped to REST API
|
||||||
slugs (e.g. `gemini-3-flash`). See `docs/ANTIGRAVITY.md` for details.
|
slugs (e.g. `gemini-3-flash`). See `docs/ANTIGRAVITY.md` for details.
|
||||||
|
|||||||
BIN
codex-launcher_3.10.11_all.deb
Normal file
BIN
codex-launcher_3.10.11_all.deb
Normal file
Binary file not shown.
BIN
codex-launcher_3.10.12_all.deb
Normal file
BIN
codex-launcher_3.10.12_all.deb
Normal file
Binary file not shown.
@@ -3,11 +3,11 @@ set -e
|
|||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
|
||||||
if [ -f "$SCRIPT_DIR/codex-launcher_3.10.10_all.deb" ]; then
|
if [ -f "$SCRIPT_DIR/codex-launcher_3.10.12_all.deb" ]; then
|
||||||
echo "Installing codex-launcher_3.10.10_all.deb ..."
|
echo "Installing codex-launcher_3.10.12_all.deb ..."
|
||||||
sudo dpkg -i "$SCRIPT_DIR/codex-launcher_3.10.10_all.deb"
|
sudo dpkg -i "$SCRIPT_DIR/codex-launcher_3.10.12_all.deb"
|
||||||
echo ""
|
echo ""
|
||||||
echo "Installed v3.10.10 via .deb package."
|
echo "Installed v3.10.12 via .deb package."
|
||||||
echo " translate-proxy.py -> /usr/bin/translate-proxy.py"
|
echo " translate-proxy.py -> /usr/bin/translate-proxy.py"
|
||||||
echo " codex-launcher-gui -> /usr/bin/codex-launcher-gui"
|
echo " codex-launcher-gui -> /usr/bin/codex-launcher-gui"
|
||||||
echo " cleanup-codex-stale -> /usr/bin/cleanup-codex-stale.sh"
|
echo " cleanup-codex-stale -> /usr/bin/cleanup-codex-stale.sh"
|
||||||
|
|||||||
@@ -83,6 +83,24 @@ model_catalog_json = ""
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
CHANGELOG = [
|
CHANGELOG = [
|
||||||
|
("3.10.12", "2026-05-26", [
|
||||||
|
"Sticky endpoint: caches last working endpoint, sequential fallback on failure",
|
||||||
|
"Endpoint order: cloudcode-pa first (matches agy CLI), daily-cloudcode-pa fallback",
|
||||||
|
"Anti-stall engine: kills stale proxy processes + clears pycache on startup",
|
||||||
|
"Smart error classification: quota vs capacity vs banned vs validation vs auth",
|
||||||
|
"Rate limit reset parsing: extracts cooldown from error body for accuracy",
|
||||||
|
"Missing headers: X-Client-Name, X-Client-Version, x-goog-api-client, sessionId",
|
||||||
|
"Guardrail skip: simple messages (hi) skip agent guardrail, no more tool-call loops",
|
||||||
|
"Claude fixes: preserve all tools, skip compaction/normalizer/sanitization for Claude",
|
||||||
|
"Normalizer model param: distinguishes Claude vs Gemini for correct behavior",
|
||||||
|
]),
|
||||||
|
("3.10.11", "2026-05-26", [
|
||||||
|
"Hybrid endpoint fallback: cloudcode-pa then daily-cloudcode-pa on 429",
|
||||||
|
"daily-cloudcode-pa.googleapis.com (same endpoint agy-core uses)",
|
||||||
|
"429 errors log full response body for debugging",
|
||||||
|
"Rate-limit marking only after ALL endpoints fail",
|
||||||
|
"Restored SERVICE_DISABLED (403) fallthrough",
|
||||||
|
]),
|
||||||
("3.10.10", "2026-05-25", [
|
("3.10.10", "2026-05-25", [
|
||||||
"Fix normalizer stripping ALL context after compaction on resumed sessions",
|
"Fix normalizer stripping ALL context after compaction on resumed sessions",
|
||||||
"No auto-reset when compaction summary present (preserves 1925+ turn history)",
|
"No auto-reset when compaction summary present (preserves 1925+ turn history)",
|
||||||
|
|||||||
@@ -616,6 +616,51 @@ class APIKeyPool(AccountPool):
|
|||||||
_cb_pool = CodebuffAccountPool("codebuff")
|
_cb_pool = CodebuffAccountPool("codebuff")
|
||||||
_google_antigravity_pool = GoogleAccountPool("antigravity")
|
_google_antigravity_pool = GoogleAccountPool("antigravity")
|
||||||
_google_cli_pool = GoogleAccountPool("cli")
|
_google_cli_pool = GoogleAccountPool("cli")
|
||||||
|
_antigravity_preferred_endpoint = None
|
||||||
|
_antigravity_endpoint_lock = threading.Lock()
|
||||||
|
|
||||||
|
def _classify_antigravity_error(status_code, body):
|
||||||
|
lower = body.lower()
|
||||||
|
if status_code == 400:
|
||||||
|
return "bad_request"
|
||||||
|
if status_code == 401:
|
||||||
|
if any(x in lower for x in ["invalid_grant", "token revoked", "token_revoked", "invalid_client"]):
|
||||||
|
return "auth_permanent"
|
||||||
|
return "auth_transient"
|
||||||
|
if status_code == 403:
|
||||||
|
if "validation_required" in lower or "account_disabled" in lower:
|
||||||
|
return "validation_required"
|
||||||
|
if "has been disabled" in lower and "violation of terms of service" in lower:
|
||||||
|
return "account_banned"
|
||||||
|
if "service_disabled" in lower:
|
||||||
|
return "service_disabled"
|
||||||
|
return "forbidden"
|
||||||
|
if status_code in (429, 503, 529):
|
||||||
|
if any(x in lower for x in ["model_capacity_exhausted", "capacity_exhausted", "model is currently overloaded", "service temporarily unavailable"]):
|
||||||
|
return "capacity_exhausted"
|
||||||
|
if any(x in lower for x in ["quota_exhausted", "resource_exhausted", "daily limit", "quota exceeded", "quotaresetdelay"]):
|
||||||
|
return "quota_exhausted"
|
||||||
|
return "rate_limited"
|
||||||
|
if status_code >= 500:
|
||||||
|
return "server_error"
|
||||||
|
return "unknown"
|
||||||
|
|
||||||
|
def _parse_rate_limit_reset(body):
|
||||||
|
import re as _re
|
||||||
|
m = _re.search(r'quotaResetDelay[:"\s]+(\d+(?:\.\d+)?)(ms|s)', body, _re.IGNORECASE)
|
||||||
|
if m:
|
||||||
|
val = float(m.group(1))
|
||||||
|
return val / 1000 if m.group(2) == 'ms' else val
|
||||||
|
m = _re.search(r'(\d+)h(\d+)m(\d+)s', body, _re.IGNORECASE)
|
||||||
|
if m:
|
||||||
|
return int(m.group(1)) * 3600 + int(m.group(2)) * 60 + int(m.group(3))
|
||||||
|
m = _re.search(r'Resets in ~(\d+)h(\d+)m', body, _re.IGNORECASE)
|
||||||
|
if m:
|
||||||
|
return int(m.group(1)) * 3600 + int(m.group(2)) * 60
|
||||||
|
m = _re.search(r'retry[-_]?after[:\s]+(\d+)\s*(?:sec|s\b)', body, _re.IGNORECASE)
|
||||||
|
if m:
|
||||||
|
return int(m.group(1))
|
||||||
|
return None
|
||||||
|
|
||||||
def _get_codebuff_account():
|
def _get_codebuff_account():
|
||||||
"""Return (token, account_dict) for best available codebuff account."""
|
"""Return (token, account_dict) for best available codebuff account."""
|
||||||
@@ -771,6 +816,20 @@ def _ensure_antigravity_version():
|
|||||||
_antigravity_version_checked = time.time()
|
_antigravity_version_checked = time.time()
|
||||||
return _antigravity_version
|
return _antigravity_version
|
||||||
|
|
||||||
|
_antigravity_client_version = "1.110.0"
|
||||||
|
_antigravity_client_version_checked = 0
|
||||||
|
|
||||||
|
def _ensure_antigravity_client_version():
|
||||||
|
global _antigravity_client_version, _antigravity_client_version_checked
|
||||||
|
env_ver = os.environ.get("ANTIGRAVITY_CLIENT_VERSION", "").strip()
|
||||||
|
if env_ver:
|
||||||
|
return env_ver
|
||||||
|
if time.time() - _antigravity_client_version_checked < 6 * 3600:
|
||||||
|
return _antigravity_client_version
|
||||||
|
_antigravity_client_version = os.environ.get("ANTIGRAVITY_CLIENT_VERSION_FALLBACK", "1.110.0")
|
||||||
|
_antigravity_client_version_checked = time.time()
|
||||||
|
return _antigravity_client_version
|
||||||
|
|
||||||
def _init_runtime():
|
def _init_runtime():
|
||||||
global CONFIG, PORT, BACKEND, TARGET_URL, API_KEY, OAUTH_PROVIDER, _antigravity_version
|
global CONFIG, PORT, BACKEND, TARGET_URL, API_KEY, OAUTH_PROVIDER, _antigravity_version
|
||||||
global MODELS, CC_VERSION, REASONING_ENABLED, REASONING_EFFORT, BGP_ROUTES
|
global MODELS, CC_VERSION, REASONING_ENABLED, REASONING_EFFORT, BGP_ROUTES
|
||||||
@@ -4280,9 +4339,10 @@ def _antigravity_is_simple_user(text):
|
|||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def _antigravity_normalize_context(input_data):
|
def _antigravity_normalize_context(input_data, model=""):
|
||||||
if not isinstance(input_data, list) or len(input_data) < 2:
|
if not isinstance(input_data, list) or len(input_data) < 2:
|
||||||
return input_data
|
return input_data
|
||||||
|
is_claude_model = "claude" in model.lower()
|
||||||
|
|
||||||
latest_user = ""
|
latest_user = ""
|
||||||
latest_user_idx = -1
|
latest_user_idx = -1
|
||||||
@@ -4346,9 +4406,15 @@ def _antigravity_normalize_context(input_data):
|
|||||||
latest_words = set(latest_user.strip().lower().split())
|
latest_words = set(latest_user.strip().lower().split())
|
||||||
has_edit_intent = bool(latest_words.intersection(_ANTIGRAVITY_EDIT_WORDS))
|
has_edit_intent = bool(latest_words.intersection(_ANTIGRAVITY_EDIT_WORDS))
|
||||||
has_ref_intent = bool(latest_words.intersection(_ANTIGRAVITY_REFERENCE_WORDS))
|
has_ref_intent = bool(latest_words.intersection(_ANTIGRAVITY_REFERENCE_WORDS))
|
||||||
keep_tools = 2 if (has_edit_intent or has_ref_intent) else 1
|
if is_claude_model:
|
||||||
|
keep_tools = len(tool_outputs)
|
||||||
|
else:
|
||||||
|
keep_tools = 2 if (has_edit_intent or has_ref_intent) else 1
|
||||||
|
|
||||||
kept_tools = tool_outputs[-keep_tools:] if tool_outputs and (has_edit_intent or has_ref_intent) else []
|
if is_claude_model:
|
||||||
|
kept_tools = tool_outputs
|
||||||
|
else:
|
||||||
|
kept_tools = tool_outputs[-keep_tools:] if tool_outputs and (has_edit_intent or has_ref_intent) else []
|
||||||
|
|
||||||
for idx_t, t_item in enumerate(kept_tools):
|
for idx_t, t_item in enumerate(kept_tools):
|
||||||
orig = t_item[1]
|
orig = t_item[1]
|
||||||
@@ -4640,7 +4706,7 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
|||||||
body["input"] = input_data
|
body["input"] = input_data
|
||||||
|
|
||||||
compacted = False
|
compacted = False
|
||||||
if policy.get("compaction") and isinstance(input_data, list):
|
if policy.get("compaction") and isinstance(input_data, list) and "claude" not in model.lower():
|
||||||
input_data, compacted = _adaptive_compact(input_data, model, policy)
|
input_data, compacted = _adaptive_compact(input_data, model, policy)
|
||||||
if compacted:
|
if compacted:
|
||||||
body = dict(body)
|
body = dict(body)
|
||||||
@@ -4819,7 +4885,7 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
|||||||
body["input"] = input_data
|
body["input"] = input_data
|
||||||
|
|
||||||
compacted = False
|
compacted = False
|
||||||
if policy.get("compaction") and isinstance(input_data, list):
|
if policy.get("compaction") and isinstance(input_data, list) and "claude" not in model.lower():
|
||||||
input_data, compacted = _adaptive_compact(input_data, model, policy)
|
input_data, compacted = _adaptive_compact(input_data, model, policy)
|
||||||
if compacted:
|
if compacted:
|
||||||
body = dict(body)
|
body = dict(body)
|
||||||
@@ -4830,8 +4896,8 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
|||||||
body = dict(body)
|
body = dict(body)
|
||||||
body["input"] = input_data
|
body["input"] = input_data
|
||||||
|
|
||||||
if OAUTH_PROVIDER == "google-antigravity" and isinstance(input_data, list):
|
if OAUTH_PROVIDER == "google-antigravity" and isinstance(input_data, list) and "claude" not in model.lower():
|
||||||
input_data = _antigravity_normalize_context(input_data)
|
input_data = _antigravity_normalize_context(input_data, model)
|
||||||
body = dict(body)
|
body = dict(body)
|
||||||
body["input"] = input_data
|
body["input"] = input_data
|
||||||
|
|
||||||
@@ -4909,7 +4975,7 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
|||||||
resp_part["functionResponse"]["id"] = call_id
|
resp_part["functionResponse"]["id"] = call_id
|
||||||
contents.append({"role": "user", "parts": [resp_part]})
|
contents.append({"role": "user", "parts": [resp_part]})
|
||||||
|
|
||||||
if OAUTH_PROVIDER.startswith("google"):
|
if OAUTH_PROVIDER.startswith("google") and "claude" not in model.lower():
|
||||||
sanitized = []
|
sanitized = []
|
||||||
last_user_text = None
|
last_user_text = None
|
||||||
last_role = None
|
last_role = None
|
||||||
@@ -5000,8 +5066,19 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
|||||||
contents = _gemini_reattach_sigs(contents)
|
contents = _gemini_reattach_sigs(contents)
|
||||||
|
|
||||||
if OAUTH_PROVIDER == "google-antigravity":
|
if OAUTH_PROVIDER == "google-antigravity":
|
||||||
|
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
|
||||||
|
is_latest_simple = _antigravity_is_simple_user(latest_user)
|
||||||
guardrail_found = any("autonomous coding agent" in json.dumps(c.get("parts", []), ensure_ascii=False) for c in contents[:2])
|
guardrail_found = any("autonomous coding agent" in json.dumps(c.get("parts", []), ensure_ascii=False) for c in contents[:2])
|
||||||
if not guardrail_found:
|
if not guardrail_found and not is_latest_simple:
|
||||||
contents.insert(0, {"role": "user", "parts": [{"text": _GEMINI_AGENT_GUARDRAIL}]})
|
contents.insert(0, {"role": "user", "parts": [{"text": _GEMINI_AGENT_GUARDRAIL}]})
|
||||||
|
|
||||||
if OAUTH_PROVIDER == "google-antigravity" and isinstance(input_data, list):
|
if OAUTH_PROVIDER == "google-antigravity" and isinstance(input_data, list):
|
||||||
@@ -5068,10 +5145,14 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
|||||||
wrapped["requestType"] = "agent"
|
wrapped["requestType"] = "agent"
|
||||||
wrapped["userAgent"] = "antigravity"
|
wrapped["userAgent"] = "antigravity"
|
||||||
wrapped["requestId"] = f"agent-{uuid.uuid4().hex[:12]}"
|
wrapped["requestId"] = f"agent-{uuid.uuid4().hex[:12]}"
|
||||||
|
wrapped["request"]["sessionId"] = f"{uuid.uuid4().hex}{int(time.time()*1000)}"
|
||||||
|
|
||||||
_allow_staging = os.environ.get("ALLOW_ANTIGRAVITY_STAGING", "0") == "1"
|
_allow_staging = os.environ.get("ALLOW_ANTIGRAVITY_STAGING", "0") == "1"
|
||||||
if OAUTH_PROVIDER == "google-antigravity":
|
if OAUTH_PROVIDER == "google-antigravity":
|
||||||
_antigravity_endpoints = ["https://cloudcode-pa.googleapis.com"]
|
_antigravity_endpoints = [
|
||||||
|
"https://cloudcode-pa.googleapis.com",
|
||||||
|
"https://daily-cloudcode-pa.googleapis.com",
|
||||||
|
]
|
||||||
if _allow_staging:
|
if _allow_staging:
|
||||||
_antigravity_endpoints.extend([
|
_antigravity_endpoints.extend([
|
||||||
"https://daily-cloudcode-pa.sandbox.googleapis.com",
|
"https://daily-cloudcode-pa.sandbox.googleapis.com",
|
||||||
@@ -5089,7 +5170,13 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
|||||||
}
|
}
|
||||||
if OAUTH_PROVIDER == "google-antigravity":
|
if OAUTH_PROVIDER == "google-antigravity":
|
||||||
version = _ensure_antigravity_version()
|
version = _ensure_antigravity_version()
|
||||||
headers["User-Agent"] = f"antigravity/{version} darwin/arm64"
|
import platform as _plat
|
||||||
|
_os_name = _plat.system().lower()
|
||||||
|
_os_arch = _plat.machine().lower().replace("x86_64", "x64").replace("aarch64", "arm64")
|
||||||
|
headers["User-Agent"] = f"antigravity/{version} {_os_name}/{_os_arch}"
|
||||||
|
headers["X-Client-Name"] = "antigravity"
|
||||||
|
headers["X-Client-Version"] = _ensure_antigravity_client_version()
|
||||||
|
headers["x-goog-api-client"] = "gl-node/18.18.2 fire/0.8.6 grpc/1.10.x"
|
||||||
else:
|
else:
|
||||||
headers["User-Agent"] = "google-api-nodejs-client/9.15.1"
|
headers["User-Agent"] = "google-api-nodejs-client/9.15.1"
|
||||||
headers["X-Goog-Api-Client"] = "gl-node/22.17.0"
|
headers["X-Goog-Api-Client"] = "gl-node/22.17.0"
|
||||||
@@ -5109,14 +5196,33 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
|||||||
if OAUTH_PROVIDER == "google-antigravity":
|
if OAUTH_PROVIDER == "google-antigravity":
|
||||||
print(f"[antigravity-endpoint] endpoints={[e.replace('https://','') for e in endpoints]} project={project_id}", file=sys.stderr)
|
print(f"[antigravity-endpoint] endpoints={[e.replace('https://','') for e in endpoints]} project={project_id}", file=sys.stderr)
|
||||||
|
|
||||||
for ep in endpoints:
|
upstream = None
|
||||||
|
chosen_ep = None
|
||||||
|
global _antigravity_preferred_endpoint
|
||||||
|
|
||||||
|
with _antigravity_endpoint_lock:
|
||||||
|
_pref = _antigravity_preferred_endpoint
|
||||||
|
|
||||||
|
if _pref and _pref in endpoints:
|
||||||
|
ordered = [_pref] + [e for e in endpoints if e != _pref]
|
||||||
|
else:
|
||||||
|
ordered = list(endpoints)
|
||||||
|
|
||||||
|
for ep in ordered:
|
||||||
target = f"{ep}/{url_suffix}"
|
target = f"{ep}/{url_suffix}"
|
||||||
req = urllib.request.Request(target, data=body_b, headers=headers)
|
req = urllib.request.Request(target, data=body_b, headers=headers)
|
||||||
try:
|
try:
|
||||||
upstream = urllib.request.urlopen(req, timeout=_upstream_timeout(body, stream))
|
upstream = urllib.request.urlopen(req, timeout=_upstream_timeout(body, stream))
|
||||||
|
chosen_ep = ep
|
||||||
|
with _antigravity_endpoint_lock:
|
||||||
|
_antigravity_preferred_endpoint = ep
|
||||||
|
if ep != _pref:
|
||||||
|
print(f"[{self._session_id}] fallback OK: {ep.replace('https://','')}", file=sys.stderr)
|
||||||
break
|
break
|
||||||
except urllib.error.HTTPError as e:
|
except urllib.error.HTTPError as e:
|
||||||
err_body = e.read().decode()
|
err_body = e.read().decode()
|
||||||
|
err_class = _classify_antigravity_error(e.code, err_body)
|
||||||
|
print(f"[{self._session_id}] {ep.replace('https://','')} {e.code} class={err_class}", file=sys.stderr)
|
||||||
if e.code == 400 and OAUTH_PROVIDER.startswith("google"):
|
if e.code == 400 and OAUTH_PROVIDER.startswith("google"):
|
||||||
try:
|
try:
|
||||||
debug_path = os.path.join(_LOG_DIR, "gemini-last-400-request.json")
|
debug_path = os.path.join(_LOG_DIR, "gemini-last-400-request.json")
|
||||||
@@ -5125,23 +5231,38 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
|||||||
print(f"[{self._session_id}] saved 400 debug request to {debug_path}", file=sys.stderr)
|
print(f"[{self._session_id}] saved 400 debug request to {debug_path}", file=sys.stderr)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
if e.code == 403 and "SERVICE_DISABLED" in err_body[:500] and ep != endpoints[-1]:
|
return self.send_json(e.code, {"error": {"type": "upstream_error", "message": _sanitize_err_body(err_body)}})
|
||||||
print(f"[{self._session_id}] {ep} SERVICE_DISABLED, trying next endpoint", file=sys.stderr)
|
if err_class == "auth_permanent":
|
||||||
|
return self.send_json(e.code, {"error": {"type": "upstream_error", "message": _sanitize_err_body(err_body)}})
|
||||||
|
if err_class in ("quota_exhausted", "rate_limited"):
|
||||||
|
reset_s = _parse_rate_limit_reset(err_body)
|
||||||
|
if ep == ordered[-1]:
|
||||||
|
pool = _google_antigravity_pool if OAUTH_PROVIDER == "google-antigravity" else _google_cli_pool
|
||||||
|
_, acct = _get_google_account(OAUTH_PROVIDER)
|
||||||
|
if acct:
|
||||||
|
cooldown = reset_s if reset_s and reset_s > 10 else 60
|
||||||
|
pool.mark_rate_limited(acct, cooldown)
|
||||||
|
print(f"[{self._session_id}] quota reset in ~{reset_s}s, cooldown={cooldown}s", file=sys.stderr)
|
||||||
|
return self.send_json(e.code, {"error": {"type": "upstream_error", "message": _sanitize_err_body(err_body)}})
|
||||||
|
print(f"[{self._session_id}] {ep.replace('https://','')} 429, trying next", file=sys.stderr)
|
||||||
|
with _antigravity_endpoint_lock:
|
||||||
|
_antigravity_preferred_endpoint = None
|
||||||
continue
|
continue
|
||||||
if e.code == 429 and ep != endpoints[-1] and _allow_staging:
|
if err_class in ("service_disabled", "forbidden", "account_banned", "validation_required"):
|
||||||
print(f"[{self._session_id}] {ep} HTTP 429, trying next endpoint", file=sys.stderr)
|
if ep == ordered[-1]:
|
||||||
|
return self.send_json(e.code, {"error": {"type": "upstream_error", "message": _sanitize_err_body(err_body)}})
|
||||||
continue
|
continue
|
||||||
if e.code == 429:
|
if ep == ordered[-1]:
|
||||||
pool = _google_antigravity_pool if OAUTH_PROVIDER == "google-antigravity" else _google_cli_pool
|
return self.send_json(e.code, {"error": {"type": "upstream_error", "message": _sanitize_err_body(err_body)}})
|
||||||
_, acct = _get_google_account(OAUTH_PROVIDER)
|
|
||||||
if acct:
|
|
||||||
pool.mark_rate_limited(acct, 60)
|
|
||||||
return self.send_json(e.code, {"error": {"type": "upstream_error", "message": _sanitize_err_body(err_body)}})
|
|
||||||
except Exception as e:
|
|
||||||
if ep == endpoints[-1]:
|
|
||||||
return self.send_json(502, {"error": {"type": "proxy_error", "message": str(e)}})
|
|
||||||
print(f"[{self._session_id}] {ep} failed: {e}, trying next", file=sys.stderr)
|
|
||||||
continue
|
continue
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[{self._session_id}] {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:
|
if stream:
|
||||||
self._forward_gemini_sse(upstream, model, body, input_data, tracker)
|
self._forward_gemini_sse(upstream, model, body, input_data, tracker)
|
||||||
@@ -6429,9 +6550,42 @@ def _handle_shutdown_signal(sig, frame):
|
|||||||
if 'SERVER' in globals() and SERVER:
|
if 'SERVER' in globals() and SERVER:
|
||||||
SERVER.shutdown()
|
SERVER.shutdown()
|
||||||
|
|
||||||
|
def _anti_stall_cleanup():
|
||||||
|
my_pid = os.getpid()
|
||||||
|
my_port = PORT
|
||||||
|
killed = []
|
||||||
|
try:
|
||||||
|
import subprocess as _sp
|
||||||
|
out = _sp.run(["pgrep", "-f", "translate-proxy"], capture_output=True, text=True, timeout=5).stdout.strip()
|
||||||
|
for pid_str in out.splitlines():
|
||||||
|
pid_str = pid_str.strip()
|
||||||
|
if not pid_str or not pid_str.isdigit():
|
||||||
|
continue
|
||||||
|
pid = int(pid_str)
|
||||||
|
if pid == my_pid:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
os.kill(pid, signal.SIGTERM)
|
||||||
|
killed.append(pid)
|
||||||
|
except (ProcessLookupError, PermissionError):
|
||||||
|
pass
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
_cache_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "__pycache__")
|
||||||
|
if os.path.isdir(_cache_dir):
|
||||||
|
import shutil
|
||||||
|
shutil.rmtree(_cache_dir, ignore_errors=True)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
if killed:
|
||||||
|
print(f"[anti-stall] killed {len(killed)} stale proxy process(es): {killed}", flush=True)
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
global SERVER, _START_TIME
|
global SERVER, _START_TIME
|
||||||
_START_TIME = time.time()
|
_START_TIME = time.time()
|
||||||
|
_anti_stall_cleanup()
|
||||||
_init_runtime()
|
_init_runtime()
|
||||||
try:
|
try:
|
||||||
_current_cfg = os.path.basename(args.config) if args.config else ""
|
_current_cfg = os.path.basename(args.config) if args.config else ""
|
||||||
|
|||||||
Reference in New Issue
Block a user