diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index de2359aca..3338bcdfe 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,27 +16,31 @@ jobs: set -euo pipefail for candidate in /Applications/Xcode_26.1.1.app /Applications/Xcode_26.1.app /Applications/Xcode.app; do if [[ -d "$candidate" ]]; then - sudo xcode-select -s "${candidate}/Contents/Developer" + sudo xcode-select -s "$candidate" echo "DEVELOPER_DIR=${candidate}/Contents/Developer" >> "$GITHUB_ENV" break fi done /usr/bin/xcodebuild -version - - name: Swift toolchain version + - name: Install Swift 6.2 toolchain run: | - set -euo pipefail - swift --version - swift package --version + curl -L https://download.swift.org/swift-6.2-release/xcode/swift-6.2-RELEASE/swift-6.2-RELEASE-osx.pkg -o /tmp/swift.pkg + sudo installer -pkg /tmp/swift.pkg -target / + echo "/Library/Developer/Toolchains/swift-6.2-RELEASE.xctoolchain/usr/bin" >> "$GITHUB_PATH" + echo "TOOLCHAINS=swift-6.2-RELEASE" >> "$GITHUB_ENV" - name: Install lint tools - run: ./Scripts/install_lint_tools.sh + run: brew install swiftlint swiftformat + + - name: SwiftFormat (lint) + run: swiftformat Sources Tests --lint - - name: Lint - run: ./Scripts/lint.sh lint + - name: SwiftLint + run: swiftlint --strict - name: Swift Test - run: swift test --no-parallel + run: swift test --parallel build-linux-cli: strategy: diff --git a/.github/workflows/upstream-monitor.yml b/.github/workflows/upstream-monitor.yml index 04140e8d6..45b3a999d 100644 --- a/.github/workflows/upstream-monitor.yml +++ b/.github/workflows/upstream-monitor.yml @@ -25,7 +25,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@v4 with: fetch-depth: 0 @@ -63,7 +63,7 @@ jobs: - name: Create or update issue if: steps.check.outputs.upstream_commits > 0 || steps.check.outputs.quotio_commits > 0 - uses: actions/github-script@v8 + uses: actions/github-script@v7 with: script: | const upstreamCommits = '${{ steps.check.outputs.upstream_commits }}'; diff --git a/Sources/CodexBar/CostHistoryChartMenuView.swift b/Sources/CodexBar/CostHistoryChartMenuView.swift index 9c77cf4ef..704f74b28 100644 --- a/Sources/CodexBar/CostHistoryChartMenuView.swift +++ b/Sources/CodexBar/CostHistoryChartMenuView.swift @@ -153,7 +153,7 @@ struct CostHistoryChartMenuView: View { for entry in sorted { guard let costUSD = entry.costUSD, costUSD > 0 else { continue } guard let date = self.dateFromDayKey(entry.date) else { continue } - let point = Point(date: date, costUSD: costUSD, totalTokens: entry.totalTokens) + let point = Point(date: date, costUSD: costUSD, totalTokens: entry.processedTokens ?? entry.totalTokens) points.append(point) pointsByKey[entry.date] = point entriesByKey[entry.date] = entry diff --git a/Sources/CodexBar/MenuCardView.swift b/Sources/CodexBar/MenuCardView.swift index 89a930dc1..f213c8c57 100644 --- a/Sources/CodexBar/MenuCardView.swift +++ b/Sources/CodexBar/MenuCardView.swift @@ -901,7 +901,8 @@ extension UsageMenuCardView.Model { guard let snapshot else { return nil } let sessionCost = snapshot.sessionCostUSD.map { UsageFormatter.usdString($0) } ?? "—" - let sessionTokens = snapshot.sessionTokens.map { UsageFormatter.tokenCountString($0) } + let sessionTokensValue = snapshot.sessionProcessedTokens ?? snapshot.sessionTokens + let sessionTokens = sessionTokensValue.map { UsageFormatter.tokenCountString($0) } let sessionLine: String = { if let sessionTokens { return "Today: \(sessionCost) · \(sessionTokens) tokens" @@ -910,8 +911,10 @@ extension UsageMenuCardView.Model { }() let monthCost = snapshot.last30DaysCostUSD.map { UsageFormatter.usdString($0) } ?? "—" - let fallbackTokens = snapshot.daily.compactMap(\.totalTokens).reduce(0, +) - let monthTokensValue = snapshot.last30DaysTokens ?? (fallbackTokens > 0 ? fallbackTokens : nil) + let fallbackTokens = snapshot.daily.compactMap { $0.processedTokens ?? $0.totalTokens }.reduce(0, +) + let monthTokensValue = snapshot.last30DaysProcessedTokens + ?? snapshot.last30DaysTokens + ?? (fallbackTokens > 0 ? fallbackTokens : nil) let monthTokens = monthTokensValue.map { UsageFormatter.tokenCountString($0) } let monthLine: String = { if let monthTokens { diff --git a/Sources/CodexBar/UsageStore+WidgetSnapshot.swift b/Sources/CodexBar/UsageStore+WidgetSnapshot.swift index f8699128d..653eb55c5 100644 --- a/Sources/CodexBar/UsageStore+WidgetSnapshot.swift +++ b/Sources/CodexBar/UsageStore+WidgetSnapshot.swift @@ -32,7 +32,7 @@ extension UsageStore { let dailyUsage = tokenSnapshot?.daily.map { entry in WidgetSnapshot.DailyUsagePoint( dayKey: entry.date, - totalTokens: entry.totalTokens, + totalTokens: entry.processedTokens ?? entry.totalTokens, costUSD: entry.costUSD) } ?? [] @@ -56,11 +56,13 @@ extension UsageStore { from snapshot: CostUsageTokenSnapshot?) -> WidgetSnapshot.TokenUsageSummary? { guard let snapshot else { return nil } - let fallbackTokens = snapshot.daily.compactMap(\.totalTokens).reduce(0, +) - let monthTokensValue = snapshot.last30DaysTokens ?? (fallbackTokens > 0 ? fallbackTokens : nil) + let fallbackTokens = snapshot.daily.compactMap { $0.processedTokens ?? $0.totalTokens }.reduce(0, +) + let monthTokensValue = snapshot.last30DaysProcessedTokens + ?? snapshot.last30DaysTokens + ?? (fallbackTokens > 0 ? fallbackTokens : nil) return WidgetSnapshot.TokenUsageSummary( sessionCostUSD: snapshot.sessionCostUSD, - sessionTokens: snapshot.sessionTokens, + sessionTokens: snapshot.sessionProcessedTokens ?? snapshot.sessionTokens, last30DaysCostUSD: snapshot.last30DaysCostUSD, last30DaysTokens: monthTokensValue) } diff --git a/Sources/CodexBarCore/CostUsageFetcher.swift b/Sources/CodexBarCore/CostUsageFetcher.swift index 2243d5218..075ce954e 100644 --- a/Sources/CodexBarCore/CostUsageFetcher.swift +++ b/Sources/CodexBarCore/CostUsageFetcher.swift @@ -91,10 +91,16 @@ public struct CostUsageFetcher: Sendable { let totalTokensFromEntries = daily.data.compactMap(\.totalTokens).reduce(0, +) let last30DaysTokens = totalTokensFromSummary ?? (totalTokensFromEntries > 0 ? totalTokensFromEntries : nil) + let processedFromSummary = daily.summary?.processedTokens + let processedFromEntries = daily.data.compactMap(\.processedTokens).reduce(0, +) + let last30DaysProcessedTokens = processedFromSummary ?? (processedFromEntries > 0 ? processedFromEntries : nil) + return CostUsageTokenSnapshot( sessionTokens: currentDay?.totalTokens, + sessionProcessedTokens: currentDay?.processedTokens, sessionCostUSD: currentDay?.costUSD, last30DaysTokens: last30DaysTokens, + last30DaysProcessedTokens: last30DaysProcessedTokens, last30DaysCostUSD: last30DaysCostUSD, daily: daily.data, updatedAt: now) diff --git a/Sources/CodexBarCore/CostUsageModels.swift b/Sources/CodexBarCore/CostUsageModels.swift index 60f5e7598..a20b3b77f 100644 --- a/Sources/CodexBarCore/CostUsageModels.swift +++ b/Sources/CodexBarCore/CostUsageModels.swift @@ -2,23 +2,29 @@ import Foundation public struct CostUsageTokenSnapshot: Sendable, Equatable { public let sessionTokens: Int? + public let sessionProcessedTokens: Int? public let sessionCostUSD: Double? public let last30DaysTokens: Int? + public let last30DaysProcessedTokens: Int? public let last30DaysCostUSD: Double? public let daily: [CostUsageDailyReport.Entry] public let updatedAt: Date public init( sessionTokens: Int?, + sessionProcessedTokens: Int? = nil, sessionCostUSD: Double?, last30DaysTokens: Int?, + last30DaysProcessedTokens: Int? = nil, last30DaysCostUSD: Double?, daily: [CostUsageDailyReport.Entry], updatedAt: Date) { self.sessionTokens = sessionTokens + self.sessionProcessedTokens = sessionProcessedTokens self.sessionCostUSD = sessionCostUSD self.last30DaysTokens = last30DaysTokens + self.last30DaysProcessedTokens = last30DaysProcessedTokens self.last30DaysCostUSD = last30DaysCostUSD self.daily = daily self.updatedAt = updatedAt @@ -57,6 +63,7 @@ public struct CostUsageDailyReport: Sendable, Decodable { public let cacheCreationTokens: Int? public let outputTokens: Int? public let totalTokens: Int? + public let processedTokens: Int? public let costUSD: Double? public let modelsUsed: [String]? public let modelBreakdowns: [ModelBreakdown]? @@ -70,6 +77,7 @@ public struct CostUsageDailyReport: Sendable, Decodable { case cacheCreationInputTokens case outputTokens case totalTokens + case processedTokens case costUSD case totalCost case modelsUsed @@ -89,6 +97,7 @@ public struct CostUsageDailyReport: Sendable, Decodable { ?? container.decodeIfPresent(Int.self, forKey: .cacheCreationInputTokens) self.outputTokens = try container.decodeIfPresent(Int.self, forKey: .outputTokens) self.totalTokens = try container.decodeIfPresent(Int.self, forKey: .totalTokens) + self.processedTokens = try container.decodeIfPresent(Int.self, forKey: .processedTokens) self.costUSD = try container.decodeIfPresent(Double.self, forKey: .costUSD) ?? container.decodeIfPresent(Double.self, forKey: .totalCost) @@ -103,6 +112,7 @@ public struct CostUsageDailyReport: Sendable, Decodable { cacheReadTokens: Int? = nil, cacheCreationTokens: Int? = nil, totalTokens: Int?, + processedTokens: Int? = nil, costUSD: Double?, modelsUsed: [String]?, modelBreakdowns: [ModelBreakdown]?) @@ -113,6 +123,7 @@ public struct CostUsageDailyReport: Sendable, Decodable { self.cacheReadTokens = cacheReadTokens self.cacheCreationTokens = cacheCreationTokens self.totalTokens = totalTokens + self.processedTokens = processedTokens self.costUSD = costUSD self.modelsUsed = modelsUsed self.modelBreakdowns = modelBreakdowns @@ -142,6 +153,7 @@ public struct CostUsageDailyReport: Sendable, Decodable { public let cacheReadTokens: Int? public let cacheCreationTokens: Int? public let totalTokens: Int? + public let processedTokens: Int? public let totalCostUSD: Double? private enum CodingKeys: String, CodingKey { @@ -152,6 +164,7 @@ public struct CostUsageDailyReport: Sendable, Decodable { case totalCacheReadTokens case totalCacheCreationTokens case totalTokens + case processedTokens case totalCostUSD case totalCost } @@ -162,6 +175,7 @@ public struct CostUsageDailyReport: Sendable, Decodable { cacheReadTokens: Int? = nil, cacheCreationTokens: Int? = nil, totalTokens: Int?, + processedTokens: Int? = nil, totalCostUSD: Double?) { self.totalInputTokens = totalInputTokens @@ -169,6 +183,7 @@ public struct CostUsageDailyReport: Sendable, Decodable { self.cacheReadTokens = cacheReadTokens self.cacheCreationTokens = cacheCreationTokens self.totalTokens = totalTokens + self.processedTokens = processedTokens self.totalCostUSD = totalCostUSD } @@ -183,6 +198,7 @@ public struct CostUsageDailyReport: Sendable, Decodable { try container.decodeIfPresent(Int.self, forKey: .cacheCreationTokens) ?? container.decodeIfPresent(Int.self, forKey: .totalCacheCreationTokens) self.totalTokens = try container.decodeIfPresent(Int.self, forKey: .totalTokens) + self.processedTokens = try container.decodeIfPresent(Int.self, forKey: .processedTokens) self.totalCostUSD = try container.decodeIfPresent(Double.self, forKey: .totalCostUSD) ?? container.decodeIfPresent(Double.self, forKey: .totalCost) diff --git a/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner+Claude.swift b/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner+Claude.swift index 5e32060b0..b4e4aa6b0 100644 --- a/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner+Claude.swift +++ b/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner+Claude.swift @@ -485,6 +485,7 @@ extension CostUsageScanner { var totalCacheRead = 0 var totalCacheCreate = 0 var totalTokens = 0 + var totalProcessedTokens = 0 var totalCost: Double = 0 var costSeen = false let costScale = 1_000_000_000.0 @@ -539,6 +540,7 @@ extension CostUsageScanner { let top = Array(breakdown.prefix(3)) let dayTotal = dayInput + dayCacheRead + dayCacheCreate + dayOutput + let dayProcessed = dayInput + dayCacheCreate + dayOutput let entryCost = dayCostSeen ? dayCost : nil entries.append(CostUsageDailyReport.Entry( date: day, @@ -547,6 +549,7 @@ extension CostUsageScanner { cacheReadTokens: dayCacheRead, cacheCreationTokens: dayCacheCreate, totalTokens: dayTotal, + processedTokens: dayProcessed, costUSD: entryCost, modelsUsed: modelNames, modelBreakdowns: top)) @@ -556,6 +559,7 @@ extension CostUsageScanner { totalCacheRead += dayCacheRead totalCacheCreate += dayCacheCreate totalTokens += dayTotal + totalProcessedTokens += dayProcessed if let entryCost { totalCost += entryCost costSeen = true @@ -570,6 +574,7 @@ extension CostUsageScanner { cacheReadTokens: totalCacheRead, cacheCreationTokens: totalCacheCreate, totalTokens: totalTokens, + processedTokens: totalProcessedTokens, totalCostUSD: costSeen ? totalCost : nil) return CostUsageDailyReport(data: entries, summary: summary) diff --git a/Tests/CodexBarTests/CostUsageScannerTests.swift b/Tests/CodexBarTests/CostUsageScannerTests.swift index 98985bd6a..11d56d451 100644 --- a/Tests/CodexBarTests/CostUsageScannerTests.swift +++ b/Tests/CodexBarTests/CostUsageScannerTests.swift @@ -204,6 +204,7 @@ struct CostUsageScannerTests { #expect(report.data[0].cacheReadTokens == 25) #expect(report.data[0].outputTokens == 80) #expect(report.data[0].totalTokens == 355) + #expect(report.data[0].processedTokens == 330) #expect((report.data[0].costUSD ?? 0) > 0) } @@ -439,6 +440,7 @@ struct CostUsageScannerTests { now: day, options: options) #expect(firstReport.data.first?.totalTokens == 355) + #expect(firstReport.data.first?.processedTokens == 330) let second: [String: Any] = [ "type": "assistant", @@ -462,6 +464,7 @@ struct CostUsageScannerTests { now: day, options: options) #expect(secondReport.data.first?.totalTokens == 430) + #expect(secondReport.data.first?.processedTokens == 400) } @Test @@ -707,6 +710,7 @@ struct CostUsageScannerTests { #expect(report.data[0].cacheReadTokens == 25) #expect(report.data[0].outputTokens == 10) #expect(report.data[0].totalTokens == 185) + #expect(report.data[0].processedTokens == 160) } @Test