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// Routes.swift3// Zyquo Router4//5// Author: Simon-Pierre Boucher6// Mail: contact@spboucher.ai7//8// Maps inbound requests to endpoints: /health, /v1/models, /v1/models/{id};9// /v1/chat/completions arrives in Phase 3. All errors are OpenAI-shaped.10//1112import Foundation13import NIOHTTP11415struct Routes: Sendable {16 let router: RequestRouter17 let auth: AuthMiddleware18 let cors: CORS19 let version: String20 let startedAt: Date21 let chat: ChatCompletionsRoute2223 private static let encoder: JSONEncoder = {24 let encoder = JSONEncoder()25 encoder.outputFormatting = [.withoutEscapingSlashes]26 return encoder27 }()2829 init(30 router: RequestRouter,31 auth: AuthMiddleware = AuthMiddleware(),32 cors: CORS = CORS(),33 version: String = "1.0.0",34 startedAt: Date = Date(),35 providerKey: @escaping @Sendable (ProviderID) -> String? = { provider in36 try? SecureKeyStore().key(for: provider)37 },38 usageMeter: UsageMeter = UsageMeter(),39 requestLog: RequestLogStore = RequestLogStore()40 ) {41 self.router = router42 self.auth = auth43 self.cors = cors44 self.version = version45 self.startedAt = startedAt46 self.chat = ChatCompletionsRoute(47 router: router,48 providerKey: providerKey,49 usageMeter: usageMeter,50 requestLog: requestLog,51 retryPolicy: RetryPolicy()52 )53 }5455 func handle(_ request: RouteRequest) async -> RouteResult {56 if request.method == .OPTIONS {57 return cors.preflightResponse()58 }5960 let path = request.path6162 // /health is deliberately unauthenticated (readiness probes).63 if request.method == .GET, path == "/health" {64 return withCORS(health())65 }6667 let localKey: APIKeyRecord?68 switch auth.authorize(request) {69 case .unauthorized(let message):70 return withCORS(OpenAIError.response(71 status: .unauthorized,72 message: message,73 type: "authentication_error",74 code: "invalid_api_key"75 ))76 case .allowed(let record):77 localKey = record78 }7980 switch (request.method, path) {81 case (.POST, "/v1/chat/completions"):82 return withCORS(await chat.handle(request, localKey: localKey))83 case (.GET, "/v1/models"):84 return withCORS(modelList())85 case (.GET, _) where path.hasPrefix("/v1/models/"):86 let id = String(path.dropFirst("/v1/models/".count))87 .removingPercentEncoding ?? String(path.dropFirst("/v1/models/".count))88 return withCORS(model(id: id))89 default:90 return withCORS(OpenAIError.response(91 status: .notFound,92 message: "Unknown request URL: \(request.method) \(path). The router serves /v1/chat/completions, /v1/models, and /health.",93 type: "invalid_request_error"94 ))95 }96 }9798 // MARK: - Endpoints99100 private func health() -> RouteResult {101 struct Health: Codable {102 var status: String103 var version: String104 var uptime: Int105 var models: Int106 }107 let payload = Health(108 status: "ok",109 version: version,110 uptime: Int(Date().timeIntervalSince(startedAt)),111 models: router.exposedModels.filter { !$0.disabled }.count112 )113 return json(payload)114 }115116 private func modelList() -> RouteResult {117 let created = Int(startedAt.timeIntervalSince1970)118 var entries: [OpenAIModelEntry] = []119 var aliasTargets: [String: String] = [:]120 for (alias, target) in router.aliases {121 aliasTargets[target] = alias122 }123 for (namespacedID, model, disabled) in router.exposedModels where !disabled {124 entries.append(OpenAIModelEntry(125 id: namespacedID,126 created: created,127 ownedBy: model.provider.rawValue,128 xZyquo: XZyquoModelInfo(129 displayName: model.displayName,130 contextWindow: model.contextWindow,131 maxOutputTokens: model.maxOutputTokens,132 vision: model.capabilities.vision,133 tools: model.capabilities.tools,134 reasoning: model.capabilities.reasoning,135 inputPerMTok: model.pricing?.inputPerMTok,136 outputPerMTok: model.pricing?.outputPerMTok,137 alias: aliasTargets[namespacedID]138 )139 ))140 }141 return json(OpenAIModelList(data: entries))142 }143144 private func model(id: String) -> RouteResult {145 do {146 let resolution = try router.resolve(id)147 let entry = OpenAIModelEntry(148 id: resolution.namespacedID,149 created: Int(startedAt.timeIntervalSince1970),150 ownedBy: resolution.model.provider.rawValue,151 xZyquo: XZyquoModelInfo(152 displayName: resolution.model.displayName,153 contextWindow: resolution.model.contextWindow,154 maxOutputTokens: resolution.model.maxOutputTokens,155 vision: resolution.model.capabilities.vision,156 tools: resolution.model.capabilities.tools,157 reasoning: resolution.model.capabilities.reasoning,158 inputPerMTok: resolution.model.pricing?.inputPerMTok,159 outputPerMTok: resolution.model.pricing?.outputPerMTok,160 alias: nil161 )162 )163 return json(entry)164 } catch let error as RequestRouter.RoutingError {165 return Self.routingErrorResponse(error)166 } catch {167 return OpenAIError.response(status: .internalServerError, message: "Internal error.", type: "server_error")168 }169 }170171 /// Shared mapping used by every route that resolves a model.172 static func routingErrorResponse(_ error: RequestRouter.RoutingError) -> RouteResult {173 switch error {174 case .unknownModel(let name), .disabledModel(let name):175 return OpenAIError.modelNotFound(name)176 case .ambiguousModel(let name, let candidates):177 return OpenAIError.response(178 status: .notFound,179 message: "The model `\(name)` is ambiguous — use a namespaced ID: \(candidates.joined(separator: ", ")).",180 type: "invalid_request_error",181 param: "model",182 code: "model_not_found"183 )184 }185 }186187 // MARK: - Helpers188189 private func json<T: Encodable>(_ payload: T, status: HTTPResponseStatus = .ok) -> RouteResult {190 guard let body = try? Self.encoder.encode(payload) else {191 return OpenAIError.response(status: .internalServerError, message: "Encoding failure.", type: "server_error")192 }193 return .complete(status: status, headers: [("Content-Type", "application/json")], body: body)194 }195196 private func withCORS(_ result: RouteResult) -> RouteResult {197 switch result {198 case .complete(let status, let headers, let body):199 return .complete(status: status, headers: headers + cors.headers, body: body)200 case .stream(let status, let headers, let body):201 return .stream(status: status, headers: headers + cors.headers, body: body)202 }203 }204}205