Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
65ed68e
fix(grok): report real token usage and list-price cost from CLI sessi…
olddonkey Aug 22, 2026
96af763
feat(grok): count OpenCodex xAI traffic toward the Grok spend row
olddonkey Aug 22, 2026
61d3984
fix(grok): fetch the models.dev catalog on Grok-only installs
olddonkey Aug 22, 2026
8fd6914
test: show cost, provenance and priced-day coverage in the gated Grok…
olddonkey Aug 22, 2026
06e11e0
test: prove the Grok fallback survives repeated probe failures
olddonkey Aug 22, 2026
3ac0ac2
fix(grok): attribute OpenCodex xAI usage only when it is OAuth-backed
olddonkey Aug 22, 2026
a4d4902
Address Grok usage review findings
olddonkey Aug 23, 2026
2869f78
Bound Grok session log scanning
olddonkey Aug 23, 2026
630b7cf
Fix Grok scanner Linux build
olddonkey Aug 23, 2026
ef2fcfc
Document Grok usage pricing
olddonkey Aug 23, 2026
a70e803
Deduplicate compatible parser hash
olddonkey Aug 23, 2026
8ba132b
Fix rebased Grok test integration
olddonkey Aug 24, 2026
f7cdead
Move Grok changelog to 0.55.1
olddonkey Aug 24, 2026
2924c46
Restore Grok menu fallback
olddonkey Aug 24, 2026
eb9b15e
Rescan empty Grok fallback
olddonkey Aug 24, 2026
4e9f3b4
Address Grok review findings
olddonkey Aug 25, 2026
a11f47f
Refresh Grok fallback after preserved failures
olddonkey Aug 25, 2026
cdcfcf9
Use fresh Grok data in spend dashboard
olddonkey Aug 25, 2026
b8bfe9a
Fix Grok rebase integration
olddonkey Aug 25, 2026
be567ee
Restore the Grok usage changelog entry
olddonkey Aug 28, 2026
74dd816
Move the Grok changelog entry into the 0.56.1 section
olddonkey Aug 30, 2026
7eecb4e
Address the ClawSweeper P2 findings on the Grok scan
olddonkey Aug 30, 2026
cb33abb
Redo the parser-hash bookkeeping for the rebased main
olddonkey Aug 30, 2026
fc8dbe6
Price Grok turns from the spend the CLI recorded
olddonkey Sep 1, 2026
a2d85f7
Derive narrowed Grok windows from the days they keep
olddonkey Sep 2, 2026
4f0150b
Preserve Grok cost sources in live history windows
olddonkey Sep 4, 2026
7917383
Preserve Grok cost sources after dashboard filtering
olddonkey Sep 4, 2026
3c33da0
Honor Grok turn-level recorded costs
olddonkey Sep 4, 2026
b9486a6
Refresh Grok integration after main changes
olddonkey Sep 5, 2026
9aba5e0
Include recorded OpenCodex Grok OAuth usage in spend
olddonkey Sep 5, 2026
231714c
Preserve standalone prices and cancel Grok scans promptly
olddonkey Sep 5, 2026
81f1e2e
Preserve OpenCodex custom price overrides
olddonkey Sep 5, 2026
c3919a2
Merge main and preserve compatible cost caches
olddonkey Sep 6, 2026
0874450
Merge main and retain Grok and Poe release notes
olddonkey Sep 6, 2026
6f4a7d1
Merge main and retain Claude warning fixes alongside Grok usage
olddonkey Sep 6, 2026
c35734a
Merge main and preserve Grok cache compatibility
olddonkey Sep 8, 2026
40f9e47
Merge main and preserve Grok accounting compatibility
olddonkey Sep 12, 2026
6d3d5af
Guard native Grok token decoding and accumulation
olddonkey Sep 12, 2026
a417361
Preserve unknown Grok totals through menu projections
olddonkey Sep 12, 2026
e7b2b22
Merge upstream/main into feat/grok-real-token-usage
olddonkey Sep 19, 2026
32a73cd
Merge upstream/main (0741047cb) into feat/grok-real-token-usage
olddonkey Sep 19, 2026
eb1647c
Merge upstream/main (0f8489dd8) into feat/grok-real-token-usage
olddonkey Sep 19, 2026
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,8 @@
- Copilot: add optional per-account AI credit allowances and a way to clear legacy defaults, retaining matching cached usage during offline edits and preserving independent reset baselines (#2647). Thanks @KSEGIT!
- Grok: add a visor critter across single- and two-meter quota layouts, respecting Hide Critters (#3028). Thanks @sm0keyyy!
- Usage & Spend: count covered calendar days, align chart labels and ranges with the selected bucket time zone, and preserve historical-pace credits after midnight daylight-saving transitions (follow-up to #3565).
- Grok: count completed-turn usage from bounded local CLI session-log scans instead of context-window occupancy, and price it from the spend the CLI recorded, falling back to clearly labeled public xAI list prices where it recorded none; standalone OpenCodex xAI history remains token-only unless the user supplies explicit custom prices (#3135, #3345). Thanks @olddonkey and @initH271!
- Usage & Spend: include reported OpenCodex Grok OAuth attempts when log import is enabled and the log records request-time credential provenance. Keep API-key and historic traffic excluded, and label OpenCodex dollars as list-price estimates (#3135). Thanks @olddonkey!
- Usage & Spend: preserve heatmap coverage, token totals, and daily ledger rows across midnight daylight-saving transitions, retaining real gaps and unscanned days (#3565). Thanks @gabrielrojasc!
- Cursor on Linux: restore automatic authentication from the signed-in app, honor absolute XDG/HOME paths, and preserve manual-cookie precedence and explicit web-mode isolation (#3539). Thanks @DonnieFi!
- Codex spend: exclude time waiting behind other scans from automatic catch-up sleep calculations while preserving scan budgets, power safeguards, and complete-history publication (#3566, related to #3508 and #3411).
Expand Down
82 changes: 82 additions & 0 deletions Scripts/capture_grok_opencodex_proof.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
#!/usr/bin/env python3
"""Capture real OpenCodex usage-writer output using its isolated upstream fixtures."""

import argparse
import hashlib
import json
import pathlib
import re
import subprocess


def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--opencodex-root", required=True, type=pathlib.Path)
parser.add_argument("--output-dir", required=True, type=pathlib.Path)
args = parser.parse_args()
root = args.opencodex_root.resolve()
expected = "146ed679c9633e5d68726217fcadc8e0b107339b"
head = subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=root, text=True).strip()
dirty = subprocess.check_output(["git", "status", "--porcelain"], cwd=root, text=True).strip()
if head != expected or dirty:
parser.error("Use a clean OpenCodex checkout at " + expected)
output = args.output_dir.resolve()
output.mkdir(parents=True, exist_ok=False)
ledger = output / "usage.jsonl"
source = root / "tests/server/server-xai-oauth-401-replay.test.ts"
original = source.read_text()
captured_test = original.replace(
"mkdtempSync, readFileSync}",
"mkdtempSync, readFileSync, existsSync, appendFileSync}",
1,
).replace(
"afterEach(() => {",
"afterEach(() => {\n if (existsSync(usageLogPath())) appendFileSync("
+ json.dumps(str(ledger)) + ", readFileSync(usageLogPath()));",
1,
).replace(
"return originalFetch(input, init);",
'throw new Error("Unexpected upstream request in isolated producer proof");',
)
captured_test = re.sub(
r'from "(\.[^"]+)"',
lambda match: "from " + json.dumps(str((source.parent / match[1]).resolve())),
captured_test,
)
test_file = output / "producer-proof.test.ts"
test_file.write_text(captured_test)
bun = root / "node_modules/.bin/bun"
version = subprocess.check_output([str(bun), "--version"], cwd=root, text=True).strip()
if version != "1.4.0":
parser.error("Use repository-pinned Bun 1.4.0")
with (output / "terminal.log").open("w") as terminal:
subprocess.run(
[str(bun), "test", str(test_file), "--test-name-pattern",
"401 then 200 performs one refresh and one replay|native Chat records canonical API-key provenance"],
cwd=root, stdout=terminal, stderr=subprocess.STDOUT, check=True,
)
raw = ledger.read_bytes()
rows = [json.loads(line) for line in raw.splitlines()]
assert len(rows) == 2
assert all(secret not in raw for secret in [
b"rejected-access", b"fresh-access", b"initial-refresh", b"xai-test-account", b"Bearer ",
])
attempts = [attempt for row in rows for attempt in row["attempts"]]
assert {row["credentialSource"] for row in attempts} == {"grok-oauth", "xai-api-key"}
result = {
"producerRepository": "https://github.com/lidge-jun/opencodex",
"producerCommit": head,
"bunVersion": version,
"upstream": "isolated fixtures; no live provider requests",
"sha256": hashlib.sha256(raw).hexdigest(),
"rows": len(rows),
"attempts": [{key: row[key] for key in [
"credentialSource", "adapter", "sendCount", "totalTokens",
]} for row in attempts],
}
(output / "capture.json").write_text(json.dumps(result, indent=2) + "\n")
print(json.dumps(result, indent=2))


if __name__ == "__main__":
main()
11 changes: 2 additions & 9 deletions Sources/CodexBar/MenuCardView+Costs.swift
Original file line number Diff line number Diff line change
Expand Up @@ -217,15 +217,8 @@ extension UsageMenuCardView.Model {
preferredCurrency: preferredCurrencyCode,
providerCurrency: snapshot.currencyCode)
} ?? "—"
let fallbackTokens: Int? = {
var sum = 0
for t in snapshot.daily.compactMap(\.totalTokens) {
let (res, of) = sum.addingReportingOverflow(t)
if of { return nil }
sum = res
}
return sum > 0 ? sum : nil
}()
let fallbackTokens = CostUsageDailyReport.completeCountSum(snapshot.daily.map(\.totalTokens))
.flatMap { $0 > 0 ? $0 : nil }
let monthTokensValue = snapshot.last30DaysTokens ?? fallbackTokens
let monthTokens = monthTokensValue.map { UsageFormatter.tokenCountString($0) }
let windowLabel = if let historyLabel = snapshot.historyLabel {
Expand Down
10 changes: 9 additions & 1 deletion Sources/CodexBar/PreferencesSpendDashboardPane.swift
Original file line number Diff line number Diff line change
Expand Up @@ -654,7 +654,15 @@ private struct SpendProviderPanel: View {
.foregroundStyle(.tertiary)
.frame(width: 26, alignment: .leading)
SpendProviderIcon(provider: row.provider, sourceKind: row.sourceKind)
Text(row.displayName).lineLimit(1)
VStack(alignment: .leading, spacing: 2) {
Text(row.displayName).lineLimit(1)
if let disclaimer = row.costDisclaimer {
Text(disclaimer)
.font(.caption2)
.foregroundStyle(.secondary)
.lineLimit(1)
}
}
Spacer()
Text(
row.totalCost == nil && row.totalTokens == nil && row.incompleteRequestCount == 0
Expand Down
42 changes: 36 additions & 6 deletions Sources/CodexBar/Providers/Grok/UsageStore+GrokLocalSessions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,22 +20,23 @@ extension UsageStore {
else { return nil }
let daily = published.daily.filter { $0.date >= firstDay && $0.date <= lastDay }
guard !daily.isEmpty else { return nil }
let tokens = daily.compactMap(\.totalTokens)
let requests = daily.compactMap(\.requestCount)
// Tokens and requests are recomputed from the retained days, so the cost has to be too. Copying the
// published total would render the full 365-day amount beside a 30-day token count.
let costs = daily.compactMap(\.costUSD)

return CostUsageTokenSnapshot(
sessionTokens: published.sessionTokens,
sessionCostUSD: published.sessionCostUSD,
sessionRequests: published.sessionRequests,
last30DaysTokens: tokens.isEmpty ? nil : tokens.reduce(0, +),
last30DaysCostUSD: published.last30DaysCostUSD,
last30DaysRequests: requests.isEmpty ? nil : requests.reduce(0, +),
last30DaysTokens: CostUsageDailyReport.completeCountSum(daily.map(\.totalTokens)),
last30DaysCostUSD: costs.isEmpty ? nil : costs.reduce(0, +),
last30DaysRequests: CostUsageDailyReport.completeCountSum(daily.map(\.requestCount)),
currencyCode: published.currencyCode,
historyDays: days,
historyCoverageIsEstablished: published.historyCoverageIsEstablished && published.historyDays >= days,
historyLabel: published.historyLabel,
meteredCostUSD: published.meteredCostUSD,
costProvenance: published.costProvenance,
costProvenance: GrokLocalSessionSummary.costProvenance(for: daily, fallback: published.costProvenance),
credentialScopeFingerprint: published.credentialScopeFingerprint,
daily: daily,
projects: published.projects,
Expand All @@ -51,6 +52,35 @@ extension UsageStore {
return summary.toCostUsageTokenSnapshot(historyDays: historyDays)
}

/// Generic window math does not know that Grok's priced rows are CLI-recorded spend. Apply the same
/// source-aware disclosure to live publications and scan results as to the remote-backed projection.
func narrowedGrokTokenSnapshot(_ published: CostUsageTokenSnapshot, historyDays: Int) -> CostUsageTokenSnapshot {
let narrowed = published.narrowed(
toHistoryDays: historyDays,
calendar: self.settings.costUsageBucketCalendar)
return CostUsageTokenSnapshot(
sessionTokens: narrowed.sessionTokens,
sessionCostUSD: narrowed.sessionCostUSD,
sessionRequests: narrowed.sessionRequests,
last30DaysTokens: narrowed.last30DaysTokens,
last30DaysCostUSD: narrowed.last30DaysCostUSD,
last30DaysRequests: narrowed.last30DaysRequests,
currencyCode: narrowed.currencyCode,
historyDays: narrowed.historyDays,
historyCoverageIsEstablished: narrowed.historyCoverageIsEstablished,
historyLabel: narrowed.historyLabel,
meteredCostUSD: narrowed.meteredCostUSD,
costProvenance: GrokLocalSessionSummary.costProvenance(
for: narrowed.daily,
fallback: published.costProvenance),
credentialScopeFingerprint: narrowed.credentialScopeFingerprint,
daily: narrowed.daily,
projects: narrowed.projects,
sessions: narrowed.sessions,
hourly: narrowed.hourly,
updatedAt: narrowed.updatedAt)
}

private static func grokLocalDayKey(for date: Date, calendar: Calendar) -> String? {
let parts = calendar.dateComponents([.year, .month, .day], from: date)
guard let year = parts.year, let month = parts.month, let day = parts.day else { return nil }
Expand Down
23 changes: 14 additions & 9 deletions Sources/CodexBar/SpendDashboardController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -293,15 +293,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 remote = store.snapshot(for: .grok)
let publication = store.tokenSnapshotPublicationForCurrentProviderConfig(for: .grok)
let grokSnapshot = if remote != nil || publication != nil {
store.tokenSnapshotForLiveProviderConsumer(
fromProviderSnapshot: remote,
provider: .grok,
historyDays: Self.scanDays)
} 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 @@ -871,10 +877,9 @@ 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(
return store.tokenSnapshotForLiveProviderConsumer(
fromProviderSnapshot: store.snapshot(for: .grok),
provider: .grok,
historyDays: self.scanDays)
Expand Down
16 changes: 15 additions & 1 deletion Sources/CodexBar/SpendDashboardModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,12 @@ struct SpendDashboardModel: Equatable, Sendable {

let incompleteRequestCount: Int

var costDisclaimer: String? {
// Provider-specific by design: Grok local-session dollars are list-price estimates, not billed spend.
guard self.provider == .grok, self.totalCost != nil else { return nil }
return UsageFormatter.costEstimateHint(provider: self.provider)
}

init(
id: String,
rank: Int,
Expand Down Expand Up @@ -551,7 +557,15 @@ struct SpendDashboardModel: Equatable, Sendable {
metered = (metered ?? 0) + meteredCost * summary.costMultiplier
}
if summary.totalCost != nil {
switch summary.input.snapshot.costProvenance {
// Provider-specific by design: Grok owns the recorded-versus-estimated meaning of its row coverage.
let provenance = if summary.input.provider == .grok {
GrokLocalSessionSummary.costProvenance(
for: summary.entries.map(\.entry),
fallback: summary.input.snapshot.costProvenance)
} else {
summary.input.snapshot.costProvenance
}
switch provenance {
case .vendorMetered:
sawVendorMeteredProvenance = true
case .listPriceEstimate:
Expand Down
6 changes: 4 additions & 2 deletions Sources/CodexBar/SpendDashboardSource+OpenCodex.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ extension SpendDashboardSource {
_ inputs: [SpendDashboardModel.ProviderInput],
request: SpendDashboardLoadRequest,
environment: [String: String] = ProcessInfo.processInfo.environment,
cacheRoot: URL? = nil,
entryLoader: ((URL) throws -> [OpenCodexUsageEntry])? = nil) -> (
inputs: [SpendDashboardModel.ProviderInput],
observation: SpendDashboardLoadResult.OpenCodexObservation)
Expand All @@ -60,7 +61,7 @@ extension SpendDashboardSource {
guard let logURL = OpenCodexUsageLog.usageLogURL(environment: environment) else {
return (inputs.filter { $0.id != SpendDashboardModel.openCodexSourceID }, .unavailable)
}
let store = OpenCodexUsageStore(cacheRoot: OpenCodexUsageLog.cacheRoot())
let store = OpenCodexUsageStore(cacheRoot: cacheRoot ?? OpenCodexUsageLog.cacheRoot())
let entries: [OpenCodexUsageEntry]
do {
entries = try entryLoader?(logURL) ?? store.loadEntries(logURL: logURL)
Expand Down Expand Up @@ -144,7 +145,8 @@ extension SpendDashboardSource {
supplement,
now: request.now,
historyDays: self.scanDays,
calendar: request.configuration.bucketCalendar),
calendar: request.configuration.bucketCalendar,
provider: input.provider),
tokenActivityCache: input.tokenActivityCache,
sourceKind: input.sourceKind)
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/CodexBar/StatusItemController+Menu.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1558,7 +1558,7 @@ extension StatusItemController {
}

func tokenSnapshotForCostHistorySubmenu(provider: UsageProvider) -> CostUsageTokenSnapshot? {
let projected = self.store.tokenSnapshot(
let projected = self.store.tokenSnapshotForLiveProviderConsumer(
fromProviderSnapshot: self.store.snapshot(for: provider.instanceID),
provider: provider)
if UsageStore.tokenCostRequiresProviderSnapshot(provider) {
Expand Down
10 changes: 7 additions & 3 deletions Sources/CodexBar/UsageStore+MenuCardModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,13 @@ extension UsageStore {
if isSettings {
tokenSnapshot = supportsTokenCost ? self.tokenSnapshot(for: provider) : nil
} else {
let projected = isLive || snapshot != nil
? self.tokenSnapshot(fromProviderSnapshot: snapshot, provider: provider)
: nil
let projected: CostUsageTokenSnapshot? = if isLive {
self.tokenSnapshotForLiveProviderConsumer(fromProviderSnapshot: snapshot, provider: provider)
} else if snapshot != nil {
self.tokenSnapshot(fromProviderSnapshot: snapshot, provider: provider)
} else {
nil
}
let stored = isLive && supportsTokenCost && !Self.tokenCostRequiresProviderSnapshot(provider)
? self.tokenSnapshot(for: provider)
: nil
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 @@ -29,6 +29,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
Loading
Loading