import Foundation public struct OpenAIAPIUsageSnapshot: Codable, Equatable, Sendable { public struct DailyBucket: Codable, Equatable, Sendable, Identifiable { public let day: String public let startTime: Date public let endTime: Date public let costUSD: Double public let requests: Int public let inputTokens: Int public let cachedInputTokens: Int public let outputTokens: Int public let totalTokens: Int public let lineItems: [LineItemBreakdown] public let models: [ModelBreakdown] public var id: String { self.day } public init( day: String, startTime: Date, endTime: Date, costUSD: Double, requests: Int, inputTokens: Int, cachedInputTokens: Int, outputTokens: Int, totalTokens: Int, lineItems: [LineItemBreakdown], models: [ModelBreakdown]) { self.day = day self.startTime = startTime self.endTime = endTime self.costUSD = costUSD self.requests = requests self.inputTokens = inputTokens self.cachedInputTokens = cachedInputTokens self.outputTokens = outputTokens self.totalTokens = totalTokens self.lineItems = lineItems self.models = models } } public struct LineItemBreakdown: Codable, Equatable, Sendable, Identifiable { public let name: String public let costUSD: Double public var id: String { self.name } public init(name: String, costUSD: Double) { self.name = name self.costUSD = costUSD } } public struct ModelBreakdown: Codable, Equatable, Sendable, Identifiable { public let name: String public let requests: Int public let inputTokens: Int public let cachedInputTokens: Int public let outputTokens: Int public let totalTokens: Int public var id: String { self.name } public init( name: String, requests: Int, inputTokens: Int, cachedInputTokens: Int, outputTokens: Int, totalTokens: Int) { self.name = name self.requests = requests self.inputTokens = inputTokens self.cachedInputTokens = cachedInputTokens self.outputTokens = outputTokens self.totalTokens = totalTokens } } public struct Summary: Equatable, Sendable { public let costUSD: Double public let requests: Int public let inputTokens: Int public let cachedInputTokens: Int public let outputTokens: Int public let totalTokens: Int public init( costUSD: Double, requests: Int, inputTokens: Int, cachedInputTokens: Int, outputTokens: Int, totalTokens: Int) { self.costUSD = costUSD self.requests = requests self.inputTokens = inputTokens self.cachedInputTokens = cachedInputTokens self.outputTokens = outputTokens self.totalTokens = totalTokens } } public let daily: [DailyBucket] public let updatedAt: Date public let historyDays: Int public let projectID: String? public init(daily: [DailyBucket], updatedAt: Date, historyDays: Int = 30, projectID: String? = nil) { self.daily = daily.sorted { $0.startTime < $1.startTime } self.updatedAt = updatedAt self.historyDays = max(1, min(365, historyDays)) self.projectID = OpenAIAPISettingsReader.cleaned(projectID) } public var last30Days: Summary { self.historyDays == 1 ? self.currentDay : self.summary(days: self.historyDays) } public var historyWindowLabel: String { self.historyDays == 1 ? "Today" : "\(self.historyDays)d" } public var historyWindowPeriodLabel: String { self.historyDays == 1 ? "Today" : "Last \(self.historyDays) days" } public var last7Days: Summary { self.summary(days: 7) } public var currentDay: Summary { self.summary(forLocalDayContaining: self.updatedAt) } public var latestDay: Summary { self.summary(days: 1) } public func summary(forLocalDayContaining date: Date, calendar _: Calendar = .current) -> Summary { let selected = self.daily.filter { bucket in CostUsageBucketInterval.contains( date, startTime: bucket.startTime, endTime: bucket.endTime) } return Summary( costUSD: selected.reduce(0) { $0 + $1.costUSD }, requests: selected.reduce(0) { $0 + $1.requests }, inputTokens: selected.reduce(0) { $0 + $1.inputTokens }, cachedInputTokens: selected.reduce(0) { $0 + $1.cachedInputTokens }, outputTokens: selected.reduce(0) { $0 + $1.outputTokens }, totalTokens: selected.reduce(0) { $0 + $1.totalTokens }) } public func summary(days: Int) -> Summary { let selected = self.daily.suffix(max(1, days)) return Summary( costUSD: selected.reduce(0) { $0 + $1.costUSD }, requests: selected.reduce(0) { $0 + $1.requests }, inputTokens: selected.reduce(0) { $0 + $1.inputTokens }, cachedInputTokens: selected.reduce(0) { $0 + $1.cachedInputTokens }, outputTokens: selected.reduce(0) { $0 + $1.outputTokens }, totalTokens: selected.reduce(0) { $0 + $1.totalTokens }) } public var topModels: [ModelBreakdown] { var totals: [String: ModelAccumulator] = [:] for day in self.daily { for model in day.models { totals[model.name, default: ModelAccumulator()].add(model) } } return totals .map { name, total in total.makeModel(name: name) } .sorted { if $0.totalTokens == $1.totalTokens { return $0.name < $1.name } return $0.totalTokens > $1.totalTokens } } public var topLineItems: [LineItemBreakdown] { var totals: [String: Double] = [:] for day in self.daily { for item in day.lineItems { totals[item.name, default: 0] += item.costUSD } } return totals .map { LineItemBreakdown(name: $0.key, costUSD: $0.value) } .sorted { if $0.costUSD == $1.costUSD { return $0.name < $1.name } return $0.costUSD > $1.costUSD } } public func toUsageSnapshot() -> UsageSnapshot { let total = self.last30Days return UsageSnapshot( primary: nil, secondary: nil, providerCost: ProviderCostSnapshot( used: total.costUSD, limit: 0, currencyCode: "USD", period: self.historyWindowPeriodLabel, updatedAt: self.updatedAt), openAIAPIUsage: self, updatedAt: self.updatedAt, identity: ProviderIdentitySnapshot( providerID: .openai, accountEmail: nil, accountOrganization: self.identityAccountOrganization, loginMethod: self.identityLoginMethod)) } private var identityLoginMethod: String { guard let projectID else { return "Admin API" } return "Admin API: \(projectID)" } private var identityAccountOrganization: String? { guard let projectID else { return nil } return "Project: \(projectID)" } public func toCostUsageTokenSnapshot() -> CostUsageTokenSnapshot { let daily = self.daily.map { bucket in let modelBreakdowns = bucket.models.map { CostUsageDailyReport.ModelBreakdown( modelName: $0.name, costUSD: nil, totalTokens: $0.totalTokens, requestCount: $0.requests) } let modelsUsed = bucket.models.map(\.name) return CostUsageDailyReport.Entry( date: bucket.day, inputTokens: bucket.inputTokens, outputTokens: bucket.outputTokens, cacheReadTokens: bucket.cachedInputTokens, cacheCreationTokens: nil, totalTokens: bucket.totalTokens, requestCount: bucket.requests, costUSD: bucket.costUSD, modelsUsed: modelsUsed.isEmpty ? nil : modelsUsed, modelBreakdowns: modelBreakdowns.isEmpty ? nil : modelBreakdowns) } let today = self.currentDay let total = self.last30Days return CostUsageTokenSnapshot( sessionTokens: today.totalTokens, sessionCostUSD: today.costUSD, sessionRequests: today.requests, last30DaysTokens: total.totalTokens, last30DaysCostUSD: total.costUSD, last30DaysRequests: total.requests, historyDays: self.historyDays, daily: daily, updatedAt: self.updatedAt) } private struct ModelAccumulator { var requests = 0 var inputTokens = 0 var cachedInputTokens = 0 var outputTokens = 0 var totalTokens = 0 mutating func add(_ model: ModelBreakdown) { self.requests += model.requests self.inputTokens += model.inputTokens self.cachedInputTokens += model.cachedInputTokens self.outputTokens += model.outputTokens self.totalTokens += model.totalTokens } func makeModel(name: String) -> ModelBreakdown { ModelBreakdown( name: name, requests: self.requests, inputTokens: self.inputTokens, cachedInputTokens: self.cachedInputTokens, outputTokens: self.outputTokens, totalTokens: self.totalTokens) } }}
import Foundation
public struct OpenAIAPIUsageSnapshot: Codable, Equatable, Sendable {
public struct DailyBucket: Codable, Equatable, Sendable, Identifiable {
public let day: String
public let startTime: Date
public let endTime: Date
public let costUSD: Double
public let requests: Int
public let inputTokens: Int
public let cachedInputTokens: Int
public let outputTokens: Int
public let totalTokens: Int
public let lineItems: [LineItemBreakdown]
public let models: [ModelBreakdown]
public var id: String {
self.day
}
public init(
day: String,
startTime: Date,
endTime: Date,
costUSD: Double,
requests: Int,
inputTokens: Int,
cachedInputTokens: Int,
outputTokens: Int,
totalTokens: Int,
lineItems: [LineItemBreakdown],
models: [ModelBreakdown])
{
self.day = day
self.startTime = startTime
self.endTime = endTime
self.costUSD = costUSD
self.requests = requests
self.inputTokens = inputTokens
self.cachedInputTokens = cachedInputTokens
self.outputTokens = outputTokens
self.totalTokens = totalTokens
self.lineItems = lineItems
self.models = models
}
}
public struct LineItemBreakdown: Codable, Equatable, Sendable, Identifiable {
public let name: String
public let costUSD: Double
public var id: String {
self.name
}
public init(name: String, costUSD: Double) {
self.name = name
self.costUSD = costUSD
}
}
public struct ModelBreakdown: Codable, Equatable, Sendable, Identifiable {
public let name: String
public let requests: Int
public let inputTokens: Int
public let cachedInputTokens: Int
public let outputTokens: Int
public let totalTokens: Int
public var id: String {
self.name
}
public init(
name: String,
requests: Int,
inputTokens: Int,
cachedInputTokens: Int,
outputTokens: Int,
totalTokens: Int)
{
self.name = name
self.requests = requests
self.inputTokens = inputTokens
self.cachedInputTokens = cachedInputTokens
self.outputTokens = outputTokens
self.totalTokens = totalTokens
}
}
public struct Summary: Equatable, Sendable {
public let costUSD: Double
public let requests: Int
public let inputTokens: Int
public let cachedInputTokens: Int
public let outputTokens: Int
public let totalTokens: Int
public init(
costUSD: Double,
requests: Int,
inputTokens: Int,
cachedInputTokens: Int,
outputTokens: Int,
totalTokens: Int)
{
self.costUSD = costUSD
self.requests = requests
self.inputTokens = inputTokens
self.cachedInputTokens = cachedInputTokens
self.outputTokens = outputTokens
self.totalTokens = totalTokens
}
}
public let daily: [DailyBucket]
public let updatedAt: Date
public let historyDays: Int
public let projectID: String?
public init(daily: [DailyBucket], updatedAt: Date, historyDays: Int = 30, projectID: String? = nil) {
self.daily = daily.sorted { $0.startTime < $1.startTime }
self.updatedAt = updatedAt
self.historyDays = max(1, min(365, historyDays))
self.projectID = OpenAIAPISettingsReader.cleaned(projectID)
}
public var last30Days: Summary {
self.historyDays == 1 ? self.currentDay : self.summary(days: self.historyDays)
}
public var historyWindowLabel: String {
self.historyDays == 1 ? "Today" : "\(self.historyDays)d"
}
public var historyWindowPeriodLabel: String {
self.historyDays == 1 ? "Today" : "Last \(self.historyDays) days"
}
public var last7Days: Summary {
self.summary(days: 7)
}
public var currentDay: Summary {
self.summary(forLocalDayContaining: self.updatedAt)
}
public var latestDay: Summary {
self.summary(days: 1)
}
public func summary(forLocalDayContaining date: Date, calendar _: Calendar = .current) -> Summary {
let selected = self.daily.filter { bucket in
CostUsageBucketInterval.contains(
date,
startTime: bucket.startTime,
endTime: bucket.endTime)
}
return Summary(
costUSD: selected.reduce(0) { $0 + $1.costUSD },
requests: selected.reduce(0) { $0 + $1.requests },
inputTokens: selected.reduce(0) { $0 + $1.inputTokens },
cachedInputTokens: selected.reduce(0) { $0 + $1.cachedInputTokens },
outputTokens: selected.reduce(0) { $0 + $1.outputTokens },
totalTokens: selected.reduce(0) { $0 + $1.totalTokens })
}
public func summary(days: Int) -> Summary {
let selected = self.daily.suffix(max(1, days))
return Summary(
costUSD: selected.reduce(0) { $0 + $1.costUSD },
requests: selected.reduce(0) { $0 + $1.requests },
inputTokens: selected.reduce(0) { $0 + $1.inputTokens },
cachedInputTokens: selected.reduce(0) { $0 + $1.cachedInputTokens },
outputTokens: selected.reduce(0) { $0 + $1.outputTokens },
totalTokens: selected.reduce(0) { $0 + $1.totalTokens })
}
public var topModels: [ModelBreakdown] {
var totals: [String: ModelAccumulator] = [:]
for day in self.daily {
for model in day.models {
totals[model.name, default: ModelAccumulator()].add(model)
}
}
return totals
.map { name, total in total.makeModel(name: name) }
.sorted {
if $0.totalTokens == $1.totalTokens { return $0.name < $1.name }
return $0.totalTokens > $1.totalTokens
}
}
public var topLineItems: [LineItemBreakdown] {
var totals: [String: Double] = [:]
for day in self.daily {
for item in day.lineItems {
totals[item.name, default: 0] += item.costUSD
}
}
return totals
.map { LineItemBreakdown(name: $0.key, costUSD: $0.value) }
.sorted {
if $0.costUSD == $1.costUSD { return $0.name < $1.name }
return $0.costUSD > $1.costUSD
}
}
public func toUsageSnapshot() -> UsageSnapshot {
let total = self.last30Days
return UsageSnapshot(
primary: nil,
secondary: nil,
providerCost: ProviderCostSnapshot(
used: total.costUSD,
limit: 0,
currencyCode: "USD",
period: self.historyWindowPeriodLabel,
updatedAt: self.updatedAt),
openAIAPIUsage: self,
updatedAt: self.updatedAt,
identity: ProviderIdentitySnapshot(
providerID: .openai,
accountEmail: nil,
accountOrganization: self.identityAccountOrganization,
loginMethod: self.identityLoginMethod))
}
private var identityLoginMethod: String {
guard let projectID else { return "Admin API" }
return "Admin API: \(projectID)"
}
private var identityAccountOrganization: String? {
guard let projectID else { return nil }
return "Project: \(projectID)"
}
public func toCostUsageTokenSnapshot() -> CostUsageTokenSnapshot {
let daily = self.daily.map { bucket in
let modelBreakdowns = bucket.models.map {
CostUsageDailyReport.ModelBreakdown(
modelName: $0.name,
costUSD: nil,
totalTokens: $0.totalTokens,
requestCount: $0.requests)
}
let modelsUsed = bucket.models.map(\.name)
return CostUsageDailyReport.Entry(
date: bucket.day,
inputTokens: bucket.inputTokens,
outputTokens: bucket.outputTokens,
cacheReadTokens: bucket.cachedInputTokens,
cacheCreationTokens: nil,
totalTokens: bucket.totalTokens,
requestCount: bucket.requests,
costUSD: bucket.costUSD,
modelsUsed: modelsUsed.isEmpty ? nil : modelsUsed,
modelBreakdowns: modelBreakdowns.isEmpty ? nil : modelBreakdowns)
}
let today = self.currentDay
let total = self.last30Days
return CostUsageTokenSnapshot(
sessionTokens: today.totalTokens,
sessionCostUSD: today.costUSD,
sessionRequests: today.requests,
last30DaysTokens: total.totalTokens,
last30DaysCostUSD: total.costUSD,
last30DaysRequests: total.requests,
historyDays: self.historyDays,
daily: daily,
updatedAt: self.updatedAt)
}
private struct ModelAccumulator {
var requests = 0
var inputTokens = 0
var cachedInputTokens = 0
var outputTokens = 0
var totalTokens = 0
mutating func add(_ model: ModelBreakdown) {
self.requests += model.requests
self.inputTokens += model.inputTokens
self.cachedInputTokens += model.cachedInputTokens
self.outputTokens += model.outputTokens
self.totalTokens += model.totalTokens
}
func makeModel(name: String) -> ModelBreakdown {
ModelBreakdown(
name: name,
requests: self.requests,
inputTokens: self.inputTokens,
cachedInputTokens: self.cachedInputTokens,
outputTokens: self.outputTokens,
totalTokens: self.totalTokens)
}
}
}