From e2f20810f0707fd4d8b344ab8797a8b8b15898af Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 20 May 2026 21:44:33 +0400 Subject: [PATCH] v3.3.0: Antigravity OAuth + Gemini CLI OAuth, full Codex agent loop with tool calls, history hardening, SSE fixes --- CHANGELOG.md | 31 ++ README.md | 20 +- codex-launcher_2.7.0_all.deb | Bin 35098 -> 0 bytes codex-launcher_3.3.0_all.deb | Bin 0 -> 45164 bytes src/codex-launcher-gui | 392 +++++++++++++++---- src/translate-proxy.py | 729 ++++++++++++++++++++++++++++++++++- 6 files changed, 1085 insertions(+), 87 deletions(-) delete mode 100644 codex-launcher_2.7.0_all.deb create mode 100644 codex-launcher_3.3.0_all.deb diff --git a/CHANGELOG.md b/CHANGELOG.md index b4a7242..7547332 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,36 @@ # Changelog +## v3.3.0 (2026-05-20) + +**Antigravity + Gemini CLI OAuth — full Codex agent loop working** + +### Gemini CLI OAuth + Antigravity OAuth +- Split Google OAuth into separate Gemini CLI OAuth and Google Antigravity OAuth presets/backends. +- Gemini CLI OAuth uses the Gemini CLI public OAuth client and Code Assist endpoints. +- Antigravity OAuth uses Antigravity OAuth credentials, Code Assist daily/autopush/prod fallback, and Antigravity-style request wrapping. +- Added Antigravity version discovery from the updater/changelog with local caching. +- Added Antigravity model alias mapping from UI-facing `antigravity-*` IDs to upstream Code Assist model IDs. + +### Responses API + Tool Flow +- Added Gemini-style history hardening for Google OAuth requests: removes empty turns, coalesces adjacent roles, drops duplicate user repeats, and enforces user-start/user-end history. +- Preserves function-call IDs across turns and adds synthetic `thoughtSignature` for historical Gemini function calls, matching Gemini CLI hardening behavior. +- Fixed Antigravity streaming Responses API compatibility: single assistant message item, text done events, content part done, output item done, final completed event, and connection close. +- Added `response.function_call_arguments.delta` and `response.function_call_arguments.done` events so Codex can execute Antigravity tool calls and create files. +- Fixed functionResponse name matching — uses the original functionCall name instead of falling back to call_id. +- Strengthened Antigravity prompt policy: use tools immediately for file changes, avoid planning-only responses, and answer directly when no suitable tool exists. + +### Reliability + Routing +- Added BGP++ route scoring, route cooldowns, token buckets, and persisted route stats. +- Added provider policy layer and adaptive context compaction. +- Added tool-call pairing validation/repair for orphaned tool outputs. +- Added Endpoint Doctor in the endpoint editor. +- Added log redaction helper for common API key/token patterns. + +## v3.1.0 (2026-05-20) + +- Initial Antigravity/Gemini CLI OAuth backend split. +- Gemini-style history hardening, SSE streaming fixes. + ## v3.0.0 (2026-05-20) **Major architectural overhaul — Phase 0 + Phase 1 of engineering roadmap** diff --git a/README.md b/README.md index 785eb9d..5b4398d 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@

Run OpenAI Codex CLI & Desktop with any AI provider.
- OpenCode • Z.AI • Anthropic • Command Code • OpenRouter • Crof.ai • NVIDIA NIM • Kilo.ai • and more + Google Antigravity • Gemini CLI • OpenCode • Z.AI • Anthropic • Command Code • OpenRouter • Crof.ai • NVIDIA NIM • Kilo.ai • and more

@@ -43,14 +43,16 @@ OpenAI's Codex CLI v2.0+ exclusively uses the **Responses API** — a protocol t | Provider | API | Works with Codex? | |----------|-----|:-:| | OpenAI | Responses API | ✅ | -| Z.AI | Chat Completions | ❌ | -| OpenCode | Chat Completions | ❌ | -| Anthropic | Messages API | ❌ | -| Command Code | Custom `/alpha/generate` | ❌ | -| Ollama | Chat Completions | ❌ | -| OpenRouter | Chat Completions | ❌ | -| NVIDIA NIM | Chat Completions | ❌ | -| Crof.ai | Chat Completions | ❌ | +| Google Antigravity (OAuth) | Code Assist / Gemini Native | ✅ | +| Gemini CLI OAuth | Code Assist | ✅ | +| Z.AI | Chat Completions | ✅ | +| OpenCode | Chat Completions | ✅ | +| Anthropic | Messages API | ✅ | +| Command Code | Custom `/alpha/generate` | ✅ | +| Ollama | Chat Completions | ✅ | +| OpenRouter | Chat Completions | ✅ | +| NVIDIA NIM | Chat Completions | ✅ | +| Crof.ai | Chat Completions | ✅ | The protocols differ in **endpoint paths**, **message formats**, **tool-call structures**, **streaming events**, and **completion semantics**. You can't just swap a base URL. diff --git a/codex-launcher_2.7.0_all.deb b/codex-launcher_2.7.0_all.deb deleted file mode 100644 index 1ba20a6e0a865b6fbd02e072974a7a39112f7250..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 35098 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 TD@^B==GC)tEEfX#kqSEs$;|+( diff --git a/codex-launcher_3.3.0_all.deb b/codex-launcher_3.3.0_all.deb new file mode 100644 index 0000000000000000000000000000000000000000..67c8d99c81f6d10a8741ff36dd4e2fb82cadab3f GIT binary patch literal 45164 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�kxy}m#_1qS#4osl2d@^M=1C- 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 literal 0 HcmV?d00001 diff --git a/src/codex-launcher-gui b/src/codex-launcher-gui index 604e4d3..c4de26a 100755 --- a/src/codex-launcher-gui +++ b/src/codex-launcher-gui @@ -4,8 +4,9 @@ import gi gi.require_version("Gtk", "3.0") from gi.repository import Gtk, GLib -import subprocess, os, signal, sys, threading, time, json, urllib.request, tempfile, shutil +import subprocess, os, signal, sys, threading, time, json, urllib.request, urllib.parse, tempfile, shutil import hashlib, socket, contextlib +import base64, secrets from pathlib import Path HOME = Path.home() @@ -25,6 +26,21 @@ model_catalog_json = "" """ CHANGELOG = [ + ("3.3.0", "2026-05-20", [ + "Added Google Antigravity OAuth backend with Code Assist endpoints and model alias mapping", + "Added Gemini CLI OAuth backend using public Gemini CLI OAuth client", + "Antigravity now creates files via tool calls — full Codex agent loop with Gemini-style history hardening", + "Fixed tool-call streaming: function_call_arguments delta/done events, thought signatures, functionResponse name matching", + "Added Endpoint Doctor, adaptive BGP scoring, provider policies, adaptive compaction, log redaction", + ]), + ("3.1.0", "2026-05-20", [ + "Initial Antigravity/Gemini CLI OAuth split, history hardening, SSE fixes", + ]), + ("3.0.0", "2026-05-20", [ + "ThreadingHTTPServer with dynamic proxy ports and health-gated Codex launch", + "Atomic config writes, safe cleanup registry, graceful shutdown, and buffered SSE streaming", + "Usage Dashboard v2, TCP_NODELAY streaming, Anthropic prompt caching, and batched usage stats", + ]), ("2.6.1", "2026-05-20", [ "Google OAuth rebuilt to emulate Gemini CLI — no client_secret.json needed", "Uses Google's public OAuth client_id (same as gemini-cli)", @@ -226,13 +242,25 @@ PROVIDER_PRESETS = { ], }, "Google Gemini (OAuth)": { - "backend_type": "openai-compat", - "base_url": "https://generativelanguage.googleapis.com/v1beta/openai", - "oauth_provider": "google", + "backend_type": "gemini-oauth-cli", + "base_url": "https://cloudcode-pa.googleapis.com", + "oauth_provider": "google-cli", "models": [ "gemini-2.5-flash", "gemini-2.5-pro", - "gemini-2.0-flash", "gemini-2.0-flash-lite", - "gemini-2.5-flash-preview-native-audio-dialog", + ], + }, + "Google Antigravity (OAuth)": { + "backend_type": "gemini-oauth-antigravity", + "base_url": "https://daily-cloudcode-pa.sandbox.googleapis.com", + "oauth_provider": "google-antigravity", + "models": [ + "antigravity-gemini-3-flash", + "antigravity-gemini-3-pro", + "antigravity-gemini-3.1-pro", + "antigravity-claude-sonnet-4-6", + "antigravity-claude-opus-4-6-thinking", + "gemini-2.5-flash", "gemini-2.5-pro", + "gemini-3-flash-preview", "gemini-3-pro-preview", "gemini-3.1-pro-preview", ], }, "OpenAdapter": { @@ -301,8 +329,10 @@ def apply_provider_preset(endpoint, preset_name): updated["base_url"] = normalize_base_url(preset["base_url"]) if preset.get("cc_version") and not updated.get("cc_version"): updated["cc_version"] = preset["cc_version"] - if not updated.get("models"): + if not updated.get("models") or (preset.get("backend_type") or "").startswith("gemini-oauth"): updated["models"] = list(preset.get("models", [])) + if preset.get("oauth_provider"): + updated["oauth_provider"] = preset["oauth_provider"] if not updated.get("default_model") and updated.get("models"): updated["default_model"] = updated["models"][0] return updated @@ -630,6 +660,18 @@ def _start_proxy_for(endpoint, logfn): port = _pick_free_port() _proxy_port = port + model_list = endpoint.get("models", []) + if (endpoint.get("backend_type") or "").startswith("gemini-oauth") and (endpoint.get("oauth_provider") or "").startswith("google"): + token_name = "google-antigravity-oauth-token.json" if endpoint.get("oauth_provider") == "google-antigravity" else "google-cli-oauth-token.json" + token_path = os.path.expanduser(f"~/.cache/codex-proxy/{token_name}") + try: + with open(token_path) as tf: + td = json.load(tf) + discovered = [] if endpoint.get("oauth_provider") == "google-antigravity" else td.get("available_models", []) + if discovered: + model_list = discovered + except Exception: + pass pcfg = { "port": port, "backend_type": endpoint["backend_type"], @@ -640,7 +682,7 @@ def _start_proxy_for(endpoint, logfn): "reasoning_enabled": endpoint.get("reasoning_enabled", True), "reasoning_effort": endpoint.get("reasoning_effort", "medium"), "models": [{"id": m, "object": "model", "created": 1700000000, "owned_by": endpoint["name"]} - for m in endpoint.get("models", [])], + for m in model_list], } pcfg_path = PROXY_CONFIG_DIR / f"proxy-{safe_name(endpoint['name'])}-{port}.json" pcfg_path.parent.mkdir(parents=True, exist_ok=True) @@ -763,7 +805,7 @@ class LauncherWin(Gtk.Window): # header row hdr = Gtk.Box(spacing=8) vbox.pack_start(hdr, False, False, 0) - lbl = Gtk.Label(label="Codex Launcher v3.0.0") + lbl = Gtk.Label(label="Codex Launcher v3.3.0") lbl.set_use_markup(True) hdr.pack_start(lbl, False, False, 0) changelog_btn = Gtk.Button(label="Changelog") @@ -1277,7 +1319,7 @@ class LauncherWin(Gtk.Window): self.log("ERROR: no model selected") return - is_bgp = name.startswith("🔀 ") + is_bgp = bool(name and name.startswith("🔀 ")) if is_bgp: pool_name = name[2:] pool = None @@ -1781,6 +1823,8 @@ class EditEndpointDialog(Gtk.Dialog): for val, lab in [("openai-compat", "OpenAI-compatible (needs proxy)"), ("anthropic", "Anthropic (needs proxy)"), ("command-code", "Command Code (needs proxy)"), + ("gemini-oauth-cli", "Gemini CLI OAuth (needs proxy)"), + ("gemini-oauth-antigravity", "Antigravity OAuth (needs proxy)"), ("native", "Native OpenAI (no proxy)")]: self._combo_type.append(val, lab) bt = self._data.get("backend_type", "openai-compat") @@ -1866,6 +1910,9 @@ class EditEndpointDialog(Gtk.Dialog): self._fetch_models_btn = Gtk.Button(label="Fetch from API") self._fetch_models_btn.connect("clicked", lambda b: self._fetch_models()) mbox.pack_start(self._fetch_models_btn, False, False, 0) + self._test_btn = Gtk.Button(label="Test Endpoint") + self._test_btn.connect("clicked", lambda b: self._diagnose_endpoint()) + mbox.pack_start(self._test_btn, False, False, 0) bulk_lbl = Gtk.Label(label="Bulk add models (one per line or comma-separated):", xalign=0) area.pack_start(bulk_lbl, False, False, 2) @@ -1970,29 +2017,52 @@ class EditEndpointDialog(Gtk.Dialog): preset_name = self._combo_preset.get_active_text() or "Custom" preset = PROVIDER_PRESETS.get(preset_name, {}) provider = preset.get("oauth_provider", "") - if provider == "google": - self._google_oauth_flow() + if (provider or "").startswith("google"): + self._google_oauth_flow(provider) - def _google_oauth_flow(self): - token_path = os.path.expanduser("~/.cache/codex-proxy/google-oauth-token.json") + def _google_oauth_flow(self, oauth_provider="google-cli"): + is_antigravity = oauth_provider == "google-antigravity" + token_path = os.path.expanduser("~/.cache/codex-proxy/google-antigravity-oauth-token.json" if is_antigravity else "~/.cache/codex-proxy/google-cli-oauth-token.json") - CLIENT_ID = "681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com" - CLIENT_SECRET = "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w" - SCOPES = [ - "https://www.googleapis.com/auth/cloud-platform", - "https://www.googleapis.com/auth/generative-language.retriever", - "https://www.googleapis.com/auth/userinfo.email", - "https://www.googleapis.com/auth/userinfo.profile", - ] - import http.server, hashlib, secrets, socket + if is_antigravity: + CLIENT_ID = "1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com" + CLIENT_SECRET = "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" + SCOPES = [ + "https://www.googleapis.com/auth/cloud-platform", + "https://www.googleapis.com/auth/userinfo.email", + "https://www.googleapis.com/auth/userinfo.profile", + "https://www.googleapis.com/auth/cclog", + "https://www.googleapis.com/auth/experimentsandconfigs", + ] + port = 51121 + redirect_uri = f"http://localhost:{port}/oauth-callback" + callback_path = "/oauth-callback" + provider_kind = "antigravity" + else: + CLIENT_ID = "681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com" + CLIENT_SECRET = "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" + SCOPES = [ + "https://www.googleapis.com/auth/cloud-platform", + "https://www.googleapis.com/auth/userinfo.email", + "https://www.googleapis.com/auth/userinfo.profile", + ] + port = 0 + redirect_uri = None + callback_path = "/oauth2callback" + provider_kind = "cli" + + import http.server - port = 8085 state = secrets.token_hex(32) - verifier = secrets.token_urlsafe(32) - challenge = hashlib.sha256(verifier.encode()).digest() - challenge_b64 = urllib.parse.quote_plus(__import__('base64').urlsafe_b64encode(challenge).rstrip(b'=').decode()) + verifier = secrets.token_urlsafe(64) + challenge = base64.urlsafe_b64encode(hashlib.sha256(verifier.encode()).digest()).rstrip(b"=").decode() + + if port == 0: + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.bind(("127.0.0.1", 0)) + port = s.getsockname()[1] + redirect_uri = f"http://127.0.0.1:{port}/oauth2callback" - redirect_uri = f"http://127.0.0.1:{port}/oauth2callback" scope_str = " ".join(SCOPES) auth_url = ( f"https://accounts.google.com/o/oauth2/v2/auth?" @@ -2001,9 +2071,9 @@ class EditEndpointDialog(Gtk.Dialog): f"&response_type=code" f"&scope={urllib.parse.quote(scope_str)}" f"&access_type=offline" - f"&prompt=consent" + f"&prompt=select_account%20consent" f"&state={state}" - f"&code_challenge={challenge_b64}" + f"&code_challenge={challenge}" f"&code_challenge_method=S256" ) @@ -2043,6 +2113,14 @@ class EditEndpointDialog(Gtk.Dialog): qs = urllib.parse.urlparse(self2.path).query params = urllib.parse.parse_qs(qs) received_state[0] = params.get("state", [None])[0] + with open("/tmp/codex-oauth-debug.log", "a") as _dbg: + _dbg.write(f"[{time.strftime('%H:%M:%S')}] GET {self2.path} state={received_state[0]} code={'code' in params}\n") + if self2.path.find(callback_path) == -1: + self2.send_response(302) + self2.send_header("Location", "https://developers.google.com/gemini-code-assist/auth_failure_gemini") + self2.end_headers() + error_holder[0] = "unexpected request" + return if "code" in params: if received_state[0] != state: self2.send_response(400) @@ -2061,63 +2139,171 @@ class EditEndpointDialog(Gtk.Dialog): self2.send_response(302) self2.send_header("Location", "https://developers.google.com/gemini-code-assist/auth_failure_gemini") self2.end_headers() - def log_message(self2, *a): pass + def log_message(self2, fmt, *args): + with open("/tmp/codex-oauth-debug.log", "a") as _dbg: + _dbg.write(f"[{time.strftime('%H:%M:%S')}] {fmt % args}\n") try: - server = http.server.HTTPServer(("127.0.0.1", port), OAuthHandler) + bind_host = "localhost" if is_antigravity else "127.0.0.1" + server = http.server.HTTPServer((bind_host, port), OAuthHandler) except OSError: self._oauth_status.set_text(f"Port {port} already in use — close other apps and retry.") spinner.stop() dlg.run(); dlg.destroy() return + def _oauth_log(msg): + with open("/tmp/codex-oauth-debug.log", "a") as _f: + _f.write(f"[{time.strftime('%H:%M:%S')}] {msg}\n") + + _oauth_log(f"Starting OAuth: port={port} redirect_uri={redirect_uri}") + def wait_for_code(): - server.handle_request() + _oauth_log("wait_for_code thread started") + deadline = time.time() + 120 + while code_holder[0] is None and error_holder[0] is None and time.time() < deadline: + server.handle_request() server.server_close() - GLib.idle_add(self._google_oauth_complete_gemini, dlg, code_holder, error_holder, - CLIENT_ID, CLIENT_SECRET, redirect_uri, token_path, spinner, verifier) + _oauth_log(f"Server closed. code={'yes' if code_holder[0] else 'no'} error={'yes' if error_holder[0] else 'no'}") + if code_holder[0]: + try: + _oauth_log("Exchanging code for token...") + token_data = urllib.parse.urlencode({ + "code": code_holder[0], + "client_id": CLIENT_ID, + "client_secret": CLIENT_SECRET, + "redirect_uri": redirect_uri, + "grant_type": "authorization_code", + "code_verifier": verifier, + }).encode() + req = urllib.request.Request("https://oauth2.googleapis.com/token", data=token_data, + headers={"Content-Type": "application/x-www-form-urlencoded"}) + resp = urllib.request.urlopen(req, timeout=30) + tokens = json.loads(resp.read()) + tokens["client_id"] = CLIENT_ID + tokens["client_secret"] = CLIENT_SECRET + tokens["provider_kind"] = provider_kind + tokens["expires_at"] = time.time() + tokens.get("expires_in", 3600) + os.makedirs(os.path.dirname(token_path), exist_ok=True) + with open(token_path, "w") as f: + json.dump(tokens, f, indent=2) + os.chmod(token_path, 0o600) + _oauth_log(f"Token saved to {token_path}") + project_id = "" + try: + _oauth_log("Discovering project ID via loadCodeAssist...") + lr = urllib.request.Request( + "https://cloudcode-pa.googleapis.com/v1internal:loadCodeAssist", + data=json.dumps({}).encode(), + headers={ + "Content-Type": "application/json", + "Authorization": f"Bearer {tokens['access_token']}", + "User-Agent": "google-api-nodejs-client/9.15.1", + }) + lresp = urllib.request.urlopen(lr, timeout=15) + ldata = json.loads(lresp.read()) + p = ldata.get("cloudaicompanionProject", "") + if isinstance(p, dict): + project_id = p.get("id", "") + elif isinstance(p, str): + project_id = p + _oauth_log(f"Project ID: {project_id or '(none)'}") + if project_id: + tokens["project_id"] = project_id + with open(token_path, "w") as f2: + json.dump(tokens, f2, indent=2) + os.chmod(token_path, 0o600) + except Exception as pe: + _oauth_log(f"loadCodeAssist failed (non-fatal): {pe}") + if is_antigravity: + found_models = [ + "gemini-2.5-flash", "gemini-2.5-pro", + "gemini-3-flash-preview", "gemini-3-pro-preview", "gemini-3.1-pro-preview", + "gemini-3-pro-low", "gemini-3-pro-high", + "gemini-3.1-pro-low", "gemini-3.1-pro-high", + "gemini-3-flash-low", "gemini-3-flash-medium", "gemini-3-flash-high", + "claude-sonnet-4-6", "claude-opus-4-6-thinking", + "claude-opus-4-6-thinking-low", "claude-opus-4-6-thinking-medium", "claude-opus-4-6-thinking-high", + "gemini-claude-sonnet-4-6", + "gemini-claude-opus-4-6-thinking-low", "gemini-claude-opus-4-6-thinking-medium", "gemini-claude-opus-4-6-thinking-high", + "gemini-3-pro-image", + ] + probe_candidates = [ + "gemini-2.5-flash", "gemini-2.5-pro", + "gemini-3-flash-preview", "gemini-3-pro-preview", "gemini-3.1-pro-preview", + ] + _oauth_log(f"Probing {len(probe_candidates)} model candidates...") + for mc in probe_candidates: + try: + pr = urllib.request.Request( + "https://daily-cloudcode-pa.sandbox.googleapis.com/v1internal:generateContent", + data=json.dumps({ + "project": project_id, + "model": mc, + "request": {"contents": [{"role": "user", "parts": [{"text": "x"}]}], + "generationConfig": {"maxOutputTokens": 1}}, + }).encode(), + headers={ + "Content-Type": "application/json", + "Authorization": f"Bearer {tokens['access_token']}", + "User-Agent": "google-api-nodejs-client/9.15.1", + "Client-Metadata": "ideType=IDE_UNSPECIFIED,platform=PLATFORM_UNSPECIFIED,pluginType=GEMINI", + }) + pr.get_method = lambda: "POST" + resp = urllib.request.urlopen(pr, timeout=10) + resp.read() + found_models.append(mc) + _oauth_log(f" {mc} → available") + except urllib.error.HTTPError as e: + if e.code == 429: + found_models.append(mc) + _oauth_log(f" {mc} → available (rate limited)") + else: + e.read() + _oauth_log(f" {mc} → HTTP {e.code}") + except Exception as e: + _oauth_log(f" {mc} → error: {e}") + else: + found_models = ["gemini-2.5-flash", "gemini-2.5-pro"] + if found_models: + tokens["available_models"] = found_models + with open(token_path, "w") as f3: + json.dump(tokens, f3, indent=2) + os.chmod(token_path, 0o600) + _oauth_log(f"Discovered {len(found_models)} models: {found_models}") + else: + _oauth_log("No models discovered (will use defaults)") + GLib.idle_add(self._oauth_success, dlg, tokens.get("access_token", ""), spinner) + return + except urllib.error.HTTPError as e: + body = e.read().decode(errors='replace') + _oauth_log(f"Token exchange HTTP {e.code}: {body}") + GLib.idle_add(self._oauth_failed, dlg, f"Token exchange failed ({e.code}): {body[:200]}", spinner) + return + except Exception as e: + _oauth_log(f"Token exchange FAILED: {e}") + GLib.idle_add(self._oauth_failed, dlg, f"Token exchange failed: {e}", spinner) + return + _oauth_log(f"OAuth failed: {error_holder[0] or 'timeout'}") + GLib.idle_add(self._oauth_failed, dlg, + error_holder[0] or "No authorization code received.", spinner) threading.Thread(target=wait_for_code, daemon=True).start() subprocess.Popen(["xdg-open", auth_url], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + dlg.connect("response", lambda d, r: d.destroy()) dlg.run() - dlg.destroy() - def _google_oauth_complete_gemini(self, dlg, code_holder, error_holder, - client_id, client_secret, redirect_uri, token_path, spinner, verifier): + def _oauth_success(self, dlg, access_token, spinner): spinner.stop() - if error_holder[0]: - self._oauth_status.set_markup(f'Error: {error_holder[0]}') - return - if not code_holder[0]: - self._oauth_status.set_text("No authorization code received.") - return + self._entry_key.set_text(access_token) + self._oauth_status.set_markup('Authorization successful! Token saved.') + dlg.set_title("Google OAuth — Success") + GLib.timeout_add(1500, lambda: dlg.response(Gtk.ResponseType.OK)) - self._oauth_status.set_text("Exchanging code for token…") - try: - token_data = urllib.parse.urlencode({ - "code": code_holder[0], - "client_id": client_id, - "client_secret": client_secret, - "redirect_uri": redirect_uri, - "grant_type": "authorization_code", - "code_verifier": verifier, - }).encode() - req = urllib.request.Request("https://oauth2.googleapis.com/token", data=token_data, - headers={"Content-Type": "application/x-www-form-urlencoded"}) - resp = urllib.request.urlopen(req, timeout=30) - tokens = json.loads(resp.read()) - tokens["client_id"] = client_id - tokens["client_secret"] = client_secret - tokens["expires_at"] = time.time() + tokens.get("expires_in", 3600) - os.makedirs(os.path.dirname(token_path), exist_ok=True) - with open(token_path, "w") as f: - json.dump(tokens, f, indent=2) - os.chmod(token_path, 0o600) - self._entry_key.set_text(tokens.get("access_token", "")) - self._oauth_status.set_markup('Authorization successful! Token saved.') - dlg.set_title("Google OAuth — Success") - except Exception as e: - self._oauth_status.set_markup(f'Token exchange failed: {e}') + def _oauth_failed(self, dlg, msg, spinner): + spinner.stop() + self._oauth_status.set_markup(f'{msg}') + GLib.timeout_add(3000, lambda: dlg.response(Gtk.ResponseType.CANCEL)) def _remove_model(self, path): current = self._combo_default.get_active_text() @@ -2163,6 +2349,70 @@ class EditEndpointDialog(Gtk.Dialog): return True, None return False, err or "No models returned by endpoint" + def _diagnose_endpoint(self): + url = self._entry_url.get_text().strip() + key = self._entry_key.get_text().strip() + bt = self._combo_type.get_active_id() or "openai-compat" + model = self._combo_default.get_active_text() or "" + + checks = [] + def add(name, ok, detail=""): + checks.append((name, ok, detail)) + + parsed = urllib.parse.urlparse(url) + add("URL format", bool(parsed.scheme and parsed.netloc), + url if parsed.scheme else "Missing scheme (https://)") + + try: + t0 = time.time() + ep = {"base_url": url, "api_key": key, "backend_type": bt} + ids, err = fetch_models_for_endpoint(ep) + lat = (time.time() - t0) * 1000 + if ids: + add("Network reachable", True, f"{lat:.0f}ms") + add("Auth valid", True) + add("/models endpoint", True, f"{len(ids)} models in {lat:.0f}ms") + if model: + add("Selected model exists", model in ids, + model if model in ids else f"'{model}' not in {ids[:5]}...") + else: + add("Selected model", False, "No model selected") + elif err and ("401" in str(err) or "403" in str(err)): + add("Network reachable", True, f"{lat:.0f}ms") + add("Auth valid", False, str(err)[:100]) + add("/models endpoint", False, "Auth failed") + else: + add("Network reachable", False, str(err or "no response")[:100]) + except Exception as e: + add("Network", False, str(e)[:100]) + + dlg = Gtk.Dialog(title="Endpoint Doctor", parent=self, modal=True) + dlg.add_button("Close", Gtk.ResponseType.CLOSE) + dlg.set_default_size(420, 300) + area = dlg.get_content_area() + area.set_margin_start(12) + area.set_margin_end(12) + area.set_margin_top(12) + area.set_margin_bottom(12) + area.set_spacing(4) + for name, ok, detail in checks: + row = Gtk.Box(spacing=6) + icon = Gtk.Label() + icon.set_markup(f'{"\u2713" if ok else "\u2717"}') + row.pack_start(icon, False, False, 0) + lbl = Gtk.Label() + lbl.set_markup(f'{name}') + row.pack_start(lbl, False, False, 0) + if detail: + det = Gtk.Label() + det.set_markup(f'{detail}') + row.pack_end(det, False, False, 0) + area.pack_start(row, False, False, 0) + dlg.show_all() + dlg.run() + dlg.destroy() + def _on_response(self, dialog, response): if response != Gtk.ResponseType.OK: self.destroy() @@ -2172,7 +2422,7 @@ class EditEndpointDialog(Gtk.Dialog): if not name: self._show_error("Name is required") return - bt = self._combo_type.get_active_id() + bt = self._combo_type.get_active_id() or PROVIDER_PRESETS.get(self._combo_preset.get_active_text() or "", {}).get("backend_type") or "openai-compat" url = self._entry_url.get_text().strip() key = self._entry_key.get_text().strip() models = [self._model_store[i][0] for i in range(len(self._model_store))] diff --git a/src/translate-proxy.py b/src/translate-proxy.py index 53996f6..d86e14d 100755 --- a/src/translate-proxy.py +++ b/src/translate-proxy.py @@ -11,7 +11,7 @@ Usage: python3 translate-proxy.py --backend openai-compat --target-url https://... --api-key sk-... """ -import json, http.server, socketserver, urllib.request, urllib.parse, urllib.error +import json, http.server, socketserver, urllib.request, urllib.parse, urllib.error, re import time, uuid, os, sys, argparse, threading, socket, collections, contextlib, signal # ═══════════════════════════════════════════════════════════════════ @@ -107,9 +107,57 @@ _active_connections = 0 _active_connections_lock = threading.Lock() _pool = uuid.uuid4().hex[:8] +_antigravity_version = "1.18.3" +_antigravity_version_checked = 0 +_antigravity_version_lock = threading.Lock() + +def _fetch_antigravity_version(): + cache_path = os.path.join(os.path.expanduser("~"), ".cache", "codex-proxy", "antigravity-version.json") + try: + with open(cache_path) as f: + cached = json.load(f) + if cached.get("version") and cached.get("checked_at", 0) > time.time() - 6 * 3600: + return cached["version"] + except Exception: + pass + urls = [ + ("https://antigravity-auto-updater-974169037036.us-central1.run.app", None), + ("https://antigravity.google/changelog", 5000), + ] + for url, limit in urls: + try: + req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"}) + resp = urllib.request.urlopen(req, timeout=5) + text = resp.read().decode(errors="replace") + if limit: + text = text[:limit] + m = re.search(r"\d+\.\d+\.\d+", text) + if m: + version = m.group(0) + try: + os.makedirs(os.path.dirname(cache_path), exist_ok=True) + with open(cache_path, "w") as f: + json.dump({"version": version, "checked_at": time.time()}, f) + except Exception: + pass + return version + except Exception: + pass + return _antigravity_version + +def _ensure_antigravity_version(): + global _antigravity_version, _antigravity_version_checked + if time.time() - _antigravity_version_checked < 6 * 3600: + return _antigravity_version + with _antigravity_version_lock: + if time.time() - _antigravity_version_checked < 6 * 3600: + return _antigravity_version + _antigravity_version = _fetch_antigravity_version() + _antigravity_version_checked = time.time() + return _antigravity_version def _init_runtime(): - global CONFIG, PORT, BACKEND, TARGET_URL, API_KEY, OAUTH_PROVIDER + global CONFIG, PORT, BACKEND, TARGET_URL, API_KEY, OAUTH_PROVIDER, _antigravity_version global MODELS, CC_VERSION, REASONING_ENABLED, REASONING_EFFORT, BGP_ROUTES CONFIG = load_config() @@ -117,12 +165,15 @@ def _init_runtime(): BACKEND = CONFIG["backend_type"] TARGET_URL = CONFIG["target_url"].rstrip("/") API_KEY = CONFIG["api_key"] - OAUTH_PROVIDER = CONFIG.get("oauth_provider", "") + OAUTH_PROVIDER = CONFIG.get("oauth_provider") or "" MODELS = CONFIG["models"] CC_VERSION = CONFIG.get("cc_version", "") REASONING_ENABLED = CONFIG.get("reasoning_enabled", True) REASONING_EFFORT = CONFIG.get("reasoning_effort", "medium") BGP_ROUTES = CONFIG.get("bgp_routes", []) + if OAUTH_PROVIDER == "google-antigravity": + _antigravity_version = _ensure_antigravity_version() + print(f"[antigravity] version={_antigravity_version}", file=sys.stderr) bgp_models = [] for _r in BGP_ROUTES: @@ -134,13 +185,33 @@ def _init_runtime(): MODELS = [{"id": m, "object": "model", "created": 1700000000, "owned_by": "bgp"} for m in bgp_models] CONFIG["models"] = MODELS + if (BACKEND or "").startswith("gemini-oauth") and (OAUTH_PROVIDER or "").startswith("google"): + token_name = "google-antigravity-oauth-token.json" if OAUTH_PROVIDER == "google-antigravity" else "google-cli-oauth-token.json" + token_path = os.path.join(os.path.expanduser("~"), ".cache", "codex-proxy", token_name) + try: + with open(token_path) as _tf: + _td = json.load(_tf) + _discovered = [] if OAUTH_PROVIDER == "google-antigravity" else _td.get("available_models", []) + if _discovered: + _seen = [] + for _m in _discovered: + if _m not in _seen: + _seen.append(_m) + MODELS = [{"id": m, "object": "model", "created": 1700000000, "owned_by": "gemini-oauth"} for m in _seen] + CONFIG["models"] = MODELS + print(f"[gemini-oauth] loaded {len(_seen)} discovered models: {_seen}", file=sys.stderr) + except Exception: + pass + def _refresh_oauth_token(): return _refresh_oauth_token_for(API_KEY, OAUTH_PROVIDER) def _refresh_oauth_token_for(api_key, oauth_provider): - if oauth_provider != "google": + oauth_provider = oauth_provider or "" + if not oauth_provider.startswith("google"): return api_key - token_path = os.path.join(os.path.expanduser("~"), ".cache", "codex-proxy", "google-oauth-token.json") + token_name = "google-antigravity-oauth-token.json" if oauth_provider == "google-antigravity" else "google-cli-oauth-token.json" + token_path = os.path.join(os.path.expanduser("~"), ".cache", "codex-proxy", token_name) if not os.path.exists(token_path): return api_key try: @@ -329,6 +400,70 @@ _CROF_ADAPTIVE = { "min_keep_recent": 4, } +_BGP_STATS_PATH = os.path.join(_LOG_DIR, "bgp-route-stats.json") +_bgp_stats_lock = threading.Lock() + +def _route_key(route): + return f"{route.get('name', '')}::{route.get('target_url', '')}::{route.get('model', '')}" + +def _load_bgp_stats(): + try: + if os.path.exists(_BGP_STATS_PATH): + return json.load(open(_BGP_STATS_PATH)) + except Exception: + pass + return {} + +def _save_bgp_stats(stats): + tmp = _BGP_STATS_PATH + ".tmp" + with open(tmp, "w") as f: + json.dump(stats, f, indent=2) + os.replace(tmp, _BGP_STATS_PATH) + +def _score_route(route, stats): + key = _route_key(route) + rs = stats.get(key, {}) + now = time.time() + if float(rs.get("open_until_ts", 0)) > now: + return 1_000_000 + priority = int(route.get("priority", 99)) + ewma = float(rs.get("ewma_latency_s", 0)) + failures = int(rs.get("consecutive_failures", 0)) + score = priority + min(ewma * 5, 50) + failures * 20 + if float(rs.get("rate_limited_until", 0)) > now: + score += 500 + return score + +def _update_route_stats(route, success, duration_s, http_code=None, error_type=None): + with _bgp_stats_lock: + stats = _load_bgp_stats() + key = _route_key(route) + rs = stats.setdefault(key, { + "ewma_latency_s": duration_s, "consecutive_failures": 0, + "last_success": None, "last_failure": None, + "open_until_ts": 0, "rate_limited_until": 0, "last_error": None, + }) + alpha = 0.25 + rs["ewma_latency_s"] = alpha * duration_s + (1 - alpha) * float(rs.get("ewma_latency_s", duration_s)) + if success: + rs["consecutive_failures"] = 0 + rs["last_success"] = time.time() + else: + rs["consecutive_failures"] = int(rs.get("consecutive_failures", 0)) + 1 + rs["last_failure"] = time.time() + rs["last_error"] = error_type or (f"http_{http_code}" if http_code else "unknown") + if http_code == 429: + rs["rate_limited_until"] = time.time() + 120 + if rs["consecutive_failures"] >= 3: + rs["open_until_ts"] = time.time() + 60 + rs["consecutive_failures"] = 0 + _save_bgp_stats(stats) + +def _sorted_bgp_routes(): + with _bgp_stats_lock: + stats = _load_bgp_stats() + return sorted(BGP_ROUTES, key=lambda r: _score_route(r, stats)) + def _crof_record(model, n_items, success): if not isinstance(n_items, int) or n_items < 1: return @@ -536,6 +671,193 @@ def _compact_input(input_data): print(f"[compact] {len(input_data)} items -> {len(head) + 1 + len(tail)} (compacted {len(body)} old items into summary)", file=sys.stderr) return head + [summary_msg] + tail +# ═══════════════════════════════════════════════════════════════════ +# Provider policies +# ═══════════════════════════════════════════════════════════════════ + +_PROVIDER_POLICIES = { + "crof": {"reasoning_mode": "off", "max_tokens": 32768, "strip_reasoning": True, + "tool_output_limit": 4000, "max_input_items": 18, "compaction": "aggressive"}, + "chats-llm": {"reasoning_mode": "off", "max_tokens": 32768, "strip_reasoning": True, + "tool_output_limit": 4000, "max_input_items": 20, "compaction": "aggressive"}, + "z.ai": {"reasoning_mode": "medium", "max_tokens": 65536, "strip_reasoning": True, + "tool_output_limit": 8000, "max_input_items": 40, "compaction": "balanced"}, + "openrouter": {"reasoning_mode": "provider_default", "max_tokens": 32768, "strip_reasoning": True, + "tool_output_limit": 6000, "max_input_items": 35, "compaction": "balanced"}, + "openadapter": {"reasoning_mode": "off", "max_tokens": 32768, "strip_reasoning": True, + "tool_output_limit": 6000, "max_input_items": 30, "compaction": "balanced"}, +} + +def provider_policy(target_url=None, backend=None): + host = urllib.parse.urlparse(target_url or TARGET_URL).netloc.lower() + for key, policy in _PROVIDER_POLICIES.items(): + if key in host: + return policy + return {} + +# ═══════════════════════════════════════════════════════════════════ +# Adaptive context compaction (model-aware) +# ═══════════════════════════════════════════════════════════════════ + +_MODEL_CONTEXT = { + "gpt-4o": 128000, "gpt-4o-mini": 128000, "gpt-5": 128000, + "claude-sonnet": 200000, "claude-haiku": 200000, + "glm-5.1": 128000, "glm-5": 128000, "glm-4": 128000, + "deepseek": 64000, "gemini-2.5-flash": 1000000, "gemini-2.5-pro": 2000000, + "mimo": 32768, "minimax": 32768, "kimi": 128000, + "_default": 32768, +} + +def _context_limit_for_model(model): + if not model: + return _MODEL_CONTEXT["_default"] + ml = model.lower() + for key, limit in _MODEL_CONTEXT.items(): + if key != "_default" and key in ml: + return limit + return _MODEL_CONTEXT["_default"] + +def _estimate_tokens(obj): + if obj is None: + return 0 + if isinstance(obj, str): + return max(1, len(obj) // 4) + try: + raw = json.dumps(obj, ensure_ascii=False) + except Exception: + raw = str(obj) + return max(1, len(raw) // 4) + +def _adaptive_compact(input_data, model, policy=None): + policy = policy or {} + context_size = int(policy.get("context_size", _context_limit_for_model(model))) + input_budget = int(context_size * 0.60) + estimated = _estimate_tokens(input_data) + if estimated <= input_budget: + return input_data, False + if not isinstance(input_data, list): + return input_data, False + reduction = max(0.15, input_budget / max(estimated, 1)) + target_items = max(int(len(input_data) * reduction), 6) + if target_items >= len(input_data): + return input_data, False + head_end = 0 + for i, item in enumerate(input_data): + t = item.get("type") + if t == "message" and item.get("role") in ("developer", "system"): + head_end = i + 1 + elif t == "message" and item.get("role") == "user" and head_end == i: + head_end = i + 1 + else: + break + head = input_data[:head_end] + keep = max(4, target_items // 3) + tail_start = max(head_end, len(input_data) - keep) + while tail_start > head_end: + t = input_data[tail_start].get("type") + if t in ("function_call_output", "function_call"): + tail_start -= 1 + elif t == "message" and input_data[tail_start].get("role") == "assistant": + tail_start -= 1 + else: + break + tail = input_data[tail_start:] + body = input_data[head_end:tail_start] + if not body: + return head + tail, True + summary_lines = [f"[Auto-compacted: {len(body)} turns removed (budget={input_budget}tok, model={model})]"] + for item in body[-5:]: + summary_lines.append(_item_summary(item, max_len=120)) + summary_msg = {"type": "message", "role": "user", + "content": [{"type": "input_text", "text": "\n".join(summary_lines)}]} + print(f"[adaptive-compact] model={model} est={estimated}tok budget={input_budget}tok " + f"items {len(input_data)}->{len(head)+1+len(tail)}", file=sys.stderr) + return head + [summary_msg] + tail, True + +# ═══════════════════════════════════════════════════════════════════ +# Tool-call pairing validator +# ═══════════════════════════════════════════════════════════════════ + +def validate_tool_pairs(input_items): + if not isinstance(input_items, list): + return [] + calls = {} + errors = [] + for idx, item in enumerate(input_items): + t = item.get("type") + if t == "function_call": + cid = item.get("call_id") or item.get("id") + if cid: + calls[cid] = idx + elif t == "function_call_output": + cid = item.get("call_id") or item.get("id") + if not cid or cid not in calls: + errors.append({"index": idx, "call_id": cid, "error": "orphan_function_call_output"}) + return errors + +def repair_orphan_tool_outputs(input_items, errors): + bad = {e["index"] for e in errors} + repaired = [] + for idx, item in enumerate(input_items): + if idx in bad: + output = item.get("output", "") + repaired.append({"type": "message", "role": "user", + "content": [{"type": "input_text", + "text": f"[Proxy: unmatched tool output]\n{str(output)[:4000]}"}]}) + else: + repaired.append(item) + return repaired + +# ═══════════════════════════════════════════════════════════════════ +# Log redaction +# ═══════════════════════════════════════════════════════════════════ + +_SECRET_PATTERNS = [ + (r"sk-[A-Za-z0-9_\-]{20,}", "[REDACTED:key]"), + (r"sk-ant-[A-Za-z0-9_\-]{20,}", "[REDACTED:anthropic]"), + (r"gh[pousr]_[A-Za-z0-9_]{20,}", "[REDACTED:github]"), + (r"Bearer\s+[A-Za-z0-9._\-]{20,}", "Bearer [REDACTED]"), +] + +def _redact(text): + if not text: + return text + import re + for pattern, replacement in _SECRET_PATTERNS: + text = re.sub(pattern, replacement, text) + return text + +# ═══════════════════════════════════════════════════════════════════ +# Rate-limit token buckets +# ═══════════════════════════════════════════════════════════════════ + +class TokenBucket: + def __init__(self, capacity=10, refill=1.0): + self.capacity = float(capacity) + self.tokens = float(capacity) + self.refill = float(refill) + self.updated = time.monotonic() + self.lock = threading.Lock() + def allow(self, cost=1): + with self.lock: + now = time.monotonic() + self.tokens = min(self.capacity, self.tokens + (now - self.updated) * self.refill) + self.updated = now + if self.tokens >= cost: + self.tokens -= cost + return True + return False + +_rate_buckets = {} +_rate_buckets_lock = threading.Lock() + +def _bucket_for_route(route): + name = route.get("name") or route.get("target_url") or "default" + with _rate_buckets_lock: + if name not in _rate_buckets: + _rate_buckets[name] = TokenBucket(capacity=10, refill=1.0) + return _rate_buckets[name] + # ═══════════════════════════════════════════════════════════════════ # OpenAI-compat backend # ═══════════════════════════════════════════════════════════════════ @@ -1154,14 +1476,31 @@ class Handler(http.server.BaseHTTPRequestHandler): self._handle_anthropic(body, model, stream) elif BACKEND == "command-code": self._handle_command_code(body, model, stream) + elif (BACKEND or "").startswith("gemini-oauth"): + self._handle_gemini_oauth(body, model, stream) else: self._handle_openai_compat(body, model, stream) def _handle_openai_compat(self, body, model, stream): input_data = body.get("input", "") + policy = provider_policy() + + pair_errors = validate_tool_pairs(input_data) + if pair_errors: + print(f"[tool-validator] repairing {len(pair_errors)} orphan tool outputs", file=sys.stderr) + input_data = repair_orphan_tool_outputs(input_data, pair_errors) + body = dict(body) + body["input"] = input_data + + compacted = False + if policy.get("compaction") and isinstance(input_data, list): + input_data, compacted = _adaptive_compact(input_data, model, policy) + if compacted: + body = dict(body) + body["input"] = input_data crof_limit = _crof_item_limit(model) - if isinstance(input_data, list) and len(input_data) > crof_limit: + if not compacted and isinstance(input_data, list) and len(input_data) > crof_limit: print(f"[crof-adaptive] proactive compact: {len(input_data)} items > limit {crof_limit}", file=sys.stderr) input_data = _crof_compact_for_retry(input_data, model) body = dict(body) @@ -1228,8 +1567,379 @@ class Handler(http.server.BaseHTTPRequestHandler): chat_body["reasoning_effort"] = REASONING_EFFORT return chat_body + def _handle_gemini_oauth(self, body, model, stream): + input_data = body.get("input", "") + policy = provider_policy() + if OAUTH_PROVIDER == "google-antigravity": + alias_map = { + "antigravity-gemini-3-flash": "gemini-3-flash", + "antigravity-gemini-3-pro": "gemini-3-pro-low", + "antigravity-gemini-3.1-pro": "gemini-3.1-pro-low", + "gemini-3-flash-preview": "gemini-3-flash", + "gemini-3-pro-preview": "gemini-3-pro-low", + "gemini-3.1-pro-preview": "gemini-3.1-pro-low", + "gemini-3-pro": "gemini-3-pro-low", + "gemini-3.1-pro": "gemini-3.1-pro-low", + "antigravity-claude-sonnet-4-6": "claude-sonnet-4-6", + "antigravity-claude-opus-4-6-thinking": "claude-opus-4-6-thinking", + } + model = alias_map.get(model, model) + + pair_errors = validate_tool_pairs(input_data) + if pair_errors: + input_data = repair_orphan_tool_outputs(input_data, pair_errors) + body = dict(body) + body["input"] = input_data + + compacted = False + if policy.get("compaction") and isinstance(input_data, list): + input_data, compacted = _adaptive_compact(input_data, model, policy) + if compacted: + body = dict(body) + body["input"] = input_data + + access_token = _refresh_oauth_token() + token_name = "google-antigravity-oauth-token.json" if OAUTH_PROVIDER == "google-antigravity" else "google-cli-oauth-token.json" + token_path = os.path.join(os.path.expanduser("~"), ".cache", "codex-proxy", token_name) + project_id = "" + try: + with open(token_path) as f: + project_id = json.load(f).get("project_id", "") + except Exception: + pass + + contents = [] + system_parts = [] + instructions = body.get("instructions", "").strip() + tool_call_names = {} + + 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", "") + if isinstance(content, list): + parts = [] + for c in content: + ct = c.get("type") + if ct == "input_text": + parts.append({"text": c.get("text", "")}) + elif ct == "text": + parts.append({"text": c.get("text", "")}) + elif ct == "input_image" or ct == "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)}) + if parts: + contents.append({"role": role, "parts": parts}) + elif isinstance(content, str): + contents.append({"role": role, "parts": [{"text": content}]}) + 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 = {} + contents.append({"role": "model", "parts": [{"functionCall": {"name": fname, "args": args, "id": call_id}, "thoughtSignature": "skip_thought_signature_validator"}]}) + 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, "") + try: + output_parsed = json.loads(output) if isinstance(output, str) else output + except Exception: + output_parsed = output + resp_part = {"functionResponse": {"name": fname or "unknown", "response": {"result": output_parsed if isinstance(output_parsed, (dict, list)) else output}}} + if call_id: + resp_part["functionResponse"]["id"] = call_id + contents.append({"role": "user", "parts": [resp_part]}) + + if OAUTH_PROVIDER.startswith("google"): + 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 + text_key = "\n".join([p.get("text", "") for p in parts if "text" in p]).strip() + if role == "user" and text_key and text_key == last_user_text: + continue + if role == last_role and role in ("user", "model") and sanitized: + sanitized[-1].setdefault("parts", []).extend(parts) + else: + 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) + while sanitized and sanitized[-1].get("role") != "user": + sanitized.pop() + contents = sanitized + + if instructions: + system_parts.append({"text": instructions}) + if OAUTH_PROVIDER == "google-antigravity": + system_parts.append({"text": ( + "You are connected through a Responses API translation proxy. " + "If tools are available and the user's request requires changing files, call the appropriate tool immediately. " + "Do not announce plans, do not say you will list files, browse, fetch, inspect, or start by exploring unless you are emitting the actual tool call in the same response. " + "For file creation requests, use tools to create or modify the file instead of only printing code in chat. " + "If no suitable tool is available, answer directly with the complete result. " + "Never answer only with a plan such as 'I will start by...' or 'I am going to...'." + )}) + + gen_config = {} + mot = body.get("max_output_tokens", 0) + if mot: + gen_config["maxOutputTokens"] = mot + 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"] + + if REASONING_ENABLED and REASONING_EFFORT != "none": + 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}] + + request_body = {"contents": contents} + 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 + + wrapped = { + "project": project_id, + "model": model, + "request": request_body, + } + if OAUTH_PROVIDER == "google-antigravity": + wrapped["requestType"] = "agent" + wrapped["userAgent"] = "antigravity" + wrapped["requestId"] = f"agent-{uuid.uuid4().hex[:12]}" + + endpoints = ([ + "https://daily-cloudcode-pa.sandbox.googleapis.com", + "https://autopush-cloudcode-pa.sandbox.googleapis.com", + "https://cloudcode-pa.googleapis.com", + ] if OAUTH_PROVIDER == "google-antigravity" else [ + "https://cloudcode-pa.googleapis.com", + ]) + action = "streamGenerateContent" if stream else "generateContent" + url_suffix = f"v1internal:{action}?alt=sse" if stream else f"v1internal:{action}" + + headers = { + "Content-Type": "application/json", + "Authorization": f"Bearer {access_token}", + } + if OAUTH_PROVIDER == "google-antigravity": + version = _ensure_antigravity_version() + headers["User-Agent"] = f"antigravity/{version} darwin/arm64" + else: + headers["User-Agent"] = "google-api-nodejs-client/9.15.1" + headers["X-Goog-Api-Client"] = "gl-node/22.17.0" + headers["Client-Metadata"] = "ideType=IDE_UNSPECIFIED,platform=PLATFORM_UNSPECIFIED,pluginType=GEMINI" + body_b = json.dumps(wrapped).encode() + print(f"[gemini-oauth] model={model} stream={stream} items={len(input_data) if isinstance(input_data, list) else 1} project={project_id}", file=sys.stderr) + + for ep in endpoints: + 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)) + break + except urllib.error.HTTPError as e: + err_body = e.read().decode() + if e.code == 400 and OAUTH_PROVIDER.startswith("google"): + try: + debug_path = os.path.join(_LOG_DIR, "gemini-last-400-request.json") + with open(debug_path, "w") as dbg: + json.dump({"endpoint": ep, "model": model, "wrapped": wrapped, "error": err_body}, dbg, indent=2) + print(f"[gemini-oauth] saved 400 debug request to {debug_path}", file=sys.stderr) + except Exception: + pass + if e.code == 429 and ep != endpoints[-1]: + print(f"[gemini-oauth] {ep} HTTP 429, trying next endpoint", file=sys.stderr) + continue + return self.send_json(e.code, {"error": {"type": "upstream_error", "message": err_body}}) + except Exception as e: + if ep == endpoints[-1]: + return self.send_json(502, {"error": {"type": "proxy_error", "message": str(e)}}) + print(f"[gemini-oauth] {ep} failed: {e}, trying next", file=sys.stderr) + continue + + if stream: + self._forward_gemini_sse(upstream, model, body, input_data) + else: + self._forward_gemini_json(upstream, model, body, input_data) + + def _forward_gemini_sse(self, upstream, model, body, input_data): + resp_id = f"resp-{uuid.uuid4().hex[:24]}" + created = int(time.time()) + self.send_response(200) + self.send_header("Content-Type", "text/event-stream") + self.send_header("Cache-Control", "no-cache") + self.send_header("Connection", "keep-alive") + self.end_headers() + + full_text = "" + output_items = [] + current_tool_calls = {} + message_started = False + message_id = f"msg-{uuid.uuid4().hex[:24]}" + + def flush_event(event_type, data): + self.wfile.write(f"event: {event_type}\ndata: {json.dumps(data)}\n\n".encode()) + self.wfile.flush() + + flush_event("response.created", {"type": "response.created", "response": {"id": resp_id, "object": "response", "model": model, "status": "in_progress", "created": created, "output": []}}) + flush_event("response.in_progress", {"type": "response.in_progress", "response": {"id": resp_id}}) + + buf = "" + stream_finished = False + for raw_line in upstream: + 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"[gemini-oauth] 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"[gemini-oauth] finish without parts: {candidates[0].get('finishReason')}", file=sys.stderr) + parts = candidates[0].get("content", {}).get("parts", []) + for part in parts: + if part.get("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}) + if OAUTH_PROVIDER == "google-antigravity" and full_text and candidates[0].get("finishReason"): + stream_finished = True + break + + out = [] + if not full_text and not current_tool_calls: + print("[gemini-oauth] WARNING: completed with empty output", file=sys.stderr) + if full_text: + out.append({"type": "message", "id": message_id, "role": "assistant", "content": [{"type": "output_text", "text": full_text}]}) + tool_outputs = [] + for cid, fc in current_tool_calls.items(): + tool_outputs.append({"type": "function_call", "id": cid, "call_id": cid, "name": fc.get("name", ""), "arguments": json.dumps(fc.get("args", fc.get("arguments", {})))}) + out.extend(tool_outputs) + + final_resp = {"id": resp_id, "object": "response", "model": model, "status": "completed", "created": created, "output": out} + if full_text: + flush_event("response.output_text.done", {"type": "response.output_text.done", "output_index": 0, "content_index": 0, "text": full_text}) + flush_event("response.content_part.done", {"type": "response.content_part.done", "output_index": 0, "content_index": 0, "part": {"type": "output_text", "text": full_text}}) + flush_event("response.output_item.done", {"type": "response.output_item.done", "output_index": 0, "item": out[0]}) + for idx, item in enumerate(tool_outputs, start=(1 if full_text else 0)): + flush_event("response.output_item.done", {"type": "response.output_item.done", "output_index": idx, "item": item}) + flush_event("response.completed", {"type": "response.completed", "response": final_resp}) + self.close_connection = True + + with _response_store_lock: + _response_store[resp_id] = final_resp + while len(_response_store) > _MAX_STORED: + _response_store.popitem(last=False) + + def _forward_gemini_json(self, upstream, model, body, input_data): + data = json.loads(upstream.read().decode()) + resp_id = f"resp-{uuid.uuid4().hex[:24]}" + created = int(time.time()) + out = [] + full_text = "" + candidates = data.get("response", data).get("candidates", []) + if candidates: + parts = candidates[0].get("content", {}).get("parts", []) + text_parts = [] + for part in parts: + if part.get("thought"): + continue + if "text" in part and not part.get("functionCall"): + text_parts.append(part["text"]) + elif part.get("functionCall"): + fc = part["functionCall"] + call_id = f"call_{uuid.uuid4().hex[:24]}" + out.append({"type": "function_call", "id": call_id, "call_id": call_id, "name": fc.get("name", ""), "arguments": json.dumps(fc.get("args", fc.get("arguments", {})))}) + if text_parts: + full_text = "".join(text_parts) + out.insert(0, {"type": "message", "id": f"msg-{uuid.uuid4().hex[:24]}", "role": "assistant", "content": [{"type": "output_text", "text": full_text}]}) + resp = {"id": resp_id, "object": "response", "model": model, "status": "completed", "created": created, "output": out} + with _response_store_lock: + _response_store[resp_id] = resp + while len(_response_store) > _MAX_STORED: + _response_store.popitem(last=False) + self.send_json(200, resp) + def _handle_bgp(self, body, model, stream, messages, input_data): - routes = sorted(BGP_ROUTES, key=lambda r: r.get("priority", 99)) + routes = _sorted_bgp_routes() + routes = [r for r in routes if _bucket_for_route(r).allow()] + if not routes: + return self.send_json(503, {"error": {"type": "bgp_rate_limited", "message": "All routes rate-limited"}}) errors = [] for route in routes: r_model = route.get("model", model) @@ -1266,11 +1976,13 @@ class Handler(http.server.BaseHTTPRequestHandler): }, browser_ua=True) print(f"[bgp] trying route '{route.get('name', r_url)}' model={r_model}", file=sys.stderr) req = urllib.request.Request(target, data=json.dumps(chat_body).encode(), headers=fwd) + t0_route = time.time() route_ok = False for attempt in range(3): try: upstream = urllib.request.urlopen(req, timeout=_upstream_timeout(body, stream)) print(f"[bgp] route '{route.get('name', r_url)}' connected OK", file=sys.stderr) + _update_route_stats(route, True, time.time() - t0_route) self._forward_oa_compat(upstream, stream, r_model, chat_body, body, input_data, fwd, target) return except urllib.error.HTTPError as e: @@ -1282,6 +1994,7 @@ class Handler(http.server.BaseHTTPRequestHandler): req = urllib.request.Request(target, data=json.dumps(chat_body).encode(), headers=fwd) continue print(f"[bgp] route '{route.get('name', r_url)}' FAILED: HTTP {e.code}: {err[:200]}", file=sys.stderr) + _update_route_stats(route, False, time.time() - t0_route, http_code=e.code) errors.append(f"{route.get('name','?')}: HTTP {e.code}") break except (ConnectionResetError, ConnectionAbortedError, BrokenPipeError) as e: @@ -1291,10 +2004,12 @@ class Handler(http.server.BaseHTTPRequestHandler): time.sleep(wait) req = urllib.request.Request(target, data=json.dumps(chat_body).encode(), headers=fwd) continue + _update_route_stats(route, False, time.time() - t0_route, error_type=str(e)) errors.append(f"{route.get('name','?')}: {e}") break except Exception as e: print(f"[bgp] route '{route.get('name', r_url)}' FAILED: {e}", file=sys.stderr) + _update_route_stats(route, False, time.time() - t0_route, error_type=str(e)) errors.append(f"{route.get('name','?')}: {e}") break