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// RequestRouter.swift3// Zyquo Router4//5// Author: Simon-Pierre Boucher6// Mail: contact@spboucher.ai7//8// Resolves a requested model string — namespaced `provider/model-id`, an9// unambiguous bare upstream ID, or a user alias — to a catalog model.10// Aliases and fallback chains are user-configured (Phase 6 UI); disabled11// models 404 through the API.12//1314import Foundation1516struct RequestRouter: Sendable {17 /// Snapshot of the catalog (never hop to the main actor per request).18 let catalog: [AIModel]19 /// User aliases, e.g. "fast" → "cerebras/llama-3.3-70b".20 var aliases: [String: String] = [:]21 /// Namespaced IDs the user disabled (they 404 through the API).22 var disabledIDs: Set<String> = []23 /// User fallback chains: namespaced ID → ordered list of namespaced IDs to try next.24 var fallbackChains: [String: [String]] = [:]2526 struct Resolution {27 let model: AIModel28 /// `provider/model-id` — always echoed back in responses.29 let namespacedID: String30 }3132 enum RoutingError: Error {33 case unknownModel(String)34 case ambiguousModel(String, candidates: [String])35 case disabledModel(String)36 }3738 init(39 catalog: [AIModel] = ModelCatalogData.all,40 aliases: [String: String] = [:],41 disabledIDs: Set<String> = [],42 fallbackChains: [String: [String]] = [:]43 ) {44 self.catalog = catalog45 self.aliases = aliases46 self.disabledIDs = disabledIDs47 self.fallbackChains = fallbackChains48 }4950 static func namespacedID(for model: AIModel) -> String {51 "\(model.provider.rawValue)/\(model.id)"52 }5354 /// Every model the router serves, in catalog order, with disabled ones flagged.55 var exposedModels: [(namespacedID: String, model: AIModel, disabled: Bool)] {56 catalog.map { model in57 let id = Self.namespacedID(for: model)58 return (id, model, disabledIDs.contains(id))59 }60 }6162 /// Resolves a requested model string. Order: alias → namespaced ID → bare ID.63 func resolve(_ requested: String) throws -> Resolution {64 let name = aliases[requested] ?? requested6566 // Namespaced: the segment before the first "/" names a provider.67 // (Model IDs themselves may contain "/" — e.g. meta-llama/… on68 // Together/DeepInfra — which is exactly why the namespace is required69 // to be a known provider prefix.)70 if let slash = name.firstIndex(of: "/"),71 let provider = ProviderID(rawValue: String(name[name.startIndex..<slash])) {72 let bareID = String(name[name.index(after: slash)...])73 if let model = catalog.first(where: { $0.provider == provider && $0.id == bareID }) {74 return try admit(model)75 }76 throw RoutingError.unknownModel(requested)77 }7879 // Bare upstream ID, accepted when unambiguous across providers.80 let matches = catalog.filter { $0.id == name }81 switch matches.count {82 case 0:83 throw RoutingError.unknownModel(requested)84 case 1:85 return try admit(matches[0])86 default:87 throw RoutingError.ambiguousModel(88 requested,89 candidates: matches.map(Self.namespacedID(for:))90 )91 }92 }9394 private func admit(_ model: AIModel) throws -> Resolution {95 let id = Self.namespacedID(for: model)96 guard !disabledIDs.contains(id) else { throw RoutingError.disabledModel(id) }97 return Resolution(model: model, namespacedID: id)98 }99}100