import Foundation #if canImport(FoundationNetworking) import FoundationNetworking #endif public struct GrokWebBillingSnapshot: Sendable, Equatable { public let usedPercent: Double? public let resetsAt: Date? public init(usedPercent: Double?, resetsAt: Date?) { self.usedPercent = usedPercent self.resetsAt = resetsAt } } public enum GrokWebBillingError: LocalizedError, Sendable { case missingCredentials case emptyResponse case invalidResponse case requestFailed(Int, String) case rpcFailed(Int, String) case parseFailed public var errorDescription: String? { switch self { case .missingCredentials: "Grok web billing requires a signed-in grok.com browser session or `grok login`." case .emptyResponse: "Grok web billing returned no protobuf payload." case .invalidResponse: "Grok web billing returned an invalid response." case let .requestFailed(status, body): if status == 401 || status == 403 { Self.reauthMessage } else { "Grok web billing request failed with HTTP \(status): \(body)" } case let .rpcFailed(status, message): if Self.isAuthenticationFailure(status: status, message: message) { Self.reauthMessage } else { "Grok web billing RPC failed with status \(status): \(message)" } case .parseFailed: "Could not parse Grok web billing usage." } } private static let reauthMessage = "Grok web billing rejected credentials. Sign in to grok.com in Chrome or run `grok login` to refresh xAI auth." static func isAuthenticationFailure(status: Int, message: String) -> Bool { if status == 16 { return true } guard status == 7 else { return false } let lower = message.lowercased() return lower.contains("bad-credentials") || lower.contains("unauthenticated") || (lower.contains("oauth2") && lower.contains("could not be validated")) || (lower.contains("access token") && (lower.contains("invalid") || lower.contains("expired") || lower.contains("could not be validated"))) } } public enum GrokWebBillingFetcher { public static let defaultEndpoint = URL(string: "https://grok.com/grok_api_v2.GrokBuildBilling/GetGrokCreditsConfig")! private static let requestTimeoutSeconds: TimeInterval = 15 public static func fetch( credentials: GrokCredentials, session transport: any ProviderHTTPTransport = ProviderHTTPClient.shared, endpoint: URL = Self.defaultEndpoint) async throws -> GrokWebBillingSnapshot { try await self.fetch( authorizationHeader: "Bearer \(credentials.accessToken)", cookieHeader: nil, transport: transport, endpoint: endpoint) } public static func fetch( cookieHeader: String, session transport: any ProviderHTTPTransport = ProviderHTTPClient.shared, endpoint: URL = Self.defaultEndpoint) async throws -> GrokWebBillingSnapshot { try await self.fetch( cookieHeader: cookieHeader, credentials: nil, session: transport, endpoint: endpoint) } public static func fetch( cookieHeader: String, credentials: GrokCredentials?, session transport: any ProviderHTTPTransport = ProviderHTTPClient.shared, endpoint: URL = Self.defaultEndpoint) async throws -> GrokWebBillingSnapshot { let authorizationHeader = credentials.flatMap { credential in credential.isExpired ? nil : "Bearer \(credential.accessToken)" } return try await self.fetch( authorizationHeader: authorizationHeader, cookieHeader: cookieHeader, transport: transport, endpoint: endpoint) } private static func fetch( authorizationHeader: String?, cookieHeader: String?, transport: any ProviderHTTPTransport, endpoint: URL) async throws -> GrokWebBillingSnapshot { do { return try await self.fetchOnce( authorizationHeader: authorizationHeader, cookieHeader: cookieHeader, transport: transport, endpoint: endpoint) } catch where self.shouldRetry(error) { return try await self.fetchOnce( authorizationHeader: authorizationHeader, cookieHeader: cookieHeader, transport: transport, endpoint: endpoint) } } private static func fetchOnce( authorizationHeader: String?, cookieHeader: String?, transport: any ProviderHTTPTransport, endpoint: URL) async throws -> GrokWebBillingSnapshot { var request = URLRequest(url: endpoint) request.httpMethod = "POST" request.timeoutInterval = Self.requestTimeoutSeconds request.httpBody = Data([0x00, 0x00, 0x00, 0x00, 0x00]) if let authorizationHeader { request.setValue(authorizationHeader, forHTTPHeaderField: "Authorization") } if let cookieHeader { request.setValue(cookieHeader, forHTTPHeaderField: "Cookie") } request.setValue("https://grok.com", forHTTPHeaderField: "Origin") request.setValue("https://grok.com/?_s=usage", forHTTPHeaderField: "Referer") request.setValue("*/*", forHTTPHeaderField: "Accept") request.setValue("application/grpc-web+proto", forHTTPHeaderField: "Content-Type") request.setValue("1", forHTTPHeaderField: "x-grpc-web") request.setValue("connect-es/2.1.1", forHTTPHeaderField: "x-user-agent") request.setValue("CodexBar", forHTTPHeaderField: "User-Agent") let response: ProviderHTTPResponse do { response = try await transport.response(for: request) } catch let error as URLError where error.code == .badServerResponse { throw GrokWebBillingError.invalidResponse } catch { throw error } guard response.statusCode == 200 else { let body = String(data: response.data.prefix(400), encoding: .utf8) ?? "" throw GrokWebBillingError.requestFailed(response.statusCode, body) } try Self.validateGRPCStatusFields(Self.grpcHeaderFields(from: response.response.allHeaderFields)) try Self.validateGRPCWebTrailers(response.data) return try Self.parseGRPCWebResponse(response.data) } private static func shouldRetry(_ error: Error) -> Bool { if let urlError = error as? URLError { return urlError.code == .timedOut || urlError.code == .networkConnectionLost } if case let GrokWebBillingError.requestFailed(status, body) = error { if [408, 502, 503, 504].contains(status) { return true } return body.localizedCaseInsensitiveContains("timeout") || body.localizedCaseInsensitiveContains("deadline") } guard case let GrokWebBillingError.rpcFailed(status, message) = error else { return false } if status == 4 { return true } guard status == 1 else { return false } return message.localizedCaseInsensitiveContains("timeout") || message.localizedCaseInsensitiveContains("deadline") || message.localizedCaseInsensitiveContains("expired") } static func parseGRPCWebResponse(_ data: Data, now: Date = Date()) throws -> GrokWebBillingSnapshot { var payloads = Self.grpcWebDataFrames(from: data) if payloads.isEmpty, Self.looksLikeProtobufPayload(data) { payloads = [data] } guard !payloads.isEmpty else { throw GrokWebBillingError.emptyResponse } var scan = ProtobufScan() for payload in payloads { scan.merge(Self.scanProtobuf(payload, depth: 0)) } let parsedPercent = scan.fixed32Fields .filter { field in field.path.last == 1 && field.value.isFinite && field.value >= 0 && field.value <= 100 } .min { lhs, rhs in lhs.path.count == rhs.path.count ? lhs.order < rhs.order : lhs.path.count < rhs.path.count } .map { Double($0.value) } let resetFields = scan.varintFields.compactMap { field -> (path: [UInt64], date: Date)? in let raw = field.value guard raw >= 1_700_000_000, raw <= 2_100_000_000 else { return nil } return (field.path, Date(timeIntervalSince1970: TimeInterval(raw))) } let futureResetFields = resetFields.filter { $0.date > now } let reset = futureResetFields .filter { $0.path == [1, 5, 1] } .map(\.date) .min() ?? futureResetFields .map(\.date) .min() let hasUsagePeriod = scan.varintFields.contains { field in field.path.starts(with: [1, 6]) || (field.path == [1, 8, 1] && (field.value == 1 || field.value == 2)) } let noUsageYet = parsedPercent == nil && scan.fixed32Fields.isEmpty && reset != nil && hasUsagePeriod guard let percent = parsedPercent ?? (noUsageYet ? 0 : nil) else { throw GrokWebBillingError.parseFailed } return GrokWebBillingSnapshot(usedPercent: percent, resetsAt: reset) } static func looksLikeProtobufPayload(_ data: Data) -> Bool { guard let first = data.first else { return false } let fieldNumber = first >> 3 let wireType = first & 0x07 return fieldNumber > 0 && (wireType == 0 || wireType == 1 || wireType == 2 || wireType == 5) } static func grpcWebDataFrames(from data: Data) -> [Data] { let bytes = [UInt8](data) var frames: [Data] = [] var index = 0 while index < bytes.count { guard index + 5 <= bytes.count else { return [] } let flags = bytes[index] let length = (Int(bytes[index + 1]) << 24) | (Int(bytes[index + 2]) << 16) | (Int(bytes[index + 3]) << 8) | Int(bytes[index + 4]) let start = index + 5 let end = start + length guard length >= 0, end <= bytes.count else { return [] } if flags & 0x80 == 0 { frames.append(Data(bytes[start.. [String: String] { var fields: [String: String] = [:] for (key, value) in headers { let normalizedKey = String(describing: key) .trimmingCharacters(in: .whitespacesAndNewlines) .lowercased() guard normalizedKey.hasPrefix("grpc-") else { continue } fields[normalizedKey] = String(describing: value) .trimmingCharacters(in: .whitespacesAndNewlines) .removingPercentEncoding ?? "" } return fields } static func grpcWebTrailerFields(from data: Data) -> [String: String] { let bytes = [UInt8](data) var fields: [String: String] = [:] var index = 0 while index + 5 <= bytes.count { let flags = bytes[index] let length = (Int(bytes[index + 1]) << 24) | (Int(bytes[index + 2]) << 16) | (Int(bytes[index + 3]) << 8) | Int(bytes[index + 4]) let start = index + 5 let end = start + length guard length >= 0, end <= bytes.count else { break } if flags & 0x80 != 0, let text = String(data: Data(bytes[start.. ProtobufScan { self.scanProtobuf(data, depth: depth, path: [], order: 0).scan } private static func scanProtobuf( _ data: Data, depth: Int, path: [UInt64], order: Int) -> (scan: ProtobufScan, order: Int) { let bytes = [UInt8](data) var scan = ProtobufScan() var index = 0 var nextOrder = order while index < bytes.count { let fieldStart = index guard let key = Self.readVarint(bytes, index: &index), key != 0 else { index = fieldStart + 1 continue } let fieldNumber = key >> 3 let wireType = key & 0x07 let fieldPath = path + [fieldNumber] switch wireType { case 0: if let value = Self.readVarint(bytes, index: &index) { scan.varintFields.append(ProtobufScan.VarintField(path: fieldPath, value: value)) } else { index = fieldStart + 1 } case 1: guard index + 8 <= bytes.count else { return (scan, nextOrder) } index += 8 case 2: guard let length = Self.readVarint(bytes, index: &index), length <= UInt64(bytes.count - index) else { index = fieldStart + 1 continue } let start = index let end = index + Int(length) if depth < 4 { let nested = Self.scanProtobuf( Data(bytes[start.. UInt64? { var value: UInt64 = 0 var shift: UInt64 = 0 while index < bytes.count, shift < 64 { let byte = bytes[index] index += 1 value |= UInt64(byte & 0x7F) << shift if byte & 0x80 == 0 { return value } shift += 7 } return nil } }