chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:22:33 +08:00
commit fc4fcbab58
1848 changed files with 472303 additions and 0 deletions
@@ -0,0 +1,327 @@
import Foundation
public struct VertexAIOAuthCredentials: Sendable {
public let accessToken: String
public let refreshToken: String
public let clientId: String
public let clientSecret: String
public let projectId: String?
public let email: String?
public let expiryDate: Date?
public init(
accessToken: String,
refreshToken: String,
clientId: String,
clientSecret: String,
projectId: String?,
email: String?,
expiryDate: Date?)
{
self.accessToken = accessToken
self.refreshToken = refreshToken
self.clientId = clientId
self.clientSecret = clientSecret
self.projectId = projectId
self.email = email
self.expiryDate = expiryDate
}
public var needsRefresh: Bool {
guard let expiryDate else { return true }
// Refresh 5 minutes before expiry
return Date().addingTimeInterval(300) > expiryDate
}
}
public enum VertexAIOAuthCredentialsError: LocalizedError, Sendable {
case notFound
case decodeFailed(String)
case missingTokens
case missingClientCredentials
public var errorDescription: String? {
switch self {
case .notFound:
"gcloud credentials not found. Run `gcloud auth application-default login` to authenticate."
case let .decodeFailed(message):
"Failed to decode gcloud credentials: \(message)"
case .missingTokens:
"gcloud credentials exist but contain no tokens."
case .missingClientCredentials:
"gcloud credentials missing client ID or secret."
}
}
}
public enum VertexAIOAuthCredentialsStore {
#if DEBUG
@TaskLocal static var gcloudAccessTokenOverrideForTesting: (@Sendable ([String: String]) async throws -> String)?
#endif
private struct ServiceAccountMetadata {
let email: String
let projectId: String?
}
private static func credentialsFilePath(
environment: [String: String] = ProcessInfo.processInfo.environment) -> URL
{
if let path = environment["GOOGLE_APPLICATION_CREDENTIALS"]?.trimmingCharacters(
in: .whitespacesAndNewlines),
!path.isEmpty
{
return URL(fileURLWithPath: path)
}
let home = FileManager.default.homeDirectoryForCurrentUser
// gcloud application default credentials location
if let configDir = environment["CLOUDSDK_CONFIG"]?.trimmingCharacters(
in: .whitespacesAndNewlines),
!configDir.isEmpty
{
return URL(fileURLWithPath: configDir)
.appendingPathComponent("application_default_credentials.json")
}
return home
.appendingPathComponent(".config")
.appendingPathComponent("gcloud")
.appendingPathComponent("application_default_credentials.json")
}
private static func projectFilePath(
environment: [String: String] = ProcessInfo.processInfo.environment) -> URL
{
let home = FileManager.default.homeDirectoryForCurrentUser
if let configDir = environment["CLOUDSDK_CONFIG"]?.trimmingCharacters(
in: .whitespacesAndNewlines),
!configDir.isEmpty
{
return URL(fileURLWithPath: configDir)
.appendingPathComponent("configurations")
.appendingPathComponent("config_default")
}
return home
.appendingPathComponent(".config")
.appendingPathComponent("gcloud")
.appendingPathComponent("configurations")
.appendingPathComponent("config_default")
}
public static func hasCredentials(
environment: [String: String] = ProcessInfo.processInfo.environment) -> Bool
{
let url = self.credentialsFilePath(environment: environment)
guard FileManager.default.fileExists(atPath: url.path),
let data = try? Data(contentsOf: url),
let json = try? self.parseJSONObject(data: data)
else {
return false
}
if self.parseServiceAccountMetadata(json: json) != nil {
return true
}
return (try? self.parseUserCredentials(json: json, environment: environment)) != nil
}
public static func load(
environment: [String: String] = ProcessInfo.processInfo.environment) throws -> VertexAIOAuthCredentials
{
let url = self.credentialsFilePath(environment: environment)
guard FileManager.default.fileExists(atPath: url.path) else {
throw VertexAIOAuthCredentialsError.notFound
}
let data = try Data(contentsOf: url)
return try self.parse(data: data, environment: environment)
}
public static func loadForFetch(
environment: [String: String] = ProcessInfo.processInfo.environment) async throws -> VertexAIOAuthCredentials
{
let url = self.credentialsFilePath(environment: environment)
guard FileManager.default.fileExists(atPath: url.path) else {
throw VertexAIOAuthCredentialsError.notFound
}
let data = try Data(contentsOf: url)
let json = try self.parseJSONObject(data: data)
if let serviceAccount = self.parseServiceAccountMetadata(json: json) {
let token = try await self.printAccessToken(environment: environment)
return VertexAIOAuthCredentials(
accessToken: token,
refreshToken: "",
clientId: "",
clientSecret: "",
projectId: serviceAccount.projectId ?? self.loadProjectId(environment: environment),
email: serviceAccount.email,
expiryDate: Date().addingTimeInterval(50 * 60))
}
return try self.parseUserCredentials(json: json, environment: environment)
}
public static func parse(data: Data) throws -> VertexAIOAuthCredentials {
try self.parse(data: data, environment: ProcessInfo.processInfo.environment)
}
public static func parse(
data: Data,
environment: [String: String]) throws -> VertexAIOAuthCredentials
{
let json = try self.parseJSONObject(data: data)
return try self.parseUserCredentials(json: json, environment: environment)
}
private static func parseJSONObject(data: Data) throws -> [String: Any] {
guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] else {
throw VertexAIOAuthCredentialsError.decodeFailed("Invalid JSON")
}
return json
}
private static func parseUserCredentials(
json: [String: Any],
environment: [String: String]) throws -> VertexAIOAuthCredentials
{
// Check for service account credentials
if self.parseServiceAccountMetadata(json: json) != nil {
throw VertexAIOAuthCredentialsError.decodeFailed(
"Service account credentials require `gcloud auth application-default print-access-token`.")
}
// User credentials from gcloud auth application-default login
guard let clientId = json["client_id"] as? String,
let clientSecret = json["client_secret"] as? String
else {
throw VertexAIOAuthCredentialsError.missingClientCredentials
}
guard let refreshToken = json["refresh_token"] as? String, !refreshToken.isEmpty else {
throw VertexAIOAuthCredentialsError.missingTokens
}
// Access token may not be present in the file; we'll need to refresh
let accessToken = json["access_token"] as? String ?? ""
// Try to get project ID from gcloud config
let projectId = Self.loadProjectId(environment: environment)
// Try to extract email from ID token if present
let email = Self.extractEmailFromIdToken(json["id_token"] as? String)
// Parse expiry if present
var expiryDate: Date?
if let expiryStr = json["token_expiry"] as? String {
let formatter = ISO8601DateFormatter()
expiryDate = formatter.date(from: expiryStr)
}
return VertexAIOAuthCredentials(
accessToken: accessToken,
refreshToken: refreshToken,
clientId: clientId,
clientSecret: clientSecret,
projectId: projectId,
email: email,
expiryDate: expiryDate)
}
public static func save(_ credentials: VertexAIOAuthCredentials) throws {
// We don't modify gcloud's credentials file; just cache the access token in memory
// The refresh happens on each app launch if needed
}
private static func parseServiceAccountMetadata(json: [String: Any]) -> ServiceAccountMetadata? {
guard let email = (json["client_email"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines),
!email.isEmpty,
let privateKey = (json["private_key"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines),
!privateKey.isEmpty
else {
return nil
}
let projectId = (json["project_id"] as? String)?
.trimmingCharacters(in: .whitespacesAndNewlines)
return ServiceAccountMetadata(
email: email,
projectId: projectId?.isEmpty == false ? projectId : nil)
}
private static func printAccessToken(environment: [String: String]) async throws -> String {
#if DEBUG
if let override = self.gcloudAccessTokenOverrideForTesting {
let token = try await override(environment)
return try self.cleanAccessToken(token)
}
#endif
let env = TTYCommandRunner.enrichedEnvironment(baseEnv: environment)
let result = try await SubprocessRunner.run(
binary: "/usr/bin/env",
arguments: ["gcloud", "auth", "application-default", "print-access-token"],
environment: env,
timeout: 20,
label: "vertexai-gcloud-adc-token")
return try self.cleanAccessToken(result.stdout)
}
private static func cleanAccessToken(_ token: String) throws -> String {
let trimmed = token.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else {
throw VertexAIOAuthCredentialsError.missingTokens
}
return trimmed
}
private static func loadProjectId(environment: [String: String]) -> String? {
let configPath = self.projectFilePath(environment: environment)
guard let content = try? String(contentsOf: configPath, encoding: .utf8) else {
return environment["GOOGLE_CLOUD_PROJECT"]
?? environment["GCLOUD_PROJECT"]
?? environment["CLOUDSDK_CORE_PROJECT"]
}
// Parse INI-style config for project
for line in content.components(separatedBy: .newlines) {
let trimmed = line.trimmingCharacters(in: .whitespaces)
if trimmed.hasPrefix("project") {
let parts = trimmed.components(separatedBy: "=")
if parts.count >= 2 {
return parts[1].trimmingCharacters(in: .whitespaces)
}
}
}
// Try environment variable
return environment["GOOGLE_CLOUD_PROJECT"]
?? environment["GCLOUD_PROJECT"]
?? environment["CLOUDSDK_CORE_PROJECT"]
}
private static func extractEmailFromIdToken(_ token: String?) -> String? {
guard let token, !token.isEmpty else { return nil }
let parts = token.components(separatedBy: ".")
guard parts.count >= 2 else { return nil }
var payload = parts[1]
.replacingOccurrences(of: "-", with: "+")
.replacingOccurrences(of: "_", with: "/")
let remainder = payload.count % 4
if remainder > 0 {
payload += String(repeating: "=", count: 4 - remainder)
}
guard let data = Data(base64Encoded: payload, options: .ignoreUnknownCharacters),
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
else {
return nil
}
return json["email"] as? String
}
}
@@ -0,0 +1,128 @@
import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
public enum VertexAITokenRefresher {
private static let tokenEndpoint = URL(string: "https://oauth2.googleapis.com/token")!
public enum RefreshError: LocalizedError, Sendable {
case expired
case revoked
case networkError(Error)
case invalidResponse(String)
public var errorDescription: String? {
switch self {
case .expired:
"Refresh token expired. Run `gcloud auth application-default login` again."
case .revoked:
"Refresh token was revoked. Run `gcloud auth application-default login` again."
case let .networkError(error):
"Network error during token refresh: \(error.localizedDescription)"
case let .invalidResponse(message):
"Invalid refresh response: \(message)"
}
}
}
public static func refresh(_ credentials: VertexAIOAuthCredentials) async throws -> VertexAIOAuthCredentials {
guard !credentials.refreshToken.isEmpty else {
throw RefreshError.invalidResponse("No refresh token available")
}
var request = URLRequest(url: Self.tokenEndpoint)
request.httpMethod = "POST"
request.timeoutInterval = 30
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
let bodyParams = [
"client_id": credentials.clientId,
"client_secret": credentials.clientSecret,
"refresh_token": credentials.refreshToken,
"grant_type": "refresh_token",
]
let bodyString = bodyParams
.map { "\($0.key)=\($0.value.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? $0.value)" }
.joined(separator: "&")
request.httpBody = bodyString.data(using: .utf8)
do {
let response = try await ProviderHTTPClient.shared.response(for: request)
let data = response.data
if response.statusCode == 400 || response.statusCode == 401 {
if let errorCode = Self.extractErrorCode(from: data) {
switch errorCode.lowercased() {
case "invalid_grant":
throw RefreshError.expired
case "unauthorized_client":
throw RefreshError.revoked
default:
throw RefreshError.invalidResponse("Error: \(errorCode)")
}
}
throw RefreshError.expired
}
guard response.statusCode == 200 else {
throw RefreshError.invalidResponse("Status \(response.statusCode)")
}
guard let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
throw RefreshError.invalidResponse("Invalid JSON")
}
let newAccessToken = json["access_token"] as? String ?? credentials.accessToken
let expiresIn = json["expires_in"] as? Double ?? 3600
let newExpiryDate = Date().addingTimeInterval(expiresIn)
// Extract email from new ID token if present
let idToken = json["id_token"] as? String
let email = Self.extractEmailFromIdToken(idToken) ?? credentials.email
return VertexAIOAuthCredentials(
accessToken: newAccessToken,
refreshToken: credentials.refreshToken,
clientId: credentials.clientId,
clientSecret: credentials.clientSecret,
projectId: credentials.projectId,
email: email,
expiryDate: newExpiryDate)
} catch let error as RefreshError {
throw error
} catch {
throw RefreshError.networkError(error)
}
}
private static func extractErrorCode(from data: Data) -> String? {
guard let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { return nil }
return json["error"] as? String
}
private static func extractEmailFromIdToken(_ token: String?) -> String? {
guard let token, !token.isEmpty else { return nil }
let parts = token.components(separatedBy: ".")
guard parts.count >= 2 else { return nil }
var payload = parts[1]
.replacingOccurrences(of: "-", with: "+")
.replacingOccurrences(of: "_", with: "/")
let remainder = payload.count % 4
if remainder > 0 {
payload += String(repeating: "=", count: 4 - remainder)
}
guard let data = Data(base64Encoded: payload, options: .ignoreUnknownCharacters),
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
else {
return nil
}
return json["email"] as? String
}
}
@@ -0,0 +1,292 @@
import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
public enum VertexAIFetchError: LocalizedError, Sendable {
case unauthorized
case forbidden
case noProject
case networkError(Error)
case invalidResponse(String)
case noData
public var errorDescription: String? {
switch self {
case .unauthorized:
"Vertex AI request unauthorized. Run `gcloud auth application-default login`."
case .forbidden:
"Access forbidden. Check your IAM permissions for Cloud Monitoring."
case .noProject:
"No Google Cloud project configured. Run `gcloud config set project PROJECT_ID`."
case let .networkError(error):
"Vertex AI network error: \(error.localizedDescription)"
case let .invalidResponse(message):
"Vertex AI response was invalid: \(message)"
case .noData:
"No Vertex AI usage data found for the current project."
}
}
}
public struct VertexAIUsageResponse: Sendable {
public let requestsUsedPercent: Double
public let tokensUsedPercent: Double?
public let resetsAt: Date?
public let resetDescription: String?
public let rawData: String?
public init(
requestsUsedPercent: Double,
tokensUsedPercent: Double?,
resetsAt: Date?,
resetDescription: String?,
rawData: String?)
{
self.requestsUsedPercent = requestsUsedPercent
self.tokensUsedPercent = tokensUsedPercent
self.resetsAt = resetsAt
self.resetDescription = resetDescription
self.rawData = rawData
}
}
public enum VertexAIUsageFetcher {
private static let log = CodexBarLog.logger(LogCategories.vertexAIFetcher)
// Cloud Monitoring API endpoint for time series
private static let monitoringEndpoint = "https://monitoring.googleapis.com/v3/projects"
private static let usageWindowSeconds: TimeInterval = 24 * 60 * 60
public static func fetchUsage(
accessToken: String,
projectId: String?) async throws -> VertexAIUsageResponse
{
guard let projectId, !projectId.isEmpty else {
throw VertexAIFetchError.noProject
}
return try await Self.fetchQuotaUsage(
accessToken: accessToken,
projectId: projectId)
}
private static func fetchQuotaUsage(
accessToken: String,
projectId: String) async throws -> VertexAIUsageResponse
{
let usageFilter = """
metric.type="serviceruntime.googleapis.com/quota/allocation/usage" \
AND resource.type="consumer_quota" \
AND resource.label.service="aiplatform.googleapis.com"
"""
let limitFilter = """
metric.type="serviceruntime.googleapis.com/quota/limit" \
AND resource.type="consumer_quota" \
AND resource.label.service="aiplatform.googleapis.com"
"""
let usageSeries = try await Self.fetchTimeSeries(
accessToken: accessToken,
projectId: projectId,
filter: usageFilter)
let limitSeries = try await Self.fetchTimeSeries(
accessToken: accessToken,
projectId: projectId,
filter: limitFilter)
let usageByKey = Self.aggregate(series: usageSeries)
let limitByKey = Self.aggregate(series: limitSeries)
guard !usageByKey.isEmpty, !limitByKey.isEmpty else {
throw VertexAIFetchError.noData
}
var maxPercent: Double?
var matchedCount = 0
var matchedKeys: Set<QuotaKey> = []
for (key, limit) in limitByKey {
guard limit > 0, let usage = usageByKey[key] else { continue }
matchedKeys.insert(key)
matchedCount += 1
let percent = (usage / limit) * 100.0
maxPercent = max(maxPercent ?? percent, percent)
}
guard let usedPercent = maxPercent, matchedCount > 0 else {
throw VertexAIFetchError.noData
}
let unmatchedUsage = Set(usageByKey.keys).subtracting(matchedKeys).count
let unmatchedLimit = Set(limitByKey.keys).subtracting(matchedKeys).count
Self.log.debug("Quota series preview", metadata: [
"usageKeys": Self.previewKeys(usageByKey),
"limitKeys": Self.previewKeys(limitByKey),
])
Self.log.info("Parsed quota", metadata: [
"usedPercent": "\(usedPercent)",
"usageSeries": "\(usageByKey.count)",
"limitSeries": "\(limitByKey.count)",
"matchedSeries": "\(matchedCount)",
"unmatchedUsage": "\(unmatchedUsage)",
"unmatchedLimit": "\(unmatchedLimit)",
])
return VertexAIUsageResponse(
requestsUsedPercent: usedPercent,
tokensUsedPercent: nil,
resetsAt: nil,
resetDescription: nil,
rawData: nil)
}
private struct MonitoringTimeSeriesResponse: Decodable {
let timeSeries: [MonitoringTimeSeries]?
let nextPageToken: String?
}
private struct MonitoringTimeSeries: Decodable {
let metric: MonitoringMetric
let resource: MonitoringResource
let points: [MonitoringPoint]
}
private struct MonitoringMetric: Decodable {
let type: String?
let labels: [String: String]?
}
private struct MonitoringResource: Decodable {
let type: String?
let labels: [String: String]?
}
private struct MonitoringPoint: Decodable {
let value: MonitoringValue
}
private struct MonitoringValue: Decodable {
let doubleValue: Double?
let int64Value: String?
}
private struct QuotaKey: Hashable {
let quotaMetric: String
let limitName: String
let location: String
}
private static func fetchTimeSeries(
accessToken: String,
projectId: String,
filter: String) async throws -> [MonitoringTimeSeries]
{
let now = Date()
let start = now.addingTimeInterval(-Self.usageWindowSeconds)
let formatter = ISO8601DateFormatter()
var pageToken: String?
var allSeries: [MonitoringTimeSeries] = []
repeat {
guard var components = URLComponents(
string: "\(Self.monitoringEndpoint)/\(projectId)/timeSeries")
else {
throw VertexAIFetchError.invalidResponse("Invalid Monitoring URL")
}
var queryItems = [
URLQueryItem(name: "filter", value: filter),
URLQueryItem(name: "interval.startTime", value: formatter.string(from: start)),
URLQueryItem(name: "interval.endTime", value: formatter.string(from: now)),
URLQueryItem(name: "aggregation.alignmentPeriod", value: "3600s"),
URLQueryItem(name: "aggregation.perSeriesAligner", value: "ALIGN_MAX"),
URLQueryItem(name: "view", value: "FULL"),
]
if let pageToken {
queryItems.append(URLQueryItem(name: "pageToken", value: pageToken))
}
components.queryItems = queryItems
guard let url = components.url else {
throw VertexAIFetchError.invalidResponse("Invalid Monitoring URL")
}
var request = URLRequest(url: url)
request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")
request.timeoutInterval = 30
let response: ProviderHTTPResponse
do {
response = try await ProviderHTTPClient.shared.response(for: request)
} catch {
throw VertexAIFetchError.networkError(error)
}
switch response.statusCode {
case 401:
throw VertexAIFetchError.unauthorized
case 403:
throw VertexAIFetchError.forbidden
case 200:
break
default:
let body = String(data: response.data, encoding: .utf8) ?? ""
throw VertexAIFetchError.invalidResponse("HTTP \(response.statusCode): \(body)")
}
let decoded = try JSONDecoder().decode(MonitoringTimeSeriesResponse.self, from: response.data)
if let series = decoded.timeSeries {
allSeries.append(contentsOf: series)
}
pageToken = decoded.nextPageToken?.isEmpty == false ? decoded.nextPageToken : nil
} while pageToken != nil
return allSeries
}
private static func aggregate(series: [MonitoringTimeSeries]) -> [QuotaKey: Double] {
var buckets: [QuotaKey: Double] = [:]
for entry in series {
guard let key = Self.quotaKey(from: entry),
let value = Self.maxPointValue(from: entry.points)
else {
continue
}
buckets[key] = max(buckets[key] ?? 0, value)
}
return buckets
}
private static func quotaKey(from series: MonitoringTimeSeries) -> QuotaKey? {
let metricLabels = series.metric.labels ?? [:]
let resourceLabels = series.resource.labels ?? [:]
let quotaMetric = metricLabels["quota_metric"]
?? resourceLabels["quota_id"]
guard let quotaMetric, !quotaMetric.isEmpty else { return nil }
let limitName = metricLabels["limit_name"] ?? ""
let location = resourceLabels["location"] ?? "global"
return QuotaKey(quotaMetric: quotaMetric, limitName: limitName, location: location)
}
private static func maxPointValue(from points: [MonitoringPoint]) -> Double? {
points.compactMap(self.pointValue).max()
}
private static func pointValue(from point: MonitoringPoint) -> Double? {
if let doubleValue = point.value.doubleValue { return doubleValue }
if let int64Value = point.value.int64Value { return Double(int64Value) }
return nil
}
private static func previewKeys(_ map: [QuotaKey: Double], maxCount: Int = 3) -> String {
guard !map.isEmpty else { return "none" }
let keys = map.keys.prefix(maxCount).map { key in
"\(key.quotaMetric)|\(key.limitName)|\(key.location)"
}
let suffix = map.count > maxCount ? " +\(map.count - maxCount)" : ""
return keys.joined(separator: ", ") + suffix
}
}
@@ -0,0 +1,110 @@
import Foundation
public enum VertexAIProviderDescriptor {
public static let descriptor: ProviderDescriptor = Self.makeDescriptor()
static func makeDescriptor() -> ProviderDescriptor {
ProviderDescriptor(
id: .vertexai,
metadata: ProviderMetadata(
id: .vertexai,
displayName: "Vertex AI",
sessionLabel: "Requests",
weeklyLabel: "Tokens",
opusLabel: nil,
supportsOpus: false,
supportsCredits: false,
creditsHint: "",
toggleTitle: "Show Vertex AI usage",
cliName: "vertexai",
defaultEnabled: false,
isPrimaryProvider: false,
usesAccountFallback: false,
dashboardURL: "https://console.cloud.google.com/vertex-ai",
statusPageURL: nil,
statusLinkURL: "https://status.cloud.google.com"),
branding: ProviderBranding(
iconStyle: .vertexai,
iconResourceName: "ProviderIcon-vertexai",
color: ProviderColor(red: 66 / 255, green: 133 / 255, blue: 244 / 255)),
tokenCost: ProviderTokenCostConfig(
supportsTokenCost: true,
noDataMessage: { "No Vertex AI cost data found in Claude logs. Ensure entries include Vertex metadata."
}),
fetchPlan: ProviderFetchPlan(
sourceModes: [.auto, .oauth],
pipeline: ProviderFetchPipeline(resolveStrategies: { _ in [VertexAIOAuthFetchStrategy()] })),
cli: ProviderCLIConfig(
name: "vertexai",
versionDetector: nil))
}
}
struct VertexAIOAuthFetchStrategy: ProviderFetchStrategy {
let id: String = "vertexai.oauth"
let kind: ProviderFetchKind = .oauth
func isAvailable(_ context: ProviderFetchContext) async -> Bool {
VertexAIOAuthCredentialsStore.hasCredentials(environment: context.env)
}
func fetch(_ context: ProviderFetchContext) async throws -> ProviderFetchResult {
var credentials = try await VertexAIOAuthCredentialsStore.loadForFetch(environment: context.env)
// Refresh token if expired
if credentials.needsRefresh {
credentials = try await VertexAITokenRefresher.refresh(credentials)
try VertexAIOAuthCredentialsStore.save(credentials)
}
// Fetch quota usage from Cloud Monitoring. If no data is found (e.g., no recent
// Vertex AI requests), return an empty snapshot so token costs can still display.
let usage: VertexAIUsageResponse?
do {
usage = try await VertexAIUsageFetcher.fetchUsage(
accessToken: credentials.accessToken,
projectId: credentials.projectId)
} catch VertexAIFetchError.noData {
// No quota data is fine - token costs from local logs can still be shown.
usage = nil
}
return self.makeResult(
usage: Self.mapUsage(usage, credentials: credentials),
sourceLabel: "oauth")
}
func shouldFallback(on error: Error, context _: ProviderFetchContext) -> Bool {
if error is VertexAIOAuthCredentialsError { return true }
if let fetchError = error as? VertexAIFetchError {
switch fetchError {
case .unauthorized, .forbidden:
return true
default:
return false
}
}
return false
}
private static func mapUsage(
_ response: VertexAIUsageResponse?,
credentials: VertexAIOAuthCredentials) -> UsageSnapshot
{
// Token cost is fetched separately via CostUsageScanner from local Claude logs.
// Quota usage from Cloud Monitoring is optional - we still show token costs if unavailable.
let identity = ProviderIdentitySnapshot(
providerID: .vertexai,
accountEmail: credentials.email,
accountOrganization: credentials.projectId,
loginMethod: "gcloud")
return UsageSnapshot(
primary: nil,
secondary: nil,
providerCost: nil,
updatedAt: Date(),
identity: identity)
}
}