Files
2026-07-13 12:22:33 +08:00

163 lines
5.7 KiB
Swift

import Foundation
enum CostUsageCacheIO {
/// Producer keys from older parser hashes whose caches are still valid under the current
/// delta semantics. Cleared for #2037: interleave containment changed how cumulative
/// totals are counted, so every earlier cache must be rebuilt.
private static let compatibleCodexProducerKeys: Set<String> = []
private static func artifactVersion(for provider: UsageProvider) -> Int {
switch provider {
case .codex:
9
case .claude, .vertexai:
4
default:
1
}
}
private static func defaultCacheRoot() -> URL {
let root = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!
return root.appendingPathComponent("CodexBar", isDirectory: true)
}
static func cacheFileURL(provider: UsageProvider, cacheRoot: URL? = nil) -> URL {
let root = cacheRoot ?? self.defaultCacheRoot()
let artifactVersion = self.artifactVersion(for: provider)
return root
.appendingPathComponent("cost-usage", isDirectory: true)
.appendingPathComponent("\(provider.rawValue)-v\(artifactVersion).json", isDirectory: false)
}
static func load(
provider: UsageProvider,
cacheRoot: URL? = nil,
producerKey: String? = nil) -> CostUsageCache
{
let url = self.cacheFileURL(provider: provider, cacheRoot: cacheRoot)
let expectedProducerKey = producerKey ?? self.currentProducerKey(provider: provider)
let compatibleProducerKeys = producerKey == nil && provider == .codex
? self.compatibleCodexProducerKeys
: []
if let decoded = self.loadCache(
at: url,
expectedProducerKey: expectedProducerKey,
compatibleProducerKeys: compatibleProducerKeys)
{
return decoded
}
return CostUsageCache()
}
private static func loadCache(
at url: URL,
expectedProducerKey: String?,
compatibleProducerKeys: Set<String>) -> CostUsageCache?
{
guard let data = try? Data(contentsOf: url) else { return nil }
guard let decoded = try? JSONDecoder().decode(CostUsageCache.self, from: data)
else { return nil }
guard decoded.version == 1 else { return nil }
if let expectedProducerKey {
guard decoded.producerKey == expectedProducerKey
|| decoded.producerKey.map(compatibleProducerKeys.contains) == true
else { return nil }
}
return decoded
}
static func save(
provider: UsageProvider,
cache: CostUsageCache,
cacheRoot: URL? = nil,
producerKey: String? = nil)
{
let url = self.cacheFileURL(provider: provider, cacheRoot: cacheRoot)
let dir = url.deletingLastPathComponent()
try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
var cache = cache
cache.producerKey = producerKey ?? self.currentProducerKey(provider: provider)
let tmp = dir.appendingPathComponent(".tmp-\(UUID().uuidString).json", isDirectory: false)
let data = (try? JSONEncoder().encode(cache)) ?? Data()
do {
try data.write(to: tmp, options: [.atomic])
if FileManager.default.fileExists(atPath: url.path) {
_ = try FileManager.default.replaceItemAt(url, withItemAt: tmp)
} else {
try FileManager.default.moveItem(at: tmp, to: url)
}
} catch {
try? FileManager.default.removeItem(at: tmp)
}
}
static func currentProducerKey(
provider: UsageProvider,
parserHash: String = CodexParserHash.value) -> String?
{
guard provider == .codex else { return nil }
return "\(provider.rawValue):cu:p\(parserHash)"
}
}
struct CostUsageCache: Codable {
var version: Int = 1
var producerKey: String?
var lastScanUnixMs: Int64 = 0
var scanSinceKey: String?
var scanUntilKey: String?
var codexPricingKey: String?
var codexPriorityMetadataKey: String?
var codexProjectMetadataVersion: Int?
var codexPriorityTurnKeys: [String: String]?
var codexPriorityTurnIDsByDay: [String: [String]]?
/// filePath -> file usage
var files: [String: CostUsageFileUsage] = [:]
/// dayKey -> model -> packed usage
var days: [String: [String: [Int]]] = [:]
/// rootPath -> mtime (for Claude roots)
var roots: [String: Int64]?
}
struct CostUsageFileUsage: Codable {
var mtimeUnixMs: Int64
var size: Int64
var days: [String: [String: [Int]]]
var parsedBytes: Int64?
var lastModel: String?
var lastTotals: CostUsageCodexTotals?
var lastCountedTotals: CostUsageCodexTotals?
var lastRawTotalsBaseline: CostUsageCodexTotals?
var lastRawTotalsWatermark: CostUsageCodexTotals?
var seenRawTotals: [CostUsageCodexTotals]?
var hasDivergentTotals: Bool?
var hasInterleavedTotals: Bool?
var lastCodexTurnID: String?
var sessionId: String?
var forkedFromId: String?
var projectPath: String?
var canonicalProjectPath: String?
var codexCostCacheComplete: Bool?
var codexCostNanos: [String: [String: Int64]]?
var codexPrioritySurchargeNanos: [String: [String: Int64]]?
var codexStandardCostNanos: [String: [String: Int64]]?
var codexPriorityCostNanos: [String: [String: Int64]]?
var codexStandardTokens: [String: [String: Int]]?
var codexPriorityTokens: [String: [String: Int]]?
var codexTurnIDs: [String]?
var codexRows: [CostUsageScanner.CodexUsageRow]?
var claudeRows: [CostUsageScanner.ClaudeUsageRow]?
}
struct CostUsageCodexTotals: Codable, Equatable {
var input: Int
var cached: Int
var output: Int
}