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
45 changes: 38 additions & 7 deletions Sources/CodexBar/PreferencesSpendDashboardPane.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,22 @@ func spendDashboardCoverageText(covered: Int, requested: Int) -> String {
"\(L("Coverage")): \(codexBarLocalizedInteger(covered)) / \(codexBarLocalizedInteger(requested))"
}

enum SpendDashboardModelHistoryPresentation: Equatable {
case unavailable
case empty
case partial
case complete
}

func spendDashboardModelHistoryPresentation(
_ group: SpendDashboardModel.CurrencyGroup) -> SpendDashboardModelHistoryPresentation
{
if group.models.isEmpty {
return group.modelHistoryCompleteness == .incomplete ? .unavailable : .empty
}
return group.modelHistoryCompleteness == .incomplete ? .partial : .complete
}

@MainActor
struct SpendDashboardPane: View {
@Bindable var settings: SettingsStore
Expand Down Expand Up @@ -314,24 +330,39 @@ private struct SpendModelPanel: View {
SpendDashboardPanel {
VStack(alignment: .leading, spacing: 0) {
Text(L("Models")).font(.headline).padding(.bottom, 8)
if self.group.modelHistoryCompleteness == .incomplete {
let presentation = spendDashboardModelHistoryPresentation(self.group)
switch presentation {
case .unavailable:
Text(L("Model breakdown unavailable"))
.foregroundStyle(.secondary)
.padding(.vertical, 10)
} else if self.group.models.isEmpty {
case .empty:
Text(L("No model-level history"))
.foregroundStyle(.secondary)
.padding(.vertical, 10)
} else {
case .partial, .complete:
if presentation == .partial {
Label(L("Model breakdown unavailable"), systemImage: "exclamationmark.triangle")
.font(.caption)
.foregroundStyle(.secondary)
.padding(.bottom, 6)
}
ForEach(self.group.models.prefix(8)) { row in
if row.rank > 1 {
Divider()
}
HStack(spacing: 10) {
Text(spendDashboardRankText(row.rank))
.font(.caption.monospacedDigit())
.foregroundStyle(.tertiary)
.frame(width: 26, alignment: .leading)
if presentation == .complete {
Text(spendDashboardRankText(row.rank))
.font(.caption.monospacedDigit())
.foregroundStyle(.tertiary)
.frame(width: 26, alignment: .leading)
} else {
Image(systemName: "circle.dashed")
.font(.caption)
.foregroundStyle(.tertiary)
.frame(width: 26, alignment: .leading)
}
SpendProviderIcon(provider: row.provider)
VStack(alignment: .leading, spacing: 2) {
Text(row.modelName).lineLimit(1)
Expand Down
4 changes: 3 additions & 1 deletion Sources/CodexBar/ShareStatsPayload.swift
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,9 @@ enum ShareStatsBuilder {
coveredDayCount: row.coveredDayCount)
}
}
let sanitizedModels = model.groups.flatMap { group in
let sanitizedModels = model.groups.filter {
$0.modelHistoryCompleteness == .complete
}.flatMap { group in
group.models.compactMap { row -> ShareStatsModelPayload? in
let estimatedCost = self.finiteCost(row.totalCost)
guard let modelName = ShareStatsSanitizer.modelName(row.modelName),
Expand Down
14 changes: 9 additions & 5 deletions Sources/CodexBar/SpendDashboardModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -172,15 +172,19 @@ struct SpendDashboardModel: Equatable, Sendable {
Self.inputSummary(input: input, bounds: bounds, calendar: calendar)
}
let providers = Self.providerRows(summaries)
let modelSummary = Self.modelSummary(summaries: summaries)
let modelHistoryCompleteness = summaries.contains(where: { $0.totalCost == nil })
? ModelHistoryCompleteness.incomplete
: modelSummary.completeness
let completeModelSummaries = summaries.filter { summary in
guard summary.totalCost != nil else { return false }
return Self.modelSummary(summaries: [summary]).completeness == .complete
}
let modelSummary = Self.modelSummary(summaries: completeModelSummaries)
let modelHistoryCompleteness = completeModelSummaries.count == summaries.count
? ModelHistoryCompleteness.complete
: ModelHistoryCompleteness.incomplete
Comment on lines +180 to +182

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 aggregate model completeness failures

When multiple individually complete summaries for the same provider/model overflow only after their model costs are combined, modelSummary correctly returns .incomplete, but this assignment ignores that result and marks the group complete solely because every source passed the per-source filter. The resulting row has a nil cost while the UI presents it as ranked and ShareStatsBuilder treats the group as eligible for Top Models; include modelSummary.completeness in the final completeness decision.

Useful? React with 👍 / 👎.

let dailyPoints = Self.dailyPoints(summaries: summaries)
return CurrencyGroup(
currencyCode: currencyCode,
providers: providers,
models: modelHistoryCompleteness == .complete ? modelSummary.rows : [],
models: modelSummary.rows,
dailyPoints: dailyPoints,
totalTokens: Self.completeIntSum(providers.map(\.totalTokens)),
totalCost: Self.completeCostSum(providers.map(\.totalCost)),
Expand Down
36 changes: 36 additions & 0 deletions Tests/CodexBarTests/ShareStatsTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,42 @@ struct ShareStatsTests {
#expect(!ShareStatsFormatting.text(payload).lowercased().contains("inf"))
}

@Test
func `partial model history does not enter shared rankings`() throws {
let group = SpendDashboardModel.CurrencyGroup(
currencyCode: "USD",
providers: [
SpendDashboardModel.ProviderRow(
id: "codex",
rank: 1,
provider: .codex,
displayName: "Codex",
totalTokens: 10,
totalCost: 2,
coveredDayCount: 7),
],
models: [
SpendDashboardModel.ModelRow(
rank: 1,
provider: .codex,
providerName: "Codex",
modelName: "gpt-5.4",
totalTokens: 10,
totalCost: 2),
],
dailyPoints: [],
totalTokens: nil,
totalCost: nil,
coveredDayCount: 7,
chartDomain: Self.date...Self.date,
modelHistoryCompleteness: .incomplete)
let payload = try #require(ShareStatsBuilder.make(
model: SpendDashboardModel(requestedDays: 7, groups: [group])))

#expect(payload.providers.count == 1)
#expect(payload.topModels.isEmpty)
}

@Test @MainActor
func `renderer creates social card PNG`() throws {
let payload = try #require(ShareStatsBuilder.make(model: Self.dashboard))
Expand Down
9 changes: 6 additions & 3 deletions Tests/CodexBarTests/SpendDashboardDateTruthTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,8 @@ struct SpendDashboardDateTruthTests {
#expect(cad.totalCost == nil)
#expect(cad.totalTokens == 30)
#expect(cad.modelHistoryCompleteness == .incomplete)
#expect(cad.models.isEmpty)
#expect(cad.models.map(\.provider) == [.mistral])
#expect(cad.models.map(\.totalCost) == [5])
#expect(cad.dailyPoints.map(\.sourceID) == ["healthy-cad"])
#expect(SpendDailyChartPresentation(
dailyPoints: cad.dailyPoints,
Expand Down Expand Up @@ -428,7 +429,8 @@ struct SpendDashboardDateTruthTests {
#expect(usd.totalCost == nil)
#expect(usd.totalTokens == nil)
#expect(usd.modelHistoryCompleteness == .incomplete)
#expect(usd.models.isEmpty)
#expect(usd.models.map(\.provider) == [.codex])
#expect(usd.models.map(\.totalCost) == [4])
#expect(usd.dailyPoints.map(\.sourceID) == ["healthy-usd"])
#expect(usd.dailyPoints.map(\.cost) == [4])
#expect(eur.totalCost == 5)
Expand Down Expand Up @@ -602,7 +604,8 @@ struct SpendDashboardDateTruthTests {

#expect(usd.totalCost == nil)
#expect(usd.modelHistoryCompleteness == .incomplete)
#expect(usd.models.isEmpty)
#expect(usd.models.map(\.provider) == [.codex])
#expect(usd.models.map(\.totalCost) == [4])
#expect(usd.dailyPoints.map(\.sourceID) == ["healthy-usd"])
#expect(usd.dailyPoints.map(\.cost) == [4])
#expect(eur.totalCost == 5)
Expand Down
8 changes: 6 additions & 2 deletions Tests/CodexBarTests/SpendDashboardModelTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ struct SpendDashboardModelTests {
}

@Test
func `uncovered same currency source hides partial model ranking`() throws {
func `uncovered same currency source keeps complete model rows without ranking them as complete`() throws {
let covered = Self.input(id: "covered", provider: .claude, currency: "USD", cost: 4)
let uncovered = SpendDashboardModel.ProviderInput(
id: "uncovered",
Expand All @@ -263,7 +263,10 @@ struct SpendDashboardModelTests {
#expect(group.totalCost == nil)
#expect(group.totalTokens == nil)
#expect(group.modelHistoryCompleteness == .incomplete)
#expect(group.models.isEmpty)
#expect(group.models.map(\.provider) == [.claude])
#expect(group.models.map(\.modelName) == ["test-model"])
#expect(group.models.map(\.totalCost) == [4])
#expect(spendDashboardModelHistoryPresentation(group) == .partial)
}

@Test
Expand All @@ -287,6 +290,7 @@ struct SpendDashboardModelTests {
#expect(group.totalTokens == nil)
#expect(group.modelHistoryCompleteness == .incomplete)
#expect(group.models.isEmpty)
#expect(spendDashboardModelHistoryPresentation(group) == .unavailable)
}

@Test
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.