250 lines · 8.0 KB · Swift
RawDownload
import Foundation // MARK: - API Response Models /// Top-level response from `GET https://admin.mistral.ai/api/billing/v2/usage`.struct MistralBillingResponse: Codable {    let completion: MistralModelUsageCategory?    let ocr: MistralModelUsageCategory?    let connectors: MistralModelUsageCategory?    let librariesApi: MistralLibrariesUsageCategory?    let fineTuning: MistralFineTuningCategory?    let audio: MistralModelUsageCategory?    let vibeUsage: Double?    let date: String?    let previousMonth: String?    let nextMonth: String?    let startDate: String?    let endDate: String?    let currency: String?    let currencySymbol: String?    let prices: [MistralPrice]?     enum CodingKeys: String, CodingKey {        case completion, ocr, connectors, audio, date, currency, prices        case librariesApi = "libraries_api"        case fineTuning = "fine_tuning"        case vibeUsage = "vibe_usage"        case previousMonth = "previous_month"        case nextMonth = "next_month"        case startDate = "start_date"        case endDate = "end_date"        case currencySymbol = "currency_symbol"    }} struct MistralModelUsageCategory: Codable {    let models: [String: MistralModelUsageData]?} struct MistralLibrariesUsageCategory: Codable {    let pages: MistralModelUsageCategory?    let tokens: MistralModelUsageCategory?} struct MistralFineTuningCategory: Codable {    let training: [String: MistralModelUsageData]?    let storage: [String: MistralModelUsageData]?} struct MistralModelUsageData: Codable {    let input: [MistralUsageEntry]?    let output: [MistralUsageEntry]?    let cached: [MistralUsageEntry]?} struct MistralUsageEntry: Codable {    let usageType: String?    let eventType: String?    let billingMetric: String?    let billingDisplayName: String?    let billingGroup: String?    let timestamp: String?    let value: Int?    let valuePaid: Int?     enum CodingKeys: String, CodingKey {        case timestamp, value        case usageType = "usage_type"        case eventType = "event_type"        case billingMetric = "billing_metric"        case billingDisplayName = "billing_display_name"        case billingGroup = "billing_group"        case valuePaid = "value_paid"    }} struct MistralPrice: Codable {    let eventType: String?    let billingMetric: String?    let billingGroup: String?    let price: String?     enum CodingKeys: String, CodingKey {        case price        case eventType = "event_type"        case billingMetric = "billing_metric"        case billingGroup = "billing_group"    }} // MARK: - Intermediate Snapshot public struct MistralDailyUsageBucket: Codable, Equatable, Sendable, Identifiable {    public struct ModelBreakdown: Codable, Equatable, Sendable, Identifiable {        public let name: String        public let cost: Double        public let inputTokens: Int        public let cachedTokens: Int        public let outputTokens: Int         public var id: String {            self.name        }         public var totalTokens: Int {            self.inputTokens + self.cachedTokens + self.outputTokens        }         public init(name: String, cost: Double, inputTokens: Int, cachedTokens: Int, outputTokens: Int) {            self.name = name            self.cost = cost            self.inputTokens = inputTokens            self.cachedTokens = cachedTokens            self.outputTokens = outputTokens        }    }     public let day: String    public let cost: Double    public let inputTokens: Int    public let cachedTokens: Int    public let outputTokens: Int    public let models: [ModelBreakdown]     public var id: String {        self.day    }     public var totalTokens: Int {        self.inputTokens + self.cachedTokens + self.outputTokens    }     public init(        day: String,        cost: Double,        inputTokens: Int,        cachedTokens: Int,        outputTokens: Int,        models: [ModelBreakdown])    {        self.day = day        self.cost = cost        self.inputTokens = inputTokens        self.cachedTokens = cachedTokens        self.outputTokens = outputTokens        self.models = models    }} public struct MistralUsageSnapshot: Codable, Sendable {    public let totalCost: Double    public let currency: String    public let currencySymbol: String    public let totalInputTokens: Int    public let totalOutputTokens: Int    public let totalCachedTokens: Int    public let modelCount: Int    public let daily: [MistralDailyUsageBucket]    public let startDate: Date?    public let endDate: Date?    public let updatedAt: Date     public init(        totalCost: Double,        currency: String,        currencySymbol: String,        totalInputTokens: Int,        totalOutputTokens: Int,        totalCachedTokens: Int,        modelCount: Int,        daily: [MistralDailyUsageBucket] = [],        startDate: Date?,        endDate: Date?,        updatedAt: Date)    {        self.totalCost = totalCost        self.currency = currency        self.currencySymbol = currencySymbol        self.totalInputTokens = totalInputTokens        self.totalOutputTokens = totalOutputTokens        self.totalCachedTokens = totalCachedTokens        self.modelCount = modelCount        self.daily = daily.sorted { $0.day < $1.day }        self.startDate = startDate        self.endDate = endDate        self.updatedAt = updatedAt    }     public func toUsageSnapshot() -> UsageSnapshot {        // Negative totalCost means a refund/credit adjustment; clamp to zero rather than        // showing a confusing negative amount in the menu bar.        let spendText = if self.totalCost > 0 {            "\(self.currencySymbol)\(String(format: "%.4f", self.totalCost)) this month"        } else {            "\(self.currencySymbol)0.0000 this month"        }        let identity = ProviderIdentitySnapshot(            providerID: .mistral,            accountEmail: nil,            accountOrganization: nil,            loginMethod: "API spend: \(spendText)")        return UsageSnapshot(            primary: nil,            secondary: nil,            providerCost: nil,            mistralUsage: self,            updatedAt: self.updatedAt,            identity: identity)    }     public func toCostUsageTokenSnapshot(historyDays: Int = 30) -> CostUsageTokenSnapshot {        let clampedHistoryDays = max(1, min(365, historyDays))        let selected = self.daily        let entries = selected.map { bucket in            let modelBreakdowns = bucket.models.map {                CostUsageDailyReport.ModelBreakdown(                    modelName: $0.name,                    costUSD: max($0.cost, 0),                    totalTokens: $0.totalTokens)            }            let modelsUsed = bucket.models.map(\.name)            return CostUsageDailyReport.Entry(                date: bucket.day,                inputTokens: bucket.inputTokens,                outputTokens: bucket.outputTokens,                cacheReadTokens: bucket.cachedTokens,                cacheCreationTokens: nil,                totalTokens: bucket.totalTokens,                costUSD: max(bucket.cost, 0),                modelsUsed: modelsUsed.isEmpty ? nil : modelsUsed,                modelBreakdowns: modelBreakdowns.isEmpty ? nil : modelBreakdowns)        }        let latest = selected.last        let totalCost = max(self.totalCost, 0)        let totalTokens = selected.isEmpty            ? self.totalInputTokens + self.totalCachedTokens + self.totalOutputTokens            : selected.reduce(0) { $0 + $1.totalTokens }        let tokens = totalTokens > 0 ? totalTokens : nil        return CostUsageTokenSnapshot(            sessionTokens: latest?.totalTokens,            sessionCostUSD: latest.map { max($0.cost, 0) },            last30DaysTokens: tokens,            last30DaysCostUSD: totalCost,            currencyCode: self.currency,            historyDays: selected.isEmpty ? clampedHistoryDays : max(1, min(365, selected.count)),            historyLabel: "This month",            daily: entries,            updatedAt: self.updatedAt)    }}