SPB Git

spb/zyquo-cloud-web Public MIT

Zyquo Cloud Web — every cloud model, one beautiful chat, entirely in your browser.

TypeScript 81.9% CSS 8.9% JavaScript 7.5% Shell 1.1% HTML 0.6%

phase0: provider reuse study, CORS matrix (empirical + documented), native UX reference

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Simon-Pierre Boucher committed 10 days ago (Jul 31, 2026)

Showing 7 changed files with +932 and −0

added .gitignore +18 −0
@@ -0,0 +1,18 @@
1 +#
2 +# .gitignore
3 +# Zyquo Cloud Web
4 +#
5 +# Author: Simon-Pierre Boucher
6 +# Mail: contact@spboucher.ai
7 +#
8 +
9 +node_modules/
10 +dist/
11 +dist-ssr/
12 +coverage/
13 +*.local
14 +.env
15 +.env.*
16 +.keys.local.json
17 +.DS_Store
18 +*.log
added CLAUDE.md +272 −0
@@ -0,0 +1,272 @@
1 +# CLAUDE.md — Zyquo Cloud Web
2 +
3 +## Project Identity
4 +
5 +**Zyquo Cloud Web** is the **browser edition of Zyquo Cloud**: a legendary, fast, beautiful **React** web app that delivers the same multi-provider AI chat experience as the native macOS **Zyquo Cloud** — but running entirely in the browser, with **no authentication, no accounts, and no backend**. Everything lives on the user's device: the user enters their own provider API keys, which are stored in **`localStorage`**, and their **conversation history is also stored locally** (localStorage / IndexedDB). There is no server, no login, no user database — open the page, add your keys, start chatting.
6 +
7 +It fronts **the same 12 providers and the exact same models as native Zyquo Cloud**, with streaming and non-streaming chat, model switching, Markdown + code rendering, and the family's premium design language. Because there is no backend, API calls go **directly from the browser to each provider's API** using the user's keys.
8 +
9 +⚠️ **Reality of a keyless, backend-less browser app (design around this honestly):**
10 +- API keys in `localStorage` are **not strongly secure** — any script running on the page (or someone with device access) can read them. Be transparent about this in the UI (a clear one-time notice), keep the app dependency-light and CSP-hardened to reduce XSS risk, and never transmit keys anywhere except directly to the chosen provider. This is a deliberate "bring-your-own-key, local-only" model, not a secure vault.
11 +- **CORS:** some providers' APIs do not allow direct browser (cross-origin) calls. During Phase 0 you MUST determine, per provider, whether direct browser calls work, and document the reality + the optional fallback (a user-supplied proxy URL / the option to run Zyquo Router locally and point the web app at it). Do not pretend all 12 work browser-side without checking.
12 +
13 +**Naming conventions (use consistently everywhere):**
14 +- Display name / product name: `Zyquo Cloud Web`
15 +- App title / PWA name: `Zyquo Cloud`
16 +- Deployment domain: **`www.zyquo.cloud`** (canonical, HTTPS; redirect apex `zyquo.cloud``www`)
17 +- Repo module prefix in file headers: `Zyquo Cloud Web`
18 +- Storage key namespace: `zyquo.cloud.web.*` (e.g., `zyquo.cloud.web.keys`, `zyquo.cloud.web.conversations`, `zyquo.cloud.web.settings`)
19 +
20 +---
21 +
22 +## 📋 MANDATORY FILE HEADER — EVERY CODE FILE
23 +
24 +**Every single code file you write** (all `.ts`, `.tsx`, `.js`, `.jsx`, `.css`, config files, `Makefile`, shell/build scripts — anything containing code) **MUST begin with this header comment**, adapted to the file's comment syntax:
25 +
26 +```ts
27 +/*
28 + * <FileName>
29 + * Zyquo Cloud Web
30 + *
31 + * Author: Simon-Pierre Boucher
32 + * Mail: contact@spboucher.ai
33 + */
34 +```
35 +
36 +For shell scripts / Makefiles / YAML (`#` comments):
37 +
38 +```bash
39 +#
40 +# <filename>
41 +# Zyquo Cloud Web
42 +#
43 +# Author: Simon-Pierre Boucher
44 +# Mail: contact@spboucher.ai
45 +#
46 +```
47 +
48 +No exceptions. If you ever create or refactor a file and the header is missing, add it. Before declaring the project done, run a sweep over the repository to verify every code file carries the header.
49 +
50 +---
51 +
52 +## 🧭 METHODOLOGY — WORK METHODICALLY, KEEP EVERYTHING COHERENT
53 +
54 +You must execute this project **strictly in phase order (0 → 8)**. Do not jump ahead, do not interleave phases, do not build the full UI before one provider streams a real completion in the browser, and do not write provider code before Phase 0 research + Zyquo Cloud study are complete.
55 +
56 +**Working rules:**
57 +
58 +1. **One phase at a time.** At the start of each phase, write a checklist into `docs/PLAN.md`; check items off as you go. At the end of each phase, run a **phase checkpoint**: build (`npm run build`), type-check, lint, fix everything, write a 3–5 line phase summary in `docs/PLAN.md` before moving on.
59 +2. **Phase gates:** Phase 0 is complete only when `docs/PROVIDER-REUSE.md` and `docs/CORS-MATRIX.md` are complete. Phase 2 is complete only when a real streaming completion works browser-side against at least one provider. Phase 3 is complete only when local persistence (keys + conversations) round-trips reliably. Phase 4 spec is the contract for all UI in Phase 6. Phase 7 is complete only when the provider matrix is verified with real keys. Phase 8 is complete only when the production build + deployment are done.
60 +3. **Single source of truth, everywhere:**
61 + - Provider/model behavior → ported from the native **Zyquo Cloud** repo (Phase 0.B), adapted to browser `fetch`/streaming; never re-invent request formats. **All Zyquo Cloud models are available.**
62 + - Colors, fonts, spacing, radii → only from `ZyquoTheme` design tokens (the same family system). Zero raw hex or magic numbers in components.
63 + - All storage access → only through a `storage/` layer (never call `localStorage` directly from components).
64 + - All provider networking → only in the `providers/` layer.
65 + - Naming → per the conventions above.
66 +4. **Coherence sweeps:** after Phases 3, 6, and 8 (consistent naming — always `Provider`, `AIModel`, `Conversation`, `Message`; no dead code; headers present; one reusable message/bubble component; folders match Phase 2).
67 +5. **Build early, build often.** Keep the type-checker and linter green; never accumulate more than one file of unbuilt changes.
68 +6. **Commit discipline:** one logical unit per commit, phase-prefixed. **Never commit any API keys** or `.env` with secrets.
69 +7. **Local-only is a hard invariant:** no analytics that capture prompt content, no backend calls except directly to the user's chosen provider (and an optional user-configured proxy). Keys and history never leave the device except as part of a provider request the user initiated.
70 +
71 +---
72 +
73 +## ⚠️ PHASE 0 — MANDATORY RESEARCH + ZYQUO CLOUD STUDY (DO THIS FIRST, BEFORE ANY CODE)
74 +
75 +Two mandatory tracks, two documents. No React until both are done.
76 +
77 +### 0.B — `docs/PROVIDER-REUSE.md` — study the native Zyquo Cloud repo and reuse its providers
78 +
79 +**Before writing provider code, read and study the native Zyquo Cloud repository** (the macOS app already built from its own CLAUDE.md). Locate it on disk (check the user's projects folder; if not found, ask the user for its path). Document and reuse:
80 +
81 +1. **Exactly how each provider's API is called** in Zyquo Cloud: base URLs, endpoints, auth headers (`Bearer` vs `x-api-key`, `anthropic-version`, Gemini key param), request/response shapes, the shared OpenAI-compatible client, the native Anthropic (Messages API) and Gemini (`generateContent`) clients, and the SSE streaming formats. The web app must call providers the **same way**, re-implemented with the browser **`fetch` API + `ReadableStream`** for SSE (instead of URLSession).
82 +2. **The complete model catalog** (`ModelCatalog` / `docs/PROVIDERS.md`) with capabilities (vision, tools, reasoning, context, max output) and pricing. **Include ALL of these models** in the web app's catalog. Reproduce it as a typed TS catalog.
83 +3. **Provider-specific streaming/parse quirks** (Anthropic event blocks, Gemini chunks, DeepSeek/Qwen reasoning fields, Perplexity citations) so the browser streaming layer normalizes them behind one interface.
84 +4. The native app's **design tokens and UX** (light-theme flagship, layout, message rendering, model chip) so the web app matches the family look. (The web app re-implements the vault concept as plain localStorage — see the honesty note in Identity — NOT the machine-bound encryption, since a browser has no equivalent; be explicit about this difference.)
85 +
86 +### 0.A — `docs/CORS-MATRIX.md` — browser feasibility per provider (INTENSIVE WEB RESEARCH)
87 +
88 +Because calls go **directly from the browser**, you MUST research and document, per provider, the CURRENT reality:
89 +
90 +1. **CORS support:** does the provider's API send `Access-Control-Allow-Origin` allowing direct browser calls? Test/verify from official docs and community reports for each of the 12 providers (OpenAI, Anthropic, xAI, Mistral, Gemini, Qwen/DashScope, DeepSeek, Kimi, Perplexity, Together, DeepInfra, Cerebras). Note that some providers explicitly support browser use (and may need a special header like Anthropic's `anthropic-dangerous-direct-browser-access` — verify current requirement) and some block CORS entirely.
91 +2. **Per-provider verdict:** ✅ direct browser call works / ⚠️ works with a specific header or caveat / ❌ blocked by CORS.
92 +3. **Fallback design:** for ❌/⚠️ providers, document the **optional user-configurable proxy**: a base-URL override per provider so a user can point at their own CORS proxy **or at a locally running Zyquo Router** (the family's local gateway) which exposes an OpenAI-compatible endpoint. The app ships fully client-side; the proxy is opt-in and user-supplied. Make the UI surface this clearly ("This provider can't be called directly from the browser — set a proxy URL or run Zyquo Router locally").
93 +4. **Streaming over fetch:** confirm SSE-over-`fetch` `ReadableStream` parsing works for each shape (OpenAI SSE, Anthropic events, Gemini stream) in the browser, incl. cancellation via `AbortController`.
94 +
95 +---
96 +
97 +## PHASE 1 — Project Setup (React)
98 +
99 +- **Stack:** **React + TypeScript (strict) + Vite**. Styling: Tailwind CSS (or CSS variables + tokens) implementing `ZyquoTheme`. State: lightweight (Zustand or React context + reducers) — no heavy framework. Routing minimal (single-page; optional hash routes for settings). Markdown: a well-maintained renderer (react-markdown + remark/rehype) with a syntax highlighter (Shiki or highlight.js) and copy buttons. No backend, no auth libraries.
100 +- **PWA (recommended):** installable, offline shell (the app UI works offline; only provider calls need network), `manifest.webmanifest`, service worker for the app shell (NOT for caching API responses/keys).
101 +- **Scripts:** `npm run dev`, `build`, `preview`, `lint`, `typecheck`, `check`.
102 +- **Security posture from the start:** strict **Content-Security-Policy** (allow connections only to the known provider API origins + a user-set proxy origin; no inline scripts; no third-party script tags), Subresource Integrity where applicable, and no dependencies that aren't needed (every dependency is XSS surface for stored keys).
103 +- **Deployment target:** static hosting (served as static assets). If deployed under the Zyquo platform, coordinate with the Node+ngrok topology on `m2u64` used by the main site, OR any static host — document in Phase 8.
104 +
105 +---
106 +
107 +## PHASE 2 — Architecture + Browser Provider Layer
108 +
109 +```
110 +src/
111 +├── main.tsx, App.tsx
112 +├── design/ # ZyquoTheme tokens, global styles, theme (light default) + dark toggle
113 +├── types/ # Provider, AIModel, Conversation, Message, Settings…
114 +├── providers/ # PORTED FROM NATIVE ZYQUO CLOUD (browser fetch/stream)
115 +│ ├── types.ts # ProviderClient interface (send, stream)
116 +│ ├── openaiCompatible.ts # OpenAI, xAI, Mistral, DashScope, DeepSeek, Kimi, Perplexity, Together, DeepInfra, Cerebras, Gemini-compat
117 +│ ├── anthropic.ts # native Messages API + browser header
118 +│ ├── gemini.ts # native generateContent
119 +│ ├── sse.ts # fetch + ReadableStream SSE parser, AbortController cancel
120 +│ └── catalog.ts # ALL models from Zyquo Cloud (typed)
121 +├── storage/ # the ONLY place that touches localStorage/IndexedDB
122 +│ ├── keys.ts # get/set/remove provider keys (localStorage, namespaced)
123 +│ ├── conversations.ts # CRUD conversations/messages (IndexedDB for scale, or localStorage)
124 +│ └── settings.ts # theme, default model, per-provider proxy base URLs
125 +├── state/ # store (Zustand): conversations, activeModel, streaming state, settings
126 +├── features/ # ai orchestration: send message, stream, stop, regenerate, title-gen
127 +├── components/ # UI (see Phase 4)
128 +├── hooks/
129 +└── lib/
130 +```
131 +
132 +- **Streaming with `fetch` + `AbortController`:** parse SSE from a `ReadableStream`; normalize every provider's stream into a common token/tool/usage event; cancel on stop or when the user sends a new message.
133 +- **PHASE GATE:** a real **streaming** completion renders token-by-token in a bare test view against at least one ✅-CORS provider, using a key from `localStorage`.
134 +
135 +---
136 +
137 +## PHASE 3 — LOCAL PERSISTENCE (KEYS + HISTORY, NO BACKEND)
138 +
139 +All state is local. **PHASE GATE:** keys and full conversation history survive a page reload and browser restart, and can be exported/imported.
140 +
141 +### 3.A — Keys (`storage/keys.ts`)
142 +- Store per-provider keys in `localStorage` under `zyquo.cloud.web.keys` (a JSON map). Provide get/set/remove, masked display (last 4 chars), and a per-provider **Test** action.
143 +- Optional light **obfuscation at rest** (e.g., encrypt with a key derived from a user-set passphrase via WebCrypto AES-GCM) offered as an opt-in "lock with passphrase" feature — but be honest in the UI that without a passphrase the keys are stored in plaintext, and even with one, a compromised page can read them once unlocked. Default is plain localStorage with a clear first-run notice; passphrase lock is a bonus.
144 +
145 +### 3.B — Conversations (`storage/conversations.ts`)
146 +- Persist conversations and messages locally. **Prefer IndexedDB** (via a tiny wrapper or `idb`) for capacity and performance with long histories; fall back to localStorage for small data. Namespace `zyquo.cloud.web.conversations`.
147 +- Store: conversation id, title, created/updated, model per conversation, system prompt, params, and the full message list (role, content, images as base64/blob refs, usage, reasoning, citations, timestamps).
148 +- CRUD + search across all conversations; autosave on every change; safe migration/versioning of the stored schema.
149 +
150 +### 3.C — Settings & data portability
151 +- `storage/settings.ts`: theme (light default), default model, per-provider proxy base URLs (from the CORS matrix), UI prefs.
152 +- **Export / Import all data** (keys optional, conversations, settings) as a JSON file — the only "backup" mechanism since there's no cloud. Clearly warn that exported files may contain API keys if the user includes them.
153 +- **Clear data** controls (per-conversation, all conversations, all keys, everything) with confirmation.
154 +
155 +---
156 +
157 +## PHASE 4 — DESIGN SYSTEM & UI (LIGHT THEME FLAGSHIP, RESPONSIVE)
158 +
159 +Match native Zyquo Cloud's look via the family `ZyquoTheme` tokens; **light theme is the default and flagship**, dark is a secondary opt-in toggle. The app is a chat client and must be **responsive / mobile-friendly** (usable on a phone browser).
160 +
161 +### 4.1 — Light theme (family standard)
162 +Reuse Zyquo Cloud's light palette: airy off-white background (`#FAFBFD`), white surfaces, sky-indigo accent (`#4E6AF0`), 0.5pt-equivalent hairlines, ultra-soft shadows on floating elements only, generous body line-height (1.45). Typography/spacing/radii identical to the family. Dark theme derived. **Default = light**, never dark-auto.
163 +
164 +### 4.2 — Layout & screens
165 +- **Shell:** a chat layout with a **sidebar** (conversations: search, new chat, pinned/date groups, rename/delete, model badge) and a **chat area** (header with editable title + centered **model chip** → the full **Model Menu** in 4.2.1; transcript; input bar). Matches native Zyquo Cloud's structure.
166 +
167 +#### 4.2.1 — The Model Menu (a headline surface — make it exceptional)
168 +Clicking the model chip opens a rich, fast **command-palette-style model picker** (⌘/Ctrl+K also opens it):
169 +- **Instant search/filter** by model name, provider, or capability; fuzzy match; keyboard-navigable (arrows + Enter), Esc to close.
170 +- **Grouped by provider** with the provider glyph/logo, collapsible sections; a **Favorites** group pinned at top (star any model); a **Recents** group.
171 +- **Filter chips/toggles:** Vision, Tools/Function-calling, Reasoning, JSON mode, Long-context, Cheapest, Fastest — filter the list live (data from the ported catalog capabilities).
172 +- **Each row shows:** model name, provider, **capability badges** (👁 vision / 🛠 tools / 🧠 reasoning), **context window**, max output, and **price per 1M tokens** (in/out) from the catalog; a subtle "not configured" state if the provider has no key yet (with a quick "Add key" affordance), and a "needs proxy" flag for CORS-blocked providers.
173 +- **Set as default**, **use for this conversation**, or **use for this one message** (regenerate with a different model right from the menu).
174 +- **Compare picker:** a "Compare…" action to select 2–4 models for side-by-side compare mode.
175 +- **Aliases:** user-defined friendly aliases (e.g., `fast`, `smart`) mapping to a chosen model, surfaced at the top.
176 +- Beautiful empty/loading/error states; virtualized list so 100+ models stay instant.
177 +- **Transcript:** user bubbles right (accent-subtle), assistant left on surface; full **Markdown** with tables, blockquotes, and **syntax-highlighted code blocks with copy**; collapsible **reasoning/"thinking"** section for reasoning models; **Perplexity citations** as numbered chips; per-message token/cost (from `usage` + catalog pricing) on hover; message actions (copy, edit & resend, regenerate — optionally with another model, delete).
178 +- **Input bar:** floating card, multiline auto-grow, **image attach/paste** for vision-capable models (base64), attachment thumbnails, params quick-toggle, circular accent **send** (⌘/Ctrl+Enter), **Stop** during streaming.
179 +- **Settings (modal or route):** tabs — **Providers & Keys** (per-provider masked field, Test button + status dot, proxy base URL field for ⚠️/❌ providers, the honesty notice about localStorage), **Models** (full catalog, capability badges, pricing, favorites, default model), **Appearance** (light/dark/system + accent choices + chat font size), **Data** (export/import, clear data), **Advanced** (passphrase lock toggle, streaming toggle, default params).
180 +- **First-run:** a clean, honest welcome — "Your keys and history stay in this browser (localStorage). Add a provider key to begin." + quick model pick. Beautiful, not scary, but transparent.
181 +- **Empty chat state:** centered Zyquo mark, greeting, a few suggested prompts, model chip.
182 +
183 +### 4.3 — Responsive & motion
184 +- **Responsive:** sidebar collapses to a drawer on phones; transcript and input bar adapt full-width; ≥44pt touch targets; code blocks scroll internally; no horizontal overflow; works portrait/landscape. Mobile is a real target.
185 +- **Motion:** smooth streaming (no jitter, blinking caret), 150ms message-in, 80ms hovers, snappy popovers, 60fps; respect `prefers-reduced-motion`.
186 +
187 +### 4.4 — Quality gate
188 +Review every state at phone + desktop widths in light (default) and dark: consistent tokens, no overflow, working drawer nav, streaming, stop, error (bad key → clear message, CORS-blocked → explain proxy/Router), empty/loading, long conversation performance (virtualize the transcript if needed), export/import, passphrase lock/unlock. Confirm **light default**. If it looks templated or breaks on a phone, iterate.
189 +
190 +---
191 +
192 +## PHASE 5 — BRAND ASSETS
193 +
194 +- Favicon set + `manifest.webmanifest` (installable PWA) using the Zyquo mark (chunky charcoal-black Z with electric-blue base) and the Cloud accent; apple-touch-icon; `theme-color` light.
195 +- App wordmark as crisp inline SVG (light/dark); reuse the Zyquo Cloud icon for the PWA/install icon so it matches the native app in the Dock/home screen.
196 +- OG/social meta for the hosted URL (static image, on-brand).
197 +
198 +---
199 +
200 +## PHASE 6 — Features (Parity with native Zyquo Cloud, browser-native)
201 +
202 +### Cool chat features (make this genuinely delightful — go beyond a basic chat box)
203 +- **Rich Model Menu** (Phase 4.2.1): searchable, capability-filtered, priced, favorites/recents, per-message model override, aliases.
204 +- **Branching / regenerate variants:** regenerate a response (same or different model) and keep **multiple variants** you can swipe/tab between; branch a conversation from any message to explore alternatives without losing the original thread.
205 +- **Edit & fork:** edit any earlier user message and re-run from that point (forking the thread); edit assistant messages for note-taking.
206 +- **Streaming niceties:** token-by-token with a blinking caret, live **tokens/sec** and elapsed timer, **Stop** and **Continue** (ask the model to keep going), and **scroll-lock with a "jump to latest" pill**.
207 +- **Multi-model compare mode:** send one prompt to 2–4 models side-by-side in columns, each streaming independently, with per-column copy/regenerate and a quick "promote this answer into the thread" action.
208 +- **Prompt Library & slash commands:** ship ≥40 quality templates; type **`/`** in the input for a slash-command menu (templates, personas, tools like `/summarize`, `/translate`, `/rewrite`, `/explain`); templates support `{{variables}}` with a quick fill form.
209 +- **Personas:** named assistants = system prompt + preferred model + params; switch persona per conversation; ship a few great defaults (Coder, Writer, Analyst, Tutor…).
210 +- **Attachments:** drag/drop or paste **images** (vision models) with thumbnails; attach **text/code/CSV/JSON/MD files** injected as context; paste large text as a collapsible block.
211 +- **Rich rendering:** GitHub-flavored **Markdown**, tables, task lists, footnotes; **syntax-highlighted code** with language label, copy button, and per-block "copy"/"wrap" toggles; **math via KaTeX**; **Mermaid diagram** rendering; collapsible **reasoning/"thinking"** panels; **Perplexity citations** as numbered, clickable chips.
212 +- **Message actions:** copy (as text or Markdown), quote-reply, pin/star a message, add a note, delete, and **read-aloud** (Web Speech API TTS) with voice/speed controls; optional **speech-to-text input** (Web Speech API) for dictation.
213 +- **Conversation tools:** auto-generated titles, **summarize this conversation**, **conversation-level search** and **in-conversation find**, tags/folders, pin, archive, duplicate, and **export a single conversation** to Markdown / JSON / (client-side) PDF; shareable **export link is NOT server-based** — export is a file since there's no backend.
214 +- **Token & cost HUD:** live per-message and per-conversation token counts and **estimated cost** (from catalog pricing), plus a context-window usage bar showing how full the context is, with **auto-trim oldest turns** (keeping system prompt) when near the limit.
215 +- **Parameter controls:** per-conversation temperature, top_p, max tokens, penalties, seed, JSON mode/`response_format`, and **reasoning effort** where supported — the panel only shows params the active model/provider actually supports.
216 +- **Quality-of-life:** command palette (⌘/Ctrl+K) for everything (new chat, switch model, search, jump to conversation, run template); full keyboard shortcuts; **undo** for deletes (soft-delete + toast); autosave everywhere; offline-friendly UI shell (PWA); **retry with backoff** on transient provider errors; graceful, human error toasts (bad key names the provider, CORS-blocked explains the proxy/Router option, rate-limit shows retry).
217 +- **Theming extras:** light default + dark toggle + a few accent choices; adjustable chat font size and message density (comfortable/compact); optional "focus mode" hiding the sidebar.
218 +
219 +### Core parity
220 +- Multi-conversation local history (search, pin, rename, delete, groups) — all in IndexedDB/localStorage
221 +- Streaming + non-streaming chat across **all 12 providers / all Cloud models** (browser fetch), model switch per conversation and per message
222 +- Vision (image attach/paste) for capable models; text-file attach injected into messages
223 +- System prompt per conversation + global default; per-conversation params (only those the provider supports)
224 +- Keys + history in `localStorage`/IndexedDB with optional passphrase lock; full export/import; clear-data controls; per-provider proxy override for CORS-blocked providers (point at a local Zyquo Router)
225 +- PWA installable; **light theme default**; fully responsive (usable on a phone browser)
226 +
227 +---
228 +
229 +## PHASE 7 — VERIFICATION (MANDATORY)
230 +
231 +The user will provide **real API keys** (same providers as Zyquo Cloud). You MUST:
232 +
233 +1. **Provider matrix (browser-side):** for **every provider/model** in the catalog, from an actual browser context, verify: non-streaming completion; **streaming** renders token-by-token; cancellation via Stop works; vision on vision-capable models; reasoning content on reasoning models; Perplexity citations. Record ✅/❌ per provider/model and the **CORS verdict** encountered (direct vs. needs-proxy). Fix everything fixable; for CORS-blocked providers, verify the **proxy/Zyquo Router fallback** path works and is clearly surfaced in the UI.
234 +2. **Persistence:** keys and full history survive reload and browser restart; export→clear→import restores exactly; schema migration works; large-history performance is smooth (virtualization if needed).
235 +3. **Security/privacy sanity:** confirm no key or prompt content is sent anywhere except the chosen provider (or user-set proxy) — inspect network traffic; confirm the CSP blocks unexpected origins; confirm the first-run localStorage notice is present; passphrase lock encrypts/decrypts correctly.
236 +4. **Responsive/cross-browser:** verify on Chrome, Safari, Firefox (desktop) and iOS Safari / Android Chrome (phone): layout, streaming, drawer nav, no overflow; **light theme loads by default**.
237 +5. Never commit or log any key; keys live only in the browser's localStorage during use.
238 +
239 +---
240 +
241 +## PHASE 8 — BUILD & DEPLOYMENT
242 +
243 +No notarization (it's a web app). Phase 8 is a proper **production build + static deployment**:
244 +1. `npm run build` → optimized static bundle (code-split, hashed assets, tree-shaken, minimal JS); zero type/lint errors; PWA assets generated; CSP finalized to the exact set of provider origins (+ a placeholder for the user's proxy origin, handled at runtime).
245 +2. **Deploy as static assets** at **`www.zyquo.cloud`** (HTTPS, apex `zyquo.cloud``www` redirect). If served under the Zyquo platform, use the same **Node server on `m2u64` exposed through ngrok** as the main site (bind the `www.zyquo.cloud` custom domain to the tunnel + DNS CNAME), OR any static host (Cloudflare Pages/Vercel/Netlify) as a documented alternative. Set security headers (CSP, HSTS, X-Content-Type-Options, Referrer-Policy) and long-cache for hashed assets.
246 +3. Because it's 100% client-side, it also runs from `file://`-style local hosting or a simple static server — document how a user can self-host it, which pairs naturally with running **Zyquo Router** locally for CORS-blocked providers.
247 +4. Post-deploy: verify HTTPS, PWA install, all provider calls from the deployed origin (CSP must allow them), light-default, and mobile.
248 +
249 +---
250 +
251 +## Engineering Standards
252 +
253 +- TypeScript strict; ESLint + Prettier clean; zero build warnings; minimal, audited dependencies (each is key-theft surface)
254 +- All `localStorage`/IndexedDB access behind `storage/`; all networking behind `providers/`; components stay presentational
255 +- Provider layer faithfully ported from native Zyquo Cloud (all models); SSE via `fetch`+`ReadableStream`+`AbortController`; robust human-readable errors (invalid key names the provider; **CORS-blocked explains the proxy/Router fallback**; rate-limit shows retry)
256 +- Design tokens only — no hardcoded colors/sizes; **light theme default**; responsive/mobile-first friendly
257 +- Strict CSP; never transmit keys/history anywhere but the chosen provider (or user proxy); transparent first-run notice about localStorage
258 +- `README.md` (dev, self-host, "use with Zyquo Router") + `docs/` (PROVIDER-REUSE, CORS-MATRIX, PLAN)
259 +- Commit in logical, phase-prefixed increments; never commit keys/secrets
260 +
261 +## Definition of Done
262 +
263 +- A React web app that reproduces Zyquo Cloud's chat experience in the browser with **no auth, no accounts, no backend**
264 +- API **keys stored in `localStorage`** and **conversation history stored locally** (IndexedDB/localStorage), surviving reloads/restarts, with export/import and clear-data — plus an optional passphrase lock and an honest first-run notice about local storage
265 +- Streaming + non-streaming chat across **all 12 providers / all models from native Zyquo Cloud**, called **directly from the browser**, with per-provider **CORS reality documented** and a **proxy / local Zyquo Router fallback** for blocked providers, verified with real keys (Phase 7 matrix)
266 +- Full chat features: a rich searchable/priced **Model Menu**, per-message model override, **branching/regenerate variants**, edit & fork, **multi-model compare**, **slash commands** + prompt library, personas, Markdown + code + **KaTeX + Mermaid**, reasoning display, citations, vision, **read-aloud / dictation**, token/cost HUD + context bar with auto-trim, and a ⌘K command palette
267 +- Deployed at **`www.zyquo.cloud`** (HTTPS, apex→www); **light theme loads by default**; dark is a secondary toggle; the app is **responsive and usable on phones**; installable PWA
268 +- Strict CSP; keys/history never leave the device except as user-initiated provider requests
269 +- The Zyquo mark/wordmark and Cloud accent applied consistently as crisp SVG
270 +- **Every code file starts with the mandatory Author/Mail header** (verified by a repo-wide sweep)
271 +- `docs/PLAN.md` shows every phase completed; PROVIDER-REUSE and CORS-MATRIX complete and traceable
272 +- Zyquo Cloud Web feels like the legendary, private, local-only browser edition of Zyquo Cloud
added docs/CORS-MATRIX.md +95 −0
@@ -0,0 +1,95 @@
1 +<!--
2 + CORS-MATRIX.md
3 + Zyquo Cloud Web
4 +
5 + Author: Simon-Pierre Boucher
6 + Mail: contact@spboucher.ai
7 +-->
8 +
9 +# CORS Matrix — Browser Feasibility per Provider (Phase 0.A)
10 +
11 +**Method.** Every verdict below is **empirical**, not guessed: on 2026-07-31 each
12 +provider's chat endpoint was probed with (1) a browser-identical `OPTIONS`
13 +preflight (`Origin: https://www.zyquo.cloud`, `Access-Control-Request-Method:
14 +POST`, `Access-Control-Request-Headers: authorization,content-type` — or the
15 +Anthropic header set), and (2) a **real authenticated POST** carrying the same
16 +`Origin`, inspecting the `Access-Control-Allow-*` response headers. A streaming
17 +(`stream: true`) request was additionally verified to return
18 +`content-type: text/event-stream` **with** `access-control-allow-origin` intact.
19 +Probe script: `scripts/cors-probe.sh` (keys read from gitignored
20 +`.keys.local.json`; never committed).
21 +
22 +## Verdict table
23 +
24 +| # | Provider | Endpoint | Preflight | POST ACAO | Verdict |
25 +|---|----------|----------|-----------|-----------|---------|
26 +| 1 | OpenAI | `https://api.openai.com/v1/chat/completions` | 200, allows `authorization,content-type`, max-age 86400 | `*` (echoes origin on preflight) | ✅ direct |
27 +| 2 | Anthropic | `https://api.anthropic.com/v1/messages` | 200, explicitly allows `x-api-key, anthropic-version, anthropic-dangerous-direct-browser-access` | `*`**only when** `anthropic-dangerous-direct-browser-access: true` is sent; without it the API returns 401 `"CORS requests must set 'anthropic-dangerous-direct-browser-access' header"` | ⚠️ direct **with header** |
28 +| 3 | xAI | `https://api.x.ai/v1/chat/completions` | 200, `allow-methods: *`, `allow-headers: *` | `*` | ✅ direct |
29 +| 4 | Mistral | `https://api.mistral.ai/v1/chat/completions` | 200, allows `Authorization, Content-Type, …` | `*` | ✅ direct |
30 +| 5 | Google Gemini (OpenAI-compat) | `https://generativelanguage.googleapis.com/v1beta/openai/chat/completions` | 200, echoes origin, max-age 3600 | echoes origin | ✅ direct |
31 +| 6 | Alibaba Qwen (DashScope intl) | `https://dashscope-intl.aliyuncs.com/compatible-mode/v1/chat/completions` | 200, echoes origin, credentials true, max-age 86400 | echoes origin | ✅ direct |
32 +| 7 | DeepSeek | `https://api.deepseek.com/chat/completions` | 200, `allow-methods: POST`, echoes origin | echoes origin | ✅ direct |
33 +| 8 | Kimi (Moonshot) | `https://api.moonshot.ai/v1/chat/completions` | 204, echoes origin, credentials true | echoes origin | ✅ direct |
34 +| 9 | Perplexity | `https://api.perplexity.ai/chat/completions` | 204, `*`, generous header allowlist, max-age 1728000 | `*` | ✅ direct |
35 +| 10 | Together AI | `https://api.together.xyz/v1/chat/completions` | 200, `*` | `*` | ✅ direct |
36 +| 11 | DeepInfra | `https://api.deepinfra.com/v1/openai/chat/completions` | 200, `*` | `*` | ✅ direct |
37 +| 12 | Cerebras | `https://api.cerebras.ai/v1/chat/completions` | 200, `*`, max-age 600 | `*` | ✅ direct |
38 +
39 +**Bottom line: all 12 providers are callable directly from the browser today.**
40 +11 are plain ✅; Anthropic is ⚠️ only in the sense that the client must add
41 +`anthropic-dangerous-direct-browser-access: true` on every request (the header
42 +name is Anthropic's deliberate reminder that browser-resident keys are exposed —
43 +which matches this app's transparent bring-your-own-key model).
44 +
45 +## Probe caveats observed (not CORS failures)
46 +
47 +- **Perplexity** rejects `max_tokens < 16` (`400 max_tokens must be at least 16`) — keep test calls ≥ 16.
48 +- **Together** now requires serverless-hosted model IDs (e.g. `meta-llama/Llama-3.3-70B-Instruct-Turbo`); older turbo IDs 400 with a "dedicated endpoint" message. CORS headers present on the error, so the verdict stands.
49 +- **Kimi/Cerebras** model IDs rotate quickly (`kimi-k3`, `kimi-k2.6`… / `gpt-oss-120b`, `gemma-4-31b`, `zai-glm-4.7`); 404s were model-name-only, CORS intact.
50 +- **DeepSeek** serves `deepseek-chat` as `deepseek-v4-flash` (alias resolution).
51 +- **xAI** resolves legacy aliases (probe with `grok-3-mini` answered as `grok-4.3`).
52 +- Several providers echo the exact origin instead of `*` (Gemini, DashScope, DeepSeek, Kimi) — irrelevant to the app, but means responses vary by `Origin` and are not cacheable cross-origin.
53 +
54 +## Documented stance (official docs, web-researched + verified 2026-07-31)
55 +
56 +- **Anthropic — the only provider with officially documented CORS support.** Added Aug 22 2024 (Claude Platform release notes, still live): CORS headers are returned when the request carries `anthropic-dangerous-direct-browser-access: true` (what the SDKs send for `dangerouslyAllowBrowser: true`). The deliberately scary name is Anthropic's warning about browser-resident keys — Anthropic frames it as acceptable for internal tools and **bring-your-own-key apps**, which is exactly this app. Sources: [release notes](https://platform.claude.com/docs/en/release-notes/api), [anthropic-sdk-typescript](https://github.com/anthropics/anthropic-sdk-typescript), [Simon Willison's explainer](https://simonwillison.net/2024/Aug/23/anthropic-dangerous-direct-browser-access/).
57 +- **OpenAI** — docs are silent on CORS; permissive ACAO is undocumented behavior. Official guidance is the strongest anti-browser language of the twelve ("Never deploy your key in client-side environments"); the Node SDK gates browsers behind `dangerouslyAllowBrowser: true`. ([key safety](https://help.openai.com/en/articles/5112595-best-practices-for-api-key-safety), [openai-node](https://github.com/openai/openai-node))
58 +- **Google Gemini** — API-key doc explicitly acknowledges client-side use and its extraction risk; documented client-side alternative (ephemeral tokens) applies only to the Live API, not the OpenAI-compat chat endpoint used here. ([API keys](https://ai.google.dev/gemini-api/docs/api-key))
59 +- **Alibaba Qwen/DashScope** — browser use officially anticipated via short-lived **temporary API keys** minted by a backend (60 s validity); permanent keys detected as publicly exposed may be auto-disabled. The app uses the **international** endpoint (`dashscope-intl.aliyuncs.com`), same as native Zyquo Cloud. ([temporary API keys](https://www.alibabacloud.com/help/en/model-studio/generate-temporary-api-key))
60 +- **xAI** — docs silent on CORS for chat; ephemeral client tokens exist only for the realtime/audio API. ([docs.x.ai](https://docs.x.ai/developers/quickstart))
61 +- **Together AI / Cerebras** — docs silent on CORS, but both official TS/Node SDKs list **web browsers as supported runtimes** with no browser gate. ([together-typescript](https://github.com/togethercomputer/together-typescript), [cerebras-cloud-sdk-node](https://github.com/Cerebras/cerebras-cloud-sdk-node))
62 +- **Mistral / DeepSeek / Kimi (Moonshot) / Perplexity / DeepInfra** — docs silent on CORS either way; Perplexity and Moonshot carry generic "never expose keys client-side" guidance. ([Mistral](https://docs.mistral.ai/api), [DeepSeek](https://api-docs.deepseek.com/), [Kimi](https://platform.kimi.ai/docs/api/overview), [Perplexity](https://docs.perplexity.ai/guides/api-key-management), [DeepInfra](https://docs.deepinfra.com/quickstart))
63 +
64 +**Implication:** outside Anthropic, permissive CORS is undocumented behavior that
65 +any provider could withdraw without notice — which is why the per-provider proxy
66 +override ships even though everything works today. The universal "don't ship
67 +your key in client code" warnings target developers embedding *their own* key;
68 +Zyquo Cloud Web's model is the opposite: each user brings **their own** key,
69 +which never leaves their device except toward the provider itself.
70 +
71 +## Fallback design (proxy / Zyquo Router) — still shipped
72 +
73 +Even though everything works today, CORS policies are provider-controlled and
74 +can change without notice. The app therefore ships a **per-provider base-URL
75 +override** (Settings → Providers & Keys → "Proxy / base URL"):
76 +
77 +- Any provider's base URL can be overridden with a user-supplied CORS proxy or
78 + a locally running **Zyquo Router** (the family's local gateway, OpenAI-compatible).
79 +- When a request fails with a network/CORS-shaped error (fetch `TypeError` with
80 + no HTTP status), the error toast explains: *"This provider may be blocking
81 + browser calls — set a proxy URL in Settings or run Zyquo Router locally."*
82 +- The override is stored in `zyquo.cloud.web.settings`; the CSP includes a
83 + runtime-managed `connect-src` strategy (see Phase 8) so a user-set proxy origin
84 + remains reachable.
85 +
86 +## Streaming over fetch
87 +
88 +- Verified: streaming responses (`text/event-stream`) carry the same
89 + `access-control-allow-origin` headers (OpenAI probe above), so
90 + `fetch` + `ReadableStream` SSE parsing works browser-side.
91 +- All 11 OpenAI-compatible providers use `data:` SSE lines with a `[DONE]`
92 + terminator; Anthropic uses named `event:` blocks. One parser
93 + (`providers/sse.ts`, ported from the native `SSEParser`) handles both.
94 +- Cancellation: `AbortController.abort()` on the fetch tears down the stream —
95 + used by Stop and new-message preemption.
added docs/NATIVE-UX.md +191 −0
@@ -0,0 +1,191 @@
1 +<!--
2 + NATIVE-UX.md
3 + Zyquo Cloud Web
4 +
5 + Author: Simon-Pierre Boucher
6 + Mail: contact@spboucher.ai
7 +-->
8 +
9 +# Native Zyquo Cloud UX Reference (Phase 0.B study — feeds Phases 4 & 6)
10 +
11 +Detailed study of the native macOS app's views (2026-07-31), used as the
12 +contract for the web UI. Web-specific departures are marked ⤷web.
13 +
14 +## Layout
15 +
16 +- **Root:** sidebar (260, min 260 / max 360) + detail; window default 1240×800.
17 + ⤷web: sidebar collapses to a drawer under ~768px.
18 +- **Sidebar (top→bottom):** wordmark (CloudZGlyph 22 + "Zyquo Cloud" 14/medium);
19 + search field (surfaceSecondary@0.7, radius 6, ⌘F); full-width accent **New
20 + Chat** button (radius 6, ⌘N); conversation list (LazyVStack spacing 2, groups
21 + **Pinned / Today / Yesterday / Previous 7 Days / Older**, headers caption/
22 + textTertiary); rows = title 13 + sub-line `modelID · relative time` caption,
23 + hover pin/trash, selected = accentSubtle, radius 6; context menu Pin / Add
24 + Tag / Remove Tag / Delete; tags capsule strip above footer; footer = gear
25 + (Settings) + usage caption `X.XK tok · ~$Y.YY`.
26 +- **Chat header (52px, hairline below):** model chip absolutely centered;
27 + left = editable title (13/medium, double-click rename); right = Compare,
28 + Export (⌘⇧E), Info popover (340 wide: tokens in/out, est. cost, editable
29 + system prompt 300×90).
30 +- **Transcript:** centered 760px column, 16px turn rhythm/padding; auto-scroll
31 + while pinned to bottom; floating **"Jump to latest"** pill (capsule, surface,
32 + hairline, soft shadow) when unpinned during streaming.
33 +- **Bubbles:** user right (accentSubtle fill, no border, ≥60px left spacer);
34 + assistant left with 26×26 circular provider avatar (accent glyph on
35 + accentSubtle) + surface bubble with 0.5px border; pad 12h/10v, radius 10;
36 + metadata row fades in on hover.
37 +- **Input bar:** floating card (pad 12, radius 14, surface, hairline, soft
38 + shadow, margins 12); optional 56px attachment strip; paperclip → auto-grow
39 + editor ("Message…", max 200px) → params button (popover) → **28×28 circular
40 + send** (arrow up, accent; danger + stop icon while streaming), ⌘↩. Drag
41 + target: 1.5px dashed accent border. Images png/jpg/webp/gif; text files
42 + <512 KB. Thumbnails 56×56 radius 6 with remove badge.
43 +- **Model chip:** provider glyph 11 + name 12.5/medium + chevron 8, pad 8h/4v,
44 + radius 6, surfaceSecondary, press-scale; opens picker popover.
45 +
46 +## Model picker (native popover 320×380)
47 +
48 +Search field → "Favorites" section (query empty only) → per-provider sections,
49 +providers with keys first then alphabetical. Row: glyph · name 12.5 · capability
50 +icons 9pt (eye/brain/wrench) · context badge · star toggle (warning color).
51 +Rank: favorites → recommended → normal → legacy. **No recents, no capability
52 +filters, click-only.** ⤷web (Phase 4.2.1 spec goes beyond native): ⌘K palette
53 +sizing, fuzzy search, filter chips, Recents group, pricing per row,
54 +not-configured / needs-proxy states, aliases, compare picker, keyboard nav,
55 +virtualized list.
56 +
57 +## Message rendering
58 +
59 +- Markdown blocks (spacing 12): headings ×1.55/1.35/1.2/1.05 semibold;
60 + blockquote = 3px accent left bar + textSecondary; task lists; ordered lists
61 + monospaced-digit in 20px slot; tables = zebra rows, header surfaceSecondary,
62 + hairline grid, radius 6; inline code mono size−1 on surfaceSecondary; links
63 + accent.
64 +- Code blocks: surfaceSecondary card radius 10; header = UPPERCASED language
65 + caption + hover-reveal copy (checkmark success 1.2s); body mono size−1,
66 + h-scroll. Syntax theme: keyword=accent, string=success, comment=textTertiary,
67 + number #B26A0B/#E0A458, type #2380C2/#62B7F0, function #6E4FD4/#A48CF2,
68 + property #2F6FBF/#7FB4E8, attribute=warning.
69 +- Reasoning: collapsed disclosure "Thinking…" (streaming) / "Thought process";
70 + expanded = mono size−2, textSecondary, surfaceSecondary pad 8 radius 6.
71 +- Citations: capsule chips — 13px accent circle with white index + title/host
72 + caption; opens URL.
73 +- Streaming caret: 7×15 accent rounded rect blinking 1↔0.15 @0.55s.
74 +- Actions (hover, 10pt): copy, edit-&-resend (user), regenerate (assistant),
75 + quote-reply (`> ` prefix into draft), delete. Edit mode = inline textarea +
76 + Cancel/Resend.
77 +- Error state: warning triangle + danger text on danger@8% radius 6.
78 +- Metadata: time `HH:mm`, `{in}→{out} tok`, `~$0.0000` when >0.
79 +
80 +## Settings (native 720×520, 5 tabs)
81 +
82 +1. **Providers & Keys** — per-provider row: glyph, name + status dot
83 + (green verified / grey saved / spinner testing / red failed), sub-line
84 + `Verified · 123 ms` or error or masked key; 220px secure field (mono 11),
85 + **Test** (round-trips provider's cheapest model), trash.
86 + ⤷web adds: proxy base-URL field, localStorage honesty notice.
87 +2. **Models** — provider picker, **Refresh from API** (`N live · M not in
88 + catalog`), Add Custom (sheet: id, name, base URL, context, vision toggle);
89 + table: star, name + Featured/Legacy badges, mono id, capability badges,
90 + context, `$in / $out`.
91 +3. **Appearance** — theme System/Light/Dark; accents: Sky Indigo #4E6AF0/#6D84F5
92 + (subtle #EBEFFD/#28304C), Graphite #5B6270/#8A93A6 (#EEF0F4/#2A2E3A), Teal
93 + #1D9A8F/#3BB5AA (#E6F6F4/#1F3A38), Amber #C77D1D/#DD9B45 (#FBF1E3/#3D3222),
94 + Rose #C94F7C/#DE7099 (#FAEAF1/#3D2733); chat font slider 12–18 step 0.5 with
95 + live preview card. ⤷web default = **light** (never dark-auto).
96 +4. **Shortcuts** — ⌘N new, ⌘K palette, ⌘F search, ⌘↩ send, ⌘⇧E export,
97 + ⌘, settings, ⌘. stop.
98 +5. **Advanced** — default system prompt (90px editor); data location note.
99 + ⤷web: Data tab (export/import/clear), passphrase lock, streaming toggle.
100 +
101 +## Command palette (⌘K, sheet 560, results 380)
102 +
103 +Sections: **Models** (max 8; empty query → recommended; switches current
104 +conversation's model) → **Personas** (max 6; applies system prompt + params +
105 +optional model) → **Templates** (max 10; inserts with `{{input}}` ← current
106 +draft). Row: accent symbol 13 + title 13 / subtitle caption. Native is
107 +click-only — ⤷web adds arrow/Enter keyboard nav.
108 +
109 +## Compare (sheet ≥900×560)
110 +
111 +2–4 equal columns, hairline separators; per-column: model chip, output-token
112 +caption, copy, regenerate, remove (>2 only); body = reasoning preview (mono
113 +size−2.5, max 6 lines) + markdown at size−0.5 + caret; footer prompt input
114 +broadcasts to all columns, each streaming independently; no system prompt,
115 +default params, not saved. ⤷web adds "promote answer into thread".
116 +
117 +## Empty state
118 +
119 +Centered slightly above middle: CloudZGlyph 88 → "Welcome to Zyquo Cloud"
120 +20/semibold → "Your keys, every cloud model, one beautiful chat."
121 +(textSecondary) → model chip → 2×2 grid (max 460) of suggestion cards
122 +(surface, radius 10, hairline, pad 12): Explain something / Review my code /
123 +Summarize a document / Translate. Native has **no first-run flow**
124 +⤷web adds the honest localStorage welcome + quick key setup.
125 +
126 +## Personas (8 built-ins)
127 +
128 +Senior Code Reviewer, Technical Writer, Product Strategist, Research Assistant,
129 +Socratic Tutor, UX Critic, Data Analyst, Ruthless Editor — dense ~120-word
130 +system prompts, no fixed model (use default), default params. Ported verbatim
131 +in Phase 6 (`features/personas.ts` data).
132 +
133 +## Prompt library (56 templates, 8 categories × 7)
134 +
135 +Writing / Coding / Analysis / Translation & Language / Business / Learning /
136 +Creativity / Productivity; each body contains `{{input}}` exactly once
137 +(missing → input appended). Ported verbatim in Phase 6. Built-ins immutable;
138 +user templates appended after.
139 +
140 +## ConversationStore behaviors to port
141 +
142 +- Conversations newest-first; search matches title or any message text;
143 + 5 sidebar groups.
144 +- Send: append user msg → placeholder assistant msg → build request from
145 + system prompt + messages (excluding placeholder and errored messages) →
146 + stream deltas; usage + estimatedCost applied at end. Per-message
147 + `overrideModel` also switches the conversation model.
148 +- Regenerate drops the trailing assistant turn (⤷web: keep as variant instead —
149 + native has **no branching/variants**; the web app adds `variants` per
150 + assistant message and branch-from-message).
151 +- Edit & resend rewrites the user message and truncates after it.
152 +- **Auto-title:** after first completed exchange (exactly 1 assistant msg,
153 + `hasAutoTitle` true): cheapest non-legacy non-reasoning model for the
154 + conversation's provider, non-streaming, maxTokens 24, prompt
155 + "Write a title of at most 5 words for this conversation. Reply with the
156 + title only, no quotes." + first user text (500 chars) + last assistant text
157 + (500 chars); trim quotes/whitespace, cap 60 chars; silent failure; manual
158 + rename disables auto-titling.
159 +- Params per conversation: temperature, topP, maxTokens, frequencyPenalty,
160 + presencePenalty, reasoningEffort, thinkingEnabled — **nil = provider default,
161 + omitted from request**; editor only shows rows the model supports, each
162 + slider gated by an enable checkbox.
163 +- Persistence: autosave on every mutation (streaming persisted at stream end,
164 + not per-token). ⤷web: IndexedDB, same document shape.
165 +
166 +## Brand mark (CloudZGlyph → SVG)
167 +
168 +Single-tint glyph, 1024 grid, tight box x 267–770 / y 257–766:
169 +
170 +```svg
171 +<svg viewBox="0 0 503 509" xmlns="http://www.w3.org/2000/svg">
172 + <g transform="translate(-267,-257)" fill="none" stroke="currentColor">
173 + <path d="M320 310H704L320 700H556" stroke-width="106"
174 + stroke-linecap="round" stroke-linejoin="round"/>
175 + <rect x="653" y="585" width="104" height="168" rx="24"
176 + fill="currentColor" stroke="none"/>
177 + </g>
178 +</svg>
179 +```
180 +
181 +Full app icon (`~/Desktop/zyquo-cloud/assets/icon/zyquo-cloud.svg`): Big Sur
182 +squircle (inset 100, 824 wide) with vertical gradient #6FA8FF → #4E6AF0@52% →
183 +#3A3F9E, top gloss, caret glow radial #BFD5FF @ (705,655,r200); Z drawn twice —
184 +plum #2A3490 contour at width 132 under near-white face gradient (#FFFFFF →
185 +#E9EFFC) at width 106; caret likewise (640,572,130×194,r34 plum under
186 +653,585,104×168,r24 face). Reused for the PWA icon in Phase 5.
187 +
188 +## Extras not ported
189 +
190 +Quick Chat (⌥Space Spotlight panel) and the menu-bar extra are macOS-only;
191 +their role is covered by the web command palette.
added docs/PLAN.md +55 −0
@@ -0,0 +1,55 @@
1 +<!--
2 + PLAN.md
3 + Zyquo Cloud Web
4 +
5 + Author: Simon-Pierre Boucher
6 + Mail: contact@spboucher.ai
7 +-->
8 +
9 +# Zyquo Cloud Web — Build Plan & Phase Log
10 +
11 +Strict phase order 0 → 8. Each phase gets a checklist here, checked off as work
12 +lands, plus a 3–5 line summary at its checkpoint.
13 +
14 +---
15 +
16 +## Phase 0 — Research + Zyquo Cloud study
17 +
18 +- [x] Locate native Zyquo Cloud repo (`~/Desktop/zyquo-cloud`)
19 +- [x] Study provider layer (ProviderID, ProviderRegistry, OpenAICompatibleClient, AnthropicClient, StreamingService/SSEParser, ProviderProtocol)
20 +- [x] Study AIModel / capabilities / pricing / ParameterSupport model
21 +- [x] Study ZyquoTheme design tokens
22 +- [x] Extract complete model catalog (170 models, 12 providers — port from ModelCatalogData.swift, NOT the drifted catalog-summary.md)
23 +- [x] Study native UX (views, model picker, personas, prompt library, brand glyph) → `docs/NATIVE-UX.md`
24 +- [x] Empirical CORS probes, all 12 providers (preflight + real POST with Origin)
25 +- [x] Anthropic browser header confirmed (`anthropic-dangerous-direct-browser-access: true`)
26 +- [x] Web research: documented CORS stance per provider (cited in CORS-MATRIX)
27 +- [x] Write `docs/CORS-MATRIX.md`
28 +- [x] Write `docs/PROVIDER-REUSE.md`
29 +
30 +**Phase 0 summary.** Native repo studied end-to-end: only two wire formats
31 +(OpenAI chat/completions ×11 incl. Gemini-compat; Anthropic Messages ×1), one
32 +SSE parser, 170-model catalog, ZyquoTheme tokens extracted. Empirical CORS
33 +probes with real keys: **all 12 providers callable directly from the browser**
34 +(Anthropic needs `anthropic-dangerous-direct-browser-access: true`); documented
35 +stances researched and cited. Proxy/Zyquo Router override still ships since
36 +only Anthropic documents CORS officially. Gate satisfied: PROVIDER-REUSE.md +
37 +CORS-MATRIX.md complete.
38 +
39 +---
40 +
41 +## Phase 1 — Project setup (React + TS + Vite) — pending
42 +
43 +## Phase 2 — Architecture + browser provider layer — pending
44 +
45 +## Phase 3 — Local persistence — pending
46 +
47 +## Phase 4 — Design system & UI — pending
48 +
49 +## Phase 5 — Brand assets — pending
50 +
51 +## Phase 6 — Features — pending
52 +
53 +## Phase 7 — Verification — pending
54 +
55 +## Phase 8 — Build & deployment — pending
added docs/PROVIDER-REUSE.md +235 −0
@@ -0,0 +1,235 @@
1 +<!--
2 + PROVIDER-REUSE.md
3 + Zyquo Cloud Web
4 +
5 + Author: Simon-Pierre Boucher
6 + Mail: contact@spboucher.ai
7 +-->
8 +
9 +# Provider Reuse — Porting the Native Zyquo Cloud Provider Layer (Phase 0.B)
10 +
11 +Source of truth: the native macOS repo at `~/Desktop/zyquo-cloud` (studied
12 +2026-07-31: `ProviderID.swift`, `ProviderRegistry.swift`,
13 +`OpenAICompatibleClient.swift`, `AnthropicClient.swift`, `StreamingService.swift`,
14 +`ProviderProtocol.swift`, `AIModel.swift`, `ModelCatalog.swift`,
15 +`ModelCatalogData.swift`, `ZyquoTheme.swift`, Views/). The web app re-implements
16 +this layer 1:1 on browser `fetch` + `ReadableStream` + `AbortController`; it
17 +never re-invents request formats.
18 +
19 +## 1. Architecture to reproduce
20 +
21 +Native shape → web shape:
22 +
23 +| Native (Swift) | Web (TS) |
24 +|---|---|
25 +| `ProviderID` enum (12 + custom) | `Provider` union type + metadata table (`providers/registry.ts`) |
26 +| `WireFormat` (`openAIChatCompletions` \| `anthropicMessages`) | same two-value union |
27 +| `ProviderRegistry.client(for:)` | `getClient(model)` — Anthropic → `anthropic.ts`, everything else → `openaiCompatible.ts` |
28 +| `OpenAICompatibleClient` (one client, 11 providers + custom) | `providers/openaiCompatible.ts` |
29 +| `AnthropicClient` (native Messages API) | `providers/anthropic.ts` |
30 +| `StreamingService` + `SSEParser` | `providers/sse.ts` (fetch + ReadableStream line splitter + SSE state machine) |
31 +| `ChatRequest` / `ChatEvent` / `ProviderClient` protocol | `providers/types.ts` (`ChatRequest`, `ChatEvent`, `ProviderClient` interface: `streamChat`, `complete`, `listModelIDs`, `testKey`) |
32 +| `ModelCatalogData.all` (170 models) | `providers/catalog.ts` (typed, complete port) |
33 +| `ProviderError` (typed, human messages) | `ProviderError` class with the same cases + messages |
34 +
35 +**Key finding:** only TWO wire formats exist. 11 of 12 providers (OpenAI, xAI,
36 +Mistral, **Gemini via its OpenAI-compat endpoint**, Qwen/DashScope, DeepSeek,
37 +Kimi, Perplexity, Together, DeepInfra, Cerebras) speak OpenAI
38 +`/chat/completions`; Anthropic alone speaks `/v1/messages`. The native app has
39 +no separate Gemini `generateContent` client — Gemini rides the compat endpoint
40 +with `Authorization: Bearer <API key>`, and the web app does the same.
41 +
42 +## 2. Base URLs & auth (exact, from `ProviderID.defaultBaseURL`)
43 +
44 +| Provider | Base URL | Auth |
45 +|---|---|---|
46 +| openai | `https://api.openai.com/v1` | `Authorization: Bearer` |
47 +| anthropic | `https://api.anthropic.com/v1` | `x-api-key` + `anthropic-version: 2023-06-01` + **(web only)** `anthropic-dangerous-direct-browser-access: true` |
48 +| xai | `https://api.x.ai/v1` | Bearer |
49 +| mistral | `https://api.mistral.ai/v1` | Bearer |
50 +| gemini | `https://generativelanguage.googleapis.com/v1beta/openai` | Bearer (the API key) |
51 +| qwen | `https://dashscope-intl.aliyuncs.com/compatible-mode/v1` | Bearer |
52 +| deepseek | `https://api.deepseek.com` | Bearer |
53 +| kimi | `https://api.moonshot.ai/v1` | Bearer |
54 +| perplexity | `https://api.perplexity.ai` | Bearer |
55 +| together | `https://api.together.xyz/v1` | Bearer |
56 +| deepinfra | `https://api.deepinfra.com/v1/openai` | Bearer |
57 +| cerebras | `https://api.cerebras.ai/v1` | Bearer |
58 +
59 +Paths appended to the base: `chat/completions` and `models` (OpenAI-compat);
60 +`messages` and `models?limit=100` (Anthropic). Base path components must be
61 +preserved when joining (`…/compatible-mode/v1`, `…/v1beta/openai`).
62 +Per-provider base-URL override (proxy / Zyquo Router) plugs in exactly where
63 +`baseURLOverride` does natively.
64 +
65 +## 3. OpenAI-compatible client — request construction rules
66 +
67 +Body fields (all optional unless noted): `model`, `messages`, `stream`,
68 +`stream_options.include_usage`, `temperature`, `top_p`, `max_tokens` **or**
69 +`max_completion_tokens`, `frequency_penalty`, `presence_penalty`,
70 +`reasoning_effort`, `enable_thinking`. Rules ported verbatim:
71 +
72 +1. **System prompt** becomes the first message with `role: "system"`; stored
73 + messages with role `system` are skipped when building the array.
74 +2. **Parameter gating:** a param is included only if the model's
75 + `ParameterSupport` allows it (providers reject unknown params).
76 + `usesMaxCompletionTokens` → send `max_completion_tokens` instead of
77 + `max_tokens` (OpenAI reasoning models, Kimi K-series, Cerebras).
78 +3. **`stream_options: {include_usage: true}`** only for: openai, xai, gemini,
79 + deepseek, kimi, together, cerebras, custom. NOT for mistral (rejects unknown
80 + params), qwen/deepinfra/perplexity (usage included automatically).
81 +4. **Mistral `reasoning_effort` mapping:** only accepts `"high"`/`"none"`
82 + map `medium→high`, `low→none`.
83 +5. **Qwen/DashScope `enable_thinking`:** only legal on **streaming** requests;
84 + omit on non-streaming.
85 +6. **Vision:** user messages with images become
86 + `content: [{type:"text",text}, {type:"image_url", image_url:{url:"data:<mime>;base64,<data>"}}]`;
87 + otherwise `content` is a plain string. Images only on user messages of
88 + vision-capable models.
89 +7. **Text-file attachments** are injected inline into the message text, fenced:
90 + ` ```<fileName>\n<contents>\n``` `.
91 +8. **`requiresStreaming` models** (Qwen qwq/qvq, several Together-hosted):
92 + non-streaming `complete()` aggregates the stream instead.
93 +
94 +### Streamed response parsing (SSE `data:` lines, `[DONE]` terminator)
95 +
96 +Per chunk (`choices[0]`):
97 +- `delta.reasoning_content ?? delta.reasoning` → reasoning delta (DeepSeek,
98 + Qwen, xAI, DeepInfra use `reasoning_content`; some use `reasoning`).
99 +- `delta.content ?? choices[0].text` → text delta (**Together quirk:** some
100 + models stream completions-style with token text in `choices[].text`).
101 +- **Mistral reasoning-model quirk:** `delta.content` may be an ARRAY of chunks
102 + `{type:"thinking"|"text", …}``thinking` chunks (each
103 + `{thinking:[{type:"text",text}]}`) join into reasoning; `text` chunks
104 + (`{text}`) join into content.
105 +- `finish_reason` → finished reason.
106 +- Top-level `citations` (array of URL strings) + `search_results`
107 + (`[{title,url}]`) → Perplexity numbered citations (emit once).
108 +- Top-level `usage` (`prompt_tokens`, `completion_tokens`,
109 + `completion_tokens_details.reasoning_tokens`) → usage event.
110 +- Malformed / unknown JSON chunks are tolerated and skipped (keep-alives).
111 +
112 +### `/models` listing quirks
113 +- Together returns a **bare array**; everyone else wraps in `{"data": […]}`.
114 +- Gemini compat prefixes ids with `models/` — strip it.
115 +- Perplexity has **no** `/models` endpoint (`supportsModelListing = false`) —
116 + key testing there does a minimal non-streaming completion (`max_tokens: 16`
117 + minimum — probe-verified) against the provider's cheapest catalog model.
118 +
119 +## 4. Anthropic client — Messages API rules
120 +
121 +- POST `…/v1/messages`; headers `x-api-key`, `anthropic-version: 2023-06-01`,
122 + plus browser header (CORS-MATRIX). **`max_tokens` is mandatory** (default
123 + 8192 when the user hasn't set one).
124 +- `system` is a top-level string param, never a message.
125 +- Message content is block-structured: image blocks
126 + `{type:"image", source:{type:"base64", media_type, data}}` come **before** the
127 + text block; empty text becomes `" "` (API rejects empty).
128 +- `thinking`: `{type:"enabled", budget_tokens: 8000}` / `{type:"disabled"}` for
129 + models with `thinkingToggle` (Claude Fable 5 has thinking always-on — no
130 + toggle sent). Claude 4.7+ models don't accept `temperature`/`top_p`
131 + (ParameterSupport encodes this per model).
132 +- **Named SSE events** (`event:` field): `message_start` (input usage) →
133 + `content_block_delta` (`delta.text` → text; `delta.thinking` → reasoning) →
134 + `message_delta` (output usage + `stop_reason`) → `message_stop`; `ping` and
135 + `content_block_start/stop` ignored; `error` events raise. Usage is emitted at
136 + stream end (input from start + output from delta).
137 +- Non-streaming: `content[]` blocks — join `type=="text"` for text,
138 + `type=="thinking"` for reasoning.
139 +
140 +## 5. SSE parser + networking (from `SSEParser`/`StreamingService`)
141 +
142 +- Line-level state machine: accumulate `event:`/`data:` fields; a **blank line**
143 + dispatches the event (multi-`data:` lines join with `\n`); `:` comment lines
144 + are keep-alives (DeepSeek sends `: keep-alive`) — skip; `id:`/`retry:`
145 + ignored; strip one leading space after `data:`; handle `\r\n`; flush a
146 + trailing unterminated event at stream end.
147 +- Web implementation: `fetch(url, {signal})` → check `res.ok` (non-2xx: read
148 + full body, map via `ProviderError.from(status, body, provider)`) → pipe
149 + `res.body` through `TextDecoderStream` → split on newlines → feed the parser.
150 +- **Error mapping:** 401/403 → invalid key (names the provider); 429 → rate
151 + limited (+ `Retry-After` if present); 400/404/422 → bad request with the
152 + provider's message; else server error. Error bodies are shape-sniffed:
153 + `{error:{message}}`, `{error:"…"}`, `{message}`, `{detail}`, Gemini arrays.
154 + A fetch `TypeError` (no HTTP status) is surfaced as a network/CORS-shaped
155 + error with the proxy/Router hint.
156 +- **Retry:** non-streaming POSTs retry ×3 with exponential backoff (4s, 8s) on
157 + 429/5xx, honoring `Retry-After`. Streaming does not auto-retry.
158 +- Cancellation: `AbortController` per in-flight generation; abort → typed
159 + `cancelled` error (maps to native `onTermination → task.cancel()`).
160 +
161 +## 6. Data model (ported to `types/`)
162 +
163 +- `AIModel`: `id` (exact wire ID), `provider`, `displayName`, `contextWindow`,
164 + `maxOutputTokens?`, `capabilities`, `pricing?`, `parameterSupport`,
165 + `isLegacy`, `isRecommended`, `customBaseURL?`. Context badge: `≥1M → "NM ctx"`,
166 + `≥1K → "NK ctx"`.
167 +- `ModelCapabilities`: `vision, tools, reasoning, streaming(=true), jsonMode,
168 + citations` (defaults false).
169 +- `ModelPricing`: `inputPerMTok/outputPerMTok` USD; `cost(in,out)` = base-rate
170 + estimate (cached/tiered pricing intentionally simplified; UI labels costs as
171 + estimates).
172 +- `ParameterSupport`: `temperature(+), topP(+), frequencyPenalty(−),
173 + presencePenalty(−), usesMaxCompletionTokens(−), reasoningEffort(−),
174 + thinkingToggle(−), requiresStreaming(−)`; preset `openAIDefault` adds both
175 + penalties.
176 +- `TokenUsage`: `inputTokens, outputTokens, reasoningTokens?` (+ addition).
177 +- `ChatEvent`: `reasoningDelta | textDelta | citations | usage | finished`.
178 +- `Citation`: `index (1-based), url, title?` (merge Perplexity `citations` +
179 + `search_results`).
180 +
181 +## 7. Model catalog (ported to `providers/catalog.ts`)
182 +
183 +**170 models** ported completely and faithfully from `ModelCatalogData.swift`
184 +(NOT from the drifted `docs/research/catalog-summary.md` which claims 195):
185 +openai 27, anthropic 11, xai 5, mistral 10, gemini 14, qwen 32, deepseek 2,
186 +kimi 12, perplexity 4, together 16, deepinfra 34, cerebras 3. 23 recommended
187 +models; legacies badged and ranked last. The full per-model data (IDs, names,
188 +context, max output, capabilities, pricing, parameter support, flags) is
189 +transcribed 1:1 in `catalog.ts` — that file IS the appendix.
190 +
191 +Catalog behavior ported from `ModelCatalog.swift`:
192 +- Ranking in pickers: favorites (0) → recommended (1) → normal (2) → legacy (3).
193 +- **Lookups keyed on `(provider, id)`** — ids duplicate across providers
194 + (e.g. `deepseek-v4-flash` exists under both deepseek and qwen;
195 + `moonshotai/Kimi-K2.6` under together and deepinfra).
196 +- `cheapestModel(provider)`: non-legacy, prefer non-reasoning, min
197 + `outputPerMTok` (nil → ∞) — used for key tests and title generation.
198 +- `defaultModel`: first recommended in catalog order → `gpt-5.6-sol`.
199 +- Dynamic `/models` refresh overlays a `liveModelIDs` set per provider (never
200 + mutates built-ins); unknown live IDs are offered as custom-model candidates.
201 +
202 +## 8. Design tokens (ported to `design/` as CSS variables — from `ZyquoTheme.swift`)
203 +
204 +| Token | Light | Dark |
205 +|---|---|---|
206 +| background | `#FAFBFD` | `#14161E` |
207 +| surface | `#FFFFFF` | `#1C1F2A` |
208 +| surfaceSecondary | `#F2F4F8` | `#232734` |
209 +| accent | `#4E6AF0` | `#6D84F5` |
210 +| accentSubtle | `#EBEFFD` | `#28304C` |
211 +| textPrimary | `#1A1C22` | `#E8EAF2` |
212 +| textSecondary | `#6B7080` | `#9BA1B5` |
213 +| textTertiary | `#9EA3B0` | `#6A7188` |
214 +| border (0.5px hairlines) | `#E4E7EE` | `#2C3040` |
215 +| success / warning / danger | `#2FA36B` / `#D9822B` / `#D64545` | `#43BD83` / `#E59A4D` / `#E36363` |
216 +
217 +Typography: system font stack (SF Pro on Apple); title 20/semibold; body
218 +13.5px default (user-adjustable 12–18), line-height **1.45**; caption 11;
219 +code mono 12.5. Spacing 4/8/12/16/20/24/32. Radii 6/10/14. Shadow: soft only
220 +(`rgba(0,0,0,0.06)`, blur 12, y 2) on floating elements. Metrics: sidebar 260,
221 +chat header 52, max message column 760, hairline 0.5. Motion: hover 80ms,
222 +send/message-in 150ms ease-out, pressed scale 0.97.
223 +
224 +Full native UX study (layout, picker, bubbles, settings, palette, compare,
225 +personas, prompt library, store behaviors, brand glyph) → `docs/NATIVE-UX.md`.
226 +
227 +## 9. Vault → localStorage (explicit difference)
228 +
229 +The native app stores keys in a machine-bound encrypted vault
230 +(`SecureKeyStore`). A browser has no equivalent primitive, so the web edition
231 +stores keys in **plain `localStorage`** (`zyquo.cloud.web.keys`), with a clear
232 +first-run notice, masked display, and an **opt-in passphrase lock**
233 +(WebCrypto AES-GCM, key derived from the passphrase) — honestly framed: once
234 +unlocked, a compromised page can still read keys. This is a deliberate
235 +bring-your-own-key local-only model, not a secure vault.
added scripts/cors-probe.sh +66 −0
@@ -0,0 +1,66 @@
1 +#!/bin/bash
2 +#
3 +# cors-probe.sh
4 +# Zyquo Cloud Web
5 +#
6 +# Author: Simon-Pierre Boucher
7 +# Mail: contact@spboucher.ai
8 +#
9 +# Phase 0.A — empirical CORS probe for all 12 providers.
10 +# For each provider: (1) OPTIONS preflight as a browser would send it,
11 +# (2) a real minimal POST with an Origin header, checking the
12 +# Access-Control-Allow-* response headers. Keys are read from
13 +# .keys.local.json (gitignored) and never printed.
14 +#
15 +
16 +set -u
17 +KEYS_FILE="$(dirname "$0")/../.keys.local.json"
18 +ORIGIN="https://www.zyquo.cloud"
19 +OUT_DIR="${1:-/tmp/zyquo-cors}"
20 +mkdir -p "$OUT_DIR"
21 +
22 +key() { python3 -c "import json,sys; print(json.load(open('$KEYS_FILE'))['$1'])"; }
23 +
24 +probe() {
25 + local name="$1" url="$2" auth_header="$3" model="$4" extra_header="${5:-}"
26 + local out="$OUT_DIR/$name.txt"
27 + {
28 + echo "=== $name$url ==="
29 + echo "--- PREFLIGHT (OPTIONS) ---"
30 + local req_headers="authorization,content-type"
31 + [ "$name" = "anthropic" ] && req_headers="x-api-key,anthropic-version,anthropic-dangerous-direct-browser-access,content-type"
32 + curl -sS -o /dev/null -D - -X OPTIONS "$url" \
33 + -H "Origin: $ORIGIN" \
34 + -H "Access-Control-Request-Method: POST" \
35 + -H "Access-Control-Request-Headers: $req_headers" \
36 + --max-time 30 2>&1 | grep -iE "^(HTTP|access-control|allow)" || echo "(no CORS headers in preflight)"
37 + echo "--- POST with Origin ---"
38 + local body='{"model":"'"$model"'","messages":[{"role":"user","content":"Say OK"}],"max_tokens":8}'
39 + [ "$name" = "anthropic" ] && body='{"model":"'"$model"'","max_tokens":8,"messages":[{"role":"user","content":"Say OK"}]}'
40 + curl -sS -o "$OUT_DIR/$name.body.json" -D - -X POST "$url" \
41 + -H "Origin: $ORIGIN" \
42 + -H "Content-Type: application/json" \
43 + -H "$auth_header" \
44 + ${extra_header:+-H "$extra_header"} \
45 + -d "$body" \
46 + --max-time 60 2>&1 | grep -iE "^(HTTP|access-control)" || echo "(no CORS headers in POST)"
47 + echo "--- POST body (first 300 chars) ---"
48 + head -c 300 "$OUT_DIR/$name.body.json"; echo
49 + } > "$out" 2>&1
50 + echo "done: $name"
51 +}
52 +
53 +probe openai "https://api.openai.com/v1/chat/completions" "Authorization: Bearer $(key openai)" "gpt-4.1-nano" &
54 +probe anthropic "https://api.anthropic.com/v1/messages" "x-api-key: $(key anthropic)" "claude-haiku-4-5-20251001" "anthropic-version: 2023-06-01" &
55 +probe xai "https://api.x.ai/v1/chat/completions" "Authorization: Bearer $(key xai)" "grok-3-mini" &
56 +probe mistral "https://api.mistral.ai/v1/chat/completions" "Authorization: Bearer $(key mistral)" "mistral-small-latest" &
57 +probe gemini "https://generativelanguage.googleapis.com/v1beta/openai/chat/completions" "Authorization: Bearer $(key gemini)" "gemini-2.5-flash-lite" &
58 +probe qwen "https://dashscope-intl.aliyuncs.com/compatible-mode/v1/chat/completions" "Authorization: Bearer $(key qwen)" "qwen-turbo" &
59 +probe deepseek "https://api.deepseek.com/chat/completions" "Authorization: Bearer $(key deepseek)" "deepseek-chat" &
60 +probe kimi "https://api.moonshot.ai/v1/chat/completions" "Authorization: Bearer $(key kimi)" "kimi-k2-0711-preview" &
61 +probe perplexity "https://api.perplexity.ai/chat/completions" "Authorization: Bearer $(key perplexity)" "sonar" &
62 +probe together "https://api.together.xyz/v1/chat/completions" "Authorization: Bearer $(key together)" "meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo" &
63 +probe deepinfra "https://api.deepinfra.com/v1/openai/chat/completions" "Authorization: Bearer $(key deepinfra)" "meta-llama/Meta-Llama-3.1-8B-Instruct" &
64 +probe cerebras "https://api.cerebras.ai/v1/chat/completions" "Authorization: Bearer $(key cerebras)" "llama3.1-8b" &
65 +wait
66 +echo "All probes complete → $OUT_DIR"
67