Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Sources/TokPeek/TokPeekApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ struct TokPeekApp: App {
private var appDelegate

@StateObject private var store = UsageStore(
loader: TokscaleClient()
loader: TokscaleClient(),
reportCache: FileUsageReportCache()
)
@StateObject private var settings = SettingsStore()
@StateObject private var launchAtLogin = LaunchAtLoginController()
Expand Down
18 changes: 9 additions & 9 deletions Sources/TokPeekKit/UsageReport.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Foundation

public struct UsageReport: Decodable, Sendable, Equatable {
public struct UsageReport: Codable, Sendable, Equatable {
public let meta: ReportMetadata
public let summary: UsageSummary
public let years: [YearSummary]
Expand Down Expand Up @@ -61,15 +61,15 @@ public struct UsageReport: Decodable, Sendable, Equatable {
}
}

public struct ReportMetadata: Decodable, Sendable, Equatable {
public struct ReportMetadata: Codable, Sendable, Equatable {
public let generatedAt: String
public let version: String
public let dateRangeStart: String
public let dateRangeEnd: String
public let processingTimeMs: UInt32
}

public struct UsageSummary: Decodable, Sendable, Equatable {
public struct UsageSummary: Codable, Sendable, Equatable {
public let totalTokens: Int64
public let totalCost: Double
public let totalDays: Int
Expand All @@ -80,15 +80,15 @@ public struct UsageSummary: Decodable, Sendable, Equatable {
public let models: [String]
}

public struct YearSummary: Decodable, Sendable, Equatable {
public struct YearSummary: Codable, Sendable, Equatable {
public let year: String
public let totalTokens: Int64
public let totalCost: Double
public let rangeStart: String
public let rangeEnd: String
}

public struct DailyContribution: Decodable, Sendable, Equatable, Identifiable {
public struct DailyContribution: Codable, Sendable, Equatable, Identifiable {
public var id: String { date }

public let date: String
Expand All @@ -99,7 +99,7 @@ public struct DailyContribution: Decodable, Sendable, Equatable, Identifiable {
public let activeTimeMs: Int64?
}

public struct HourlyContribution: Decodable, Sendable, Equatable, Identifiable {
public struct HourlyContribution: Codable, Sendable, Equatable, Identifiable {
public var id: String { hour }

public let hour: String
Expand All @@ -120,21 +120,21 @@ public struct HourlyContribution: Decodable, Sendable, Equatable, Identifiable {
}
}

public struct DailyTotals: Decodable, Sendable, Equatable {
public struct DailyTotals: Codable, Sendable, Equatable {
public let tokens: Int64
public let cost: Double
public let messages: Int
}

public struct TokenBreakdown: Decodable, Sendable, Equatable {
public struct TokenBreakdown: Codable, Sendable, Equatable {
public let input: Int64
public let output: Int64
public let cacheRead: Int64
public let cacheWrite: Int64
public let reasoning: Int64
}

public struct ClientContribution: Decodable, Sendable, Equatable, Identifiable {
public struct ClientContribution: Codable, Sendable, Equatable, Identifiable {
public var id: String {
"\(client)|\(providerId)|\(modelId)"
}
Expand Down
96 changes: 96 additions & 0 deletions Sources/TokPeekKit/UsageReportCache.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import Foundation

public struct UsageReportSnapshot: Codable, Sendable, Equatable {
public let request: UsageRequest
public let report: UsageReport
public let refreshedAt: Date

public init(
request: UsageRequest,
report: UsageReport,
refreshedAt: Date
) {
self.request = request
self.report = report
self.refreshedAt = refreshedAt
}
}

public protocol UsageReportCaching: Sendable {
func load() async -> UsageReportSnapshot?
func save(_ snapshot: UsageReportSnapshot) async
}

public actor FileUsageReportCache: UsageReportCaching {
private struct Envelope: Codable {
let version: Int
let snapshot: UsageReportSnapshot
}

private static let formatVersion = 1
private let fileURL: URL

public init() {
fileURL = Self.defaultFileURL()
}

public init(fileURL: URL) {
self.fileURL = fileURL
}

public func load() async -> UsageReportSnapshot? {
guard let data = try? Data(contentsOf: fileURL) else {
return nil
}

guard
let envelope = try? JSONDecoder().decode(
Envelope.self,
from: data
),
envelope.version == Self.formatVersion
else {
try? FileManager.default.removeItem(at: fileURL)
return nil
}

return envelope.snapshot
}

public func save(_ snapshot: UsageReportSnapshot) async {
let envelope = Envelope(
version: Self.formatVersion,
snapshot: snapshot
)

do {
try FileManager.default.createDirectory(
at: fileURL.deletingLastPathComponent(),
withIntermediateDirectories: true
)
let data = try JSONEncoder().encode(envelope)
try data.write(to: fileURL, options: .atomic)
} catch {
// A disposable startup cache must never make a successful usage
// refresh look like a failure.
}
}

private static func defaultFileURL() -> URL {
let baseDirectory =
FileManager.default.urls(
for: .cachesDirectory,
in: .userDomainMask
).first ?? FileManager.default.temporaryDirectory

return baseDirectory
.appendingPathComponent(
"com.tokpeek.app",
isDirectory: true
)
.appendingPathComponent(
"usage-report-v1.json",
isDirectory: false
)
}
}
2 changes: 1 addition & 1 deletion Sources/TokPeekKit/UsageRequest.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Foundation

public struct UsageRequest: Codable, Sendable, Equatable {
public struct UsageRequest: Codable, Sendable, Hashable {
public var homeDirectory: String?
public var clients: [String]?
public var since: String?
Expand Down
Loading