Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ OpenUsage lives in your menu bar and shows you how much of your AI coding subscr

- [**Amp**](docs/providers/amp.md) / free tier, bonus, credits
- [**Antigravity**](docs/providers/antigravity.md) / all models
- [**Antigravity IDE**](docs/providers/antigravity-ide.md) / all models
- [**Claude**](docs/providers/claude.md) / session, weekly, extra usage, local token usage (ccusage)
- [**Codex**](docs/providers/codex.md) / session, weekly, reviews, credits
- [**Copilot**](docs/providers/copilot.md) / premium, chat, completions
Expand Down
52 changes: 52 additions & 0 deletions docs/providers/antigravity-ide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Antigravity IDE

> Antigravity IDE is the standalone 2.0 version of [Antigravity](antigravity.md). Shares the same Codeium language server binary and Connect-RPC protocol.

## Overview

- **Vendor:** Google
- **Protocol:** Connect RPC v1 (JSON over HTTP) on local language server
- **Service:** `exa.language_server_pb.LanguageServerService`
- **Auth:** CSRF token from process args
- **Quota:** fraction (0.0–1.0, where 1.0 = 100% remaining)
- **Quota window:** 5 hours
- **Requires:** Antigravity IDE running (language server process)

## Differences from Antigravity

| | Antigravity | Antigravity IDE |
|---|---|---|
| App bundle | `Antigravity.app` | `Antigravity IDE.app` |
| LS marker (`--app_data_dir`) | `antigravity` | `antigravity-ide` |
| State DB path | `~/Library/Application Support/Antigravity/...` | `~/Library/Application Support/Antigravity IDE/...` |
| OAuth in SQLite | ✅ `antigravityUnifiedStateSync.oauthToken` | ❌ Not present |
| Cloud Code API fallback | ✅ | ❌ (no OAuth tokens) |

## Discovery

Same as [Antigravity](antigravity.md#discovery), but with marker `antigravity-ide`:

```bash
# Find process
ps -ax -o pid=,command= | grep 'language_server_macos.*antigravity-ide'
# Match: --app_data_dir antigravity-ide
```

## Endpoints

Identical to [Antigravity](antigravity.md#endpoints) — same LS binary, same RPC service:

- `GetUserStatus` (primary)
- `GetCommandModelConfigs` (fallback)

Metadata uses `ideName: "antigravity-ide"` and `extensionName: "antigravity-ide"`.

## Plugin Strategy

1. Discover LS process via `ctx.host.ls.discover()` with marker `antigravity-ide`
2. Probe ports with `GetUnleashData` to find the Connect-RPC endpoint
3. Call `GetUserStatus` for plan name + per-model quota
4. Fall back to `GetCommandModelConfigs` if `GetUserStatus` fails
5. If LS not found or all calls fail: error "Start Antigravity IDE and try again."

No Cloud Code API fallback — Antigravity IDE's SQLite does not contain OAuth tokens.
3 changes: 3 additions & 0 deletions plugins/antigravity-ide/icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
238 changes: 238 additions & 0 deletions plugins/antigravity-ide/plugin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
(function () {
var LS_SERVICE = "exa.language_server_pb.LanguageServerService"
var CC_MODEL_BLACKLIST = {
"MODEL_CHAT_20706": true,
"MODEL_CHAT_23310": true,
"MODEL_GOOGLE_GEMINI_2_5_FLASH": true,
"MODEL_GOOGLE_GEMINI_2_5_FLASH_THINKING": true,
"MODEL_GOOGLE_GEMINI_2_5_FLASH_LITE": true,
"MODEL_GOOGLE_GEMINI_2_5_PRO": true,
"MODEL_PLACEHOLDER_M19": true,
"MODEL_PLACEHOLDER_M9": true,
"MODEL_PLACEHOLDER_M12": true,
}

// --- LS discovery ---

function discoverLs(ctx) {
return ctx.host.ls.discover({
processName: "language_server_macos",
markers: ["antigravity-ide"],
csrfFlag: "--csrf_token",
portFlag: "--extension_server_port",
})
}

function probePort(ctx, scheme, port, csrf) {
ctx.host.http.request({
method: "POST",
url: scheme + "://127.0.0.1:" + port + "/" + LS_SERVICE + "/GetUnleashData",
headers: {
"Content-Type": "application/json",
"Connect-Protocol-Version": "1",
"x-codeium-csrf-token": csrf,
},
bodyText: JSON.stringify({
context: {
properties: {
devMode: "false",
extensionVersion: "unknown",
ide: "antigravity-ide",
ideVersion: "unknown",
os: "macos",
},
},
}),
timeoutMs: 5000,
dangerouslyIgnoreTls: scheme === "https",
})
// Any HTTP response means this port is alive (even 400 validation errors).
return true
}

function findWorkingPort(ctx, discovery) {
var ports = discovery.ports || []
for (var i = 0; i < ports.length; i++) {
var port = ports[i]
// Try HTTPS first (LS may use self-signed cert), then HTTP
try { if (probePort(ctx, "https", port, discovery.csrf)) return { port: port, scheme: "https" } } catch (e) { /* ignore */ }
try { if (probePort(ctx, "http", port, discovery.csrf)) return { port: port, scheme: "http" } } catch (e) { /* ignore */ }
ctx.host.log.info("port " + port + " probe failed on both schemes")
}
if (discovery.extensionPort) return { port: discovery.extensionPort, scheme: "http" }
return null
}

function callLs(ctx, port, scheme, csrf, method, body) {
var resp = ctx.host.http.request({
method: "POST",
url: scheme + "://127.0.0.1:" + port + "/" + LS_SERVICE + "/" + method,
headers: {
"Content-Type": "application/json",
"Connect-Protocol-Version": "1",
"x-codeium-csrf-token": csrf,
},
bodyText: JSON.stringify(body || {}),
timeoutMs: 10000,
dangerouslyIgnoreTls: scheme === "https",
})
if (resp.status < 200 || resp.status >= 300) {
ctx.host.log.warn("callLs " + method + " returned " + resp.status)
return null
}
return ctx.util.tryParseJson(resp.bodyText)
}

// --- Line builders ---

function normalizeLabel(label) {
// "Gemini 3 Pro (High)" -> "Gemini 3 Pro"
return label.replace(/\s*\([^)]*\)\s*$/, "").trim()
}

function poolLabel(normalizedLabel) {
var lower = normalizedLabel.toLowerCase()
if (lower.indexOf("gemini") !== -1 && lower.indexOf("pro") !== -1) return "Gemini Pro"
if (lower.indexOf("gemini") !== -1 && lower.indexOf("flash") !== -1) return "Gemini Flash"
// All non-Gemini models (Claude, GPT-OSS, etc.) share a single quota pool
return "Claude"
}

function modelSortKey(label) {
var lower = label.toLowerCase()
// Gemini Pro variants first, then other Gemini, then Claude Opus, then other Claude, then rest
if (lower.indexOf("gemini") !== -1 && lower.indexOf("pro") !== -1) return "0a_" + label
if (lower.indexOf("gemini") !== -1) return "0b_" + label
if (lower.indexOf("claude") !== -1 && lower.indexOf("opus") !== -1) return "1a_" + label
if (lower.indexOf("claude") !== -1) return "1b_" + label
return "2_" + label
}

var QUOTA_PERIOD_MS = 5 * 60 * 60 * 1000 // 5 hours

function modelLine(ctx, label, remainingFraction, resetTime) {
var clamped = Math.max(0, Math.min(1, remainingFraction))
var used = Math.round((1 - clamped) * 100)
return ctx.line.progress({
label: label,
used: used,
limit: 100,
format: { kind: "percent" },
resetsAt: resetTime || undefined,
periodDurationMs: QUOTA_PERIOD_MS,
})
}

function buildModelLines(ctx, configs) {
var deduped = {}
for (var i = 0; i < configs.length; i++) {
var c = configs[i]
var label = (typeof c.label === "string") ? c.label.trim() : ""
if (!label) continue
var qi = c.quotaInfo
var frac = (qi && typeof qi.remainingFraction === "number") ? qi.remainingFraction : 0
var rtime = (qi && qi.resetTime) || undefined
var pool = poolLabel(normalizeLabel(label))
if (!deduped[pool] || frac < deduped[pool].remainingFraction) {
deduped[pool] = {
label: pool,
remainingFraction: frac,
resetTime: rtime,
}
}
}

var models = []
var keys = Object.keys(deduped)
for (var i = 0; i < keys.length; i++) {
var m = deduped[keys[i]]
m.sortKey = modelSortKey(m.label)
models.push(m)
}

models.sort(function (a, b) {
return a.sortKey < b.sortKey ? -1 : a.sortKey > b.sortKey ? 1 : 0
})

var lines = []
for (var i = 0; i < models.length; i++) {
lines.push(modelLine(ctx, models[i].label, models[i].remainingFraction, models[i].resetTime))
}
return lines
}

// --- Probe ---

function probe(ctx) {
var discovery = discoverLs(ctx)
if (!discovery) throw "Start Antigravity IDE and try again."

var found = findWorkingPort(ctx, discovery)
if (!found) throw "Start Antigravity IDE and try again."

ctx.host.log.info("using LS at " + found.scheme + "://127.0.0.1:" + found.port)

var metadata = {
ideName: "antigravity-ide",
extensionName: "antigravity-ide",
ideVersion: "unknown",
locale: "en",
}

// Try GetUserStatus first, fall back to GetCommandModelConfigs
var data = null
try {
data = callLs(ctx, found.port, found.scheme, discovery.csrf, "GetUserStatus", { metadata: metadata })
} catch (e) {
ctx.host.log.warn("GetUserStatus threw: " + String(e))
}
var hasUserStatus = data && data.userStatus

if (!hasUserStatus) {
ctx.host.log.warn("GetUserStatus failed, trying GetCommandModelConfigs")
data = callLs(ctx, found.port, found.scheme, discovery.csrf, "GetCommandModelConfigs", { metadata: metadata })
}

// Parse model configs
var configs
if (hasUserStatus) {
configs = (data.userStatus.cascadeModelConfigData || {}).clientModelConfigs || []
} else if (data && data.clientModelConfigs) {
configs = data.clientModelConfigs
} else {
throw "Start Antigravity IDE and try again."
}

var filtered = []
for (var j = 0; j < configs.length; j++) {
var mid = configs[j].modelOrAlias && configs[j].modelOrAlias.model
if (mid && CC_MODEL_BLACKLIST[mid]) continue
filtered.push(configs[j])
}

var lines = buildModelLines(ctx, filtered)
if (lines.length === 0) throw "Start Antigravity IDE and try again."

var plan = null
if (hasUserStatus) {
// Prefer userTier.name (Google's own subscription system) over the legacy
// planInfo.planName field inherited from Windsurf/Codeium, which always
// returns "Pro" for all paid tiers including Google AI Ultra.
var ut = data.userStatus.userTier
var userTierName =
ut && typeof ut.name === "string" && ut.name.trim() ? ut.name.trim() : null
if (userTierName) {
plan = userTierName
} else {
var ps = data.userStatus.planStatus || {}
var pi = ps.planInfo || {}
plan =
typeof pi.planName === "string" && pi.planName.trim() ? pi.planName.trim() : null
}
}

return { plan: plan, lines: lines }
}

globalThis.__openusage_plugin = { id: "antigravity-ide", probe: probe }
})()
27 changes: 27 additions & 0 deletions plugins/antigravity-ide/plugin.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"schemaVersion": 1,
"id": "antigravity-ide",
"name": "Antigravity IDE",
"version": "0.0.1",
"entry": "plugin.js",
"icon": "icon.svg",
"brandColor": "#000000",
"lines": [
{
"type": "progress",
"label": "Gemini Pro",
"scope": "overview",
"primaryOrder": 1
},
{
"type": "progress",
"label": "Gemini Flash",
"scope": "overview"
},
{
"type": "progress",
"label": "Claude",
"scope": "overview"
}
]
}
Loading
Loading