2415 lines · 105.0 KB · Swift
RawDownload
#if canImport(CryptoKit)import CryptoKit#elseimport Crypto#endifimport Foundation // swiftlint:disable type_body_length file_lengthenum CostUsageScanner {    typealias CancellationCheck = () throws -> Void     static let log = CodexBarLog.logger(LogCategories.tokenCost)    static let codexActiveSessionLookbackDays = 30    static let costScale = 1_000_000_000.0     enum ClaudeLogProviderFilter {        case all        case vertexAIOnly        case excludeVertexAI    }     struct Options {        var codexSessionsRoot: URL?        var claudeProjectsRoots: [URL]?        var cacheRoot: URL?        var codexTraceDatabaseURL: URL?        var refreshMinIntervalSeconds: TimeInterval = 60        var claudeLogProviderFilter: ClaudeLogProviderFilter = .all        /// Force a full rescan, ignoring per-file cache and incremental offsets.        var forceRescan: Bool = false         init(            codexSessionsRoot: URL? = nil,            claudeProjectsRoots: [URL]? = nil,            cacheRoot: URL? = nil,            codexTraceDatabaseURL: URL? = nil,            claudeLogProviderFilter: ClaudeLogProviderFilter = .all,            forceRescan: Bool = false)        {            self.codexSessionsRoot = codexSessionsRoot            self.claudeProjectsRoots = claudeProjectsRoots            self.cacheRoot = cacheRoot            self.codexTraceDatabaseURL = codexTraceDatabaseURL            self.claudeLogProviderFilter = claudeLogProviderFilter            self.forceRescan = forceRescan        }    }     struct CodexParseResult {        let days: [String: [String: [Int]]]        var parsedBytes: Int64        let lastModel: String?        let lastTotals: CostUsageCodexTotals?        let lastCountedTotals: CostUsageCodexTotals?        let lastRawTotalsBaseline: CostUsageCodexTotals?        let hasDivergentTotals: Bool        let lastCodexTurnID: String?        let sessionId: String?        let forkedFromId: String?        let rows: [CodexUsageRow]    }     struct CodexUsageRow: Codable, Equatable {        let day: String        let model: String        let turnID: String?        let input: Int        let cached: Int        let output: Int    }     struct CodexScanState {        var seenSessionIds: Set<String> = []        var seenFileIds: Set<String> = []    }     private struct CodexTimestampedTotals {        let timestamp: String        let date: Date?        let totals: CostUsageCodexTotals    }     enum CodexForkBaseline {        case resolved(CostUsageCodexTotals?)        case unresolved    }     private static func codexTotalsEqual(_ lhs: CostUsageCodexTotals?, _ rhs: CostUsageCodexTotals?) -> Bool {        lhs?.input == rhs?.input && lhs?.cached == rhs?.cached && lhs?.output == rhs?.output    }     private static func codexTotalsAtLeast(_ lhs: CostUsageCodexTotals, _ rhs: CostUsageCodexTotals) -> Bool {        lhs.input >= rhs.input && lhs.cached >= rhs.cached && lhs.output >= rhs.output    }     private static func codexTotalsAtMost(_ lhs: CostUsageCodexTotals, _ rhs: CostUsageCodexTotals) -> Bool {        lhs.input <= rhs.input && lhs.cached <= rhs.cached && lhs.output <= rhs.output    }     private static func codexShouldPreferTotalDelta(        rawBaseline: CostUsageCodexTotals?,        currentTotal: CostUsageCodexTotals,        totalDelta: CostUsageCodexTotals,        lastDelta: CostUsageCodexTotals,        sawDivergentTotals: Bool) -> Bool    {        guard !sawDivergentTotals, let rawBaseline else { return false }        return Self.codexTotalsAtLeast(currentTotal, rawBaseline)            && Self.codexTotalsAtMost(totalDelta, lastDelta)    }     private static func codexAddTotals(        _ lhs: CostUsageCodexTotals,        _ rhs: CostUsageCodexTotals) -> CostUsageCodexTotals    {        CostUsageCodexTotals(            input: lhs.input + rhs.input,            cached: lhs.cached + rhs.cached,            output: lhs.output + rhs.output)    }     private static func codexMinTotals(        _ lhs: CostUsageCodexTotals,        _ rhs: CostUsageCodexTotals) -> CostUsageCodexTotals    {        CostUsageCodexTotals(            input: min(lhs.input, rhs.input),            cached: min(lhs.cached, rhs.cached),            output: min(lhs.output, rhs.output))    }     private static func codexTotalDelta(        from baseline: CostUsageCodexTotals?,        to current: CostUsageCodexTotals) -> CostUsageCodexTotals    {        let baseline = baseline ?? .init(input: 0, cached: 0, output: 0)        return CostUsageCodexTotals(            input: max(0, current.input - baseline.input),            cached: max(0, current.cached - baseline.cached),            output: max(0, current.output - baseline.output))    }     private static func codexDivergentTotalDelta(        rawBaseline: CostUsageCodexTotals?,        countedBaseline: CostUsageCodexTotals?,        current: CostUsageCodexTotals) -> CostUsageCodexTotals    {        let rawBaseline = rawBaseline ?? .init(input: 0, cached: 0, output: 0)        let countedBaseline = countedBaseline ?? .init(input: 0, cached: 0, output: 0)         func delta(raw: Int, counted: Int, current: Int) -> Int {            if current >= raw {                return max(0, current - raw)            }            return max(0, current - counted)        }         return CostUsageCodexTotals(            input: delta(raw: rawBaseline.input, counted: countedBaseline.input, current: current.input),            cached: delta(raw: rawBaseline.cached, counted: countedBaseline.cached, current: current.cached),            output: delta(raw: rawBaseline.output, counted: countedBaseline.output, current: current.output))    }     struct CodexScanResources {        let fileIndex: CodexSessionFileIndex        let inheritedResolver: CodexInheritedTotalsResolver        let modelsDevCatalog: ModelsDevCatalog?        let modelsDevCacheRoot: URL?        let priorityTurns: [String: CodexPriorityTurnMetadata]    }     struct CodexFileScanContext {        let range: CostUsageDayRange        let forceFullScan: Bool        let dropDeferredCodexRows: Bool        let requiresTurnIDCache: Bool        let changedPriorityTurnIDs: Set<String>        let resources: CodexScanResources        let checkCancellation: CancellationCheck?    }     struct CodexRefreshPlan {        let refreshMs: Int64        let roots: [URL]        let rootsFingerprint: [String: Int64]        let rootsChanged: Bool        let windowExpanded: Bool        let needsCostCacheMigration: Bool        let modelsDevCatalog: ModelsDevCatalog?        let codexPricingKey: String        let codexPriorityMetadataKey: String        let hasPriorityMetadata: Bool        let priorityTurns: [String: CodexPriorityTurnMetadata]        let priorityTurnKeys: [String: String]        let priorityTurnIDsByDay: [String: [String]]        let pricingChanged: Bool        let priorityMetadataChanged: Bool        let priorityTurnsChanged: Bool        let needsTurnIDCacheMigration: Bool        let changedPriorityTurnIDs: Set<String>        let shouldRefresh: Bool    }     final class CodexSessionFileIndex {        private let files: [URL]        private let filePaths: Set<String>        private let roots: [URL]        private let checkCancellation: CancellationCheck?        private var nextUnindexedFile = 0        private var didIndexRoots = false        private var fileURLBySessionId: [String: URL] = [:]        private var missingSessionIds: Set<String> = []         init(            files: [URL],            roots: [URL],            cachedSessionFiles: [String: URL] = [:],            checkCancellation: CancellationCheck? = nil)        {            self.files = files            self.filePaths = Set(files.map(\.path))            self.roots = roots            self.fileURLBySessionId = cachedSessionFiles            self.checkCancellation = checkCancellation        }         func remember(fileURL: URL, sessionId: String?) {            guard let sessionId, !sessionId.isEmpty else { return }            self.fileURLBySessionId[sessionId] = fileURL        }         func fileURL(for sessionId: String) throws -> URL? {            if let cached = self.fileURLBySessionId[sessionId] {                return cached            }            if self.missingSessionIds.contains(sessionId) {                return nil            }             while self.nextUnindexedFile < self.files.count {                try self.checkCancellation?()                let fileURL = self.files[self.nextUnindexedFile]                self.nextUnindexedFile += 1                guard let indexedSessionId = try CostUsageScanner.parseCodexSessionIdentifier(                    fileURL: fileURL,                    checkCancellation: self.checkCancellation)                else {                    continue                }                self.fileURLBySessionId[indexedSessionId] = fileURL                if indexedSessionId == sessionId {                    return fileURL                }            }             if !self.didIndexRoots {                try self.indexRoots()                if let indexed = self.fileURLBySessionId[sessionId] {                    return indexed                }            }             self.missingSessionIds.insert(sessionId)            return nil        }         private func indexRoots() throws {            self.didIndexRoots = true            guard !self.roots.isEmpty else { return }            for root in self.roots {                try self.checkCancellation?()                guard let enumerator = FileManager.default.enumerator(                    at: root,                    includingPropertiesForKeys: [.isRegularFileKey],                    options: [.skipsHiddenFiles, .skipsPackageDescendants])                else { continue }                 while let fileURL = enumerator.nextObject() as? URL {                    try self.checkCancellation?()                    guard fileURL.pathExtension.lowercased() == "jsonl" else { continue }                    guard !self.filePaths.contains(fileURL.path) else { continue }                    guard let indexedSessionId = try CostUsageScanner.parseCodexSessionIdentifier(                        fileURL: fileURL,                        checkCancellation: self.checkCancellation)                    else {                        continue                    }                    self.fileURLBySessionId[indexedSessionId] = fileURL                }            }        }    }     final class CodexInheritedTotalsResolver {        private let fileIndex: CodexSessionFileIndex        private let checkCancellation: CancellationCheck?        private var snapshotsBySessionId: [String: [CodexTimestampedTotals]] = [:]         init(fileIndex: CodexSessionFileIndex, checkCancellation: CancellationCheck?) {            self.fileIndex = fileIndex            self.checkCancellation = checkCancellation        }         func inheritedTotals(for sessionId: String, atOrBefore cutoffTimestamp: String) throws -> CodexForkBaseline {            guard !cutoffTimestamp.isEmpty else {                CostUsageScanner.log.warning(                    "Codex cost usage fork timestamp missing; treating parent baseline as unresolved",                    metadata: ["sessionId": sessionId])                return .unresolved            }            let cutoffDate = CostUsageScanner.dateFromTimestamp(cutoffTimestamp)            if cutoffDate == nil {                CostUsageScanner.log.warning(                    "Codex cost usage could not parse fork timestamp; falling back to lexical comparison",                    metadata: ["sessionId": sessionId, "timestamp": cutoffTimestamp])            }            guard let snapshots = try self.snapshots(for: sessionId) else { return .unresolved }            var inherited: CostUsageCodexTotals?            for snapshot in snapshots {                let isAtOrBefore: Bool = if let snapshotDate = snapshot.date, let cutoffDate {                    snapshotDate <= cutoffDate                } else {                    snapshot.timestamp <= cutoffTimestamp                }                if isAtOrBefore {                    inherited = snapshot.totals                }            }            return .resolved(inherited)        }         private func snapshots(for sessionId: String) throws -> [CodexTimestampedTotals]? {            if let cached = self.snapshotsBySessionId[sessionId] {                return cached            }            try self.checkCancellation?()            guard let fileURL = try self.fileIndex.fileURL(for: sessionId) else {                CostUsageScanner.log.warning(                    "Codex cost usage parent session file not found",                    metadata: ["sessionId": sessionId])                return nil            }            let parsed = try CostUsageScanner.parseCodexTokenSnapshots(                fileURL: fileURL,                checkCancellation: self.checkCancellation)            guard let parsedSessionId = parsed.sessionId else {                CostUsageScanner.log.warning(                    "Codex cost usage parent session missing session metadata",                    metadata: ["sessionId": sessionId, "path": fileURL.path])                return nil            }            if parsedSessionId != sessionId {                CostUsageScanner.log.warning(                    "Codex cost usage parent session resolved to mismatched session id",                    metadata: [                        "requestedSessionId": sessionId,                        "resolvedSessionId": parsedSessionId,                        "path": fileURL.path,                    ])                return nil            }            self.snapshotsBySessionId[sessionId] = parsed.snapshots            return parsed.snapshots        }    }     struct ClaudeParseResult {        let days: [String: [String: [Int]]]        let rows: [ClaudeUsageRow]        let parsedBytes: Int64    }     enum ClaudePathRole: String, Codable {        case parent        case subagent    }     struct ClaudeUsageRow: Codable {        let dayKey: String        let model: String        let sessionId: String?        let messageId: String?        let requestId: String?        let timestampUnixMs: Int64?        let isSidechain: Bool        let pathRole: ClaudePathRole        let input: Int        let cacheRead: Int        let cacheCreate: Int        let cacheCreate1h: Int?        let output: Int        let costNanos: Int        let costPriced: Bool?    }     static func loadDailyReport(        provider: UsageProvider,        since: Date,        until: Date,        now: Date = Date(),        options: Options = Options()) -> CostUsageDailyReport    {        (            try? self.loadDailyReportCancellable(                provider: provider,                since: since,                until: until,                now: now,                options: options,                checkCancellation: nil)) ?? CostUsageDailyReport(data: [], summary: nil)    }     static func loadDailyReportCancellable(        provider: UsageProvider,        since: Date,        until: Date,        now: Date = Date(),        options: Options = Options(),        checkCancellation: CancellationCheck?) throws -> CostUsageDailyReport    {        let range = CostUsageDayRange(since: since, until: until)        let emptyReport = CostUsageDailyReport(data: [], summary: nil)        try checkCancellation?()         switch provider {        case .codex:            return try self.loadCodexDaily(                range: range,                now: now,                options: options,                checkCancellation: checkCancellation)        case .claude:            return try self.loadClaudeDaily(                provider: .claude,                range: range,                now: now,                options: options,                checkCancellation: checkCancellation)        case .vertexai:            var filtered = options            if filtered.claudeLogProviderFilter == .all {                filtered.claudeLogProviderFilter = .vertexAIOnly            }            return try self.loadClaudeDaily(                provider: .vertexai,                range: range,                now: now,                options: filtered,                checkCancellation: checkCancellation)        case .openai, .azureopenai, .zai, .gemini, .antigravity, .cursor, .opencode, .opencodego, .alibaba,             .alibabatokenplan, .factory,             .copilot, .devin, .minimax, .manus, .kilo, .kiro, .kimi, .kimik2, .moonshot, .augment, .jetbrains, .amp,             .ollama, .t3chat, .synthetic, .openrouter, .elevenlabs, .warp, .perplexity, .mimo, .doubao, .abacus,             .mistral, .deepseek, .codebuff, .crof, .windsurf, .zed, .venice, .commandcode, .stepfun, .bedrock, .grok,             .groq, .llmproxy, .litellm, .deepgram, .poe, .chutes:            return emptyReport        }    }     // MARK: - Day keys     struct CostUsageDayRange {        let sinceKey: String        let untilKey: String        let scanSinceKey: String        let scanUntilKey: String         init(since: Date, until: Date) {            self.sinceKey = Self.dayKey(from: since)            self.untilKey = Self.dayKey(from: until)            self.scanSinceKey = Self.dayKey(from: Calendar.current.date(byAdding: .day, value: -1, to: since) ?? since)            self.scanUntilKey = Self.dayKey(from: Calendar.current.date(byAdding: .day, value: 1, to: until) ?? until)        }         static func dayKey(from date: Date) -> String {            let cal = Calendar.current            let comps = cal.dateComponents([.year, .month, .day], from: date)            let y = comps.year ?? 1970            let m = comps.month ?? 1            let d = comps.day ?? 1            return String(format: "%04d-%02d-%02d", y, m, d)        }         static func isInRange(dayKey: String, since: String, until: String) -> Bool {            if dayKey < since { return false }            if dayKey > until { return false }            return true        }    }     // MARK: - Codex     private static func defaultCodexSessionsRoot(options: Options) -> URL {        if let override = options.codexSessionsRoot { return override }        let env = ProcessInfo.processInfo.environment["CODEX_HOME"]?.trimmingCharacters(in: .whitespacesAndNewlines)        if let env, !env.isEmpty {            return URL(fileURLWithPath: env).appendingPathComponent("sessions", isDirectory: true)        }        return FileManager.default.homeDirectoryForCurrentUser            .appendingPathComponent(".codex", isDirectory: true)

Lines 1–500 of 2415.