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()) }}
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())
}
}