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-chatasdeepseek-v4-flash(alias resolution). - xAI resolves legacy aliases (probe with
grok-3-minianswered asgrok-4.3). - Several providers echo the exact origin instead of
*(Gemini, DashScope, DeepSeek, Kimi) — irrelevant to the app, but means responses vary byOriginand 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 fordangerouslyAllowBrowser: 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, anthropic-sdk-typescript, Simon Willison's explainer. - 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, 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)
- 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) - xAI — docs silent on CORS for chat; ephemeral client tokens exist only for the realtime/audio API. (docs.x.ai)
- 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, 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, DeepSeek, Kimi, Perplexity, DeepInfra)
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
TypeErrorwith 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-managedconnect-srcstrategy (see Phase 8) so a user-set proxy origin remains reachable.
Streaming over fetch
- Verified: streaming responses (
text/event-stream) carry the sameaccess-control-allow-originheaders (OpenAI probe above), sofetch+ReadableStreamSSE parsing works browser-side. - All 11 OpenAI-compatible providers use
data:SSE lines with a[DONE]terminator; Anthropic uses namedevent:blocks. One parser (providers/sse.ts, ported from the nativeSSEParser) handles both. - Cancellation:
AbortController.abort()on the fetch tears down the stream — used by Stop and new-message preemption.