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
32 changes: 21 additions & 11 deletions Sources/CodexBar/InlineUsageDashboardContent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ extension UsageMenuCardView.Model {
if input.provider == .poe,
let usage = input.snapshot?.poeUsage
{
return self.poeUsageNotes(usage)
return self.poeUsageNotes(usage, now: input.now)
}

if input.provider == .ollama,
Expand All @@ -89,7 +89,7 @@ extension UsageMenuCardView.Model {
}

static func openAIAPIUsageNotes(_ usage: OpenAIAPIUsageSnapshot) -> [String] {
let today = usage.latestDay
let today = usage.currentDay
let seven = usage.last7Days
let thirty = usage.last30Days
let historyLabel = usage.historyWindowLabel
Expand All @@ -113,8 +113,12 @@ extension UsageMenuCardView.Model {
return notes
}

static func poeUsageNotes(_ usage: PoeUsageHistorySnapshot) -> [String] {
let today = usage.latestDay
static func poeUsageNotes(
_ usage: PoeUsageHistorySnapshot,
now: Date = Date(),
calendar: Calendar = .current) -> [String]
{
let today = usage.currentDay(now: now, calendar: calendar)
let week = usage.last7Days
let month = usage.last30Days
let todayUSD = today.costUSD.map { " · \(UsageFormatter.usdString($0))" } ?? ""
Expand Down Expand Up @@ -196,7 +200,7 @@ extension UsageMenuCardView.Model {
let usage = input.snapshot?.poeUsage,
!usage.daily.isEmpty
{
return Self.poeInlineDashboard(usage)
return Self.poeInlineDashboard(usage, now: input.now)
}
if [.codex, .claude, .vertexai, .bedrock].contains(input.provider),
input.tokenCostUsageEnabled,
Expand Down Expand Up @@ -229,8 +233,12 @@ extension UsageMenuCardView.Model {
}
}

static func poeInlineDashboard(_ usage: PoeUsageHistorySnapshot) -> InlineUsageDashboardModel {
let today = usage.latestDay
static func poeInlineDashboard(
_ usage: PoeUsageHistorySnapshot,
now: Date = Date(),
calendar: Calendar = .current) -> InlineUsageDashboardModel
{
let today = usage.currentDay(now: now, calendar: calendar)
let week = usage.last7Days
let month = usage.last30Days
let points = usage.daily.suffix(30).map {
Expand Down Expand Up @@ -318,7 +326,9 @@ extension UsageMenuCardView.Model {
value: cost,
accessibilityValue: "\(entry.date): \(Self.costString(cost, currencyCode: snapshot.currencyCode))")
}
let latest = snapshot.daily.max { lhs, rhs in lhs.date < rhs.date }
let latest = CostUsageTokenSnapshot.latestEntry(in: snapshot.daily)
let usesLatestPrimary = provider == .bedrock || provider == .mistral
let primaryCostUSD = usesLatestPrimary ? latest?.costUSD : snapshot.sessionCostUSD
var details: [String] = []
if let topModel = Self.topCostModel(from: snapshot.daily) {
details.append("\(L("Top model")): \(Self.shortModelName(topModel))")
Expand All @@ -337,8 +347,8 @@ extension UsageMenuCardView.Model {
valueStyle: Self.costValueStyle(currencyCode: snapshot.currencyCode),
kpis: [
.init(
title: provider == .bedrock || provider == .mistral ? L("Latest") : L("Today"),
value: latest?.costUSD.map { Self.costString($0, currencyCode: snapshot.currencyCode) } ?? "—",
title: usesLatestPrimary ? L("Latest") : L("Today"),
value: primaryCostUSD.map { Self.costString($0, currencyCode: snapshot.currencyCode) } ?? "—",
emphasis: true),
.init(
title: historyTitle,
Expand Down Expand Up @@ -378,7 +388,7 @@ extension UsageMenuCardView.Model {
fileprivate static func claudeAdminAPIInlineDashboard(_ usage: ClaudeAdminAPIUsageSnapshot)
-> InlineUsageDashboardModel
{
let today = usage.latestDay
let today = usage.currentDay
let last7 = usage.last7Days
let last30 = usage.last30Days
let points = usage.daily.suffix(30).map {
Expand Down
6 changes: 3 additions & 3 deletions Sources/CodexBar/MenuDescriptor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,7 @@ struct MenuDescriptor {
entries: inout [Entry],
usage: OpenAIAPIUsageSnapshot)
{
let today = usage.latestDay
let today = usage.currentDay
let last7 = usage.last7Days
let last30 = usage.last30Days
let historyLabel = usage.historyWindowLabel
Expand All @@ -319,7 +319,7 @@ struct MenuDescriptor {
entries: inout [Entry],
usage: ClaudeAdminAPIUsageSnapshot)
{
let today = usage.latestDay
let today = usage.currentDay
let last7 = usage.last7Days
let last30 = usage.last30Days

Expand Down Expand Up @@ -393,7 +393,7 @@ struct MenuDescriptor {
entries: inout [Entry],
usage: PoeUsageHistorySnapshot)
{
let today = usage.latestDay
let today = usage.currentDay()
let week = usage.last7Days
let month = usage.last30Days
let todayCostSuffix = today.costUSD.map { " · \(UsageFormatter.usdString($0))" } ?? ""
Expand Down
44 changes: 26 additions & 18 deletions Sources/CodexBarCore/CostUsageFetcher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,11 @@ public struct CostUsageFetcher: Sendable {
environment: environment,
since: since,
until: until)
return Self.tokenSnapshot(from: daily, now: now, historyDays: clampedHistoryDays)
return Self.tokenSnapshot(
from: daily,
now: now,
historyDays: clampedHistoryDays,
useCurrentLocalDayForSession: false)
}

var options = overrideScannerOptions ?? CostUsageScanner.Options()
Expand Down Expand Up @@ -276,23 +280,27 @@ public struct CostUsageFetcher: Sendable {
static func tokenSnapshot(
from daily: CostUsageDailyReport,
now: Date,
historyDays: Int = 30) -> CostUsageTokenSnapshot
historyDays: Int = 30,
useCurrentLocalDayForSession: Bool = true) -> CostUsageTokenSnapshot
{
// Pick the most recent day; break ties by cost/tokens to keep a stable "session" row.
let currentDay = daily.data.compactMap { entry -> (entry: CostUsageDailyReport.Entry, date: Date)? in
guard let date = CostUsageDateParser.parse(entry.date) else { return nil }
return (entry, date)
let sessionEntry = useCurrentLocalDayForSession
? CostUsageTokenSnapshot.entry(in: daily.data, forLocalDayContaining: now)
: CostUsageTokenSnapshot.latestEntry(in: daily.data)
let hasHistoricalRows = !daily.data.isEmpty
let sessionTokens: Int? = if let sessionEntry {
sessionEntry.totalTokens
} else if hasHistoricalRows {
0
} else {
nil
}
let sessionCostUSD: Double? = if let sessionEntry {
sessionEntry.costUSD
} else if hasHistoricalRows {
0
} else {
nil
}
.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
// Prefer summary totals when present; fall back to summing daily entries.
let totalFromSummary = daily.summary?.totalCostUSD
let totalFromEntries = daily.data.compactMap(\.costUSD).reduce(0, +)
Expand All @@ -302,8 +310,8 @@ public struct CostUsageFetcher: Sendable {
let last30DaysTokens = totalTokensFromSummary ?? (totalTokensFromEntries > 0 ? totalTokensFromEntries : nil)

return CostUsageTokenSnapshot(
sessionTokens: currentDay?.totalTokens,
sessionCostUSD: currentDay?.costUSD,
sessionTokens: sessionTokens,
sessionCostUSD: sessionCostUSD,
last30DaysTokens: last30DaysTokens,
last30DaysCostUSD: last30DaysCostUSD,
historyDays: historyDays,
Expand Down
57 changes: 57 additions & 0 deletions Sources/CodexBarCore/CostUsageModels.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,41 @@ public struct CostUsageTokenSnapshot: Sendable, Equatable {
self.daily = daily
self.updatedAt = updatedAt
}

public func currentDayEntry(calendar: Calendar = .current) -> CostUsageDailyReport.Entry? {
Self.entry(in: self.daily, forLocalDayContaining: self.updatedAt, 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
}
}
}

public struct CostUsageDailyReport: Sendable, Decodable {
Expand Down Expand Up @@ -806,7 +841,29 @@ enum CostUsageDateParser {
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.timeZone = timeZone
formatter.dateFormat = format
formatter.isLenient = false
threadDict[cacheKey] = formatter
return formatter
}
}

enum CostUsageBucketInterval {
static func contains(
_ date: Date,
startTime: Date,
endTime: Date) -> Bool
{
guard startTime < endTime else { return false }
return startTime <= date && date < endTime
}
}

enum CostUsageLocalDay {
static func key(from date: Date, calendar: Calendar = .current) -> String {
let components = calendar.dateComponents([.year, .month, .day], from: date)
let year = components.year ?? 0
let month = components.month ?? 0
let day = components.day ?? 0
return String(format: "%04d-%02d-%02d", year, month, day)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -129,10 +129,30 @@ public struct ClaudeAdminAPIUsageSnapshot: Codable, Equatable, Sendable {
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 },
inputTokens: selected.reduce(0) { $0 + $1.inputTokens },
cacheCreationInputTokens: selected.reduce(0) { $0 + $1.cacheCreationInputTokens },
cacheReadInputTokens: selected.reduce(0) { $0 + $1.cacheReadInputTokens },
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(
Expand Down
30 changes: 25 additions & 5 deletions Sources/CodexBarCore/Providers/OpenAI/OpenAIAPIUsageSnapshot.swift
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ public struct OpenAIAPIUsageSnapshot: Codable, Equatable, Sendable {
}

public var last30Days: Summary {
self.summary(days: self.historyDays)
self.historyDays == 1 ? self.currentDay : self.summary(days: self.historyDays)
}

public var historyWindowLabel: String {
Expand All @@ -141,10 +141,30 @@ public struct OpenAIAPIUsageSnapshot: Codable, Equatable, Sendable {
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(
Expand Down Expand Up @@ -238,12 +258,12 @@ public struct OpenAIAPIUsageSnapshot: Codable, Equatable, Sendable {
modelsUsed: modelsUsed.isEmpty ? nil : modelsUsed,
modelBreakdowns: modelBreakdowns.isEmpty ? nil : modelBreakdowns)
}
let latest = self.latestDay
let today = self.currentDay
let total = self.last30Days
return CostUsageTokenSnapshot(
sessionTokens: latest.totalTokens,
sessionCostUSD: latest.costUSD,
sessionRequests: latest.requests,
sessionTokens: today.totalTokens,
sessionCostUSD: today.costUSD,
sessionRequests: today.requests,
last30DaysTokens: total.totalTokens,
last30DaysCostUSD: total.costUSD,
last30DaysRequests: total.requests,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,14 @@ public struct PoeUsageHistorySnapshot: Codable, Equatable, Sendable {
self.summary(days: 1)
}

public func currentDay(now: Date = Date(), calendar: Calendar = .current) -> Summary {
let selected = self.entries.filter { calendar.isDate($0.createdAt, inSameDayAs: now) }
let points = selected.reduce(0) { $0 + max(0, $1.points) }
let costValues = selected.compactMap(\.costUSD).map { max(0, $0) }
let cost: Double? = costValues.isEmpty ? nil : costValues.reduce(0, +)
return Summary(points: points, requests: selected.count, costUSD: cost)
}

public var last7Days: Summary {
self.summary(days: 7)
}
Expand Down
Loading
Loading