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.2 KB · 109 lines swift
Raw Blame History
1//2//  UsageMeter.swift3//  Zyquo Router4//5//  Author: Simon-Pierre Boucher6//  Mail: contact@spboucher.ai7//8//  Tokens + estimated cost per request, aggregated per model/provider/key.9//  Phase 2 records in memory; persistence and dashboard aggregation land with10//  the observability work.11//1213import Foundation1415/// One completed (or failed) routed request.16struct UsageRecord: Codable, Identifiable, Sendable {17    let id: UUID18    var date: Date19    var namespacedModelID: String20    var provider: ProviderID21    var localKeyName: String?22    var usage: TokenUsage23    var usageEstimated: Bool24    var estimatedCost: Double?25    var latency: TimeInterval26    var streamed: Bool27    var status: Int2829    init(30        id: UUID = UUID(),31        date: Date = Date(),32        namespacedModelID: String,33        provider: ProviderID,34        localKeyName: String? = nil,35        usage: TokenUsage = TokenUsage(),36        usageEstimated: Bool = false,37        estimatedCost: Double? = nil,38        latency: TimeInterval = 0,39        streamed: Bool = false,40        status: Int = 20041    ) {42        self.id = id43        self.date = date44        self.namespacedModelID = namespacedModelID45        self.provider = provider46        self.localKeyName = localKeyName47        self.usage = usage48        self.usageEstimated = usageEstimated49        self.estimatedCost = estimatedCost50        self.latency = latency51        self.streamed = streamed52        self.status = status53    }54}5556actor UsageMeter {57    private(set) var records: [UsageRecord] = []58    /// Streams currently being forwarded to clients (dashboard tile).59    private(set) var activeStreams = 06061    func record(_ entry: UsageRecord) {62        records.append(entry)63    }6465    func streamBegan() {66        activeStreams += 167    }6869    func streamEnded() {70        activeStreams = max(0, activeStreams - 1)71    }7273    /// Requests per minute over the trailing window (sparkline buckets).74    func requestsPerMinute(minutes: Int) -> [Int] {75        let now = Date()76        var buckets = [Int](repeating: 0, count: minutes)77        for record in records {78            let age = now.timeIntervalSince(record.date)79            guard age >= 0, age < Double(minutes * 60) else { continue }80            buckets[minutes - 1 - Int(age / 60)] += 181        }82        return buckets83    }8485    /// Share of today's requests per provider (breakdown bar).86    func providerBreakdown(since cutoff: Date) -> [(provider: ProviderID, count: Int)] {87        var counts: [ProviderID: Int] = [:]88        for record in records where record.date >= cutoff {89            counts[record.provider, default: 0] += 190        }91        return counts.sorted { $0.value > $1.value }.map { ($0.key, $0.value) }92    }9394    /// Totals since a cutoff (today's tiles on the dashboard).95    func totals(since cutoff: Date) -> (requests: Int, usage: TokenUsage, cost: Double, errors: Int) {96        var usage = TokenUsage()97        var cost = 0.098        var requests = 099        var errors = 0100        for record in records where record.date >= cutoff {101            requests += 1102            usage = usage + record.usage103            cost += record.estimatedCost ?? 0104            if record.status >= 400 { errors += 1 }105        }106        return (requests, usage, cost, errors)107    }108}109