Skip to content
Draft
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
8 changes: 6 additions & 2 deletions Sources/CodexBar/InlineUsageDashboardContent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -321,12 +321,16 @@ extension UsageMenuCardView.Model {
let latest = snapshot.daily.max { lhs, rhs in lhs.date < rhs.date }
var details: [String] = []
if let topModel = Self.topCostModel(from: snapshot.daily) {
details.append("\(L("Top model")): \(Self.shortModelName(topModel))")
let topLabel = provider == .codex &&
UsageMenuCardView.Model.isCodexDashboardCreditCostSnapshot(snapshot)
? "Top source"
: L("Top model")
details.append("\(topLabel): \(Self.shortModelName(topModel))")
}
if let requestCount = snapshot.last30DaysRequests {
details.append("\(requestHistoryTitle): \(UsageFormatter.tokenCountString(requestCount)) \(L("requests"))")
}
if let hint = Self.tokenUsageHint(provider: provider) {
if let hint = Self.tokenUsageHint(provider: provider, snapshot: snapshot) {
details.append(hint)
} else {
details.append(L("cost_estimate_hint"))
Expand Down
32 changes: 23 additions & 9 deletions Sources/CodexBar/MenuCardView+Costs.swift
Original file line number Diff line number Diff line change
Expand Up @@ -111,27 +111,41 @@ extension UsageMenuCardView.Model {
return TokenUsageSection(
sessionLine: sessionLine,
monthLine: monthLine,
hintLine: Self.tokenUsageHint(provider: provider),
hintLine: Self.tokenUsageHint(provider: provider, snapshot: snapshot),
errorLine: err,
errorCopyText: (error?.isEmpty ?? true) ? nil : error)
}

static func tokenUsageHint(provider: UsageProvider) -> String? {
static func tokenUsageHint(provider: UsageProvider, snapshot: CostUsageTokenSnapshot) -> String? {
switch provider {
case .codex:
L("Estimated from local Codex logs for the selected account.")
if self.isCodexDashboardCreditCostSnapshot(snapshot) {
return "Estimated from OpenAI dashboard credits for the selected account."
}
return L("Estimated from local Codex logs for the selected account.")
case .claude:
UsageFormatter.costEstimateHint(provider: provider)
return UsageFormatter.costEstimateHint(provider: provider)
case .vertexai:
L("cost_estimate_hint")
return L("cost_estimate_hint")
case .bedrock:
L("AWS Cost Explorer billing can lag.")
return L("AWS Cost Explorer billing can lag.")
case .openai:
L("Reported by OpenAI Admin API organization usage.")
return L("Reported by OpenAI Admin API organization usage.")
case .mistral:
L("Reported by Mistral billing usage.")
return L("Reported by Mistral billing usage.")
default:
nil
return nil
}
}

static func isCodexDashboardCreditCostSnapshot(_ snapshot: CostUsageTokenSnapshot) -> Bool {
let dashboardServices = Set(["exec", "desktop app", "cli", "unknown"])
return snapshot.daily.contains { entry in
(entry.modelsUsed ?? []).contains {
dashboardServices.contains($0.trimmingCharacters(in: .whitespacesAndNewlines).lowercased())
} || (entry.modelBreakdowns ?? []).contains {
dashboardServices.contains($0.modelName.trimmingCharacters(in: .whitespacesAndNewlines).lowercased())
}
}
}

Expand Down
10 changes: 8 additions & 2 deletions Sources/CodexBar/StatusItemController+Menu.swift
Original file line number Diff line number Diff line change
Expand Up @@ -461,7 +461,7 @@ extension StatusItemController {
let hasCreditsHistory = codexProjection?.hasCreditsHistory == true
let hasUsageBreakdown = codexProjection?.hasUsageBreakdown == true
let hasCostHistory = self.settings.isCostUsageEffectivelyEnabled(for: currentProvider) &&
(self.store.tokenSnapshot(for: currentProvider)?.daily.isEmpty == false)
(self.tokenSnapshotForCostHistorySubmenu(provider: currentProvider)?.daily.isEmpty == false)
let canShowBuyCredits = self.settings.showOptionalCreditsAndExtraUsage &&
codexProjection?.canShowBuyCredits == true
let hasOpenAIWebMenuItems = !showAllAccounts &&
Expand Down Expand Up @@ -1515,7 +1515,13 @@ extension StatusItemController {
if UsageStore.tokenCostRequiresProviderSnapshot(provider) {
return projected
}
return projected ?? self.store.tokenSnapshot(for: provider)
let local = projected ?? self.store.tokenSnapshot(for: provider)
if provider == .codex,
let dashboard = self.codexDashboardCostSnapshotForLiveCard(localSnapshot: local)
{
return dashboard
}
return local
}

func makeOpenAIAPIUsageSubmenu(provider: UsageProvider, width: CGFloat? = nil) -> NSMenu? {
Expand Down
26 changes: 25 additions & 1 deletion Sources/CodexBar/StatusItemController+MenuCardModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,15 @@ extension StatusItemController {
snapshotOverride: snapshotOverride,
errorOverride: errorOverride,
now: now)
let localTokenSnapshot = projectedTokenSnapshot ?? storedTokenSnapshot
let dashboardTokenSnapshot: CostUsageTokenSnapshot? = if target == .codex,
surface == .liveCard
{
self.codexDashboardCostSnapshotForLiveCard(
projection: codexProjection,
localSnapshot: localTokenSnapshot,
now: now)
} else { nil }
let credits: CreditsSnapshot?
let creditsError: String?
let dashboard: OpenAIDashboardSnapshot?
Expand All @@ -48,7 +57,7 @@ extension StatusItemController {
dashboard = nil
dashboardError = codexProjection.userFacingErrors.dashboard
if surface == .liveCard {
tokenSnapshot = projectedTokenSnapshot ?? storedTokenSnapshot
tokenSnapshot = dashboardTokenSnapshot ?? localTokenSnapshot
tokenError = self.store.tokenError(for: target)
} else {
tokenSnapshot = projectedTokenSnapshot
Expand Down Expand Up @@ -128,6 +137,21 @@ extension StatusItemController {
AccountInfo(email: account.email, plan: account.workspaceLabel)
}

func codexDashboardCostSnapshotForLiveCard(
projection: CodexConsumerProjection? = nil,
localSnapshot: CostUsageTokenSnapshot? = nil,
now: Date = Date()) -> CostUsageTokenSnapshot?
{
let projection = projection ?? self.store.codexConsumerProjectionIfNeeded(
for: .codex,
surface: .liveCard,
now: now)
guard projection?.dashboardVisibility == .attached else { return nil }
return self.store.openAIDashboard?.toCostUsageTokenSnapshot(
historyDays: self.settings.costUsageHistoryDays,
merging: localSnapshot)
}

private func quotaWarningMarkerThresholds(provider: UsageProvider, window: QuotaWarningWindow) -> [Int] {
guard self.settings.quotaWarningMarkersVisible else { return [] }
guard self.settings.quotaWarningEnabled(provider: provider, window: window) else { return [] }
Expand Down
21 changes: 17 additions & 4 deletions Sources/CodexBar/UsageBreakdownChartMenuView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -375,13 +375,16 @@ struct UsageBreakdownChartMenuView: View {
}

let dayLabel = date.formatted(.dateTime.month(.abbreviated).day())
let total = day.totalCreditsUsed.formatted(.number.precision(.fractionLength(0...2)))
let totalCredits = Self.creditsString(day.totalCreditsUsed)
let totalUSD = Self.usdString(fromCredits: day.totalCreditsUsed)
let total = "\(totalUSD) · \(totalCredits) credits"
if day.services.isEmpty {
return ("\(dayLabel): \(total)", nil)
}
if day.services.count <= 1, let first = day.services.first {
let used = first.creditsUsed.formatted(.number.precision(.fractionLength(0...2)))
return ("\(dayLabel): \(used)", first.service)
let used = "\(Self.usdString(fromCredits: first.creditsUsed)) · " +
"\(Self.creditsString(first.creditsUsed)) credits"
return ("\(dayLabel): \(total)", "\(first.service) \(used)")
}

let services = day.services
Expand All @@ -390,9 +393,19 @@ struct UsageBreakdownChartMenuView: View {
return lhs.creditsUsed > rhs.creditsUsed
}
.prefix(3)
.map { "\($0.service) \($0.creditsUsed.formatted(.number.precision(.fractionLength(0...2))))" }
.map { "\($0.service) \(Self.usdString(fromCredits: $0.creditsUsed))" }
.joined(separator: " · ")

return ("\(dayLabel): \(total)", services)
}

private static func usdString(fromCredits credits: Double) -> String {
UsageFormatter.currencyString(
OpenAIDashboardSnapshot.codexUSD(fromCredits: credits),
currencyCode: "USD")
}

private static func creditsString(_ credits: Double) -> String {
credits.formatted(.number.precision(.fractionLength(0...2)))
}
}
79 changes: 79 additions & 0 deletions Sources/CodexBarCore/OpenAIDashboardModels.swift
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,8 @@ public struct OpenAIDashboardSnapshot: Codable, Equatable, Sendable {
}

extension OpenAIDashboardSnapshot {
public static let codexCreditsPerUSD = 25.0

public func toUsageSnapshot(
provider: UsageProvider = .codex,
accountEmail: String? = nil,
Expand All @@ -144,6 +146,83 @@ extension OpenAIDashboardSnapshot {
guard let creditsRemaining else { return nil }
return CreditsSnapshot(remaining: creditsRemaining, events: self.creditEvents, updatedAt: self.updatedAt)
}

public func toCostUsageTokenSnapshot(
historyDays: Int = 30,
merging localSnapshot: CostUsageTokenSnapshot? = nil) -> CostUsageTokenSnapshot?
{
let clampedHistoryDays = max(1, min(365, historyDays))
let breakdown = OpenAIDashboardDailyBreakdown
.removingSkillUsageServices(from: self.usageBreakdown)
.sorted { lhs, rhs in lhs.day < rhs.day }
.suffix(clampedHistoryDays)
guard !breakdown.isEmpty else { return nil }

let daily = breakdown.compactMap { day -> CostUsageDailyReport.Entry? in
let costUSD = Self.codexUSD(fromCredits: day.totalCreditsUsed)
guard costUSD > 0 else { return nil }
let services = day.services
.filter { $0.creditsUsed > 0 }
.sorted { lhs, rhs in
if lhs.creditsUsed == rhs.creditsUsed { return lhs.service < rhs.service }
return lhs.creditsUsed > rhs.creditsUsed
}
let modelBreakdowns = services.map {
CostUsageDailyReport.ModelBreakdown(
modelName: $0.service,
costUSD: Self.codexUSD(fromCredits: $0.creditsUsed),
totalTokens: nil)
}
return CostUsageDailyReport.Entry(
date: day.day,
inputTokens: nil,
outputTokens: nil,
totalTokens: nil,
costUSD: costUSD,
modelsUsed: services.map(\.service),
modelBreakdowns: modelBreakdowns.isEmpty ? nil : modelBreakdowns)
}
guard !daily.isEmpty else { return nil }

let latest = daily.last
let totalCostUSD = daily.compactMap(\.costUSD).reduce(0, +)
let mergedDaily = localSnapshot.map { Self.mergeDashboardCostDailyEntries(daily, with: $0.daily) } ?? daily
return CostUsageTokenSnapshot(
sessionTokens: localSnapshot?.sessionTokens,
sessionCostUSD: latest?.costUSD,
sessionRequests: localSnapshot?.sessionRequests,
last30DaysTokens: localSnapshot?.last30DaysTokens,
last30DaysCostUSD: totalCostUSD > 0 ? totalCostUSD : nil,
last30DaysRequests: localSnapshot?.last30DaysRequests,
historyDays: clampedHistoryDays,
daily: mergedDaily,
updatedAt: self.updatedAt)
}

public static func codexUSD(fromCredits credits: Double) -> Double {
credits / self.codexCreditsPerUSD
}

private static func mergeDashboardCostDailyEntries(
_ dashboardEntries: [CostUsageDailyReport.Entry],
with localEntries: [CostUsageDailyReport.Entry]) -> [CostUsageDailyReport.Entry]
{
let localByDate = Dictionary(localEntries.map { ($0.date, $0) }, uniquingKeysWith: { _, latest in latest })
return dashboardEntries.map { dashboard in
guard let local = localByDate[dashboard.date] else { return dashboard }
return CostUsageDailyReport.Entry(
date: dashboard.date,
inputTokens: local.inputTokens,
outputTokens: local.outputTokens,
cacheReadTokens: local.cacheReadTokens,
cacheCreationTokens: local.cacheCreationTokens,
totalTokens: local.totalTokens,
requestCount: local.requestCount,
costUSD: dashboard.costUSD,
modelsUsed: dashboard.modelsUsed,
modelBreakdowns: dashboard.modelBreakdowns)
}
}
}

public struct OpenAIDashboardDailyBreakdown: Codable, Equatable, Sendable {
Expand Down
94 changes: 94 additions & 0 deletions Tests/CodexBarTests/MenuCardCostHintTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -93,4 +93,98 @@ struct MenuCardCostHintTests {

#expect(model.tokenUsage?.monthLine.hasPrefix("Today: ") == true)
}

@Test
func `codex dashboard credit cost uses dashboard hint and omits token suffix`() throws {
let now = Date()
let metadata = try #require(ProviderDefaults.metadata[.codex])
let snapshot = CostUsageTokenSnapshot(
sessionTokens: nil,
sessionCostUSD: 19.95,
last30DaysTokens: nil,
last30DaysCostUSD: 123.45,
daily: [
.init(
date: "2026-06-19",
inputTokens: nil,
outputTokens: nil,
totalTokens: nil,
costUSD: 19.95,
modelsUsed: ["Exec"],
modelBreakdowns: [
.init(modelName: "Exec", costUSD: 18.29, totalTokens: nil),
]),
],
updatedAt: now)
let model = UsageMenuCardView.Model.make(.init(
provider: .codex,
metadata: metadata,
snapshot: nil,
credits: nil,
creditsError: nil,
dashboard: nil,
dashboardError: nil,
tokenSnapshot: snapshot,
tokenError: nil,
account: AccountInfo(email: nil, plan: nil),
isRefreshing: false,
lastError: nil,
usageBarsShowUsed: false,
resetTimeDisplayStyle: .countdown,
tokenCostUsageEnabled: true,
showOptionalCreditsAndExtraUsage: true,
hidePersonalInfo: false,
now: now))

#expect(model.tokenUsage?.sessionLine == "Today: $19.95")
#expect(model.tokenUsage?.monthLine == "Last 30 days: $123.45")
#expect(model.tokenUsage?.hintLine?.contains("dashboard credits") == true)
}

@Test
func `codex dashboard credit cost keeps local token suffix when merged`() throws {
let now = Date()
let metadata = try #require(ProviderDefaults.metadata[.codex])
let snapshot = CostUsageTokenSnapshot(
sessionTokens: 30_000_000,
sessionCostUSD: 19.64,
last30DaysTokens: 4_700_000_000,
last30DaysCostUSD: 123.45,
daily: [
.init(
date: "2026-06-19",
inputTokens: 20_000_000,
outputTokens: 10_000_000,
totalTokens: 30_000_000,
costUSD: 19.64,
modelsUsed: ["Exec", "Desktop App"],
modelBreakdowns: [
.init(modelName: "Exec", costUSD: 18.29, totalTokens: nil),
]),
],
updatedAt: now)
let model = UsageMenuCardView.Model.make(.init(
provider: .codex,
metadata: metadata,
snapshot: nil,
credits: nil,
creditsError: nil,
dashboard: nil,
dashboardError: nil,
tokenSnapshot: snapshot,
tokenError: nil,
account: AccountInfo(email: nil, plan: nil),
isRefreshing: false,
lastError: nil,
usageBarsShowUsed: false,
resetTimeDisplayStyle: .countdown,
tokenCostUsageEnabled: true,
showOptionalCreditsAndExtraUsage: true,
hidePersonalInfo: false,
now: now))

#expect(model.tokenUsage?.sessionLine == "Today: $19.64 · 30M tokens")
#expect(model.tokenUsage?.monthLine == "Last 30 days: $123.45 · 4.7B tokens")
#expect(model.tokenUsage?.hintLine?.contains("dashboard credits") == true)
}
}
Loading