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# PROVIDER-REUSE.md — Zyquo Router2Study of the Zyquo Cloud repository (../zyquo-cloud) for provider-layer reuse.34<!--5 PROVIDER-REUSE.md6 Zyquo Router78 Author: Simon-Pierre Boucher9 Mail: contact@spboucher.ai10-->1112Source repo studied: `/Users/simon-pierreboucher/Desktop/zyquo-cloud` (Swift 5.9 SPM executable, macOS 13+,13single dependency `swift-markdown` — see `Package.swift:12-39`). Catalog and research were compiled/verified14by Cloud with **live keys on 2026-07-30** (`docs/PROVIDERS.md:8-13`, Phase 7 amendments at15`docs/PROVIDERS.md:1638-1719`). Every claim below is traceable to a file/line in that repo.1617---1819## 1. How each provider's API is called in Zyquo Cloud2021### 1.1 Architecture — two clients, one registry2223Cloud's entire provider layer is **four files** in `Sources/ZyquoCloud/Providers/` plus one shared24networking service:2526| File | Role |27|---|---|28| `Sources/ZyquoCloud/Providers/ProviderProtocol.swift` | `ChatRequest`, `ChatEvent`, `protocol ProviderClient`, `ProviderError` |29| `Sources/ZyquoCloud/Providers/OpenAICompatibleClient.swift` | One client for **11 of 12** providers (everything except Anthropic) + custom endpoints |30| `Sources/ZyquoCloud/Providers/AnthropicClient.swift` | Native Anthropic Messages API (`/v1/messages`) |31| `Sources/ZyquoCloud/Providers/ProviderRegistry.swift` | `wireFormat` → client resolution |32| `Sources/ZyquoCloud/Services/StreamingService.swift` | `SSEParser`, `SSEEvent`, shared `URLSession`, retry/backoff, error mapping |3334**There is NO `GeminiClient.swift` in Zyquo Cloud.** Gemini is driven through its **OpenAI-compatible35endpoint** `https://generativelanguage.googleapis.com/v1beta/openai` with Bearer auth36(`Sources/ZyquoCloud/Models/ProviderID.swift:64`), through `OpenAICompatibleClient` like the other37OpenAI-shaped providers. Cloud's research notes the compat endpoint's limitations and that a native38client would be needed for rich thinking display (`docs/PROVIDERS.md:560`), but Cloud shipped39compat-only. Zyquo Router's CLAUDE.md assumes a native `GeminiClient` exists — **it does not**; the40Router must either keep the compat path (near-pass-through, easiest) or write the native translator41itself per Phase 0.A research (see §5 caveats).4243The protocol (`ProviderProtocol.swift:31-42`):4445```swift46protocol ProviderClient {47 var providerID: ProviderID { get }48 func streamChat(_ request: ChatRequest, apiKey: String) -> AsyncThrowingStream<ChatEvent, Error>49 func complete(_ request: ChatRequest, apiKey: String) async throws -> Message50 func listModelIDs(apiKey: String) async throws -> [String]51}52```5354The provider-agnostic request/event types (`ProviderProtocol.swift:13-28`):5556```swift57struct ChatRequest {58 var model: AIModel59 var systemPrompt: String?60 var messages: [Message]61 var parameters: ChatParameters62 var stream: Bool = true63}64enum ChatEvent {65 case reasoningDelta(String)66 case textDelta(String)67 case citations([Citation])68 case usage(TokenUsage)69 case finished(reason: String?)70}71```7273A default-protocol extension provides `testKey(_:fallbackModel:) async throws -> TimeInterval`74(`ProviderProtocol.swift:49-68`): calls `listModelIDs` where available, else a 16-token75`"Reply with exactly: OK"` completion (Perplexity has no `/models` — returns 404,76`ProviderID.swift:77-79`, `docs/PROVIDERS.md:27`).7778`ProviderRegistry` (`ProviderRegistry.swift:13-34`) resolves clients purely from79`ProviderID.wireFormat`:8081```swift82switch model.provider.wireFormat {83case .anthropicMessages: return AnthropicClient()84case .openAIChatCompletions: return OpenAICompatibleClient(provider: model.provider,85 baseURLOverride: model.customBaseURL)86}87```8889`WireFormat` has exactly two cases (`ProviderID.swift:88-93`): `.openAIChatCompletions` (11/1290providers) and `.anthropicMessages`.9192### 1.2 Provider identity, base URLs, auth9394`ProviderID` is a 13-case enum (`Sources/ZyquoCloud/Models/ProviderID.swift:12-25`):95`openai, anthropic, xai, mistral, gemini, qwen, deepseek, kimi, perplexity, together, deepinfra,96cerebras, custom`. Exact `defaultBaseURL` constants (`ProviderID.swift:58-74`):9798| Provider | `defaultBaseURL` | Auth header | Wire format | `/models`? |99|---|---|---|---|---|100| OpenAI | `https://api.openai.com/v1` | `Authorization: Bearer <key>` | OpenAI (origin) | ✅ |101| Anthropic | `https://api.anthropic.com/v1` | `x-api-key: <key>` + `anthropic-version: 2023-06-01` | Anthropic Messages | ✅ (`?limit=100`) |102| xAI | `https://api.x.ai/v1` | Bearer | OpenAI-compat | ✅ |103| Mistral | `https://api.mistral.ai/v1` | Bearer | OpenAI-compat | ✅ |104| Google Gemini | `https://generativelanguage.googleapis.com/v1beta/openai` | Bearer (compat endpoint) | OpenAI-compat | ✅ (IDs prefixed `models/` — stripped) |105| Qwen (DashScope intl) | `https://dashscope-intl.aliyuncs.com/compatible-mode/v1` | Bearer | OpenAI-compat | ✅ |106| DeepSeek | `https://api.deepseek.com` | Bearer | OpenAI-compat | ✅ |107| Kimi (Moonshot) | `https://api.moonshot.ai/v1` | Bearer | OpenAI-compat | ✅ |108| Perplexity | `https://api.perplexity.ai` | Bearer | OpenAI-compat + search extras | ❌ (404) |109| Together AI | `https://api.together.xyz/v1` | Bearer | OpenAI-compat | ✅ (bare array, not `{"data":[…]}`) |110| DeepInfra | `https://api.deepinfra.com/v1/openai` | Bearer | OpenAI-compat | ✅ |111| Cerebras | `https://api.cerebras.ai/v1` | Bearer | OpenAI-compat | ✅ |112| custom | `nil` — from user endpoint config (`baseURLOverride`) | Bearer | OpenAI-compat | varies |113114Paths are appended with `base.appendingPathComponent(path)` so multi-segment bases115(`…/compatible-mode/v1`, `…/v1beta/openai`) are preserved116(`OpenAICompatibleClient.swift:229-241`). Chat endpoint path is `"chat/completions"`; Anthropic's is117`"messages"`. Anthropic constants: `apiVersion = "2023-06-01"`, `defaultMaxTokens = 8192`118(`AnthropicClient.swift:19-20`).119120### 1.3 `OpenAICompatibleClient` — request/response wire types121122All wire types are **private nested Codable structs** (an adaptation point for the Router — see §5).123124**Request** (`OpenAICompatibleClient.swift:28-103`) — `WireRequest`:125`model, messages: [WireMessage], stream, streamOptions (stream_options.include_usage), temperature,126topP (top_p), maxTokens (max_tokens), maxCompletionTokens (max_completion_tokens), frequencyPenalty,127presencePenalty, reasoningEffort (reasoning_effort), enableThinking (enable_thinking)`.128`WireMessage { role: String, content: WireContent }`; `WireContent` encodes either a plain string or129`[WirePart]` where `WirePart` is `.text(String)` or `.imageURL(String)` (encoded as130`{"type":"image_url","image_url":{"url":"data:<mime>;base64,…"}}`). Images are always sent as131base64 data URIs (`OpenAICompatibleClient.swift:302-320`).132133**Body construction** (`buildBody(_:)`, `OpenAICompatibleClient.swift:257-300`) is gated by the134model's `ParameterSupport` (see §2): only supported params are encoded. Notable per-provider logic:135136- `wantsStreamOptions` (`OpenAICompatibleClient.swift:243-255`): `stream_options:{include_usage:true}`137 sent to **openai, xai, gemini, deepseek, kimi, together, cerebras, custom**; **omitted** for138 mistral (rejects unknown params), qwen, deepinfra, perplexity (usage arrives automatically).139- Mistral `reasoning_effort` only accepts `"high"`/`"none"` → client maps `low → none`, else `high`140 (`OpenAICompatibleClient.swift:286-293`; `docs/PROVIDERS.md:1703`).141- Qwen/DashScope `enable_thinking` is only legal on **streaming** requests142 (`OpenAICompatibleClient.swift:294-297`).143- `usesMaxCompletionTokens` switches `max_tokens` → `max_completion_tokens` (OpenAI reasoning144 models, Cerebras, Kimi K-series) (`OpenAICompatibleClient.swift:277-283`).145- System prompt injected as a leading `{"role":"system"}` message146 (`OpenAICompatibleClient.swift:259-261`).147- Text-file attachments are inlined into the message text fenced with the file name148 (`OpenAICompatibleClient.swift:306-309`).149150**Response** (`OpenAICompatibleClient.swift:107-223`) — decoded with plain `JSONDecoder`, unknown151fields ignored:152153- `WireChunk { choices: [WireChoice]?, usage: WireUsage?, citations: [String]?, searchResults: [WireSearchResult]? }`154 (`search_results` = Perplexity).155- `WireChoice { delta: WireDelta?, message: WireDelta?, text: String?, finishReason: String? }` — the156 same struct decodes both streaming chunks (`delta`) and non-streaming responses (`message`);157 `text` is the Together completions-style fallback (`choices[].text` instead of `delta.content`,158 `OpenAICompatibleClient.swift:119-131`; `docs/PROVIDERS.md:1712-1713`).159- `WireDelta { content, reasoningContent ("reasoning_content"), reasoning }` with a **custom160 `init(from:)`** (`OpenAICompatibleClient.swift:143-167`): `content` is normally a string, but161 Mistral reasoning models return an **array of `ContentChunk`s**162 (`{type:"thinking"|"text",…}` with nested `thinking:[{type:"text",text:…}]`) which are flattened163 into text + reasoning.164- `WireUsage { promptTokens, completionTokens, completionTokensDetails.reasoningTokens }` →165 `TokenUsage` (`OpenAICompatibleClient.swift:187-210`).166- `/models` decoding tries `{"data":[{id}]}` then a **bare array** (Together), and strips the167 `models/` prefix Gemini's compat endpoint adds (`listModelIDs`,168 `OpenAICompatibleClient.swift:407-421`).169170**Streaming loop** (`streamChat`, `OpenAICompatibleClient.swift:324-372`): wraps everything in an171`AsyncThrowingStream` whose backing `Task` is cancelled `onTermination`; iterates172`StreamingService.sseEvents(for:provider:)`; breaks on `data: [DONE]`; **silently skips173undecodable chunks** (keep-alives, unknown shapes); yields `.reasoningDelta` (from174`reasoning_content` or `reasoning`), `.textDelta` (from `delta.content` or `choices[].text`),175`.citations` (once, Perplexity), `.usage`, then a final `.finished(reason:)` carrying the last176`finish_reason` seen.177178**Non-streaming** (`complete`, `OpenAICompatibleClient.swift:374-405`): plain POST via179`StreamingService.postJSON`, decodes the same `WireChunk` (reading `choice.message ?? choice.delta`),180builds a `Message` with `reasoning`, `citations`, `usage`, and `estimatedCost` computed from181`model.pricing.cost(inputTokens:outputTokens:)`. If `parameterSupport.requiresStreaming` is set the182client transparently aggregates the stream instead (`completeViaStream`,183`OpenAICompatibleClient.swift:425-454`).184185**Perplexity citations** (`OpenAICompatibleClient.swift:468-476`): top-level `citations` (array of186URL strings) merged with `search_results` titles into numbered `Citation` values187(`Sources/ZyquoCloud/Models/Message.swift:93-105`).188189### 1.4 `AnthropicClient` — native Messages API190191`Sources/ZyquoCloud/Providers/AnthropicClient.swift`. Headers: `x-api-key` + `anthropic-version:1922023-06-01` (`AnthropicClient.swift:142-154`). **Never** routed through the OpenAI client193(`wantsStreamOptions` explicitly notes "never routed here", `OpenAICompatibleClient.swift:252-253`).194195**Request** (`WireRequest`, `AnthropicClient.swift:24-48`): `model`, **mandatory `max_tokens`**196(default 8192 when the user set none), `messages: [WireMessage]` (system messages filtered out),197top-level `system: String?`, `stream`, `temperature`, `top_p`, and198`thinking: {type: "enabled", budget_tokens: 8000} | {type: "disabled"}` when the model has199`thinkingToggle` (`AnthropicClient.swift:156-181`). Content is block-structured: `WireBlock.text`200and `WireBlock.image(mediaType:base64:)` encoding201`{"type":"image","source":{"type":"base64","media_type":…,"data":…}}`202(`AnthropicClient.swift:55-78`). Empty text is sent as `" "` (Anthropic rejects empty blocks,203`AnthropicClient.swift:196`). Note per-model gating: Claude 4.7+/5 removed temperature/top_p —204encoded in each model's `ParameterSupport` (`AnthropicClient.swift:171-174`,205`ModelCatalogData.swift:249,257,266`).206207**Streaming** (`streamChat`, `AnthropicClient.swift:202-259`) decodes the **named SSE events**208(the event name comes from `sse.event ?? event.type`):209210| Event | Handling |211|---|---|212| `message_start` | `usage.inputTokens` captured from `message.usage.input_tokens` |213| `content_block_delta` | `delta.text` → `.textDelta`; `delta.thinking` → `.reasoningDelta` |214| `message_delta` | `usage.output_tokens` (final count) + `delta.stop_reason` |215| `error` | thrown as `ProviderError.serverError(status: 200, message:)` (mid-stream error events) |216| `message_stop`, `ping`, `content_block_start/stop`, unknown | ignored |217218There is **no `[DONE]` sentinel** on Anthropic streams. `.usage` and `.finished(reason: stopReason)`219are yielded at stream end. Non-streaming (`complete`, `AnthropicClient.swift:261-287`) joins220`content[].type == "text"` and `"thinking"` blocks. `listModelIDs` GETs `messages`-sibling221`models?limit=100` (`AnthropicClient.swift:289-299`).222223### 1.5 SSE parsing — `StreamingService` / `SSEParser`224225`Sources/ZyquoCloud/Services/StreamingService.swift`. `SSEEvent { event: String?, data: String }`226(`StreamingService.swift:12-17`). `SSEParser` (`StreamingService.swift:22-47`) is a line-fed227incremental parser: accumulates `event:`/`data:` fields (multi-line `data:` joined with `\n`),228emits a complete event at the blank-line separator, **ignores `:` comment lines** (DeepSeek sends229`: keep-alive`) and `id:`/`retry:`/unknown fields. Unit-tested in230`Tests/ZyquoCloudTests/SSEParserTests.swift`.231232`StreamingService.sseEvents(for:provider:)` (`StreamingService.swift:63-118`):233- Shared `URLSession` with `timeoutIntervalForRequest = 120`, `timeoutIntervalForResource = 900`234 (long streams), `User-Agent: ZyquoCloud/1.0 (macOS)` (`StreamingService.swift:53-59`).235- Uses `session.bytes(for:)`; non-2xx → drains the full error body and throws236 `ProviderError.from(status:body:provider:)`.237- **Splits bytes on `\n` manually** (handling `\r\n`) because `AsyncBytes.lines` *skips empty238 lines*, which are the SSE event separators (`StreamingService.swift:80-95`) — a real pitfall the239 Router must keep.240- Flushes a trailing partial line/event at EOF; checks `Task.isCancelled` per byte-boundary;241 cancellation surfaces as `ProviderError.cancelled`.242243**Retry policy**: `postJSON` (`StreamingService.swift:122-156`) retries **3 attempts** on 429/5xx244with `Retry-After` honored when present, else exponential backoff `pow(2, attempt) * 2` seconds245(4s, 8s). `getJSON` delegates to `postJSON` (same mapping). **Streaming requests are NOT retried**246(single attempt).247248### 1.6 Error types249250`ProviderError` (`ProviderProtocol.swift:73-146`), a `LocalizedError`:251`invalidAPIKey(ProviderID)`, `rateLimited(ProviderID, retryAfter:)`, `serverError(ProviderID,252status:, message:)`, `badRequest(ProviderID, message:)`, `networkError(underlying:)`,253`invalidResponse(ProviderID, detail:)`, `missingAPIKey(ProviderID)`, `noModelAvailable(ProviderID)`,254`cancelled`. Status mapping (`from(status:body:provider:)`): 401/403 → `invalidAPIKey`; 429 →255`rateLimited` (retryAfter not parsed here); 400/404/422 → `badRequest`; else `serverError`.256`extractMessage(from:)` (`ProviderProtocol.swift:127-145`) probes the provider error shapes257`{"error":{"message":…}}`, `{"error":"…"}`, `{"message":…}`, `{"detail":…}`, and Gemini's258top-level array form, falling back to the first 300 bytes of the body. The Router's error259translation to OpenAI wire format (`{"error":{message,type,param,code}}` + proper status) maps260directly onto these cases.261262---263264## 2. The complete model catalog265266**Swift types** (`Sources/ZyquoCloud/Models/AIModel.swift`):267268- `AIModel` (`AIModel.swift:15-43`): `id` (exact upstream model ID), `provider: ProviderID`,269 `displayName`, `contextWindow: Int`, `maxOutputTokens: Int?`, `capabilities: ModelCapabilities`,270 `pricing: ModelPricing?`, `parameterSupport: ParameterSupport`, `isLegacy`, `isRecommended`,271 `customBaseURL: URL?`.272- `ModelCapabilities` (`AIModel.swift:47-60`): `vision, tools, reasoning, streaming (default true),273 jsonMode, citations`.274- `ModelPricing` (`AIModel.swift:64-72`): **USD per 1M tokens**, `inputPerMTok` / `outputPerMTok`,275 with `func cost(inputTokens:outputTokens:) -> Double`. Cached/tiered pricing intentionally276 simplified to base rate; UI labels costs "estimates". **This is where the Router's cost meter277 gets its numbers.**278- `ParameterSupport` (`AIModel.swift:77-93`): `temperature, topP, frequencyPenalty,279 presencePenalty, usesMaxCompletionTokens, reasoningEffort, thinkingToggle, requiresStreaming`;280 preset `.openAIDefault = ParameterSupport(frequencyPenalty: true, presencePenalty: true)`.281- `TokenUsage` (`AIModel.swift:96-111`): `inputTokens, outputTokens, reasoningTokens?` + `+`.282283**Data lives in** `Sources/ZyquoCloud/Services/ModelCatalogData.swift` (1365 lines; `static let all284= openai + anthropic + xai + mistral + gemini + qwen + deepseek + kimi + perplexity + together +285deepinfra + cerebras`, `ModelCatalogData.swift:1361-1363`), generated from `docs/PROVIDERS.md` and286`docs/research/*.md` on **2026-07-30**, live-verified (Phase 7 amendments,287`docs/PROVIDERS.md:1638-1719`). Runtime access via `@MainActor final class ModelCatalog:288ObservableObject` (`Sources/ZyquoCloud/Services/ModelCatalog.swift:17-72`) which layers dynamic289`/models` refreshes (`applyLiveListing`, `unknownLiveIDs`) and user custom models on top, and290provides `cheapestModel(for:)` (non-reasoning preferred, min `outputPerMTok`).291292Scope note (`ModelCatalogData.swift:13-16`): **chat-completions-capable chat models only** —293embeddings, audio, realtime, image/video gen, moderation, and Responses-API-only models are294excluded by design.295296**Total: 170 models across 12 providers.** The Router exposes ALL of them as `provider/model-id`297(e.g. `anthropic/claude-opus-5`, `qwen/qwen3.7-max`, `together/moonshotai/Kimi-K3` — note Together/298DeepInfra IDs already contain `/`, so the Router's namespacing must split on the **first** `/`299only). Legend: V=vision, T=tools, R=reasoning, J=jsonMode, C=citations; prices are USD per MTok300in/out; ✩=isRecommended, †=isLegacy.301302### OpenAI (27) — `ModelCatalogData.swift:33-239`303304Preset `openAIReasoning` (`ModelCatalogData.swift:26-29`): no temperature/top_p,305`max_completion_tokens`, `reasoning_effort`.306307| Model ID | Ctx | Max out | Caps | $ in | $ out | Notes |308|---|---|---|---|---|---|---|309| `gpt-5.6-sol` ✩ | 1,050,000 | 128,000 | V T R J | 5.00 | 30.00 | openAIReasoning |310| `gpt-5.6-terra` ✩ | 1,050,000 | 128,000 | V T R J | 2.50 | 15.00 | openAIReasoning |311| `gpt-5.6-luna` | 1,050,000 | 128,000 | V T R J | 1.00 | 6.00 | openAIReasoning |312| `chat-latest` | 128,000 | — | V T J | 5.00 | 30.00 | freq/pres pen; rejects `max_tokens` (uses `max_completion_tokens`) |313| `gpt-5.5` | 400,000 | — | V T R J | 5.00 | 30.00 | openAIReasoning |314| `gpt-5.4` | 400,000 | 128,000 | V T R J | 2.50 | 15.00 | openAIReasoning |315| `gpt-5.4-mini` | 400,000 | — | V T R J | 0.75 | 4.50 | openAIReasoning |316| `gpt-5.4-nano` | 400,000 | — | V T R J | 0.20 | 1.25 | openAIReasoning |317| `gpt-5.3-chat-latest` | 128,000 | — | V T J | — | — | `max_completion_tokens` |318| `gpt-5.2` | 400,000 | 128,000 | V T R J | 1.75 | 14.00 | openAIReasoning |319| `gpt-5.2-chat-latest` | 128,000 | 16,000 | V T J | 1.75 | 14.00 | `max_completion_tokens` |320| `gpt-5.1` | 400,000 | 128,000 | V T R J | 1.25 | 10.00 | openAIReasoning |321| `gpt-5` | 400,000 | 128,000 | V T R J | 1.25 | 10.00 | openAIReasoning |322| `gpt-5-mini` | 400,000 | 128,000 | V T R J | 0.25 | 2.00 | openAIReasoning |323| `gpt-5-nano` | 400,000 | 128,000 | V T R J | 0.05 | 0.40 | openAIReasoning |324| `o3` | 200,000 | 100,000 | V T R J | 2.00 | 8.00 | openAIReasoning |325| `o4-mini` | 200,000 | 100,000 | V T R J | 1.10 | 4.40 | openAIReasoning |326| `o3-mini` † | 200,000 | 100,000 | T R J | 1.10 | 4.40 | openAIReasoning |327| `o1` † | 200,000 | 100,000 | V T R J | 15.00 | 60.00 | openAIReasoning |328| `gpt-4.1` † | 1,047,576 | 32,768 | V T J | 2.00 | 8.00 | .openAIDefault |329| `gpt-4.1-mini` † | 1,047,576 | 32,768 | V T J | 0.40 | 1.60 | .openAIDefault |330| `gpt-4.1-nano` † | 1,047,576 | 32,768 | V T J | 0.10 | 0.40 | .openAIDefault |331| `gpt-4o` † | 128,000 | 16,384 | V T J | 2.50 | 10.00 | .openAIDefault |332| `gpt-4o-mini` † | 128,000 | 16,384 | V T J | 0.15 | 0.60 | .openAIDefault |333| `gpt-4-turbo` † | 128,000 | 4,096 | V T J | 10.00 | 30.00 | .openAIDefault |334| `gpt-4` † | 8,192 | 8,192 | T | 30.00 | 60.00 | .openAIDefault |335| `gpt-3.5-turbo` † | 16,385 | 4,096 | T J | 0.50 | 1.50 | .openAIDefault |336337### Anthropic (11) — `ModelCatalogData.swift:243-327`338339| Model ID | Ctx | Max out | Caps | $ in | $ out | Notes |340|---|---|---|---|---|---|---|341| `claude-opus-5` ✩ | 1,000,000 | 128,000 | V T R J | 5.00 | 25.00 | no temp/top_p; thinkingToggle |342| `claude-sonnet-5` ✩ | 1,000,000 | 128,000 | V T R J | 3.00 | 15.00 | no temp/top_p; thinkingToggle |343| `claude-fable-5` | 1,000,000 | 128,000 | V T R J | 10.00 | 50.00 | thinking always on — **no toggle**; no temp/top_p |344| `claude-opus-4-8` | 1,000,000 | 128,000 | V T R J | 5.00 | 25.00 | no temp/top_p; thinkingToggle |345| `claude-opus-4-7` | 1,000,000 | 128,000 | V T R J | 5.00 | 25.00 | no temp/top_p; thinkingToggle |346| `claude-opus-4-6` | 1,000,000 | 128,000 | V T R J | 5.00 | 25.00 | temp/top_p OK; thinkingToggle |347| `claude-sonnet-4-6` | 1,000,000 | 128,000 | V T R J | 3.00 | 15.00 | temp/top_p OK; thinkingToggle |348| `claude-haiku-4-5-20251001` | 200,000 | 64,000 | V T R J | 1.00 | 5.00 | temp/top_p OK; thinkingToggle |349| `claude-opus-4-5-20251101` † | 200,000 | 64,000 | V T R J | 5.00 | 25.00 | thinkingToggle |350| `claude-sonnet-4-5-20250929` † | 1,000,000 | 64,000 | V T R J | 3.00 | 15.00 | thinkingToggle |351| `claude-opus-4-1-20250805` † | 200,000 | 32,000 | V T R J | 15.00 | 75.00 | thinkingToggle |352353### xAI (5) — `ModelCatalogData.swift:331-369`354355| Model ID | Ctx | Max out | Caps | $ in | $ out | Notes |356|---|---|---|---|---|---|---|357| `grok-4.5` ✩ | 500,000 | — | V T R J | 2.00 | 6.00 | reasoning_effort |358| `grok-4.3` | 1,000,000 | — | V T R J | 1.25 | 2.50 | reasoning_effort |359| `grok-4.20` | 1,000,000 | — | V T R J | 1.25 | 2.50 | rejects reasoning_effort |360| `grok-4.20-non-reasoning` | 1,000,000 | — | V T J | 1.25 | 2.50 | |361| `grok-code-fast-1` ✩ | 256,000 | — | V T R J | 1.00 | 2.00 | rejects reasoning_effort |362363Note: `grok-4.20`, `grok-4.20-non-reasoning`, `grok-code-fast-1` resolve on chat completions but do364**not** appear in xAI `/models` (`docs/PROVIDERS.md:1706`).365366### Mistral (10) — `ModelCatalogData.swift:373-451`367368| Model ID | Ctx | Caps | $ in | $ out | Notes |369|---|---|---|---|---|---|370| `mistral-medium-latest` ✩ (Medium 3.5) | 262,144 | V T R J | 1.50 | 7.50 | freq/pres + reasoning_effort |371| `mistral-large-latest` ✩ (Large 3) | 262,144 | V T J | 0.50 | 1.50 | .openAIDefault |372| `mistral-small-latest` ✩ (Small 4) | 262,144 | V T R J | 0.15 | 0.60 | freq/pres + reasoning_effort |373| `codestral-latest` | 256,000 | T J | 0.30 | 0.90 | .openAIDefault |374| `ministral-14b-latest` | 262,144 | V T J | 0.20 | 0.20 | .openAIDefault |375| `ministral-8b-latest` | 262,144 | V T J | 0.15 | 0.15 | .openAIDefault |376| `ministral-3b-latest` | 131,072 | V T J | 0.10 | 0.10 | .openAIDefault |377| `magistral-medium-latest` † | 131,072 | T R J | 2.00 | 5.00 | ThinkChunk content arrays |378| `devstral-latest` † (Devstral 2) | 262,144 | T J | 0.40 | 2.00 | |379| `open-mistral-nemo` † | 131,072 | T J | 0.15 | 0.15 | |380381### Google Gemini (14) — `ModelCatalogData.swift:455-559` (all max out 65,536 except Gemma 32,768)382383| Model ID | Ctx | Caps | $ in | $ out | Notes |384|---|---|---|---|---|---|385| `gemini-3.6-flash` ✩ | 1,048,576 | V T R J | 1.50 | 7.50 | reasoning_effort |386| `gemini-3.5-flash` | 1,048,576 | V T R J | 1.50 | 9.00 | reasoning_effort |387| `gemini-3.5-flash-lite` ✩ | 1,048,576 | V T R J | 0.30 | 2.50 | reasoning_effort |388| `gemini-3.1-pro-preview` ✩ | 1,048,576 | V T R J | 2.00 | 12.00 | reasoning_effort |389| `gemini-3.1-flash-lite` | 1,048,576 | V T R J | 0.25 | 1.50 | reasoning_effort |390| `gemini-2.5-pro` | 1,048,576 | V T R J | 1.25 | 10.00 | reasoning_effort |391| `gemini-2.5-flash` | 1,048,576 | V T R J | 0.30 | 2.50 | reasoning_effort |392| `gemini-2.5-flash-lite` | 1,048,576 | V T R J | 0.10 | 0.40 | reasoning_effort |393| `gemini-pro-latest` | 1,048,576 | V T R J | — | — | rolling alias, pricing varies |394| `gemini-flash-latest` | 1,048,576 | V T R J | — | — | rolling alias |395| `gemini-flash-lite-latest` | 1,048,576 | V T R J | — | — | rolling alias |396| `gemini-3-flash-preview` | 1,048,576 | V T R J | 0.50 | 3.00 | reasoning_effort |397| `gemma-4-26b-a4b-it` | 262,144 | J | — | — | max out 32,768 |398| `gemma-4-31b-it` | 262,144 | J | — | — | max out 32,768 |399400### Alibaba Qwen / DashScope (32) — `ModelCatalogData.swift:563-799` (pricing mostly unrecorded)401402| Model ID | Ctx | Max out | Caps | $ in | $ out | Notes |403|---|---|---|---|---|---|---|404| `qwen3.7-max` ✩ | 1,000,000 | — | T R J | 2.50 | 7.50 | enable_thinking |405| `qwen3.7-plus` ✩ | 1,000,000 | 65,536 | V T R J | 0.32 | 1.28 | enable_thinking |406| `qwen3.7-flash` ✩ | 1,000,000 | 65,536 | V T R J | 0.03 | 0.13 | enable_thinking |407| `qwen3.6-plus` | 1,000,000 | — | V T R J | — | — | enable_thinking |408| `qwen3.6-flash` | 1,000,000 | — | V T R J | — | — | enable_thinking |409| `qwen3.5-plus` | 1,000,000 | — | V T R J | — | — | enable_thinking |410| `qwen3.5-flash` | 1,000,000 | — | V T R J | — | — | enable_thinking |411| `qwen-max` | 128,000 | — | T R J | — | — | enable_thinking |412| `qwen-plus` | 1,000,000 | — | T R J | — | — | enable_thinking |413| `qwen-turbo` † | 1,000,000 | — | T R J | — | — | enable_thinking |414| `qwen-flash` | 1,000,000 | — | T R J | — | — | enable_thinking |415| `qwen3-coder-plus` | 1,000,000 | — | T J | — | — | |416| `qwen3-coder-flash` | 1,000,000 | — | T J | — | — | |417| `qwen3-coder-next` | 262,144 | — | T J | — | — | |418| `qwen3-coder-480b-a35b-instruct` | 262,144 | — | T J | — | — | |419| `qwen3-vl-plus` | 1,000,000 | 65,536 | V T R J | — | — | enable_thinking |420| `qwen3-vl-flash` | 1,000,000 | 65,536 | V T R J | — | — | enable_thinking |421| `qwen3-vl-235b-a22b-instruct` | 131,072 | — | V T J | — | — | |422| `qwen3-vl-235b-a22b-thinking` | 131,072 | — | V T R J | — | — | |423| `qvq-max` | 131,072 | — | V R J | — | — | **requiresStreaming** |424| `qwq-plus` | 131,072 | — | T R J | — | — | **requiresStreaming** |425| `qwen3.5-397b-a17b` | 262,144 | — | T R J | — | — | enable_thinking |426| `qwen3.5-122b-a10b` | 262,144 | — | T R J | — | — | enable_thinking |427| `qwen3.5-35b-a3b` | 262,144 | — | T R J | — | — | enable_thinking |428| `qwen3-235b-a22b-instruct-2507` | 262,144 | — | T J | — | — | |429| `qwen3-235b-a22b-thinking-2507` | 262,144 | — | T R J | — | — | |430| `qwen3-next-80b-a3b-instruct` | 262,144 | — | T J | — | — | |431| `qwen3-next-80b-a3b-thinking` | 262,144 | — | T R J | — | — | |432| `deepseek-v4-pro` | 1,000,000 | — | T R J | — | — | 3rd-party hosted; enable_thinking |433| `deepseek-v4-flash` | 1,000,000 | — | T R J | — | — | 3rd-party hosted; enable_thinking |434| `glm-5.2` | 198,000 | — | T R J | — | — | 3rd-party hosted; enable_thinking |435| `kimi-k2.7-code` | 262,144 | — | T R J | — | — | 3rd-party hosted; enable_thinking |436437### DeepSeek (2) — `ModelCatalogData.swift:803-820`438439| Model ID | Ctx | Max out | Caps | $ in | $ out | Notes |440|---|---|---|---|---|---|---|441| `deepseek-v4-flash` ✩ | 1,000,000 | 384,000 | T R J | 0.14 | 0.28 | reasoning_effort + thinkingToggle |442| `deepseek-v4-pro` ✩ | 1,000,000 | 384,000 | T R J | 0.435 | 0.87 | reasoning_effort + thinkingToggle |443444(`deepseek-chat`/`deepseek-reasoner` were retired 2026-07-24, `docs/PROVIDERS.md:42-44`.)445446### Kimi / Moonshot (12) — `ModelCatalogData.swift:824-920`447448| Model ID | Ctx | Max out | Caps | $ in | $ out | Notes |449|---|---|---|---|---|---|---|450| `kimi-k3` ✩ | 1,048,576 | 131,072 | V T R J | 3.00 | 15.00 | no temp/top_p; max_completion_tokens; reasoning_effort (thinking always on) |451| `kimi-k2.7-code` ✩ | 262,144 | — | V T R J | 0.95 | 4.00 | no temp/top_p; max_completion_tokens |452| `kimi-k2.7-code-highspeed` | 262,144 | — | V T R J | 1.90 | 8.00 | no temp/top_p; max_completion_tokens |453| `kimi-k2.6` | 262,144 | — | V T R J | 0.95 | 4.00 | + thinkingToggle |454| `kimi-k2.5` | 262,144 | — | V T R J | 0.60 | 3.00 | + thinkingToggle |455| `moonshot-v1-8k` † | 8,192 | — | T J | 0.20 | 2.00 | temp capped 1.0 upstream |456| `moonshot-v1-32k` † | 32,768 | — | T J | 1.00 | 3.00 | |457| `moonshot-v1-128k` † | 131,072 | — | T J | 2.00 | 5.00 | |458| `moonshot-v1-auto` † | 131,072 | — | T J | — | — | |459| `moonshot-v1-8k-vision-preview` † | 8,192 | — | V T J | 0.20 | 2.00 | |460| `moonshot-v1-32k-vision-preview` † | 32,768 | — | V T J | 1.00 | 3.00 | |461| `moonshot-v1-128k-vision-preview` † | 131,072 | — | V T J | 2.00 | 5.00 | |462463### Perplexity (4) — `ModelCatalogData.swift:924-955` (all `citations: true`; no `/models` endpoint)464465| Model ID | Ctx | Max out | Caps | $ in | $ out | Notes |466|---|---|---|---|---|---|---|467| `sonar` ✩ | 128,000 | 128,000 | J C | 1.00 | 1.00 | |468| `sonar-pro` ✩ | 200,000 | 8,000 | J C | 3.00 | 15.00 | |469| `sonar-reasoning-pro` | 128,000 | — | R J C | 2.00 | 8.00 | reasoning as inline `<think>` blocks |470| `sonar-deep-research` | 128,000 | — | R J C | 2.00 | 8.00 | multi-minute agentic runs — skipped in Cloud's automated sweep |471472### Together AI (16) — `ModelCatalogData.swift:959-1077`473474| Model ID | Ctx | Caps | $ in | $ out | Notes |475|---|---|---|---|---|---|476| `moonshotai/Kimi-K3` ✩ | 1,000,000 | T R J | 3.00 | 15.00 | |477| `moonshotai/Kimi-K2.7-Code` | 262,144 | T R J | 0.95 | 4.00 | |478| `moonshotai/Kimi-K2.6` | 262,144 | T R J | 1.20 | 4.50 | |479| `deepseek-ai/DeepSeek-V4-Pro` ✩ | 512,000 | T R J | 1.74 | 3.48 | |480| `zai-org/GLM-5.2` | 512,000 | T R J | 1.40 | 4.40 | |481| `Qwen/Qwen3.7-Max` | 1,000,000 | T R J | 1.25 | 3.75 | **requiresStreaming** |482| `Qwen/Qwen3.7-Plus` | 1,000,000 | T J | 0.32 | 1.28 | **requiresStreaming** |483| `Qwen/Qwen3.6-Plus` | 1,000,000 | T J | 0.50 | 3.00 | **requiresStreaming** |484| `Qwen/Qwen3.5-9B` | 262,144 | T J | 0.17 | 0.25 | **requiresStreaming**; streams completions-style (`choices[].text`) |485| `meta-llama/Llama-3.3-70B-Instruct-Turbo` | 131,072 | T J | 1.04 | 1.04 | |486| `openai/gpt-oss-120b` ✩ | 131,072 | T R J | 0.15 | 0.60 | reasoning_effort |487| `openai/gpt-oss-20b` | 131,072 | T R J | 0.05 | 0.20 | reasoning_effort |488| `nvidia/nemotron-3-ultra-550b-a55b` | 512,288 | T R J | 0.60 | 3.60 | |489| `MiniMaxAI/MiniMax-M3` | 524,288 | T R J | 0.30 | 1.20 | |490| `google/gemma-4-31B-it` | 262,144 | T J | 0.39 | 0.97 | **requiresStreaming**; vision disabled 2026-07-30 (empty answers on image input) |491| `thinkingmachines/Inkling` | 524,288 | T R J | 1.00 | 4.05 | |492493### DeepInfra (34) — `ModelCatalogData.swift:1081-1328`494495| Model ID | Ctx | Caps | $ in | $ out | Notes |496|---|---|---|---|---|---|497| `anthropic/claude-fable-5` | 1,000,000 | V T R J | 10.00 | 50.00 | proxied frontier |498| `anthropic/claude-opus-5` | 1,000,000 | V T R J | 5.00 | 25.00 | proxied |499| `anthropic/claude-sonnet-5` | 1,000,000 | V T R J | 2.00 | 10.00 | proxied |500| `anthropic/claude-opus-4-8` | 1,000,000 | V T R J | 5.00 | 25.00 | proxied |501| `anthropic/claude-haiku-4-5` | 200,000 | V T R J | 1.00 | 5.00 | proxied |502| `google/gemini-3.1-pro` | 1,000,000 | V T R J | 2.00 | 12.00 | proxied |503| `google/gemini-3.5-flash` | 1,000,000 | V T R J | 1.50 | 9.00 | proxied |504| `google/gemini-3.1-flash-lite` | 1,000,000 | V T J | 0.25 | 1.50 | proxied |505| `google/gemini-2.5-pro` | 1,000,000 | V T R J | 1.25 | 10.00 | proxied |506| `google/gemini-2.5-flash` | 1,000,000 | V T R J | 0.30 | 2.50 | proxied |507| `deepseek-ai/DeepSeek-V4-Pro` ✩ | 1,048,576 | T R J | 1.30 | 2.60 | |508| `deepseek-ai/DeepSeek-V4-Flash` ✩ | 1,048,576 | T J | 0.09 | 0.18 | |509| `deepseek-ai/DeepSeek-V3.1` | 163,840 | T R J | 0.25 | 0.95 | |510| `deepseek-ai/DeepSeek-R1-0528` | 163,840 | R | 0.50 | 2.15 | reasoning only |511| `moonshotai/Kimi-K2.7-Code` | 262,144 | T R J | 0.74 | 3.50 | |512| `moonshotai/Kimi-K2.6` | 262,144 | T R J | 0.75 | 3.50 | |513| `moonshotai/Kimi-K2.5` | 262,144 | T J | 0.45 | 2.25 | **requiresStreaming** |514| `zai-org/GLM-5.2` ✩ | 1,048,576 | T R J | 0.75 | 2.40 | |515| `zai-org/GLM-4.7` | 202,752 | T R J | 0.40 | 1.75 | |516| `Qwen/Qwen3.7-Max` | 256,000 | T R J | 2.50 | 7.50 | |517| `Qwen/Qwen3.5-397B-A17B` | 262,144 | T R J | 0.45 | 3.00 | |518| `Qwen/Qwen3-235B-A22B-Instruct-2507` | 262,144 | T J | 0.09 | 0.55 | |519| `Qwen/Qwen3-235B-A22B-Thinking-2507` | 262,144 | T R J | 0.23 | 2.30 | |520| `Qwen/Qwen3-Coder-480B-A35B-Instruct-Turbo` | 262,144 | T J | 0.30 | 1.00 | |521| `Qwen/Qwen3-VL-235B-A22B-Instruct` | 262,144 | V T J | 0.20 | 0.88 | |522| `meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8` | 1,048,576 | V T J | 0.20 | 0.80 | |523| `meta-llama/Llama-4-Scout-17B-16E-Instruct` | 327,680 | V T J | 0.10 | 0.30 | |524| `meta-llama/Llama-3.3-70B-Instruct-Turbo` | 131,072 | T J | 0.10 | 0.32 | |525| `meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo` | 131,072 | T J | 0.02 | 0.04 | |526| `openai/gpt-oss-120b` ✩ | 131,072 | T R J | 0.037 | 0.17 | reasoning_effort |527| `openai/gpt-oss-20b` | 131,072 | T R J | 0.03 | 0.14 | reasoning_effort |528| `MiniMaxAI/MiniMax-M3` | 524,288 | T R J | 0.30 | 1.20 | |529| `nvidia/NVIDIA-Nemotron-3-Ultra-550B-A55B` | 262,144 | T R J | 0.50 | 2.20 | |530| `mistralai/Mistral-Small-3.2-24B-Instruct-2506` | 128,000 | V T J | 0.075 | 0.20 | |531532(`google/gemma-4-31B-it` was **removed** from DeepInfra 2026-07-30 — endpoint hangs 60s+ with zero533bytes, `ModelCatalogData.swift:1326-1327`, `docs/PROVIDERS.md:1714-1715`.)534535### Cerebras (3) — `ModelCatalogData.swift:1332-1357` (all max out 40,000; `max_completion_tokens` required)536537| Model ID | Ctx | Caps | $ in | $ out | Notes |538|---|---|---|---|---|---|539| `gpt-oss-120b` ✩ | 131,072 | T R J | 0.35 | 0.75 | reasoning_effort |540| `gemma-4-31b` | 131,072 | V T R J | 0.99 | 1.49 | reasoning_effort |541| `zai-glm-4.7` † | 131,072 | T R J | 2.25 | 2.75 | discontinued 2026-08-17 |542543---544545## 3. The secure key vault — `SecureKeyStore`546547`Sources/ZyquoCloud/Services/SecureKeyStore.swift` (174 lines). **Confirmed: deliberately NOT the548macOS Keychain** — the header says so verbatim (`SecureKeyStore.swift:8`). The only `Security.framework`549usage in the file is `SecRandomCopyBytes(kSecRandomDefault, …)` for salt generation550(`SecureKeyStore.swift:161-166`) — no `SecItem*` / keychain item APIs anywhere. A repo grep confirms551no other Keychain usage in Zyquo Cloud.552553### File location & format554555- Vault file: `~/Library/Application Support/ZyquoCloud/vault.zq` — built as556 `PersistenceService.shared.rootDirectory.appendingPathComponent("vault.zq")`557 (`SecureKeyStore.swift:56-57`; root dir from558 `Sources/ZyquoCloud/Services/PersistenceService.swift:24-27`). **Router equivalent:**559 `~/Library/Application Support/ZyquoRouter/vault.zq` (same format).560- Binary layout (`SecureKeyStore.swift:11-13`): **`[salt 32B][AES-GCM nonce 12B][ciphertext+tag 16B]`**561 — everything after the salt is the CryptoKit `AES.GCM.SealedBox.combined` representation562 (nonce ‖ ciphertext ‖ tag), opened with `AES.GCM.SealedBox(combined:)` + `AES.GCM.open(_:using:)`563 (`SecureKeyStore.swift:71-72`) and produced by `AES.GCM.seal(_:using:).combined`564 (`SecureKeyStore.swift:84-85`). Nonce is generated by CryptoKit per seal (fresh every save).565- Plaintext: a JSON dictionary `{"openai": "sk-…", "anthropic": "sk-ant-…", …}` — keys are566 **`ProviderID.rawValue` strings** (`SecureKeyStore.swift:13`, `key(for:)`/`setKey(_:for:)` at567 `SecureKeyStore.swift:94-108`).568- Written atomically with `.completeFileProtection` (`SecureKeyStore.swift:91`); the directory is569 created on demand.570571### Master key derivation (`SecureKeyStore.swift:118-159`)572573```swift574HKDF<SHA256>.deriveKey(575 inputKeyMaterial: SymmetricKey(data: machineEntropy() ‖ pepper),576 salt: <32-byte vault salt>,577 info: Data("ZyquoCloud.vault.v1".utf8),578 outputByteCount: 32 // AES-256 key579)580```581582- `machineEntropy` = **IOPlatformUUID** (read from IOKit's `IOPlatformExpertDevice` registry entry583 via `IORegistryEntryCreateCFProperty(…, kIOPlatformUUIDKey, …)`,584 `SecureKeyStore.swift:138-148`) ‖ `NSHomeDirectory()` — binds the vault to this machine **and**585 account. Injectable via init for tests (`SecureKeyStore.swift:52-59`,586 `Tests/ZyquoCloudTests/SecureKeyStoreTests.swift`).587- **Pepper**: 30 compiled-in bytes, stored XOR `0x5A` so the value never appears verbatim in the588 binary, reassembled at runtime (`pepper()`, `SecureKeyStore.swift:152-159`).589- Salt: 32 random bytes (`SecRandomCopyBytes`) generated on first save; **re-saves reuse the590 existing salt** (`existingSalt()`, `SecureKeyStore.swift:169-173`) so the derived key stays stable.591- Constants: `saltLength = 32`, `keyLength = 32` (`SecureKeyStore.swift:45-46`).592593### Public API surface (`SecureKeyStore.swift:61-114`)594595```swift596init(vaultURL: URL? = nil, machineEntropy: (() throws -> Data)? = nil)597func loadKeys() throws -> [String: String] // empty dict if no vault file598func saveKeys(_ keys: [String: String]) throws // atomic full-dictionary rewrite599func key(for provider: ProviderID) throws -> String?600func setKey(_ apiKey: String, for provider: ProviderID) throws601func deleteKey(for provider: ProviderID) throws602static func redacted(_ apiKey: String) -> String // "••••abcd" (last 4 only)603```604605Errors: `VaultError.corrupted` ("The key vault is damaged or belongs to another machine." — also606thrown when GCM auth fails, i.e. wrong machine) and `VaultError.machineIdentityUnavailable`607(`SecureKeyStore.swift:31-43`).608609The UI-facing wrapper is `@MainActor final class KeyVaultStore: ObservableObject`610(`Sources/ZyquoCloud/ViewModels/KeyVaultStore.swift:16-93`): per-provider `KeyStatus`611(`unset/saved/testing/verified(latency:)/failed(message:)`), redacted display strings, and612`testKey(for:catalog:)` using `ProviderRegistry` + `ModelCatalog.cheapestModel(for:)`. Keys are613decrypted on demand and never retained beyond the call (`KeyVaultStore.swift:55-61`).614615**Router decision for identical UX**: reuse `SecureKeyStore` byte-for-byte in design. Two constants616differ per app: the vault path root (`ZyquoRouter` folder) and the HKDF `info` string617(`"ZyquoCloud.vault.v1"`). Keeping `info` identical AND pointing at Cloud's vault file would let the618apps literally share one vault; the Zyquo Router CLAUDE.md asks for the **same design and format**619(users manage keys identically), not necessarily the same file — decide in Phase 2/3. If the Router620keeps its own vault, use `info: "ZyquoRouter.vault.v1"` and its own `vault.zq` under621`~/Library/Application Support/ZyquoRouter/`. Either way: no Keychain, ever.622623---624625## 4. Provider streaming quirks the Router's translation layer must normalize626627All recorded in code and in `docs/PROVIDERS.md` (cross-provider notes at lines 32-46; Phase 7628amendments at lines 1638-1719):6296301. **Reasoning content field zoo** — normalize to one field (Router decision: `reasoning_content`631 on message/delta, per OpenAI-compat majority):632 - `delta.reasoning_content`: DeepSeek (on by default for v4-flash), Qwen, Kimi K-series, some633 DeepInfra models (`OpenAICompatibleClient.swift:133-146`).634 - `delta.reasoning` (no `_content`): some hosts (decoded as fallback,635 `OpenAICompatibleClient.swift:344`); Together exposes `message.reasoning` for hosted636 reasoning models (`docs/PROVIDERS.md:28`).637 - Anthropic: `thinking_delta` inside `content_block_delta` events; non-streaming `thinking`638 content blocks (`AnthropicClient.swift:226-232,270-271`).639 - Mistral: `content` arrives as an **array of ThinkChunk/TextChunk objects**640 (`{type:"thinking",thinking:[{type:"text",text:…}]}`) — must be flattened641 (`OpenAICompatibleClient.swift:143-184`; `docs/PROVIDERS.md:1704`).642 - Perplexity `sonar-reasoning-pro`: reasoning arrives as inline **`<think>…</think>` text** in643 the content itself (`docs/PROVIDERS.md:27,37`) — Cloud does not split it; the Router must644 decide (pass through or extract).645 - OpenAI: **no reasoning text at all** on chat completions — only646 `usage.completion_tokens_details.reasoning_tokens` (`docs/PROVIDERS.md:178`).6472. **Usage-in-stream behavior** (`OpenAICompatibleClient.swift:243-255`; `docs/PROVIDERS.md:38-39`):648 `stream_options:{include_usage:true}` needed for OpenAI, xAI, Gemini-compat, DeepSeek, Kimi,649 Together, Cerebras (final chunk has empty `choices` + `usage`); Qwen, DeepInfra, Perplexity650 include usage automatically (and Mistral rejects the param). Anthropic splits usage:651 `input_tokens` in `message_start`, final `output_tokens` in `message_delta`.6523. **Keep-alive comments**: DeepSeek sends `: keep-alive` SSE comment lines — `SSEParser` drops653 any `:`-prefixed line (`StreamingService.swift:36`). Undecodable data chunks are skipped, not654 fatal (`OpenAICompatibleClient.swift:339-342`).6554. **Finish-reason quirks**: Together emits nonstandard `finish_reason: "eos"`656 (`docs/PROVIDERS.md:35`) — the Router must map it to `stop`. Anthropic uses `stop_reason`657 (`end_turn`, `max_tokens`, `stop_sequence`, `tool_use`) delivered in `message_delta`, which the658 Router maps to OpenAI `finish_reason` (`stop`/`length`/`stop`/`tool_calls`).6595. **Together completions-style streams**: some models (`Qwen/Qwen3.5-9B`,660 `google/gemma-4-31B-it`) stream tokens in `choices[].text` instead of `delta.content`661 (`OpenAICompatibleClient.swift:122-124`; `docs/PROVIDERS.md:1712-1713`).6626. **Models that reject non-streaming calls** (`ParameterSupport.requiresStreaming`,663 `AIModel.swift:88-90`): Qwen `qvq-max`/`qwq-plus`; Together `Qwen3.7-Max/-Plus`, `Qwen3.6-Plus`,664 `Qwen3.5-9B`, `google/gemma-4-31B-it`; DeepInfra `moonshotai/Kimi-K2.5`. For a non-streaming665 router request, aggregate the stream (Cloud's `completeViaStream` pattern).6667. **Perplexity citations**: top-level `citations: [String]` (URLs) + `search_results` (titles) on667 chunks and responses — surfaced once per stream (`OpenAICompatibleClient.swift:356-359,468-476`).668 No `/models` endpoint (404): the Router must serve Perplexity models from the built-in catalog669 only.6708. **Gemini compat endpoint**: `/models` IDs prefixed `models/` (stripped,671 `OpenAICompatibleClient.swift:411,420`); unknown fields like `extra_content`/`thought_signature`672 in deltas must be ignored; native-only features (thought summaries, `thoughtsTokenCount`) are673 not exposed on the compat endpoint (`docs/PROVIDERS.md:560`).6749. **Unknown-field tolerance generally**: OpenAI adds `obfuscation` fields; decoders must ignore675 unknown JSON keys everywhere (`docs/PROVIDERS.md:34-35`).67610. **Anthropic mid-stream errors**: an `error` SSE event can arrive with HTTP 200677 (`AnthropicClient.swift:240-243`) — the Router must convert it into an OpenAI-format error678 (or a terminal chunk if the stream already started).67911. **Parameter strip tables** (per-model `ParameterSupport`): OpenAI reasoning models and Claude680 4.7+/5 reject `temperature`/`top_p`; Cerebras + OpenAI reasoning + Kimi K-series require681 `max_completion_tokens`; Mistral `reasoning_effort` accepts only `high`/`none`; Qwen682 `enable_thinking` only when streaming; xAI `grok-4.20`/`grok-code-fast-1` reject683 `reasoning_effort`. This is exactly the Router's `CompatAdjuster` data.68412. **Vision minimums**: xAI and Qwen reject images smaller than 8px (`docs/PROVIDERS.md:1707`).68513. **Anthropic streams have no `data: [DONE]`** — termination is the `message_stop` event; the686 Router's SSEWriter must synthesize `[DONE]` itself for downstream OpenAI clients.687688---689690## 5. Reuse plan — files to port from Cloud → Router691692### 5.1 Port nearly verbatim (change header `Zyquo Cloud` → `Zyquo Router`; keep type names)693694| Source (zyquo-cloud) | Destination (zyquo-router) | Changes |695|---|---|---|696| `Sources/ZyquoCloud/Models/ProviderID.swift` | `Sources/ZyquoRouter/Models/ProviderID.swift` | header only (keep all base URLs, `WireFormat`, `supportsModelListing`) |697| `Sources/ZyquoCloud/Models/AIModel.swift` | `Sources/ZyquoRouter/Models/AIModel.swift` | header only (`AIModel`, `ModelCapabilities`, `ModelPricing`, `ParameterSupport`, `TokenUsage`) |698| `Sources/ZyquoCloud/Services/ModelCatalogData.swift` | `Sources/ZyquoRouter/Services/ModelCatalogData.swift` | header only — **all 170 models, verbatim**; keep in sync with Cloud going forward |699| `Sources/ZyquoCloud/Services/ModelCatalog.swift` | `Sources/ZyquoRouter/Services/ModelCatalog.swift` | header; keep `@MainActor ObservableObject` for the UI, but the server-side `RequestRouter` needs a `Sendable` snapshot of `all` (don't hop to the main actor per request) |700| `Sources/ZyquoCloud/Services/StreamingService.swift` | `Sources/ZyquoRouter/Services/StreamingService.swift` | header; change User-Agent to `ZyquoRouter/1.0 (macOS)`; consider moving retry policy into `Router/RetryPolicy.swift` (see 5.3) |701| `Sources/ZyquoCloud/Services/SecureKeyStore.swift` | `Sources/ZyquoRouter/Services/SecureKeyStore.swift` | header; vault root becomes `~/Library/Application Support/ZyquoRouter/`; decide HKDF `info` (`"ZyquoRouter.vault.v1"` for a separate vault, or keep Cloud's string + path to share one vault); keep pepper mechanism (may reuse the same obfuscated bytes) |702| `Sources/ZyquoCloud/Services/PersistenceService.swift` | `Sources/ZyquoRouter/Services/PersistenceService.swift` | header; root folder `ZyquoRouter`; drop `Conversation` specifics, keep generic `load/save` |703| `Sources/ZyquoCloud/Providers/ProviderProtocol.swift` | `Sources/ZyquoRouter/Providers/ProviderProtocol.swift` | header; **extend** (see 5.2) |704| `Sources/ZyquoCloud/Providers/ProviderRegistry.swift` | `Sources/ZyquoRouter/Providers/ProviderRegistry.swift` | header only |705| `Sources/ZyquoCloud/Providers/OpenAICompatibleClient.swift` | `Sources/ZyquoRouter/Providers/OpenAICompatibleClient.swift` | header + extensions (see 5.2) |706| `Sources/ZyquoCloud/Providers/AnthropicClient.swift` | `Sources/ZyquoRouter/Providers/AnthropicClient.swift` | header + extensions (see 5.2) |707| `Tests/ZyquoCloudTests/SSEParserTests.swift` | `Tests/ZyquoRouterTests/SSEParserTests.swift` | header/module rename |708| `Tests/ZyquoCloudTests/SecureKeyStoreTests.swift` | `Tests/ZyquoRouterTests/SecureKeyStoreTests.swift` | header/module rename |709| (subset of) `Sources/ZyquoCloud/Models/Message.swift` | `Sources/ZyquoRouter/Models/Message.swift` | keep `Message`, `Attachment`, `Citation`; drop chat-UI fields if unused (`isStreaming`, `errorText`) or keep for log display |710711`KeyVaultStore.swift` (ViewModel) ports with light edits for the Router's Keys screen (Provider712Keys tab is spec'd "identical UX to Zyquo Cloud").713714### 5.2 What Cloud's clients DON'T give the Router (must be added, not just ported)715716Cloud is a chat app; its clients expose a **UI-oriented, lossy** event stream. The Router needs a717**spec-complete OpenAI surface**. Gaps found in the code:7187191. **No tool calling anywhere.** `WireRequest` has no `tools`/`tool_choice`; `WireDelta` decodes no720 `tool_calls`; `ChatEvent` has no tool case; Anthropic's client sends no `tools` and ignores721 `tool_use`/`input_json_delta` (grep of `Sources/ZyquoCloud/Providers/` for "tool" → zero hits).722 The Router must extend the wire types and `ChatEvent` (e.g. `.toolCallDelta(index:id:name:argumentsDelta:)`)723 and implement Anthropic `tool_use`/`tool_result` ↔ OpenAI `tool_calls`/`role:"tool"` translation724 per `docs/ROUTER-RESEARCH.md`.7252. **No `response_format`/JSON mode, `stop`, `n`, `seed`, `logprobs`, `user`** in `WireRequest` —726 the catalog tracks `jsonMode` capability but Cloud never sends it. Add these fields (gated by727 `CompatAdjuster`).7283. **Lossy events**: `ChatEvent` collapses per-choice structure (only `choices.first` is read,729 `OpenAICompatibleClient.swift:343`), drops chunk `id`/`created`/`model`/`system_fingerprint`,730 and merges role/content deltas. Fine for a chat UI; the Router must emit **byte-perfect731 `chat.completion.chunk`s**. Recommended approach: keep Cloud's request-construction +732 `StreamingService`/`SSEParser` + auth/quirk logic **as-is**, but widen the response path —733 either (a) make the wire response types non-private and add a raw-chunk streaming API734 (`streamRawChunks(_:apiKey:) -> AsyncThrowingStream<Data, Error>` yielding upstream SSE data735 payloads for OpenAI-compatible providers, enabling near-pass-through), plus a translated path736 for Anthropic; or (b) enrich `ChatEvent` to carry everything (finish per choice, tool deltas,737 ids, raw usage). Option (a) is closest to how LiteLLM/OpenRouter behave for compat upstreams and738 preserves fidelity; the Anthropic translator then builds spec-exact chunks from the named739 events already parsed in `AnthropicClient.streamChat`.7404. **`ChatRequest` is UI-shaped** (`model: AIModel`, `messages: [Message]` with attachments). The741 Router receives OpenAI wire JSON; it should define a canonical internal request742 (`Translate/OpenAINormalizer.swift`) and map it into the ported clients' body builders — or743 refactor `buildBody` to accept the canonical type. Keep `ParameterSupport` gating exactly as744 Cloud does; it *is* the per-provider param strip/translate table (`CompatAdjuster` seed data).7455. **`ChatParameters`** lives in `Sources/ZyquoCloud/Models/Conversation.swift` (temperature, topP,746 maxTokens, frequency/presencePenalty, reasoningEffort, thinkingEnabled) — port the struct (or747 inline it) since both clients consume it.7486. **Retry semantics**: Cloud retries only non-streaming calls (3 attempts in749 `StreamingService.postJSON`). The Router's `RetryPolicy` (Phase 2 layout) should own this and750 also handle pre-first-byte retry for streaming + `Retry-After` propagation to clients.7517. **Gemini**: no native client exists (see §1.1). Near-term: reuse the compat path (verified live752 by Cloud, including vision via data URIs and `reasoning_effort`). If Phase 3's gate demands the753 native `generateContent` translation, `GeminiTranslator.swift` is **net-new work** guided by754 `docs/ROUTER-RESEARCH.md` — nothing to port from Cloud beyond `docs/PROVIDERS.md:524-696`755 research.7568. **Usage estimation**: when upstream omits usage, Cloud just shows nothing. The Router spec757 requires estimated-and-flagged usage — net-new (token estimation), though `TokenUsage` and758 `ModelPricing.cost` port directly for the metering.759760### 5.3 Destination layout (matches Router CLAUDE.md Phase 2)761762```763Sources/ZyquoRouter/764├── Models/ ProviderID.swift, AIModel.swift, Message.swift (+ChatParameters) ← ported765├── Providers/ ProviderProtocol.swift, ProviderRegistry.swift,766│ OpenAICompatibleClient.swift, AnthropicClient.swift ← ported + extended767│ (GeminiClient.swift only if native path chosen — net-new)768├── Services/ StreamingService.swift, SecureKeyStore.swift,769│ ModelCatalog.swift, ModelCatalogData.swift, PersistenceService.swift ← ported770├── Translate/ OpenAINormalizer.swift, AnthropicTranslator.swift,771│ GeminiTranslator.swift, CompatAdjuster.swift ← new (seeded by ParameterSupport + §4)772├── Router/ RequestRouter.swift, RetryPolicy.swift, UsageMeter.swift ← new (RetryPolicy absorbs postJSON backoff)773└── Server/ … ← new774```775776### 5.4 Porting checklist777778- [ ] Rewrite every file header comment `Zyquo Cloud` → `Zyquo Router` (mandatory header sweep).779- [ ] `ZyquoCloud/1.0` User-Agent → `ZyquoRouter/1.0`; HKDF info + vault path decision recorded.780- [ ] Keep type names Cloud uses: `ProviderClient`, `ProviderID`, `WireFormat`, `AIModel`,781 `ModelCatalog`, `SecureKeyStore`, `SSEParser`, `TokenUsage`, `ProviderError` (Router782 CLAUDE.md's uniform-naming rule already includes `ProviderClient`).783- [ ] `ModelCatalogData.swift` stays byte-identical to Cloud's (modulo header) — single source of784 truth for the `GET /v1/models` catalog and pricing/cost metering.785- [ ] Extend wire types for tools / response_format / stop / n / seed; add raw-chunk streaming path.786- [ ] Port fixture-worthy behaviors into unit tests: SSE blank-line handling, `: keep-alive`,787 Mistral ThinkChunk flattening, Together `choices[].text`, Anthropic event mapping, Gemini788 `models/` prefix strip, `finish_reason:"eos"` → `stop`.789