chore: add TokenLens sources and ignore rules
This commit is contained in:
105
NativeTokenLens/Sources/TokenLensCore/TokenUsageParser.swift
Normal file
105
NativeTokenLens/Sources/TokenLensCore/TokenUsageParser.swift
Normal file
@@ -0,0 +1,105 @@
|
||||
import Foundation
|
||||
|
||||
public enum TokenUsageParser {
|
||||
public static func parseClaudeLine(_ line: String, sourcePath: String) -> UsageRecord? {
|
||||
guard let object = parseJSONObject(line) else { return nil }
|
||||
let message = object["message"] as? [String: Any]
|
||||
let usage = message?["usage"] as? [String: Any] ?? object["usage"] as? [String: Any]
|
||||
guard let usage else { return nil }
|
||||
guard let timestamp = parseDate(object["timestamp"] ?? object["createdAt"] ?? object["date"]) else { return nil }
|
||||
|
||||
return UsageRecord(
|
||||
provider: .claude,
|
||||
source: sourceLabel(sourcePath: sourcePath, provider: .claude),
|
||||
sourcePath: sourcePath,
|
||||
timestamp: timestamp,
|
||||
sessionID: stringValue(object["sessionId"]),
|
||||
model: stringValue(message?["model"]) == "" ? "Claude" : stringValue(message?["model"]),
|
||||
inputTokens: intValue(usage["input_tokens"]),
|
||||
outputTokens: intValue(usage["output_tokens"]),
|
||||
cachedTokens: intValue(usage["cache_read_input_tokens"]) + intValue(usage["cache_creation_input_tokens"]),
|
||||
reasoningTokens: 0,
|
||||
toolTokens: 0
|
||||
)
|
||||
}
|
||||
|
||||
public static func parseCodexLine(_ line: String, sourcePath: String) -> UsageRecord? {
|
||||
guard let object = parseJSONObject(line),
|
||||
let payload = object["payload"] as? [String: Any],
|
||||
let info = payload["info"] as? [String: Any],
|
||||
let usage = info["last_token_usage"] as? [String: Any],
|
||||
let timestamp = parseDate(object["timestamp"])
|
||||
else { return nil }
|
||||
|
||||
return UsageRecord(
|
||||
provider: .codex,
|
||||
source: sourceLabel(sourcePath: sourcePath, provider: .codex),
|
||||
sourcePath: sourcePath,
|
||||
timestamp: timestamp,
|
||||
sessionID: stringValue(payload["id"] ?? payload["thread_id"]),
|
||||
model: stringValue(payload["model"]) == "" ? "Codex" : stringValue(payload["model"]),
|
||||
inputTokens: intValue(usage["input_tokens"]),
|
||||
outputTokens: intValue(usage["output_tokens"]),
|
||||
cachedTokens: intValue(usage["cached_input_tokens"]),
|
||||
reasoningTokens: intValue(usage["reasoning_output_tokens"]),
|
||||
toolTokens: 0
|
||||
)
|
||||
}
|
||||
|
||||
public static func parseGeminiLine(_ line: String, sourcePath: String) -> UsageRecord? {
|
||||
guard let object = parseJSONObject(line),
|
||||
let tokens = object["tokens"] as? [String: Any],
|
||||
let timestamp = parseDate(object["timestamp"])
|
||||
else { return nil }
|
||||
|
||||
return UsageRecord(
|
||||
provider: .gemini,
|
||||
source: sourceLabel(sourcePath: sourcePath, provider: .gemini),
|
||||
sourcePath: sourcePath,
|
||||
timestamp: timestamp,
|
||||
sessionID: stringValue(object["id"]),
|
||||
model: stringValue(object["model"]) == "" ? "Gemini" : stringValue(object["model"]),
|
||||
inputTokens: intValue(tokens["input"]),
|
||||
outputTokens: intValue(tokens["output"]),
|
||||
cachedTokens: intValue(tokens["cached"]),
|
||||
reasoningTokens: intValue(tokens["thoughts"]),
|
||||
toolTokens: intValue(tokens["tool"]),
|
||||
explicitTotal: intValue(tokens["total"]) == 0 ? nil : intValue(tokens["total"])
|
||||
)
|
||||
}
|
||||
|
||||
static func parseDate(_ value: Any?) -> Date? {
|
||||
guard let text = value as? String else { return nil }
|
||||
let isoFormatter = ISO8601DateFormatter()
|
||||
isoFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
||||
let fallbackISOFormatter = ISO8601DateFormatter()
|
||||
fallbackISOFormatter.formatOptions = [.withInternetDateTime]
|
||||
return isoFormatter.date(from: text) ?? fallbackISOFormatter.date(from: text)
|
||||
}
|
||||
|
||||
private static func parseJSONObject(_ line: String) -> [String: Any]? {
|
||||
guard let data = line.data(using: .utf8) else { return nil }
|
||||
return (try? JSONSerialization.jsonObject(with: data)) as? [String: Any]
|
||||
}
|
||||
|
||||
private static func stringValue(_ value: Any?) -> String {
|
||||
value as? String ?? ""
|
||||
}
|
||||
|
||||
private static func intValue(_ value: Any?) -> Int {
|
||||
if let int = value as? Int { return int }
|
||||
if let double = value as? Double { return Int(double) }
|
||||
if let string = value as? String { return Int(string) ?? 0 }
|
||||
return 0
|
||||
}
|
||||
|
||||
private static func sourceLabel(sourcePath: String, provider: Provider) -> String {
|
||||
switch provider {
|
||||
case .claude: return "projects/*.jsonl"
|
||||
case .codex: return "sessions/*.jsonl"
|
||||
case .gemini: return "tmp/chats/*.jsonl"
|
||||
case .hermes: return "state.db/sessions"
|
||||
case .opencode: return "opencode.db/session"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user