Skip to content

Use Antigravity 2.0 language server quotas#141

Open
Daltonganger wants to merge 3 commits into
opgginc:mainfrom
Daltonganger:fix/antigravity-2-language-server
Open

Use Antigravity 2.0 language server quotas#141
Daltonganger wants to merge 3 commits into
opgginc:mainfrom
Daltonganger:fix/antigravity-2-language-server

Conversation

@Daltonganger
Copy link
Copy Markdown
Contributor

Overview

This PR updates the Antigravity provider to use the local Antigravity 2.0 language server as its primary quota source.

The previous implementation depended on older cache/keychain quota data and exposed stale raw model IDs such as gemini-2.5-*. Antigravity 2.0 exposes the current model quota data through its local language server, which is the same source used by the Antigravity UI.

Changes

  • Detect the running Antigravity language server process.
  • Extract the local CSRF token from process arguments.
  • Discover the local HTTP language-server port.
  • Fetch current model quota data from:
    • /exa.language_server_pb.LanguageServerService/GetAvailableModels
  • Parse current model labels, quota remaining fractions, and reset times.
  • Display current Antigravity quota rows:
    • Claude Sonnet 4.6 (Thinking)
    • Claude Opus 4.6 (Thinking)
    • GPT-OSS 120B (Medium)
    • Gemini 3.5 Flash (High)
    • Gemini 3.5 Flash (Medium)
    • Gemini 3.1 Pro (High)
    • Gemini 3.1 Pro (Low)
  • Keep older cache, keychain, and account paths as fallbacks.
  • Add keychain fallback for Antigravity credentials stored under gemini / antigravity.
  • Add debug logging for endpoint detection and selected quota source.

Validation

  • SwiftLint passed with 0 violations.
  • Full macOS test suite passed locally.
  • Runtime verification on the combined local build confirmed:
    • Antigravity fetches through the local Antigravity 2.0 language server.
    • Sonnet, Opus, GPT-OSS, and current Gemini rows are displayed.
    • Old raw model IDs are no longer shown.
    • Menu shows Token From: Antigravity 2.0 Language Server.

@op-gg-ai-devops
Copy link
Copy Markdown
Contributor

op-gg-ai-devops Bot commented May 20, 2026

✅ AI Code Review Completed

Review finished. Check the PR for inline comments.


📋 View Logs | 🤖 Model: anthropic/claude-opus-4-7|high

Copy link
Copy Markdown
Contributor

@op-gg-ai-devops op-gg-ai-devops Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice work bringing back the Antigravity 2.0 language server path with a clean 4-layer fallback chain (LS → cache → keychain → accounts). the runCommandAsync defer { cancel + terminate } cleanup is a solid improvement over the previous "only run on the happy path" version — that one definitely leaked processes on throws.

a few things worth fixing before this ships though, and one of them is genuinely concerning:

blocker

  • the display label table is wrong (antigravityDisplayLabel, lines 723-736). gemini-2.5-flash → "Gemini 3.5 Flash"? gemini-2.5-pro → "Gemini 3.1 Pro"? gemini-3.1-flash-**lite** → "Gemini 3.1 Pro (Low)"? there is no Gemini 3.5, and flash-lite mapped to a Pro label is just crossed wires. this will show wrong model names to every user on the cache and accounts-fallback paths (the LS path uses displayName from the server, so it dodges this, which is probably why it slipped past local testing).

worth tightening

  • the LS endpoint discovery does the gRPC POST twice — once inside isLanguageServerEndpointAvailable to probe, then again inside fetchFromLanguageServer. easy win: have the probe return the parsed payload directly so you fetch once.
  • the file-level doc comment still says "This no longer relies on the localhost language server API" — but the whole point of this PR is that it does, as the primary path. quick docstring update.
  • --csrf_token is parsed assuming space-separated form. if Antigravity ever ships it as --csrf_token=VALUE (or any other key=value shape), the lookup falls through to the cache path silently. cheap to harden.

nits / fyi (no need to change)

  • Int(minRemaining) truncates fractional percent (99.6 → 99). same pattern exists in the original accounts-fallback path so probably a deliberate file-wide choice, but worth knowing.
  • Array(NSOrderedSet(array: visibleIDs)) as? [String] ?? visibleIDs is ObjC-bridge-y. a tiny var seen: Set<String> = []; ids.filter { seen.insert($0).inserted } reads cleaner, totally optional.
  • couldn't run a smoke build here (Linux runner, no Xcode) so type-checking is on CI / your local build.

once the label table is fixed this is good to go.

guard let pid = parts.first, let csrfIndex = parts.firstIndex(of: "--csrf_token"), parts.indices.contains(csrfIndex + 1) else {
throw ProviderError.decodingError("Unable to parse Antigravity language server process")
}
let csrfToken = parts[csrfIndex + 1]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: Bug csrf_token parsing assumes space-separated form; --csrf_token=VALUE silently breaks LS path
CSRF token is parsed assuming --csrf_token VALUE with a space separator. if Antigravity ever switches to --csrf_token=VALUE (very common Chromium/Electron convention) firstIndex(of: "--csrf_token") returns nil and the whole LS path silently throws decodingError and falls through to cache. you'll only notice when usage in the menu suddenly looks stale.

worth handling both forms — e.g. scan parts for any element starting with --csrf_token and split on = if present, else take the next token.

Comment on lines +160 to +162
private func fetchFromLanguageServer() async throws -> ProviderResult {
let endpoint = try await resolveLanguageServerEndpoint()
let parsed = try await fetchLanguageServerModels(endpoint: endpoint)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Major: Perf double HTTP call to GetAvailableModels: probe fetches the payload, then we throw it away and fetch again
isLanguageServerEndpointAvailable already does a full POST /GetAvailableModels to probe each candidate port, then fetchFromLanguageServer calls fetchLanguageServerModels again on the chosen endpoint. so for the happy path you're paying for two identical HTTP round-trips per fetch (every 30s by default).

simplest fix: have resolveLanguageServerEndpoint return both the endpoint and the parsed payload from the successful probe, then fetchFromLanguageServer just consumes that. something like:

private struct ResolvedEndpoint {
    let endpoint: AntigravityLanguageServerEndpoint
    let parsed: AntigravityParsedCacheUsage
}

then drop the second fetchLanguageServerModels call entirely.

Comment on lines +723 to +736
private func antigravityDisplayLabel(for rawLabel: String) -> String {
switch rawLabel.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() {
case "gemini-2.5-flash":
return "Gemini 3.5 Flash (High)"
case "gemini-2.5-flash-lite":
return "Gemini 3.5 Flash (Medium)"
case "gemini-2.5-pro":
return "Gemini 3.1 Pro (High)"
case "gemini-3.1-flash-lite":
return "Gemini 3.1 Pro (Low)"
default:
return rawLabel
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Critical: Bug wrong Gemini version labels: cache + fallback paths show non-existent model names to users
this whole table is showing wrong model names. there is no public "Gemini 3.5" — both Google's release blog and the Antigravity model catalog use "Gemini 3 Pro" and "Gemini 3 Flash". so:

  • gemini-2.5-flash → "Gemini 3.5 Flash (High)" — 3.5 doesn't exist
  • gemini-2.5-flash-lite → "Gemini 3.5 Flash (Medium)" — same, and -lite becoming "(Medium)" is a separate guess
  • gemini-2.5-pro → "Gemini 3.1 Pro (High)" — should be "3" or "2.5", not "3.1"
  • gemini-3.1-flash-lite → "Gemini 3.1 Pro (Low)" — flash-lite is not Pro, this row is fully crossed

this label function is invoked from both parseQuotaBuckets (accounts + keychain fallback) and parseCachedUsage (cache path), so every user not on the live LS path will see the wrong model names in the menu. the LS path is the only one that escapes this because it reads displayName straight from the server.

suggested fix: either drop the rewrite entirely and return rawLabel (the API IDs are already self-explanatory), or map carefully to the real catalog — gemini-2.5-flash → "Gemini 2.5 Flash", gemini-2.5-pro → "Gemini 2.5 Pro", gemini-3-flash → "Gemini 3 Flash", gemini-3-pro → "Gemini 3 Pro", with the tier suffix only when the input actually carries one.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e76f0afe18

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

executableURL: URL(fileURLWithPath: "/bin/sh"),
arguments: [
"-lc",
"ps ax -o pid= -o command= | grep '/Applications/Antigravity.app/Contents/Resources/bin/language_server' | grep -v grep | head -1"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Resolve server process without a fixed app bundle path

The language-server discovery command only matches processes launched from /Applications/Antigravity.app/..., so installs running from a different bundle path (for example ~/Applications, renamed app bundles, or side-by-side versioned bundles) will never be detected. In those environments fetchFromLanguageServer() always fails and the provider silently falls back to cache/accounts data, which regresses quota freshness/accuracy for users who are actually running Antigravity 2.0.

Useful? React with 👍 / 👎.

@op-gg-ai-devops
Copy link
Copy Markdown
Contributor

op-gg-ai-devops Bot commented May 20, 2026

✅ AI Code Review Completed

Review finished. Check the PR for inline comments.


📋 View Logs | 🤖 Model: anthropic/claude-opus-4-7|high

Copy link
Copy Markdown
Contributor

@op-gg-ai-devops op-gg-ai-devops Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice follow-up — the previous round's blockers are all addressed cleanly:

  • ✅ the label table is fixed (gemini-2.5-flash → "Gemini 2.5 Flash", gemini-2.5-pro → "Gemini 2.5 Pro", gemini-3.1-flash-lite → "Gemini 3.1 Flash Lite"). no more imaginary "Gemini 3.5" leaking into the menu for cache / accounts users.
  • ✅ the double GetAvailableModels round-trip is gone — resolveLanguageServerEndpoint now returns AntigravityResolvedLanguageServer { endpoint, parsedUsage } and fetchFromLanguageServer just consumes the cached payload. clean abstraction.
  • parseCSRFToken now handles both --csrf_token VALUE and --csrf_token=VALUE. good defensive parsing.
  • ✅ file-level doc comment finally matches reality ("...uses the local Antigravity 2.0 language server first").
  • ✅ Codex's bundle-path concern is also handled — process discovery now greps --app_data_dir antigravity instead of /Applications/Antigravity.app/..., so Homebrew / ~/Applications / renamed bundles work.
  • runCommandAsync cleanup moved into a defer { group.cancelAll(); if process.isRunning { process.terminate() } }, so the leak on throw is closed off too.

one new thing worth fixing before merge though — see inline.

blocker

  • keychain-fallback endpoint URL looks wrong (line 441). https://daily-cloudcode-pa.googleapis.com/v1internal:retrieveUserQuota is not Google's prod endpoint and not the sandbox one either — prod is https://cloudcode-pa.googleapis.com (used everywhere else in this file and in GeminiCLIProvider), and the actual sandbox is https://daily-cloudcode-pa.sandbox.googleapis.com (note the .sandbox infix). this is almost certainly a typo. left as-is the keychain fallback will silently fail and degrade to the accounts fallback for everyone whose cache is unavailable.

nits / fyi (no need to change)

  • parseListeningPorts regex would also extract the port from an [::1]:1234 (LISTEN) line, but we always probe http://127.0.0.1:port. if the language server ever binds v6-only this layer fails silently and we fall through. not a regression vs. the original PR, just something to keep in mind.
  • Int(minRemaining) still truncates fractional percent (99.6 → 99) — consistent with the rest of the file, just flagging.
  • couldn't run a smoke build here (Linux runner, no Xcode), so type-checking is on CI / your local build.

once the URL is fixed this is good to go.

throw ProviderError.authenticationFailed("Unable to refresh Antigravity keychain token")
}

guard let url = URL(string: "https://daily-cloudcode-pa.googleapis.com/v1internal:retrieveUserQuota") else {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Major: Bug keychain-fallback URL: daily-cloudcode-pa.googleapis.com is not the prod or sandbox endpoint
this URL is almost certainly wrong. the rest of this file (line 535) and GeminiCLIProvider.swift:270 both hit https://cloudcode-pa.googleapis.com/v1internal:retrieveUserQuota (prod). Google's actual daily/sandbox cluster is https://daily-cloudcode-pa.sandbox.googleapis.com — note the .sandbox infix that's missing here. daily-cloudcode-pa.googleapis.com does DNS-resolve (it points at Google's API GFEs), but it isn't part of the documented Antigravity endpoint set (CLIProxy/Vibeproxy/Picoclaw/anti-gravity-quota all use prod or the .sandbox form).

practical effect: if a user ever lands on this fallback (LS not running + cache fetch failed + keychain token present), the request will either 404 or just throw, and we'll degrade to the accounts fallback silently. for keychain-only users (no antigravity-accounts.json) this means no quota at all instead of cached data.

guard let url = URL(string: "https://cloudcode-pa.googleapis.com/v1internal:retrieveUserQuota") else {

if you actually intended to hit the daily sandbox for testing, use https://daily-cloudcode-pa.sandbox.googleapis.com/v1internal:retrieveUserQuota and leave a TODO — but prod is almost certainly what you want here.

@op-gg-ai-devops
Copy link
Copy Markdown
Contributor

op-gg-ai-devops Bot commented May 20, 2026

✅ AI Code Review Completed

Review finished. Check the PR for inline comments.


📋 View Logs | 🤖 Model: anthropic/claude-opus-4-7|high

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 49eae005c2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +221 to +222
guard let baseURL = URL(string: "http://127.0.0.1:\(port)") else { continue }
if let parsedUsage = await probeLanguageServerEndpoint(baseURL: baseURL, csrfToken: csrfToken) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Probe HTTPS language-server endpoints before fallback

resolveLanguageServerEndpoint() currently probes candidate ports only as http://127.0.0.1:<port>, so installs where the Antigravity local server accepts TLS-only requests will always fail language-server fetch and silently degrade to cache/keychain/accounts data. The repository’s own diagnostics (scripts/query-antigravity-server.sh) explicitly tries https before http, which indicates both transports must be supported for reliable detection.

Useful? React with 👍 / 👎.

Comment on lines +515 to +517
let payload = try JSONDecoder().decode(AntigravityKeychainPayload.self, from: jsonData)
guard let refreshToken = nonEmptyTrimmed(payload.token?.refreshToken) else {
throw ProviderError.authenticationFailed("Antigravity keychain refresh token is missing")
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Accept common keychain token payload shapes

loadKeychainRefreshToken() only accepts a nested token.refresh_token JSON structure, so valid keychain entries in other known shapes (for example top-level refreshToken/refresh_token or plain token strings) are treated as missing credentials. That makes the new keychain fallback path fail even when usable credentials are present, especially for users without a healthy antigravity-accounts.json fallback.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor

@op-gg-ai-devops op-gg-ai-devops Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice iteration — the production endpoint fix (daily-cloudcode-pa.googleapis.comcloudcode-pa.googleapis.com) lands the previous blocker cleanly, and the rest of the four-layer fallback chain (LS → cache → keychain → accounts) is holding up well after three rounds of review.

quick rundown of what landed:

  • ✅ keychain-fallback URL is now the real production endpoint, matching GeminiCLIProvider and the accounts-fallback path
  • runCommandAsync cleanup via defer { cancelAll + terminate } still solid
  • ✅ display label table (Gemini 2.5 Flash / Pro / Flash Lite, Gemini 3.1 Flash Lite) is correct
  • parseCSRFToken handles both --csrf_token VALUE and --csrf_token=VALUE
  • resolveLanguageServerEndpoint returns parsed usage so we don't double-POST GetAvailableModels
  • parseISO8601Date handles both fractional and non-fractional seconds (matches the codebase pattern)

one thing worth a second look before merge — see inline. nothing scary, more of a "this fallback might silently return partial data" concern.

nits / fyi (no need to change)

  • still no unit tests for parseCSRFToken, parseListeningPorts, antigravityDisplayLabel, languageServerModelsURL, or preferredLanguageServerModelIDs — these are all pure functions and would be cheap regression coverage if you want to do a follow-up
  • Int(minRemaining) truncation (99.6 → 99) is still here, consistent with the rest of the file, just flagging
  • parseListeningPorts would also pull a port out of an [::1]:1234 (LISTEN) line — we always probe http://127.0.0.1:port so a v6-only bind would fall through. carried over from earlier rounds, just FYI
  • couldn't run a smoke build here (Linux runner, no Xcode) — SwiftLint is green on CI, build is still in progress at time of review

once the keychain-fallback body question is settled this is good to go.

request.httpMethod = "POST"
request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = "{}".data(using: .utf8)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: Bug keychain fallback POSTs {} without project, may return partial buckets
small concern on this fallback: the POST body here is {} with no project field, but GeminiCLIProvider.fetchQuotaForAccount calls the same cloudcode-pa.googleapis.com/v1internal:retrieveUserQuota endpoint with an explicit comment that says:

// project parameter is required to get all models including gemini-3 variants
request.httpBody = "{\"project\":\"\(projectId)\"}".data(using: .utf8)

so two ways this can play out for someone whose cache path failed:

  1. the endpoint resolves the user's default project from the access token's scope → fallback returns buckets minus gemini-3 → we accept it (the !buckets.isEmpty check at line 465 passes) → keychain path "succeeds" with incomplete data and the accounts-fallback (which DOES pass project) is never tried
  2. the endpoint refuses without a project → 4xx or empty buckets → we throw and fall through to accounts-fallback anyway, so just an extra round trip

case 1 is the unhappy one — partial model breakdown in the UI with no signal that something's incomplete.

if you can pull a project ID from somewhere reachable in this path (the antigravity-accounts.json read in resolveFallbackAccount is right there, or you could parse it out of the cached userStatusProtoBinaryBase64/state.vscdb even when the proto-cache decode of quota data failed), it'd close this gap. otherwise a one-line comment saying "this fallback intentionally omits project; partial buckets are accepted" is enough — at least future readers know it's deliberate.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant