-
Notifications
You must be signed in to change notification settings - Fork 785
Expand file tree
/
Copy pathCostUsageCache.swift
More file actions
88 lines (75 loc) · 2.95 KB
/
CostUsageCache.swift
File metadata and controls
88 lines (75 loc) · 2.95 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
import Foundation
enum CostUsageCacheIO {
private static func artifactVersion(for provider: UsageProvider) -> Int {
switch provider {
case .codex:
3
case .claude, .vertexai:
2
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) -> CostUsageCache {
let url = self.cacheFileURL(provider: provider, cacheRoot: cacheRoot)
if let decoded = self.loadCache(at: url) { return decoded }
return CostUsageCache()
}
private static func loadCache(at url: URL) -> 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 }
return decoded
}
static func save(provider: UsageProvider, cache: CostUsageCache, cacheRoot: URL? = nil) {
let url = self.cacheFileURL(provider: provider, cacheRoot: cacheRoot)
let dir = url.deletingLastPathComponent()
try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
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])
_ = try FileManager.default.replaceItemAt(url, withItemAt: tmp)
} catch {
try? FileManager.default.removeItem(at: tmp)
}
}
}
struct CostUsageCache: Codable {
var version: Int = 1
var lastScanUnixMs: Int64 = 0
/// 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 sessionId: String?
var forkedFromId: String?
var claudeRows: [CostUsageScanner.ClaudeUsageRow]?
}
struct CostUsageCodexTotals: Codable {
var input: Int
var cached: Int
var output: Int
}