import Foundation #if canImport(FoundationNetworking) import FoundationNetworking #endif public struct MoonshotUsageSnapshot: Sendable { public let summary: MoonshotUsageSummary public init(summary: MoonshotUsageSummary) { self.summary = summary } public func toUsageSnapshot() -> UsageSnapshot { self.summary.toUsageSnapshot() } } public struct MoonshotUsageSummary: Sendable { public let availableBalance: Double public let voucherBalance: Double public let cashBalance: Double public let updatedAt: Date public init( availableBalance: Double, voucherBalance: Double, cashBalance: Double, updatedAt: Date) { self.availableBalance = availableBalance self.voucherBalance = voucherBalance self.cashBalance = cashBalance self.updatedAt = updatedAt } public func toUsageSnapshot() -> UsageSnapshot { let balance = UsageFormatter.usdString(self.availableBalance) let loginMethod: String if self.cashBalance < 0 { let deficit = UsageFormatter.usdString(abs(self.cashBalance)) loginMethod = "Balance: \(balance) ยท \(deficit) in deficit" } else { loginMethod = "Balance: \(balance)" } let identity = ProviderIdentitySnapshot( providerID: .moonshot, accountEmail: nil, accountOrganization: nil, loginMethod: loginMethod) return UsageSnapshot( primary: nil, secondary: nil, tertiary: nil, providerCost: nil, updatedAt: self.updatedAt, identity: identity) } } private struct MoonshotBalanceResponse: Decodable { let code: Int let data: MoonshotBalanceData let scode: String let status: Bool } private struct MoonshotBalanceData: Decodable { let availableBalance: Double let voucherBalance: Double let cashBalance: Double private enum CodingKeys: String, CodingKey { case availableBalance = "available_balance" case voucherBalance = "voucher_balance" case cashBalance = "cash_balance" } } public enum MoonshotUsageError: LocalizedError, Sendable { case missingCredentials case networkError(String) case apiError(String) case parseFailed(String) public var errorDescription: String? { switch self { case .missingCredentials: "Missing Moonshot API key." case let .networkError(message): "Moonshot network error: \(message)" case let .apiError(message): "Moonshot API error: \(message)" case let .parseFailed(message): "Failed to parse Moonshot response: \(message)" } } } public struct MoonshotUsageFetcher: Sendable { private static let log = CodexBarLog.logger(LogCategories.moonshotUsage) private static let timeoutSeconds: TimeInterval = 15 public static func fetchUsage( apiKey: String, region: MoonshotRegion = .international, session transport: any ProviderHTTPTransport = ProviderHTTPClient.shared) async throws -> MoonshotUsageSnapshot { let cleaned = apiKey.trimmingCharacters(in: .whitespacesAndNewlines) guard !cleaned.isEmpty else { throw MoonshotUsageError.missingCredentials } var request = URLRequest(url: self.resolveBalanceURL(region: region)) request.httpMethod = "GET" request.setValue("Bearer \(cleaned)", forHTTPHeaderField: "Authorization") request.setValue("application/json", forHTTPHeaderField: "Accept") request.timeoutInterval = Self.timeoutSeconds let response: ProviderHTTPResponse do { response = try await transport.response(for: request) } catch let error as URLError where error.code == .badServerResponse { throw MoonshotUsageError.networkError("Invalid response") } catch { throw error } guard response.statusCode == 200 else { Self.log.error("Moonshot API returned HTTP \(response.statusCode)") throw MoonshotUsageError.apiError("HTTP \(response.statusCode)") } let summary = try self.parseSummary(data: response.data) return MoonshotUsageSnapshot(summary: summary) } public static func resolveBalanceURL(region: MoonshotRegion) -> URL { region.balanceURL } static func _parseSummaryForTesting(_ data: Data) throws -> MoonshotUsageSummary { try self.parseSummary(data: data) } private static func parseSummary(data: Data) throws -> MoonshotUsageSummary { let response: MoonshotBalanceResponse do { response = try JSONDecoder().decode(MoonshotBalanceResponse.self, from: data) } catch { throw MoonshotUsageError.parseFailed(error.localizedDescription) } guard response.code == 0, response.status else { throw MoonshotUsageError.apiError("code \(response.code), scode \(response.scode)") } return MoonshotUsageSummary( availableBalance: response.data.availableBalance, voucherBalance: response.data.voucherBalance, cashBalance: response.data.cashBalance, updatedAt: Date()) } }