import Foundation 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 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], 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.updatedAt = updatedAt }} 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 } } 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<String> = [] 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 += cacheCreationTokensLines 1–500 of 786. Load more
import Foundation 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 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], 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.updatedAt = updatedAt } } 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 } } 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<String> = [] 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