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// ServerController.swift3// Zyquo Router4//5// Author: Simon-Pierre Boucher6// Mail: contact@spboucher.ai7//8// Owns the embedded server's lifecycle for the app: Start/Stop, state for9// the status pill, and the served endpoint URL. The server itself runs in a10// detached task; stopping cancels it, which drains the accept loop and11// cancels in-flight requests (and their upstream calls).12//1314import Foundation15import SwiftUI1617@MainActor18final class ServerController: ObservableObject {19 enum State: Equatable {20 case stopped21 case starting22 case running(port: Int)23 case failed(String)24 }2526 @Published private(set) var state: State = .stopped27 @AppStorage("serverPort") var port = 878728 @AppStorage("bindLAN") var bindLAN = false2930 /// Shared with Routes at Start; the dashboard reads totals from it.31 let usageMeter = UsageMeter()32 /// Live traffic log shared with the Requests screen.33 let requestLog = RequestLogStore()34 /// When the current run started (uptime tile).35 @Published private(set) var startedAt: Date?3637 private var serverTask: Task<Void, Never>?3839 var endpointURL: String {40 "http://localhost:\(port)/v1"41 }4243 var isRunning: Bool {44 if case .running = state { return true }45 return false46 }4748 func start() {49 guard serverTask == nil else { return }50 state = .starting5152 let host = bindLAN ? "0.0.0.0" : "127.0.0.1"53 let port = port54 let localKeys = PersistenceService.shared.load([APIKeyRecord].self, from: "local-keys.json") ?? []5556 // LAN exposure is gated behind at least one local API key (D12).57 if bindLAN, !localKeys.contains(where: \.enabled) {58 state = .failed("LAN mode requires at least one enabled local API key — create one in Keys, or switch back to Localhost only.")59 return60 }6162 let routerConfig = RouterConfigStore.snapshot()63 let routes = Routes(64 router: RequestRouter(65 aliases: routerConfig.aliases,66 disabledIDs: routerConfig.disabledIDs,67 fallbackChains: routerConfig.fallbackChains68 ),69 auth: AuthMiddleware(keys: localKeys),70 usageMeter: usageMeter,71 requestLog: requestLog72 )73 let server = HTTPServer(host: host, port: port) { request in74 await routes.handle(request)75 }7677 // The Task inherits MainActor isolation, so state updates after `run`78 // returns are direct; only the nonisolated onRunning callback hops.79 serverTask = Task {80 do {81 try await server.run {82 Task { @MainActor [weak self] in83 guard let self else { return }84 self.state = .running(port: port)85 self.startedAt = Date()86 }87 }88 self.serverTask = nil89 self.state = .stopped90 self.startedAt = nil91 } catch is CancellationError {92 self.serverTask = nil93 self.state = .stopped94 self.startedAt = nil95 } catch {96 self.serverTask = nil97 self.state = .failed(error.localizedDescription)98 self.startedAt = nil99 }100 }101 }102103 func stop() {104 serverTask?.cancel()105 }106107 func toggle() {108 isRunning || state == .starting ? stop() : start()109 }110}111