162 lines
5.2 KiB
Swift
162 lines
5.2 KiB
Swift
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())
|
|
}
|
|
}
|