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%
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