diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c950e88..ae8cc73 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -220,10 +220,10 @@ jobs: with: persist-credentials: false - - name: Install dependencies (jq, curl) + - name: Install dependencies (jq, curl, sqlite3) run: | sudo apt-get update -qq - sudo apt-get install -y --no-install-recommends jq curl + sudo apt-get install -y --no-install-recommends jq curl sqlite3 - name: Smoke test — get-claude-usage (no credentials) run: | @@ -263,6 +263,38 @@ jobs: } echo "get-provider-usage smoke: OK — provider=${PROVIDER}" + - name: Integration test — Antigravity quota fixture + run: | + OUT="$(ANTIGRAVITY_RESPONSE_FILE=tests/fixtures/antigravity-valid.json \ + ANTIGRAVITY_ACCOUNT_EMAIL=test@example.invalid \ + providers/get-provider-usage "antigravity" "")" + echo "$OUT" | jq -e ' + length == 1 + and .[0].provider == "antigravity" + and .[0].usage.primary.resetDescription == "Claude Opus 4.6" + and .[0].usage.primary.usedPercent == 75 + and .[0].usage.secondary.resetDescription == "Gemini 3.5 Flash" + and .[0].usage.secondary.usedPercent == 50 + and .[0].usage.tertiary.resetDescription == "GPT-OSS 120B" + and .[0].usage.tertiary.usedPercent == 40 + ' >/dev/null + + AUTH_ERROR="$(ANTIGRAVITY_RESPONSE_FILE=tests/fixtures/antigravity-valid.json \ + ANTIGRAVITY_HTTP_STATUS=401 providers/get-provider-usage "antigravity" "")" + echo "$AUTH_ERROR" | jq -e '.[0].error.code == 401' >/dev/null + + RATE_ERROR="$(ANTIGRAVITY_RESPONSE_FILE=tests/fixtures/antigravity-valid.json \ + ANTIGRAVITY_HTTP_STATUS=429 providers/get-provider-usage "antigravity" "")" + echo "$RATE_ERROR" | jq -e '.[0].error.code == 429' >/dev/null + + MISSING_SESSION="$(ANTIGRAVITY_STATE_DB=/does/not/exist \ + providers/get-provider-usage "antigravity" "")" + echo "$MISSING_SESSION" | jq -e '.[0].error.code == 2' >/dev/null + + SCHEMA_ERROR="$(ANTIGRAVITY_RESPONSE_FILE=tests/fixtures/antigravity-schema-changed.json \ + providers/get-provider-usage "antigravity" "")" + echo "$SCHEMA_ERROR" | jq -e '.[0].error.message | contains("schema changed")' >/dev/null + - name: Smoke test — all stubs delegate correctly (structure check) run: | FAIL=0 diff --git a/AiOverviewControlSettings.qml b/AiOverviewControlSettings.qml index a3e11dd..256c3b2 100644 --- a/AiOverviewControlSettings.qml +++ b/AiOverviewControlSettings.qml @@ -60,6 +60,7 @@ PluginSettings { { id:"codex", name:"Codex", icon:"terminal", mode:"telemetry", requirement:"codex CLI", envVar:"", note:"Official app-server rate limits" }, { id:"claude", name:"Claude", icon:"psychology", mode:"telemetry", requirement:"claude CLI or ~/.claude", envVar:"", note:"Local analytics and authenticated usage" }, { id:"copilot", name:"Copilot", icon:"code", mode:"telemetry", requirement:"gh CLI or GitHub token", envVar:"COPILOT_GITHUB_TOKEN", note:"Authenticated Copilot quota from the GitHub session" }, + { id:"antigravity", name:"Antigravity", icon:"rocket_launch", mode:"telemetry", requirement:"Antigravity CLI keyring or IDE session", envVar:"", note:"Per-model quota and reset times from the internal read-only endpoint" }, { id:"gemini", name:"Gemini", icon:"star", mode:"telemetry", requirement:"gemini CLI or API key", envVar:"GEMINI_API_KEY", note:"Authentication status; quota remains in AI Studio" }, { id:"9router", name:"9Router", icon:"share", mode:"telemetry", requirement:"local 9Router database", envVar:"", note:"Local requests, tokens and cost" }, { id:"openrouter", name:"OpenRouter", icon:"route", mode:"telemetry", requirement:"API key or 9Router data", envVar:"OPENROUTER_API_KEY", note:"Official key usage and limits" }, diff --git a/AiOverviewControlWidget.qml b/AiOverviewControlWidget.qml index 6019615..9fcde5f 100644 --- a/AiOverviewControlWidget.qml +++ b/AiOverviewControlWidget.qml @@ -134,6 +134,7 @@ PluginComponent { "codex", "claude", "copilot", + "antigravity", "gemini", "9router", "openrouter", @@ -526,6 +527,7 @@ PluginComponent { codex: "Codex", claude: "Claude", copilot: "Copilot", + antigravity: "Antigravity", cursor: "Cursor", gemini: "Gemini", openrouter: "OpenRouter", @@ -722,6 +724,7 @@ PluginComponent { if (providerId === "codex") return "data_object"; if (providerId === "claude") return "psychology"; if (providerId === "copilot") return "hub"; + if (providerId === "antigravity") return "rocket_launch"; if (providerId === "gemini") return "auto_awesome"; if (providerId === "openrouter") return "route"; if (providerId === "9router") return "share"; @@ -758,6 +761,7 @@ PluginComponent { if (providerId === "claude") return Theme.warning; if (providerId === "codex") return Theme.success; if (providerId === "copilot") return Theme.primary; + if (providerId === "antigravity") return Theme.primary; if (providerId === "gemini") return Theme.secondary; if (providerId === "openrouter") return Theme.primary; if (providerId === "9router") return Theme.secondary; @@ -816,6 +820,10 @@ PluginComponent { const windowData = primaryUsageWindow(provider); if (windowData && windowData.displayValue && String(windowData.displayValue).length > 0) { const label = windowData.resetDescription || t("status.usage", "usage"); + const reset = provider.provider === "antigravity" ? formatTimeUntil(windowData.resetsAt) : ""; + if (reset && reset !== "—") { + return `${source} · ${label} · ${windowData.displayValue} · ${t("status.reset", "reset")} ${reset}`; + } return `${source} · ${label} · ${windowData.displayValue}`; } const reset = providerReset(provider); @@ -973,6 +981,7 @@ PluginComponent { claude: "https://claude.ai/settings/usage", codex: "https://chatgpt.com/codex/settings/usage", copilot: "https://github.com/settings/copilot/features", + antigravity: "", gemini: "https://aistudio.google.com/usage", openrouter: "https://openrouter.ai/activity", deepseek: "https://platform.deepseek.com/usage", diff --git a/CHANGELOG.md b/CHANGELOG.md index 260ae57..5c59bb4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,20 @@ ## [Unreleased] +## 1.5.0 - 2026-06-28 + +### Providers +- **Antigravity provider added (Quota coverage).** Surfaces real per-model quota and reset windows from the Google Cloud Code Assist endpoint (`v1internal:fetchAvailableModels` on `cloudcode-pa.googleapis.com`) — the same protocol used by Antigravity IDE and the official `gemini-cli`. Quotas are grouped into current model families: Claude Opus 4.6, Claude Sonnet 4.6, Gemini 3.5 Flash, Gemini 3.1 Pro, and GPT-OSS 120B. +- **Dual credential discovery.** Authentication prefers the signed-in `agy` CLI profile via Linux Secret Service (`secret-tool`), then falls back to the IDE's `globalStorage/state.vscdb` (`antigravityAuthStatus`) for desktop builds. The OAuth bearer token is supplied to `curl` via stdin and never appears in process arguments, stdout, or logs. +- **Health probe.** `get-provider-health` now recognizes `antigravity` and reports ready/missing based on the keyring or SQLite state availability. + +### Fixed +- **Antigravity Authorization header.** The bearer token was not interpolated into the `curl` config line (missing `%s`), so the live path always failed authentication while fixture tests passed. Token interpolation now works and the live path returns real quota data. + +### Notes +- Provider count: 33 → 34. +- Antigravity additionally requires `sqlite3` on the system for the IDE-state fallback path. + ## 1.4.12 - 2026-06-26 ### Providers diff --git a/README.md b/README.md index 535ccfd..82cb655 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ billing, authentication, and local usage telemetry — right in your DankBar. [![CI](https://github.com/bernardopg/AiOverviewControl/actions/workflows/ci.yml/badge.svg)](https://github.com/bernardopg/AiOverviewControl/actions/workflows/ci.yml) [![Release](https://img.shields.io/github/v/release/bernardopg/AiOverviewControl)](https://github.com/bernardopg/AiOverviewControl/releases/latest) [![License](https://img.shields.io/github/license/bernardopg/AiOverviewControl)](./LICENSE) -[![Providers](https://img.shields.io/badge/providers-33-7C4DFF)](./docs/providers.md) +[![Providers](https://img.shields.io/badge/providers-34-7C4DFF)](./docs/providers.md) [![Languages](https://img.shields.io/badge/UI%20languages-5-00BFA5)](./docs/i18n-crowdin.md) [Install](#installation) · [Screenshots](#screenshots) · [Providers](./docs/providers.md) · @@ -48,7 +48,7 @@ it does not. No dashboard scraping. No fabricated percentages. Ever. | | | | --- | --- | -| 📊 **Unified dashboard** | 33 AI providers and developer tools in one place. | +| 📊 **Unified dashboard** | 34 AI providers and developer tools in one place. | | 🛰️ **Fleet overview** | Cross-provider rollup in the hero — quota-only average load, hottest provider, how many are near their cap, and the soonest reset. | | ⏱️ **Official Codex windows** | Rate-limit windows straight from `codex app-server`. | | 🤖 **Deep Claude analytics** | Quota plus local token, session, model, project, and cost analytics. | @@ -84,7 +84,7 @@ Provider cards use one of these honest coverage levels: | Coverage | Meaning | | --- | --- | -| **Quota** | Returns real rate-limit/spend windows and used percentage (Codex, Copilot, OpenRouter, Z.ai, GLM). | +| **Quota** | Returns real rate-limit/spend windows and used percentage (Codex, Copilot, Antigravity, OpenRouter, Z.ai, GLM). | | **Balance** | Returns remaining prepaid balance or credits in real currency (Kimi, DeepSeek, Together). | | **Analytics** | Reads consumption counters or provider-owned local data (Cloudflare GraphQL, 9Router, Claude, Ollama). | | **Authentication** | Verifies credentials via a read-only endpoint without stable quota data (Gemini, Mistral, NVIDIA, MiniMax, Qwen, xAI, and more). | @@ -97,6 +97,7 @@ Notable integrations: | Codex | Official `codex app-server` account and rate-limit methods. | | Claude Code | OAuth quota plus local `~/.claude/projects` analytics. | | GitHub Copilot | Authenticated GitHub/Copilot quota snapshot. | +| Antigravity | Per-model quota and reset times from the Cloud Code Assist endpoint used by Antigravity IDE and gemini-cli. | | 9Router | Local SQLite or JSON usage data, including routed-model telemetry. | | OpenRouter | Key limits, spend, balance, and 30-day model activity. | | Kimi (Moonshot) | `GET /v1/users/me/balance` — available, voucher, and cash balance (USD/CNY). | @@ -114,7 +115,7 @@ The full matrix, credentials, and upstream references are documented in - DankMaterialShell running on Quickshell. - `bash`, `jq`, and `curl`. -- Provider-specific CLIs or credentials only for providers you enable. +- Provider-specific CLIs or credentials only for providers you enable; Antigravity additionally requires `sqlite3`. Recommended baseline for the default provider set: diff --git a/docs/README.pt-BR.md b/docs/README.pt-BR.md index f71cf05..e713eac 100644 --- a/docs/README.pt-BR.md +++ b/docs/README.pt-BR.md @@ -12,7 +12,7 @@ billing, autenticação e telemetria local de uso de IA — direto na sua DankBa [![CI](https://github.com/bernardopg/AiOverviewControl/actions/workflows/ci.yml/badge.svg)](https://github.com/bernardopg/AiOverviewControl/actions/workflows/ci.yml) [![Release](https://img.shields.io/github/v/release/bernardopg/AiOverviewControl)](https://github.com/bernardopg/AiOverviewControl/releases/latest) [![Licença](https://img.shields.io/github/license/bernardopg/AiOverviewControl)](../LICENSE) -[![Provedores](https://img.shields.io/badge/provedores-33-7C4DFF)](./providers.md) +[![Provedores](https://img.shields.io/badge/provedores-34-7C4DFF)](./providers.md) [![Idiomas](https://img.shields.io/badge/idiomas%20de%20UI-5-00BFA5)](./i18n-crowdin.md) [Instalação](#instalação) · [Screenshots](#screenshots) · [Provedores](./providers.md) · @@ -97,6 +97,7 @@ Integrações medidas notáveis: | Codex | Métodos oficiais de conta e rate-limit do `codex app-server`. | | Claude Code | Cota OAuth mais analytics local de `~/.claude/projects`. | | GitHub Copilot | Snapshot autenticado de cota GitHub/Copilot. | +| Antigravity | Cotas por modelo e horários de reset via endpoint Cloud Code Assist, usado pela IDE Antigravity e pelo gemini-cli. | | 9Router | Dados locais de uso em SQLite ou JSON, incluindo telemetria por modelo roteado. | | OpenRouter | Limites de chave, gasto, saldo e atividade de modelos em 30 dias. | | DeepSeek, Kimi, Together | APIs de saldo ou crédito do provedor. | @@ -112,7 +113,7 @@ A matriz completa, credenciais e referências upstream estão documentadas em - DankMaterialShell rodando sobre Quickshell. - `bash`, `jq` e `curl`. -- CLIs ou credenciais específicas apenas para os provedores habilitados. +- CLIs ou credenciais específicas apenas para os provedores habilitados; Antigravity também requer `sqlite3`. Linha de base recomendada para o conjunto padrão de provedores: diff --git a/docs/architecture.md b/docs/architecture.md index dfe3de4..b75500c 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -11,6 +11,7 @@ providers/get-provider-health Prerequisite checks for settings providers/get-codex-usage Codex app-server protocol bridge providers/get-claude-usage Claude local analytics and quota bridge providers/get-copilot-usage Authenticated GitHub Copilot quota bridge +providers/get-antigravity-usage Local Antigravity session quota bridge providers/get-provider-wrapper Single-provider wrapper providers/get-*-usage Canonical provider entrypoints ``` @@ -62,6 +63,10 @@ Errors return: `get-codex-usage` starts `codex app-server`, sends `initialize`, `account/read`, and `account/rateLimits/read`, then maps the official response to the common schema. The bridge uses a bounded process lifetime and never reads browser state. +## Antigravity protocol + +`get-antigravity-usage` reads the current Antigravity CLI OAuth profile from the desktop keyring, falling back to the IDE SQLite state used by older builds, and calls the read-only `v1internal:fetchAvailableModels` endpoint on `cloudcode-pa.googleapis.com` — the same host used by Antigravity IDE and the official gemini-cli. It groups model quotas into current families (Claude Opus 4.6, Claude Sonnet 4.6, Gemini 3.5 Flash, Gemini 3.1 Pro, GPT-OSS 120B), ordered by highest consumption. The bearer token is supplied to curl over stdin and is never printed or placed in process arguments. + ## Settings keys | Key | Default | Purpose | diff --git a/docs/configuration.md b/docs/configuration.md index 115de9d..c4ff2c3 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -29,6 +29,8 @@ codex,claude,copilot Add API providers only after their required environment variables are available to the DMS process. +Antigravity does not use an environment variable. It prefers the signed-in CLI profile from Linux Secret Service via `secret-tool`, then falls back to `${XDG_CONFIG_HOME:-~/.config}/Antigravity/User/globalStorage/state.vscdb` via `sqlite3` for older IDE builds. + ## Environment variables | Provider | Variables | diff --git a/docs/providers.md b/docs/providers.md index c8b1f78..751af66 100644 --- a/docs/providers.md +++ b/docs/providers.md @@ -21,7 +21,7 @@ Every provider maps to exactly one coverage level. The level dictates what the w | Level | Meaning | Example providers | | --- | --- | --- | -| **Quota** | Real `usedPercent` + reset window from an official protocol/API. | `codex`, `copilot`, `openrouter`, `zai`, `glm` | +| **Quota** | Real `usedPercent` + reset window from a protocol/API. | `codex`, `copilot`, `antigravity`, `openrouter`, `zai`, `glm` | | **Balance** | Remaining prepaid balance / credits in real currency. | `kimi`, `deepseek`, `together` | | **Analytics** | Consumption counters (requests/tokens/neurons/cost) with no remaining-quota value. | `cloudflare` (GraphQL), `9router`, `claude` (local) | | **Auth** | Read-only key validation only — no usage numbers. | `gemini`, `mistral`, `nvidia`, `qwen`, `byteplus`, `groq`, `cohere`, `replicate`, `fireworks`, `minimax`, `xai`, `kilo` | @@ -83,6 +83,17 @@ The matrix below summarises the **authentication/billing surface** for every sup +antigravity +Quota (Cloud Code Assist) +✅ local Antigravity OAuth session +⚠️ per-model remaining fraction + reset time +✅ Antigravity plan +— +~/.config/Antigravity +Antigravity IDE +v1internal:fetchAvailableModels on cloudcode-pa.googleapis.com + + gemini Auth ✅ GET /v1beta/models diff --git a/plugin.json b/plugin.json index 1597d1d..28988a9 100644 --- a/plugin.json +++ b/plugin.json @@ -2,7 +2,7 @@ "id": "aiOverviewControl", "name": "AiOverviewControl", "description": "Plugin-managed AI quota, billing, authentication, and local usage telemetry for DankMaterialShell", - "version": "1.4.12", + "version": "1.5.0", "author": "bernardopg", "icon": "monitoring", "type": "widget", diff --git a/providers/get-antigravity-usage b/providers/get-antigravity-usage new file mode 100755 index 0000000..a98a351 --- /dev/null +++ b/providers/get-antigravity-usage @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +exec "$SCRIPT_DIR/get-provider-wrapper" antigravity diff --git a/providers/get-provider-health b/providers/get-provider-health index 1cccaad..7bc48b3 100755 --- a/providers/get-provider-health +++ b/providers/get-provider-health @@ -19,6 +19,10 @@ status_for() { claude) command -v claude >/dev/null 2>&1 || [ -d "$HOME/.claude" ] || { status="missing"; detail="claude CLI or ~/.claude"; } ;; copilot) command -v gh >/dev/null 2>&1 || has_any_env COPILOT_GITHUB_TOKEN GH_TOKEN GITHUB_TOKEN || { status="missing"; detail="gh CLI or GitHub token"; } ;; gemini) command -v gemini >/dev/null 2>&1 || has_any_env GEMINI_API_KEY GOOGLE_API_KEY GOOGLE_GENERATIVE_AI_API_KEY || { status="missing"; detail="gemini CLI or API key"; } ;; + antigravity) + command -v secret-tool >/dev/null 2>&1 || command -v sqlite3 >/dev/null 2>&1 || { status="missing"; detail="secret-tool or sqlite3"; } + [ "$status" != "ready" ] || command -v secret-tool >/dev/null 2>&1 || [ -r "${XDG_CONFIG_HOME:-$HOME/.config}/Antigravity/User/globalStorage/state.vscdb" ] || { status="missing"; detail="CLI keyring or IDE state"; } + ;; 9router) [ -f "$HOME/.9router/db/data.sqlite" ] || [ -f "$HOME/.9router/usage.json" ] || { status="missing"; detail="9Router usage database"; } ;; openrouter) has_any_env OPENROUTER_API_KEY || [ -f "$HOME/.9router/db/data.sqlite" ] || { status="missing"; detail="OPENROUTER_API_KEY or 9Router data"; } ;; deepseek) has_any_env DEEPSEEK_API_KEY || { status="missing"; detail="DEEPSEEK_API_KEY"; } ;; diff --git a/providers/get-provider-usage b/providers/get-provider-usage index 5b8da62..d84fb9c 100755 --- a/providers/get-provider-usage +++ b/providers/get-provider-usage @@ -205,6 +205,216 @@ fetch_gemini_native() { json_error gemini gemini-local 2 provider "Gemini is not authenticated locally. Run gemini once, or set GEMINI_API_KEY/GOOGLE_API_KEY." } +extract_antigravity_keyring_token() { + jq -r ' + def token_string: + if type == "object" then + .access_token // .accessToken // .token // .Token // empty + elif type == "string" and test("^ya29\\.") then + . + else + empty + end; + if type == "object" then + (.token | token_string) + // token_string + // empty + else + token_string + end + ' 2>/dev/null || true +} + +extract_antigravity_keyring_account() { + jq -r ' + if type == "object" then + .email // .account_email // (.token.email // .token.account_email // empty) // "Antigravity CLI" + else + "Antigravity CLI" + end + ' 2>/dev/null || printf 'Antigravity CLI' +} + +fetch_antigravity_native() { + local state_db="${ANTIGRAVITY_STATE_DB:-${XDG_CONFIG_HOME:-$HOME/.config}/Antigravity/User/globalStorage/state.vscdb}" + local project_file="${ANTIGRAVITY_PROJECT_FILE:-$HOME/.gemini/antigravity-cli/cache/default_project_id.txt}" + # The production host used by both Antigravity IDE and the official gemini-cli. + # Keep an override for testing or future endpoint migrations. + local endpoint="${ANTIGRAVITY_API_BASE_URL:-https://cloudcode-pa.googleapis.com}" + local response_file="${ANTIGRAVITY_RESPONSE_FILE:-}" + local account="Antigravity" + local login_method="local OAuth session" + local tmp_body http_status auth_json token project request_body message keyring_payload service + + tmp_body="$(mktemp)" + + # A response fixture keeps parsing tests deterministic and avoids requiring + # a live Google session in CI. It is never selected during normal runtime. + if [ -n "$response_file" ]; then + if [ ! -r "$response_file" ]; then + rm -f "$tmp_body" + json_error antigravity antigravity-internal 2 runtime "Antigravity response fixture is not readable." + return 0 + fi + cp "$response_file" "$tmp_body" + http_status="${ANTIGRAVITY_HTTP_STATUS:-200}" + account="${ANTIGRAVITY_ACCOUNT_EMAIL:-Antigravity}" + else + token="" + + # Antigravity CLI keeps its current OAuth profile in the desktop keyring. + # Lookup is deliberately limited to known service names; the secret is + # captured in memory and never emitted. Standard oauth2.Token JSON and a + # plain access-token value are both accepted for forward compatibility. + if command -v secret-tool >/dev/null 2>&1; then + keyring_payload="$(timeout 3 secret-tool lookup service gemini username antigravity 2>/dev/null || true)" + if [ -n "$keyring_payload" ]; then + token="$(printf '%s' "$keyring_payload" | extract_antigravity_keyring_token)" + account="$(printf '%s' "$keyring_payload" | extract_antigravity_keyring_account)" + if [ -z "$token" ] && [[ "$keyring_payload" == ya29.* ]]; then + token="$keyring_payload" + account="Antigravity CLI" + fi + [ -z "$token" ] || login_method="CLI keyring OAuth" + fi + + for service in "${ANTIGRAVITY_KEYRING_SERVICE:-Antigravity CLI}" "antigravity-cli" "Antigravity"; do + [ -z "$token" ] || break + keyring_payload="$(timeout 3 secret-tool lookup service "$service" 2>/dev/null || true)" + [ -n "$keyring_payload" ] || continue + token="$(printf '%s' "$keyring_payload" | extract_antigravity_keyring_token)" + account="$(printf '%s' "$keyring_payload" | extract_antigravity_keyring_account)" + if [ -z "$token" ] && [[ "$keyring_payload" == ya29.* ]]; then + token="$keyring_payload" + account="Antigravity CLI" + fi + if [ -n "$token" ]; then + login_method="CLI keyring OAuth" + break + fi + done + fi + + # Older IDE builds also leave an access token in state.vscdb. Keep it as a + # fallback, but prefer the keyring because the database copy can be stale. + if [ -z "$token" ] && command -v sqlite3 >/dev/null 2>&1 && [ -r "$state_db" ]; then + auth_json="$(sqlite3 "$state_db" "select value from ItemTable where key='antigravityAuthStatus' limit 1;" 2>/dev/null || true)" + token="$(printf '%s' "$auth_json" | jq -r '.apiKey // empty' 2>/dev/null || true)" + account="$(printf '%s' "$auth_json" | jq -r '.email // "Antigravity IDE"' 2>/dev/null || printf 'Antigravity IDE')" + login_method="IDE state OAuth" + fi + + if [ -z "$token" ]; then + rm -f "$tmp_body" + json_error antigravity antigravity-local 2 provider "No usable Antigravity session was found in the CLI keyring or IDE state. Run agy or open Antigravity and sign in." + return 0 + fi + + project="" + [ ! -r "$project_file" ] || project="$(sed -n '1p' "$project_file")" + request_body="$(jq -cn --arg project "$project" 'if $project == "" then {} else {project:$project} end')" + + # Feed the bearer header through a pipe-backed config descriptor and the + # request body through stdin. Neither token nor project appears in the + # process argument list, stdout, stderr, or a temporary file. + http_status="$( + curl -sS --max-time 10 \ + --config <(printf 'header = "Authorization: Bearer %s"\n' "$token") \ + -o "$tmp_body" -w '%{http_code}' \ + -X POST \ + -H "Content-Type: application/json" \ + -H "User-Agent: antigravity/linux/amd64" \ + -H "X-Goog-Api-Client: google-cloud-sdk vscode_cloudshelleditor/0.1" \ + --data-binary @- \ + "$endpoint/v1internal:fetchAvailableModels" 2>/dev/null \ + <<< "$request_body" \ + || true + )" + fi + + if [ "$http_status" != "200" ]; then + message="$(jq -r '.error.message // empty' "$tmp_body" 2>/dev/null || true)" + rm -f "$tmp_body" + case "$http_status" in + 401|403) + json_error antigravity antigravity-internal "$http_status" provider "Antigravity session expired. Open Antigravity and sign in again." + ;; + 429) + [ -n "$message" ] || message="Antigravity quota endpoint is rate-limited. Retry after its reset time." + json_error antigravity antigravity-internal 429 provider "$message" + ;; + *) + [ -n "$message" ] || message="Antigravity quota request returned HTTP ${http_status:-0}." + json_error antigravity antigravity-internal "${http_status:-1}" provider "$message" + ;; + esac + return 0 + fi + + if ! jq -e '.models | type == "object"' "$tmp_body" >/dev/null 2>&1; then + rm -f "$tmp_body" + json_error antigravity antigravity-internal 1 provider "Antigravity quota response schema changed: models object is missing." + return 0 + fi + + jq -c --arg account "$account" --arg login_method "$login_method" ' + def clamp: + if type != "number" then null + elif . < 0 then 0 + elif . > 1 then 1 + else . end; + def family($key; $entry): + (($key + " " + ($entry.displayName // "") + " " + ($entry.modelName // "")) | ascii_downcase) as $name + | if ($name | contains("opus")) then "Claude Opus 4.6" + elif ($name | test("sonnet\\b")) then "Claude Sonnet 4.6" + elif (($name | test("gemini[- .]?3[- .]?5")) and ($name | contains("flash"))) then "Gemini 3.5 Flash" + elif (($name | test("gemini[- .]?3[- .]?1")) and ($name | contains("pro"))) then "Gemini 3.1 Pro" + elif (($name | test("gemini[- .]?3")) and ($name | contains("flash"))) then "Gemini 3 Flash" + elif ($name | test("gpt[- ]?oss")) then "GPT-OSS 120B" + else null end; + def window($group): { + usedPercent: (((1 - $group.remaining) * 10000 | round) / 100), + windowMinutes: null, + resetsAt: $group.resetsAt, + resetDescription: $group.name, + displayValue: (((1 - $group.remaining) * 100 | round | tostring) + "% used") + }; + [ + .models | to_entries[] + | . as $model + | (family(.key; .value)) as $family + | (.value.quotaInfo.remainingFraction | clamp) as $remaining + | select($family != null and $remaining != null) + | {name:$family, remaining:$remaining, resetsAt:(.value.quotaInfo.resetTime // null)} + ] + | group_by(.name) + | map({ + name: .[0].name, + remaining: (map(.remaining) | min), + resetsAt: (map(.resetsAt | select(. != null)) | sort | first // null) + }) + | sort_by(.remaining) + | map(window(.)) as $windows + | if ($windows | length) == 0 then + error("no supported quota entries") + else { + provider:"antigravity", + source:"antigravity-internal", + usage:{ + identity:{providerID:"antigravity",accountEmail:$account,loginMethod:$login_method}, + accountEmail:$account, + loginMethod:$login_method, + primary:$windows[0], + secondary:($windows[1] // null), + tertiary:($windows[2] // null), + updatedAt:(now|todateiso8601) + }, + credits:{remaining:"per-model quota"} + } end + ' "$tmp_body" 2>/dev/null || json_error antigravity antigravity-internal 1 provider "Antigravity quota response contains no supported model quota entries." + rm -f "$tmp_body" +} + fetch_openrouter_native() { local key="${OPENROUTER_API_KEY:-}" if [ -z "$key" ]; then @@ -1303,6 +1513,9 @@ fetch_provider() { gemini) fetch_gemini_native ;; + antigravity) + fetch_antigravity_native + ;; 9router) fetch_9router_native 9router 9router ;; diff --git a/tests/fixtures/antigravity-schema-changed.json b/tests/fixtures/antigravity-schema-changed.json new file mode 100644 index 0000000..1206964 --- /dev/null +++ b/tests/fixtures/antigravity-schema-changed.json @@ -0,0 +1,3 @@ +{ + "availableModels": [] +} diff --git a/tests/fixtures/antigravity-valid.json b/tests/fixtures/antigravity-valid.json new file mode 100644 index 0000000..6c458f9 --- /dev/null +++ b/tests/fixtures/antigravity-valid.json @@ -0,0 +1,35 @@ +{ + "models": { + "claude-opus-4-6-thinking": { + "displayName": "Claude Opus 4.6 (Thinking)", + "quotaInfo": { + "remainingFraction": 0.25, + "resetTime": "2026-06-27T03:00:00Z" + } + }, + "gemini-3.1-pro-high": { + "displayName": "Gemini 3.1 Pro (High)", + "quotaInfo": { + "remainingFraction": 0.8, + "resetTime": "2026-06-27T04:00:00Z" + } + }, + "gemini-3.5-flash-low": { + "displayName": "Gemini 3.5 Flash (Medium)", + "quotaInfo": { + "remainingFraction": 0.5, + "resetTime": "2026-06-27T05:00:00Z" + } + }, + "gpt-oss-120b-medium": { + "displayName": "GPT-OSS 120B (Medium)", + "quotaInfo": { + "remainingFraction": 0.6, + "resetTime": "2026-06-27T06:00:00Z" + } + }, + "unrelated-model": { + "displayName": "Unrelated model" + } + } +}