Skip to content
Open
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: 20 additions & 12 deletions Sources/CodexBar/SpendDashboardController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -282,15 +282,21 @@ enum SpendDashboardSource {
// Provider-specific by design: Grok local session tokens are independent of the
// remote billing snapshot, so a failed probe still publishes readable logs.
if provider == .grok {
if let snapshot = store.tokenSnapshot(
fromProviderSnapshot: store.snapshot(for: .grok),
provider: .grok,
historyDays: Self.scanDays)
{
let grokSnapshot = if let usage = store.snapshot(for: .grok) {
store.tokenSnapshot(
fromProviderSnapshot: usage,
provider: .grok,
historyDays: Self.scanDays)
} else if let published = store.tokenSnapshotPublicationForCurrentProviderConfig(for: .grok) {
published.snapshot
} else {
await store.scanAndPublishGrokLocalTokenSnapshot(historyDays: Self.scanDays)
}
if let grokSnapshot {
inputs.append(SpendDashboardModel.ProviderInput(
provider: .grok,
displayName: store.metadata(for: .grok).displayName,
snapshot: snapshot))
snapshot: grokSnapshot))
} else {
confirmedEmptySourceIDs.insert(UsageProvider.grok.rawValue)
}
Expand Down Expand Up @@ -789,13 +795,15 @@ enum SpendDashboardSource {
provider: UsageProvider,
publication: CurrentProviderConfigTokenPublication) -> CostUsageTokenSnapshot?
{
// Provider-specific by design: Grok's catalog input is the local session scan, even when
// the remote billing snapshot is missing.
// Provider-specific by design: a failed Grok probe publishes its detached local scan.
if provider == .grok {
return store.tokenSnapshot(
fromProviderSnapshot: store.snapshot(for: .grok),
provider: .grok,
historyDays: self.scanDays)
if let usage = store.snapshot(for: .grok) {
return store.tokenSnapshot(
fromProviderSnapshot: usage,
provider: .grok,
historyDays: self.scanDays)
}
return publication.snapshot
}
if UsageStore.tokenCostRequiresProviderSnapshot(provider),
let usage = store.snapshot(for: provider.instanceID),
Expand Down
3 changes: 2 additions & 1 deletion Sources/CodexBar/SpendDashboardSource+OpenCodex.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ extension SpendDashboardSource {
entries: entries,
now: request.now,
historyDays: Self.scanDays,
calendar: request.configuration.bucketCalendar)
calendar: request.configuration.bucketCalendar,
oauthBackedProviderIDs: OpenCodexUsageLog.oauthBackedProviderIDs(environment: environment))
var merged = inputs.filter { $0.id != SpendDashboardModel.openCodexSourceID }
var published = false

Expand Down
17 changes: 17 additions & 0 deletions Sources/CodexBar/UsageStore+QuotaWarnings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,23 @@ extension UsageStore {
let displayName: String?
}

func postQuotaWarning(_ event: QuotaWarningEvent, provider: UsageProvider) {
self.sessionQuotaNotifier.postQuotaWarning(
event: event,
provider: provider,
soundEnabled: self.settings.quotaWarningSoundEnabled,
onScreenAlertEnabled: self.settings.quotaWarningOnScreenAlertEnabled)
}

func postPredictivePaceWarning(_ event: PredictivePaceWarningEvent, provider: UsageProvider, now: Date) {
self.sessionQuotaNotifier.postPredictivePaceWarning(
event: event,
provider: provider,
soundEnabled: self.settings.quotaWarningSoundEnabled,
onScreenAlertEnabled: self.settings.quotaWarningOnScreenAlertEnabled,
now: now)
}

func handleQuotaWarningTransitions(
provider: UsageProvider,
snapshot: UsageSnapshot,
Expand Down
13 changes: 5 additions & 8 deletions Sources/CodexBar/UsageStore+Refresh.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1450,14 +1450,11 @@ extension UsageStore {
// Provider-specific by design: local ~/.grok/sessions tokens remain readable
// when the remote billing probe fails.
if provider == .grok {
if let local = self.tokenSnapshot(
fromProviderSnapshot: nil,
provider: .grok,
historyDays: SpendDashboardSource.scanDays)
{
self.publishTokenSnapshot(local, for: provider)
} else {
self.clearTokenSnapshot(for: provider)
if self.tokenSnapshotPublicationForCurrentProviderConfig(for: provider) == nil {
Task { @MainActor [weak self] in
await self?.scanAndPublishGrokLocalTokenSnapshot(
historyDays: GrokLocalSessionScanner.maximumLookbackDays)
}
}
} else if Self.tokenCostRequiresProviderSnapshot(provider) {
self.clearTokenSnapshot(for: provider)
Expand Down
72 changes: 68 additions & 4 deletions Sources/CodexBar/UsageStore+TokenCost.swift
Original file line number Diff line number Diff line change
Expand Up @@ -455,8 +455,8 @@ extension UsageStore {
{
let windowDays = historyDays ?? self.settings.costUsageHistoryDays
// Provider-specific by design: snapshot-backed spend sources own their live billing
// projection. Grok contributes local session tokens only; xAI contributes Management API
// daily spend only. Neither converts a quota or prepaid balance into dollars.
// projection. Grok contributes local session list-price estimates; xAI contributes
// Management API daily spend. Neither converts a quota or prepaid balance into dollars.
switch provider {
case .openai:
return snapshot?.openAIAPIUsage?.toCostUsageTokenSnapshot()
Expand All @@ -475,13 +475,77 @@ extension UsageStore {
case .xai:
return snapshot.flatMap { XAICostUsageMapping.tokenSnapshot(from: $0, historyDays: windowDays) }
case .grok:
return GrokLocalSessionScanner.summarize(lookbackDays: windowDays)
.toCostUsageTokenSnapshot(historyDays: windowDays)
return snapshot?.costUsage?.narrowed(
toHistoryDays: windowDays,
calendar: self.settings.costUsageBucketCalendar)
default:
return nil
}
}

@discardableResult
func scanAndPublishGrokLocalTokenSnapshot(historyDays: Int) async -> CostUsageTokenSnapshot? {
// Provider-specific by design: this fallback owns Grok's local session scan and publication.
let provider = UsageProvider.grok
let requestedHistoryDays = min(max(1, historyDays), GrokLocalSessionScanner.maximumLookbackDays)
if let publication = self.tokenSnapshotPublicationForCurrentProviderConfig(for: provider) {
return publication.snapshot?.narrowed(
toHistoryDays: requestedHistoryDays,
calendar: self.settings.costUsageBucketCalendar)
}
if let task = self.grokLocalTokenScanTask {
return await task.value?.narrowed(
toHistoryDays: requestedHistoryDays,
calendar: self.settings.costUsageBucketCalendar)
}

let environment = self.environmentBase
let publicationRevision = self.providerPublicationRevision(for: provider)
let providerConfigRevision = self.settings.providerConfigRevision(for: provider)
let scannerOverride = self._test_grokLocalTokenScannerOverride
let token = UUID()
let task = Task { @MainActor [weak self] () -> CostUsageTokenSnapshot? in
let snapshot: CostUsageTokenSnapshot?
if let scannerOverride {
snapshot = await scannerOverride(GrokLocalSessionScanner.maximumLookbackDays)
} else {
let scanTask = Task.detached(priority: .utility) {
await GrokLocalSessionScanner.summarizeRequestingPricingRefresh(
env: environment,
lookbackDays: GrokLocalSessionScanner.maximumLookbackDays)
.toCostUsageTokenSnapshot(historyDays: GrokLocalSessionScanner.maximumLookbackDays)
}
snapshot = await withTaskCancellationHandler {
await scanTask.value
} onCancel: {
scanTask.cancel()
}
}
guard let self,
!Task.isCancelled,
self.providerPublicationRevisionIsCurrent(publicationRevision, for: provider),
self.settings.providerConfigRevision(for: provider) == providerConfigRevision,
self.isEnabled(provider)
else { return nil }
if let snapshot {
self.publishTokenSnapshot(snapshot, for: provider)
} else {
self.publishConfirmedEmptyTokenSnapshot(for: provider)
}
return snapshot
}
self.grokLocalTokenScanToken = token
self.grokLocalTokenScanTask = task
let snapshot = await task.value
if self.grokLocalTokenScanToken == token {
self.grokLocalTokenScanTask = nil
self.grokLocalTokenScanToken = nil
}
return snapshot?.narrowed(
toHistoryDays: requestedHistoryDays,
calendar: self.settings.costUsageBucketCalendar)
}

nonisolated static func tokenCostRequiresProviderSnapshot(_ provider: UsageProvider) -> Bool {
// Provider-specific by design: these providers project live usage snapshots into the
// shared spend catalog instead of running the local CostUsageFetcher JSONL pipeline.
Expand Down
27 changes: 10 additions & 17 deletions Sources/CodexBar/UsageStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,8 @@ final class UsageStore {
var tokenSnapshots: [ProviderInstanceID: CostUsageTokenSnapshot] = [:]
var tokenSnapshotPublications: [ProviderInstanceID: TokenSnapshotPublication] = [:]
var tokenSnapshotPublicationRevisions: [ProviderInstanceID: UInt64] = [:]
@ObservationIgnored var grokLocalTokenScanTask: Task<CostUsageTokenSnapshot?, Never>?
@ObservationIgnored var grokLocalTokenScanToken: UUID?
var spendDashboardTokenPublications: [ProviderInstanceID: TokenSnapshotPublication] = [:]
var spendDashboardTokenPublicationRevisions: [ProviderInstanceID: UInt64] = [:]
var spendDashboardPublication = SpendDashboardPublication.empty
Expand Down Expand Up @@ -275,6 +277,8 @@ final class UsageStore {
Date,
String?,
Int) async throws -> CostUsageTokenSnapshot)?
@ObservationIgnored var _test_grokLocalTokenScannerOverride: (@MainActor (
Int) async -> CostUsageTokenSnapshot?)?
@ObservationIgnored var _test_cachedCodexTokenSnapshotLoaderOverride: (@MainActor (
Date,
String?,
Expand Down Expand Up @@ -963,6 +967,7 @@ final class UsageStore {
self.codexPlanHistoryBackfillTask?.cancel()
self.resetBoundaryRefreshTask?.cancel()
self.planUtilizationHistoryLoadTask?.cancel()
self.grokLocalTokenScanTask?.cancel()
}

enum SessionQuotaWindowSource: String {
Expand All @@ -971,23 +976,6 @@ final class UsageStore {
case antigravityQuotaSummary
case antigravityLegacy
}

func postQuotaWarning(_ event: QuotaWarningEvent, provider: UsageProvider) {
self.sessionQuotaNotifier.postQuotaWarning(
event: event,
provider: provider,
soundEnabled: self.settings.quotaWarningSoundEnabled,
onScreenAlertEnabled: self.settings.quotaWarningOnScreenAlertEnabled)
}

func postPredictivePaceWarning(_ event: PredictivePaceWarningEvent, provider: UsageProvider, now: Date) {
self.sessionQuotaNotifier.postPredictivePaceWarning(
event: event,
provider: provider,
soundEnabled: self.settings.quotaWarningSoundEnabled,
onScreenAlertEnabled: self.settings.quotaWarningOnScreenAlertEnabled,
now: now)
}
}

extension UsageStore {
Expand Down Expand Up @@ -1618,6 +1606,11 @@ extension UsageStore {
self.cancelCodexCostCatchUp()
self.cancelSpendDashboardCodexCostCatchUp()
}
if provider == .grok {
self.grokLocalTokenScanTask?.cancel()
self.grokLocalTokenScanTask = nil
self.grokLocalTokenScanToken = nil
}
self.clearTokenSnapshot(for: provider)
self.clearSpendDashboardTokenSnapshot(for: provider)
self.tokenErrors[provider.instanceID] = nil
Expand Down
66 changes: 66 additions & 0 deletions Sources/CodexBarCore/CostUsageModels.swift
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,72 @@ public struct CostUsageTokenSnapshot: Sendable, Equatable {
Self.entry(in: self.daily, forLocalDayContaining: self.updatedAt, calendar: calendar)
}

/// Reprojects this snapshot from its retained daily rows into a smaller rolling window.
public func narrowed(toHistoryDays requestedDays: Int, calendar: Calendar = .current) -> Self {
let days = min(max(1, requestedDays), max(1, self.historyDays))
let today = calendar.startOfDay(for: self.updatedAt)
let start = calendar.date(byAdding: .day, value: -(days - 1), to: today) ?? today
let startKey = CostUsageLocalDay.key(from: start, calendar: calendar)
let endKey = CostUsageLocalDay.key(from: today, calendar: calendar)
let entries = self.daily.filter { entry in
guard let dayKey = Self.localDayKey(for: entry.date, calendar: calendar) else { return false }
return dayKey >= startKey && dayKey <= endKey
}
let derived = CostUsageFetcher.tokenSnapshot(
from: CostUsageDailyReport(data: entries, summary: nil),
now: self.updatedAt,
historyDays: days,
useCurrentLocalDayForSession: true,
calendar: calendar,
historyCoverageIsEstablished: self.historyCoverageIsEstablished,
meteredCostUSD: days == self.historyDays ? self.meteredCostUSD : nil,
costProvenance: self.costProvenance,
credentialScopeFingerprint: self.credentialScopeFingerprint,
historyLabel: self.historyLabel,
projects: self.projects,
sessions: self.sessions,
updatedAt: self.updatedAt)
let sessionRequests: Int? = if let current = Self.entry(
in: entries,
forLocalDayContaining: self.updatedAt,
calendar: calendar)
{
current.requestCount
} else if !entries.isEmpty || self.historyCoverageIsEstablished {
0
} else {
nil
}
let requests = entries.compactMap(\.requestCount)
let allEntriesCarryRequests = !entries.isEmpty && entries.allSatisfy { $0.requestCount != nil }
let totalRequests: Int? = if allEntriesCarryRequests {
requests.reduce(0, +)
} else if self.historyCoverageIsEstablished, entries.isEmpty {
0
} else {
nil
}
return Self(
sessionTokens: derived.sessionTokens,
sessionCostUSD: derived.sessionCostUSD,
sessionRequests: sessionRequests,
last30DaysTokens: derived.last30DaysTokens,
last30DaysCostUSD: derived.last30DaysCostUSD,
last30DaysRequests: totalRequests,
currencyCode: self.currencyCode,
historyDays: days,
historyCoverageIsEstablished: self.historyCoverageIsEstablished,
historyLabel: self.historyLabel,
meteredCostUSD: derived.meteredCostUSD,
costProvenance: self.costProvenance,
credentialScopeFingerprint: self.credentialScopeFingerprint,
daily: entries,
projects: self.projects,
sessions: self.sessions,
hourly: self.hourly,
updatedAt: self.updatedAt)
}

public func summary(forLastDays requestedDays: Int, calendar: Calendar = .current) -> CostUsageWindowSummary {
let days = max(1, requestedDays)
let today = calendar.startOfDay(for: self.updatedAt)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Generated by Scripts/regenerate-codex-parser-hash.sh. Do not edit by hand.

enum CodexParserHash {
static let value = "3c984b655688593f"
static let value = "abc6d9f19ea31f91"
}
Loading