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// TranslatorTests.swift3// Zyquo Router4//5// Author: Simon-Pierre Boucher6// Mail: contact@spboucher.ai7//8// Fixture tests for the translation layer: recorded Anthropic SSE events →9// OpenAI chunks, recorded Gemini SSE → OpenAI chunks, CompatAdjuster10// request/response tables, request builders. Fixtures follow the transcripts11// in docs/ROUTER-RESEARCH.md §3.12//1314import XCTest15@testable import ZyquoRouter1617final class TranslatorTests: XCTestCase {18 private func chatRequest(_ dict: [String: Any]) throws -> ChatCompletionRequest {19 try ChatCompletionRequest(body: JSONSerialization.data(withJSONObject: dict))20 }2122 private func model(23 _ id: String,24 _ provider: ProviderID,25 reasoning: Bool = false,26 maxCompletionTokens: Bool = false27 ) -> AIModel {28 AIModel(29 id: id, provider: provider, displayName: id,30 contextWindow: 128_000, maxOutputTokens: 8192,31 capabilities: ModelCapabilities(vision: true, tools: true, reasoning: reasoning),32 pricing: nil,33 parameterSupport: ParameterSupport(usesMaxCompletionTokens: maxCompletionTokens)34 )35 }3637 private func json(_ data: Data) -> [String: Any] {38 (try? JSONSerialization.jsonObject(with: data)) as? [String: Any] ?? [:]39 }4041 // MARK: - Anthropic request building4243 func testAnthropicRequestTranslation() throws {44 let request = try chatRequest([45 "model": "anthropic/claude-x",46 "temperature": 1.4,47 "messages": [48 ["role": "system", "content": "You are terse."],49 ["role": "user", "content": "Weather?"],50 ["role": "assistant", "content": NSNull(), "tool_calls": [[51 "id": "call_abc", "type": "function",52 "function": ["name": "get_weather", "arguments": "{\"location\": \"Paris\"}"],53 ]]],54 ["role": "tool", "tool_call_id": "call_abc", "content": "18°C"],55 ],56 "tools": [["type": "function", "function": [57 "name": "get_weather",58 "parameters": ["type": "object", "properties": ["location": ["type": "string"]]],59 ]]],60 "parallel_tool_calls": false,61 ])62 let body = AnthropicTranslator.buildRequest(request, model: model("claude-x", .anthropic))6364 XCTAssertEqual(body["model"] as? String, "claude-x")65 XCTAssertEqual(body["system"] as? String, "You are terse.")66 XCTAssertEqual(body["max_tokens"] as? Int, 8192, "synthesized from catalog when omitted")67 XCTAssertEqual(body["temperature"] as? Double, 1.0, "clamped to Anthropic's 0–1")6869 let messages = body["messages"] as! [[String: Any]]70 XCTAssertEqual(messages.count, 3)71 let assistantBlocks = messages[1]["content"] as! [[String: Any]]72 XCTAssertEqual(assistantBlocks[0]["type"] as? String, "tool_use")73 XCTAssertEqual((assistantBlocks[0]["input"] as? [String: Any])?["location"] as? String, "Paris")74 let toolResult = (messages[2]["content"] as! [[String: Any]])[0]75 XCTAssertEqual(toolResult["type"] as? String, "tool_result")76 XCTAssertEqual(toolResult["tool_use_id"] as? String, "call_abc")7778 let tools = body["tools"] as! [[String: Any]]79 XCTAssertNotNil(tools[0]["input_schema"])80 let toolChoice = body["tool_choice"] as! [String: Any]81 XCTAssertEqual(toolChoice["type"] as? String, "auto")82 XCTAssertEqual(toolChoice["disable_parallel_tool_use"] as? Bool, true)83 }8485 // MARK: - Anthropic stream machine (fixture from research §3.1.4)8687 func testAnthropicStreamToOpenAIChunks() {88 var machine = AnthropicTranslator.StreamMachine(89 emitter: ChunkEmitter(model: "anthropic/claude-x"),90 includeUsage: true91 )92 func consume(_ event: String, _ data: String) -> [[String: Any]] {93 machine.consume(SSEEvent(event: event, data: data)).payloads.map(json)94 }9596 let start = consume("message_start", #"{"type":"message_start","message":{"id":"msg_1","usage":{"input_tokens":472,"cache_read_input_tokens":10,"output_tokens":2}}}"#)97 XCTAssertEqual((start[0]["choices"] as! [[String: Any]])[0]["delta"] as! [String: String],98 ["role": "assistant", "content": ""])99100 XCTAssertTrue(consume("ping", #"{"type":"ping"}"#).isEmpty)101 XCTAssertTrue(consume("content_block_start", #"{"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}"#).isEmpty)102103 let text = consume("content_block_delta", #"{"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Okay,"}}"#)104 XCTAssertEqual(((text[0]["choices"] as! [[String: Any]])[0]["delta"] as! [String: Any])["content"] as? String, "Okay,")105106 let toolStart = consume("content_block_start", #"{"type":"content_block_start","index":1,"content_block":{"type":"tool_use","id":"toolu_1","name":"get_weather","input":{}}}"#)107 let toolDelta = ((toolStart[0]["choices"] as! [[String: Any]])[0]["delta"] as! [String: Any])["tool_calls"] as! [[String: Any]]108 XCTAssertEqual(toolDelta[0]["index"] as? Int, 0)109 XCTAssertEqual(toolDelta[0]["id"] as? String, "toolu_1")110 XCTAssertEqual((toolDelta[0]["function"] as! [String: Any])["arguments"] as? String, "")111112 XCTAssertTrue(consume("content_block_delta", #"{"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":""}}"#).isEmpty, "empty fragments skipped")113114 let args = consume("content_block_delta", #"{"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"{\"location\":\"Paris\"}"}}"#)115 let argsDelta = ((args[0]["choices"] as! [[String: Any]])[0]["delta"] as! [String: Any])["tool_calls"] as! [[String: Any]]116 XCTAssertNil(argsDelta[0]["id"], "no id repetition on argument deltas")117 XCTAssertEqual((argsDelta[0]["function"] as! [String: Any])["arguments"] as? String, #"{"location":"Paris"}"#)118119 _ = consume("content_block_stop", #"{"type":"content_block_stop","index":1}"#)120 let finish = consume("message_delta", #"{"type":"message_delta","delta":{"stop_reason":"tool_use"},"usage":{"output_tokens":89}}"#)121 XCTAssertEqual((finish[0]["choices"] as! [[String: Any]])[0]["finish_reason"] as? String, "tool_calls")122123 let (stopPayloads, done) = machine.consume(SSEEvent(event: "message_stop", data: #"{"type":"message_stop"}"#))124 XCTAssertTrue(done)125 let usageChunk = json(stopPayloads[0])126 XCTAssertEqual((usageChunk["choices"] as? [Any])?.count, 0, "usage chunk has empty choices")127 let usage = usageChunk["usage"] as! [String: Any]128 XCTAssertEqual(usage["prompt_tokens"] as? Int, 482, "input + cache reads")129 XCTAssertEqual(usage["completion_tokens"] as? Int, 89)130 }131132 // MARK: - Gemini request building + stream machine133134 func testGeminiRequestTranslation() throws {135 let request = try chatRequest([136 "model": "gemini/gemini-x",137 "max_tokens": 100,138 "temperature": 0.7,139 "stop": "\n\n",140 "messages": [141 ["role": "system", "content": "Be brief."],142 ["role": "user", "content": "Hi"],143 ["role": "assistant", "content": NSNull(), "tool_calls": [[144 "id": "call_1", "type": "function",145 "function": ["name": "lookup", "arguments": "{\"q\":\"x\"}"],146 ]]],147 ["role": "tool", "tool_call_id": "call_1", "content": "plain text result"],148 ],149 ])150 let body = GeminiTranslator.buildRequest(request, model: model("gemini-x", .gemini))151152 XCTAssertNotNil(body["systemInstruction"])153 let contents = body["contents"] as! [[String: Any]]154 XCTAssertEqual(contents[1]["role"] as? String, "model", "assistant → model")155 let functionResponse = ((contents[2]["parts"] as! [[String: Any]])[0]["functionResponse"] as! [String: Any])156 XCTAssertEqual(functionResponse["name"] as? String, "lookup", "name resolved from tool_call_id")157 XCTAssertEqual((functionResponse["response"] as! [String: Any])["result"] as? String, "plain text result",158 "non-JSON tool output wrapped in an object")159 let generation = body["generationConfig"] as! [String: Any]160 XCTAssertEqual(generation["maxOutputTokens"] as? Int, 100)161 XCTAssertEqual(generation["stopSequences"] as? [String], ["\n\n"])162 }163164 func testGeminiStreamToOpenAIChunks() {165 var machine = GeminiTranslator.StreamMachine(166 emitter: ChunkEmitter(model: "gemini/gemini-x"),167 includeUsage: true168 )169 let first = machine.consume(SSEEvent(event: nil, data: #"{"candidates":[{"content":{"parts":[{"text":"The"}],"role":"model"},"index":0}]}"#)).map(json)170 XCTAssertEqual((first[0]["choices"] as! [[String: Any]])[0]["delta"] as! [String: String],171 ["role": "assistant", "content": ""], "role chunk synthesized")172 XCTAssertEqual(((first[1]["choices"] as! [[String: Any]])[0]["delta"] as! [String: Any])["content"] as? String, "The")173174 // functionCall arrives complete → STOP must map to tool_calls.175 let tool = machine.consume(SSEEvent(event: nil, data: #"{"candidates":[{"content":{"parts":[{"functionCall":{"name":"get_weather","args":{"location":"Paris"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":57,"candidatesTokenCount":12,"thoughtsTokenCount":88,"totalTokenCount":157}}"#)).map(json)176 let toolStart = ((tool[0]["choices"] as! [[String: Any]])[0]["delta"] as! [String: Any])["tool_calls"] as! [[String: Any]]177 XCTAssertEqual((toolStart[0]["function"] as! [String: Any])["name"] as? String, "get_weather")178 XCTAssertTrue((toolStart[0]["id"] as! String).hasPrefix("call_"), "missing id synthesized")179 let finish = tool[2]180 XCTAssertEqual((finish["choices"] as! [[String: Any]])[0]["finish_reason"] as? String, "tool_calls",181 "STOP + functionCall override")182183 let final = machine.finalPayloads().map(json)184 XCTAssertEqual(final.count, 1, "finish already sent; only usage remains")185 let usage = final[0]["usage"] as! [String: Any]186 XCTAssertEqual(usage["prompt_tokens"] as? Int, 57)187 XCTAssertEqual(usage["completion_tokens"] as? Int, 100, "candidates + thoughts")188 XCTAssertEqual((usage["completion_tokens_details"] as! [String: Any])["reasoning_tokens"] as? Int, 88)189 }190191 // MARK: - CompatAdjuster192193 func testCompatAdjusterRequestTables() {194 let base: [String: Any] = [195 "model": "mistral/mistral-x",196 "messages": [["role": "user", "content": "hi"]],197 "seed": 42,198 "max_tokens": 100,199 "custom_extra": "kept",200 ]201202 let mistral = CompatAdjuster.adjustRequest(base, model: model("mistral-x", .mistral), stream: true)203 XCTAssertEqual(mistral["model"] as? String, "mistral-x", "namespace stripped")204 XCTAssertEqual(mistral["random_seed"] as? Int, 42, "seed renamed")205 XCTAssertNil(mistral["seed"])206 XCTAssertEqual(mistral["custom_extra"] as? String, "kept", "unknown keys pass through (D5)")207 XCTAssertEqual((mistral["stream_options"] as? [String: Any])?["include_usage"] as? Bool, true)208209 let kimi = CompatAdjuster.adjustRequest(210 ["model": "kimi/k", "messages": [["role": "user", "content": "hi"]], "temperature": 1.8],211 model: model("k", .kimi), stream: false212 )213 XCTAssertEqual(kimi["temperature"] as? Double, 1.0, "Moonshot clamps to [0,1]")214215 let cerebras = CompatAdjuster.adjustRequest(216 ["model": "cerebras/c", "messages": [["role": "user", "content": "hi"]], "max_tokens": 50],217 model: model("c", .cerebras, maxCompletionTokens: true), stream: false218 )219 XCTAssertEqual(cerebras["max_completion_tokens"] as? Int, 50)220 XCTAssertNil(cerebras["max_tokens"], "never send both")221222 let xai = CompatAdjuster.adjustRequest(223 ["model": "xai/g", "messages": [["role": "user", "content": "hi"]],224 "presence_penalty": 0.5, "stop": ["x"]],225 model: model("g", .xai, reasoning: true), stream: false226 )227 XCTAssertNil(xai["presence_penalty"], "Grok reasoning models 400 on penalties")228 XCTAssertNil(xai["stop"])229 }230231 func testCompatAdjusterResponseNormalization() {232 // Together "eos" + Mistral thinking-array + Perplexity <think>.233 let together = CompatAdjuster.normalizeResponse(234 ["model": "m", "choices": [["index": 0, "finish_reason": "eos",235 "message": ["role": "assistant", "content": "hi"]]]],236 namespacedModel: "together/m", provider: .together237 )238 XCTAssertEqual(together["model"] as? String, "together/m")239 XCTAssertEqual((together["choices"] as! [[String: Any]])[0]["finish_reason"] as? String, "stop")240241 let mistral = CompatAdjuster.normalizeResponse(242 ["choices": [["index": 0, "finish_reason": "stop", "message": [243 "role": "assistant",244 "content": [245 ["type": "thinking", "thinking": [["type": "text", "text": "hmm"]]],246 ["type": "text", "text": "answer"],247 ],248 ]]]],249 namespacedModel: "mistral/m", provider: .mistral250 )251 let mistralMessage = (mistral["choices"] as! [[String: Any]])[0]["message"] as! [String: Any]252 XCTAssertEqual(mistralMessage["content"] as? String, "answer")253 XCTAssertEqual(mistralMessage["reasoning_content"] as? String, "hmm")254255 let perplexity = CompatAdjuster.normalizeResponse(256 ["choices": [["index": 0, "finish_reason": "stop", "message": [257 "role": "assistant", "content": "<think>reasoning here</think>\nfinal answer",258 ]]]],259 namespacedModel: "perplexity/sonar-reasoning", provider: .perplexity260 )261 let perplexityMessage = (perplexity["choices"] as! [[String: Any]])[0]["message"] as! [String: Any]262 XCTAssertEqual(perplexityMessage["content"] as? String, "final answer")263 XCTAssertEqual(perplexityMessage["reasoning_content"] as? String, "reasoning here")264 }265266 func testUsageChunkSwallowedWhenClientDidNotAsk() {267 let swallowed = CompatAdjuster.normalizeChunk(268 ["choices": [] as [Any], "usage": ["prompt_tokens": 1, "completion_tokens": 2, "total_tokens": 3]],269 namespacedModel: "openai/x", provider: .openai, clientWantsUsage: false270 )271 XCTAssertNil(swallowed)272 }273274 // MARK: - Request parsing275276 func testRequestParsingErrors() {277 XCTAssertThrowsError(try chatRequest(["messages": [["role": "user", "content": "hi"]]]))278 XCTAssertThrowsError(try chatRequest(["model": "x"]))279 XCTAssertThrowsError(try chatRequest([280 "model": "x", "n": 3,281 "messages": [["role": "user", "content": "hi"]],282 ]), "n>1 rejected router-wide")283 }284}285