# CORS Matrix — Browser Feasibility per Provider (Phase 0.A) **Method.** Every verdict below is **empirical**, not guessed: on 2026-07-31 each provider's chat endpoint was probed with (1) a browser-identical `OPTIONS` preflight (`Origin: https://www.zyquo.cloud`, `Access-Control-Request-Method: POST`, `Access-Control-Request-Headers: authorization,content-type` — or the Anthropic header set), and (2) a **real authenticated POST** carrying the same `Origin`, inspecting the `Access-Control-Allow-*` response headers. A streaming (`stream: true`) request was additionally verified to return `content-type: text/event-stream` **with** `access-control-allow-origin` intact. Probe script: `scripts/cors-probe.sh` (keys read from gitignored `.keys.local.json`; never committed). ## Verdict table | # | Provider | Endpoint | Preflight | POST ACAO | Verdict | |---|----------|----------|-----------|-----------|---------| | 1 | OpenAI | `https://api.openai.com/v1/chat/completions` | 200, allows `authorization,content-type`, max-age 86400 | `*` (echoes origin on preflight) | ✅ direct | | 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** | | 3 | xAI | `https://api.x.ai/v1/chat/completions` | 200, `allow-methods: *`, `allow-headers: *` | `*` | ✅ direct | | 4 | Mistral | `https://api.mistral.ai/v1/chat/completions` | 200, allows `Authorization, Content-Type, …` | `*` | ✅ direct | | 5 | Google Gemini (OpenAI-compat) | `https://generativelanguage.googleapis.com/v1beta/openai/chat/completions` | 200, echoes origin, max-age 3600 | echoes origin | ✅ direct | | 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 | | 7 | DeepSeek | `https://api.deepseek.com/chat/completions` | 200, `allow-methods: POST`, echoes origin | echoes origin | ✅ direct | | 8 | Kimi (Moonshot) | `https://api.moonshot.ai/v1/chat/completions` | 204, echoes origin, credentials true | echoes origin | ✅ direct | | 9 | Perplexity | `https://api.perplexity.ai/chat/completions` | 204, `*`, generous header allowlist, max-age 1728000 | `*` | ✅ direct | | 10 | Together AI | `https://api.together.xyz/v1/chat/completions` | 200, `*` | `*` | ✅ direct | | 11 | DeepInfra | `https://api.deepinfra.com/v1/openai/chat/completions` | 200, `*` | `*` | ✅ direct | | 12 | Cerebras | `https://api.cerebras.ai/v1/chat/completions` | 200, `*`, max-age 600 | `*` | ✅ direct | **Bottom line: all 12 providers are callable directly from the browser today.** 11 are plain ✅; Anthropic is ⚠️ only in the sense that the client must add `anthropic-dangerous-direct-browser-access: true` on every request (the header name is Anthropic's deliberate reminder that browser-resident keys are exposed — which matches this app's transparent bring-your-own-key model). ## Probe caveats observed (not CORS failures) - **Perplexity** rejects `max_tokens < 16` (`400 max_tokens must be at least 16`) — keep test calls ≥ 16. - **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. - **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. - **DeepSeek** serves `deepseek-chat` as `deepseek-v4-flash` (alias resolution). - **xAI** resolves legacy aliases (probe with `grok-3-mini` answered as `grok-4.3`). - 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. ## Documented stance (official docs, web-researched + verified 2026-07-31) - **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/). - **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)) - **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)) - **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)) - **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)) - **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)) - **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)) **Implication:** outside Anthropic, permissive CORS is undocumented behavior that any provider could withdraw without notice — which is why the per-provider proxy override ships even though everything works today. The universal "don't ship your key in client code" warnings target developers embedding *their own* key; Zyquo Cloud Web's model is the opposite: each user brings **their own** key, which never leaves their device except toward the provider itself. ## Fallback design (proxy / Zyquo Router) — still shipped Even though everything works today, CORS policies are provider-controlled and can change without notice. The app therefore ships a **per-provider base-URL override** (Settings → Providers & Keys → "Proxy / base URL"): - Any provider's base URL can be overridden with a user-supplied CORS proxy or a locally running **Zyquo Router** (the family's local gateway, OpenAI-compatible). - When a request fails with a network/CORS-shaped error (fetch `TypeError` with no HTTP status), the error toast explains: *"This provider may be blocking browser calls — set a proxy URL in Settings or run Zyquo Router locally."* - The override is stored in `zyquo.cloud.web.settings`; the CSP includes a runtime-managed `connect-src` strategy (see Phase 8) so a user-set proxy origin remains reachable. ## Streaming over fetch - Verified: streaming responses (`text/event-stream`) carry the same `access-control-allow-origin` headers (OpenAI probe above), so `fetch` + `ReadableStream` SSE parsing works browser-side. - All 11 OpenAI-compatible providers use `data:` SSE lines with a `[DONE]` terminator; Anthropic uses named `event:` blocks. One parser (`providers/sse.ts`, ported from the native `SSEParser`) handles both. - Cancellation: `AbortController.abort()` on the fetch tears down the stream — used by Stop and new-message preemption.