148 lines
5.5 KiB
Swift
148 lines
5.5 KiB
Swift
import Foundation
|
|
|
|
public struct GrokUsageSnapshot: Sendable {
|
|
public let billing: GrokBillingResponse?
|
|
public let webBilling: GrokWebBillingSnapshot?
|
|
public let credentials: GrokCredentials?
|
|
public let localSummary: GrokLocalSessionSummary?
|
|
public let cliVersion: String?
|
|
public let updatedAt: Date
|
|
|
|
public init(
|
|
billing: GrokBillingResponse?,
|
|
webBilling: GrokWebBillingSnapshot? = nil,
|
|
credentials: GrokCredentials?,
|
|
localSummary: GrokLocalSessionSummary?,
|
|
cliVersion: String?,
|
|
updatedAt: Date)
|
|
{
|
|
self.billing = billing
|
|
self.webBilling = webBilling
|
|
self.credentials = credentials
|
|
self.localSummary = localSummary
|
|
self.cliVersion = cliVersion
|
|
self.updatedAt = updatedAt
|
|
}
|
|
|
|
public func toUsageSnapshot() -> UsageSnapshot {
|
|
// Primary window: credit usage (against included limit) from the CLI RPC,
|
|
// falling back to the web billing RPC used by grok.com when the agent surface lacks billing.
|
|
var primary: RateWindow?
|
|
if let billing,
|
|
let percent = billing.monthlyUsedPercent
|
|
{
|
|
primary = RateWindow(
|
|
usedPercent: percent,
|
|
windowMinutes: billing.billingPeriodMinutes,
|
|
resetsAt: billing.billingPeriodEndDate,
|
|
resetDescription: nil)
|
|
} else if let webBilling,
|
|
let percent = webBilling.usedPercent
|
|
{
|
|
primary = RateWindow(
|
|
usedPercent: percent,
|
|
windowMinutes: nil,
|
|
resetsAt: webBilling.resetsAt,
|
|
resetDescription: nil)
|
|
}
|
|
|
|
let identity = ProviderIdentitySnapshot(
|
|
providerID: .grok,
|
|
accountEmail: self.credentials?.email,
|
|
accountOrganization: self.credentials?.teamId,
|
|
loginMethod: self.credentials?.loginMethod)
|
|
|
|
return UsageSnapshot(
|
|
primary: primary,
|
|
secondary: nil,
|
|
tertiary: nil,
|
|
updatedAt: self.updatedAt,
|
|
identity: identity)
|
|
}
|
|
}
|
|
|
|
public struct GrokStatusProbe: Sendable {
|
|
public init() {}
|
|
|
|
public static func detectVersion(env: [String: String] = ProcessInfo.processInfo.environment) -> String? {
|
|
guard let binary = BinaryLocator.resolveGrokBinary(env: env) else { return nil }
|
|
guard let output = ProviderVersionDetector.run(
|
|
path: binary,
|
|
args: ["--version"],
|
|
environment: env,
|
|
mergeStandardError: true)
|
|
else { return nil }
|
|
// Output is like "grok 0.1.210 (8b63e9068c)" — strip the leading "grok " so
|
|
// callers can prefix the CLI name themselves without duplicating it.
|
|
let withoutPrefix = output.replacingOccurrences(
|
|
of: #"^grok\s+"#,
|
|
with: "",
|
|
options: [.regularExpression])
|
|
.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
return withoutPrefix.isEmpty ? nil : withoutPrefix
|
|
}
|
|
|
|
public func fetch(env: [String: String] = ProcessInfo.processInfo.environment) async throws -> GrokUsageSnapshot {
|
|
// Credentials are optional: we still show identity-less state if the user
|
|
// hasn't logged in, with a clear hint via the RPC error.
|
|
let credentials = try? GrokCredentialsStore.load(env: env)
|
|
|
|
var billing: GrokBillingResponse?
|
|
var rpcError: Error?
|
|
do {
|
|
let client = try GrokRPCClient(environment: env)
|
|
defer { client.shutdown() }
|
|
try await client.initialize()
|
|
billing = try await client.fetchBilling()
|
|
} catch {
|
|
rpcError = error
|
|
}
|
|
|
|
// Local fallback summary always succeeds (empty if no sessions yet).
|
|
let localSummary = GrokLocalSessionScanner.summarize(env: env)
|
|
let cliVersion = Self.detectVersion(env: env)
|
|
|
|
// `localSummary` is *not* currently projected into a visible RateWindow or
|
|
// identity field, so a stale `~/.grok/sessions/` directory must not
|
|
// suppress the auth-required hint. CLI-only fetches need a billing
|
|
// response; the provider pipeline owns the separate web fallback.
|
|
if billing == nil {
|
|
throw rpcError ?? GrokRPCError.notAuthenticated
|
|
}
|
|
|
|
return GrokUsageSnapshot(
|
|
billing: billing,
|
|
webBilling: nil,
|
|
credentials: Self.credentialsForSnapshot(
|
|
credentials: credentials,
|
|
billing: billing,
|
|
webBilling: nil),
|
|
localSummary: localSummary,
|
|
cliVersion: cliVersion,
|
|
updatedAt: Date())
|
|
}
|
|
|
|
static func credentialsForSnapshot(
|
|
credentials: GrokCredentials?,
|
|
billing: GrokBillingResponse?,
|
|
webBilling: GrokWebBillingSnapshot? = nil) -> GrokCredentials?
|
|
{
|
|
// If remote usage succeeded, xAI accepted auth and the local
|
|
// identity is still useful even when the persisted expires_at is stale.
|
|
if billing != nil || webBilling != nil { return credentials }
|
|
return credentials.flatMap { $0.isExpired ? nil : $0 }
|
|
}
|
|
|
|
static func shouldSurfaceRemoteAuthError(_ error: Error?) -> Bool {
|
|
guard let error = error as? GrokWebBillingError else { return false }
|
|
switch error {
|
|
case let .requestFailed(status, _):
|
|
return status == 401 || status == 403
|
|
case let .rpcFailed(status, _):
|
|
return status == 16
|
|
case .missingCredentials, .emptyResponse, .invalidResponse, .parseFailed:
|
|
return false
|
|
}
|
|
}
|
|
}
|