# Zyquo Agent — Provider Reuse (Phase 0.B, contract for the provider layer) Compiled 2026-07-30 from an in-depth study of the sibling **Zyquo Cloud** repository at `/Users/simon-pierreboucher/Desktop/zyquo-cloud` (source files under `Sources/ZyquoCloud/`, plus `docs/PROVIDERS.md` — Cloud's own live-probed provider research — and `docs/VERIFICATION.md`). **Rule of this document:** Zyquo Agent calls every model *exactly* the way Zyquo Cloud does. The files listed in §1 are ported (near-)verbatim; the only addition is a **normalized tool-calling interface** layered onto the same `ProviderClient` protocol (§5–§6). The model picker offers **the same catalog as Cloud** (§2) with the agent-capable subset marked and defaulted (§3). The key vault is byte-format-compatible in design (§4). --- ## 1. How each provider's API is called (Cloud's provider layer, file by file) ### 1.1 Architecture overview Cloud's entire provider layer is **four files** plus shared networking and models: | File (in `zyquo-cloud/Sources/ZyquoCloud/`) | Role | |---|---| | `Providers/ProviderProtocol.swift` | `ChatRequest`, `ChatEvent`, `protocol ProviderClient`, `ProviderError` (typed, human-readable, maps HTTP status + heterogeneous error bodies) | | `Providers/ProviderRegistry.swift` | The *only* place that maps provider → client via `ProviderID.wireFormat` | | `Providers/OpenAICompatibleClient.swift` | ONE client for the 11 OpenAI-schema providers + custom endpoints; all quirks live here | | `Providers/AnthropicClient.swift` | Native Anthropic Messages API (`/v1/messages`) client | | `Models/ProviderID.swift` | The 12 built-in providers + `.custom`: display names, base URLs, `wireFormat`, `supportsModelListing` | | `Models/AIModel.swift` | `AIModel`, `ModelCapabilities` (incl. `tools: Bool`), `ModelPricing`, `ParameterSupport`, `TokenUsage` | | `Models/Message.swift` | `Message` (role/text/reasoning/attachments/citations/usage/cost), `Attachment`, `Citation` | | `Models/Conversation.swift` | `ChatParameters` (temperature, topP, maxTokens, penalties, `reasoningEffort`, `thinkingEnabled`), `Persona` | | `Services/StreamingService.swift` | `SSEEvent`, incremental `SSEParser`, shared `URLSession`, `sseEvents(for:provider:)`, `postJSON`/`getJSON` with backoff | | `Services/ModelCatalog.swift` | `@MainActor ObservableObject` catalog: built-in + custom + live `/models` diff + favorites + `cheapestModel(for:)` + `defaultModel` | | `Services/ModelCatalogData.swift` | The full built-in catalog (generated from `docs/PROVIDERS.md`; 170 models — reproduced in §2) | | `Services/SecureKeyStore.swift` | AES-256-GCM key vault, NO Keychain (§4) | | `Verify/VerifyHarness.swift` | Phase-7 live verification harness pattern (env keys, per-model OK-test, stdout table + doc output) — reuse the pattern for Agent's tool-calling verification | **Key finding: there is NO `GeminiClient` in Zyquo Cloud.** Gemini is served through Google's **OpenAI-compatibility endpoint** (`https://generativelanguage.googleapis.com/v1beta/openai`, Bearer auth) via `OpenAICompatibleClient`. Cloud's research (`docs/PROVIDERS.md` §Gemini, verified live) confirms the compat endpoint supports chat + streaming + **function calling (`tools`)** + structured outputs + vision + `reasoning_effort`. Zyquo Agent's CLAUDE.md sketch lists a `GeminiClient.swift`; per this study the correct, Cloud-identical approach is to **keep Gemini on the compat endpoint** (Gemini tool calls then arrive as standard OpenAI `tool_calls`, one uniform streaming path). A native `GeminiClient` (functionCall/functionResponse parts) is only needed if Phase 7 finds compat-endpoint tool streaming inadequate — see §5.4. ### 1.2 Core protocol types (`ProviderProtocol.swift`) ```swift struct ChatRequest { // provider-agnostic; clients translate to wire format var model: AIModel var systemPrompt: String? var messages: [Message] var parameters: ChatParameters var stream: Bool = true } enum ChatEvent { // streamed back to the UI case reasoningDelta(String) case textDelta(String) case citations([Citation]) case usage(TokenUsage) case finished(reason: String?) } protocol ProviderClient { var providerID: ProviderID { get } func streamChat(_ request: ChatRequest, apiKey: String) -> AsyncThrowingStream func complete(_ request: ChatRequest, apiKey: String) async throws -> Message func listModelIDs(apiKey: String) async throws -> [String] } // + extension ProviderClient.testKey(_:fallbackModel:) -> TimeInterval // (uses /models when supported, else a 16-token "Reply with exactly: OK" completion; // fallbackModel needed only for Perplexity, which has no /models endpoint) ``` `ProviderError` cases: `invalidAPIKey`, `rateLimited(retryAfter:)`, `serverError(status:message:)`, `badRequest`, `networkError`, `invalidResponse`, `missingAPIKey`, `noModelAvailable`, `cancelled` — each with a polished `errorDescription`. `ProviderError.from(status:body:provider:)` maps 401/403→invalidAPIKey, 429→rateLimited, 400/404/422→badRequest, else serverError, and `extractMessage(from:)` tolerates all observed error shapes: `{"error":{"message":…}}`, `{"error":"…"}`, `{"message":…}`, `{"detail":…}`, and Gemini's array-wrapped errors. ### 1.3 Provider table (from `ProviderID.swift`, all verified live by Cloud on 2026-07-30) | # | `ProviderID` case | Display name | Base URL | Auth | Wire format | `/models`? | |---|---|---|---|---|---|---| | 1 | `.openai` | OpenAI | `https://api.openai.com/v1` | `Authorization: Bearer` | OpenAI chat/completions | ✅ | | 2 | `.anthropic` | Anthropic | `https://api.anthropic.com/v1` | `x-api-key` + `anthropic-version: 2023-06-01` | **Anthropic Messages** | ✅ (rich metadata, `?limit=100`) | | 3 | `.xai` | xAI | `https://api.x.ai/v1` | Bearer | OpenAI-compat | ✅ | | 4 | `.mistral` | Mistral | `https://api.mistral.ai/v1` | Bearer | OpenAI-compat | ✅ (per-model capability flags incl. `function_calling`) | | 5 | `.gemini` | Google Gemini | `https://generativelanguage.googleapis.com/v1beta/openai` | Bearer (compat endpoint) | OpenAI-compat | ✅ (IDs prefixed `models/` — client strips) | | 6 | `.qwen` | Alibaba Qwen | `https://dashscope-intl.aliyuncs.com/compatible-mode/v1` | Bearer | OpenAI-compat | ✅ | | 7 | `.deepseek` | DeepSeek | `https://api.deepseek.com` | Bearer | OpenAI-compat | ✅ (2 models) | | 8 | `.kimi` | Kimi (Moonshot) | `https://api.moonshot.ai/v1` | Bearer | OpenAI-compat | ✅ | | 9 | `.perplexity` | Perplexity | `https://api.perplexity.ai` | Bearer | OpenAI-compat + search extras | ❌ (404 — `supportsModelListing == false`) | | 10 | `.together` | Together AI | `https://api.together.xyz/v1` | Bearer | OpenAI-compat | ✅ (bare array, not `{"data":[…]}`) | | 11 | `.deepinfra` | DeepInfra | `https://api.deepinfra.com/v1/openai` | Bearer | OpenAI-compat | ✅ | | 12 | `.cerebras` | Cerebras | `https://api.cerebras.ai/v1` | Bearer | OpenAI-compat | ✅ (3 models) | | — | `.custom` | Custom | user-supplied `customBaseURL` on the `AIModel` | Bearer | OpenAI-compat | ✅ | `wireFormat`: `.anthropicMessages` for `.anthropic`, `.openAIChatCompletions` for everything else. Env-var names used by the verify harness (reuse for Agent's Phase 7): `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `XAI_API_KEY`, `MISTRAL_API_KEY`, `GEMINI_API_KEY`, `DASHSCOPE_API_KEY`, `DEEPSEEK_API_KEY`, `MOONSHOT_API_KEY`, `PERPLEXITY_API_KEY`, `TOGETHER_API_KEY`, `DEEPINFRA_API_KEY`, `CEREBRAS_API_KEY`. ### 1.4 `OpenAICompatibleClient` — request/response design - **Endpoint:** `POST {base}/chat/completions` (path appended with `appendingPathComponent`, preserving base paths like `/compatible-mode/v1` and `/v1beta/openai`); `GET {base}/models`. - **Request wire types (private `Encodable` structs):** `WireRequest` (`model`, `messages`, `stream`, `stream_options`, `temperature`, `top_p`, `max_tokens`, `max_completion_tokens`, `frequency_penalty`, `presence_penalty`, `reasoning_effort`, `enable_thinking`), `WireMessage` (`role` + `content`), `WireContent` (plain string OR parts array), `WirePart` (`{"type":"text"}` / `{"type":"image_url","image_url":{"url":"data:…;base64,…"}}`). - **Parameter gating:** every optional field is included **only if** the model's `ParameterSupport` allows it (providers 400 on unknown/unsupported params). Notable: `usesMaxCompletionTokens` → send `max_completion_tokens` instead of `max_tokens` (OpenAI reasoning models, Kimi K-series, Cerebras); Mistral maps `reasoning_effort` medium→"high", low→"none" (only accepts high/none); Qwen `enable_thinking` is only legal when `stream:true`. - **`stream_options: {"include_usage": true}`** is sent for openai/xai/gemini/deepseek/kimi/ together/cerebras/custom; omitted for mistral (rejects unknown params), qwen, deepinfra, perplexity (usage included automatically). - **Response wire types (private `Decodable`):** `WireChunk` (`choices`, `usage`, plus Perplexity `citations` + `search_results`), `WireChoice` (`delta` for streaming, `message` for non-streaming, bare `text` for Together's completions-style streams, `finish_reason`), `WireDelta` (`content`, `reasoning_content`, `reasoning`; custom `init(from:)` also decodes Mistral's content-**array** ThinkChunk/TextChunk shape), `WireUsage` (`prompt_tokens`/`completion_tokens`/`completion_tokens_details.reasoning_tokens`). - **Streaming:** consumes `StreamingService.sseEvents`, stops on `data: [DONE]`, silently tolerates undecodable keep-alive chunks, yields `.reasoningDelta` (from `reasoning_content` or `reasoning`), `.textDelta` (from `delta.content` or `choice.text`), `.citations` (once, Perplexity), `.usage`, then `.finished(reason: finish_reason)`. - **`complete`:** if `parameterSupport.requiresStreaming` (Qwen qwq/qvq, several Together/ DeepInfra-hosted models reject `stream:false`), it aggregates the stream instead (`completeViaStream`); otherwise plain POST via `StreamingService.postJSON` (3 attempts, exponential backoff on 429/5xx honoring `Retry-After`). - **`listModelIDs`:** decodes `{"data":[{"id"}]}` OR Together's bare `[{"id"}]`; strips Gemini's `models/` prefix. - **Attachments:** text files are injected inline as fenced blocks; images become base64 data-URI `image_url` parts (user messages on vision models only). ### 1.5 `AnthropicClient` — native Messages API - **Endpoint:** `POST /v1/messages`; headers `x-api-key: `, `anthropic-version: 2023-06-01`, `Content-Type: application/json`. `GET /v1/models?limit=100` for listing. - **Request:** `WireRequest` — `model`, **mandatory `max_tokens`** (default 8192 when unset), `messages` (block-structured: `WireMessage{role, content:[WireBlock]}` with `.text` / `.image(base64 source)` blocks; empty text becomes `" "`), top-level `system` string, `stream`, `temperature`/`top_p` (gated — Claude 4.7+/5 reject them, encoded per-model in `ParameterSupport`), and `thinking: {"type":"enabled","budget_tokens":8000}` / `{"type":"disabled"}` when `thinkingToggle` is supported. (Cloud's PROVIDERS.md documents the full per-model thinking matrix, incl. `{"type":"adaptive"}` for 4.6+ and "omit entirely" for `claude-fable-5` where thinking is always on.) - **Streaming (named SSE events, no `[DONE]`):** the client switches on `sse.event ?? decoded.type`: - `message_start` → capture `message.usage.input_tokens` - `content_block_delta` → `delta.text` → `.textDelta`; `delta.thinking` → `.reasoningDelta` - `message_delta` → `usage.output_tokens` + `delta.stop_reason` - `error` → mid-stream error surfaced as `ProviderError.serverError` - `message_stop` / `ping` / `content_block_start` / `content_block_stop` → currently ignored (Agent's port MUST handle `content_block_start`/`stop` + `input_json_delta` — §5.1) - **Non-streaming:** decodes `content: [Block{type,text,thinking}]`, joins `text` blocks into the message body and `thinking` blocks into `reasoning`; reads `stop_reason` and `usage`. ### 1.6 `StreamingService` — shared SSE plumbing (port unchanged) - Shared `URLSession`: `timeoutIntervalForRequest = 120`, `timeoutIntervalForResource = 900`, `User-Agent: ZyquoCloud/1.0 (macOS)` (rename to `ZyquoAgent/1.0 (macOS)`). - `SSEParser`: incremental line parser handling `event:`/`data:` fields, multi-line `data:` joins, `:` comment/keep-alive lines (DeepSeek sends `: keep-alive`), CRLF, and a trailing flush for streams ending without a final blank line. **Critical detail preserved:** `URLSession.AsyncBytes.lines` skips empty lines (the SSE event separators) — the byte stream is split manually on `\n`. - `sseEvents(for:provider:)`: on non-2xx, reads the full error body and throws the typed `ProviderError`; wraps `CancellationError` → `.cancelled`; `onTermination` cancels the task (this is what makes the Stop button actually abort the HTTP stream — same mechanism will make Agent runs cancellable). - Unit tests exist in `Tests/ZyquoCloudTests/SSEParserTests.swift` — port them. --- ## 2. The full model catalog (from `Services/ModelCatalogData.swift`, generated 2026-07-30) **170 built-in models across 12 providers.** Zyquo Agent ships this exact catalog. Legend — caps: **V** vision, **T** tools/function-calling, **R** reasoning output, **J** JSON mode, **C** citations; price = USD per 1M tokens in/out (— = not published); ⭐ = `isRecommended`, 🕰 = `isLegacy`; **🤖 = agent-capable** (the subset per §3); **bold 🤖** entries are the suggested per-provider agent defaults. Param notes: `mct` = uses `max_completion_tokens`, `re` = `reasoning_effort`, `tt` = thinking toggle, `rs` = requiresStreaming, `no-t/p` = temperature & top_p rejected. ### OpenAI (27) | Model ID | Display name | Ctx | Max out | Caps | $/1M | Params | Flags | Agent | |---|---|---|---|---|---|---|---|---| | `gpt-5.6-sol` | GPT-5.6 Sol | 1,050,000 | 128K | VTRJ | 5.00/30.00 | no-t/p, mct, re | ⭐ | 🤖 | | `gpt-5.6-terra` | GPT-5.6 Terra | 1,050,000 | 128K | VTRJ | 2.50/15.00 | no-t/p, mct, re | ⭐ | **🤖 default** | | `gpt-5.6-luna` | GPT-5.6 Luna | 1,050,000 | 128K | VTRJ | 1.00/6.00 | no-t/p, mct, re | | 🤖 | | `chat-latest` | ChatGPT Latest | 128,000 | — | VTJ | 5.00/30.00 | mct | | — (rolling chat tuning) | | `gpt-5.5` | GPT-5.5 | 400,000 | — | VTRJ | 5.00/30.00 | no-t/p, mct, re | | 🤖 | | `gpt-5.4` | GPT-5.4 | 400,000 | 128K | VTRJ | 2.50/15.00 | no-t/p, mct, re | | 🤖 | | `gpt-5.4-mini` | GPT-5.4 mini | 400,000 | — | VTRJ | 0.75/4.50 | no-t/p, mct, re | | 🤖 | | `gpt-5.4-nano` | GPT-5.4 nano | 400,000 | — | VTRJ | 0.20/1.25 | no-t/p, mct, re | | — (nano tier, weak for deep agents) | | `gpt-5.3-chat-latest` | GPT-5.3 Chat Latest | 128,000 | — | VTJ | — | mct | | — | | `gpt-5.2` | GPT-5.2 | 400,000 | 128K | VTRJ | 1.75/14.00 | no-t/p, mct, re | | 🤖 | | `gpt-5.2-chat-latest` | GPT-5.2 Chat Latest | 128,000 | 16K | VTJ | 1.75/14.00 | mct | | — | | `gpt-5.1` | GPT-5.1 | 400,000 | 128K | VTRJ | 1.25/10.00 | no-t/p, mct, re | | 🤖 | | `gpt-5` | GPT-5 | 400,000 | 128K | VTRJ | 1.25/10.00 | no-t/p, mct, re | | 🤖 | | `gpt-5-mini` | GPT-5 mini | 400,000 | 128K | VTRJ | 0.25/2.00 | no-t/p, mct, re | | 🤖 | | `gpt-5-nano` | GPT-5 nano | 400,000 | 128K | VTRJ | 0.05/0.40 | no-t/p, mct, re | | — | | `o3` | OpenAI o3 | 200,000 | 100K | VTRJ | 2.00/8.00 | no-t/p, mct, re | | 🤖 | | `o4-mini` | OpenAI o4-mini | 200,000 | 100K | VTRJ | 1.10/4.40 | no-t/p, mct, re | | 🤖 | | `o3-mini` | OpenAI o3-mini | 200,000 | 100K | TRJ | 1.10/4.40 | no-t/p, mct, re | 🕰 | — | | `o1` | OpenAI o1 | 200,000 | 100K | VTRJ | 15.00/60.00 | no-t/p, mct, re | 🕰 | — | | `gpt-4.1` | GPT-4.1 | 1,047,576 | 32,768 | VTJ | 2.00/8.00 | openAIDefault | 🕰 | — | | `gpt-4.1-mini` | GPT-4.1 mini | 1,047,576 | 32,768 | VTJ | 0.40/1.60 | openAIDefault | 🕰 | — | | `gpt-4.1-nano` | GPT-4.1 nano | 1,047,576 | 32,768 | VTJ | 0.10/0.40 | openAIDefault | 🕰 | — | | `gpt-4o` | GPT-4o | 128,000 | 16,384 | VTJ | 2.50/10.00 | openAIDefault | 🕰 | — | | `gpt-4o-mini` | GPT-4o mini | 128,000 | 16,384 | VTJ | 0.15/0.60 | openAIDefault | 🕰 | — | | `gpt-4-turbo` | GPT-4 Turbo | 128,000 | 4,096 | VTJ | 10.00/30.00 | openAIDefault | 🕰 | — | | `gpt-4` | GPT-4 | 8,192 | 8,192 | T | 30.00/60.00 | openAIDefault | 🕰 | — | | `gpt-3.5-turbo` | GPT-3.5 Turbo | 16,385 | 4,096 | TJ | 0.50/1.50 | openAIDefault | 🕰 | — | ### Anthropic (11) | Model ID | Display name | Ctx | Max out | Caps | $/1M | Params | Flags | Agent | |---|---|---|---|---|---|---|---|---| | `claude-opus-5` | Claude Opus 5 | 1,000,000 | 128K | VTRJ | 5.00/25.00 | no-t/p, tt | ⭐ | 🤖 | | `claude-sonnet-5` | Claude Sonnet 5 | 1,000,000 | 128K | VTRJ | 3.00/15.00 | no-t/p, tt | ⭐ | **🤖 default (and overall app default)** | | `claude-fable-5` | Claude Fable 5 | 1,000,000 | 128K | VTRJ | 10.00/50.00 | no-t/p, thinking always on (no toggle) | | 🤖 | | `claude-opus-4-8` | Claude Opus 4.8 | 1,000,000 | 128K | VTRJ | 5.00/25.00 | no-t/p, tt | | 🤖 | | `claude-opus-4-7` | Claude Opus 4.7 | 1,000,000 | 128K | VTRJ | 5.00/25.00 | no-t/p, tt | | 🤖 | | `claude-opus-4-6` | Claude Opus 4.6 | 1,000,000 | 128K | VTRJ | 5.00/25.00 | t/p ok, tt | | 🤖 | | `claude-sonnet-4-6` | Claude Sonnet 4.6 | 1,000,000 | 128K | VTRJ | 3.00/15.00 | t/p ok, tt | | 🤖 | | `claude-haiku-4-5-20251001` | Claude Haiku 4.5 | 200,000 | 64K | VTRJ | 1.00/5.00 | t/p ok, tt | | 🤖 (fast tier; also Cloud's auto-title model) | | `claude-opus-4-5-20251101` | Claude Opus 4.5 | 200,000 | 64K | VTRJ | 5.00/25.00 | t/p ok, tt | 🕰 | — | | `claude-sonnet-4-5-20250929` | Claude Sonnet 4.5 | 1,000,000 | 64K | VTRJ | 3.00/15.00 | t/p ok, tt | 🕰 | — | | `claude-opus-4-1-20250805` | Claude Opus 4.1 | 200,000 | 32K | VTRJ | 15.00/75.00 | t/p ok, tt | 🕰 | — | ### xAI / Grok (5) | Model ID | Display name | Ctx | Caps | $/1M | Params | Flags | Agent | |---|---|---|---|---|---|---|---| | `grok-4.5` | Grok 4.5 | 500,000 | VTRJ | 2.00/6.00 | re | ⭐ | **🤖 default** | | `grok-4.3` | Grok 4.3 | 1,000,000 | VTRJ | 1.25/2.50 | re | | 🤖 | | `grok-4.20` | Grok 4.20 Reasoning | 1,000,000 | VTRJ | 1.25/2.50 | | | 🤖 | | `grok-4.20-non-reasoning` | Grok 4.20 Non-Reasoning | 1,000,000 | VTJ | 1.25/2.50 | | | 🤖 | | `grok-code-fast-1` | Grok Code Fast 1 | 256,000 | VTRJ | 1.00/2.00 | | ⭐ | 🤖 (agentic-coding tuned) | ### Mistral (10) | Model ID | Display name | Ctx | Caps | $/1M | Params | Flags | Agent | |---|---|---|---|---|---|---|---| | `mistral-medium-latest` | Mistral Medium 3.5 | 262,144 | VTRJ | 1.50/7.50 | pen, re | ⭐ | **🤖 default** | | `mistral-large-latest` | Mistral Large 3 | 262,144 | VTJ | 0.50/1.50 | openAIDefault | ⭐ | 🤖 | | `mistral-small-latest` | Mistral Small 4 | 262,144 | VTRJ | 0.15/0.60 | pen, re | ⭐ | 🤖 | | `codestral-latest` | Codestral | 256,000 | TJ | 0.30/0.90 | openAIDefault | | — (FIM/code-completion focus) | | `ministral-14b-latest` | Ministral 3 14B | 262,144 | VTJ | 0.20/0.20 | openAIDefault | | — (small) | | `ministral-8b-latest` | Ministral 3 8B | 262,144 | VTJ | 0.15/0.15 | openAIDefault | | — | | `ministral-3b-latest` | Ministral 3 3B | 131,072 | VTJ | 0.10/0.10 | openAIDefault | | — | | `magistral-medium-latest` | Magistral Medium | 131,072 | TRJ | 2.00/5.00 | openAIDefault | 🕰 | — | | `devstral-latest` | Devstral 2 | 262,144 | TJ | 0.40/2.00 | openAIDefault | 🕰 | — | | `open-mistral-nemo` | Mistral Nemo | 131,072 | TJ | 0.15/0.15 | openAIDefault | 🕰 | — | ### Google Gemini (14) — served via the OpenAI-compat endpoint | Model ID | Display name | Ctx | Max out | Caps | $/1M | Flags | Agent | |---|---|---|---|---|---|---|---| | `gemini-3.6-flash` | Gemini 3.6 Flash | 1,048,576 | 65,536 | VTRJ | 1.50/7.50 | ⭐ | **🤖 default** | | `gemini-3.5-flash` | Gemini 3.5 Flash | 1,048,576 | 65,536 | VTRJ | 1.50/9.00 | | 🤖 | | `gemini-3.5-flash-lite` | Gemini 3.5 Flash-Lite | 1,048,576 | 65,536 | VTRJ | 0.30/2.50 | ⭐ | 🤖 (budget tier) | | `gemini-3.1-pro-preview` | Gemini 3.1 Pro (Preview) | 1,048,576 | 65,536 | VTRJ | 2.00/12.00 | ⭐ | 🤖 | | `gemini-3.1-flash-lite` | Gemini 3.1 Flash-Lite | 1,048,576 | 65,536 | VTRJ | 0.25/1.50 | | — | | `gemini-2.5-pro` | Gemini 2.5 Pro | 1,048,576 | 65,536 | VTRJ | 1.25/10.00 | | 🤖 | | `gemini-2.5-flash` | Gemini 2.5 Flash | 1,048,576 | 65,536 | VTRJ | 0.30/2.50 | | 🤖 | | `gemini-2.5-flash-lite` | Gemini 2.5 Flash-Lite | 1,048,576 | 65,536 | VTRJ | 0.10/0.40 | | — | | `gemini-pro-latest` | Gemini Pro (Latest) | 1,048,576 | 65,536 | VTRJ | — | | 🤖 (rolling alias) | | `gemini-flash-latest` | Gemini Flash (Latest) | 1,048,576 | 65,536 | VTRJ | — | | 🤖 (rolling alias) | | `gemini-flash-lite-latest` | Gemini Flash-Lite (Latest) | 1,048,576 | 65,536 | VTRJ | — | | — | | `gemini-3-flash-preview` | Gemini 3 Flash (Preview) | 1,048,576 | 65,536 | VTRJ | 0.50/3.00 | | — | | `gemma-4-26b-a4b-it` | Gemma 4 26B | 262,144 | 32,768 | J | — | | **excluded — no tools** | | `gemma-4-31b-it` | Gemma 4 31B | 262,144 | 32,768 | J | — | | **excluded — no tools** | All Gemini chat models take `reasoning_effort` on the compat endpoint. ### Alibaba Qwen / DashScope (32) | Model ID | Display name | Ctx | Caps | $/1M | Params | Flags | Agent | |---|---|---|---|---|---|---|---| | `qwen3.7-max` | Qwen3.7 Max | 1,000,000 | TRJ | 2.50/7.50 | tt | ⭐ | **🤖 default** | | `qwen3.7-plus` | Qwen3.7 Plus | 1,000,000 | VTRJ | 0.32/1.28 | tt | ⭐ | 🤖 | | `qwen3.7-flash` | Qwen3.7 Flash | 1,000,000 | VTRJ | 0.03/0.13 | tt | ⭐ | 🤖 (budget) | | `qwen3.6-plus` | Qwen3.6 Plus | 1,000,000 | VTRJ | — | tt | | 🤖 | | `qwen3.6-flash` | Qwen3.6 Flash | 1,000,000 | VTRJ | — | tt | | — | | `qwen3.5-plus` | Qwen3.5 Plus | 1,000,000 | VTRJ | — | tt | | 🤖 | | `qwen3.5-flash` | Qwen3.5 Flash | 1,000,000 | VTRJ | — | tt | | — | | `qwen-max` | Qwen Max | 128,000 | TRJ | — | tt | | — | | `qwen-plus` | Qwen Plus | 1,000,000 | TRJ | — | tt | | — | | `qwen-turbo` | Qwen Turbo | 1,000,000 | TRJ | — | tt | 🕰 | — | | `qwen-flash` | Qwen Flash | 1,000,000 | TRJ | — | tt | | — | | `qwen3-coder-plus` | Qwen3 Coder Plus | 1,000,000 | TJ | — | | | 🤖 (agentic-coding tuned) | | `qwen3-coder-flash` | Qwen3 Coder Flash | 1,000,000 | TJ | — | | | 🤖 | | `qwen3-coder-next` | Qwen3 Coder Next | 262,144 | TJ | — | | | 🤖 ("multi-turn tool interactions") | | `qwen3-coder-480b-a35b-instruct` | Qwen3 Coder 480B A35B | 262,144 | TJ | — | | | 🤖 | | `qwen3-vl-plus` | Qwen3 VL Plus | 1,000,000 | VTRJ | — | tt | | — (vision-focused) | | `qwen3-vl-flash` | Qwen3 VL Flash | 1,000,000 | VTRJ | — | tt | | — | | `qwen3-vl-235b-a22b-instruct` | Qwen3 VL 235B Instruct | 131,072 | VTJ | — | | | — | | `qwen3-vl-235b-a22b-thinking` | Qwen3 VL 235B Thinking | 131,072 | VTRJ | — | | | — | | `qvq-max` | QVQ Max | 131,072 | VRJ | — | rs | | **excluded — no tools** | | `qwq-plus` | QwQ Plus | 131,072 | TRJ | — | rs | | — (requiresStreaming; tool-calls unreliable) | | `qwen3.5-397b-a17b` | Qwen3.5 397B A17B | 262,144 | TRJ | — | tt | | 🤖 | | `qwen3.5-122b-a10b` | Qwen3.5 122B A10B | 262,144 | TRJ | — | tt | | — | | `qwen3.5-35b-a3b` | Qwen3.5 35B A3B | 262,144 | TRJ | — | tt | | — | | `qwen3-235b-a22b-instruct-2507` | Qwen3 235B Instruct 2507 | 262,144 | TJ | — | | | — | | `qwen3-235b-a22b-thinking-2507` | Qwen3 235B Thinking 2507 | 262,144 | TRJ | — | | | — | | `qwen3-next-80b-a3b-instruct` | Qwen3 Next 80B Instruct | 262,144 | TJ | — | | | — | | `qwen3-next-80b-a3b-thinking` | Qwen3 Next 80B Thinking | 262,144 | TRJ | — | | | — | | `deepseek-v4-pro` | DeepSeek V4 Pro (DashScope) | 1,000,000 | TRJ | — | tt | | 🤖 | | `deepseek-v4-flash` | DeepSeek V4 Flash (DashScope) | 1,000,000 | TRJ | — | tt | | — (prefer first-party DeepSeek) | | `glm-5.2` | GLM 5.2 (DashScope) | 198,000 | TRJ | — | tt | | 🤖 | | `kimi-k2.7-code` | Kimi K2.7 Code (DashScope) | 262,144 | TRJ | — | tt | | — (prefer first-party Kimi) | ### DeepSeek (2) | Model ID | Display name | Ctx | Max out | Caps | $/1M | Params | Flags | Agent | |---|---|---|---|---|---|---|---|---| | `deepseek-v4-flash` | DeepSeek V4 Flash | 1,000,000 | 384K | TRJ | 0.14/0.28 | re, tt | ⭐ | 🤖 | | `deepseek-v4-pro` | DeepSeek V4 Pro | 1,000,000 | 384K | TRJ | 0.435/0.87 | re, tt | ⭐ | **🤖 default** | Both support up to 128 functions per request; `finish_reason` may be the DeepSeek-specific `insufficient_system_resource` ("servers overloaded"). ### Kimi / Moonshot (12) | Model ID | Display name | Ctx | Max out | Caps | $/1M | Params | Flags | Agent | |---|---|---|---|---|---|---|---|---| | `kimi-k3` | Kimi K3 | 1,048,576 | 131,072 | VTRJ | 3.00/15.00 | no-t/p, mct, re (thinking always on) | ⭐ | **🤖 default** | | `kimi-k2.7-code` | Kimi K2.7 Code | 262,144 | — | VTRJ | 0.95/4.00 | no-t/p, mct | ⭐ | 🤖 (agentic-coding tuned) | | `kimi-k2.7-code-highspeed` | Kimi K2.7 Code Highspeed | 262,144 | — | VTRJ | 1.90/8.00 | no-t/p, mct | | 🤖 | | `kimi-k2.6` | Kimi K2.6 | 262,144 | — | VTRJ | 0.95/4.00 | no-t/p, mct, tt | | 🤖 | | `kimi-k2.5` | Kimi K2.5 | 262,144 | — | VTRJ | 0.60/3.00 | no-t/p, mct, tt | | 🤖 | | `moonshot-v1-8k` | Moonshot v1 8K | 8,192 | — | TJ | 0.20/2.00 | openAIDefault | 🕰 | — | | `moonshot-v1-32k` | Moonshot v1 32K | 32,768 | — | TJ | 1.00/3.00 | openAIDefault | 🕰 | — | | `moonshot-v1-128k` | Moonshot v1 128K | 131,072 | — | TJ | 2.00/5.00 | openAIDefault | 🕰 | — | | `moonshot-v1-auto` | Moonshot v1 Auto | 131,072 | — | TJ | — | openAIDefault | 🕰 | — | | `moonshot-v1-8k-vision-preview` | Moonshot v1 8K Vision | 8,192 | — | VTJ | 0.20/2.00 | openAIDefault | 🕰 | — | | `moonshot-v1-32k-vision-preview` | Moonshot v1 32K Vision | 32,768 | — | VTJ | 1.00/3.00 | openAIDefault | 🕰 | — | | `moonshot-v1-128k-vision-preview` | Moonshot v1 128K Vision | 131,072 | — | VTJ | 2.00/5.00 | openAIDefault | 🕰 | — | ### Perplexity (4) — **entire provider excluded from the agent-capable subset** | Model ID | Display name | Ctx | Caps | $/1M | Flags | Agent | |---|---|---|---|---|---|---| | `sonar` | Sonar | 128,000 | JC | 1.00/1.00 | ⭐ | **excluded** | | `sonar-pro` | Sonar Pro | 200,000 | JC | 3.00/15.00 | ⭐ | **excluded** | | `sonar-reasoning-pro` | Sonar Reasoning Pro | 128,000 | RJC | 2.00/8.00 | | **excluded** | | `sonar-deep-research` | Sonar Deep Research | 128,000 | RJC | 2.00/8.00 | | **excluded** | Cloud's research is explicit: "no vision/image input; **no tool/function calling on the Sonar chat API**". These are web-search answer engines. Keep them in the picker (same list as Cloud) but never selectable as the agent driver — or gray them out with an explanation. ### Together AI (16) | Model ID | Display name | Ctx | Caps | $/1M | Params | Flags | Agent | |---|---|---|---|---|---|---|---| | `moonshotai/Kimi-K3` | Kimi K3 | 1,000,000 | TRJ | 3.00/15.00 | openAIDefault | ⭐ | 🤖 | | `moonshotai/Kimi-K2.7-Code` | Kimi K2.7 Code | 262,144 | TRJ | 0.95/4.00 | openAIDefault | | 🤖 | | `moonshotai/Kimi-K2.6` | Kimi K2.6 | 262,144 | TRJ | 1.20/4.50 | openAIDefault | | — | | `deepseek-ai/DeepSeek-V4-Pro` | DeepSeek V4 Pro | 512,000 | TRJ | 1.74/3.48 | openAIDefault | ⭐ | **🤖 default** | | `zai-org/GLM-5.2` | GLM 5.2 | 512,000 | TRJ | 1.40/4.40 | openAIDefault | | 🤖 | | `Qwen/Qwen3.7-Max` | Qwen3.7 Max | 1,000,000 | TRJ | 1.25/3.75 | pen, rs | | 🤖 | | `Qwen/Qwen3.7-Plus` | Qwen3.7 Plus | 1,000,000 | TJ | 0.32/1.28 | pen, rs | | — | | `Qwen/Qwen3.6-Plus` | Qwen3.6 Plus | 1,000,000 | TJ | 0.50/3.00 | pen, rs | | — | | `Qwen/Qwen3.5-9B` | Qwen3.5 9B | 262,144 | TJ | 0.17/0.25 | pen, rs | | — (small) | | `meta-llama/Llama-3.3-70B-Instruct-Turbo` | Llama 3.3 70B Turbo | 131,072 | TJ | 1.04/1.04 | openAIDefault | | — | | `openai/gpt-oss-120b` | GPT-OSS 120B | 131,072 | TRJ | 0.15/0.60 | pen, re | ⭐ | 🤖 | | `openai/gpt-oss-20b` | GPT-OSS 20B | 131,072 | TRJ | 0.05/0.20 | pen, re | | — (may hallucinate tool calls — Cloud note) | | `nvidia/nemotron-3-ultra-550b-a55b` | Nemotron 3 Ultra 550B | 512,288 | TRJ | 0.60/3.60 | openAIDefault | | 🤖 | | `MiniMaxAI/MiniMax-M3` | MiniMax M3 | 524,288 | TRJ | 0.30/1.20 | openAIDefault | | 🤖 | | `google/gemma-4-31B-it` | Gemma 4 31B | 262,144 | TJ (vision disabled — live-verified empty answers) | 0.39/0.97 | pen, rs | | — | | `thinkingmachines/Inkling` | Inkling | 524,288 | TRJ | 1.00/4.05 | openAIDefault | | — | Together quirks preserved in the client: bare-array `/models`, completions-style `choices[].text` streaming for some models, extra `finish_reason: "eos"`. ### DeepInfra (34) | Model ID | Display name | Ctx | Caps | $/1M | Flags | Agent | |---|---|---|---|---|---|---| | `anthropic/claude-fable-5` | Claude Fable 5 | 1,000,000 | VTRJ | 10.00/50.00 | | 🤖 | | `anthropic/claude-opus-5` | Claude Opus 5 | 1,000,000 | VTRJ | 5.00/25.00 | | 🤖 | | `anthropic/claude-sonnet-5` | Claude Sonnet 5 | 1,000,000 | VTRJ | 2.00/10.00 | | 🤖 | | `anthropic/claude-opus-4-8` | Claude Opus 4.8 | 1,000,000 | VTRJ | 5.00/25.00 | | 🤖 | | `anthropic/claude-haiku-4-5` | Claude Haiku 4.5 | 200,000 | VTRJ | 1.00/5.00 | | 🤖 | | `google/gemini-3.1-pro` | Gemini 3.1 Pro | 1,000,000 | VTRJ | 2.00/12.00 | | 🤖 | | `google/gemini-3.5-flash` | Gemini 3.5 Flash | 1,000,000 | VTRJ | 1.50/9.00 | | 🤖 | | `google/gemini-3.1-flash-lite` | Gemini 3.1 Flash-Lite | 1,000,000 | VTJ | 0.25/1.50 | | — | | `google/gemini-2.5-pro` | Gemini 2.5 Pro | 1,000,000 | VTRJ | 1.25/10.00 | | — | | `google/gemini-2.5-flash` | Gemini 2.5 Flash | 1,000,000 | VTRJ | 0.30/2.50 | | — | | `deepseek-ai/DeepSeek-V4-Pro` | DeepSeek V4 Pro | 1,048,576 | TRJ | 1.30/2.60 | ⭐ | **🤖 default** | | `deepseek-ai/DeepSeek-V4-Flash` | DeepSeek V4 Flash | 1,048,576 | TJ | 0.09/0.18 | ⭐ | 🤖 | | `deepseek-ai/DeepSeek-V3.1` | DeepSeek V3.1 | 163,840 | TRJ | 0.25/0.95 | | — | | `deepseek-ai/DeepSeek-R1-0528` | DeepSeek R1 0528 | 163,840 | R (no tools) | 0.50/2.15 | | **excluded — no tools** | | `moonshotai/Kimi-K2.7-Code` | Kimi K2.7 Code | 262,144 | TRJ | 0.74/3.50 | | 🤖 | | `moonshotai/Kimi-K2.6` | Kimi K2.6 | 262,144 | TRJ | 0.75/3.50 | | — | | `moonshotai/Kimi-K2.5` | Kimi K2.5 | 262,144 | TJ (rs) | 0.45/2.25 | | — | | `zai-org/GLM-5.2` | GLM 5.2 | 1,048,576 | TRJ | 0.75/2.40 | ⭐ | 🤖 | | `zai-org/GLM-4.7` | GLM 4.7 | 202,752 | TRJ | 0.40/1.75 | | — | | `Qwen/Qwen3.7-Max` | Qwen3.7 Max | 256,000 | TRJ | 2.50/7.50 | | 🤖 | | `Qwen/Qwen3.5-397B-A17B` | Qwen3.5 397B A17B | 262,144 | TRJ | 0.45/3.00 | | — | | `Qwen/Qwen3-235B-A22B-Instruct-2507` | Qwen3 235B Instruct 2507 | 262,144 | TJ | 0.09/0.55 | | — | | `Qwen/Qwen3-235B-A22B-Thinking-2507` | Qwen3 235B Thinking 2507 | 262,144 | TRJ | 0.23/2.30 | | — | | `Qwen/Qwen3-Coder-480B-A35B-Instruct-Turbo` | Qwen3 Coder 480B Turbo | 262,144 | TJ | 0.30/1.00 | | 🤖 | | `Qwen/Qwen3-VL-235B-A22B-Instruct` | Qwen3 VL 235B | 262,144 | VTJ | 0.20/0.88 | | — | | `meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8` | Llama 4 Maverick | 1,048,576 | VTJ | 0.20/0.80 | | — | | `meta-llama/Llama-4-Scout-17B-16E-Instruct` | Llama 4 Scout | 327,680 | VTJ | 0.10/0.30 | | — | | `meta-llama/Llama-3.3-70B-Instruct-Turbo` | Llama 3.3 70B Turbo | 131,072 | TJ | 0.10/0.32 | | — | | `meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo` | Llama 3.1 8B Turbo | 131,072 | TJ | 0.02/0.04 | | — | | `openai/gpt-oss-120b` | GPT-OSS 120B | 131,072 | TRJ | 0.037/0.17 | ⭐ | 🤖 | | `openai/gpt-oss-20b` | GPT-OSS 20B | 131,072 | TRJ | 0.03/0.14 | | — | | `MiniMaxAI/MiniMax-M3` | MiniMax M3 | 524,288 | TRJ | 0.30/1.20 | | 🤖 | | `nvidia/NVIDIA-Nemotron-3-Ultra-550B-A55B` | Nemotron 3 Ultra 550B | 262,144 | TRJ | 0.50/2.20 | | — | | `mistralai/Mistral-Small-3.2-24B-Instruct-2506` | Mistral Small 3.2 24B | 128,000 | VTJ | 0.075/0.20 | | — | (Note: `google/gemma-4-31B-it` was removed from DeepInfra on 2026-07-30 — endpoint hangs. DeepInfra-proxied Claude/Gemini speak the plain OpenAI schema, no native thinking config.) ### Cerebras (3) | Model ID | Display name | Ctx | Max out | Caps | $/1M | Params | Flags | Agent | |---|---|---|---|---|---|---|---|---| | `gpt-oss-120b` | GPT-OSS 120B | 131,072 | 40,000 | TRJ | 0.35/0.75 | pen, mct, re | ⭐ | **🤖 default** (~3,000 tok/s — extremely fast loops) | | `gemma-4-31b` | Gemma 4 31B | 131,072 | 40,000 | VTRJ | 0.99/1.49 | pen, mct, re | | 🤖 (parallel tools, strict schemas) | | `zai-glm-4.7` | GLM 4.7 | 131,072 | 40,000 | TRJ | 2.25/2.75 | pen, mct, re | 🕰 | — (discontinued 2026-08-17) | --- ## 3. The agent-capable subset (what Zyquo Agent marks and defaults to) **Criteria** (from the catalog's capability flags + Cloud's per-provider research): 1. `capabilities.tools == true` — native function calling, live-verified by Cloud; 2. strong multi-step reasoning (flagship/frontier tier, or agentic-coding tuned); 3. context window ≥ ~128K (agent transcripts with tool results grow fast); 4. reliable streaming of tool-call arguments; 5. not legacy, not a niche tuning (chat-alias, vision-only, search-only). **Counts:** ~70 of the 170 catalog models are marked 🤖 agent-capable (per-model marks in the §2 tables). Everything else remains in the picker (same list as Cloud) but is visually de-emphasized and produces a "not recommended for agent tasks" hint if selected. **Overall default agent model: `claude-sonnet-5` (Anthropic)** — 1M context, adaptive thinking, best-in-class tool use, and the Messages API's tool_use blocks are the most explicit tool-calling contract of the twelve providers. Per-provider defaults are bolded in §2. **Recommended top tier** (surface first in the model chip): `claude-sonnet-5`, `claude-opus-5`, `gpt-5.6-terra`, `gpt-5.6-sol`, `gemini-3.6-flash`, `grok-4.5`, `grok-code-fast-1`, `deepseek-v4-pro`, `kimi-k3`, `kimi-k2.7-code`, `qwen3.7-max`, `mistral-medium-latest`, Cerebras `gpt-oss-120b` (speed king for tight loops). **Excluded from agent use, with reasons:** | Exclusion | Reason | |---|---| | Perplexity — all 4 sonar models | No function calling at all on the Sonar chat API (Cloud, live-verified); search answer engines, not agents | | `gemma-4-26b-a4b-it`, `gemma-4-31b-it` (Gemini) | `tools: false` in catalog; function calling unverified for open Gemma on the Gemini API | | `qvq-max` (Qwen) | `tools: false`; visual-reasoning only; requiresStreaming | | `deepseek-ai/DeepSeek-R1-0528` (DeepInfra) | `tools: false` in catalog — R1 has no reliable function calling | | `qwq-plus` (Qwen) | requiresStreaming + first-gen reasoning; tool-calling reliability poor | | All `isLegacy` models (gpt-4/4o/4.1 family, o1/o3-mini, moonshot-v1 classic, Claude 4.5/4.1, magistral/devstral/nemo, zai-glm-4.7@Cerebras) | Superseded; weaker tool use; several deprecate mid-2026 | | Chat-tuned rolling aliases (`chat-latest`, `gpt-5.x-chat-latest`) | Conversational tuning, no reasoning; not built for long agentic runs | | nano/small tiers (`gpt-5.4-nano`, `gpt-5-nano`, ministral 3/8/14B, `Qwen3.5-9B`, `gpt-oss-20b`, Llama 3.x) | Function calling exists but multi-step planning reliability is inadequate for "deep agentic" work (gpt-oss-20b documented by Cerebras/Together as prone to hallucinated tool calls) | | VL-focused Qwen models | Optimized for image understanding, not command loops | | `codestral-latest`, `thinkingmachines/Inkling`, Together `google/gemma-4-31B-it` | Code-completion / unverified-tooling niches | All exclusions are *soft* (UI de-emphasis + default filter), except Perplexity + `tools:false` models which are *hard* exclusions — the agent loop refuses to start with a model whose `capabilities.tools == false`. **Phase 7 contract:** every 🤖 model above gets the scripted (a) schema-receipt, (b) valid tool call for "list the files in the workspace using the shell tool", (c) tool_result consumption, (d) streaming test — following the `VerifyHarness` pattern (`Sources/ZyquoCloud/Verify/VerifyHarness.swift`): env keys, per-model results table, written to `docs/VERIFICATION.md`. --- ## 4. SecureKeyStore — the AES-256-GCM vault (NO Keychain), reused verbatim Source: `zyquo-cloud/Sources/ZyquoCloud/Services/SecureKeyStore.swift` (+ tests in `Tests/ZyquoCloudTests/SecureKeyStoreTests.swift`). Port both. - **File:** `~/Library/Application Support/ZyquoCloud/vault.zq` → Agent uses `~/Library/Application Support/ZyquoAgent/vault.zq` (via `PersistenceService.shared.rootDirectory`, which is the app-support folder named after the app). - **Blob layout:** `[salt 32 B][AES-GCM combined: nonce 12 B ‖ ciphertext ‖ tag 16 B]`. Written atomically with `.completeFileProtection`. Load validates `count > 32 + 12 + 16` and throws `VaultError.corrupted` on any decrypt failure. - **Plaintext:** JSON `[String: String]` — provider `rawValue` → API key (`{"openai":"sk-…","anthropic":"sk-ant-…", …}`). - **Key derivation:** `HKDF.deriveKey(ikm: machineEntropy ‖ pepper, salt: vault salt, info: "ZyquoCloud.vault.v1", outputByteCount: 32)` → `AES.GCM` `SymmetricKey`. - `machineEntropy` = IOPlatformUUID (IOKit `IOPlatformExpertDevice` / `kIOPlatformUUIDKey`) ‖ `NSHomeDirectory()` — binds the vault to machine + account. Injectable closure for tests. - `pepper` = 30 compiled-in bytes XOR `0x5A`, assembled at runtime (never a literal in the binary). - Re-saves reuse the **existing salt** (`existingSalt()`) so derivation stays stable. - **API:** `loadKeys()`, `saveKeys(_:)`, `key(for: ProviderID)`, `setKey(_:for:)`, `deleteKey(for:)`, `static redacted(_:)` → `"••••abcd"` (last 4 chars only, everywhere in UI). - **Agent decisions:** keep the **same design and blob format**; change only the HKDF `info` string to `"ZyquoAgent.vault.v1"` and (recommended) a distinct pepper — the two apps keep separate vaults with an identical user experience; keys are entered per-app (a Cloud vault cannot be decrypted by Agent by design — machine-bound, app-info-bound). Keep the rules: decrypt on demand only, never log, never plaintext on disk, no Keychain. - The SwiftUI wrapper `ViewModels/KeyVaultStore.swift` (per-provider key status, test-key latency chip) is also reusable for the Settings → Providers & Keys tab. --- ## 5. Provider-specific tool-calling quirks (what the normalized interface must absorb) Zyquo Cloud declares `capabilities.tools` per model but **never sends tools** — Zyquo Agent adds that. Cloud's `docs/PROVIDERS.md` already documents each provider's tool wire format (request schema sections + finish_reason inventories). Consolidated contract: ### 5.1 Anthropic (native Messages API) — the odd one out - **Declare:** top-level `tools: [{name, description, input_schema: }]`; `tool_choice: {"type":"auto"|"any"|"tool","name":…}` (+ `disable_parallel_tool_use`). - **Model calls a tool:** assistant `content` contains a `{"type":"tool_use","id":"toolu_…","name":…,"input":{…}}` block; `stop_reason == "tool_use"`. - **Streaming:** `content_block_start` announces the `tool_use` block (with `id` + `name`, empty input); arguments then stream as `content_block_delta` with `{"type":"input_json_delta","partial_json":"…"}` fragments — accumulate per block `index` and JSON-parse at `content_block_stop`. (Cloud's client currently ignores `content_block_start/stop` and `input_json_delta` — the port must add these three cases.) - **Return the result:** append a **user** message whose content is `[{"type":"tool_result","tool_use_id":"toolu_…","content":"…", "is_error":bool}]` — NOT a special role. Multiple tool_results go in one user turn for parallel calls. Assistant `thinking` blocks must be passed back **unchanged** on the same model. - **Done signal:** `stop_reason: "end_turn"` (also `max_tokens`, `stop_sequence`, `pause_turn`, `refusal` — Fable 5/Opus 5 can refuse with HTTP 200 — `model_context_window_exceeded`). No `[DONE]` sentinel; stream ends at `message_stop`. - **Other quirks (already in Cloud's code/notes):** `max_tokens` mandatory; per-model thinking-config matrix (adaptive vs enabled+budget vs omit-for-Fable-5); temperature/top_p rejected on 4.7+/5; HTTP 529 `overloaded_error` retry; mid-stream `error` events. ### 5.2 OpenAI-compatible family (OpenAI, xAI, Mistral, Gemini-compat, Qwen, DeepSeek, Kimi, Together, DeepInfra, Cerebras, custom) - **Declare:** `tools: [{"type":"function","function":{name, description, parameters: }}]`; `tool_choice: "none"|"auto"|"required"|{"type":"function","function":{"name":…}}` (Mistral additionally accepts `"any"`; default `parallel_tool_calls: true` on Mistral/Cerebras; Cerebras supports `strict: true` schemas). - **Model calls tools:** assistant message carries `tool_calls: [{id, type:"function", function:{name, arguments: ""}}]`; `finish_reason == "tool_calls"`. - **Streaming deltas:** `choices[].delta.tool_calls` is an array of **index-keyed fragments**: first fragment for an index carries `id` + `function.name`, subsequent ones carry `function.arguments` string chunks. Accumulate per `index`, then JSON-parse each arguments string when the stream finishes (`finish_reason:"tool_calls"` or `[DONE]`). - **Return the result:** append the assistant message **with its `tool_calls` array intact**, then one message per call: `{"role":"tool","tool_call_id":…, "content":""}` (Kimi/DeepSeek/Mistral all follow this; `name` optionally included). - **Done signal:** `finish_reason: "stop"` (`length`, `content_filter`; Together adds `eos`; DeepSeek adds `insufficient_system_resource`); stream terminates with `data: [DONE]`. - **Per-provider notes already encoded or documented by Cloud:** - **OpenAI:** reasoning models need `max_completion_tokens` and reject sampling params; ignore unknown `obfuscation` field in chunks. - **Gemini (compat):** tools/function-calling officially supported on the compat endpoint; ignore `extra_content`/`thought_signature` extras in deltas; `/models` IDs prefixed `models/`; Google Search grounding available via `tools` on Gemini 3+ (don't mix with function tools unless verified). - **Mistral:** content can be an **array** of ThinkChunk/TextChunk objects (handled in `WireDelta.init(from:)`); `tool_choice: "any"`; `reasoning_effort` only high/none. - **Qwen/DashScope:** `enable_thinking` only with `stream:true`; `reasoning_content` deltas; qwq/qvq reject non-streaming. - **DeepSeek:** `reasoning_content` deltas stream **before** content; thinking on by default on v4-flash; ≤128 tools; `: keep-alive` SSE comments. - **Kimi:** `reasoning_content` before content on K-series; built-in `$web_search` tool uses `type:"builtin_function"` (do NOT use for Agent — our tools are local); temperature unsupported on K-series (no-t/p + mct). - **Together:** some models stream completions-style `choices[].text`; bare-array `/models`; `finish_reason:"eos"`. - **Cerebras:** `max_completion_tokens` required-style; `strict:true` tool schemas; gpt-oss-120b may emit malformed/hallucinated tool calls — validate arguments against the schema and re-prompt on failure (this validation belongs in the normalized layer for ALL providers). - **Perplexity:** no tools — hard-excluded (§3); `citations`/`search_results` fields must keep being tolerated by the decoder. - **Reasoning surfaces to keep uniform:** `delta.reasoning_content` (DeepSeek/Qwen/Kimi/some DeepInfra), `delta.reasoning` (Together/Cerebras gpt-oss), Mistral ThinkChunk arrays, Anthropic `thinking_delta`, Perplexity inline `` — all already normalized to `ChatEvent.reasoningDelta` by Cloud's clients; unchanged. ### 5.3 The normalized interface Zyquo Agent adds Extend the ported types (one uniform surface; per-provider translation stays inside clients): ```swift struct ToolSpec: Codable { // handed to the client from ToolRegistry var name: String var description: String var parametersJSONSchema: String // canonical JSON Schema (object) as a string } struct ToolCall: Codable, Identifiable, Hashable { var id: String // provider call id (toolu_… / call_…); synthesize for providers that omit it var name: String var argumentsJSON: String // raw accumulated JSON string; parsed+validated by the loop } struct ToolResult: Codable { // threaded back on the next request var toolCallID: String var content: String // stringified output (stdout/stderr summary, file text…) var isError: Bool } // ChatRequest additions var tools: [ToolSpec] = [] var toolChoice: ToolChoice = .auto // .auto / .none / .required / .named(String) // Message additions (so history round-trips correctly per provider) var toolCalls: [ToolCall]? = nil // on assistant turns var toolResults: [ToolResult]? = nil // rendered as role:"tool" messages (OpenAI) or // tool_result user blocks (Anthropic) by the client // ChatEvent additions case toolCallStarted(index: Int, id: String, name: String) // live chip in the UI case toolCallArgumentsDelta(index: Int, delta: String) // streamed args case toolCalls([ToolCall]) // finalized, parsed set ``` `.finished(reason:)` is normalized by the client to a small enum surfaced alongside the raw string: `endTurn` (`stop`/`end_turn`/`eos`), `toolUse` (`tool_calls`/`tool_use`), `maxTokens` (`length`/`max_tokens`), `refusal`, `other(String)` — the `AgentLoop` branches only on this. ### 5.4 Gemini native (only if Phase 7 demands it) If the compat endpoint's tool streaming proves insufficient, a native `GeminiClient` would use `POST /v1beta/models/{model}:streamGenerateContent?alt=sse` with `x-goog-api-key`, declare `tools: [{functionDeclarations:[{name,description,parameters}]}]` + `toolConfig: {functionCallingConfig: {mode: AUTO|ANY|NONE, allowedFunctionNames}}`, receive `candidates[].content.parts[].functionCall {name,args}` (complete JSON, not deltas), and reply with a user part `functionResponse {name, response}`; completion signaled by `finishReason: "STOP"`. **Not planned for the initial port** — Cloud ships without it and all Gemini catalog models advertise tools on the compat endpoint. --- ## 6. Porting plan **Port verbatim** (rename module header `Zyquo Cloud` → `Zyquo Agent`, `ZyquoCloud` strings → `ZyquoAgent` where they are app-facing: User-Agent, app-support folder, HKDF info, pepper): | From `zyquo-cloud/Sources/ZyquoCloud/…` | To `zyquo-agent/Sources/ZyquoAgent/…` | Changes | |---|---|---| | `Models/ProviderID.swift` | `Models/ProviderID.swift` | none | | `Models/AIModel.swift` | `Models/AIModel.swift` | add `var agentCapable: Bool` (derived: `capabilities.tools && !isLegacy && curated list from §3`) | | `Models/Message.swift` | `Models/Message.swift` | add `toolCalls` / `toolResults` (§5.3) | | `Models/Conversation.swift` (ChatParameters, Persona) | `Models/ChatParameters.swift` | extract the shared value types; Conversation itself becomes Agent's Task model | | `Providers/ProviderProtocol.swift` | `Providers/ProviderProtocol.swift` | add `tools`/`toolChoice` to `ChatRequest`; add tool `ChatEvent` cases; normalized finish reason | | `Providers/ProviderRegistry.swift` | `Providers/ProviderRegistry.swift` | none | | `Providers/OpenAICompatibleClient.swift` | `Providers/OpenAICompatibleClient.swift` | encode `tools`/`tool_choice`/`parallel_tool_calls`; decode + stream `delta.tool_calls` (index-keyed accumulation); emit assistant `tool_calls` + `role:"tool"` messages when building bodies from history | | `Providers/AnthropicClient.swift` | `Providers/AnthropicClient.swift` | encode `tools`/`tool_choice`; handle `content_block_start`/`input_json_delta`/`content_block_stop` for `tool_use` blocks; encode `tool_use` assistant blocks + `tool_result` user blocks from history | | `Services/StreamingService.swift` | `Services/StreamingService.swift` | User-Agent → `ZyquoAgent/1.0 (macOS)` | | `Services/ModelCatalog.swift` + `Services/ModelCatalogData.swift` | `Services/ModelCatalog.swift` + `ModelCatalogData.swift` | identical catalog; add `agentCapableModels` filter + per-provider agent defaults (§3) | | `Services/SecureKeyStore.swift` | `Services/SecureKeyStore.swift` | info string `ZyquoAgent.vault.v1`, new pepper, vault under `ZyquoAgent/` | | `Services/PersistenceService.swift` | `Services/PersistenceService.swift` | root folder `ZyquoAgent` | | `ViewModels/KeyVaultStore.swift` | `ViewModels/KeyVaultStore.swift` | reuse for Settings → Providers & Keys | | `Tests/ZyquoCloudTests/SSEParserTests.swift`, `SecureKeyStoreTests.swift`, `ModelTests.swift` | `Tests/ZyquoAgentTests/…` | rename; extend with tool-call streaming fixtures | | `Verify/VerifyHarness.swift` | `Verify/VerifyHarness.swift` | replace the "OK" test with the Phase-7 tool-calling battery (§3), run only on 🤖 models | **Add new (Agent-only):** `ToolSpec`/`ToolCall`/`ToolResult` types (§5.3); per-client tool encoding/decoding; argument-JSON validation against the declared schema with a re-prompt path (needed for gpt-oss-class models); the `Agent/`, `Tools/`, `Execution/`, `Workspace/` layers per Phase 2 — none of which touch provider wire code. **Renames for the ZyquoAgent module:** file headers to `Zyquo Agent`; `ZyquoCloud` → `ZyquoAgent` only in User-Agent, PersistenceService folder, HKDF info, pepper, and test module names. Wire formats, base URLs, headers, catalog IDs, error mapping, SSE parsing: **unchanged — that is the whole point.** **Invariants inherited from Cloud (do not regress):** - Decoders tolerate unknown fields and malformed keep-alive chunks; never crash a stream. - Every optional request param is gated by `ParameterSupport`; never send unsupported params. - Cancellation flows through `AsyncThrowingStream.onTermination` → `Task.cancel()` → HTTP abort. - `429/5xx` backoff on non-streaming calls; typed `ProviderError` with human-readable text. - `cheapestModel(for:)` (non-reasoning preferred) for utility calls like auto-titles. - Catalog (`ModelCatalogData.swift`) and this doc stay in sync with any Phase-7 findings.