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)
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 withappendingPathComponent, preserving base paths like/compatible-mode/v1and/v1beta/openai);GET {base}/models. - Request wire types (private
Encodablestructs):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
ParameterSupportallows it (providers 400 on unknown/unsupported params). Notable:usesMaxCompletionTokens→ sendmax_completion_tokensinstead ofmax_tokens(OpenAI reasoning models, Kimi K-series, Cerebras); Mistral mapsreasoning_effortmedium→"high", low→"none" (only accepts high/none); Qwenenable_thinkingis only legal whenstream: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 Perplexitycitations+search_results),WireChoice(deltafor streaming,messagefor non-streaming, baretextfor Together's completions-style streams,finish_reason),WireDelta(content,reasoning_content,reasoning; custominit(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 ondata: [DONE], silently tolerates undecodable keep-alive chunks, yields.reasoningDelta(fromreasoning_contentorreasoning),.textDelta(fromdelta.contentorchoice.text),.citations(once, Perplexity),.usage, then.finished(reason: finish_reason). complete: ifparameterSupport.requiresStreaming(Qwen qwq/qvq, several Together/ DeepInfra-hosted models rejectstream:false), it aggregates the stream instead (completeViaStream); otherwise plain POST viaStreamingService.postJSON(3 attempts, exponential backoff on 429/5xx honoringRetry-After).listModelIDs: decodes{"data":[{"id"}]}OR Together's bare[{"id"}]; strips Gemini'smodels/prefix.- Attachments: text files are injected inline as fenced blocks; images become base64
data-URI
image_urlparts (user messages on vision models only).
1.5 AnthropicClient — native Messages API
- Endpoint:
POST /v1/messages; headersx-api-key: <key>,anthropic-version: 2023-06-01,Content-Type: application/json.GET /v1/models?limit=100for listing. - Request:
WireRequest—model, mandatorymax_tokens(default 8192 when unset),messages(block-structured:WireMessage{role, content:[WireBlock]}with.text/.image(base64 source)blocks; empty text becomes" "), top-levelsystemstring,stream,temperature/top_p(gated — Claude 4.7+/5 reject them, encoded per-model inParameterSupport), andthinking: {"type":"enabled","budget_tokens":8000}/{"type":"disabled"}whenthinkingToggleis supported. (Cloud's PROVIDERS.md documents the full per-model thinking matrix, incl.{"type":"adaptive"}for 4.6+ and "omit entirely" forclaude-fable-5where thinking is always on.) - Streaming (named SSE events, no
[DONE]): the client switches onsse.event ?? decoded.type:message_start→ capturemessage.usage.input_tokenscontent_block_delta→delta.text→.textDelta;delta.thinking→.reasoningDeltamessage_delta→usage.output_tokens+delta.stop_reasonerror→ mid-stream error surfaced asProviderError.serverErrormessage_stop/ping/content_block_start/content_block_stop→ currently ignored (Agent's port MUST handlecontent_block_start/stop+input_json_delta— §5.1)
- Non-streaming: decodes
content: [Block{type,text,thinking}], joinstextblocks into the message body andthinkingblocks intoreasoning; readsstop_reasonandusage.
1.6 StreamingService — shared SSE plumbing (port unchanged)
- Shared
URLSession:timeoutIntervalForRequest = 120,timeoutIntervalForResource = 900,User-Agent: ZyquoCloud/1.0 (macOS)(rename toZyquoAgent/1.0 (macOS)). SSEParser: incremental line parser handlingevent:/data:fields, multi-linedata: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.linesskips 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 typedProviderError; wrapsCancellationError→.cancelled;onTerminationcancels 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):
capabilities.tools == true— native function calling, live-verified by Cloud;- strong multi-step reasoning (flagship/frontier tier, or agentic-coding tuned);
- context window ≥ ~128K (agent transcripts with tool results grow fast);
- reliable streaming of tool-call arguments;
- 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(viaPersistenceService.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 validatescount > 32 + 12 + 16and throwsVaultError.corruptedon any decrypt failure. - Plaintext: JSON
[String: String]— providerrawValue→ 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.GCMSymmetricKey.machineEntropy= IOPlatformUUID (IOKitIOPlatformExpertDevice/kIOPlatformUUIDKey) ‖NSHomeDirectory()— binds the vault to machine + account. Injectable closure for tests.pepper= 30 compiled-in bytes XOR0x5A, 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
infostring 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
contentcontains a{"type":"tool_use","id":"toolu_…","name":…,"input":{…}}block;stop_reason == "tool_use". - Streaming:
content_block_startannounces thetool_useblock (withid+name, empty input); arguments then stream ascontent_block_deltawith{"type":"input_json_delta","partial_json":"…"}fragments — accumulate per blockindexand JSON-parse atcontent_block_stop. (Cloud's client currently ignorescontent_block_start/stopandinput_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. Assistantthinkingblocks must be passed back unchanged on the same model. - Done signal:
stop_reason: "end_turn"(alsomax_tokens,stop_sequence,pause_turn,refusal— Fable 5/Opus 5 can refuse with HTTP 200 —model_context_window_exceeded). No[DONE]sentinel; stream ends atmessage_stop. - Other quirks (already in Cloud's code/notes):
max_tokensmandatory; per-model thinking-config matrix (adaptive vs enabled+budget vs omit-for-Fable-5); temperature/top_p rejected on 4.7+/5; HTTP 529overloaded_errorretry; mid-streamerrorevents.
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"; defaultparallel_tool_calls: trueon Mistral/Cerebras; Cerebras supportsstrict: trueschemas). - 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_callsis an array of index-keyed fragments: first fragment for an index carriesid+function.name, subsequent ones carryfunction.argumentsstring chunks. Accumulate perindex, 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_callsarray intact, then one message per call:{"role":"tool","tool_call_id":…, "content":"<result string>"}(Kimi/DeepSeek/Mistral all follow this;nameoptionally included). - Done signal:
finish_reason: "stop"(length,content_filter; Together addseos; DeepSeek addsinsufficient_system_resource); stream terminates withdata: [DONE]. - Per-provider notes already encoded or documented by Cloud:
- OpenAI: reasoning models need
max_completion_tokensand reject sampling params; ignore unknownobfuscationfield in chunks. - Gemini (compat): tools/function-calling officially supported on the compat endpoint;
ignore
extra_content/thought_signatureextras in deltas;/modelsIDs prefixedmodels/; Google Search grounding available viatoolson 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_effortonly high/none. - Qwen/DashScope:
enable_thinkingonly withstream:true;reasoning_contentdeltas; qwq/qvq reject non-streaming. - DeepSeek:
reasoning_contentdeltas stream before content; thinking on by default on v4-flash; ≤128 tools;: keep-aliveSSE comments. - Kimi:
reasoning_contentbefore content on K-series; built-in$web_searchtool usestype:"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_tokensrequired-style;strict:truetool 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_resultsfields must keep being tolerated by the decoder.
- OpenAI: reasoning models need
- Reasoning surfaces to keep uniform:
delta.reasoning_content(DeepSeek/Qwen/Kimi/some DeepInfra),delta.reasoning(Together/Cerebras gpt-oss), Mistral ThinkChunk arrays, Anthropicthinking_delta, Perplexity inline<think>— all already normalized toChatEvent.reasoningDeltaby 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):
struct ToolSpec: Codable { // handed to the client from ToolRegistry
var name: String
var description: String
var parametersJSONSchema: String // canonical JSON Schema (object) as a string
}
struct ToolCall: Codable, Identifiable, Hashable {
var id: String // provider call id (toolu_… / call_…); synthesize for providers that omit it
var name: String
var argumentsJSON: String // raw accumulated JSON string; parsed+validated by the loop
}
struct ToolResult: Codable { // threaded back on the next request
var toolCallID: String
var content: String // stringified output (stdout/stderr summary, file text…)
var isError: Bool
}
// ChatRequest additions
var tools: [ToolSpec] = []
var toolChoice: ToolChoice = .auto // .auto / .none / .required / .named(String)
// Message additions (so history round-trips correctly per provider)
var toolCalls: [ToolCall]? = nil // on assistant turns
var toolResults: [ToolResult]? = nil // rendered as role:"tool" messages (OpenAI) or
// tool_result user blocks (Anthropic) by the client
// ChatEvent additions
case toolCallStarted(index: Int, id: String, name: String) // live chip in the UI
case toolCallArgumentsDelta(index: Int, delta: String) // streamed args
case toolCalls([ToolCall]) // finalized, parsed set.finished(reason:) is normalized by the client to a small enum surfaced alongside the raw
string: endTurn (stop/end_turn/eos), toolUse (tool_calls/tool_use), maxTokens
(length/max_tokens), refusal, other(String) — the AgentLoop branches only on this.
5.4 Gemini native (only if Phase 7 demands it)
If the compat endpoint's tool streaming proves insufficient, a native GeminiClient would use
POST /v1beta/models/{model}:streamGenerateContent?alt=sse with x-goog-api-key, declare
tools: [{functionDeclarations:[{name,description,parameters}]}] +
toolConfig: {functionCallingConfig: {mode: AUTO|ANY|NONE, allowedFunctionNames}}, receive
candidates[].content.parts[].functionCall {name,args} (complete JSON, not deltas), and reply
with a user part functionResponse {name, response}; completion signaled by
finishReason: "STOP". Not planned for the initial port — Cloud ships without it and all
Gemini catalog models advertise tools on the compat endpoint.
6. Porting plan
Port verbatim (rename module header Zyquo Cloud → Zyquo Agent, ZyquoCloud strings →
ZyquoAgent where they are app-facing: User-Agent, app-support folder, HKDF info, pepper):
From zyquo-cloud/Sources/ZyquoCloud/… |
To zyquo-agent/Sources/ZyquoAgent/… |
Changes |
|---|---|---|
Models/ProviderID.swift |
Models/ProviderID.swift |
none |
Models/AIModel.swift |
Models/AIModel.swift |
add var agentCapable: Bool (derived: capabilities.tools && !isLegacy && curated list from §3) |
Models/Message.swift |
Models/Message.swift |
add toolCalls / toolResults (§5.3) |
Models/Conversation.swift (ChatParameters, Persona) |
Models/ChatParameters.swift |
extract the shared value types; Conversation itself becomes Agent's Task model |
Providers/ProviderProtocol.swift |
Providers/ProviderProtocol.swift |
add tools/toolChoice to ChatRequest; add tool ChatEvent cases; normalized finish reason |
Providers/ProviderRegistry.swift |
Providers/ProviderRegistry.swift |
none |
Providers/OpenAICompatibleClient.swift |
Providers/OpenAICompatibleClient.swift |
encode tools/tool_choice/parallel_tool_calls; decode + stream delta.tool_calls (index-keyed accumulation); emit assistant tool_calls + role:"tool" messages when building bodies from history |
Providers/AnthropicClient.swift |
Providers/AnthropicClient.swift |
encode tools/tool_choice; handle content_block_start/input_json_delta/content_block_stop for tool_use blocks; encode tool_use assistant blocks + tool_result user blocks from history |
Services/StreamingService.swift |
Services/StreamingService.swift |
User-Agent → ZyquoAgent/1.0 (macOS) |
Services/ModelCatalog.swift + Services/ModelCatalogData.swift |
Services/ModelCatalog.swift + ModelCatalogData.swift |
identical catalog; add agentCapableModels filter + per-provider agent defaults (§3) |
Services/SecureKeyStore.swift |
Services/SecureKeyStore.swift |
info string ZyquoAgent.vault.v1, new pepper, vault under ZyquoAgent/ |
Services/PersistenceService.swift |
Services/PersistenceService.swift |
root folder ZyquoAgent |
ViewModels/KeyVaultStore.swift |
ViewModels/KeyVaultStore.swift |
reuse for Settings → Providers & Keys |
Tests/ZyquoCloudTests/SSEParserTests.swift, SecureKeyStoreTests.swift, ModelTests.swift |
Tests/ZyquoAgentTests/… |
rename; extend with tool-call streaming fixtures |
Verify/VerifyHarness.swift |
Verify/VerifyHarness.swift |
replace the "OK" test with the Phase-7 tool-calling battery (§3), run only on 🤖 models |
Add new (Agent-only): ToolSpec/ToolCall/ToolResult types (§5.3); per-client tool
encoding/decoding; argument-JSON validation against the declared schema with a re-prompt path
(needed for gpt-oss-class models); the Agent/, Tools/, Execution/, Workspace/ layers
per Phase 2 — none of which touch provider wire code.
Renames for the ZyquoAgent module: file headers to Zyquo Agent; ZyquoCloud →
ZyquoAgent only in User-Agent, PersistenceService folder, HKDF info, pepper, and test module
names. Wire formats, base URLs, headers, catalog IDs, error mapping, SSE parsing: unchanged
— that is the whole point.
Invariants inherited from Cloud (do not regress):
- Decoders tolerate unknown fields and malformed keep-alive chunks; never crash a stream.
- Every optional request param is gated by
ParameterSupport; never send unsupported params. - Cancellation flows through
AsyncThrowingStream.onTermination→Task.cancel()→ HTTP abort. 429/5xxbackoff on non-streaming calls; typedProviderErrorwith 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.