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

# 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<ChatEvent, Error>
    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: <key>, anthropic-version: 2023-06-01, Content-Type: application/json. GET /v1/models?limit=100 for listing.
  • Request: WireRequestmodel, 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_deltadelta.text.textDelta; delta.thinking.reasoningDelta
    • message_deltausage.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<SHA256>.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: <JSON 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: <JSON Schema>}}]; 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: "<JSON string>"}}]; 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":"<result string>"} (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 <think> — 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 CloudZyquo 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; ZyquoCloudZyquoAgent 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.onTerminationTask.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.