SPB Git

spb/zyquo-router Public MIT

One local endpoint, every AI provider — a private OpenAI-compatible LLM gateway for your Mac (170 models, 12 providers).

Swift 95.7% Python 2.3% Shell 1.2% Makefile 0.9%
3.9 KB · 121 lines swift
Raw Blame History
1//2//  RequestLogStore.swift3//  Zyquo Router4//5//  Author: Simon-Pierre Boucher6//  Mail: contact@spboucher.ai7//8//  Live traffic log: a bounded in-memory ring of RequestLogEntry. Bodies are9//  carried in memory but the UI redacts them by default (explicit per-session10//  reveal). Never persisted to disk unless the user exports; provider keys11//  never enter an entry.12//1314import Foundation1516/// One routed request as shown in the Requests screen.17struct RequestLogEntry: Identifiable, Sendable {18    let id: UUID19    var date: Date20    var method: String21    var path: String22    var namespacedModelID: String23    var provider: ProviderID24    var status: Int25    var streamed: Bool26    var latency: TimeInterval27    /// Time to first upstream byte (streaming) — nil for buffered calls.28    var upstreamTTFB: TimeInterval?29    var usage: TokenUsage30    var usageEstimated: Bool31    var estimatedCost: Double?32    var localKeyName: String?33    var errorMessage: String?34    /// Pretty-printed client request JSON (redacted in UI by default).35    var requestBody: String36    /// Pretty-printed response JSON, or the accumulated stream preview.37    var responseBody: String3839    init(40        id: UUID = UUID(),41        date: Date = Date(),42        method: String = "POST",43        path: String = "/v1/chat/completions",44        namespacedModelID: String,45        provider: ProviderID,46        status: Int,47        streamed: Bool,48        latency: TimeInterval,49        upstreamTTFB: TimeInterval? = nil,50        usage: TokenUsage = TokenUsage(),51        usageEstimated: Bool = false,52        estimatedCost: Double? = nil,53        localKeyName: String? = nil,54        errorMessage: String? = nil,55        requestBody: String = "",56        responseBody: String = ""57    ) {58        self.id = id59        self.date = date60        self.method = method61        self.path = path62        self.namespacedModelID = namespacedModelID63        self.provider = provider64        self.status = status65        self.streamed = streamed66        self.latency = latency67        self.upstreamTTFB = upstreamTTFB68        self.usage = usage69        self.usageEstimated = usageEstimated70        self.estimatedCost = estimatedCost71        self.localKeyName = localKeyName72        self.errorMessage = errorMessage73        self.requestBody = requestBody74        self.responseBody = responseBody75    }76}7778actor RequestLogStore {79    /// Ring capacity — old entries fall off the front.80    static let capacity = 5008182    private(set) var entries: [RequestLogEntry] = []83    /// Monotonic revision so the UI can cheaply detect changes.84    private(set) var revision = 08586    func append(_ entry: RequestLogEntry) {87        entries.append(entry)88        if entries.count > Self.capacity {89            entries.removeFirst(entries.count - Self.capacity)90        }91        revision += 192    }9394    func clear() {95        entries.removeAll()96        revision += 197    }9899    /// Metadata-only JSON export (bodies excluded by design).100    func exportJSON() -> Data {101        let rows = entries.map { entry -> [String: Any] in102            [103                "date": ISO8601DateFormatter().string(from: entry.date),104                "model": entry.namespacedModelID,105                "provider": entry.provider.rawValue,106                "status": entry.status,107                "streamed": entry.streamed,108                "latency_ms": Int(entry.latency * 1000),109                "ttfb_ms": entry.upstreamTTFB.map { Int($0 * 1000) } as Any,110                "input_tokens": entry.usage.inputTokens,111                "output_tokens": entry.usage.outputTokens,112                "usage_estimated": entry.usageEstimated,113                "cost_usd": entry.estimatedCost as Any,114                "key": entry.localKeyName as Any,115                "error": entry.errorMessage as Any,116            ]117        }118        return (try? JSONSerialization.data(withJSONObject: rows, options: [.prettyPrinted, .sortedKeys])) ?? Data("[]".utf8)119    }120}121