SPB Git

spb/zyquo-agent Public MIT

The autonomous agent that actually operates your Mac — plans, runs real commands, verifies its own work.

Swift 94.7% Shell 4.1% Python 0.7% Makefile 0.5%
49.6 KB · 702 lines markdown
Rendered Raw Blame History
1<!--2  PROVIDER-REUSE.md3  Zyquo Agent45  Author: Simon-Pierre Boucher6  Mail: contact@spboucher.ai7-->89# Zyquo Agent — Provider Reuse (Phase 0.B, contract for the provider layer)1011Compiled 2026-07-30 from an in-depth study of the sibling **Zyquo Cloud** repository at12`/Users/simon-pierreboucher/Desktop/zyquo-cloud` (source files under `Sources/ZyquoCloud/`,13plus `docs/PROVIDERS.md` — Cloud's own live-probed provider research — and `docs/VERIFICATION.md`).1415**Rule of this document:** Zyquo Agent calls every model *exactly* the way Zyquo Cloud does.16The files listed in §1 are ported (near-)verbatim; the only addition is a **normalized17tool-calling interface** layered onto the same `ProviderClient` protocol (§5–§6). The model18picker offers **the same catalog as Cloud** (§2) with the agent-capable subset marked and19defaulted (§3). The key vault is byte-format-compatible in design (§4).2021---2223## 1. How each provider's API is called (Cloud's provider layer, file by file)2425### 1.1 Architecture overview2627Cloud's entire provider layer is **four files** plus shared networking and models:2829| File (in `zyquo-cloud/Sources/ZyquoCloud/`) | Role |30|---|---|31| `Providers/ProviderProtocol.swift` | `ChatRequest`, `ChatEvent`, `protocol ProviderClient`, `ProviderError` (typed, human-readable, maps HTTP status + heterogeneous error bodies) |32| `Providers/ProviderRegistry.swift` | The *only* place that maps provider → client via `ProviderID.wireFormat` |33| `Providers/OpenAICompatibleClient.swift` | ONE client for the 11 OpenAI-schema providers + custom endpoints; all quirks live here |34| `Providers/AnthropicClient.swift` | Native Anthropic Messages API (`/v1/messages`) client |35| `Models/ProviderID.swift` | The 12 built-in providers + `.custom`: display names, base URLs, `wireFormat`, `supportsModelListing` |36| `Models/AIModel.swift` | `AIModel`, `ModelCapabilities` (incl. `tools: Bool`), `ModelPricing`, `ParameterSupport`, `TokenUsage` |37| `Models/Message.swift` | `Message` (role/text/reasoning/attachments/citations/usage/cost), `Attachment`, `Citation` |38| `Models/Conversation.swift` | `ChatParameters` (temperature, topP, maxTokens, penalties, `reasoningEffort`, `thinkingEnabled`), `Persona` |39| `Services/StreamingService.swift` | `SSEEvent`, incremental `SSEParser`, shared `URLSession`, `sseEvents(for:provider:)`, `postJSON`/`getJSON` with backoff |40| `Services/ModelCatalog.swift` | `@MainActor ObservableObject` catalog: built-in + custom + live `/models` diff + favorites + `cheapestModel(for:)` + `defaultModel` |41| `Services/ModelCatalogData.swift` | The full built-in catalog (generated from `docs/PROVIDERS.md`; 170 models — reproduced in §2) |42| `Services/SecureKeyStore.swift` | AES-256-GCM key vault, NO Keychain (§4) |43| `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 |4445**Key finding: there is NO `GeminiClient` in Zyquo Cloud.** Gemini is served through Google's46**OpenAI-compatibility endpoint** (`https://generativelanguage.googleapis.com/v1beta/openai`,47Bearer auth) via `OpenAICompatibleClient`. Cloud's research (`docs/PROVIDERS.md` §Gemini,48verified live) confirms the compat endpoint supports chat + streaming + **function calling49(`tools`)** + structured outputs + vision + `reasoning_effort`. Zyquo Agent's CLAUDE.md sketch50lists a `GeminiClient.swift`; per this study the correct, Cloud-identical approach is to **keep51Gemini on the compat endpoint** (Gemini tool calls then arrive as standard OpenAI `tool_calls`,52one uniform streaming path). A native `GeminiClient` (functionCall/functionResponse parts) is53only needed if Phase 7 finds compat-endpoint tool streaming inadequate — see §5.4.5455### 1.2 Core protocol types (`ProviderProtocol.swift`)5657```swift58struct ChatRequest {                    // provider-agnostic; clients translate to wire format59    var model: AIModel60    var systemPrompt: String?61    var messages: [Message]62    var parameters: ChatParameters63    var stream: Bool = true64}6566enum ChatEvent {                        // streamed back to the UI67    case reasoningDelta(String)68    case textDelta(String)69    case citations([Citation])70    case usage(TokenUsage)71    case finished(reason: String?)72}7374protocol ProviderClient {75    var providerID: ProviderID { get }76    func streamChat(_ request: ChatRequest, apiKey: String) -> AsyncThrowingStream<ChatEvent, Error>77    func complete(_ request: ChatRequest, apiKey: String) async throws -> Message78    func listModelIDs(apiKey: String) async throws -> [String]79}80// + extension ProviderClient.testKey(_:fallbackModel:) -> TimeInterval81//   (uses /models when supported, else a 16-token "Reply with exactly: OK" completion;82//    fallbackModel needed only for Perplexity, which has no /models endpoint)83```8485`ProviderError` cases: `invalidAPIKey`, `rateLimited(retryAfter:)`, `serverError(status:message:)`,86`badRequest`, `networkError`, `invalidResponse`, `missingAPIKey`, `noModelAvailable`, `cancelled`87each with a polished `errorDescription`. `ProviderError.from(status:body:provider:)` maps88401/403→invalidAPIKey, 429→rateLimited, 400/404/422→badRequest, else serverError, and89`extractMessage(from:)` tolerates all observed error shapes: `{"error":{"message":…}}`,90`{"error":"…"}`, `{"message":…}`, `{"detail":…}`, and Gemini's array-wrapped errors.9192### 1.3 Provider table (from `ProviderID.swift`, all verified live by Cloud on 2026-07-30)9394| # | `ProviderID` case | Display name | Base URL | Auth | Wire format | `/models`? |95|---|---|---|---|---|---|---|96| 1 | `.openai` | OpenAI | `https://api.openai.com/v1` | `Authorization: Bearer` | OpenAI chat/completions | ✅ |97| 2 | `.anthropic` | Anthropic | `https://api.anthropic.com/v1` | `x-api-key` + `anthropic-version: 2023-06-01` | **Anthropic Messages** | ✅ (rich metadata, `?limit=100`) |98| 3 | `.xai` | xAI | `https://api.x.ai/v1` | Bearer | OpenAI-compat | ✅ |99| 4 | `.mistral` | Mistral | `https://api.mistral.ai/v1` | Bearer | OpenAI-compat | ✅ (per-model capability flags incl. `function_calling`) |100| 5 | `.gemini` | Google Gemini | `https://generativelanguage.googleapis.com/v1beta/openai` | Bearer (compat endpoint) | OpenAI-compat | ✅ (IDs prefixed `models/` — client strips) |101| 6 | `.qwen` | Alibaba Qwen | `https://dashscope-intl.aliyuncs.com/compatible-mode/v1` | Bearer | OpenAI-compat | ✅ |102| 7 | `.deepseek` | DeepSeek | `https://api.deepseek.com` | Bearer | OpenAI-compat | ✅ (2 models) |103| 8 | `.kimi` | Kimi (Moonshot) | `https://api.moonshot.ai/v1` | Bearer | OpenAI-compat | ✅ |104| 9 | `.perplexity` | Perplexity | `https://api.perplexity.ai` | Bearer | OpenAI-compat + search extras | ❌ (404 — `supportsModelListing == false`) |105| 10 | `.together` | Together AI | `https://api.together.xyz/v1` | Bearer | OpenAI-compat | ✅ (bare array, not `{"data":[…]}`) |106| 11 | `.deepinfra` | DeepInfra | `https://api.deepinfra.com/v1/openai` | Bearer | OpenAI-compat | ✅ |107| 12 | `.cerebras` | Cerebras | `https://api.cerebras.ai/v1` | Bearer | OpenAI-compat | ✅ (3 models) |108| — | `.custom` | Custom | user-supplied `customBaseURL` on the `AIModel` | Bearer | OpenAI-compat | ✅ |109110`wireFormat`: `.anthropicMessages` for `.anthropic`, `.openAIChatCompletions` for everything else.111Env-var names used by the verify harness (reuse for Agent's Phase 7): `OPENAI_API_KEY`,112`ANTHROPIC_API_KEY`, `XAI_API_KEY`, `MISTRAL_API_KEY`, `GEMINI_API_KEY`, `DASHSCOPE_API_KEY`,113`DEEPSEEK_API_KEY`, `MOONSHOT_API_KEY`, `PERPLEXITY_API_KEY`, `TOGETHER_API_KEY`,114`DEEPINFRA_API_KEY`, `CEREBRAS_API_KEY`.115116### 1.4 `OpenAICompatibleClient` — request/response design117118- **Endpoint:** `POST {base}/chat/completions` (path appended with `appendingPathComponent`,119  preserving base paths like `/compatible-mode/v1` and `/v1beta/openai`); `GET {base}/models`.120- **Request wire types (private `Encodable` structs):** `WireRequest` (`model`, `messages`,121  `stream`, `stream_options`, `temperature`, `top_p`, `max_tokens`, `max_completion_tokens`,122  `frequency_penalty`, `presence_penalty`, `reasoning_effort`, `enable_thinking`), `WireMessage`123  (`role` + `content`), `WireContent` (plain string OR parts array), `WirePart`124  (`{"type":"text"}` / `{"type":"image_url","image_url":{"url":"data:…;base64,…"}}`).125- **Parameter gating:** every optional field is included **only if** the model's126  `ParameterSupport` allows it (providers 400 on unknown/unsupported params). Notable:127  `usesMaxCompletionTokens` → send `max_completion_tokens` instead of `max_tokens`128  (OpenAI reasoning models, Kimi K-series, Cerebras); Mistral maps `reasoning_effort`129  medium→"high", low→"none" (only accepts high/none); Qwen `enable_thinking` is only legal130  when `stream:true`.131- **`stream_options: {"include_usage": true}`** is sent for openai/xai/gemini/deepseek/kimi/132  together/cerebras/custom; omitted for mistral (rejects unknown params), qwen, deepinfra,133  perplexity (usage included automatically).134- **Response wire types (private `Decodable`):** `WireChunk` (`choices`, `usage`, plus135  Perplexity `citations` + `search_results`), `WireChoice` (`delta` for streaming, `message`136  for non-streaming, bare `text` for Together's completions-style streams, `finish_reason`),137  `WireDelta` (`content`, `reasoning_content`, `reasoning`; custom `init(from:)` also decodes138  Mistral's content-**array** ThinkChunk/TextChunk shape), `WireUsage`139  (`prompt_tokens`/`completion_tokens`/`completion_tokens_details.reasoning_tokens`).140- **Streaming:** consumes `StreamingService.sseEvents`, stops on `data: [DONE]`, silently141  tolerates undecodable keep-alive chunks, yields `.reasoningDelta` (from `reasoning_content`142  or `reasoning`), `.textDelta` (from `delta.content` or `choice.text`), `.citations` (once,143  Perplexity), `.usage`, then `.finished(reason: finish_reason)`.144- **`complete`:** if `parameterSupport.requiresStreaming` (Qwen qwq/qvq, several Together/145  DeepInfra-hosted models reject `stream:false`), it aggregates the stream instead146  (`completeViaStream`); otherwise plain POST via `StreamingService.postJSON` (3 attempts,147  exponential backoff on 429/5xx honoring `Retry-After`).148- **`listModelIDs`:** decodes `{"data":[{"id"}]}` OR Together's bare `[{"id"}]`; strips149  Gemini's `models/` prefix.150- **Attachments:** text files are injected inline as fenced blocks; images become base64151  data-URI `image_url` parts (user messages on vision models only).152153### 1.5 `AnthropicClient` — native Messages API154155- **Endpoint:** `POST /v1/messages`; headers `x-api-key: <key>`, `anthropic-version: 2023-06-01`,156  `Content-Type: application/json`. `GET /v1/models?limit=100` for listing.157- **Request:** `WireRequest``model`, **mandatory `max_tokens`** (default 8192 when unset),158  `messages` (block-structured: `WireMessage{role, content:[WireBlock]}` with `.text` /159  `.image(base64 source)` blocks; empty text becomes `" "`), top-level `system` string,160  `stream`, `temperature`/`top_p` (gated — Claude 4.7+/5 reject them, encoded per-model in161  `ParameterSupport`), and `thinking: {"type":"enabled","budget_tokens":8000}` /162  `{"type":"disabled"}` when `thinkingToggle` is supported. (Cloud's PROVIDERS.md documents the163  full per-model thinking matrix, incl. `{"type":"adaptive"}` for 4.6+ and "omit entirely" for164  `claude-fable-5` where thinking is always on.)165- **Streaming (named SSE events, no `[DONE]`):** the client switches on166  `sse.event ?? decoded.type`:167  - `message_start` → capture `message.usage.input_tokens`168  - `content_block_delta``delta.text``.textDelta`; `delta.thinking``.reasoningDelta`169  - `message_delta``usage.output_tokens` + `delta.stop_reason`170  - `error` → mid-stream error surfaced as `ProviderError.serverError`171  - `message_stop` / `ping` / `content_block_start` / `content_block_stop` → currently ignored172    (Agent's port MUST handle `content_block_start`/`stop` + `input_json_delta` — §5.1)173- **Non-streaming:** decodes `content: [Block{type,text,thinking}]`, joins `text` blocks into174  the message body and `thinking` blocks into `reasoning`; reads `stop_reason` and `usage`.175176### 1.6 `StreamingService` — shared SSE plumbing (port unchanged)177178- Shared `URLSession`: `timeoutIntervalForRequest = 120`, `timeoutIntervalForResource = 900`,179  `User-Agent: ZyquoCloud/1.0 (macOS)` (rename to `ZyquoAgent/1.0 (macOS)`).180- `SSEParser`: incremental line parser handling `event:`/`data:` fields, multi-line `data:`181  joins, `:` comment/keep-alive lines (DeepSeek sends `: keep-alive`), CRLF, and a trailing182  flush for streams ending without a final blank line. **Critical detail preserved:**183  `URLSession.AsyncBytes.lines` skips empty lines (the SSE event separators) — the byte stream184  is split manually on `\n`.185- `sseEvents(for:provider:)`: on non-2xx, reads the full error body and throws the typed186  `ProviderError`; wraps `CancellationError``.cancelled`; `onTermination` cancels the task187  (this is what makes the Stop button actually abort the HTTP stream — same mechanism will188  make Agent runs cancellable).189- Unit tests exist in `Tests/ZyquoCloudTests/SSEParserTests.swift` — port them.190191---192193## 2. The full model catalog (from `Services/ModelCatalogData.swift`, generated 2026-07-30)194195**170 built-in models across 12 providers.** Zyquo Agent ships this exact catalog.196Legend — caps: **V** vision, **T** tools/function-calling, **R** reasoning output, **J** JSON197mode, **C** citations; price = USD per 1M tokens in/out (— = not published);198⭐ = `isRecommended`, 🕰 = `isLegacy`; **🤖 = agent-capable** (the subset per §3);199**bold 🤖** entries are the suggested per-provider agent defaults.200Param notes: `mct` = uses `max_completion_tokens`, `re` = `reasoning_effort`,201`tt` = thinking toggle, `rs` = requiresStreaming, `no-t/p` = temperature & top_p rejected.202203### OpenAI (27)204205| Model ID | Display name | Ctx | Max out | Caps | $/1M | Params | Flags | Agent |206|---|---|---|---|---|---|---|---|---|207| `gpt-5.6-sol` | GPT-5.6 Sol | 1,050,000 | 128K | VTRJ | 5.00/30.00 | no-t/p, mct, re | ⭐ | 🤖 |208| `gpt-5.6-terra` | GPT-5.6 Terra | 1,050,000 | 128K | VTRJ | 2.50/15.00 | no-t/p, mct, re | ⭐ | **🤖 default** |209| `gpt-5.6-luna` | GPT-5.6 Luna | 1,050,000 | 128K | VTRJ | 1.00/6.00 | no-t/p, mct, re | | 🤖 |210| `chat-latest` | ChatGPT Latest | 128,000 | — | VTJ | 5.00/30.00 | mct | | — (rolling chat tuning) |211| `gpt-5.5` | GPT-5.5 | 400,000 | — | VTRJ | 5.00/30.00 | no-t/p, mct, re | | 🤖 |212| `gpt-5.4` | GPT-5.4 | 400,000 | 128K | VTRJ | 2.50/15.00 | no-t/p, mct, re | | 🤖 |213| `gpt-5.4-mini` | GPT-5.4 mini | 400,000 | — | VTRJ | 0.75/4.50 | no-t/p, mct, re | | 🤖 |214| `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) |215| `gpt-5.3-chat-latest` | GPT-5.3 Chat Latest | 128,000 | — | VTJ | — | mct | | — |216| `gpt-5.2` | GPT-5.2 | 400,000 | 128K | VTRJ | 1.75/14.00 | no-t/p, mct, re | | 🤖 |217| `gpt-5.2-chat-latest` | GPT-5.2 Chat Latest | 128,000 | 16K | VTJ | 1.75/14.00 | mct | | — |218| `gpt-5.1` | GPT-5.1 | 400,000 | 128K | VTRJ | 1.25/10.00 | no-t/p, mct, re | | 🤖 |219| `gpt-5` | GPT-5 | 400,000 | 128K | VTRJ | 1.25/10.00 | no-t/p, mct, re | | 🤖 |220| `gpt-5-mini` | GPT-5 mini | 400,000 | 128K | VTRJ | 0.25/2.00 | no-t/p, mct, re | | 🤖 |221| `gpt-5-nano` | GPT-5 nano | 400,000 | 128K | VTRJ | 0.05/0.40 | no-t/p, mct, re | | — |222| `o3` | OpenAI o3 | 200,000 | 100K | VTRJ | 2.00/8.00 | no-t/p, mct, re | | 🤖 |223| `o4-mini` | OpenAI o4-mini | 200,000 | 100K | VTRJ | 1.10/4.40 | no-t/p, mct, re | | 🤖 |224| `o3-mini` | OpenAI o3-mini | 200,000 | 100K | TRJ | 1.10/4.40 | no-t/p, mct, re | 🕰 | — |225| `o1` | OpenAI o1 | 200,000 | 100K | VTRJ | 15.00/60.00 | no-t/p, mct, re | 🕰 | — |226| `gpt-4.1` | GPT-4.1 | 1,047,576 | 32,768 | VTJ | 2.00/8.00 | openAIDefault | 🕰 | — |227| `gpt-4.1-mini` | GPT-4.1 mini | 1,047,576 | 32,768 | VTJ | 0.40/1.60 | openAIDefault | 🕰 | — |228| `gpt-4.1-nano` | GPT-4.1 nano | 1,047,576 | 32,768 | VTJ | 0.10/0.40 | openAIDefault | 🕰 | — |229| `gpt-4o` | GPT-4o | 128,000 | 16,384 | VTJ | 2.50/10.00 | openAIDefault | 🕰 | — |230| `gpt-4o-mini` | GPT-4o mini | 128,000 | 16,384 | VTJ | 0.15/0.60 | openAIDefault | 🕰 | — |231| `gpt-4-turbo` | GPT-4 Turbo | 128,000 | 4,096 | VTJ | 10.00/30.00 | openAIDefault | 🕰 | — |232| `gpt-4` | GPT-4 | 8,192 | 8,192 | T | 30.00/60.00 | openAIDefault | 🕰 | — |233| `gpt-3.5-turbo` | GPT-3.5 Turbo | 16,385 | 4,096 | TJ | 0.50/1.50 | openAIDefault | 🕰 | — |234235### Anthropic (11)236237| Model ID | Display name | Ctx | Max out | Caps | $/1M | Params | Flags | Agent |238|---|---|---|---|---|---|---|---|---|239| `claude-opus-5` | Claude Opus 5 | 1,000,000 | 128K | VTRJ | 5.00/25.00 | no-t/p, tt | ⭐ | 🤖 |240| `claude-sonnet-5` | Claude Sonnet 5 | 1,000,000 | 128K | VTRJ | 3.00/15.00 | no-t/p, tt | ⭐ | **🤖 default (and overall app default)** |241| `claude-fable-5` | Claude Fable 5 | 1,000,000 | 128K | VTRJ | 10.00/50.00 | no-t/p, thinking always on (no toggle) | | 🤖 |242| `claude-opus-4-8` | Claude Opus 4.8 | 1,000,000 | 128K | VTRJ | 5.00/25.00 | no-t/p, tt | | 🤖 |243| `claude-opus-4-7` | Claude Opus 4.7 | 1,000,000 | 128K | VTRJ | 5.00/25.00 | no-t/p, tt | | 🤖 |244| `claude-opus-4-6` | Claude Opus 4.6 | 1,000,000 | 128K | VTRJ | 5.00/25.00 | t/p ok, tt | | 🤖 |245| `claude-sonnet-4-6` | Claude Sonnet 4.6 | 1,000,000 | 128K | VTRJ | 3.00/15.00 | t/p ok, tt | | 🤖 |246| `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) |247| `claude-opus-4-5-20251101` | Claude Opus 4.5 | 200,000 | 64K | VTRJ | 5.00/25.00 | t/p ok, tt | 🕰 | — |248| `claude-sonnet-4-5-20250929` | Claude Sonnet 4.5 | 1,000,000 | 64K | VTRJ | 3.00/15.00 | t/p ok, tt | 🕰 | — |249| `claude-opus-4-1-20250805` | Claude Opus 4.1 | 200,000 | 32K | VTRJ | 15.00/75.00 | t/p ok, tt | 🕰 | — |250251### xAI / Grok (5)252253| Model ID | Display name | Ctx | Caps | $/1M | Params | Flags | Agent |254|---|---|---|---|---|---|---|---|255| `grok-4.5` | Grok 4.5 | 500,000 | VTRJ | 2.00/6.00 | re | ⭐ | **🤖 default** |256| `grok-4.3` | Grok 4.3 | 1,000,000 | VTRJ | 1.25/2.50 | re | | 🤖 |257| `grok-4.20` | Grok 4.20 Reasoning | 1,000,000 | VTRJ | 1.25/2.50 | | | 🤖 |258| `grok-4.20-non-reasoning` | Grok 4.20 Non-Reasoning | 1,000,000 | VTJ | 1.25/2.50 | | | 🤖 |259| `grok-code-fast-1` | Grok Code Fast 1 | 256,000 | VTRJ | 1.00/2.00 | | ⭐ | 🤖 (agentic-coding tuned) |260261### Mistral (10)262263| Model ID | Display name | Ctx | Caps | $/1M | Params | Flags | Agent |264|---|---|---|---|---|---|---|---|265| `mistral-medium-latest` | Mistral Medium 3.5 | 262,144 | VTRJ | 1.50/7.50 | pen, re | ⭐ | **🤖 default** |266| `mistral-large-latest` | Mistral Large 3 | 262,144 | VTJ | 0.50/1.50 | openAIDefault | ⭐ | 🤖 |267| `mistral-small-latest` | Mistral Small 4 | 262,144 | VTRJ | 0.15/0.60 | pen, re | ⭐ | 🤖 |268| `codestral-latest` | Codestral | 256,000 | TJ | 0.30/0.90 | openAIDefault | | — (FIM/code-completion focus) |269| `ministral-14b-latest` | Ministral 3 14B | 262,144 | VTJ | 0.20/0.20 | openAIDefault | | — (small) |270| `ministral-8b-latest` | Ministral 3 8B | 262,144 | VTJ | 0.15/0.15 | openAIDefault | | — |271| `ministral-3b-latest` | Ministral 3 3B | 131,072 | VTJ | 0.10/0.10 | openAIDefault | | — |272| `magistral-medium-latest` | Magistral Medium | 131,072 | TRJ | 2.00/5.00 | openAIDefault | 🕰 | — |273| `devstral-latest` | Devstral 2 | 262,144 | TJ | 0.40/2.00 | openAIDefault | 🕰 | — |274| `open-mistral-nemo` | Mistral Nemo | 131,072 | TJ | 0.15/0.15 | openAIDefault | 🕰 | — |275276### Google Gemini (14) — served via the OpenAI-compat endpoint277278| Model ID | Display name | Ctx | Max out | Caps | $/1M | Flags | Agent |279|---|---|---|---|---|---|---|---|280| `gemini-3.6-flash` | Gemini 3.6 Flash | 1,048,576 | 65,536 | VTRJ | 1.50/7.50 | ⭐ | **🤖 default** |281| `gemini-3.5-flash` | Gemini 3.5 Flash | 1,048,576 | 65,536 | VTRJ | 1.50/9.00 | | 🤖 |282| `gemini-3.5-flash-lite` | Gemini 3.5 Flash-Lite | 1,048,576 | 65,536 | VTRJ | 0.30/2.50 | ⭐ | 🤖 (budget tier) |283| `gemini-3.1-pro-preview` | Gemini 3.1 Pro (Preview) | 1,048,576 | 65,536 | VTRJ | 2.00/12.00 | ⭐ | 🤖 |284| `gemini-3.1-flash-lite` | Gemini 3.1 Flash-Lite | 1,048,576 | 65,536 | VTRJ | 0.25/1.50 | | — |285| `gemini-2.5-pro` | Gemini 2.5 Pro | 1,048,576 | 65,536 | VTRJ | 1.25/10.00 | | 🤖 |286| `gemini-2.5-flash` | Gemini 2.5 Flash | 1,048,576 | 65,536 | VTRJ | 0.30/2.50 | | 🤖 |287| `gemini-2.5-flash-lite` | Gemini 2.5 Flash-Lite | 1,048,576 | 65,536 | VTRJ | 0.10/0.40 | | — |288| `gemini-pro-latest` | Gemini Pro (Latest) | 1,048,576 | 65,536 | VTRJ | — | | 🤖 (rolling alias) |289| `gemini-flash-latest` | Gemini Flash (Latest) | 1,048,576 | 65,536 | VTRJ | — | | 🤖 (rolling alias) |290| `gemini-flash-lite-latest` | Gemini Flash-Lite (Latest) | 1,048,576 | 65,536 | VTRJ | — | | — |291| `gemini-3-flash-preview` | Gemini 3 Flash (Preview) | 1,048,576 | 65,536 | VTRJ | 0.50/3.00 | | — |292| `gemma-4-26b-a4b-it` | Gemma 4 26B | 262,144 | 32,768 | J | — | | **excluded — no tools** |293| `gemma-4-31b-it` | Gemma 4 31B | 262,144 | 32,768 | J | — | | **excluded — no tools** |294295All Gemini chat models take `reasoning_effort` on the compat endpoint.296297### Alibaba Qwen / DashScope (32)298299| Model ID | Display name | Ctx | Caps | $/1M | Params | Flags | Agent |300|---|---|---|---|---|---|---|---|301| `qwen3.7-max` | Qwen3.7 Max | 1,000,000 | TRJ | 2.50/7.50 | tt | ⭐ | **🤖 default** |302| `qwen3.7-plus` | Qwen3.7 Plus | 1,000,000 | VTRJ | 0.32/1.28 | tt | ⭐ | 🤖 |303| `qwen3.7-flash` | Qwen3.7 Flash | 1,000,000 | VTRJ | 0.03/0.13 | tt | ⭐ | 🤖 (budget) |304| `qwen3.6-plus` | Qwen3.6 Plus | 1,000,000 | VTRJ | — | tt | | 🤖 |305| `qwen3.6-flash` | Qwen3.6 Flash | 1,000,000 | VTRJ | — | tt | | — |306| `qwen3.5-plus` | Qwen3.5 Plus | 1,000,000 | VTRJ | — | tt | | 🤖 |307| `qwen3.5-flash` | Qwen3.5 Flash | 1,000,000 | VTRJ | — | tt | | — |308| `qwen-max` | Qwen Max | 128,000 | TRJ | — | tt | | — |309| `qwen-plus` | Qwen Plus | 1,000,000 | TRJ | — | tt | | — |310| `qwen-turbo` | Qwen Turbo | 1,000,000 | TRJ | — | tt | 🕰 | — |311| `qwen-flash` | Qwen Flash | 1,000,000 | TRJ | — | tt | | — |312| `qwen3-coder-plus` | Qwen3 Coder Plus | 1,000,000 | TJ | — | | | 🤖 (agentic-coding tuned) |313| `qwen3-coder-flash` | Qwen3 Coder Flash | 1,000,000 | TJ | — | | | 🤖 |314| `qwen3-coder-next` | Qwen3 Coder Next | 262,144 | TJ | — | | | 🤖 ("multi-turn tool interactions") |315| `qwen3-coder-480b-a35b-instruct` | Qwen3 Coder 480B A35B | 262,144 | TJ | — | | | 🤖 |316| `qwen3-vl-plus` | Qwen3 VL Plus | 1,000,000 | VTRJ | — | tt | | — (vision-focused) |317| `qwen3-vl-flash` | Qwen3 VL Flash | 1,000,000 | VTRJ | — | tt | | — |318| `qwen3-vl-235b-a22b-instruct` | Qwen3 VL 235B Instruct | 131,072 | VTJ | — | | | — |319| `qwen3-vl-235b-a22b-thinking` | Qwen3 VL 235B Thinking | 131,072 | VTRJ | — | | | — |320| `qvq-max` | QVQ Max | 131,072 | VRJ | — | rs | | **excluded — no tools** |321| `qwq-plus` | QwQ Plus | 131,072 | TRJ | — | rs | | — (requiresStreaming; tool-calls unreliable) |322| `qwen3.5-397b-a17b` | Qwen3.5 397B A17B | 262,144 | TRJ | — | tt | | 🤖 |323| `qwen3.5-122b-a10b` | Qwen3.5 122B A10B | 262,144 | TRJ | — | tt | | — |324| `qwen3.5-35b-a3b` | Qwen3.5 35B A3B | 262,144 | TRJ | — | tt | | — |325| `qwen3-235b-a22b-instruct-2507` | Qwen3 235B Instruct 2507 | 262,144 | TJ | — | | | — |326| `qwen3-235b-a22b-thinking-2507` | Qwen3 235B Thinking 2507 | 262,144 | TRJ | — | | | — |327| `qwen3-next-80b-a3b-instruct` | Qwen3 Next 80B Instruct | 262,144 | TJ | — | | | — |328| `qwen3-next-80b-a3b-thinking` | Qwen3 Next 80B Thinking | 262,144 | TRJ | — | | | — |329| `deepseek-v4-pro` | DeepSeek V4 Pro (DashScope) | 1,000,000 | TRJ | — | tt | | 🤖 |330| `deepseek-v4-flash` | DeepSeek V4 Flash (DashScope) | 1,000,000 | TRJ | — | tt | | — (prefer first-party DeepSeek) |331| `glm-5.2` | GLM 5.2 (DashScope) | 198,000 | TRJ | — | tt | | 🤖 |332| `kimi-k2.7-code` | Kimi K2.7 Code (DashScope) | 262,144 | TRJ | — | tt | | — (prefer first-party Kimi) |333334### DeepSeek (2)335336| Model ID | Display name | Ctx | Max out | Caps | $/1M | Params | Flags | Agent |337|---|---|---|---|---|---|---|---|---|338| `deepseek-v4-flash` | DeepSeek V4 Flash | 1,000,000 | 384K | TRJ | 0.14/0.28 | re, tt | ⭐ | 🤖 |339| `deepseek-v4-pro` | DeepSeek V4 Pro | 1,000,000 | 384K | TRJ | 0.435/0.87 | re, tt | ⭐ | **🤖 default** |340341Both support up to 128 functions per request; `finish_reason` may be the DeepSeek-specific342`insufficient_system_resource` ("servers overloaded").343344### Kimi / Moonshot (12)345346| Model ID | Display name | Ctx | Max out | Caps | $/1M | Params | Flags | Agent |347|---|---|---|---|---|---|---|---|---|348| `kimi-k3` | Kimi K3 | 1,048,576 | 131,072 | VTRJ | 3.00/15.00 | no-t/p, mct, re (thinking always on) | ⭐ | **🤖 default** |349| `kimi-k2.7-code` | Kimi K2.7 Code | 262,144 | — | VTRJ | 0.95/4.00 | no-t/p, mct | ⭐ | 🤖 (agentic-coding tuned) |350| `kimi-k2.7-code-highspeed` | Kimi K2.7 Code Highspeed | 262,144 | — | VTRJ | 1.90/8.00 | no-t/p, mct | | 🤖 |351| `kimi-k2.6` | Kimi K2.6 | 262,144 | — | VTRJ | 0.95/4.00 | no-t/p, mct, tt | | 🤖 |352| `kimi-k2.5` | Kimi K2.5 | 262,144 | — | VTRJ | 0.60/3.00 | no-t/p, mct, tt | | 🤖 |353| `moonshot-v1-8k` | Moonshot v1 8K | 8,192 | — | TJ | 0.20/2.00 | openAIDefault | 🕰 | — |354| `moonshot-v1-32k` | Moonshot v1 32K | 32,768 | — | TJ | 1.00/3.00 | openAIDefault | 🕰 | — |355| `moonshot-v1-128k` | Moonshot v1 128K | 131,072 | — | TJ | 2.00/5.00 | openAIDefault | 🕰 | — |356| `moonshot-v1-auto` | Moonshot v1 Auto | 131,072 | — | TJ | — | openAIDefault | 🕰 | — |357| `moonshot-v1-8k-vision-preview` | Moonshot v1 8K Vision | 8,192 | — | VTJ | 0.20/2.00 | openAIDefault | 🕰 | — |358| `moonshot-v1-32k-vision-preview` | Moonshot v1 32K Vision | 32,768 | — | VTJ | 1.00/3.00 | openAIDefault | 🕰 | — |359| `moonshot-v1-128k-vision-preview` | Moonshot v1 128K Vision | 131,072 | — | VTJ | 2.00/5.00 | openAIDefault | 🕰 | — |360361### Perplexity (4) — **entire provider excluded from the agent-capable subset**362363| Model ID | Display name | Ctx | Caps | $/1M | Flags | Agent |364|---|---|---|---|---|---|---|365| `sonar` | Sonar | 128,000 | JC | 1.00/1.00 | ⭐ | **excluded** |366| `sonar-pro` | Sonar Pro | 200,000 | JC | 3.00/15.00 | ⭐ | **excluded** |367| `sonar-reasoning-pro` | Sonar Reasoning Pro | 128,000 | RJC | 2.00/8.00 | | **excluded** |368| `sonar-deep-research` | Sonar Deep Research | 128,000 | RJC | 2.00/8.00 | | **excluded** |369370Cloud's research is explicit: "no vision/image input; **no tool/function calling on the Sonar371chat API**". These are web-search answer engines. Keep them in the picker (same list as Cloud)372but never selectable as the agent driver — or gray them out with an explanation.373374### Together AI (16)375376| Model ID | Display name | Ctx | Caps | $/1M | Params | Flags | Agent |377|---|---|---|---|---|---|---|---|378| `moonshotai/Kimi-K3` | Kimi K3 | 1,000,000 | TRJ | 3.00/15.00 | openAIDefault | ⭐ | 🤖 |379| `moonshotai/Kimi-K2.7-Code` | Kimi K2.7 Code | 262,144 | TRJ | 0.95/4.00 | openAIDefault | | 🤖 |380| `moonshotai/Kimi-K2.6` | Kimi K2.6 | 262,144 | TRJ | 1.20/4.50 | openAIDefault | | — |381| `deepseek-ai/DeepSeek-V4-Pro` | DeepSeek V4 Pro | 512,000 | TRJ | 1.74/3.48 | openAIDefault | ⭐ | **🤖 default** |382| `zai-org/GLM-5.2` | GLM 5.2 | 512,000 | TRJ | 1.40/4.40 | openAIDefault | | 🤖 |383| `Qwen/Qwen3.7-Max` | Qwen3.7 Max | 1,000,000 | TRJ | 1.25/3.75 | pen, rs | | 🤖 |384| `Qwen/Qwen3.7-Plus` | Qwen3.7 Plus | 1,000,000 | TJ | 0.32/1.28 | pen, rs | | — |385| `Qwen/Qwen3.6-Plus` | Qwen3.6 Plus | 1,000,000 | TJ | 0.50/3.00 | pen, rs | | — |386| `Qwen/Qwen3.5-9B` | Qwen3.5 9B | 262,144 | TJ | 0.17/0.25 | pen, rs | | — (small) |387| `meta-llama/Llama-3.3-70B-Instruct-Turbo` | Llama 3.3 70B Turbo | 131,072 | TJ | 1.04/1.04 | openAIDefault | | — |388| `openai/gpt-oss-120b` | GPT-OSS 120B | 131,072 | TRJ | 0.15/0.60 | pen, re | ⭐ | 🤖 |389| `openai/gpt-oss-20b` | GPT-OSS 20B | 131,072 | TRJ | 0.05/0.20 | pen, re | | — (may hallucinate tool calls — Cloud note) |390| `nvidia/nemotron-3-ultra-550b-a55b` | Nemotron 3 Ultra 550B | 512,288 | TRJ | 0.60/3.60 | openAIDefault | | 🤖 |391| `MiniMaxAI/MiniMax-M3` | MiniMax M3 | 524,288 | TRJ | 0.30/1.20 | openAIDefault | | 🤖 |392| `google/gemma-4-31B-it` | Gemma 4 31B | 262,144 | TJ (vision disabled — live-verified empty answers) | 0.39/0.97 | pen, rs | | — |393| `thinkingmachines/Inkling` | Inkling | 524,288 | TRJ | 1.00/4.05 | openAIDefault | | — |394395Together quirks preserved in the client: bare-array `/models`, completions-style396`choices[].text` streaming for some models, extra `finish_reason: "eos"`.397398### DeepInfra (34)399400| Model ID | Display name | Ctx | Caps | $/1M | Flags | Agent |401|---|---|---|---|---|---|---|402| `anthropic/claude-fable-5` | Claude Fable 5 | 1,000,000 | VTRJ | 10.00/50.00 | | 🤖 |403| `anthropic/claude-opus-5` | Claude Opus 5 | 1,000,000 | VTRJ | 5.00/25.00 | | 🤖 |404| `anthropic/claude-sonnet-5` | Claude Sonnet 5 | 1,000,000 | VTRJ | 2.00/10.00 | | 🤖 |405| `anthropic/claude-opus-4-8` | Claude Opus 4.8 | 1,000,000 | VTRJ | 5.00/25.00 | | 🤖 |406| `anthropic/claude-haiku-4-5` | Claude Haiku 4.5 | 200,000 | VTRJ | 1.00/5.00 | | 🤖 |407| `google/gemini-3.1-pro` | Gemini 3.1 Pro | 1,000,000 | VTRJ | 2.00/12.00 | | 🤖 |408| `google/gemini-3.5-flash` | Gemini 3.5 Flash | 1,000,000 | VTRJ | 1.50/9.00 | | 🤖 |409| `google/gemini-3.1-flash-lite` | Gemini 3.1 Flash-Lite | 1,000,000 | VTJ | 0.25/1.50 | | — |410| `google/gemini-2.5-pro` | Gemini 2.5 Pro | 1,000,000 | VTRJ | 1.25/10.00 | | — |411| `google/gemini-2.5-flash` | Gemini 2.5 Flash | 1,000,000 | VTRJ | 0.30/2.50 | | — |412| `deepseek-ai/DeepSeek-V4-Pro` | DeepSeek V4 Pro | 1,048,576 | TRJ | 1.30/2.60 | ⭐ | **🤖 default** |413| `deepseek-ai/DeepSeek-V4-Flash` | DeepSeek V4 Flash | 1,048,576 | TJ | 0.09/0.18 | ⭐ | 🤖 |414| `deepseek-ai/DeepSeek-V3.1` | DeepSeek V3.1 | 163,840 | TRJ | 0.25/0.95 | | — |415| `deepseek-ai/DeepSeek-R1-0528` | DeepSeek R1 0528 | 163,840 | R (no tools) | 0.50/2.15 | | **excluded — no tools** |416| `moonshotai/Kimi-K2.7-Code` | Kimi K2.7 Code | 262,144 | TRJ | 0.74/3.50 | | 🤖 |417| `moonshotai/Kimi-K2.6` | Kimi K2.6 | 262,144 | TRJ | 0.75/3.50 | | — |418| `moonshotai/Kimi-K2.5` | Kimi K2.5 | 262,144 | TJ (rs) | 0.45/2.25 | | — |419| `zai-org/GLM-5.2` | GLM 5.2 | 1,048,576 | TRJ | 0.75/2.40 | ⭐ | 🤖 |420| `zai-org/GLM-4.7` | GLM 4.7 | 202,752 | TRJ | 0.40/1.75 | | — |421| `Qwen/Qwen3.7-Max` | Qwen3.7 Max | 256,000 | TRJ | 2.50/7.50 | | 🤖 |422| `Qwen/Qwen3.5-397B-A17B` | Qwen3.5 397B A17B | 262,144 | TRJ | 0.45/3.00 | | — |423| `Qwen/Qwen3-235B-A22B-Instruct-2507` | Qwen3 235B Instruct 2507 | 262,144 | TJ | 0.09/0.55 | | — |424| `Qwen/Qwen3-235B-A22B-Thinking-2507` | Qwen3 235B Thinking 2507 | 262,144 | TRJ | 0.23/2.30 | | — |425| `Qwen/Qwen3-Coder-480B-A35B-Instruct-Turbo` | Qwen3 Coder 480B Turbo | 262,144 | TJ | 0.30/1.00 | | 🤖 |426| `Qwen/Qwen3-VL-235B-A22B-Instruct` | Qwen3 VL 235B | 262,144 | VTJ | 0.20/0.88 | | — |427| `meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8` | Llama 4 Maverick | 1,048,576 | VTJ | 0.20/0.80 | | — |428| `meta-llama/Llama-4-Scout-17B-16E-Instruct` | Llama 4 Scout | 327,680 | VTJ | 0.10/0.30 | | — |429| `meta-llama/Llama-3.3-70B-Instruct-Turbo` | Llama 3.3 70B Turbo | 131,072 | TJ | 0.10/0.32 | | — |430| `meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo` | Llama 3.1 8B Turbo | 131,072 | TJ | 0.02/0.04 | | — |431| `openai/gpt-oss-120b` | GPT-OSS 120B | 131,072 | TRJ | 0.037/0.17 | ⭐ | 🤖 |432| `openai/gpt-oss-20b` | GPT-OSS 20B | 131,072 | TRJ | 0.03/0.14 | | — |433| `MiniMaxAI/MiniMax-M3` | MiniMax M3 | 524,288 | TRJ | 0.30/1.20 | | 🤖 |434| `nvidia/NVIDIA-Nemotron-3-Ultra-550B-A55B` | Nemotron 3 Ultra 550B | 262,144 | TRJ | 0.50/2.20 | | — |435| `mistralai/Mistral-Small-3.2-24B-Instruct-2506` | Mistral Small 3.2 24B | 128,000 | VTJ | 0.075/0.20 | | — |436437(Note: `google/gemma-4-31B-it` was removed from DeepInfra on 2026-07-30 — endpoint hangs.438DeepInfra-proxied Claude/Gemini speak the plain OpenAI schema, no native thinking config.)439440### Cerebras (3)441442| Model ID | Display name | Ctx | Max out | Caps | $/1M | Params | Flags | Agent |443|---|---|---|---|---|---|---|---|---|444| `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) |445| `gemma-4-31b` | Gemma 4 31B | 131,072 | 40,000 | VTRJ | 0.99/1.49 | pen, mct, re | | 🤖 (parallel tools, strict schemas) |446| `zai-glm-4.7` | GLM 4.7 | 131,072 | 40,000 | TRJ | 2.25/2.75 | pen, mct, re | 🕰 | — (discontinued 2026-08-17) |447448---449450## 3. The agent-capable subset (what Zyquo Agent marks and defaults to)451452**Criteria** (from the catalog's capability flags + Cloud's per-provider research):4531. `capabilities.tools == true` — native function calling, live-verified by Cloud;4542. strong multi-step reasoning (flagship/frontier tier, or agentic-coding tuned);4553. context window ≥ ~128K (agent transcripts with tool results grow fast);4564. reliable streaming of tool-call arguments;4575. not legacy, not a niche tuning (chat-alias, vision-only, search-only).458459**Counts:** ~70 of the 170 catalog models are marked 🤖 agent-capable (per-model marks in the460§2 tables). Everything else remains in the picker (same list as Cloud) but is visually461de-emphasized and produces a "not recommended for agent tasks" hint if selected.462463**Overall default agent model: `claude-sonnet-5` (Anthropic)** — 1M context, adaptive thinking,464best-in-class tool use, and the Messages API's tool_use blocks are the most explicit465tool-calling contract of the twelve providers. Per-provider defaults are bolded in §2.466467**Recommended top tier** (surface first in the model chip): `claude-sonnet-5`, `claude-opus-5`,468`gpt-5.6-terra`, `gpt-5.6-sol`, `gemini-3.6-flash`, `grok-4.5`, `grok-code-fast-1`,469`deepseek-v4-pro`, `kimi-k3`, `kimi-k2.7-code`, `qwen3.7-max`, `mistral-medium-latest`,470Cerebras `gpt-oss-120b` (speed king for tight loops).471472**Excluded from agent use, with reasons:**473474| Exclusion | Reason |475|---|---|476| Perplexity — all 4 sonar models | No function calling at all on the Sonar chat API (Cloud, live-verified); search answer engines, not agents |477| `gemma-4-26b-a4b-it`, `gemma-4-31b-it` (Gemini) | `tools: false` in catalog; function calling unverified for open Gemma on the Gemini API |478| `qvq-max` (Qwen) | `tools: false`; visual-reasoning only; requiresStreaming |479| `deepseek-ai/DeepSeek-R1-0528` (DeepInfra) | `tools: false` in catalog — R1 has no reliable function calling |480| `qwq-plus` (Qwen) | requiresStreaming + first-gen reasoning; tool-calling reliability poor |481| 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 |482| Chat-tuned rolling aliases (`chat-latest`, `gpt-5.x-chat-latest`) | Conversational tuning, no reasoning; not built for long agentic runs |483| 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) |484| VL-focused Qwen models | Optimized for image understanding, not command loops |485| `codestral-latest`, `thinkingmachines/Inkling`, Together `google/gemma-4-31B-it` | Code-completion / unverified-tooling niches |486487All exclusions are *soft* (UI de-emphasis + default filter), except Perplexity + `tools:false`488models which are *hard* exclusions — the agent loop refuses to start with a model whose489`capabilities.tools == false`.490491**Phase 7 contract:** every 🤖 model above gets the scripted (a) schema-receipt, (b) valid492tool call for "list the files in the workspace using the shell tool", (c) tool_result493consumption, (d) streaming test — following the `VerifyHarness` pattern494(`Sources/ZyquoCloud/Verify/VerifyHarness.swift`): env keys, per-model results table,495written to `docs/VERIFICATION.md`.496497---498499## 4. SecureKeyStore — the AES-256-GCM vault (NO Keychain), reused verbatim500501Source: `zyquo-cloud/Sources/ZyquoCloud/Services/SecureKeyStore.swift`502(+ tests in `Tests/ZyquoCloudTests/SecureKeyStoreTests.swift`). Port both.503504- **File:** `~/Library/Application Support/ZyquoCloud/vault.zq` → Agent uses505  `~/Library/Application Support/ZyquoAgent/vault.zq` (via `PersistenceService.shared.rootDirectory`,506  which is the app-support folder named after the app).507- **Blob layout:** `[salt 32 B][AES-GCM combined: nonce 12 B ‖ ciphertext ‖ tag 16 B]`.508  Written atomically with `.completeFileProtection`. Load validates509  `count > 32 + 12 + 16` and throws `VaultError.corrupted` on any decrypt failure.510- **Plaintext:** JSON `[String: String]` — provider `rawValue` → API key511  (`{"openai":"sk-…","anthropic":"sk-ant-…", …}`).512- **Key derivation:** `HKDF<SHA256>.deriveKey(ikm: machineEntropy ‖ pepper, salt: vault salt,513  info: "ZyquoCloud.vault.v1", outputByteCount: 32)` → `AES.GCM` `SymmetricKey`.514  - `machineEntropy` = IOPlatformUUID (IOKit `IOPlatformExpertDevice` /515    `kIOPlatformUUIDKey`) ‖ `NSHomeDirectory()` — binds the vault to machine + account.516    Injectable closure for tests.517  - `pepper` = 30 compiled-in bytes XOR `0x5A`, assembled at runtime (never a literal in518    the binary).519  - Re-saves reuse the **existing salt** (`existingSalt()`) so derivation stays stable.520- **API:** `loadKeys()`, `saveKeys(_:)`, `key(for: ProviderID)`, `setKey(_:for:)`,521  `deleteKey(for:)`, `static redacted(_:)``"••••abcd"` (last 4 chars only, everywhere in UI).522- **Agent decisions:** keep the **same design and blob format**; change only the HKDF `info`523  string to `"ZyquoAgent.vault.v1"` and (recommended) a distinct pepper — the two apps keep524  separate vaults with an identical user experience; keys are entered per-app (a Cloud vault525  cannot be decrypted by Agent by design — machine-bound, app-info-bound). Keep the rules:526  decrypt on demand only, never log, never plaintext on disk, no Keychain.527- The SwiftUI wrapper `ViewModels/KeyVaultStore.swift` (per-provider key status, test-key528  latency chip) is also reusable for the Settings → Providers & Keys tab.529530---531532## 5. Provider-specific tool-calling quirks (what the normalized interface must absorb)533534Zyquo Cloud declares `capabilities.tools` per model but **never sends tools** — Zyquo Agent535adds that. Cloud's `docs/PROVIDERS.md` already documents each provider's tool wire format536(request schema sections + finish_reason inventories). Consolidated contract:537538### 5.1 Anthropic (native Messages API) — the odd one out539540- **Declare:** top-level `tools: [{name, description, input_schema: <JSON Schema>}]`;541  `tool_choice: {"type":"auto"|"any"|"tool","name":…}` (+ `disable_parallel_tool_use`).542- **Model calls a tool:** assistant `content` contains a543  `{"type":"tool_use","id":"toolu_…","name":…,"input":{…}}` block; `stop_reason == "tool_use"`.544- **Streaming:** `content_block_start` announces the `tool_use` block (with `id` + `name`,545  empty input); arguments then stream as `content_block_delta` with546  `{"type":"input_json_delta","partial_json":"…"}` fragments — accumulate per block `index`547  and JSON-parse at `content_block_stop`. (Cloud's client currently ignores548  `content_block_start/stop` and `input_json_delta` — the port must add these three cases.)549- **Return the result:** append a **user** message whose content is550  `[{"type":"tool_result","tool_use_id":"toolu_…","content":"…", "is_error":bool}]`551  NOT a special role. Multiple tool_results go in one user turn for parallel calls.552  Assistant `thinking` blocks must be passed back **unchanged** on the same model.553- **Done signal:** `stop_reason: "end_turn"` (also `max_tokens`, `stop_sequence`,554  `pause_turn`, `refusal` — Fable 5/Opus 5 can refuse with HTTP 200 —555  `model_context_window_exceeded`). No `[DONE]` sentinel; stream ends at `message_stop`.556- **Other quirks (already in Cloud's code/notes):** `max_tokens` mandatory; per-model557  thinking-config matrix (adaptive vs enabled+budget vs omit-for-Fable-5); temperature/top_p558  rejected on 4.7+/5; HTTP 529 `overloaded_error` retry; mid-stream `error` events.559560### 5.2 OpenAI-compatible family (OpenAI, xAI, Mistral, Gemini-compat, Qwen, DeepSeek, Kimi, Together, DeepInfra, Cerebras, custom)561562- **Declare:** `tools: [{"type":"function","function":{name, description, parameters: <JSON Schema>}}]`;563  `tool_choice: "none"|"auto"|"required"|{"type":"function","function":{"name":…}}`564  (Mistral additionally accepts `"any"`; default `parallel_tool_calls: true` on565  Mistral/Cerebras; Cerebras supports `strict: true` schemas).566- **Model calls tools:** assistant message carries567  `tool_calls: [{id, type:"function", function:{name, arguments: "<JSON string>"}}]`;568  `finish_reason == "tool_calls"`.569- **Streaming deltas:** `choices[].delta.tool_calls` is an array of **index-keyed fragments**:570  first fragment for an index carries `id` + `function.name`, subsequent ones carry571  `function.arguments` string chunks. Accumulate per `index`, then JSON-parse each arguments572  string when the stream finishes (`finish_reason:"tool_calls"` or `[DONE]`).573- **Return the result:** append the assistant message **with its `tool_calls` array intact**,574  then one message per call: `{"role":"tool","tool_call_id":…, "content":"<result string>"}`575  (Kimi/DeepSeek/Mistral all follow this; `name` optionally included).576- **Done signal:** `finish_reason: "stop"` (`length`, `content_filter`; Together adds `eos`;577  DeepSeek adds `insufficient_system_resource`); stream terminates with `data: [DONE]`.578- **Per-provider notes already encoded or documented by Cloud:**579  - **OpenAI:** reasoning models need `max_completion_tokens` and reject sampling params;580    ignore unknown `obfuscation` field in chunks.581  - **Gemini (compat):** tools/function-calling officially supported on the compat endpoint;582    ignore `extra_content`/`thought_signature` extras in deltas; `/models` IDs prefixed583    `models/`; Google Search grounding available via `tools` on Gemini 3+ (don't mix with584    function tools unless verified).585  - **Mistral:** content can be an **array** of ThinkChunk/TextChunk objects (handled in586    `WireDelta.init(from:)`); `tool_choice: "any"`; `reasoning_effort` only high/none.587  - **Qwen/DashScope:** `enable_thinking` only with `stream:true`; `reasoning_content`588    deltas; qwq/qvq reject non-streaming.589  - **DeepSeek:** `reasoning_content` deltas stream **before** content; thinking on by590    default on v4-flash; ≤128 tools; `: keep-alive` SSE comments.591  - **Kimi:** `reasoning_content` before content on K-series; built-in `$web_search` tool592    uses `type:"builtin_function"` (do NOT use for Agent — our tools are local); temperature593    unsupported on K-series (no-t/p + mct).594  - **Together:** some models stream completions-style `choices[].text`; bare-array `/models`;595    `finish_reason:"eos"`.596  - **Cerebras:** `max_completion_tokens` required-style; `strict:true` tool schemas;597    gpt-oss-120b may emit malformed/hallucinated tool calls — validate arguments against the598    schema and re-prompt on failure (this validation belongs in the normalized layer for599    ALL providers).600  - **Perplexity:** no tools — hard-excluded (§3); `citations`/`search_results` fields must601    keep being tolerated by the decoder.602- **Reasoning surfaces to keep uniform:** `delta.reasoning_content` (DeepSeek/Qwen/Kimi/some603  DeepInfra), `delta.reasoning` (Together/Cerebras gpt-oss), Mistral ThinkChunk arrays,604  Anthropic `thinking_delta`, Perplexity inline `<think>` — all already normalized to605  `ChatEvent.reasoningDelta` by Cloud's clients; unchanged.606607### 5.3 The normalized interface Zyquo Agent adds608609Extend the ported types (one uniform surface; per-provider translation stays inside clients):610611```swift612struct ToolSpec: Codable {              // handed to the client from ToolRegistry613    var name: String614    var description: String615    var parametersJSONSchema: String     // canonical JSON Schema (object) as a string616}617618struct ToolCall: Codable, Identifiable, Hashable {619    var id: String                       // provider call id (toolu_… / call_…); synthesize for providers that omit it620    var name: String621    var argumentsJSON: String            // raw accumulated JSON string; parsed+validated by the loop622}623624struct ToolResult: Codable {             // threaded back on the next request625    var toolCallID: String626    var content: String                  // stringified output (stdout/stderr summary, file text…)627    var isError: Bool628}629630// ChatRequest additions631var tools: [ToolSpec] = []632var toolChoice: ToolChoice = .auto       // .auto / .none / .required / .named(String)633634// Message additions (so history round-trips correctly per provider)635var toolCalls: [ToolCall]? = nil         // on assistant turns636var toolResults: [ToolResult]? = nil     // rendered as role:"tool" messages (OpenAI) or637                                         // tool_result user blocks (Anthropic) by the client638639// ChatEvent additions640case toolCallStarted(index: Int, id: String, name: String)   // live chip in the UI641case toolCallArgumentsDelta(index: Int, delta: String)       // streamed args642case toolCalls([ToolCall])                                   // finalized, parsed set643```644645`.finished(reason:)` is normalized by the client to a small enum surfaced alongside the raw646string: `endTurn` (`stop`/`end_turn`/`eos`), `toolUse` (`tool_calls`/`tool_use`), `maxTokens`647(`length`/`max_tokens`), `refusal`, `other(String)` — the `AgentLoop` branches only on this.648649### 5.4 Gemini native (only if Phase 7 demands it)650651If the compat endpoint's tool streaming proves insufficient, a native `GeminiClient` would use652`POST /v1beta/models/{model}:streamGenerateContent?alt=sse` with `x-goog-api-key`, declare653`tools: [{functionDeclarations:[{name,description,parameters}]}]` +654`toolConfig: {functionCallingConfig: {mode: AUTO|ANY|NONE, allowedFunctionNames}}`, receive655`candidates[].content.parts[].functionCall {name,args}` (complete JSON, not deltas), and reply656with a user part `functionResponse {name, response}`; completion signaled by657`finishReason: "STOP"`. **Not planned for the initial port** — Cloud ships without it and all658Gemini catalog models advertise tools on the compat endpoint.659660---661662## 6. Porting plan663664**Port verbatim** (rename module header `Zyquo Cloud``Zyquo Agent`, `ZyquoCloud` strings →665`ZyquoAgent` where they are app-facing: User-Agent, app-support folder, HKDF info, pepper):666667| From `zyquo-cloud/Sources/ZyquoCloud/…` | To `zyquo-agent/Sources/ZyquoAgent/…` | Changes |668|---|---|---|669| `Models/ProviderID.swift` | `Models/ProviderID.swift` | none |670| `Models/AIModel.swift` | `Models/AIModel.swift` | add `var agentCapable: Bool` (derived: `capabilities.tools && !isLegacy && curated list from §3`) |671| `Models/Message.swift` | `Models/Message.swift` | add `toolCalls` / `toolResults` (§5.3) |672| `Models/Conversation.swift` (ChatParameters, Persona) | `Models/ChatParameters.swift` | extract the shared value types; Conversation itself becomes Agent's Task model |673| `Providers/ProviderProtocol.swift` | `Providers/ProviderProtocol.swift` | add `tools`/`toolChoice` to `ChatRequest`; add tool `ChatEvent` cases; normalized finish reason |674| `Providers/ProviderRegistry.swift` | `Providers/ProviderRegistry.swift` | none |675| `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 |676| `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 |677| `Services/StreamingService.swift` | `Services/StreamingService.swift` | User-Agent → `ZyquoAgent/1.0 (macOS)` |678| `Services/ModelCatalog.swift` + `Services/ModelCatalogData.swift` | `Services/ModelCatalog.swift` + `ModelCatalogData.swift` | identical catalog; add `agentCapableModels` filter + per-provider agent defaults (§3) |679| `Services/SecureKeyStore.swift` | `Services/SecureKeyStore.swift` | info string `ZyquoAgent.vault.v1`, new pepper, vault under `ZyquoAgent/` |680| `Services/PersistenceService.swift` | `Services/PersistenceService.swift` | root folder `ZyquoAgent` |681| `ViewModels/KeyVaultStore.swift` | `ViewModels/KeyVaultStore.swift` | reuse for Settings → Providers & Keys |682| `Tests/ZyquoCloudTests/SSEParserTests.swift`, `SecureKeyStoreTests.swift`, `ModelTests.swift` | `Tests/ZyquoAgentTests/…` | rename; extend with tool-call streaming fixtures |683| `Verify/VerifyHarness.swift` | `Verify/VerifyHarness.swift` | replace the "OK" test with the Phase-7 tool-calling battery (§3), run only on 🤖 models |684685**Add new (Agent-only):** `ToolSpec`/`ToolCall`/`ToolResult` types (§5.3); per-client tool686encoding/decoding; argument-JSON validation against the declared schema with a re-prompt path687(needed for gpt-oss-class models); the `Agent/`, `Tools/`, `Execution/`, `Workspace/` layers688per Phase 2 — none of which touch provider wire code.689690**Renames for the ZyquoAgent module:** file headers to `Zyquo Agent`; `ZyquoCloud`691`ZyquoAgent` only in User-Agent, PersistenceService folder, HKDF info, pepper, and test module692names. Wire formats, base URLs, headers, catalog IDs, error mapping, SSE parsing: **unchanged693— that is the whole point.**694695**Invariants inherited from Cloud (do not regress):**696- Decoders tolerate unknown fields and malformed keep-alive chunks; never crash a stream.697- Every optional request param is gated by `ParameterSupport`; never send unsupported params.698- Cancellation flows through `AsyncThrowingStream.onTermination``Task.cancel()` → HTTP abort.699- `429/5xx` backoff on non-streaming calls; typed `ProviderError` with human-readable text.700- `cheapestModel(for:)` (non-reasoning preferred) for utility calls like auto-titles.701- Catalog (`ModelCatalogData.swift`) and this doc stay in sync with any Phase-7 findings.702