// // RequestLogStore.swift // Zyquo Router // // Author: Simon-Pierre Boucher // Mail: contact@spboucher.ai // // Live traffic log: a bounded in-memory ring of RequestLogEntry. Bodies are // carried in memory but the UI redacts them by default (explicit per-session // reveal). Never persisted to disk unless the user exports; provider keys // never enter an entry. // import Foundation /// One routed request as shown in the Requests screen. struct RequestLogEntry: Identifiable, Sendable { let id: UUID var date: Date var method: String var path: String var namespacedModelID: String var provider: ProviderID var status: Int var streamed: Bool var latency: TimeInterval /// Time to first upstream byte (streaming) — nil for buffered calls. var upstreamTTFB: TimeInterval? var usage: TokenUsage var usageEstimated: Bool var estimatedCost: Double? var localKeyName: String? var errorMessage: String? /// Pretty-printed client request JSON (redacted in UI by default). var requestBody: String /// Pretty-printed response JSON, or the accumulated stream preview. var responseBody: String init( id: UUID = UUID(), date: Date = Date(), method: String = "POST", path: String = "/v1/chat/completions", namespacedModelID: String, provider: ProviderID, status: Int, streamed: Bool, latency: TimeInterval, upstreamTTFB: TimeInterval? = nil, usage: TokenUsage = TokenUsage(), usageEstimated: Bool = false, estimatedCost: Double? = nil, localKeyName: String? = nil, errorMessage: String? = nil, requestBody: String = "", responseBody: String = "" ) { self.id = id self.date = date self.method = method self.path = path self.namespacedModelID = namespacedModelID self.provider = provider self.status = status self.streamed = streamed self.latency = latency self.upstreamTTFB = upstreamTTFB self.usage = usage self.usageEstimated = usageEstimated self.estimatedCost = estimatedCost self.localKeyName = localKeyName self.errorMessage = errorMessage self.requestBody = requestBody self.responseBody = responseBody } } actor RequestLogStore { /// Ring capacity — old entries fall off the front. static let capacity = 500 private(set) var entries: [RequestLogEntry] = [] /// Monotonic revision so the UI can cheaply detect changes. private(set) var revision = 0 func append(_ entry: RequestLogEntry) { entries.append(entry) if entries.count > Self.capacity { entries.removeFirst(entries.count - Self.capacity) } revision += 1 } func clear() { entries.removeAll() revision += 1 } /// Metadata-only JSON export (bodies excluded by design). func exportJSON() -> Data { let rows = entries.map { entry -> [String: Any] in [ "date": ISO8601DateFormatter().string(from: entry.date), "model": entry.namespacedModelID, "provider": entry.provider.rawValue, "status": entry.status, "streamed": entry.streamed, "latency_ms": Int(entry.latency * 1000), "ttfb_ms": entry.upstreamTTFB.map { Int($0 * 1000) } as Any, "input_tokens": entry.usage.inputTokens, "output_tokens": entry.usage.outputTokens, "usage_estimated": entry.usageEstimated, "cost_usd": entry.estimatedCost as Any, "key": entry.localKeyName as Any, "error": entry.errorMessage as Any, ] } return (try? JSONSerialization.data(withJSONObject: rows, options: [.prettyPrinted, .sortedKeys])) ?? Data("[]".utf8) } }