Skip to content
Open
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
9 changes: 9 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ on:
paths:
- '**.swift'
- '**.xcodeproj/**'
- 'scripts/claude-version.sh'
- 'scripts/query-claude.sh'
- 'scripts/tests/query-claude-version-test.sh'
- '.github/workflows/test.yml'
pull_request:
branches:
Expand All @@ -16,6 +19,9 @@ on:
paths:
- '**.swift'
- '**.xcodeproj/**'
- 'scripts/claude-version.sh'
- 'scripts/query-claude.sh'
- 'scripts/tests/query-claude-version-test.sh'
- '.github/workflows/test.yml'
workflow_dispatch:

Expand All @@ -27,6 +33,9 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Test Claude version detection
run: bash scripts/tests/query-claude-version-test.sh

- name: Setup Xcode
uses: maxim-lobanov/setup-xcode@v1
Expand Down
4 changes: 4 additions & 0 deletions CopilotMonitor/CopilotMonitor.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
0B6CF8BEB936A2055F19AA8C /* CodexProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76783C44AA2329AE3FA7E981 /* CodexProvider.swift */; };
0EA5E7A5BCD47582D6C2175B /* TokenManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBF0C48368166F3AD0C79F92 /* TokenManager.swift */; };
23AA5FD37F2062EACCA79F50 /* GeminiCLIProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B63B85E0F2A8C591D7E26AE2 /* GeminiCLIProviderTests.swift */; };
ANTIGRAVITYTESTBF11111 /* AntigravityProviderVarintTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ANTIGRAVITYTESTFR11111 /* AntigravityProviderVarintTests.swift */; };
27A0B56ADAC5B75A3270F90D /* CopilotProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4AE672516E564CC52739BD9 /* CopilotProvider.swift */; };
283348F92F313096004DADE1 /* ProviderResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = PR2222222222222222222222 /* ProviderResult.swift */; };
283348FA2F313096004DADE1 /* UsageHistory.swift in Sources */ = {isa = PBXBuildFile; fileRef = E44444444444444444444444 /* UsageHistory.swift */; };
Expand Down Expand Up @@ -186,6 +187,7 @@
AD0CA52627AF05BD67B56E /* StatusBarIconView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusBarIconView.swift; sourceTree = "<group>"; };
AM2222222222222222222222 /* AppMigrationHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppMigrationHelper.swift; sourceTree = "<group>"; };
B63B85E0F2A8C591D7E26AE2 /* GeminiCLIProviderTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GeminiCLIProviderTests.swift; sourceTree = "<group>"; };
ANTIGRAVITYTESTFR11111 /* AntigravityProviderVarintTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AntigravityProviderVarintTests.swift; sourceTree = "<group>"; };
BE39D5663A7ABADC7E54B91D /* ProviderProtocol.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ProviderProtocol.swift; sourceTree = "<group>"; };
C1C7C9FDB4600114F02B2915 /* MenuDesignToken.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MenuDesignToken.swift; sourceTree = "<group>"; };
C8493409A9188CFF5B73D5B3 /* MenuDesignTokenTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MenuDesignTokenTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -445,6 +447,7 @@
T22222222222222222222222 /* ProviderUsageTests.swift */,
TOOOOOOOOOOOOOOOOOOOOOOO /* Fixtures */,
B63B85E0F2A8C591D7E26AE2 /* GeminiCLIProviderTests.swift */,
ANTIGRAVITYTESTFR11111 /* AntigravityProviderVarintTests.swift */,
ED35D8BD06A436306085737D /* CopilotProviderTests.swift */,
OR3333333333333333333333 /* OpenRouterProviderTests.swift */,
2D2B4000E33060BA5E00351B /* DependencyTests.swift */,
Expand Down Expand Up @@ -704,6 +707,7 @@
files = (
T11111111111111111111111 /* ProviderUsageTests.swift in Sources */,
23AA5FD37F2062EACCA79F50 /* GeminiCLIProviderTests.swift in Sources */,
ANTIGRAVITYTESTBF11111 /* AntigravityProviderVarintTests.swift in Sources */,
632E52F6050FAD5CAF928E36 /* CopilotProviderTests.swift in Sources */,
OR4444444444444444444444 /* OpenRouterProviderTests.swift in Sources */,
668B6906C95903D51823808A /* DependencyTests.swift in Sources */,
Expand Down
101 changes: 40 additions & 61 deletions CopilotMonitor/CopilotMonitor/Providers/AntigravityProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -166,41 +166,19 @@ final class AntigravityProvider: ProviderProtocol {
private func fetchFromAccountsFallback(cacheError: Error) async throws -> ProviderResult {
guard let account = resolveFallbackAccount() else {
throw ProviderError.providerError(
"Antigravity cache unavailable and no enabled antigravity-accounts.json account with project ID was found"
"Antigravity cache unavailable and no enabled antigravity-accounts.json account with a refresh token was found"
)
}

guard let accessToken = await tokenManager.refreshGeminiAccessToken(refreshToken: account.refreshToken) else {
throw ProviderError.authenticationFailed("Unable to refresh Antigravity fallback token")
}

guard let url = URL(string: "https://cloudcode-pa.googleapis.com/v1internal:retrieveUserQuota") else {
throw ProviderError.networkError("Invalid API endpoint")
}

var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = "{\"project\":\"\(account.projectId)\"}".data(using: .utf8)

let (data, response) = try await session.data(for: request)
guard let httpResponse = response as? HTTPURLResponse else {
throw ProviderError.networkError("Invalid response type")
}

if httpResponse.statusCode == 401 {
throw ProviderError.authenticationFailed("Antigravity fallback token expired")
}

guard (200...299).contains(httpResponse.statusCode) else {
throw ProviderError.networkError("HTTP \(httpResponse.statusCode)")
}

let quotaResponse = try JSONDecoder().decode(GeminiQuotaResponse.self, from: data)
guard !quotaResponse.buckets.isEmpty else {
throw ProviderError.decodingError("Empty buckets array")
}
let quotaResponse = try await GeminiQuotaAPI.fetchQuota(
accessToken: accessToken,
projectId: account.projectId,
session: session
)

let parsed = parseQuotaBuckets(quotaResponse.buckets)
let minRemaining = parsed.modelBreakdown.values.min() ?? 0.0
Expand Down Expand Up @@ -240,46 +218,22 @@ final class AntigravityProvider: ProviderProtocol {
return nil
}

let preferredIndexes: [Int?] = [
antigravityAccounts.activeIndexByFamily?["gemini"],
antigravityAccounts.activeIndex
]

func accountAtPreferredIndex() -> AntigravityAccounts.Account? {
for preferredIndex in preferredIndexes {
guard let index = preferredIndex,
antigravityAccounts.accounts.indices.contains(index) else {
continue
}

let account = antigravityAccounts.accounts[index]
if account.enabled == false {
continue
}

return account
}

return antigravityAccounts.accounts.first(where: { $0.enabled != false })
}

guard let account = accountAtPreferredIndex() else {
logger.warning("Antigravity fallback unavailable: no enabled account found")
guard let account = Self.selectFallbackAccount(from: antigravityAccounts) else {
logger.warning("Antigravity fallback unavailable: no enabled account with a refresh token found")
return nil
}

let refreshToken = account.refreshToken?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
guard !refreshToken.isEmpty else {
guard let refreshToken = nonEmptyTrimmed(account.refreshToken) else {
logger.warning("Antigravity fallback unavailable: selected account is missing refresh token")
return nil
}

let primaryProjectId = account.projectId?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
let fallbackProjectId = account.managedProjectId?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
let projectId = primaryProjectId.isEmpty ? fallbackProjectId : primaryProjectId
guard !projectId.isEmpty else {
logger.warning("Antigravity fallback unavailable: selected account is missing project ID")
return nil
let projectId = GeminiProjectPolicy.resolve(
Comment thread
Daltonganger marked this conversation as resolved.
primary: account.projectId,
fallback: account.managedProjectId
)
if projectId == GeminiProjectPolicy.fallbackProjectId {
logger.info("Antigravity fallback account has no project ID; using default project fallback")
}

return AntigravityFallbackAccount(
Expand All @@ -294,6 +248,31 @@ final class AntigravityProvider: ProviderProtocol {
)
}

static func selectFallbackAccount(from accounts: AntigravityAccounts) -> AntigravityAccounts.Account? {
let preferredIndexes: [Int?] = [
accounts.activeIndexByFamily?["gemini"],
accounts.activeIndex
]
var orderedIndexes = preferredIndexes.compactMap { $0 }
orderedIndexes.append(contentsOf: accounts.accounts.indices)
var visitedIndexes = Set<Int>()

for index in orderedIndexes {
guard visitedIndexes.insert(index).inserted,
accounts.accounts.indices.contains(index) else {
continue
}

let account = accounts.accounts[index]
let refreshToken = account.refreshToken?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
if account.enabled != false, !refreshToken.isEmpty {
return account
}
}

return nil
}

private func parseQuotaBuckets(_ buckets: [GeminiQuotaResponse.Bucket]) -> AntigravityParsedCacheUsage {
var modelBreakdown: [String: Double] = [:]
var modelResetTimes: [String: Date] = [:]
Expand Down
Loading
Loading