Skip to content
Closed
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
### Fixed
- CLI: stop standalone version lookup from walking past the filesystem root and hanging with unbounded memory on affected macOS versions (#2856). Thanks @Manwholikespie!
- Cost store: prevent launch-time executor-assumption crashes on macOS 15 by keeping synchronous SQLite cache bridges on their validated serial queue (#2857). Thanks @Manwholikespie!
- Codex: preserve request-level long-context pricing when rescans rebuild fork-deduplicated cost rows, preventing combined daily usage from activating the >272K tier (#2858). Thanks @thomaschow19!

## 0.49.2 — 2026-08-10

Expand Down
75 changes: 64 additions & 11 deletions Sources/CodexBarCLI/CLICostCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -144,14 +144,20 @@ extension CodexBarCLI {
return Self.renderProjectCostText(header: header, snapshot: snapshot)
}

let todayCost = snapshot.sessionCostUSD
.map { UsageFormatter.currencyString($0, currencyCode: snapshot.currencyCode) } ?? "—"
let todayTokens = snapshot.sessionTokens.map { UsageFormatter.tokenCountString($0) }
let todayCost = Self.renderCostValue(
snapshot.sessionCostUSD,
tokens: snapshot.sessionTokens,
currencyCode: snapshot.currencyCode,
describeIncomplete: provider == .codex)
let todayLine = todayTokens.map { "Today: \(todayCost) · \($0) tokens" } ?? "Today: \(todayCost)"

let monthCost = snapshot.last30DaysCostUSD
.map { UsageFormatter.currencyString($0, currencyCode: snapshot.currencyCode) } ?? "—"
let monthTokens = snapshot.last30DaysTokens.map { UsageFormatter.tokenCountString($0) }
let monthCost = Self.renderCostValue(
snapshot.last30DaysCostUSD,
tokens: snapshot.last30DaysTokens,
currencyCode: snapshot.currencyCode,
describeIncomplete: provider == .codex)
let historyLabel = snapshot.historyLabel
?? (snapshot.historyDays == 1 ? "Today" : "Last \(snapshot.historyDays) days")
let monthLine = monthTokens.map {
Expand Down Expand Up @@ -181,17 +187,23 @@ extension CodexBarCLI {
return lines.joined(separator: "\n")
}
for project in snapshot.projects {
let cost = project.totalCostUSD
.map { UsageFormatter.currencyString($0, currencyCode: snapshot.currencyCode) } ?? "—"
let cost = Self.renderCostValue(
project.totalCostUSD,
tokens: project.totalTokens,
currencyCode: snapshot.currencyCode,
describeIncomplete: true)
let tokens = project.totalTokens.map { UsageFormatter.tokenCountString($0) }
let summary = tokens.map { "\(cost) · \($0) tokens" } ?? cost
lines.append("\(project.name): \(summary)")
if let path = project.path {
lines.append(" \(path)")
}
for source in project.sources {
let sourceCost = source.totalCostUSD
.map { UsageFormatter.currencyString($0, currencyCode: snapshot.currencyCode) } ?? "—"
let sourceCost = Self.renderCostValue(
source.totalCostUSD,
tokens: source.totalTokens,
currencyCode: snapshot.currencyCode,
describeIncomplete: true)
let sourceTokens = source.totalTokens.map { UsageFormatter.tokenCountString($0) }
let sourceSummary = sourceTokens.map { "\(sourceCost) · \($0) tokens" } ?? sourceCost
lines.append(" - \(source.name): \(sourceSummary)")
Expand All @@ -215,6 +227,21 @@ extension CodexBarCLI {
return "\u{001B}[1;36m\(header)\u{001B}[0m"
}

private static func renderCostValue(
_ costUSD: Double?,
tokens: Int?,
currencyCode: String,
describeIncomplete: Bool) -> String
{
if let costUSD {
return UsageFormatter.currencyString(costUSD, currencyCode: currencyCode)
}
if describeIncomplete, (tokens ?? 0) > 0 {
return "Unavailable (incomplete pricing data)"
}
return "—"
}

static func costProviders(from selection: ProviderSelection) -> [UsageProvider] {
selection.asList.filter { Self.costSupportedProviders.contains($0) }
}
Expand Down Expand Up @@ -261,7 +288,10 @@ extension CodexBarCLI {
meteredCostUSD: snapshot?.meteredCostUSD,
daily: daily,
projects: projects,
totals: snapshot.flatMap(Self.costTotals(from:)),
// Provider-specific by design: Codex omits totals when request-tier pricing is incomplete.
totals: snapshot.flatMap {
Self.costTotals(from: $0, requireCompleteCosts: provider == .codex)
},
error: error.map { Self.makeErrorPayload($0) })
}

Expand All @@ -287,7 +317,10 @@ extension CodexBarCLI {
totalTokens: breakdown.totalTokens)
}

private static func costTotals(from snapshot: CostUsageTokenSnapshot) -> CostTotalsPayload? {
private static func costTotals(
from snapshot: CostUsageTokenSnapshot,
requireCompleteCosts: Bool) -> CostTotalsPayload?
{
let entries = snapshot.daily
guard !entries.isEmpty else {
guard snapshot.last30DaysTokens != nil || snapshot.last30DaysCostUSD != nil else { return nil }
Expand All @@ -312,6 +345,7 @@ extension CodexBarCLI {
var sawCacheCreation = false
var sawTokens = false
var sawCost = false
var hasUnpricedUsage = false

for entry in entries {
if let input = entry.inputTokens {
Expand All @@ -337,17 +371,36 @@ extension CodexBarCLI {
if let cost = entry.costUSD {
totalCost += cost
sawCost = true
} else if requireCompleteCosts, Self.hasCostBearingUsage(entry) {
hasUnpricedUsage = true
}
}

let completeCostUSD: Double? = if requireCompleteCosts, hasUnpricedUsage {
nil
} else if sawCost {
totalCost
} else {
snapshot.last30DaysCostUSD
}

// Prefer totals derived from daily rows; fall back to snapshot aggregates when rows omit fields.
return CostTotalsPayload(
totalInputTokens: sawInput ? totalInput : nil,
totalOutputTokens: sawOutput ? totalOutput : nil,
cacheReadTokens: sawCacheRead ? totalCacheRead : nil,
cacheCreationTokens: sawCacheCreation ? totalCacheCreation : nil,
totalTokens: sawTokens ? totalTokens : snapshot.last30DaysTokens,
totalCostUSD: sawCost ? totalCost : snapshot.last30DaysCostUSD)
totalCostUSD: completeCostUSD)
}

private static func hasCostBearingUsage(_ entry: CostUsageDailyReport.Entry) -> Bool {
max(
entry.totalTokens ?? 0,
(entry.inputTokens ?? 0)
+ (entry.cacheReadTokens ?? 0)
+ (entry.cacheCreationTokens ?? 0)
+ (entry.outputTokens ?? 0)) > 0
}

private static func decodeCostHistoryDays(from values: ParsedValues) -> Int {
Expand Down
41 changes: 33 additions & 8 deletions Sources/CodexBarCore/CostUsageFetcher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -467,12 +467,14 @@ public struct CostUsageFetcher: Sendable {
retryUnknownPricing: false)
}

// Provider-specific by design: Codex must propagate unknown request-tier costs through every aggregate.
return Self.tokenSnapshot(
from: scanResult.daily,
now: now,
historyDays: clampedHistoryDays,
calendar: scanOptions.calendar,
historyCoverageIsEstablished: scanResult.historyCoverageIsEstablished,
requireCompleteCosts: provider == .codex,
projects: scanResult.projects,
sessions: scanResult.sessions,
updatedAt: scanResult.staleSnapshotUpdatedAt)
Expand Down Expand Up @@ -576,8 +578,10 @@ public struct CostUsageFetcher: Sendable {
try checkCancellation()
if provider == .codex {
piDaily = piReport
daily = CostUsageDailyReport.mergedRequiringCompleteCosts([daily, piReport])
} else {
daily = CostUsageDailyReport.merged([daily, piReport])
}
daily = CostUsageDailyReport.merged([daily, piReport])
}
if provider == .codex {
projects = Self.mergedProjectBreakdowns(
Expand Down Expand Up @@ -889,11 +893,12 @@ public struct CostUsageFetcher: Sendable {
// rescan on the strength of another source's scan.
return CachedCodexTokenSnapshotResult(
snapshot: Self.tokenSnapshot(
from: CostUsageDailyReport.merged(reports),
from: CostUsageDailyReport.mergedRequiringCompleteCosts(reports),
now: now,
historyDays: clampedHistoryDays,
calendar: options.calendar,
historyCoverageIsEstablished: Self.codexHistoryCoverageIsEstablished(options: options),
requireCompleteCosts: true,
projects: Self.mergedProjectBreakdowns(projects),
sessions: sessions,
updatedAt: scanTimes.min()),
Expand Down Expand Up @@ -1038,6 +1043,7 @@ public struct CostUsageFetcher: Sendable {
useCurrentLocalDayForSession: Bool = true,
calendar: Calendar = .current,
historyCoverageIsEstablished: Bool = true,
requireCompleteCosts: Bool = false,
meteredCostUSD: Double? = nil,
credentialScopeFingerprint: String? = nil,
historyLabel: String? = nil,
Expand All @@ -1063,10 +1069,16 @@ public struct CostUsageFetcher: Sendable {
} else {
nil
}
// Prefer summary totals when present; fall back to summing daily entries.
let totalFromSummary = daily.summary?.totalCostUSD
// Prefer summary totals when present; fall back to summing daily entries. Codex uses a
// strict policy because a missing request-tier estimate makes every enclosing total incomplete.
let costsAreComplete = !requireCompleteCosts || daily.data.allSatisfy { entry in
entry.costUSD != nil || !Self.hasCostBearingUsage(entry)
}
let totalFromSummary = costsAreComplete ? daily.summary?.totalCostUSD : nil
let totalFromEntries = daily.data.compactMap(\.costUSD).reduce(0, +)
let last30DaysCostUSD = totalFromSummary ?? (totalFromEntries > 0 ? totalFromEntries : nil)
let last30DaysCostUSD = costsAreComplete
? totalFromSummary ?? (totalFromEntries > 0 ? totalFromEntries : nil)
: nil
let totalTokensFromSummary = daily.summary?.totalTokens
let totalTokensFromEntries = daily.data.compactMap(\.totalTokens).reduce(0, +)
let last30DaysTokens = totalTokensFromSummary ?? (totalTokensFromEntries > 0 ? totalTokensFromEntries : nil)
Expand All @@ -1087,6 +1099,16 @@ public struct CostUsageFetcher: Sendable {
updatedAt: updatedAt ?? now)
}

private static func hasCostBearingUsage(_ entry: CostUsageDailyReport.Entry) -> Bool {
let explicitOrDerivedTotal = max(
entry.totalTokens ?? 0,
(entry.inputTokens ?? 0)
+ (entry.cacheReadTokens ?? 0)
+ (entry.cacheCreationTokens ?? 0)
+ (entry.outputTokens ?? 0))
return explicitOrDerivedTotal > 0
}

package static func resolvedCodexScanDurationPerRefresh(
provider: UsageProvider,
bypassScannerDebounce: Bool,
Expand Down Expand Up @@ -1175,7 +1197,7 @@ public struct CostUsageFetcher: Sendable {
}
}
return dailyByPath.map { key, reports in
let merged = CostUsageDailyReport.merged(reports)
let merged = CostUsageDailyReport.mergedRequiringCompleteCosts(reports)
return CostUsageProjectBreakdown(
name: namesByPath[key] ?? CostUsageProjectBreakdown.unknownProjectName,
path: key.isEmpty ? nil : key,
Expand Down Expand Up @@ -1207,7 +1229,7 @@ public struct CostUsageFetcher: Sendable {
sourceNamesByPath: [String: String]) -> [CostUsageProjectSourceBreakdown]
{
sourceDailyByPath.map { key, reports in
let merged = CostUsageDailyReport.merged(reports)
let merged = CostUsageDailyReport.mergedRequiringCompleteCosts(reports)
return CostUsageProjectSourceBreakdown(
name: sourceNamesByPath[key] ?? CostUsageProjectBreakdown.unknownProjectName,
path: key.isEmpty ? nil : key,
Expand Down Expand Up @@ -1236,6 +1258,7 @@ public struct CostUsageFetcher: Sendable {
var sawTotalTokens = false
var costUSD: Double = 0
var sawCost = false
var hasUnpricedUsage = false

mutating func add(_ breakdown: CostUsageDailyReport.ModelBreakdown) {
if let totalTokens = breakdown.totalTokens {
Expand All @@ -1245,13 +1268,15 @@ public struct CostUsageFetcher: Sendable {
if let costUSD = breakdown.costUSD {
self.costUSD += costUSD
self.sawCost = true
} else if (breakdown.totalTokens ?? 0) > 0 {
self.hasUnpricedUsage = true
}
}

func build(modelName: String) -> CostUsageDailyReport.ModelBreakdown {
CostUsageDailyReport.ModelBreakdown(
modelName: modelName,
costUSD: self.sawCost ? self.costUSD : nil,
costUSD: self.sawCost && !self.hasUnpricedUsage ? self.costUSD : nil,
totalTokens: self.sawTotalTokens ? self.totalTokens : nil)
}
}
Expand Down
Loading