Skip to content
Open
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
38 changes: 25 additions & 13 deletions Sources/CodexBarCore/CostUsageFetcher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -193,10 +193,7 @@ public struct CostUsageFetcher: Sendable {
throw CostUsageError.unsupportedProvider(provider)
}

let until = now
let clampedHistoryDays = max(1, min(365, historyDays))
// Rolling window is inclusive, so a 30-day display starts 29 days before `now`.
let since = Calendar.current.date(byAdding: .day, value: -(clampedHistoryDays - 1), to: now) ?? now

if let remoteSnapshot = try await self.loadRemoteTokenSnapshot(
provider: provider,
Expand All @@ -212,6 +209,8 @@ public struct CostUsageFetcher: Sendable {
overrideScannerOptions,
provider: provider,
codexHomePath: codexHomePath)
// Rolling window is inclusive, so a 30-day display starts 29 days before `now`.
let since = options.calendar.date(byAdding: .day, value: -(clampedHistoryDays - 1), to: now) ?? now
let scopedCodexHomePath = codexHomePath?.trimmingCharacters(in: .whitespacesAndNewlines)
let shouldMergePiUsage = provider != .codex || scopedCodexHomePath?.isEmpty != false
await Self.refreshPricingIfAllowed(
Expand All @@ -236,6 +235,7 @@ public struct CostUsageFetcher: Sendable {
if resolvedPiOptions.cacheRoot == nil {
resolvedPiOptions.cacheRoot = options.cacheRoot
}
resolvedPiOptions.calendar = options.calendar
if forceRefresh || bypassScannerDebounce {
resolvedPiOptions.refreshMinIntervalSeconds = 0
}
Expand All @@ -251,7 +251,7 @@ public struct CostUsageFetcher: Sendable {
var daily = try CostUsageScanner.loadDailyReportCancellable(
provider: provider,
since: since,
until: until,
until: now,
now: now,
options: scanOptions,
checkCancellation: checkCancellation)
Expand All @@ -267,7 +267,7 @@ public struct CostUsageFetcher: Sendable {
daily = try CostUsageScanner.loadDailyReportCancellable(
provider: provider,
since: since,
until: until,
until: now,
now: now,
options: fallback,
checkCancellation: checkCancellation)
Expand All @@ -282,7 +282,8 @@ public struct CostUsageFetcher: Sendable {
let cache = CostUsageScanner.codexCache(
CostUsageCacheIO.load(provider: .codex, cacheRoot: scanOptions.cacheRoot),
scopedTo: roots)
let range = CostUsageScanner.CostUsageDayRange(since: since, until: until)
let range = CostUsageScanner.CostUsageDayRange(
since: since, until: now, calendar: scanOptions.calendar)
projects = CostUsageScanner.buildCodexProjectBreakdownsFromCache(
cache: cache,
range: range,
Expand All @@ -297,7 +298,7 @@ public struct CostUsageFetcher: Sendable {
let piReport = try PiSessionCostScanner.loadDailyReportCancellable(
provider: provider,
since: since,
until: until,
until: now,
now: now,
options: piOptions,
checkCancellation: checkCancellation)
Expand Down Expand Up @@ -349,6 +350,7 @@ public struct CostUsageFetcher: Sendable {
from: scanResult.daily,
now: now,
historyDays: clampedHistoryDays,
calendar: scanOptions.calendar,
projects: scanResult.projects,
sessions: scanResult.sessions)
}
Expand Down Expand Up @@ -470,10 +472,16 @@ public struct CostUsageFetcher: Sendable {
// cooperative pool alongside the scans themselves.
let cachedSnapshot: CachedCodexTokenSnapshotResult?? = try? await CostUsageScanExecutor.run { _ in
let clampedHistoryDays = max(1, min(365, historyDays))
let until = now
let since = Calendar.current.date(byAdding: .day, value: -(clampedHistoryDays - 1), to: now) ?? now
let range = CostUsageScanner.CostUsageDayRange(since: since, until: until)
let options = overrideScannerOptions ?? CostUsageScanner.Options()
let until = now
let since = options.calendar.date(
byAdding: .day,
value: -(clampedHistoryDays - 1),
to: now) ?? now
let range = CostUsageScanner.CostUsageDayRange(
since: since,
until: until,
calendar: options.calendar)
let roots = CostUsageScanner.codexSessionsRoots(options: options)
let cache = CostUsageScanner.codexCache(
CostUsageCacheIO.load(provider: .codex, cacheRoot: options.cacheRoot),
Expand All @@ -487,7 +495,8 @@ public struct CostUsageFetcher: Sendable {
var scanTimes: [Date] = []
var piMerged = false

if !cache.days.isEmpty,
if cache.timeZoneIdentifier == range.calendar.timeZone.identifier,
!cache.days.isEmpty,
cache.roots == CostUsageScanner.codexRootsFingerprint(options: options),
!CostUsageScanner.requestedWindowExpandsCache(range: range, cache: cache)
{
Expand Down Expand Up @@ -521,7 +530,8 @@ public struct CostUsageFetcher: Sendable {
since: since,
until: until,
now: now,
cacheRoot: options.cacheRoot)
cacheRoot: options.cacheRoot,
calendar: options.calendar)
{
reports.append(piResult.report)
piMerged = true
Expand All @@ -546,6 +556,7 @@ public struct CostUsageFetcher: Sendable {
from: CostUsageDailyReport.merged(reports),
now: now,
historyDays: clampedHistoryDays,
calendar: options.calendar,
projects: Self.mergedProjectBreakdowns(projects),
sessions: sessions,
updatedAt: scanTimes.min()),
Expand Down Expand Up @@ -627,6 +638,7 @@ public struct CostUsageFetcher: Sendable {
now: Date,
historyDays: Int = 30,
useCurrentLocalDayForSession: Bool = true,
calendar: Calendar = .current,
meteredCostUSD: Double? = nil,
credentialScopeFingerprint: String? = nil,
historyLabel: String? = nil,
Expand All @@ -635,7 +647,7 @@ public struct CostUsageFetcher: Sendable {
updatedAt: Date? = nil) -> CostUsageTokenSnapshot
{
let sessionEntry = useCurrentLocalDayForSession
? CostUsageTokenSnapshot.entry(in: daily.data, forLocalDayContaining: now)
? CostUsageTokenSnapshot.entry(in: daily.data, forLocalDayContaining: now, calendar: calendar)
: CostUsageTokenSnapshot.latestEntry(in: daily.data)
let hasHistoricalRows = !daily.data.isEmpty
let sessionTokens: Int? = if let sessionEntry {
Expand Down
7 changes: 7 additions & 0 deletions Sources/CodexBarCore/CostUsageModels.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1064,7 +1064,14 @@ enum CostUsageBucketInterval {
}

enum CostUsageLocalDay {
static func gregorianCalendar(matching calendar: Calendar = .current) -> Calendar {
var gregorian = Calendar(identifier: .gregorian)
gregorian.timeZone = calendar.timeZone
return gregorian
}

static func key(from date: Date, calendar: Calendar = .current) -> String {
let calendar = Self.gregorianCalendar(matching: calendar)
let components = calendar.dateComponents([.year, .month, .day], from: date)
let year = components.year ?? 0
let month = components.month ?? 0
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Generated by Scripts/regenerate-codex-parser-hash.sh. Do not edit by hand.

enum CodexParserHash {
static let value = "48ac20dad61e9a7f"
static let value = "44f1dfa1688efc9f"
}
13 changes: 10 additions & 3 deletions Sources/CodexBarCore/PiSessionCostCache.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import Foundation

enum PiSessionCostCacheIO {
/// Artifact schema version. Pricing changes are tracked separately by `pricingKey`.
private static let artifactVersion = 7
private static let artifactVersion = 8

private static func defaultCacheRoot() -> URL {
let root = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!
Expand All @@ -27,11 +27,17 @@ enum PiSessionCostCacheIO {
return decoded
}

static func save(cache: PiSessionCostCache, cacheRoot: URL? = nil) {
static func save(
cache: PiSessionCostCache,
cacheRoot: URL? = nil,
calendar: Calendar = .current)
{
let url = self.cacheFileURL(cacheRoot: cacheRoot)
let dir = url.deletingLastPathComponent()
try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)

var cache = cache
cache.timeZoneIdentifier = calendar.timeZone.identifier
let tmp = dir.appendingPathComponent(".tmp-\(UUID().uuidString).json", isDirectory: false)
let data = (try? JSONEncoder().encode(cache)) ?? Data()
do {
Expand All @@ -52,11 +58,12 @@ struct PiSessionCostCache: Codable {
var lastScanUnixMs: Int64 = 0
var scanSinceKey: String?
var scanUntilKey: String?
var timeZoneIdentifier: String?
var pricingKey: String?
var daysByProvider: [String: [String: [String: PiPackedUsage]]] = [:]
var files: [String: PiSessionFileUsage] = [:]

init(version: Int = 7) {
init(version: Int = 8) {
self.version = version
}
}
Expand Down
73 changes: 53 additions & 20 deletions Sources/CodexBarCore/PiSessionCostScanner.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,22 @@ enum PiSessionCostScanner {
var piSessionsRoot: URL?
var ompSessionsRoot: URL?
var cacheRoot: URL?
var calendar: Calendar
var refreshMinIntervalSeconds: TimeInterval = 60
var forceRescan: Bool = false

init(
piSessionsRoot: URL? = nil,
ompSessionsRoot: URL? = nil,
cacheRoot: URL? = nil,
calendar: Calendar = .current,
refreshMinIntervalSeconds: TimeInterval = 60,
forceRescan: Bool = false)
{
self.piSessionsRoot = piSessionsRoot
self.ompSessionsRoot = ompSessionsRoot
self.cacheRoot = cacheRoot
self.calendar = calendar
self.refreshMinIntervalSeconds = refreshMinIntervalSeconds
self.forceRescan = forceRescan
}
Expand Down Expand Up @@ -108,8 +111,14 @@ enum PiSessionCostScanner {
return CostUsageDailyReport(data: [], summary: nil)
}

let range = CostUsageScanner.CostUsageDayRange(since: since, until: until)
let range = CostUsageScanner.CostUsageDayRange(
since: since,
until: until,
calendar: options.calendar)
var cache = PiSessionCostCacheIO.load(cacheRoot: options.cacheRoot)
if cache.timeZoneIdentifier != range.calendar.timeZone.identifier {
cache = PiSessionCostCache()
}
let nowMs = Int64(now.timeIntervalSince1970 * 1000)
let refreshMs = Int64(max(0, options.refreshMinIntervalSeconds) * 1000)
let pricingContext = self.pricingContext(now: now, cacheRoot: options.cacheRoot)
Expand All @@ -125,10 +134,14 @@ enum PiSessionCostScanner {
if shouldRefresh {
try checkCancellation?()
let roots = self.defaultSessionRoots(options: options)
let startCutoff = self.dateFromDayKey(range.scanSinceKey) ?? since
let startCutoff = self.dateFromDayKey(range.scanSinceKey, calendar: range.calendar) ?? since
var files: [SessionFileCandidate] = []
for (rootIndex, root) in roots.enumerated() {
for url in self.listPiSessionFiles(root: root, startCutoffLocal: startCutoff) {
for url in self.listPiSessionFiles(
root: root,
startCutoffLocal: startCutoff,
calendar: range.calendar)
{
files.append(SessionFileCandidate(url: url, rootIndex: rootIndex))
}
}
Expand Down Expand Up @@ -168,7 +181,10 @@ enum PiSessionCostScanner {
cache.pricingKey = pricingContext.pricingKey
cache.lastScanUnixMs = nowMs
try checkCancellation?()
PiSessionCostCacheIO.save(cache: cache, cacheRoot: options.cacheRoot)
PiSessionCostCacheIO.save(
cache: cache,
cacheRoot: options.cacheRoot,
calendar: range.calendar)
}

return self.buildReport(
Expand All @@ -188,27 +204,31 @@ enum PiSessionCostScanner {
since: Date,
until: Date,
now: Date = Date(),
cacheRoot: URL? = nil) -> CostUsageDailyReport?
cacheRoot: URL? = nil,
calendar: Calendar = .current) -> CostUsageDailyReport?
{
self.loadCachedDailyReportResult(
provider: provider,
since: since,
until: until,
now: now,
cacheRoot: cacheRoot)?.report
cacheRoot: cacheRoot,
calendar: calendar)?.report
}

static func loadCachedDailyReportResult(
provider: UsageProvider,
since: Date,
until: Date,
now: Date = Date(),
cacheRoot: URL? = nil) -> CachedDailyReportResult?
cacheRoot: URL? = nil,
calendar: Calendar = .current) -> CachedDailyReportResult?
{
guard provider == .codex || provider == .claude else { return nil }

let range = CostUsageScanner.CostUsageDayRange(since: since, until: until)
let range = CostUsageScanner.CostUsageDayRange(since: since, until: until, calendar: calendar)
let cache = PiSessionCostCacheIO.load(cacheRoot: cacheRoot)
guard cache.timeZoneIdentifier == range.calendar.timeZone.identifier else { return nil }
guard !cache.daysByProvider.isEmpty else { return nil }
guard !self.requestedWindowExpandsCache(range: range, cache: cache) else { return nil }

Expand Down Expand Up @@ -271,7 +291,11 @@ enum PiSessionCostScanner {
}
}

private static func listPiSessionFiles(root: URL, startCutoffLocal: Date) -> [URL] {
private static func listPiSessionFiles(
root: URL,
startCutoffLocal: Date,
calendar: Calendar) -> [URL]
{
guard FileManager.default.fileExists(atPath: root.path) else { return [] }

let keys: Set<URLResourceKey> = [.isRegularFileKey, .contentModificationDateKey]
Expand All @@ -292,7 +316,11 @@ enum PiSessionCostScanner {
let startedAt = self.parseSessionStartFromFilename(item.lastPathComponent)
let modifiedAt = values?.contentModificationDate
if self
.shouldIncludeFile(startedAt: startedAt, modifiedAt: modifiedAt, startCutoffLocal: startCutoffLocal)
.shouldIncludeFile(
startedAt: startedAt,
modifiedAt: modifiedAt,
startCutoffLocal: startCutoffLocal,
calendar: calendar)
{
output.append(item)
}
Expand All @@ -304,12 +332,13 @@ enum PiSessionCostScanner {
private static func shouldIncludeFile(
startedAt: Date?,
modifiedAt: Date?,
startCutoffLocal: Date) -> Bool
startCutoffLocal: Date,
calendar: Calendar) -> Bool
{
if let modifiedAt, self.localMidnight(modifiedAt) >= startCutoffLocal {
if let modifiedAt, self.localMidnight(modifiedAt, calendar: calendar) >= startCutoffLocal {
return true
}
if let startedAt, self.localMidnight(startedAt) >= startCutoffLocal {
if let startedAt, self.localMidnight(startedAt, calendar: calendar) >= startCutoffLocal {
return true
}
return false
Expand Down Expand Up @@ -508,7 +537,9 @@ enum PiSessionCostScanner {
fallback: currentModelContext)
guard let identity else { return }
guard let date = self.timestampDate(entry: object, message: message) else { return }
let dayKey = CostUsageScanner.CostUsageDayRange.dayKey(from: date)
let dayKey = CostUsageScanner.CostUsageDayRange.dayKey(
from: date,
calendar: range.calendar)
let usage = self.extractUsage(
provider: identity.provider,
modelName: identity.modelName,
Expand Down Expand Up @@ -1039,21 +1070,23 @@ extension PiSessionCostScanner {
?? self.isoFormatterBox.plain.date(from: text)
}

private static func localMidnight(_ date: Date) -> Date {
let components = Calendar.current.dateComponents([.year, .month, .day], from: date)
return Calendar.current.date(from: components) ?? date
private static func localMidnight(_ date: Date, calendar: Calendar) -> Date {
let calendar = CostUsageScanner.CostUsageDayRange.localGregorianCalendar(matching: calendar)
let components = calendar.dateComponents([.year, .month, .day], from: date)
return calendar.date(from: components) ?? date
}

private static func dateFromDayKey(_ key: String) -> Date? {
private static func dateFromDayKey(_ key: String, calendar: Calendar) -> Date? {
let parts = key.split(separator: "-")
guard parts.count == 3,
let year = Int(parts[0]),
let month = Int(parts[1]),
let day = Int(parts[2]) else { return nil }

let calendar = CostUsageScanner.CostUsageDayRange.localGregorianCalendar(matching: calendar)
var components = DateComponents()
components.calendar = Calendar.current
components.timeZone = TimeZone.current
components.calendar = calendar
components.timeZone = calendar.timeZone
components.year = year
components.month = month
components.day = day
Expand Down
Loading