diff --git a/core/framework/config.py b/core/framework/config.py index 27125168..bdf80099 100644 --- a/core/framework/config.py +++ b/core/framework/config.py @@ -116,6 +116,16 @@ def get_worker_api_key() -> str | None: except ImportError: pass + if worker_llm.get("use_antigravity_subscription"): + try: + from framework.runner.runner import get_antigravity_token + + token = get_antigravity_token() + if token: + return token + except ImportError: + pass + api_key_env_var = worker_llm.get("api_key_env_var") if api_key_env_var: return os.environ.get(api_key_env_var) @@ -134,6 +144,8 @@ def get_worker_api_base() -> str | None: return "https://chatgpt.com/backend-api/codex" if worker_llm.get("use_kimi_code_subscription"): return "https://api.kimi.com/coding" + if worker_llm.get("use_antigravity_subscription"): + return "http://localhost:8069/v1" if worker_llm.get("api_base"): return worker_llm["api_base"] if str(worker_llm.get("provider", "")).lower() == "openrouter": @@ -251,6 +263,17 @@ def get_api_key() -> str | None: except ImportError: pass + # Antigravity subscription: read OAuth token from accounts JSON + if llm.get("use_antigravity_subscription"): + try: + from framework.runner.runner import get_antigravity_token + + token = get_antigravity_token() + if token: + return token + except ImportError: + pass + # Standard env-var path (covers ZAI Code and all API-key providers) api_key_env_var = llm.get("api_key_env_var") if api_key_env_var: @@ -280,6 +303,9 @@ def get_api_base() -> str | None: if llm.get("use_kimi_code_subscription"): # Kimi Code uses an Anthropic-compatible endpoint (no /v1 suffix). return "https://api.kimi.com/coding" + if llm.get("use_antigravity_subscription"): + # Antigravity routes through a local OpenAI-compatible proxy. + return "http://localhost:8069/v1" if llm.get("api_base"): return llm["api_base"] if str(llm.get("provider", "")).lower() == "openrouter": diff --git a/core/framework/llm/litellm.py b/core/framework/llm/litellm.py index 2999a41f..7697cdd8 100644 --- a/core/framework/llm/litellm.py +++ b/core/framework/llm/litellm.py @@ -525,6 +525,8 @@ class LiteLLMProvider(LLMProvider): self._codex_backend = bool( self.api_base and "chatgpt.com/backend-api/codex" in self.api_base ) + # Antigravity routes through a local OpenAI-compatible proxy — no patches needed. + self._antigravity = bool(self.api_base and "localhost:8069" in self.api_base) if litellm is None: raise ImportError( diff --git a/core/framework/runner/runner.py b/core/framework/runner/runner.py index 7dc0c5ba..57e41cdf 100644 --- a/core/framework/runner/runner.py +++ b/core/framework/runner/runner.py @@ -552,6 +552,319 @@ def get_kimi_code_token() -> str | None: return None +# --------------------------------------------------------------------------- +# Antigravity subscription token helpers +# --------------------------------------------------------------------------- + +# Antigravity IDE (native macOS/Linux app) stores OAuth tokens in its +# VSCode-style SQLite state database under the key +# "antigravityUnifiedStateSync.oauthToken" as a base64-encoded protobuf blob. +ANTIGRAVITY_IDE_STATE_DB = ( + Path.home() + / "Library" + / "Application Support" + / "Antigravity" + / "User" + / "globalStorage" + / "state.vscdb" +) +# Linux fallback for the IDE state DB +ANTIGRAVITY_IDE_STATE_DB_LINUX = ( + Path.home() / ".config" / "Antigravity" / "User" / "globalStorage" / "state.vscdb" +) +# antigravity-auth CLI tool stores credentials in JSON files +ANTIGRAVITY_AUTH_FILE = Path.home() / ".config" / "opencode" / "antigravity-accounts.json" +ANTIGRAVITY_AUTH_FILE_FALLBACK = Path.home() / ".config" / "antigravity_auth" / "accounts.json" + +ANTIGRAVITY_OAUTH_TOKEN_URL = "https://oauth2.googleapis.com/token" +ANTIGRAVITY_OAUTH_CLIENT_ID = ( + "1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com" +) +_ANTIGRAVITY_TOKEN_LIFETIME_SECS = 3600 # Google access tokens expire in 1 hour +_ANTIGRAVITY_IDE_STATE_DB_KEY = "antigravityUnifiedStateSync.oauthToken" + + +def _read_antigravity_ide_credentials() -> dict | None: + """Read credentials from the Antigravity IDE's SQLite state database. + + The Antigravity desktop IDE (VSCode-based) stores its OAuth token as a + base64-encoded protobuf blob in a SQLite database. The access token is + a standard Google OAuth ``ya29.*`` bearer token. + + Returns: + Dict with ``accessToken`` and optionally ``refreshToken`` keys, + plus ``_source: "ide"`` to skip file-based save on refresh. + Returns None if the database is absent or the key is not found. + """ + import re + import sqlite3 + + for db_path in (ANTIGRAVITY_IDE_STATE_DB, ANTIGRAVITY_IDE_STATE_DB_LINUX): + if not db_path.exists(): + continue + try: + con = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True) + try: + row = con.execute( + "SELECT value FROM ItemTable WHERE key = ?", + (_ANTIGRAVITY_IDE_STATE_DB_KEY,), + ).fetchone() + finally: + con.close() + + if not row: + continue + + import base64 + + blob = base64.b64decode(row[0]) + + # The protobuf blob contains the access token (ya29.*) and + # refresh token (1//*) as length-prefixed UTF-8 strings. + # Decode the inner base64 layer and extract with regex. + inner_b64_candidates = re.findall(rb"[A-Za-z0-9+/=_\-]{40,}", blob) + access_token: str | None = None + refresh_token: str | None = None + for candidate in inner_b64_candidates: + try: + padded = candidate + b"=" * (-len(candidate) % 4) + inner = base64.urlsafe_b64decode(padded) + except Exception: + continue + if not access_token: + m = re.search(rb"ya29\.[A-Za-z0-9_\-\.]+", inner) + if m: + access_token = m.group(0).decode("ascii") + if not refresh_token: + m = re.search(rb"1//[A-Za-z0-9_\-\.]+", inner) + if m: + refresh_token = m.group(0).decode("ascii") + if access_token and refresh_token: + break + + if access_token: + return { + "accounts": [ + { + "accessToken": access_token, + "refreshToken": refresh_token or "", + } + ], + "_source": "ide", + "_db_path": str(db_path), + } + except Exception as exc: + logger.debug("Failed to read Antigravity IDE state DB: %s", exc) + continue + + return None + + +def _read_antigravity_credentials() -> dict | None: + """Read Antigravity auth data from all supported credential sources. + + Checks in order: + 1. Antigravity IDE SQLite state database (native macOS/Linux app) + 2. antigravity-auth CLI JSON file (~/.config/opencode/antigravity-accounts.json) + 3. antigravity-auth CLI fallback (~/.config/antigravity_auth/accounts.json) + + Returns: + Auth data dict with an ``accounts`` list on success, None otherwise. + """ + # 1. Native Antigravity IDE (primary on macOS) + ide_creds = _read_antigravity_ide_credentials() + if ide_creds: + return ide_creds + + # 2 & 3. antigravity-auth CLI tool JSON files + for path in (ANTIGRAVITY_AUTH_FILE, ANTIGRAVITY_AUTH_FILE_FALLBACK): + if not path.exists(): + continue + try: + with open(path, encoding="utf-8") as f: + data = json.load(f) + accounts = data.get("accounts", []) + if accounts and isinstance(accounts[0], dict): + return data + except (json.JSONDecodeError, OSError): + continue + return None + + +def _is_antigravity_token_expired(auth_data: dict) -> bool: + """Check whether the Antigravity access token is expired or near expiry. + + For IDE-sourced credentials: uses the state DB's mtime as last_refresh + since the IDE keeps the DB fresh while it's running. + For JSON-sourced credentials: uses the ``last_refresh`` field or file mtime. + """ + import time + from datetime import datetime + + now = time.time() + + if auth_data.get("_source") == "ide": + # The IDE refreshes tokens automatically while running. + # Use the DB file's mtime as a proxy for when the token was last updated. + try: + db_path = Path(auth_data.get("_db_path", str(ANTIGRAVITY_IDE_STATE_DB))) + last_refresh: float = db_path.stat().st_mtime + except OSError: + return True + expires_at = last_refresh + _ANTIGRAVITY_TOKEN_LIFETIME_SECS + return now >= (expires_at - _TOKEN_REFRESH_BUFFER_SECS) + + last_refresh_val: float | str | None = auth_data.get("last_refresh") + if last_refresh_val is None: + try: + path = ( + ANTIGRAVITY_AUTH_FILE + if ANTIGRAVITY_AUTH_FILE.exists() + else ANTIGRAVITY_AUTH_FILE_FALLBACK + ) + last_refresh_val = path.stat().st_mtime + except OSError: + return True + elif isinstance(last_refresh_val, str): + try: + last_refresh_val = datetime.fromisoformat( + last_refresh_val.replace("Z", "+00:00") + ).timestamp() + except (ValueError, TypeError): + return True + + expires_at = float(last_refresh_val) + _ANTIGRAVITY_TOKEN_LIFETIME_SECS + return now >= (expires_at - _TOKEN_REFRESH_BUFFER_SECS) + + +def _refresh_antigravity_token(refresh_token: str) -> dict | None: + """Refresh the Antigravity access token via Google OAuth. + + POSTs form-encoded ``grant_type=refresh_token`` to the Google token + endpoint using Antigravity's public OAuth client ID. + + Returns: + Parsed response dict (containing ``access_token``) on success, + None on any error. + """ + import urllib.error + import urllib.parse + import urllib.request + + data = urllib.parse.urlencode( + { + "grant_type": "refresh_token", + "refresh_token": refresh_token, + "client_id": ANTIGRAVITY_OAUTH_CLIENT_ID, + } + ).encode("utf-8") + + req = urllib.request.Request( + ANTIGRAVITY_OAUTH_TOKEN_URL, + data=data, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + method="POST", + ) + + try: + with urllib.request.urlopen(req, timeout=15) as resp: # noqa: S310 + return json.loads(resp.read()) + except (urllib.error.URLError, json.JSONDecodeError, TimeoutError, OSError) as exc: + logger.debug("Antigravity token refresh failed: %s", exc) + return None + + +def _save_refreshed_antigravity_credentials(auth_data: dict, token_data: dict) -> None: + """Write refreshed tokens back to the Antigravity JSON credentials file. + + Skipped for IDE-sourced credentials (the IDE manages its own DB). + Updates ``accounts[0].accessToken`` (and ``refreshToken`` if present), + then persists ``last_refresh`` as an ISO-8601 UTC string. + """ + from datetime import datetime + + # IDE manages its own state — we do not write back to its SQLite DB + if auth_data.get("_source") == "ide": + return + + try: + accounts = auth_data.get("accounts", []) + if not accounts: + return + account = accounts[0] + account["accessToken"] = token_data["access_token"] + if "refresh_token" in token_data: + account["refreshToken"] = token_data["refresh_token"] + auth_data["accounts"] = accounts + auth_data["last_refresh"] = datetime.now(UTC).isoformat() + + target_path = ( + ANTIGRAVITY_AUTH_FILE + if ANTIGRAVITY_AUTH_FILE.exists() + else ANTIGRAVITY_AUTH_FILE_FALLBACK + ) + target_path.parent.mkdir(parents=True, exist_ok=True) + fd = os.open(target_path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600) + with os.fdopen(fd, "w", encoding="utf-8") as f: + json.dump(auth_data, f, indent=2) + logger.debug("Antigravity credentials refreshed and saved") + except (OSError, KeyError) as exc: + logger.debug("Failed to save refreshed Antigravity credentials: %s", exc) + + +def get_antigravity_token() -> str | None: + """Get the OAuth access token from an Antigravity subscription. + + Credential sources checked in order: + 1. Antigravity IDE SQLite state DB (native app, macOS/Linux) + 2. antigravity-auth CLI JSON file + + For IDE credentials the token is read directly (the IDE refreshes it + automatically while running). For JSON credentials an automatic OAuth + refresh is attempted when the token is near expiry. + + Returns: + The ``ya29.*`` Google OAuth access token, or None if unavailable. + """ + auth_data = _read_antigravity_credentials() + if not auth_data: + return None + + accounts = auth_data.get("accounts", []) + if not accounts: + return None + account = accounts[0] + + access_token = account.get("accessToken") + if not access_token: + return None + + if not _is_antigravity_token_expired(auth_data): + return access_token + + # Token is expired or near expiry — attempt a refresh + refresh_token = account.get("refreshToken") + if not refresh_token: + logger.warning( + "Antigravity token expired and no refresh token available. " + "Re-open the Antigravity IDE to refresh, or run 'antigravity-auth accounts add'." + ) + return access_token # return stale token; proxy may still accept it briefly + + logger.info("Antigravity token expired or near expiry, refreshing...") + token_data = _refresh_antigravity_token(refresh_token) + + if token_data and "access_token" in token_data: + _save_refreshed_antigravity_credentials(auth_data, token_data) + return token_data["access_token"] + + logger.warning( + "Antigravity token refresh failed. " + "Re-open the Antigravity IDE or run 'antigravity-auth accounts add'." + ) + return access_token + + @dataclass class AgentInfo: """Information about an exported agent.""" @@ -1158,6 +1471,7 @@ class AgentRunner: use_claude_code = llm_config.get("use_claude_code_subscription", False) use_codex = llm_config.get("use_codex_subscription", False) use_kimi_code = llm_config.get("use_kimi_code_subscription", False) + use_antigravity = llm_config.get("use_antigravity_subscription", False) api_base = llm_config.get("api_base") api_key = None @@ -1179,6 +1493,12 @@ class AgentRunner: if not api_key: print("Warning: Kimi Code subscription configured but no key found.") print("Run 'kimi /login' to authenticate, then try again.") + elif use_antigravity: + # Get OAuth token from Antigravity subscription (Google OAuth) + api_key = get_antigravity_token() + if not api_key: + print("Warning: Antigravity subscription configured but no token found.") + print("Run 'antigravity-auth accounts add' to authenticate, then try again.") if api_key and use_claude_code: # Use litellm's built-in Anthropic OAuth support. @@ -1217,6 +1537,15 @@ class AgentRunner: api_key=api_key, api_base=api_base, ) + elif api_key and use_antigravity: + # Antigravity routes through a local OpenAI-compatible proxy + # started with 'antigravity-auth serve' on localhost:8069. + # No special headers required — the proxy handles auth internally. + self._llm = LiteLLMProvider( + model=self.model, + api_key=api_key, + api_base="http://localhost:8069/v1", + ) else: # Local models (e.g. Ollama) don't need an API key if self._is_local_model(self.model): diff --git a/core/tests/test_antigravity_eventloop.py b/core/tests/test_antigravity_eventloop.py new file mode 100644 index 00000000..64aec3ed --- /dev/null +++ b/core/tests/test_antigravity_eventloop.py @@ -0,0 +1,174 @@ +"""Integration test: Run a real EventLoopNode against the Antigravity backend. + +Run: .venv/bin/python core/tests/test_antigravity_eventloop.py + +Requires: + - ~/.config/opencode/antigravity-accounts.json with valid credentials + (run 'antigravity-auth accounts add' to authenticate) + - antigravity-auth serve running on localhost:8069 + (run 'antigravity-auth serve' in a separate terminal) +""" + +import asyncio +import logging +import sys +from unittest.mock import MagicMock + +sys.path.insert(0, "core") + +logging.basicConfig(level=logging.WARNING, format="%(levelname)s %(name)s: %(message)s") +# Show our provider's retry/stream logs +logging.getLogger("framework.llm.litellm").setLevel(logging.DEBUG) + +from framework.config import RuntimeConfig # noqa: E402 +from framework.graph.event_loop_node import EventLoopNode, LoopConfig # noqa: E402 +from framework.graph.node import NodeContext, NodeResult, NodeSpec, SharedMemory # noqa: E402 +from framework.llm.litellm import LiteLLMProvider # noqa: E402 + + +def make_provider() -> LiteLLMProvider: + cfg = RuntimeConfig() + if not cfg.api_key: + print("ERROR: No Antigravity token found.") + print(" 1. Run 'antigravity-auth accounts add' to authenticate.") + print(" 2. Run 'antigravity-auth serve' to start the local proxy.") + print(" 3. Configure Hive: run quickstart.sh and select option 7 (Antigravity).") + sys.exit(1) + print(f"Model : {cfg.model}") + print(f"Base : {cfg.api_base}") + print(f"Antigravity : {'localhost:8069' in (cfg.api_base or '')}") + return LiteLLMProvider( + model=cfg.model, + api_key=cfg.api_key, + api_base=cfg.api_base, + **cfg.extra_kwargs, + ) + + +def make_context( + llm: LiteLLMProvider, + *, + node_id: str = "test", + system_prompt: str = "You are a helpful assistant.", + output_keys: list[str] | None = None, +) -> NodeContext: + if output_keys is None: + output_keys = ["answer"] + + spec = NodeSpec( + id=node_id, + name="Test Node", + description="Integration test node", + node_type="event_loop", + output_keys=output_keys, + system_prompt=system_prompt, + ) + + runtime = MagicMock() + runtime.start_run = MagicMock(return_value="run-1") + runtime.decide = MagicMock(return_value="dec-1") + runtime.record_outcome = MagicMock() + runtime.end_run = MagicMock() + + memory = SharedMemory() + + return NodeContext( + runtime=runtime, + node_id=node_id, + node_spec=spec, + memory=memory, + input_data={}, + llm=llm, + available_tools=[], + max_tokens=4096, + ) + + +async def run_test( + name: str, llm: LiteLLMProvider, system: str, output_keys: list[str] +) -> NodeResult: + print(f"\n{'=' * 60}") + print(f"TEST: {name}") + print(f"{'=' * 60}") + + ctx = make_context(llm, system_prompt=system, output_keys=output_keys) + node = EventLoopNode(config=LoopConfig(max_iterations=3)) + + try: + result = await node.execute(ctx) + print(f" Success : {result.success}") + print(f" Output : {result.output}") + if result.error: + print(f" Error : {result.error}") + return result + except Exception as e: + print(f" EXCEPTION: {type(e).__name__}: {e}") + import traceback + + traceback.print_exc() + return NodeResult(success=False, error=str(e)) + + +async def main(): + llm = make_provider() + print() + + # Test 1: Simple text output — the node should call set_output to fill "answer" + r1 = await run_test( + name="Simple text generation", + llm=llm, + system=( + "You are a helpful assistant. When asked a question, use the " + "set_output tool to store your answer in the 'answer' key. " + "Keep answers short (1-2 sentences)." + ), + output_keys=["answer"], + ) + + # Test 2: If test 1 failed, try bare stream() to isolate the issue + if not r1.success: + print(f"\n{'=' * 60}") + print("FALLBACK: Testing bare provider.stream() directly") + print(f"{'=' * 60}") + try: + from framework.llm.stream_events import ( + FinishEvent, + StreamErrorEvent, + TextDeltaEvent, + ToolCallEvent, + ) + + text = "" + events = [] + async for event in llm.stream( + messages=[{"role": "user", "content": "Say hello in 3 words."}], + ): + events.append(type(event).__name__) + if isinstance(event, TextDeltaEvent): + text = event.snapshot + elif isinstance(event, FinishEvent): + print( + f" Finish: stop={event.stop_reason}" + f" in={event.input_tokens}" + f" out={event.output_tokens}" + ) + elif isinstance(event, StreamErrorEvent): + print(f" StreamError: {event.error} (recoverable={event.recoverable})") + elif isinstance(event, ToolCallEvent): + print(f" ToolCall: {event.tool_name}") + print(f" Text : {text!r}") + print(f" Events : {events}") + print(f" RESULT : {'OK' if text else 'EMPTY'}") + except Exception as e: + print(f" EXCEPTION: {type(e).__name__}: {e}") + import traceback + + traceback.print_exc() + + print(f"\n{'=' * 60}") + print("DONE") + print(f"{'=' * 60}") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/quickstart.sh b/quickstart.sh index 3c282720..06eea83c 100755 --- a/quickstart.sh +++ b/quickstart.sh @@ -847,7 +847,7 @@ prompt_model_selection() { } # Function to save configuration -# Args: provider_id env_var model max_tokens max_context_tokens [use_claude_code_sub] [api_base] [use_codex_sub] +# Args: provider_id env_var model max_tokens max_context_tokens [use_claude_code_sub] [api_base] [use_codex_sub] [use_antigravity_sub] save_configuration() { local provider_id="$1" local env_var="$2" @@ -857,6 +857,7 @@ save_configuration() { local use_claude_code_sub="${6:-}" local api_base="${7:-}" local use_codex_sub="${8:-}" + local use_antigravity_sub="${9:-}" # Fallbacks if not provided if [ -z "$model" ]; then @@ -878,6 +879,7 @@ save_configuration() { "$use_claude_code_sub" \ "$api_base" \ "$use_codex_sub" \ + "$use_antigravity_sub" \ "$(date -u +"%Y-%m-%dT%H:%M:%S+00:00")" 2>/dev/null <<'PY' import json import sys @@ -892,8 +894,9 @@ from pathlib import Path use_claude_code_sub, api_base, use_codex_sub, + use_antigravity_sub, created_at, -) = sys.argv[1:10] +) = sys.argv[1:11] cfg_path = Path.home() / ".hive" / "configuration.json" cfg_path.parent.mkdir(parents=True, exist_ok=True) @@ -925,6 +928,12 @@ if use_codex_sub == "true": else: config["llm"].pop("use_codex_subscription", None) +if use_antigravity_sub == "true": + config["llm"]["use_antigravity_subscription"] = True + config["llm"].pop("api_key_env_var", None) +else: + config["llm"].pop("use_antigravity_subscription", None) + if api_base: config["llm"]["api_base"] = api_base else: @@ -993,6 +1002,19 @@ if [ -n "${HIVE_API_KEY:-}" ]; then HIVE_CRED_DETECTED=true fi +ANTIGRAVITY_CRED_DETECTED=false +# Check native Antigravity IDE (macOS/Linux) SQLite state DB first +if [ -f "$HOME/Library/Application Support/Antigravity/User/globalStorage/state.vscdb" ]; then + ANTIGRAVITY_CRED_DETECTED=true +elif [ -f "$HOME/.config/Antigravity/User/globalStorage/state.vscdb" ]; then + ANTIGRAVITY_CRED_DETECTED=true +# Fallback: antigravity-auth CLI tool JSON files +elif [ -f "$HOME/.config/opencode/antigravity-accounts.json" ]; then + ANTIGRAVITY_CRED_DETECTED=true +elif [ -f "$HOME/.config/antigravity_auth/accounts.json" ]; then + ANTIGRAVITY_CRED_DETECTED=true +fi + # Detect API key providers if [ "$USE_ASSOC_ARRAYS" = true ]; then for env_var in "${!PROVIDER_NAMES[@]}"; do @@ -1035,6 +1057,8 @@ try: sub = "codex" elif llm.get("use_kimi_code_subscription"): sub = "kimi_code" + elif llm.get("use_antigravity_subscription"): + sub = "antigravity" elif llm.get("provider", "") == "minimax" or "api.minimax.io" in llm.get("api_base", ""): sub = "minimax_code" elif llm.get("provider", "") == "hive" or "adenhq.com" in llm.get("api_base", ""): @@ -1058,6 +1082,7 @@ if [ -n "$PREV_SUB_MODE" ] || [ -n "$PREV_PROVIDER" ]; then codex) [ "$CODEX_CRED_DETECTED" = true ] && PREV_CRED_VALID=true ;; kimi_code) [ "$KIMI_CRED_DETECTED" = true ] && PREV_CRED_VALID=true ;; hive_llm) [ "$HIVE_CRED_DETECTED" = true ] && PREV_CRED_VALID=true ;; + antigravity) [ "$ANTIGRAVITY_CRED_DETECTED" = true ] && PREV_CRED_VALID=true ;; *) # API key provider — check if the env var is set if [ -n "$PREV_ENV_VAR" ] && [ -n "${!PREV_ENV_VAR}" ]; then @@ -1074,15 +1099,16 @@ if [ -n "$PREV_SUB_MODE" ] || [ -n "$PREV_PROVIDER" ]; then minimax_code) DEFAULT_CHOICE=4 ;; kimi_code) DEFAULT_CHOICE=5 ;; hive_llm) DEFAULT_CHOICE=6 ;; + antigravity) DEFAULT_CHOICE=7 ;; esac if [ -z "$DEFAULT_CHOICE" ]; then case "$PREV_PROVIDER" in - anthropic) DEFAULT_CHOICE=7 ;; - openai) DEFAULT_CHOICE=8 ;; - gemini) DEFAULT_CHOICE=9 ;; - groq) DEFAULT_CHOICE=10 ;; - cerebras) DEFAULT_CHOICE=11 ;; - openrouter) DEFAULT_CHOICE=12 ;; + anthropic) DEFAULT_CHOICE=8 ;; + openai) DEFAULT_CHOICE=9 ;; + gemini) DEFAULT_CHOICE=10 ;; + groq) DEFAULT_CHOICE=11 ;; + cerebras) DEFAULT_CHOICE=12 ;; + openrouter) DEFAULT_CHOICE=13 ;; minimax) DEFAULT_CHOICE=4 ;; kimi) DEFAULT_CHOICE=5 ;; hive) DEFAULT_CHOICE=6 ;; @@ -1138,14 +1164,21 @@ else echo -e " ${CYAN}6)${NC} Hive LLM ${DIM}(use your Hive API key)${NC}" fi +# 7) Antigravity +if [ "$ANTIGRAVITY_CRED_DETECTED" = true ]; then + echo -e " ${CYAN}7)${NC} Antigravity Subscription ${DIM}(use your Google/Gemini plan)${NC} ${GREEN}(credential detected)${NC}" +else + echo -e " ${CYAN}7)${NC} Antigravity Subscription ${DIM}(use your Google/Gemini plan)${NC}" +fi + echo "" echo -e " ${CYAN}${BOLD}API key providers:${NC}" -# 7-12) API key providers — show (credential detected) if key already set +# 8-13) API key providers — show (credential detected) if key already set PROVIDER_MENU_ENVS=(ANTHROPIC_API_KEY OPENAI_API_KEY GEMINI_API_KEY GROQ_API_KEY CEREBRAS_API_KEY OPENROUTER_API_KEY) PROVIDER_MENU_NAMES=("Anthropic (Claude) - Recommended" "OpenAI (GPT)" "Google Gemini - Free tier available" "Groq - Fast, free tier" "Cerebras - Fast, free tier" "OpenRouter - Bring any OpenRouter model") for idx in "${!PROVIDER_MENU_ENVS[@]}"; do - num=$((idx + 7)) + num=$((idx + 8)) env_var="${PROVIDER_MENU_ENVS[$idx]}" if [ -n "${!env_var}" ]; then echo -e " ${CYAN}$num)${NC} ${PROVIDER_MENU_NAMES[$idx]} ${GREEN}(credential detected)${NC}" @@ -1154,7 +1187,7 @@ for idx in "${!PROVIDER_MENU_ENVS[@]}"; do fi done -SKIP_CHOICE=$((7 + ${#PROVIDER_MENU_ENVS[@]})) +SKIP_CHOICE=$((8 + ${#PROVIDER_MENU_ENVS[@]})) echo -e " ${CYAN}$SKIP_CHOICE)${NC} Skip for now" echo "" @@ -1297,36 +1330,59 @@ case $choice in echo -e " ${DIM}Model: $SELECTED_MODEL | API: ${HIVE_LLM_ENDPOINT}${NC}" ;; 7) + # Antigravity Subscription + if [ "$ANTIGRAVITY_CRED_DETECTED" = false ]; then + echo "" + echo -e "${YELLOW} Antigravity credentials not found.${NC}" + echo -e " Run ${CYAN}antigravity-auth accounts add${NC} to authenticate," + echo -e " then run this quickstart again." + echo "" + exit 1 + else + SUBSCRIPTION_MODE="antigravity" + SELECTED_PROVIDER_ID="openai" + SELECTED_MODEL="gemini-3-flash" + SELECTED_MAX_TOKENS=32768 + SELECTED_MAX_CONTEXT_TOKENS=1000000 # Gemini 3 Flash — 1M context window + echo "" + echo -e "${YELLOW} ⚠ Using Antigravity can technically cause your account suspension. Please use at your own risk.${NC}" + echo "" + echo -e "${GREEN}⬢${NC} Using Antigravity subscription" + echo -e " ${DIM}Model: gemini-3-flash | Proxy: localhost:8069/v1${NC}" + echo -e " ${DIM}Ensure 'antigravity-auth serve' is running before starting agents.${NC}" + fi + ;; + 8) SELECTED_ENV_VAR="ANTHROPIC_API_KEY" SELECTED_PROVIDER_ID="anthropic" PROVIDER_NAME="Anthropic" SIGNUP_URL="https://console.anthropic.com/settings/keys" ;; - 8) + 9) SELECTED_ENV_VAR="OPENAI_API_KEY" SELECTED_PROVIDER_ID="openai" PROVIDER_NAME="OpenAI" SIGNUP_URL="https://platform.openai.com/api-keys" ;; - 9) + 10) SELECTED_ENV_VAR="GEMINI_API_KEY" SELECTED_PROVIDER_ID="gemini" PROVIDER_NAME="Google Gemini" SIGNUP_URL="https://aistudio.google.com/apikey" ;; - 10) + 11) SELECTED_ENV_VAR="GROQ_API_KEY" SELECTED_PROVIDER_ID="groq" PROVIDER_NAME="Groq" SIGNUP_URL="https://console.groq.com/keys" ;; - 11) + 12) SELECTED_ENV_VAR="CEREBRAS_API_KEY" SELECTED_PROVIDER_ID="cerebras" PROVIDER_NAME="Cerebras" SIGNUP_URL="https://cloud.cerebras.ai/" ;; - 12) + 13) SELECTED_ENV_VAR="OPENROUTER_API_KEY" SELECTED_PROVIDER_ID="openrouter" SELECTED_API_BASE="https://openrouter.ai/api/v1" @@ -1491,6 +1547,8 @@ if [ -n "$SELECTED_PROVIDER_ID" ]; then save_configuration "$SELECTED_PROVIDER_ID" "" "$SELECTED_MODEL" "$SELECTED_MAX_TOKENS" "$SELECTED_MAX_CONTEXT_TOKENS" "true" "" > /dev/null || SAVE_OK=false elif [ "$SUBSCRIPTION_MODE" = "codex" ]; then save_configuration "$SELECTED_PROVIDER_ID" "" "$SELECTED_MODEL" "$SELECTED_MAX_TOKENS" "$SELECTED_MAX_CONTEXT_TOKENS" "" "" "true" > /dev/null || SAVE_OK=false + elif [ "$SUBSCRIPTION_MODE" = "antigravity" ]; then + save_configuration "$SELECTED_PROVIDER_ID" "" "$SELECTED_MODEL" "$SELECTED_MAX_TOKENS" "$SELECTED_MAX_CONTEXT_TOKENS" "" "" "" "true" > /dev/null || SAVE_OK=false elif [ "$SUBSCRIPTION_MODE" = "zai_code" ]; then save_configuration "$SELECTED_PROVIDER_ID" "$SELECTED_ENV_VAR" "$SELECTED_MODEL" "$SELECTED_MAX_TOKENS" "$SELECTED_MAX_CONTEXT_TOKENS" "" "https://api.z.ai/api/coding/paas/v4" > /dev/null || SAVE_OK=false elif [ "$SUBSCRIPTION_MODE" = "minimax_code" ]; then diff --git a/scripts/setup_worker_model.sh b/scripts/setup_worker_model.sh index 653ecedd..fe4ac261 100755 --- a/scripts/setup_worker_model.sh +++ b/scripts/setup_worker_model.sh @@ -424,7 +424,7 @@ prompt_model_selection() { } # ── Save worker_llm section to configuration.json ──────────────────── -# Args: provider_id env_var model max_tokens max_context_tokens [use_claude_code_sub] [api_base] [use_codex_sub] +# Args: provider_id env_var model max_tokens max_context_tokens [use_claude_code_sub] [api_base] [use_codex_sub] [use_antigravity_sub] save_worker_configuration() { local provider_id="$1" @@ -435,6 +435,7 @@ save_worker_configuration() { local use_claude_code_sub="${6:-}" local api_base="${7:-}" local use_codex_sub="${8:-}" + local use_antigravity_sub="${9:-}" if [ -z "$model" ]; then model="$(get_default_model "$provider_id")" @@ -451,7 +452,8 @@ save_worker_configuration() { "$max_context_tokens" \ "$use_claude_code_sub" \ "$api_base" \ - "$use_codex_sub" 2>/dev/null <<'PY' + "$use_codex_sub" \ + "$use_antigravity_sub" 2>/dev/null <<'PY' import json import sys from pathlib import Path @@ -465,7 +467,8 @@ from pathlib import Path use_claude_code_sub, api_base, use_codex_sub, -) = sys.argv[1:9] + use_antigravity_sub, +) = sys.argv[1:10] cfg_path = Path.home() / ".hive" / "configuration.json" cfg_path.parent.mkdir(parents=True, exist_ok=True) @@ -496,6 +499,12 @@ if use_codex_sub == "true": else: config["worker_llm"].pop("use_codex_subscription", None) +if use_antigravity_sub == "true": + config["worker_llm"]["use_antigravity_subscription"] = True + config["worker_llm"].pop("api_key_env_var", None) +else: + config["worker_llm"].pop("use_antigravity_subscription", None) + if api_base: config["worker_llm"]["api_base"] = api_base else: @@ -591,6 +600,19 @@ if [ -n "${HIVE_API_KEY:-}" ]; then HIVE_CRED_DETECTED=true fi +ANTIGRAVITY_CRED_DETECTED=false +# Check native Antigravity IDE (macOS/Linux) SQLite state DB first +if [ -f "$HOME/Library/Application Support/Antigravity/User/globalStorage/state.vscdb" ]; then + ANTIGRAVITY_CRED_DETECTED=true +elif [ -f "$HOME/.config/Antigravity/User/globalStorage/state.vscdb" ]; then + ANTIGRAVITY_CRED_DETECTED=true +# Fallback: antigravity-auth CLI tool JSON files +elif [ -f "$HOME/.config/opencode/antigravity-accounts.json" ]; then + ANTIGRAVITY_CRED_DETECTED=true +elif [ -f "$HOME/.config/antigravity_auth/accounts.json" ]; then + ANTIGRAVITY_CRED_DETECTED=true +fi + # Detect API key providers if [ "$USE_ASSOC_ARRAYS" = true ]; then for env_var in "${!PROVIDER_NAMES[@]}"; do @@ -633,6 +655,8 @@ try: sub = "codex" elif llm.get("use_kimi_code_subscription"): sub = "kimi_code" + elif llm.get("use_antigravity_subscription"): + sub = "antigravity" elif llm.get("provider", "") == "minimax" or "api.minimax.io" in llm.get("api_base", ""): sub = "minimax_code" elif llm.get("provider", "") == "hive" or "adenhq.com" in llm.get("api_base", ""): @@ -656,6 +680,7 @@ if [ -n "$PREV_SUB_MODE" ] || [ -n "$PREV_PROVIDER" ]; then codex) [ "$CODEX_CRED_DETECTED" = true ] && PREV_CRED_VALID=true ;; kimi_code) [ "$KIMI_CRED_DETECTED" = true ] && PREV_CRED_VALID=true ;; hive_llm) [ "$HIVE_CRED_DETECTED" = true ] && PREV_CRED_VALID=true ;; + antigravity) [ "$ANTIGRAVITY_CRED_DETECTED" = true ] && PREV_CRED_VALID=true ;; *) # API key provider — check if the env var is set if [ -n "$PREV_ENV_VAR" ] && [ -n "${!PREV_ENV_VAR}" ]; then @@ -672,15 +697,16 @@ if [ -n "$PREV_SUB_MODE" ] || [ -n "$PREV_PROVIDER" ]; then minimax_code) DEFAULT_CHOICE=4 ;; kimi_code) DEFAULT_CHOICE=5 ;; hive_llm) DEFAULT_CHOICE=6 ;; + antigravity) DEFAULT_CHOICE=7 ;; esac if [ -z "$DEFAULT_CHOICE" ]; then case "$PREV_PROVIDER" in - anthropic) DEFAULT_CHOICE=7 ;; - openai) DEFAULT_CHOICE=8 ;; - gemini) DEFAULT_CHOICE=9 ;; - groq) DEFAULT_CHOICE=10 ;; - cerebras) DEFAULT_CHOICE=11 ;; - openrouter) DEFAULT_CHOICE=12 ;; + anthropic) DEFAULT_CHOICE=8 ;; + openai) DEFAULT_CHOICE=9 ;; + gemini) DEFAULT_CHOICE=10 ;; + groq) DEFAULT_CHOICE=11 ;; + cerebras) DEFAULT_CHOICE=12 ;; + openrouter) DEFAULT_CHOICE=13 ;; minimax) DEFAULT_CHOICE=4 ;; kimi) DEFAULT_CHOICE=5 ;; hive) DEFAULT_CHOICE=6 ;; @@ -736,14 +762,21 @@ else echo -e " ${CYAN}6)${NC} Hive LLM ${DIM}(use your Hive API key)${NC}" fi +# 7) Antigravity +if [ "$ANTIGRAVITY_CRED_DETECTED" = true ]; then + echo -e " ${CYAN}7)${NC} Antigravity Subscription ${DIM}(use your Google/Gemini plan)${NC} ${GREEN}(credential detected)${NC}" +else + echo -e " ${CYAN}7)${NC} Antigravity Subscription ${DIM}(use your Google/Gemini plan)${NC}" +fi + echo "" echo -e " ${CYAN}${BOLD}API key providers:${NC}" -# 7-12) API key providers — show (credential detected) if key already set +# 8-13) API key providers — show (credential detected) if key already set PROVIDER_MENU_ENVS=(ANTHROPIC_API_KEY OPENAI_API_KEY GEMINI_API_KEY GROQ_API_KEY CEREBRAS_API_KEY OPENROUTER_API_KEY) PROVIDER_MENU_NAMES=("Anthropic (Claude) - Recommended" "OpenAI (GPT)" "Google Gemini - Free tier available" "Groq - Fast, free tier" "Cerebras - Fast, free tier" "OpenRouter - Bring any OpenRouter model") for idx in "${!PROVIDER_MENU_ENVS[@]}"; do - num=$((idx + 7)) + num=$((idx + 8)) env_var="${PROVIDER_MENU_ENVS[$idx]}" if [ -n "${!env_var}" ]; then echo -e " ${CYAN}$num)${NC} ${PROVIDER_MENU_NAMES[$idx]} ${GREEN}(credential detected)${NC}" @@ -752,7 +785,7 @@ for idx in "${!PROVIDER_MENU_ENVS[@]}"; do fi done -SKIP_CHOICE=$((7 + ${#PROVIDER_MENU_ENVS[@]})) +SKIP_CHOICE=$((8 + ${#PROVIDER_MENU_ENVS[@]})) echo -e " ${CYAN}$SKIP_CHOICE)${NC} Skip for now" echo "" @@ -895,36 +928,59 @@ case $choice in echo -e " ${DIM}Model: $SELECTED_MODEL | API: ${HIVE_LLM_ENDPOINT}${NC}" ;; 7) + # Antigravity Subscription + if [ "$ANTIGRAVITY_CRED_DETECTED" = false ]; then + echo "" + echo -e "${YELLOW} Antigravity credentials not found.${NC}" + echo -e " Run ${CYAN}antigravity-auth accounts add${NC} to authenticate," + echo -e " then run this script again." + echo "" + exit 1 + else + SUBSCRIPTION_MODE="antigravity" + SELECTED_PROVIDER_ID="openai" + SELECTED_MODEL="gemini-3-flash" + SELECTED_MAX_TOKENS=32768 + SELECTED_MAX_CONTEXT_TOKENS=1000000 # Gemini 3 Flash — 1M context window + echo "" + echo -e "${YELLOW} ⚠ Using Antigravity can technically cause your account suspension. Please use at your own risk.${NC}" + echo "" + echo -e "${GREEN}⬢${NC} Using Antigravity subscription" + echo -e " ${DIM}Model: gemini-3-flash | Proxy: localhost:8069/v1${NC}" + echo -e " ${DIM}Ensure 'antigravity-auth serve' is running before starting agents.${NC}" + fi + ;; + 8) SELECTED_ENV_VAR="ANTHROPIC_API_KEY" SELECTED_PROVIDER_ID="anthropic" PROVIDER_NAME="Anthropic" SIGNUP_URL="https://console.anthropic.com/settings/keys" ;; - 8) + 9) SELECTED_ENV_VAR="OPENAI_API_KEY" SELECTED_PROVIDER_ID="openai" PROVIDER_NAME="OpenAI" SIGNUP_URL="https://platform.openai.com/api-keys" ;; - 9) + 10) SELECTED_ENV_VAR="GEMINI_API_KEY" SELECTED_PROVIDER_ID="gemini" PROVIDER_NAME="Google Gemini" SIGNUP_URL="https://aistudio.google.com/apikey" ;; - 10) + 11) SELECTED_ENV_VAR="GROQ_API_KEY" SELECTED_PROVIDER_ID="groq" PROVIDER_NAME="Groq" SIGNUP_URL="https://console.groq.com/keys" ;; - 11) + 12) SELECTED_ENV_VAR="CEREBRAS_API_KEY" SELECTED_PROVIDER_ID="cerebras" PROVIDER_NAME="Cerebras" SIGNUP_URL="https://cloud.cerebras.ai/" ;; - 12) + 13) SELECTED_ENV_VAR="OPENROUTER_API_KEY" SELECTED_PROVIDER_ID="openrouter" SELECTED_API_BASE="https://openrouter.ai/api/v1" @@ -1086,6 +1142,8 @@ if [ -n "$SELECTED_PROVIDER_ID" ]; then save_worker_configuration "$SELECTED_PROVIDER_ID" "" "$SELECTED_MODEL" "$SELECTED_MAX_TOKENS" "$SELECTED_MAX_CONTEXT_TOKENS" "true" "" > /dev/null || SAVE_OK=false elif [ "$SUBSCRIPTION_MODE" = "codex" ]; then save_worker_configuration "$SELECTED_PROVIDER_ID" "" "$SELECTED_MODEL" "$SELECTED_MAX_TOKENS" "$SELECTED_MAX_CONTEXT_TOKENS" "" "" "true" > /dev/null || SAVE_OK=false + elif [ "$SUBSCRIPTION_MODE" = "antigravity" ]; then + save_worker_configuration "$SELECTED_PROVIDER_ID" "" "$SELECTED_MODEL" "$SELECTED_MAX_TOKENS" "$SELECTED_MAX_CONTEXT_TOKENS" "" "" "" "true" > /dev/null || SAVE_OK=false elif [ "$SUBSCRIPTION_MODE" = "zai_code" ]; then save_worker_configuration "$SELECTED_PROVIDER_ID" "$SELECTED_ENV_VAR" "$SELECTED_MODEL" "$SELECTED_MAX_TOKENS" "$SELECTED_MAX_CONTEXT_TOKENS" "" "https://api.z.ai/api/coding/paas/v4" > /dev/null || SAVE_OK=false elif [ "$SUBSCRIPTION_MODE" = "minimax_code" ]; then