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<CodingKeys>) -> [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 }Lines 1–500 of 1000. Load more
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<CodingKeys>) -> [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 }