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// ChatCompletionRequest.swift3// Zyquo Router4//5// Author: Simon-Pierre Boucher6// Mail: contact@spboucher.ai7//8// The parsed inbound OpenAI /v1/chat/completions request. The raw JSON object9// is preserved (unknown keys pass through to OpenAI-compatible upstreams per10// decision D5); typed accessors cover everything the router itself needs for11// routing, translation, and capability checks.12//1314import Foundation1516struct ChatCompletionRequest {17 /// The request exactly as received (top-level JSON object).18 let raw: [String: Any]1920 let model: String21 let messages: [OAIMessage]22 let stream: Bool23 let includeUsage: Bool2425 enum ParseError: LocalizedError {26 case notAnObject27 case missing(String)28 case invalid(String, detail: String)2930 var errorDescription: String? {31 switch self {32 case .notAnObject: return "The request body must be a JSON object."33 case .missing(let param): return "Missing required parameter: '\(param)'."34 case .invalid(let param, let detail): return "Invalid value for '\(param)': \(detail)."35 }36 }3738 var param: String? {39 switch self {40 case .notAnObject: return nil41 case .missing(let param), .invalid(let param, _): return param42 }43 }44 }4546 init(body: Data) throws {47 guard let object = try? JSONSerialization.jsonObject(with: body),48 let dict = object as? [String: Any] else {49 throw ParseError.notAnObject50 }51 raw = dict5253 guard let model = dict["model"] as? String, !model.isEmpty else {54 throw ParseError.missing("model")55 }56 self.model = model5758 guard let rawMessages = dict["messages"] as? [[String: Any]], !rawMessages.isEmpty else {59 throw ParseError.missing("messages")60 }61 messages = try rawMessages.enumerated().map { index, message in62 try OAIMessage(json: message, index: index)63 }6465 stream = dict["stream"] as? Bool ?? false66 includeUsage = (dict["stream_options"] as? [String: Any])?["include_usage"] as? Bool ?? false6768 if let n = dict["n"] as? Int, n > 1 {69 // Router-wide policy (research §3.2.2): n>1 is rejected for70 // uniform behavior across upstreams.71 throw ParseError.invalid("n", detail: "the router supports n=1 only")72 }73 }7475 /// max_completion_tokens wins over the deprecated max_tokens.76 var maxTokens: Int? {77 (raw["max_completion_tokens"] as? Int) ?? (raw["max_tokens"] as? Int)78 }7980 var tools: [[String: Any]]? { raw["tools"] as? [[String: Any]] }81 var hasTools: Bool { !(tools ?? []).isEmpty }8283 /// Any message carrying an image content part (vision capability check).84 var hasImageContent: Bool {85 messages.contains { message in86 message.contentParts?.contains { ($0["type"] as? String) == "image_url" } ?? false87 }88 }89}9091/// One inbound message, loosely typed: content may be a string, an array of92/// content parts, or null (assistant tool-call turns).93struct OAIMessage {94 let role: String95 let json: [String: Any]9697 init(json: [String: Any], index: Int) throws {98 guard let role = json["role"] as? String else {99 throw ChatCompletionRequest.ParseError.invalid("messages[\(index)].role", detail: "missing role")100 }101 self.role = role102 self.json = json103 }104105 var contentString: String? { json["content"] as? String }106 var contentParts: [[String: Any]]? { json["content"] as? [[String: Any]] }107 var toolCalls: [[String: Any]]? { json["tool_calls"] as? [[String: Any]] }108 var toolCallID: String? { json["tool_call_id"] as? String }109110 /// All text in this message (string content or text parts joined).111 var flattenedText: String {112 if let text = contentString { return text }113 guard let parts = contentParts else { return "" }114 return parts.compactMap { part in115 (part["type"] as? String) == "text" ? part["text"] as? String : nil116 }.joined(separator: "\n")117 }118}119