import Foundation public struct CostUsageWindowSummary: Sendable, Equatable { public let days: Int public let totalTokens: Int? public let totalCostUSD: Double? public let totalRequests: Int? public let entryCount: Int public init( days: Int, totalTokens: Int?, totalCostUSD: Double?, totalRequests: Int?, entryCount: Int) { self.days = days self.totalTokens = totalTokens self.totalCostUSD = totalCostUSD self.totalRequests = totalRequests self.entryCount = entryCount } } public struct CostUsageTokenSnapshot: Sendable, Equatable { public let sessionTokens: Int? public let sessionCostUSD: Double? public let sessionRequests: Int? public let last30DaysTokens: Int? public let last30DaysCostUSD: Double? public let last30DaysRequests: Int? public let currencyCode: String public let historyDays: Int public let historyLabel: String? public let daily: [CostUsageDailyReport.Entry] public let projects: [CostUsageProjectBreakdown] public let updatedAt: Date public init( sessionTokens: Int?, sessionCostUSD: Double?, sessionRequests: Int? = nil, last30DaysTokens: Int?, last30DaysCostUSD: Double?, last30DaysRequests: Int? = nil, currencyCode: String = "USD", historyDays: Int = 30, historyLabel: String? = nil, daily: [CostUsageDailyReport.Entry], projects: [CostUsageProjectBreakdown] = [], updatedAt: Date) { self.sessionTokens = sessionTokens self.sessionCostUSD = sessionCostUSD self.sessionRequests = sessionRequests self.last30DaysTokens = last30DaysTokens self.last30DaysCostUSD = last30DaysCostUSD self.last30DaysRequests = last30DaysRequests self.currencyCode = currencyCode.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? "USD" : currencyCode.trimmingCharacters(in: .whitespacesAndNewlines).uppercased() self.historyDays = historyDays self.historyLabel = historyLabel self.daily = daily self.projects = projects self.updatedAt = updatedAt } public func currentDayEntry(calendar: Calendar = .current) -> CostUsageDailyReport.Entry? { Self.entry(in: self.daily, forLocalDayContaining: self.updatedAt, calendar: calendar) } public func summary(forLastDays requestedDays: Int, calendar: Calendar = .current) -> CostUsageWindowSummary { let days = max(1, requestedDays) let today = calendar.startOfDay(for: self.updatedAt) let start = calendar.date(byAdding: .day, value: -(days - 1), to: today) ?? today let startKey = CostUsageLocalDay.key(from: start, calendar: calendar) let endKey = CostUsageLocalDay.key(from: today, calendar: calendar) let entries = self.daily.filter { entry in guard let dayKey = Self.localDayKey(for: entry.date, calendar: calendar) else { return false } return dayKey >= startKey && dayKey <= endKey } let costs = entries.compactMap(\.costUSD) let tokens = entries.compactMap(\.totalTokens) let requests = entries.compactMap(\.requestCount) return CostUsageWindowSummary( days: days, totalTokens: tokens.isEmpty ? nil : tokens.reduce(0, +), totalCostUSD: costs.isEmpty ? nil : costs.reduce(0, +), totalRequests: requests.isEmpty ? nil : requests.reduce(0, +), entryCount: entries.count) } public func comparisonSummaries( periods: [Int] = [7, 30, 90], calendar: Calendar = .current) -> [CostUsageWindowSummary] { Array(Set(periods.map { max(1, $0) })) .filter { $0 < self.historyDays } .sorted() .map { self.summary(forLastDays: $0, calendar: calendar) } } public static func latestEntry(in entries: [CostUsageDailyReport.Entry]) -> CostUsageDailyReport.Entry? { entries.compactMap { entry -> (entry: CostUsageDailyReport.Entry, date: Date)? in guard let date = CostUsageDateParser.parse(entry.date) else { return nil } return (entry, date) } .max { lhs, rhs in if lhs.date != rhs.date { return lhs.date < rhs.date } let lCost = lhs.entry.costUSD ?? -1 let rCost = rhs.entry.costUSD ?? -1 if lCost != rCost { return lCost < rCost } let lTokens = lhs.entry.totalTokens ?? -1 let rTokens = rhs.entry.totalTokens ?? -1 if lTokens != rTokens { return lTokens < rTokens } return lhs.entry.date < rhs.entry.date }?.entry } public static func entry( in entries: [CostUsageDailyReport.Entry], forLocalDayContaining date: Date, calendar: Calendar = .current) -> CostUsageDailyReport.Entry? { let dayKey = CostUsageLocalDay.key(from: date, calendar: calendar) return entries.first { entry in let rawDate = entry.date.trimmingCharacters(in: .whitespacesAndNewlines) if rawDate == dayKey { return true } guard let parsed = CostUsageDateParser.parse(rawDate) else { return false } return CostUsageLocalDay.key(from: parsed, calendar: calendar) == dayKey } } private static func localDayKey(for rawDate: String, calendar: Calendar) -> String? { let trimmed = rawDate.trimmingCharacters(in: .whitespacesAndNewlines) if trimmed.count >= 10 { let prefix = String(trimmed.prefix(10)) if prefix.count == 10, prefix[prefix.index(prefix.startIndex, offsetBy: 4)] == "-", prefix[prefix.index(prefix.startIndex, offsetBy: 7)] == "-" { return prefix } } guard let parsed = CostUsageDateParser.parse(trimmed) else { return nil } return CostUsageLocalDay.key(from: parsed, calendar: calendar) } } public struct CostUsageProjectBreakdown: Sendable, Equatable { public static let unknownProjectName = "Unknown project" public let name: String public let path: String? public let totalTokens: Int? public let totalCostUSD: Double? public let daily: [CostUsageDailyReport.Entry] public let modelBreakdowns: [CostUsageDailyReport.ModelBreakdown]? public let sources: [CostUsageProjectSourceBreakdown] public init( name: String, path: String?, totalTokens: Int?, totalCostUSD: Double?, daily: [CostUsageDailyReport.Entry], modelBreakdowns: [CostUsageDailyReport.ModelBreakdown]?, sources: [CostUsageProjectSourceBreakdown] = []) { self.name = name.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? Self.unknownProjectName : name let cleanPath = path?.trimmingCharacters(in: .whitespacesAndNewlines) self.path = cleanPath?.isEmpty == true ? nil : cleanPath self.totalTokens = totalTokens self.totalCostUSD = totalCostUSD self.daily = daily self.modelBreakdowns = modelBreakdowns self.sources = sources } } public struct CostUsageProjectSourceBreakdown: Sendable, Equatable { public let name: String public let path: String? public let totalTokens: Int? public let totalCostUSD: Double? public let daily: [CostUsageDailyReport.Entry] public let modelBreakdowns: [CostUsageDailyReport.ModelBreakdown]? public init( name: String, path: String?, totalTokens: Int?, totalCostUSD: Double?, daily: [CostUsageDailyReport.Entry], modelBreakdowns: [CostUsageDailyReport.ModelBreakdown]?) { self.name = name.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? CostUsageProjectBreakdown.unknownProjectName : name let cleanPath = path?.trimmingCharacters(in: .whitespacesAndNewlines) self.path = cleanPath?.isEmpty == true ? nil : cleanPath self.totalTokens = totalTokens self.totalCostUSD = totalCostUSD self.daily = daily self.modelBreakdowns = modelBreakdowns } } public struct CostUsageDailyReport: Sendable, Decodable { public struct ModelBreakdown: Sendable, Decodable, Equatable { public let modelName: String public let costUSD: Double? public let totalTokens: Int? public let requestCount: Int? public let standardCostUSD: Double? public let priorityCostUSD: Double? public let standardTokens: Int? public let priorityTokens: Int? private enum CodingKeys: String, CodingKey { case modelName case costUSD case cost case totalTokens case requestCount case requests case standardCostUSD case priorityCostUSD case standardTokens case priorityTokens } public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.modelName = try container.decode(String.self, forKey: .modelName) self.costUSD = try container.decodeIfPresent(Double.self, forKey: .costUSD) ?? container.decodeIfPresent(Double.self, forKey: .cost) self.totalTokens = try container.decodeIfPresent(Int.self, forKey: .totalTokens) self.requestCount = try container.decodeIfPresent(Int.self, forKey: .requestCount) ?? container.decodeIfPresent(Int.self, forKey: .requests) self.standardCostUSD = try container.decodeIfPresent(Double.self, forKey: .standardCostUSD) self.priorityCostUSD = try container.decodeIfPresent(Double.self, forKey: .priorityCostUSD) self.standardTokens = try container.decodeIfPresent(Int.self, forKey: .standardTokens) self.priorityTokens = try container.decodeIfPresent(Int.self, forKey: .priorityTokens) } public init( modelName: String, costUSD: Double?, totalTokens: Int? = nil, requestCount: Int? = nil, standardCostUSD: Double? = nil, priorityCostUSD: Double? = nil, standardTokens: Int? = nil, priorityTokens: Int? = nil) { self.modelName = modelName self.costUSD = costUSD self.totalTokens = totalTokens self.requestCount = requestCount self.standardCostUSD = standardCostUSD self.priorityCostUSD = priorityCostUSD self.standardTokens = standardTokens self.priorityTokens = priorityTokens } } public struct Entry: Sendable, Decodable, Equatable { public let date: String public let inputTokens: Int? public let cacheReadTokens: Int? public let cacheCreationTokens: Int? public let outputTokens: Int? public let totalTokens: Int? public let requestCount: Int? public let costUSD: Double? public let modelsUsed: [String]? public let modelBreakdowns: [ModelBreakdown]? private enum CodingKeys: String, CodingKey { case date case inputTokens case cacheReadTokens case cacheCreationTokens case cacheReadInputTokens case cacheCreationInputTokens case outputTokens case totalTokens case requestCount case requests case costUSD case totalCost case modelsUsed case models case modelBreakdowns } public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.date = try container.decode(String.self, forKey: .date) self.inputTokens = try container.decodeIfPresent(Int.self, forKey: .inputTokens) self.cacheReadTokens = try container.decodeIfPresent(Int.self, forKey: .cacheReadTokens) ?? container.decodeIfPresent(Int.self, forKey: .cacheReadInputTokens) self.cacheCreationTokens = try container.decodeIfPresent(Int.self, forKey: .cacheCreationTokens) ?? container.decodeIfPresent(Int.self, forKey: .cacheCreationInputTokens) self.outputTokens = try container.decodeIfPresent(Int.self, forKey: .outputTokens) self.totalTokens = try container.decodeIfPresent(Int.self, forKey: .totalTokens) self.requestCount = try container.decodeIfPresent(Int.self, forKey: .requestCount) ?? container.decodeIfPresent(Int.self, forKey: .requests) self.costUSD = try container.decodeIfPresent(Double.self, forKey: .costUSD) ?? container.decodeIfPresent(Double.self, forKey: .totalCost) self.modelsUsed = Self.decodeModelsUsed(from: container) self.modelBreakdowns = try container.decodeIfPresent([ModelBreakdown].self, forKey: .modelBreakdowns) } public init( date: String, inputTokens: Int?, outputTokens: Int?, cacheReadTokens: Int? = nil, cacheCreationTokens: Int? = nil, totalTokens: Int?, requestCount: Int? = nil, costUSD: Double?, modelsUsed: [String]?, modelBreakdowns: [ModelBreakdown]?) { self.date = date self.inputTokens = inputTokens self.outputTokens = outputTokens self.cacheReadTokens = cacheReadTokens self.cacheCreationTokens = cacheCreationTokens self.totalTokens = totalTokens self.requestCount = requestCount self.costUSD = costUSD self.modelsUsed = modelsUsed self.modelBreakdowns = modelBreakdowns } private static func decodeModelsUsed(from container: KeyedDecodingContainer) -> [String]? { func decodeStringList(_ key: CodingKeys) -> [String]? { (try? container.decodeIfPresent([String].self, forKey: key)).flatMap(\.self) } if let modelsUsed = decodeStringList(.modelsUsed) { return modelsUsed } if let models = decodeStringList(.models) { return models } guard container.contains(.models) else { return nil } guard let modelMap = try? container.nestedContainer(keyedBy: CostUsageAnyCodingKey.self, forKey: .models) else { return nil } let modelNames = modelMap.allKeys.map(\.stringValue).sorted() return modelNames.isEmpty ? nil : modelNames } } public struct Summary: Sendable, Decodable, Equatable { public let totalInputTokens: Int? public let totalOutputTokens: Int? public let cacheReadTokens: Int? public let cacheCreationTokens: Int? public let totalTokens: Int? public let totalCostUSD: Double? private enum CodingKeys: String, CodingKey { case totalInputTokens case totalOutputTokens case cacheReadTokens case cacheCreationTokens case totalCacheReadTokens case totalCacheCreationTokens case totalTokens case totalCostUSD case totalCost } public init( totalInputTokens: Int?, totalOutputTokens: Int?, cacheReadTokens: Int? = nil, cacheCreationTokens: Int? = nil, totalTokens: Int?, totalCostUSD: Double?) { self.totalInputTokens = totalInputTokens self.totalOutputTokens = totalOutputTokens self.cacheReadTokens = cacheReadTokens self.cacheCreationTokens = cacheCreationTokens self.totalTokens = totalTokens self.totalCostUSD = totalCostUSD } public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.totalInputTokens = try container.decodeIfPresent(Int.self, forKey: .totalInputTokens) self.totalOutputTokens = try container.decodeIfPresent(Int.self, forKey: .totalOutputTokens) self.cacheReadTokens = try container.decodeIfPresent(Int.self, forKey: .cacheReadTokens) ?? container.decodeIfPresent(Int.self, forKey: .totalCacheReadTokens) self.cacheCreationTokens = try container.decodeIfPresent(Int.self, forKey: .cacheCreationTokens) ?? container.decodeIfPresent(Int.self, forKey: .totalCacheCreationTokens) self.totalTokens = try container.decodeIfPresent(Int.self, forKey: .totalTokens) self.totalCostUSD = try container.decodeIfPresent(Double.self, forKey: .totalCostUSD) ?? container.decodeIfPresent(Double.self, forKey: .totalCost) } } public let data: [Entry] public let summary: Summary? private enum CodingKeys: String, CodingKey { case type case data case summary case daily case totals } public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) if container.contains(.type) { _ = try container.decode(String.self, forKey: .type) self.data = try container.decode([Entry].self, forKey: .data) self.summary = try container.decodeIfPresent(Summary.self, forKey: .summary) return } self.data = try container.decode([Entry].self, forKey: .daily) if container.contains(.totals) { let totals = try container.decode(CostUsageLegacyTotals.self, forKey: .totals) self.summary = Summary( totalInputTokens: totals.totalInputTokens, totalOutputTokens: totals.totalOutputTokens, cacheReadTokens: totals.cacheReadTokens, cacheCreationTokens: totals.cacheCreationTokens, totalTokens: totals.totalTokens, totalCostUSD: totals.totalCost) } else { self.summary = nil } } public init(data: [Entry], summary: Summary?) { self.data = data self.summary = summary } } extension CostUsageDailyReport { private struct BreakdownAccumulator { var totalTokens: Int = 0 var sawTotalTokens = false var costUSD: Double = 0 var sawCost = false var standardCostUSD: Double = 0 var sawStandardCost = false var priorityCostUSD: Double = 0 var sawPriorityCost = false var standardTokens: Int = 0 var sawStandardTokens = false var priorityTokens: Int = 0 var sawPriorityTokens = false mutating func add(_ breakdown: ModelBreakdown) { if let totalTokens = breakdown.totalTokens { self.totalTokens += totalTokens self.sawTotalTokens = true } if let costUSD = breakdown.costUSD { self.costUSD += costUSD self.sawCost = true } if let standardCostUSD = breakdown.standardCostUSD { self.standardCostUSD += standardCostUSD self.sawStandardCost = true } if let priorityCostUSD = breakdown.priorityCostUSD { self.priorityCostUSD += priorityCostUSD self.sawPriorityCost = true } if let standardTokens = breakdown.standardTokens { self.standardTokens += standardTokens self.sawStandardTokens = true } if let priorityTokens = breakdown.priorityTokens { self.priorityTokens += priorityTokens self.sawPriorityTokens = true } } func build(modelName: String) -> ModelBreakdown { ModelBreakdown( modelName: modelName, costUSD: self.sawCost ? self.costUSD : nil, totalTokens: self.sawTotalTokens ? self.totalTokens : nil, standardCostUSD: self.sawStandardCost ? self.standardCostUSD : nil, priorityCostUSD: self.sawPriorityCost ? self.priorityCostUSD : nil, standardTokens: self.sawStandardTokens ? self.standardTokens : nil, priorityTokens: self.sawPriorityTokens ? self.priorityTokens : nil) } } private struct EntryAccumulator { var inputTokens: Int = 0 var sawInputTokens = false var cacheReadTokens: Int = 0 var sawCacheReadTokens = false var cacheCreationTokens: Int = 0 var sawCacheCreationTokens = false var outputTokens: Int = 0 var sawOutputTokens = false var totalTokens: Int = 0 var sawTotalTokens = false var derivedTotalTokensWithoutExplicitTotal: Int = 0 var costUSD: Double = 0 var sawCost = false var modelsUsed: Set = [] var breakdowns: [String: BreakdownAccumulator] = [:] mutating func add(_ entry: Entry) { let entryDerivedTotalTokens = (entry.inputTokens ?? 0) + (entry.cacheReadTokens ?? 0) + (entry.cacheCreationTokens ?? 0) + (entry.outputTokens ?? 0) if let inputTokens = entry.inputTokens { self.inputTokens += inputTokens self.sawInputTokens = true } if let cacheReadTokens = entry.cacheReadTokens { self.cacheReadTokens += cacheReadTokens self.sawCacheReadTokens = true } if let cacheCreationTokens = entry.cacheCreationTokens { self.cacheCreationTokens += cacheCreationTokens self.sawCacheCreationTokens = true } if let outputTokens = entry.outputTokens { self.outputTokens += outputTokens self.sawOutputTokens = true } if let totalTokens = entry.totalTokens { self.totalTokens += totalTokens self.sawTotalTokens = true } else if entryDerivedTotalTokens > 0 { self.derivedTotalTokensWithoutExplicitTotal += entryDerivedTotalTokens } if let costUSD = entry.costUSD { self.costUSD += costUSD self.sawCost = true } if let modelsUsed = entry.modelsUsed { self.modelsUsed.formUnion(modelsUsed) } if let modelBreakdowns = entry.modelBreakdowns { for breakdown in modelBreakdowns { var accumulator = self.breakdowns[breakdown.modelName] ?? BreakdownAccumulator() accumulator.add(breakdown) self.breakdowns[breakdown.modelName] = accumulator self.modelsUsed.insert(breakdown.modelName) } } } func build(date: String) -> Entry { let derivedTotalTokens = self.inputTokens + self.cacheReadTokens + self.cacheCreationTokens + self.outputTokens let totalTokens: Int? = if self.sawTotalTokens { self.totalTokens + self.derivedTotalTokensWithoutExplicitTotal } else if derivedTotalTokens > 0 { derivedTotalTokens } else { nil } let modelBreakdowns: [ModelBreakdown]? = { guard !self.breakdowns.isEmpty else { return nil } return CostUsageDailyReport.sortedModelBreakdowns( self.breakdowns .map { modelName, accumulator in accumulator.build(modelName: modelName) }) }() let modelsUsed = self.modelsUsed.isEmpty ? nil : self.modelsUsed.sorted() return Entry( date: date, inputTokens: self.sawInputTokens ? self.inputTokens : nil, outputTokens: self.sawOutputTokens ? self.outputTokens : nil, cacheReadTokens: self.sawCacheReadTokens ? self.cacheReadTokens : nil, cacheCreationTokens: self.sawCacheCreationTokens ? self.cacheCreationTokens : nil, totalTokens: totalTokens, costUSD: self.sawCost ? self.costUSD : nil, modelsUsed: modelsUsed, modelBreakdowns: modelBreakdowns) } } public func merged(with other: CostUsageDailyReport) -> CostUsageDailyReport { Self.merged([self, other]) } public static func merged(_ reports: [CostUsageDailyReport]) -> CostUsageDailyReport { let entries = self.mergedEntries(from: reports) guard !entries.isEmpty else { return CostUsageDailyReport(data: [], summary: nil) } return CostUsageDailyReport(data: entries, summary: self.mergedSummary(from: entries)) } private static func mergedEntries(from reports: [CostUsageDailyReport]) -> [Entry] { var dayAccumulators: [String: EntryAccumulator] = [:] for report in reports { for entry in report.data { var accumulator = dayAccumulators[entry.date] ?? EntryAccumulator() accumulator.add(entry) dayAccumulators[entry.date] = accumulator } } return dayAccumulators .keys .sorted() .map { date in dayAccumulators[date, default: EntryAccumulator()].build(date: date) } } private static func mergedSummary(from entries: [Entry]) -> Summary { var totalInputTokens = 0 var sawTotalInputTokens = false var totalOutputTokens = 0 var sawTotalOutputTokens = false var totalCacheReadTokens = 0 var sawTotalCacheReadTokens = false var totalCacheCreationTokens = 0 var sawTotalCacheCreationTokens = false var totalTokens = 0 var sawTotalTokens = false var totalCostUSD = 0.0 var sawTotalCostUSD = false for entry in entries { if let inputTokens = entry.inputTokens { totalInputTokens += inputTokens sawTotalInputTokens = true } if let outputTokens = entry.outputTokens { totalOutputTokens += outputTokens sawTotalOutputTokens = true } if let cacheReadTokens = entry.cacheReadTokens { totalCacheReadTokens += cacheReadTokens sawTotalCacheReadTokens = true } if let cacheCreationTokens = entry.cacheCreationTokens { totalCacheCreationTokens += cacheCreationTokens sawTotalCacheCreationTokens = true } if let entryTotalTokens = entry.totalTokens { totalTokens += entryTotalTokens sawTotalTokens = true } if let costUSD = entry.costUSD { totalCostUSD += costUSD sawTotalCostUSD = true } } return Summary( totalInputTokens: sawTotalInputTokens ? totalInputTokens : nil, totalOutputTokens: sawTotalOutputTokens ? totalOutputTokens : nil, cacheReadTokens: sawTotalCacheReadTokens ? totalCacheReadTokens : nil, cacheCreationTokens: sawTotalCacheCreationTokens ? totalCacheCreationTokens : nil, totalTokens: sawTotalTokens ? totalTokens : nil, totalCostUSD: sawTotalCostUSD ? totalCostUSD : nil) } private static func sortedModelBreakdowns(_ breakdowns: [ModelBreakdown]) -> [ModelBreakdown] { breakdowns.sorted { lhs, rhs in let lhsCost = lhs.costUSD ?? -1 let rhsCost = rhs.costUSD ?? -1 if lhsCost != rhsCost { return lhsCost > rhsCost } let lhsTokens = lhs.totalTokens ?? -1 let rhsTokens = rhs.totalTokens ?? -1 if lhsTokens != rhsTokens { return lhsTokens > rhsTokens } return lhs.modelName > rhs.modelName } } } public struct CostUsageSessionReport: Sendable, Decodable { public struct Entry: Sendable, Decodable, Equatable { public let session: String public let inputTokens: Int? public let outputTokens: Int? public let totalTokens: Int? public let costUSD: Double? public let lastActivity: String? private enum CodingKeys: String, CodingKey { case session case sessionId case inputTokens case outputTokens case totalTokens case costUSD case totalCost case lastActivity } public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.session = try container.decodeIfPresent(String.self, forKey: .session) ?? container.decode(String.self, forKey: .sessionId) self.inputTokens = try container.decodeIfPresent(Int.self, forKey: .inputTokens) self.outputTokens = try container.decodeIfPresent(Int.self, forKey: .outputTokens) self.totalTokens = try container.decodeIfPresent(Int.self, forKey: .totalTokens) self.costUSD = try container.decodeIfPresent(Double.self, forKey: .costUSD) ?? container.decodeIfPresent(Double.self, forKey: .totalCost) self.lastActivity = try container.decodeIfPresent(String.self, forKey: .lastActivity) } } public struct Summary: Sendable, Decodable, Equatable { public let totalCostUSD: Double? private enum CodingKeys: String, CodingKey { case totalCostUSD case totalCost } public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.totalCostUSD = try container.decodeIfPresent(Double.self, forKey: .totalCostUSD) ?? container.decodeIfPresent(Double.self, forKey: .totalCost) } } public let data: [Entry] public let summary: Summary? private enum CodingKeys: String, CodingKey { case type case data case summary case sessions case totals } public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) if container.contains(.type) { _ = try container.decode(String.self, forKey: .type) self.data = try container.decode([Entry].self, forKey: .data) self.summary = try container.decodeIfPresent(Summary.self, forKey: .summary) return } self.data = try container.decode([Entry].self, forKey: .sessions) self.summary = try container.decodeIfPresent(Summary.self, forKey: .totals) } } public struct CostUsageMonthlyReport: Sendable, Decodable { public struct Entry: Sendable, Decodable, Equatable { public let month: String public let totalTokens: Int? public let costUSD: Double? private enum CodingKeys: String, CodingKey { case month case totalTokens case costUSD case totalCost } public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.month = try container.decode(String.self, forKey: .month) self.totalTokens = try container.decodeIfPresent(Int.self, forKey: .totalTokens) self.costUSD = try container.decodeIfPresent(Double.self, forKey: .costUSD) ?? container.decodeIfPresent(Double.self, forKey: .totalCost) } } public struct Summary: Sendable, Decodable, Equatable { public let totalTokens: Int? public let totalCostUSD: Double? private enum CodingKeys: String, CodingKey { case totalTokens case costUSD case totalCostUSD case totalCost } public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.totalTokens = try container.decodeIfPresent(Int.self, forKey: .totalTokens) self.totalCostUSD = try container.decodeIfPresent(Double.self, forKey: .totalCostUSD) ?? container.decodeIfPresent(Double.self, forKey: .costUSD) ?? container.decodeIfPresent(Double.self, forKey: .totalCost) } } public let data: [Entry] public let summary: Summary? private enum CodingKeys: String, CodingKey { case type case data case summary case monthly case totals } public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) if container.contains(.type) { _ = try container.decode(String.self, forKey: .type) self.data = try container.decode([Entry].self, forKey: .data) self.summary = try container.decodeIfPresent(Summary.self, forKey: .summary) return } self.data = try container.decode([Entry].self, forKey: .monthly) self.summary = try container.decodeIfPresent(Summary.self, forKey: .totals) } } private struct CostUsageLegacyTotals: Decodable { let totalInputTokens: Int? let totalOutputTokens: Int? let cacheReadTokens: Int? let cacheCreationTokens: Int? let totalTokens: Int? let totalCost: Double? private enum CodingKeys: String, CodingKey { case totalInputTokens case totalOutputTokens case cacheReadTokens case cacheCreationTokens case totalCacheReadTokens case totalCacheCreationTokens case totalTokens case totalCost } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.totalInputTokens = try container.decodeIfPresent(Int.self, forKey: .totalInputTokens) self.totalOutputTokens = try container.decodeIfPresent(Int.self, forKey: .totalOutputTokens) self.cacheReadTokens = try container.decodeIfPresent(Int.self, forKey: .cacheReadTokens) ?? container.decodeIfPresent(Int.self, forKey: .totalCacheReadTokens) self.cacheCreationTokens = try container.decodeIfPresent(Int.self, forKey: .cacheCreationTokens) ?? container.decodeIfPresent(Int.self, forKey: .totalCacheCreationTokens) self.totalTokens = try container.decodeIfPresent(Int.self, forKey: .totalTokens) self.totalCost = try container.decodeIfPresent(Double.self, forKey: .totalCost) } } private struct CostUsageAnyCodingKey: CodingKey { var intValue: Int? var stringValue: String init?(intValue: Int) { self.intValue = intValue self.stringValue = "\(intValue)" } init?(stringValue: String) { self.stringValue = stringValue self.intValue = nil } } enum CostUsageDateParser { private static let isoWithFractionalSecondsKey = "CostUsageDateParser.isoWithFractionalSeconds" private static let isoInternetDateTimeKey = "CostUsageDateParser.isoInternetDateTime" private static let dayFormatterKey = "CostUsageDateParser.dayFormatter" private static let monthDayYearFormatterKey = "CostUsageDateParser.monthDayYearFormatter" private static let monthYearFormatterKey = "CostUsageDateParser.monthYearFormatter" private static let fullMonthYearFormatterKey = "CostUsageDateParser.fullMonthYearFormatter" private static let yearMonthFormatterKey = "CostUsageDateParser.yearMonthFormatter" static func parse(_ text: String?) -> Date? { guard let text, !text.isEmpty else { return nil } let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines) if let d = self.isoFormatter( key: self.isoWithFractionalSecondsKey, options: [.withInternetDateTime, .withFractionalSeconds]) .date(from: trimmed) { return d } if let d = self.isoFormatter(key: self.isoInternetDateTimeKey, options: [.withInternetDateTime]) .date(from: trimmed) { return d } if let d = self.dateFormatter(key: self.dayFormatterKey, format: "yyyy-MM-dd").date(from: trimmed) { return d } if let d = self.dateFormatter(key: self.monthDayYearFormatterKey, format: "MMM d, yyyy") .date(from: trimmed) { return d } return nil } static func parseMonth(_ text: String?) -> Date? { guard let text, !text.isEmpty else { return nil } let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines) if let d = self.dateFormatter(key: self.monthYearFormatterKey, format: "MMM yyyy").date(from: trimmed) { return d } if let d = self.dateFormatter(key: self.fullMonthYearFormatterKey, format: "MMMM yyyy").date(from: trimmed) { return d } if let d = self.dateFormatter(key: self.yearMonthFormatterKey, format: "yyyy-MM").date(from: trimmed) { return d } return nil } private static func isoFormatter( key: String, options: ISO8601DateFormatter.Options) -> ISO8601DateFormatter { let threadDict = Thread.current.threadDictionary if let cached = threadDict[key] as? ISO8601DateFormatter { return cached } let formatter = ISO8601DateFormatter() formatter.formatOptions = options threadDict[key] = formatter return formatter } private static func dateFormatter(key: String, format: String) -> DateFormatter { let threadDict = Thread.current.threadDictionary let timeZone = TimeZone.current let cacheKey = "\(key).\(timeZone.identifier)" if let cached = threadDict[cacheKey] as? DateFormatter { return cached } let formatter = DateFormatter() formatter.locale = Locale(identifier: "en_US_POSIX") formatter.timeZone = timeZone formatter.dateFormat = format formatter.isLenient = false threadDict[cacheKey] = formatter return formatter } } enum CostUsageBucketInterval { static func contains( _ date: Date, startTime: Date, endTime: Date) -> Bool { guard startTime < endTime else { return false } return startTime <= date && date < endTime } } enum CostUsageLocalDay { static func key(from date: Date, calendar: Calendar = .current) -> String { let components = calendar.dateComponents([.year, .month, .day], from: date) let year = components.year ?? 0 let month = components.month ?? 0 let day = components.day ?? 0 return String(format: "%04d-%02d-%02d", year, month, day) } }