924 lines · 37.7 KB · Swift
RawDownload
import Foundation public enum CostUsageError: LocalizedError, Sendable {    case unsupportedProvider(UsageProvider)    case timedOut(seconds: Int)    case cursorPaginationIncomplete(expected: Int?, received: Int)    case cursorPaginationInconsistent(expected: Int, received: Int)     public var errorDescription: String? {        switch self {        case let .unsupportedProvider(provider):            return "Cost summary is not supported for \(provider.rawValue)."        case let .timedOut(seconds):            if seconds >= 60, seconds % 60 == 0 {                return "Cost refresh timed out after \(seconds / 60)m."            }            return "Cost refresh timed out after \(seconds)s."        case let .cursorPaginationIncomplete(expected, received):            if let expected {                return "Cursor cost refresh was incomplete (received \(received) of \(expected) events)."            }            return "Cursor cost refresh reached its pagination safety limit after \(received) events."        case let .cursorPaginationInconsistent(expected, received):            return "Cursor cost pagination was inconsistent (expected \(expected), received \(received) events)."        }    }} public struct CostUsageFetcher: Sendable {    package struct CachedCodexTokenSnapshotResult: Sendable {        package let snapshot: CostUsageTokenSnapshot        package let lastRefreshAt: Date?    }     private let scannerOptions: CostUsageScanner.Options?     public init(cacheRoot: URL? = nil) {        self.scannerOptions = cacheRoot.map { CostUsageScanner.Options(cacheRoot: $0) }    }     init(scannerOptions: CostUsageScanner.Options) {        self.scannerOptions = scannerOptions    }     public func loadCachedCodexTokenSnapshot(        now: Date = Date(),        codexHomePath: String? = nil,        historyDays: Int = 30) async -> CostUsageTokenSnapshot?    {        await Self.loadCachedCodexTokenSnapshot(            now: now,            codexHomePath: codexHomePath,            historyDays: historyDays,            scannerOptions: self.scannerOptionsOverride())    }     package func loadCachedCodexTokenSnapshotResult(        now: Date = Date(),        codexHomePath: String? = nil,        historyDays: Int = 30) async -> CachedCodexTokenSnapshotResult?    {        await Self.loadCachedCodexTokenSnapshotResult(            now: now,            codexHomePath: codexHomePath,            historyDays: historyDays,            scannerOptions: self.scannerOptionsOverride())    }     public func loadTokenSnapshot(        provider: UsageProvider,        environment: [String: String] = ProcessInfo.processInfo.environment,        now: Date = Date(),        forceRefresh: Bool = false,        allowVertexClaudeFallback: Bool = false,        codexHomePath: String? = nil,        historyDays: Int = 30,        cursorCookieHeaderOverride: String? = nil,        allowPricingRefresh: Bool = true,        refreshPricingInBackground: Bool = true,        includePiSessions: Bool = true) async throws -> CostUsageTokenSnapshot    {        try await Self.loadTokenSnapshot(            provider: provider,            environment: environment,            now: now,            forceRefresh: forceRefresh,            allowVertexClaudeFallback: allowVertexClaudeFallback,            codexHomePath: codexHomePath,            historyDays: historyDays,            cursorCookieHeaderOverride: cursorCookieHeaderOverride,            allowPricingRefresh: allowPricingRefresh,            refreshPricingInBackground: refreshPricingInBackground,            includePiSessions: includePiSessions,            bypassScannerDebounce: false,            scannerOptions: self.scannerOptionsOverride())    }     package func loadTokenSnapshot(        provider: UsageProvider,        environment: [String: String] = ProcessInfo.processInfo.environment,        now: Date = Date(),        forceRefresh: Bool = false,        allowVertexClaudeFallback: Bool = false,        codexHomePath: String? = nil,        historyDays: Int = 30,        cursorCookieHeaderOverride: String? = nil,        allowPricingRefresh: Bool = true,        refreshPricingInBackground: Bool = true,        includePiSessions: Bool = true,        bypassScannerDebounce: Bool) async throws -> CostUsageTokenSnapshot    {        try await Self.loadTokenSnapshot(            provider: provider,            environment: environment,            now: now,            forceRefresh: forceRefresh,            allowVertexClaudeFallback: allowVertexClaudeFallback,            codexHomePath: codexHomePath,            historyDays: historyDays,            cursorCookieHeaderOverride: cursorCookieHeaderOverride,            allowPricingRefresh: allowPricingRefresh,            refreshPricingInBackground: refreshPricingInBackground,            includePiSessions: includePiSessions,            bypassScannerDebounce: bypassScannerDebounce,            scannerOptions: self.scannerOptionsOverride())    }     @available(*, deprecated, message: "Codex token-cost scans are uncapped; this limit is ignored.")    public func loadTokenSnapshot(        provider: UsageProvider,        environment: [String: String] = ProcessInfo.processInfo.environment,        now: Date = Date(),        forceRefresh: Bool = false,        allowVertexClaudeFallback: Bool = false,        codexHomePath: String? = nil,        historyDays: Int = 30,        allowPricingRefresh: Bool = true,        refreshPricingInBackground: Bool = true,        automaticCodexScanByteLimit _: Int64?) async throws -> CostUsageTokenSnapshot    {        try await self.loadTokenSnapshot(            provider: provider,            environment: environment,            now: now,            forceRefresh: forceRefresh,            allowVertexClaudeFallback: allowVertexClaudeFallback,            codexHomePath: codexHomePath,            historyDays: historyDays,            allowPricingRefresh: allowPricingRefresh,            refreshPricingInBackground: refreshPricingInBackground)    }     private func scannerOptionsOverride() -> CostUsageScanner.Options? {        self.scannerOptions    }     private static func resolvedScannerOptions(        _ override: CostUsageScanner.Options?,        provider: UsageProvider,        codexHomePath: String?) -> CostUsageScanner.Options    {        var options = override ?? CostUsageScanner.Options()        if provider == .codex,           let codexHomePath = codexHomePath?.trimmingCharacters(in: .whitespacesAndNewlines),           !codexHomePath.isEmpty        {            options.codexSessionsRoot = URL(fileURLWithPath: codexHomePath, isDirectory: true)                .appendingPathComponent("sessions", isDirectory: true)        }        return options    }     static func loadTokenSnapshot(        provider: UsageProvider,        environment: [String: String] = ProcessInfo.processInfo.environment,        now: Date = Date(),        forceRefresh: Bool = false,        allowVertexClaudeFallback: Bool = false,        codexHomePath: String? = nil,        historyDays: Int = 30,        cursorCookieHeaderOverride: String? = nil,        allowPricingRefresh: Bool = true,        refreshPricingInBackground: Bool = true,        includePiSessions: Bool = true,        bypassScannerDebounce: Bool = false,        scannerOptions overrideScannerOptions: CostUsageScanner.Options? = nil,        piScannerOptions overridePiScannerOptions: PiSessionCostScanner            .Options? = nil,        modelsDevClient: ModelsDevClient = ModelsDevClient(),        retryUnknownPricing: Bool = true) async throws -> CostUsageTokenSnapshot    {        guard self.supportsTokenSnapshot(provider) else {            throw CostUsageError.unsupportedProvider(provider)        }         let until = now        let clampedHistoryDays = max(1, min(365, historyDays))        // Rolling window is inclusive, so a 30-day display starts 29 days before `now`.        let since = Calendar.current.date(byAdding: .day, value: -(clampedHistoryDays - 1), to: now) ?? now         if let remoteSnapshot = try await self.loadRemoteTokenSnapshot(            provider: provider,            environment: environment,            now: now,            historyDays: clampedHistoryDays,            cursorCookieHeaderOverride: cursorCookieHeaderOverride)        {            return remoteSnapshot        }         var options = Self.resolvedScannerOptions(            overrideScannerOptions,            provider: provider,            codexHomePath: codexHomePath)        let scopedCodexHomePath = codexHomePath?.trimmingCharacters(in: .whitespacesAndNewlines)        let shouldMergePiUsage = provider != .codex || scopedCodexHomePath?.isEmpty != false        await Self.refreshPricingIfAllowed(            options: PricingRefreshOptions(                provider: provider,                isAllowed: allowPricingRefresh,                retryUnknown: retryUnknownPricing,                inBackground: refreshPricingInBackground),            now: now,            cacheRoot: options.cacheRoot,            client: modelsDevClient)         if provider == .vertexai {            options.claudeLogProviderFilter = allowVertexClaudeFallback ? .all : .vertexAIOnly        } else if provider == .claude {            options.claudeLogProviderFilter = .excludeVertexAI        }        if forceRefresh || bypassScannerDebounce {            options.refreshMinIntervalSeconds = 0        }        var resolvedPiOptions = overridePiScannerOptions ?? PiSessionCostScanner.Options()        if resolvedPiOptions.cacheRoot == nil {            resolvedPiOptions.cacheRoot = options.cacheRoot        }        if forceRefresh || bypassScannerDebounce {            resolvedPiOptions.refreshMinIntervalSeconds = 0        }        let piOptions = resolvedPiOptions         try Task.checkCancellation()        // The corpus scans below are synchronous and can run for minutes on large session        // archives. They execute on the dedicated scan queue so they never occupy a cooperative        // pool thread; CostUsageScanExecutor bridges this task's cancellation into the        // scanner-level checks.        let scanOptions = options        let scanResult = try await CostUsageScanExecutor.run { checkCancellation in            var daily = try CostUsageScanner.loadDailyReportCancellable(                provider: provider,                since: since,                until: until,                now: now,                options: scanOptions,                checkCancellation: checkCancellation)            try checkCancellation()             if provider == .vertexai,               !allowVertexClaudeFallback,               scanOptions.claudeLogProviderFilter == .vertexAIOnly,               daily.data.isEmpty            {                var fallback = scanOptions                fallback.claudeLogProviderFilter = .all                daily = try CostUsageScanner.loadDailyReportCancellable(                    provider: provider,                    since: since,                    until: until,                    now: now,                    options: fallback,                    checkCancellation: checkCancellation)                try checkCancellation()            }             var projects: [CostUsageProjectBreakdown] = []            var sessions: [CostUsageSessionBreakdown] = []            var piDaily: CostUsageDailyReport?            if provider == .codex {                let roots = CostUsageScanner.codexSessionsRoots(options: scanOptions)                let cache = CostUsageScanner.codexCache(                    CostUsageCacheIO.load(provider: .codex, cacheRoot: scanOptions.cacheRoot),                    scopedTo: roots)                let range = CostUsageScanner.CostUsageDayRange(since: since, until: until)                projects = CostUsageScanner.buildCodexProjectBreakdownsFromCache(                    cache: cache,                    range: range,                    modelsDevCacheRoot: scanOptions.cacheRoot)                sessions = CostUsageScanner.buildCodexSessionBreakdownsFromCache(                    cache: cache,                    range: range,                    modelsDevCacheRoot: scanOptions.cacheRoot,                    sessionRoots: roots)            }            if includePiSessions, provider == .claude || (provider == .codex && shouldMergePiUsage) {                let piReport = try PiSessionCostScanner.loadDailyReportCancellable(                    provider: provider,                    since: since,                    until: until,                    now: now,                    options: piOptions,                    checkCancellation: checkCancellation)                try checkCancellation()                if provider == .codex {                    piDaily = piReport                }                daily = CostUsageDailyReport.merged([daily, piReport])            }            if provider == .codex {                projects = Self.mergedProjectBreakdowns(                    projects + [piDaily.flatMap(Self.unknownProjectBreakdown(from:))].compactMap(\.self))                if piDaily?.data.isEmpty == false {                    sessions = []                }            }            return (daily: daily, projects: projects, sessions: sessions)        }         if allowPricingRefresh,           retryUnknownPricing,           let request = Self.unknownPricingRefreshRequest(               provider: provider,               daily: scanResult.daily,               now: now,               cacheRoot: options.cacheRoot,               client: modelsDevClient),           await Self.refreshUnknownPricingIfNeeded(request, inBackground: refreshPricingInBackground)        {            return try await self.loadTokenSnapshot(                provider: provider,                environment: environment,                now: now,                forceRefresh: forceRefresh,                allowVertexClaudeFallback: allowVertexClaudeFallback,                codexHomePath: codexHomePath,                historyDays: historyDays,                cursorCookieHeaderOverride: cursorCookieHeaderOverride,                allowPricingRefresh: allowPricingRefresh,                refreshPricingInBackground: false,                includePiSessions: includePiSessions,                scannerOptions: options,                piScannerOptions: piOptions,                modelsDevClient: modelsDevClient,                retryUnknownPricing: false)        }         return Self.tokenSnapshot(            from: scanResult.daily,            now: now,            historyDays: clampedHistoryDays,            projects: scanResult.projects,            sessions: scanResult.sessions)    }     private struct PricingRefreshOptions: Sendable {        let provider: UsageProvider        let isAllowed: Bool        let retryUnknown: Bool        let inBackground: Bool    }     private static func refreshPricingIfAllowed(        options: PricingRefreshOptions,        now: Date,        cacheRoot: URL?,        client: ModelsDevClient) async    {        guard options.isAllowed,              options.retryUnknown,              options.provider == .codex || options.provider == .claude        else { return }         if options.inBackground {            Task.detached(priority: .utility) {                await ModelsDevPricingPipeline.refreshIfNeeded(now: now, cacheRoot: cacheRoot, client: client)            }        } else {            await ModelsDevPricingPipeline.refreshIfNeeded(now: now, cacheRoot: cacheRoot, client: client)        }    }     private struct UnknownPricingRefreshRequest: Sendable {        let providerID: String        let modelIDs: Set<String>        let now: Date        let cacheRoot: URL?        let client: ModelsDevClient    }     private static func unknownPricingRefreshRequest(        provider: UsageProvider,        daily: CostUsageDailyReport,        now: Date,        cacheRoot: URL?,        client: ModelsDevClient) -> UnknownPricingRefreshRequest?    {        guard provider == .codex || provider == .claude else { return nil }        let unknownModelIDs = Set(daily.data.flatMap { entry in            entry.modelBreakdowns?.compactMap { breakdown -> String? in                guard breakdown.costUSD == nil else { return nil }                if provider == .codex,                   CostUsagePricing.isCodexUnattributedModel(breakdown.modelName)                {                    return nil                }                return breakdown.modelName            } ?? []        })        guard !unknownModelIDs.isEmpty else { return nil }         return UnknownPricingRefreshRequest(            providerID: provider == .codex ? "openai" : "anthropic",            modelIDs: unknownModelIDs,            now: now,            cacheRoot: cacheRoot,            client: client)    }     private static func refreshUnknownPricingIfNeeded(        _ request: UnknownPricingRefreshRequest,        inBackground: Bool) async -> Bool    {        if inBackground {            Task.detached(priority: .utility) {                _ = await ModelsDevPricingPipeline.refreshForUnknownModelsIfNeeded(                    providerID: request.providerID,                    modelIDs: request.modelIDs,                    now: request.now,                    cacheRoot: request.cacheRoot,                    client: request.client)            }            return false        }        return await ModelsDevPricingPipeline.refreshForUnknownModelsIfNeeded(            providerID: request.providerID,            modelIDs: request.modelIDs,            now: request.now,            cacheRoot: request.cacheRoot,            client: request.client) == .pricingAvailable    }     static func loadCachedCodexTokenSnapshot(        now: Date = Date(),        codexHomePath: String? = nil,        historyDays: Int = 30,        scannerOptions overrideScannerOptions: CostUsageScanner.Options? = nil) async -> CostUsageTokenSnapshot?    {        await self.loadCachedCodexTokenSnapshotResult(            now: now,            codexHomePath: codexHomePath,            historyDays: historyDays,            scannerOptions: overrideScannerOptions)?.snapshot    }     static func loadCachedCodexTokenSnapshotResult(        now: Date = Date(),        codexHomePath: String? = nil,        historyDays: Int = 30,        scannerOptions overrideScannerOptions: CostUsageScanner.Options? = nil) async        -> CachedCodexTokenSnapshotResult?    {        if let codexHomePath = codexHomePath?.trimmingCharacters(in: .whitespacesAndNewlines),           !codexHomePath.isEmpty        {            return nil        }         // Decoding the persisted scan cache parses multi-megabyte JSON; keep it off the        // cooperative pool alongside the scans themselves.        let cachedSnapshot: CachedCodexTokenSnapshotResult?? = try? await CostUsageScanExecutor.run { _ in            let clampedHistoryDays = max(1, min(365, historyDays))            let until = now            let since = Calendar.current.date(byAdding: .day, value: -(clampedHistoryDays - 1), to: now) ?? now            let range = CostUsageScanner.CostUsageDayRange(since: since, until: until)            let options = overrideScannerOptions ?? CostUsageScanner.Options()            let roots = CostUsageScanner.codexSessionsRoots(options: options)            let cache = CostUsageScanner.codexCache(                CostUsageCacheIO.load(provider: .codex, cacheRoot: options.cacheRoot),                scopedTo: roots)            var reports: [CostUsageDailyReport] = []            var projects: [CostUsageProjectBreakdown] = []            var sessions: [CostUsageSessionBreakdown] = []            // Raw inputs for the derived result fields below: the native cache's own scan            // time, every constituent scan time, and whether a second source joined the merge.            var nativeScanAt: Date?            var scanTimes: [Date] = []            var piMerged = false             if !cache.days.isEmpty,               cache.roots == CostUsageScanner.codexRootsFingerprint(options: options),               !CostUsageScanner.requestedWindowExpandsCache(range: range, cache: cache)            {                let daily = CostUsageScanner.buildCodexReportFromCache(                    cache: cache,                    range: range,                    modelsDevCacheRoot: options.cacheRoot)                if !daily.data.isEmpty {                    reports.append(daily)                    if cache.lastScanUnixMs > 0 {

Lines 1–500 of 924.