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.5 KB · 111 lines swift
Raw Blame History
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