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
13 changes: 13 additions & 0 deletions Sources/CodexBar/SpendDashboardModel+ModelBreakdown.swift
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,19 @@ extension SpendDashboardModel {
}
}

/// Provider-specific by design: Cursor usage events can omit totalCents without voiding priced rows.
static func hasExplicitlyUnpriceableLedgerCost(
_ provider: UsageProvider,
_ entry: CostUsageDailyReport.Entry) -> Bool
{
switch provider {
case .codex, .cursor:
self.hasExplicitlyUnpriceableCodexCost(entry)
default:
false
}
}

private static func hasCompleteModelCostCoverage(_ entry: CostUsageDailyReport.Entry) -> Bool {
var totalCost = 0.0
var sawNamedBreakdown = false
Expand Down
5 changes: 2 additions & 3 deletions Sources/CodexBar/SpendDashboardModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -539,10 +539,9 @@ struct SpendDashboardModel: Equatable, Sendable {
}
guard coverage.contains(day) else { continue }
guard let cost = validCost(entry.costUSD) else {
// Provider-specific by design: only the Codex ledger carries explicit unpriceable model/day evidence.
// Provider-specific by design: Codex and Cursor can omit prices on some model/day rows.
guard input.snapshot.historyCoverageIsEstablished,
input.provider == .codex,
Self.hasExplicitlyUnpriceableCodexCost(entry)
Self.hasExplicitlyUnpriceableLedgerCost(input.provider, entry)
else { return false }
continue
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -500,15 +500,19 @@ struct CursorUsageEventsFetcher: Sendable {
var outputTokens: Int? = 0
var cacheReadTokens: Int? = 0
var cacheCreationTokens: Int? = 0
var costUSD: Double? = 0
var costUSD: Double?
var costInvalid = false
var requestCount: Int? = 0

mutating func add(_ usage: CursorEventTokenUsage) {
self.inputTokens = Self.checkedSum(self.inputTokens, usage.inputTokens)
self.outputTokens = Self.checkedSum(self.outputTokens, usage.outputTokens)
self.cacheReadTokens = Self.checkedSum(self.cacheReadTokens, usage.cacheReadTokens)
self.cacheCreationTokens = Self.checkedSum(self.cacheCreationTokens, usage.cacheWriteTokens)
self.costUSD = Self.checkedCostSum(self.costUSD, usage.totalCents)
self.costUSD = Self.checkedKnownCostSum(
self.costUSD,
usage.totalCents,
alreadyInvalid: &self.costInvalid)
self.requestCount = Self.checkedSum(self.requestCount, 1)
}

Expand All @@ -534,10 +538,25 @@ struct CursorUsageEventsFetcher: Sendable {
}
}

static func checkedCostSum(_ lhsUSD: Double?, _ rhsCents: Double?) -> Double? {
guard let lhsUSD, let rhsCents, rhsCents >= 0 else { return nil }
let sum = lhsUSD + rhsCents / 100.0
return sum.isFinite ? sum : nil
static func checkedKnownCostSum(
_ lhsUSD: Double?,
_ rhsCents: Double?,
alreadyInvalid: inout Bool) -> Double?
{
if alreadyInvalid {
return nil
}
guard let rhsCents else { return lhsUSD }
guard rhsCents >= 0, rhsCents.isFinite else {
alreadyInvalid = true
return nil
}
let sum = (lhsUSD ?? 0) + rhsCents / 100.0
guard sum.isFinite else {
alreadyInvalid = true
return nil
}
return sum
}
}

Expand All @@ -547,7 +566,7 @@ struct CursorUsageEventsFetcher: Sendable {
var cacheReadTokens: Int? = 0
var cacheCreationTokens: Int? = 0
var requestCount: Int? = 0
var costUSD: Double? = 0
var costUSD: Double?
var breakdowns: [CostUsageDailyReport.ModelBreakdown] = []

for (model, accumulator) in models {
Expand All @@ -556,7 +575,7 @@ struct CursorUsageEventsFetcher: Sendable {
cacheReadTokens = ModelAccumulator.checkedSum([cacheReadTokens, accumulator.cacheReadTokens])
cacheCreationTokens = ModelAccumulator.checkedSum([cacheCreationTokens, accumulator.cacheCreationTokens])
requestCount = ModelAccumulator.checkedSum([requestCount, accumulator.requestCount])
costUSD = Self.checkedUSDTotal(costUSD, accumulator.costUSD)
costUSD = Self.checkedKnownUSDTotal(costUSD, accumulator.costUSD)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve invalidity when combining Cursor model costs

When one model on a day contains a negative/nonfinite totalCents and another model has a valid price, costInvalid makes the first accumulator's cost nil, but this reducer treats that nil exactly like an intentionally unpriced model and publishes the valid model's subtotal. The dashboard can then accept the malformed model as an allowed omitted-price row and present an apparently valid lower bound. Fresh evidence beyond the prior same-model issue is that later events now remain invalid, but the cross-model reducer still discards that invalid state; propagate costInvalid through day aggregation rather than passing only costUSD.

Useful? React with 👍 / 👎.

breakdowns.append(CostUsageDailyReport.ModelBreakdown(
modelName: model,
costUSD: accumulator.costUSD,
Expand Down Expand Up @@ -588,14 +607,14 @@ struct CursorUsageEventsFetcher: Sendable {
var totalCacheRead: Int? = 0
var totalCacheCreation: Int? = 0
var totalTokens: Int? = 0
var totalCost: Double? = 0
var totalCost: Double? = entries.isEmpty ? 0 : nil
for entry in entries {
totalInput = ModelAccumulator.checkedSum([totalInput, entry.inputTokens])
totalOutput = ModelAccumulator.checkedSum([totalOutput, entry.outputTokens])
totalCacheRead = ModelAccumulator.checkedSum([totalCacheRead, entry.cacheReadTokens])
totalCacheCreation = ModelAccumulator.checkedSum([totalCacheCreation, entry.cacheCreationTokens])
totalTokens = ModelAccumulator.checkedSum([totalTokens, entry.totalTokens])
totalCost = Self.checkedUSDTotal(totalCost, entry.costUSD)
totalCost = Self.checkedKnownUSDTotal(totalCost, entry.costUSD)
}
return CostUsageDailyReport.Summary(
totalInputTokens: totalInput,
Expand All @@ -606,10 +625,18 @@ struct CursorUsageEventsFetcher: Sendable {
totalCostUSD: totalCost)
}

private static func checkedUSDTotal(_ lhs: Double?, _ rhs: Double?) -> Double? {
guard let lhs, let rhs else { return nil }
let sum = lhs + rhs
return sum.isFinite ? sum : nil
private static func checkedKnownUSDTotal(_ lhs: Double?, _ rhs: Double?) -> Double? {
switch (lhs, rhs) {
case let (left?, right?):
let sum = left + right
return sum.isFinite ? sum : nil
case let (left?, nil):
return left
case let (nil, right?):
return right
case (nil, nil):
return nil
}
}

private static func sortedBreakdowns(
Expand Down
82 changes: 82 additions & 0 deletions Tests/CodexBarTests/CursorUsageEventsFetcherTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,88 @@ struct CursorUsageEventsFetcherTests {
#expect(report.summary?.totalCostUSD == nil)
}

@Test
func `reports keep priced cents when a sibling event omits total cents`() {
let events = [
Self.event(
timestampMS: 1_700_000_000_000,
model: "claude-4.5-sonnet",
input: 5,
totalCents: 100),
Self.event(
timestampMS: 1_700_000_001_000,
model: "gpt-5",
input: 7,
totalCents: nil),
]

let report = CursorUsageEventsFetcher.makeDailyReport(from: events, calendar: Self.utcCalendar)
let priced = report.data[0].modelBreakdowns?.first { $0.modelName == "claude-4.5-sonnet" }
let unpriced = report.data[0].modelBreakdowns?.first { $0.modelName == "gpt-5" }

#expect(report.data.count == 1)
#expect(Self.approxEqual(report.data[0].costUSD, 1.0))
#expect(Self.approxEqual(priced?.costUSD, 1.0))
#expect(unpriced?.costUSD == nil)
#expect(unpriced?.totalTokens == 7)
#expect(Self.approxEqual(report.summary?.totalCostUSD, 1.0))
}

@Test
func `reports do not revive a model cost after an invalid cents event`() {
let events = [
Self.event(
timestampMS: 1_700_000_000_000,
model: "gpt-5",
input: 5,
totalCents: 100),
Self.event(
timestampMS: 1_700_000_001_000,
model: "gpt-5",
input: 7,
totalCents: -1),
Self.event(
timestampMS: 1_700_000_002_000,
model: "gpt-5",
input: 3,
totalCents: 50),
]

let report = CursorUsageEventsFetcher.makeDailyReport(from: events, calendar: Self.utcCalendar)

#expect(report.data.count == 1)
#expect(report.data[0].costUSD == nil)
#expect(report.data[0].modelBreakdowns?.first?.costUSD == nil)
#expect(report.data[0].modelBreakdowns?.first?.totalTokens == 15)
#expect(report.summary?.totalCostUSD == nil)
}

@Test
func `reports keep priced days when another day omits total cents`() {
let events = [
Self.event(
timestampMS: 1_700_000_000_000,
model: "claude-4.5-sonnet",
input: 5,
totalCents: 100),
Self.event(
timestampMS: 1_700_172_800_000,
model: "gpt-5",
input: 7,
totalCents: nil),
]

let report = CursorUsageEventsFetcher.makeDailyReport(from: events, calendar: Self.utcCalendar)
let priced = report.data.first { $0.costUSD != nil }
let unpriced = report.data.first { $0.costUSD == nil }

#expect(report.data.count == 2)
#expect(Self.approxEqual(priced?.costUSD, 1.0))
#expect(unpriced?.costUSD == nil)
#expect(unpriced?.totalTokens == 7)
#expect(Self.approxEqual(report.summary?.totalCostUSD, 1.0))
}

@Test
func `reports preserve unknown aggregate tokens on cross event overflow`() {
let events = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2362,7 +2362,7 @@ struct ProviderArchitectureGatekeeperTests {
reason: "This exact shared construct dispatches a provider-owned capability at the generic integration boundary."),
AllowedProviderConstruct(
path: "Sources/CodexBar/SpendDashboardModel.swift",
line: 810,
line: 809,
anchor: "guard provider == .mistral else { return displayCalendar }",
expectedProviderIDs: ["mistral"],
expectedReferenceCount: 1,
Expand Down
57 changes: 57 additions & 0 deletions Tests/CodexBarTests/SpendDashboardPartialCostTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,63 @@ struct SpendDashboardPartialCostTests {
#expect(group.dailyPoints.map(\.cost) == [3])
}

@Test
func `established Cursor history keeps priced days when another day omits cost`() throws {
let snapshot = Self.snapshot(
entries: [
Self.entry(day: "2026-07-15", cost: 1, tokens: 5, model: "claude-4.5-sonnet"),
Self.entry(day: "2026-07-16", cost: nil, tokens: 7, model: "gpt-5"),
],
last30DaysTokens: 12,
last30DaysCostUSD: 1)
let group = try Self.group(inputs: [
.init(provider: .cursor, displayName: "Cursor", snapshot: snapshot),
])

#expect(snapshot.historyCoverageIsEstablished)
#expect(group.totalCost == 1)
#expect(group.totalTokens == 12)
#expect(group.dailyPoints.map(\.cost) == [1])
}

@Test
func `unestablished Cursor history with an unresolved day keeps spend unavailable`() throws {
let snapshot = Self.snapshot(
entries: [
Self.entry(day: "2026-07-15", cost: 1, tokens: 5, model: "claude-4.5-sonnet"),
Self.entry(day: "2026-07-16", cost: nil, tokens: 7, model: "gpt-5"),
],
historyCoverageIsEstablished: false,
last30DaysTokens: 12,
last30DaysCostUSD: 1)
let group = try Self.group(inputs: [
.init(provider: .cursor, displayName: "Cursor", snapshot: snapshot),
])

#expect(!snapshot.historyCoverageIsEstablished)
#expect(group.totalCost == nil)
#expect(group.dailyPoints.isEmpty)
}

@Test
func `established Cursor history retains priced days when some events omit total cents`() throws {
let snapshot = Self.snapshot(
entries: [
Self.entry(day: "2026-07-15", cost: 3, tokens: 30, model: "claude-4.5-sonnet"),
Self.entry(day: "2026-07-16", cost: nil, tokens: 40, model: "gpt-5"),
],
last30DaysTokens: 70,
last30DaysCostUSD: 3)
let group = try Self.group(inputs: [
.init(provider: .cursor, displayName: "Cursor", snapshot: snapshot),
])

#expect(group.totalCost == 3)
#expect(group.totalTokens == 70)
#expect(group.dailyPoints.map(\.cost) == [3])
#expect(group.modelHistoryCompleteness == .incomplete)
}

@Test
func `incomplete Codex history with an unresolved day keeps spend unavailable`() throws {
let snapshot = Self.snapshot(
Expand Down