diff --git a/README.md b/README.md
index 667d2d3de..40857156d 100644
--- a/README.md
+++ b/README.md
@@ -12,8 +12,8 @@
-
-
+
+
@@ -89,7 +89,7 @@ In a world of generic text editors and bloated writing software, WorldScript Stu
- **✍️ From Macro to Micro:** Most tools focus only on writing. We cover the _entire_ creative lifecycle — from high-level plot structure and world-building down to sentence-by-sentence prose refinement.
- **🧠 Intelligent Partnership:** The AI is not a ghostwriter — it's a Socratic partner, a tireless brainstormer, and a creative muse. It's designed to break blocks and expand your own potential, not replace it.
-- **🔒 Ultimate Privacy & Ownership:** Your manuscript and project data stay on this device by default (IndexedDB in the browser, or local files in the desktop app). Cloud AI features send only the prompts and context you trigger to the provider you configure. Use local/Ollama/WebLLM mode if you want AI without sending text to any cloud API. There is no WorldScript account — you stay in control of exports and backups.
+- **🔒 Ultimate Privacy & Ownership:** Your manuscript and project data stay on this device by default (IndexedDB in the browser, or local files in the desktop app). Cloud AI features send only the prompts and context you trigger to the provider you configure — every provider is called directly from your browser/device, with **one exception**: Claude on the web/PWA (Vercel/Cloudflare Pages) relays through WorldScript's own stateless serverless proxy, since Anthropic blocks direct browser requests (desktop calls Anthropic directly, like every other provider — see [Encryption — which mechanism protects what](#-encryption--which-mechanism-protects-what)). Use local/Ollama/WebLLM mode if you want AI without sending text to any cloud API. There is no WorldScript account — you stay in control of exports and backups.
- **🔬 Built-in Quality Tools:** Go beyond writing with the AI Critic, Plot-Hole Detector, and RAG Consistency Checker — tools that help you catch narrative weaknesses before your readers do.
- **⚡ Browser-Native AI:** A 4-layer local inference stack (WebGPU → ONNX WASM → Transformers.js → heuristics) means local AI works even without Ollama — entirely in-browser, no server, no download manager.
@@ -337,6 +337,19 @@ different data, with different key material:
See [`docs/SECURITY-THREAT-MODEL.md`](docs/SECURITY-THREAT-MODEL.md) for the full threat-model mapping.
+**Claude is the one provider that isn't a direct browser/device→provider call, on the web build
+specifically.** Anthropic doesn't send CORS headers permitting direct browser access, so:
+
+- **Desktop (Tauri):** calls `api.anthropic.com` directly via native HTTP — no proxy, same trust
+ model as every other provider.
+- **Web/PWA on Vercel or Cloudflare Pages:** relays through WorldScript's own stateless serverless
+ proxy (`api/claude-proxy.ts` / `functions/api/claude-proxy.ts`) — your key transits that one
+ request through infrastructure WorldScript runs, en route to Anthropic. The proxy never logs or
+ persists the key, prompt, or response (see `docs/SECURITY-THREAT-MODEL.md` § *Claude serverless
+ proxy trust-model change*).
+- **Web/PWA on GitHub Pages:** Claude is unavailable — GitHub Pages is static-only and can't host
+ the proxy at all. Use the desktop app or a Vercel/Cloudflare Pages deployment instead.
+
### ⚡ Performance, Analytics & Extensibility
Infrastructure-level features that keep the app fast and extensible as projects grow — all on by default unless noted:
@@ -383,7 +396,7 @@ Infrastructure-level features that keep the app fast and extensible as projects
### 🌐 Full Multi-Language Support
-Shipped UI locales with **2856 i18n keys** across all 19 languages — zero hardcoded user-facing strings:
+Shipped UI locales with **2857 i18n keys** across all 19 languages — zero hardcoded user-facing strings:
- 🇩🇪 **German** (Deutsch)
- 🇬🇧 **English**
@@ -423,7 +436,7 @@ WorldScript Studio supports **9 distinct AI execution paths**, automatically rou
|-------|----------|----------|-------|
| **Cloud 1** | Google Gemini | API key (BYOK) | Primary cloud path; Gemini 2.0 Flash |
| **Cloud 2** | OpenAI | API key (BYOK) | GPT-4o, GPT-4o-mini |
-| **Cloud 3** | Anthropic Claude | API key (BYOK) | Claude 3.5 Sonnet |
+| **Cloud 3** | Anthropic Claude | API key (BYOK) | Claude Opus 4.7, Sonnet 4.6, Haiku 4.5 — desktop (native) and Vercel/Cloudflare Pages (via serverless proxy); unavailable on GitHub Pages, see [privacy note](#-encryption--which-mechanism-protects-what) |
| **Cloud 4** | Grok (xAI) | API key (BYOK) | grok-3, grok-3-mini |
| **Cloud 5** | **OpenRouter** | Free or paid key | Unified gateway: DeepSeek R1, Llama 3.3 70B, Qwen 2.5 72B + 100s more; `:free` suffix = zero cost |
| **Local 1** | Ollama | Local server | Default model: Qwen3 8B; configurable URL |
@@ -492,8 +505,8 @@ The Settings → AI panel shows a live GPU status badge with adapter details and
| **PDF Export** | jsPDF | Client-side, configurable PDF document generation |
| **Document Export** | docx + jszip | Word-compatible `.docx` generation (lazy-loaded) |
| **PWA** | Service Worker + Web App Manifest v3 | Offline support, installability, Workbox chunking |
-| **i18n** | Custom React Context (`I18nContext.tsx`) | 2856 keys × 19 locales (de/en/es/fr/it + ar/he/fa RTL Beta + ja/zh/pt/el/fi/sv/hu/is/eu Beta); EN fallback; `localStorage` persistence |
-| **Testing** | Vitest 4.x (6477 tests / 529 files) + Playwright E2E | Unit/integration + cross-browser E2E; Stryker mutation (manual workflow) |
+| **i18n** | Custom React Context (`I18nContext.tsx`) | 2857 keys × 19 locales (de/en/es/fr/it + ar/he/fa RTL Beta + ja/zh/pt/el/fi/sv/hu/is/eu Beta); EN fallback; `localStorage` persistence |
+| **Testing** | Vitest 4.x (6477+ tests / 532 files) + Playwright E2E | Unit/integration + cross-browser E2E; Stryker mutation (manual workflow) |
| **Code Quality** | Biome (lint + format) + TypeScript 7 (tsgo) strict | `--error-on-warnings` in CI; zero `any` policy |
| **Visualization** | Force-directed graph | Interactive character relationship network |
| **Desktop** | Tauri v2 | Cross-platform installer; auto-updater via `latest.json` |
@@ -531,7 +544,7 @@ WorldScript-Studio/
│ ├── sw.js # PWA Service Worker
│ └── manifest.json # PWA Web App Manifest v3
├── tests/
-│ ├── unit/ # Vitest unit tests (6477 tests, 529 files) — count spans tests/, components/, packages/*/tests/, not just this folder
+│ ├── unit/ # Vitest unit tests (6477+ tests, 532 files) — count spans tests/, components/, packages/*/tests/, not just this folder
│ │ ├── ai/ # aiSmallModules, aiCoreFallbackPaths
│ │ └── settings/ # WebLlmPanel, AiSections
│ └── e2e/ # Playwright specs + helpers.ts
@@ -692,9 +705,9 @@ The main pipeline is [`.github/workflows/ci.yml`](.github/workflows/ci.yml). Opt
| `scorecard` | weekly + `main` push | OpenSSF Scorecard — SARIF uploaded to GitHub Code Scanning |
**Current test metrics (2026-07-30, CI-reported):**
-- **6477 unit tests** across **529 test files** — all passing
+- **6477+ unit tests** across **532 test files** — all passing
- Coverage thresholds: lines ≥ 74 · branches ≥ 60 · functions ≥ 67 · statements ≥ 72 — enforced in CI (see Codecov badge for live metrics)
-- i18n: **2856 keys × 19 locales** (en/de/fr/es/it + ar/he/fa RTL Beta + ja/zh/pt/el/fi/sv/hu/is/eu Beta)
+- i18n: **2857 keys × 19 locales** (en/de/fr/es/it + ar/he/fa RTL Beta + ja/zh/pt/el/fi/sv/hu/is/eu Beta)
**CI-cloud-first workflow (recommended):** On constrained hardware run **`pnpm run lint && pnpm run i18n:check && pnpm run typecheck`** locally, then push and let CI handle coverage, E2E, Lighthouse, and Stryker. Authoritative numbers come from CI artifacts (Codecov, JUnit). After CI goes green, update the README badges and `AUDIT.md` quality-gate line from the reported metrics. See **[`docs/CI.md`](docs/CI.md) § Cloud CI-first vs local development** for the full post-merge doc-update checklist.
diff --git a/api/_shared/claudeProxyCore.ts b/api/_shared/claudeProxyCore.ts
new file mode 100644
index 000000000..b740eec66
--- /dev/null
+++ b/api/_shared/claudeProxyCore.ts
@@ -0,0 +1,182 @@
+// QNBS-v3 (ADR-0016 Track B): platform-agnostic relay core, shared by the Vercel Edge Function
+// (api/claude-proxy.ts) and the Cloudflare Pages Function (functions/api/claude-proxy.ts) so the
+// abuse-control logic — the actual security-relevant part — is written and tested exactly once.
+// Web-standard Request/Response only; no platform-specific types, so it needs neither @vercel/node
+// nor @cloudflare/workers-types as a new dependency.
+import { z } from 'zod';
+
+const ANTHROPIC_MESSAGES_URL = 'https://api.anthropic.com/v1/messages';
+const ANTHROPIC_VERSION = '2023-06-01';
+
+// QNBS-v3: mirrors the model dropdown in AiProviderCard.tsx (Track A) — keeping the enum in sync
+// is a deliberate defense-in-depth constraint, not just laziness: it stops the public endpoint from
+// being used to probe/relay requests for arbitrary future Anthropic model ids.
+const ALLOWED_MODELS = ['claude-opus-4-7', 'claude-sonnet-4-6', 'claude-haiku-4-5'] as const;
+
+// QNBS-v3: this app only ever sends a single user message (see streamAnthropic in
+// aiProviderService.ts) — the small array cap leaves room for a future multi-turn use case without
+// letting the body-size/message-count limits do any real abuse-prevention work.
+const MAX_MESSAGES = 20;
+const MAX_MESSAGE_CHARS = 100_000;
+const MAX_BODY_BYTES = 262_144; // 256 KiB
+const MAX_TOKENS_CEILING = 8192;
+const OUTBOUND_TIMEOUT_MS = 20_000;
+const RATE_LIMIT_WINDOW_MS = 60_000;
+const RATE_LIMIT_MAX_REQUESTS = 20;
+// QNBS-v3: bounds the rate-limit map itself — without this a burst of distinct spoofed
+// X-Forwarded-For values would turn the limiter into its own unbounded-memory DoS vector.
+const RATE_LIMIT_MAX_TRACKED_CLIENTS = 5000;
+
+const claudeProxyRequestSchema = z.object({
+ apiKey: z.string().min(20).max(200),
+ model: z.enum(ALLOWED_MODELS),
+ maxTokens: z.number().int().positive().max(MAX_TOKENS_CEILING).optional(),
+ messages: z
+ .array(
+ z.object({
+ role: z.enum(['user', 'assistant']),
+ content: z.string().min(1).max(MAX_MESSAGE_CHARS),
+ }),
+ )
+ .min(1)
+ .max(MAX_MESSAGES),
+});
+
+// QNBS-v3: module-level so it survives across invocations on a warm edge/worker instance — a
+// best-effort, single-instance limiter (explicitly accepted in the plan/ADR: true distributed rate
+// limiting needs a platform KV/rate-limit product, which this app does not depend on).
+const rateLimitLog = new Map();
+
+function isRateLimited(clientId: string): boolean {
+ const now = Date.now();
+ const recent = (rateLimitLog.get(clientId) ?? []).filter(
+ (timestamp) => now - timestamp < RATE_LIMIT_WINDOW_MS,
+ );
+ recent.push(now);
+ rateLimitLog.set(clientId, recent);
+ // QNBS-v3 (CodeRabbit, PR #301): a spoofed x-forwarded-for burst must not be able to reset every
+ // real client's window via a wholesale .clear() (CWE-770) — evict stale entries first, then oldest
+ // by insertion order, and never evict the current client.
+ if (rateLimitLog.size > RATE_LIMIT_MAX_TRACKED_CLIENTS) {
+ for (const [id, stamps] of rateLimitLog) {
+ if (id !== clientId && stamps.every((t) => now - t >= RATE_LIMIT_WINDOW_MS)) {
+ rateLimitLog.delete(id);
+ }
+ }
+ for (const id of rateLimitLog.keys()) {
+ if (rateLimitLog.size <= RATE_LIMIT_MAX_TRACKED_CLIENTS) break;
+ if (id !== clientId) rateLimitLog.delete(id);
+ }
+ }
+ return recent.length > RATE_LIMIT_MAX_REQUESTS;
+}
+
+function clientIdFor(request: Request): string {
+ const forwardedFor = request.headers.get('x-forwarded-for');
+ const first = forwardedFor?.split(',')[0]?.trim();
+ return first && first.length > 0 ? first : 'unknown';
+}
+
+function isSameOriginRequest(request: Request): boolean {
+ const origin = request.headers.get('origin');
+ if (!origin) return false;
+ try {
+ return new URL(origin).host === new URL(request.url).host;
+ } catch {
+ return false;
+ }
+}
+
+function jsonResponse(status: number, body: Record): Response {
+ return new Response(JSON.stringify(body), {
+ status,
+ headers: {
+ 'content-type': 'application/json',
+ // QNBS-v3: this is a stateless per-request relay of user-specific, key-authenticated content
+ // — it must never be cached by any CDN/browser layer.
+ 'cache-control': 'no-store',
+ },
+ });
+}
+
+/**
+ * Relays a validated `{ apiKey, model, messages, maxTokens? }` body to the Anthropic Messages API
+ * and returns Anthropic's own JSON response (or a proxy-level error) unmodified, so client-side
+ * parsing is identical whether it called Anthropic directly (Track A, desktop) or via this proxy
+ * (Track B, web). Never logs the API key, prompt, or response — see ADR-0016 statelessness guarantee.
+ */
+export async function handleClaudeProxyRequest(
+ request: Request,
+ fetchImpl: typeof fetch = fetch,
+): Promise {
+ if (request.method !== 'POST') {
+ return jsonResponse(405, { error: 'Method not allowed' });
+ }
+
+ if (!isSameOriginRequest(request)) {
+ return jsonResponse(403, { error: 'Origin not allowed' });
+ }
+
+ const clientId = clientIdFor(request);
+ if (isRateLimited(clientId)) {
+ return jsonResponse(429, { error: 'Rate limit exceeded' });
+ }
+
+ const contentLength = Number(request.headers.get('content-length') ?? '0');
+ if (contentLength > MAX_BODY_BYTES) {
+ return jsonResponse(413, { error: 'Request body too large' });
+ }
+
+ let rawBody: string;
+ try {
+ rawBody = await request.text();
+ } catch {
+ return jsonResponse(400, { error: 'Could not read request body' });
+ }
+ if (rawBody.length > MAX_BODY_BYTES) {
+ return jsonResponse(413, { error: 'Request body too large' });
+ }
+
+ let parsedBody: unknown;
+ try {
+ parsedBody = JSON.parse(rawBody);
+ } catch {
+ return jsonResponse(400, { error: 'Invalid JSON body' });
+ }
+
+ const validated = claudeProxyRequestSchema.safeParse(parsedBody);
+ if (!validated.success) {
+ return jsonResponse(400, { error: 'Request body failed validation' });
+ }
+ const { apiKey, model, messages, maxTokens } = validated.data;
+
+ try {
+ const upstreamResponse = await fetchImpl(ANTHROPIC_MESSAGES_URL, {
+ method: 'POST',
+ headers: {
+ 'x-api-key': apiKey,
+ 'anthropic-version': ANTHROPIC_VERSION,
+ 'content-type': 'application/json',
+ },
+ body: JSON.stringify({
+ model,
+ max_tokens: maxTokens ?? 2048,
+ messages,
+ }),
+ signal: AbortSignal.timeout(OUTBOUND_TIMEOUT_MS),
+ });
+ const upstreamText = await upstreamResponse.text();
+ return new Response(upstreamText, {
+ status: upstreamResponse.status,
+ headers: {
+ 'content-type': upstreamResponse.headers.get('content-type') ?? 'application/json',
+ 'cache-control': 'no-store',
+ },
+ });
+ } catch (err) {
+ const isTimeout = err instanceof Error && err.name === 'TimeoutError';
+ return jsonResponse(isTimeout ? 504 : 502, {
+ error: isTimeout ? 'Upstream request timed out' : 'Upstream request failed',
+ });
+ }
+}
diff --git a/api/claude-proxy.ts b/api/claude-proxy.ts
new file mode 100644
index 000000000..56cdcdc65
--- /dev/null
+++ b/api/claude-proxy.ts
@@ -0,0 +1,12 @@
+// QNBS-v3 (ADR-0016 Track B): Vercel Edge Function entry point. All abuse-control and relay logic
+// lives in ./_shared/claudeProxyCore.ts (shared with the Cloudflare Pages equivalent in
+// functions/api/claude-proxy.ts) — this file only adapts the platform's runtime config + handler shape.
+// Edge (not Node) runtime: no new dependency (@vercel/node) needed, since the handler is written
+// against the Web-standard Request/Response types already available via the "DOM" lib.
+import { handleClaudeProxyRequest } from './_shared/claudeProxyCore';
+
+export const config = { runtime: 'edge' };
+
+export default function handler(request: Request): Promise {
+ return handleClaudeProxyRequest(request);
+}
diff --git a/components/settings/AiProviderCard.tsx b/components/settings/AiProviderCard.tsx
index 83819ad56..8b2aafcd2 100644
--- a/components/settings/AiProviderCard.tsx
+++ b/components/settings/AiProviderCard.tsx
@@ -11,6 +11,7 @@ import {
scanLocalOpenAiCompatibleEndpoints,
testAIConnection,
} from '../../services/aiProviderService';
+import { isServerlessProxyCapable } from '../../services/deployTarget';
import { storageService } from '../../services/storageService';
import { isTauriRuntime } from '../../services/tauriRuntime';
import type { AdvancedAiSettings, AIProvider, LocalBackendPreset } from '../../types';
@@ -20,6 +21,7 @@ import { Icon } from '../ui/Icon';
import { Input } from '../ui/Input';
import { Select } from '../ui/Select';
import { Spinner } from '../ui/Spinner';
+import { AnthropicProviderFields } from './AnthropicProviderFields';
interface AiProviderCardProps {
advancedAi: AdvancedAiSettings;
@@ -40,6 +42,9 @@ export const AiProviderCard: FC = ({
// QNBS-v3 (T0): canonical detection (`__TAURI_INTERNALS__`-aware); `__TAURI__` alone read as web
// in the real desktop shell, hiding desktop-only provider affordances.
const isDesktop = isTauriRuntime();
+ // QNBS-v3 (ADR-0016 Track B): web/PWA Claude support depends on api/claude-proxy existing on the
+ // deployment — Vercel/Cloudflare Pages can host it, GitHub Pages (static-only) never can.
+ const isAnthropicProxyCapableWeb = !isDesktop && isServerlessProxyCapable();
// QNBS-v3: for Ollama in a browser, the auto-test effect and the manual "Test connection"
// button are both disabled (see below) — testStatus can never leave 'idle' here, so the
// generic status badge must not render its idle→"Ready" label, which would misleadingly
@@ -665,62 +670,19 @@ export const AiProviderCard: FC = ({
)}
- {/* QNBS-v3 (ADR-0016 Track A): desktop bypasses Anthropic's CORS restriction via the
- native-HTTP path (localServerFetch) — real key input instead of the warning-only
- block web/PWA still needs until Track B's proxy ships. */}
- {provider === 'anthropic' && isDesktop && (
-
+ );
+};
diff --git a/docs/SECURITY-THREAT-MODEL.md b/docs/SECURITY-THREAT-MODEL.md
index e2ff8109e..0baa311d3 100644
--- a/docs/SECURITY-THREAT-MODEL.md
+++ b/docs/SECURITY-THREAT-MODEL.md
@@ -45,6 +45,7 @@ This document provides a formal STRIDE threat analysis for WorldScript Studio, m
| DuckDB analytics unencrypted (SEC-6) | **Bounded by design:** only local metadata is persisted (titles, loglines, character names, codex excerpts, word counts, embeddings) — **never manuscript prose**, and **nothing leaves the device**. Gated by `enableDuckDbAnalytics` **and** the Settings → Privacy "Analytics" opt-out (`isAnalyticsPersistenceAllowed` in `app/listenerMiddleware.ts`); turning the toggle off stops all DuckDB writes + inference telemetry. Full OPFS file / cell-level (`*_enc`) encryption is **deferred to v2.0** — DuckDB-WASM owns the OPFS file write, so transparent file encryption is high-risk for the limited exposure. The `duckdbEncryption.ts` shim has 0 callers by design until then. | `app/listenerMiddleware.ts:isAnalyticsPersistenceAllowed`, `services/duckdb/duckdbEncryption.ts` |
| Prompt injection exposing context | Prompt sanitization | `services/ai/ragPromptAssembly.ts:sanitizePromptBlock()` |
| Prompt injection via AI-proposed edits | Control-character / lone-surrogate validation; per-item skip | `services/proForge/applyReviewEdits.ts:validateProposedText()` |
+| Claude BYOK key transits WorldScript's own infrastructure (web/PWA only — see §Claude serverless proxy below) | Stateless relay, no logging of key/prompt/response on any path; same-origin check rejects third-party callers | `api/_shared/claudeProxyCore.ts:handleClaudeProxyRequest()` |
### D - Denial of Service
@@ -54,6 +55,7 @@ This document provides a formal STRIDE threat analysis for WorldScript Studio, m
| Worker pool exhaustion | PriorityTaskQueue with MAX_QUEUE_SIZE=32 | `packages/worker-bus/src/taskQueue.ts` |
| Infinite AI retry loops | Exponential backoff with cap (30s) | `services/ai/aiRetry.ts` |
| Malicious plugin CPU burn | Worker isolation with timeout | `workers/plugin.worker.ts` (P0-2) |
+| Public `claude-proxy` endpoint used as an open relay / resource-exhaustion surface (CWE-400) | Zod schema validation, 256 KiB body-size cap (checked via header **and** actual body length), same-origin check, per-client in-memory rate limit (20 req/60s), 20s outbound timeout to Anthropic | `api/_shared/claudeProxyCore.ts` |
### E - Elevation of Privilege
@@ -147,6 +149,7 @@ Goal: Recover a user's cloud-provider API key from the Tauri desktop install
| `index.html` (web PWA) | I | CSP `connect-src 'self' https:` — broad HTTPS by design for BYOK; no `http:`/`ws:` wildcards | ⚠️ Documented tradeoff ([ADR-0004](adr/0004-csp-connect-src-byok-tradeoff.md)) |
| `vercel.json` / `public/_headers` / `nginx.conf` | I | `Content-Security-Policy` response header, mirrors the meta CSP (`frame-ancestors 'none'` only takes effect as a header) | ✅ Complete on Vercel/CF/Docker. **GitHub Pages cannot set response headers at all** — the `index.html` meta CSP is the sole enforcement there. |
| `script-src` (all 5 CSP surfaces) | D (denial of advertised functionality) | `'wasm-unsafe-eval'` (not `'unsafe-eval'`) — WebAssembly compile/instantiate for WebLLM/ONNX/Transformers.js/DuckDB-WASM/Whisper/Kokoro; plugin-sandbox WASM denial (`workers/plugin.worker.ts`) is a separate JS-level guard, unaffected | ✅ Complete ([ADR-0013](adr/0013-csp-wasm-and-blob-frames.md)) — was absent 2026-05-27 to 2026-07-29, blocking the entire local-inference stack in production (F-01/F-02) |
+| `api/_shared/claudeProxyCore.ts` (Vercel Edge Function + Cloudflare Pages Function) | I, D | Schema validation, body-size cap, same-origin check, per-client rate limit, outbound timeout, no logging of key/prompt/response on any path | ✅ Complete ([ADR-0016](adr/0016-native-grok-and-claude-providers.md) Track B) |
### CSP connect-src: web-vs-Tauri asymmetry (ADR-0004)
@@ -189,6 +192,38 @@ success and error paths, independent of CSP; the adversarial tests in
`tests/unit/workers/plugin.worker.test.ts` remain green, unaffected. Full decision record:
[`docs/adr/0013-csp-wasm-and-blob-frames.md`](adr/0013-csp-wasm-and-blob-frames.md).
+### Claude serverless proxy trust-model change (ADR-0016 Track B)
+
+Every cloud AI provider in WorldScript except Claude-on-web is a **direct browser→provider** call —
+the user's API key leaves their machine and goes straight to Gemini/OpenAI/Grok/OpenRouter, never
+touching WorldScript's own infrastructure. **This is the one exception.** Anthropic blocks direct
+browser requests entirely (no CORS allowlist WorldScript can request), so the web/PWA build (Vercel,
+Cloudflare Pages — not GitHub Pages, which is static-only and can host neither function) relays
+Claude calls through `api/claude-proxy.ts` / `functions/api/claude-proxy.ts`, both thin
+platform-specific wrappers around the shared `api/_shared/claudeProxyCore.ts` relay. Concretely: the
+user's browser → WorldScript's own Vercel/Cloudflare deployment → `api.anthropic.com`. The desktop
+app (Tauri, ADR-0012's native-HTTP escape hatch — see [ADR-0016 Track A](adr/0016-native-grok-and-claude-providers.md))
+does **not** go through this proxy; it calls Anthropic directly, matching every other provider's
+trust model.
+
+**Statelessness guarantee:** the proxy is a pure relay. It never writes the API key, prompt, or
+response to any log, database, or cache — `tests/unit/api/claudeProxyCore.test.ts` asserts
+`console.log`/`.warn`/`.error` are never called on any code path (success, validation failure, rate
+limit, upstream error). **Abuse controls** (the endpoint is public and unauthenticated by
+necessity — it exists so *any* user's own browser can reach it): Zod schema validation, a 256 KiB
+body-size cap enforced against both the declared `Content-Length` header and the actual received
+body length (defeats a spoofed header), a same-origin check (`Origin` header must match the
+deployment's own host — rejects third-party pages driving traffic through the proxy with a stolen
+or attacker-supplied key), a best-effort per-client-IP rate limit (20 requests/60s, in-memory —
+genuinely per-instance, not distributed; see the code comment for why a platform KV/rate-limit
+product was judged out of scope), and a 20s timeout on the outbound call to Anthropic so a hung
+upstream can't tie up function instances indefinitely.
+
+**What this does *not* change:** the proxy never sees the user's manuscript content in a way it
+didn't already see as the request body — it is a transit point, not a new data store. It also
+doesn't affect the CSP tradeoff above (ADR-0004): the client→proxy leg is same-origin (`'self'`,
+already allowed), and the proxy→Anthropic leg is server-side, never subject to browser CSP at all.
+
## Security Checklist
- [x] PBKDF2 iterations ≥ 600,000 (OWASP 2024 minimum)
@@ -203,6 +238,7 @@ success and error paths, independent of CSP; the adversarial tests in
- [x] DuckDB analytics privacy-gated (SEC-6) — writes require `enableDuckDbAnalytics` **and** the Settings → Privacy "Analytics" opt-out (`isAnalyticsPersistenceAllowed`, `app/listenerMiddleware.ts`); only local metadata is stored, never prose, nothing leaves the device.
- [ ] DuckDB OPFS at-rest encryption (P0-4 / SEC-6) — **deferred to v2.0.** Encryption module + unit tests exist (`services/duckdb/duckdbEncryption.ts`) but stay unwired (0 callers by design): DuckDB-WASM owns the OPFS file write, so transparent file encryption is high-risk for the bounded, local-only metadata exposure.
- [x] Voice WASM download UX (P0-5) — `components/voice/VoiceModelDownloadModal.tsx`
+- [x] Claude web proxy (ADR-0016 Track B): stateless (no key/prompt/response logging), schema-validated, body-size-capped, same-origin-checked, rate-limited, timeout-bounded — `api/_shared/claudeProxyCore.ts`
## References
diff --git a/docs/adr/0016-native-grok-and-claude-providers.md b/docs/adr/0016-native-grok-and-claude-providers.md
index 182810546..59b430977 100644
--- a/docs/adr/0016-native-grok-and-claude-providers.md
+++ b/docs/adr/0016-native-grok-and-claude-providers.md
@@ -106,9 +106,24 @@ for the full design.
could have Claude too, at the cost of routing every such request through a fixed external host
the user didn't choose) is not part of this decision — it's called out in the plan as a choice
for the maintainer to make explicitly later, not a default.
-- **Not yet resolved:** whether Claude's image-generation call site should get an equivalent fix
- depends on whether Anthropic's API even offers image generation — verify before wiring UI for a
- capability that may not exist.
+- **Resolved during Track B implementation:** Claude's image-generation call site (`generateImage()`'s
+ `case 'anthropic':` in `services/aiProviderService.ts`) is left throwing, on every platform —
+ Anthropic's API has no image-generation endpoint at all, so this was never a CORS/proxy bug to fix.
+- **Resolved during Track B implementation:** no dedicated E2E spec was added for the Claude Settings
+ flow. Neither Grok (Phase 1) nor any other cloud provider (Gemini/OpenAI/Ollama/OpenRouter) has
+ one in this repo — that class of Settings-form interaction is covered at the component/RTL level
+ only, and the desktop/proxy-web/GitHub-Pages branching added here follows the same pattern
+ (`tests/unit/settings/AiProviderCard.test.tsx`, `tests/unit/aiProviderService.test.ts`,
+ `tests/unit/api/claudeProxyCore.test.ts`). Introducing a first-of-its-kind provider E2E spec —
+ with no established mocking pattern for `/api/claude-proxy` or base-path simulation — would be new
+ test infrastructure beyond what this fix requires, not a gap relative to actual repo convention.
+- **Resolved during Track B implementation:** `services/ai/providerFactory.ts`'s `providerToKind()`
+ deliberately keeps returning `'unsupported'` for `'anthropic'` — wiring the newer Vercel-AI-SDK
+ Writer-streaming layer would need the `@ai-sdk/anthropic` package (Anthropic's Messages API isn't
+ OpenAI-Chat-Completions-shaped, unlike Grok/Ollama, which both work through `createOpenAI`), a new
+ runtime dependency out of scope for this plan. Both tracks instead wire the actual live call path,
+ `aiProviderService.ts` — the same one every existing Settings UI flow (Test Connection, ProForge,
+ Writer thunks) already goes through.
## References
diff --git a/functions/api/claude-proxy.ts b/functions/api/claude-proxy.ts
new file mode 100644
index 000000000..9fa1d857a
--- /dev/null
+++ b/functions/api/claude-proxy.ts
@@ -0,0 +1,17 @@
+// QNBS-v3 (ADR-0016 Track B): Cloudflare Pages Function entry point, at functions/api/claude-proxy.ts
+// so its route (/api/claude-proxy) matches the Vercel Edge Function's (api/claude-proxy.ts) exactly
+// — Cloudflare Pages routes a Function by its path *relative to functions/*, with no automatic
+// "/api" prefix, unlike some other platforms' conventions. All abuse-control and relay logic lives
+// in ../../api/_shared/claudeProxyCore.ts (shared with the Vercel entry point) — this file only
+// adapts Cloudflare's `onRequest`/context handler shape. Typed by hand against the documented
+// Cloudflare Pages Functions context shape rather than the `@cloudflare/workers-types` package, to
+// avoid adding a new dependency for a single structural type.
+import { handleClaudeProxyRequest } from '../../api/_shared/claudeProxyCore';
+
+interface CloudflarePagesContext {
+ request: Request;
+}
+
+export function onRequest(context: CloudflarePagesContext): Promise {
+ return handleClaudeProxyRequest(context.request);
+}
diff --git a/locales/ar/settings.json b/locales/ar/settings.json
index 8360e4bdb..a016fbe48 100644
--- a/locales/ar/settings.json
+++ b/locales/ar/settings.json
@@ -107,6 +107,7 @@
"settings.ai.anthropicCorsNote": "لا يمكن استدعاء Anthropic Claude مباشرة من المتصفّح. أعدّ خادم وكيل أو استخدم تطبيق Tauri لسطح المكتب لاستدعاءات Anthropic.",
"settings.ai.anthropicHint": "يتطلب Anthropic خادم وكيل أو تطبيق Tauri لسطح المكتب لاستدعاءات API.",
"settings.ai.anthropicKey": "Claude API Key",
+ "settings.ai.anthropicProxyNote": "Claude requests from the browser are relayed through WorldScript's own serverless proxy on the way to Anthropic — the only cloud provider routed this way. Your key is never stored server-side.",
"settings.ai.connectionFailed": "فشل الاتصال",
"settings.ai.connectionSuccess": "نجح الاتصال",
"settings.ai.corsRestriction": "قيد CORS",
@@ -245,12 +246,12 @@
"settings.ai.temperature.creative": "2 – إبداعي",
"settings.ai.temperature.precise": "0 – دقيق",
"settings.ai.testConnection": "اختبار الاتصال",
- "settings.ai.testError.backendProxyRequired": "يتطلب Claude وكيل خلفية (قيود CORS)",
"settings.ai.testError.desktopRequired": "تتوفر Ollama والخوادم المحلية المتوافقة مع OpenAI فقط في تطبيق سطح المكتب. تحظر المتصفحات الاتصالات المباشرة من صفحات الويب بـ localhost (CORS وPrivate Network Access).",
"settings.ai.testError.httpError": "HTTP {{status}}",
"settings.ai.testError.noApiKey": "لم يتم تعيين مفتاح API لـ {{provider}}",
"settings.ai.testError.noWebgpu": "WebGPU غير متوفر في هذا المتصفح — يحتاج WebLLM إلى WebGPU (جرّب Chrome/Edge أو فعّل العلامات).",
"settings.ai.testError.pluginUnavailable": "شبكة الخادم المحلي غير متوفرة: فشل تحميل مكوّن HTTP لسطح المكتب.",
+ "settings.ai.testError.proxyUnavailableStaticHost": "Claude is not available on this deployment: it has no serverless proxy (GitHub Pages is static-only). Use the desktop app, or a Vercel/Cloudflare Pages deployment instead.",
"settings.ai.testError.timeout": "انتهت مهلة الاتصال ({{url}})",
"settings.ai.testError.unexpected": "حدث خطأ غير متوقع. يرجى المحاولة مرة أخرى.",
"settings.ai.testError.unknownProvider": "مزوّد غير معروف",
diff --git a/locales/de/settings.json b/locales/de/settings.json
index 2c2db754a..1051cf586 100644
--- a/locales/de/settings.json
+++ b/locales/de/settings.json
@@ -104,12 +104,13 @@
"settings.ai.adaptive.vramTier": "GPU-VRAM-Stufe",
"settings.ai.adaptive.warmedModels": "Geladene Modelle",
"settings.ai.adaptive.warmedModelsHint": "Modelle im Speicher für schnelle Inferenz bereit",
- "settings.ai.anthropicCorsNote": "Anthropic Claude kann nicht direkt aus dem Browser aufgerufen werden. Nutze die Tauri-Desktop-App für Anthropic-Aufrufe oder warte auf die geplante Proxy-Server-Unterstützung.",
- "settings.ai.anthropicHint": "Anthropic erfordert die Tauri-Desktop-App oder einen Proxy-Server (in Vorbereitung) für Aufrufe im Browser.",
+ "settings.ai.anthropicCorsNote": "Claude ist auf diesem Deployment nicht verfügbar: GitHub Pages ist rein statisch und kann den Serverless-Proxy von WorldScript nicht ausführen. Nutzen Sie stattdessen die Desktop-App oder ein Vercel-/Cloudflare-Pages-Deployment.",
+ "settings.ai.anthropicHint": "Claude funktioniert in der Desktop-App (nativ) sowie im Browser auf Vercel-/Cloudflare-Pages-Deployments (über den eigenen Serverless-Proxy von WorldScript) — nicht auf dem statischen GitHub-Pages-Mirror.",
"settings.ai.anthropicKey": "Claude API-Schlüssel",
+ "settings.ai.anthropicProxyNote": "Claude-Anfragen aus dem Browser werden über den eigenen Serverless-Proxy von WorldScript an Anthropic weitergeleitet — als einziger Cloud-Anbieter auf diesem Weg. Ihr Schlüssel wird niemals serverseitig gespeichert.",
"settings.ai.connectionFailed": "Verbindung fehlgeschlagen",
"settings.ai.connectionSuccess": "Verbindung erfolgreich",
- "settings.ai.corsRestriction": "CORS-Einschränkung",
+ "settings.ai.corsRestriction": "Hier nicht verfügbar",
"settings.ai.creativity": "KI-Kreativitätsstufe",
"settings.ai.creativityDescription": "Höhere Werte erzeugen unerwartetere Ergebnisse.",
"settings.ai.customModel": "Benutzerdefiniertes Modell",
@@ -245,12 +246,12 @@
"settings.ai.temperature.creative": "2 – Kreativ",
"settings.ai.temperature.precise": "0 – Präzise",
"settings.ai.testConnection": "Verbindung testen",
- "settings.ai.testError.backendProxyRequired": "Claude benötigt einen Backend-Proxy (CORS-Einschränkung)",
"settings.ai.testError.desktopRequired": "Ollama und lokale OpenAI-kompatible Server sind nur in der Desktop-App verfügbar. Browser blockieren direkte Verbindungen von Webseiten zu localhost (CORS und Private Network Access).",
"settings.ai.testError.httpError": "HTTP {{status}}",
"settings.ai.testError.noApiKey": "Kein {{provider}} API-Schlüssel gesetzt",
"settings.ai.testError.noWebgpu": "WebGPU in diesem Browser nicht verfügbar — WebLLM benötigt WebGPU (Chrome/Edge verwenden oder Flags aktivieren).",
"settings.ai.testError.pluginUnavailable": "Lokale Serververnetzung nicht verfügbar: Das Desktop-HTTP-Plugin konnte nicht geladen werden.",
+ "settings.ai.testError.proxyUnavailableStaticHost": "Claude ist auf diesem Deployment nicht verfügbar: Es gibt keinen Serverless-Proxy (GitHub Pages ist rein statisch). Nutzen Sie stattdessen die Desktop-App oder ein Vercel-/Cloudflare-Pages-Deployment.",
"settings.ai.testError.timeout": "Zeitüberschreitung bei der Verbindung ({{url}})",
"settings.ai.testError.unexpected": "Ein unerwarteter Fehler ist aufgetreten. Bitte versuchen Sie es erneut.",
"settings.ai.testError.unknownProvider": "Unbekannter Anbieter",
diff --git a/locales/el/settings.json b/locales/el/settings.json
index 38f18c29f..a45247d97 100644
--- a/locales/el/settings.json
+++ b/locales/el/settings.json
@@ -107,6 +107,7 @@
"settings.ai.anthropicCorsNote": "Το Anthropic Claude δεν μπορεί να κληθεί απευθείας από το πρόγραμμα περιήγησης. Ρυθμίστε έναν διακομιστή μεσολάβησης ή χρησιμοποιήστε την εφαρμογή επιφάνειας εργασίας Tauri για κλήσεις Anthropic.",
"settings.ai.anthropicHint": "Το Anthropic απαιτεί διακομιστή μεσολάβησης ή την εφαρμογή επιφάνειας εργασίας Tauri για κλήσεις API.",
"settings.ai.anthropicKey": "Claude API Key",
+ "settings.ai.anthropicProxyNote": "Claude requests from the browser are relayed through WorldScript's own serverless proxy on the way to Anthropic — the only cloud provider routed this way. Your key is never stored server-side.",
"settings.ai.connectionFailed": "Η σύνδεση απέτυχε",
"settings.ai.connectionSuccess": "Επιτυχής σύνδεση",
"settings.ai.corsRestriction": "Περιορισμός CORS",
@@ -245,12 +246,12 @@
"settings.ai.temperature.creative": "2 – Δημιουργικό",
"settings.ai.temperature.precise": "0 – Ακριβές",
"settings.ai.testConnection": "Δοκιμαστική σύνδεση",
- "settings.ai.testError.backendProxyRequired": "Το Claude απαιτεί διακομιστή μεσολάβησης backend (περιορισμός CORS)",
"settings.ai.testError.desktopRequired": "Το Ollama και οι τοπικοί διακομιστές συμβατοί με OpenAI είναι διαθέσιμοι μόνο στην εφαρμογή επιφάνειας εργασίας. Οι φυλλομετρητές αποκλείουν άμεσες συνδέσεις από ιστοσελίδες προς το localhost (CORS και Private Network Access).",
"settings.ai.testError.httpError": "HTTP {{status}}",
"settings.ai.testError.noApiKey": "Δεν έχει οριστεί κλειδί API {{provider}}",
"settings.ai.testError.noWebgpu": "Το WebGPU δεν είναι διαθέσιμο σε αυτό το πρόγραμμα περιήγησης — το WebLLM απαιτεί WebGPU (δοκιμάστε Chrome/Edge ή ενεργοποιήστε τις σημαίες).",
"settings.ai.testError.pluginUnavailable": "Το δίκτυο τοπικού διακομιστή δεν είναι διαθέσιμο: η πρόσθετη λειτουργία HTTP επιφάνειας εργασίας απέτυχε να φορτωθεί.",
+ "settings.ai.testError.proxyUnavailableStaticHost": "Claude is not available on this deployment: it has no serverless proxy (GitHub Pages is static-only). Use the desktop app, or a Vercel/Cloudflare Pages deployment instead.",
"settings.ai.testError.timeout": "Λήξη χρονικού ορίου σύνδεσης ({{url}})",
"settings.ai.testError.unexpected": "Παρουσιάστηκε ένα μη αναμενόμενο σφάλμα. Δοκιμάστε ξανά.",
"settings.ai.testError.unknownProvider": "Άγνωστος πάροχος",
diff --git a/locales/en/settings.json b/locales/en/settings.json
index 62d549968..8d55ca5d0 100644
--- a/locales/en/settings.json
+++ b/locales/en/settings.json
@@ -104,12 +104,13 @@
"settings.ai.adaptive.vramTier": "GPU VRAM tier",
"settings.ai.adaptive.warmedModels": "Loaded models",
"settings.ai.adaptive.warmedModelsHint": "Models ready in memory for fast inference",
- "settings.ai.anthropicCorsNote": "Anthropic Claude cannot be called directly from the browser. Use the Tauri desktop app for Anthropic calls, or wait for the upcoming proxy server support.",
- "settings.ai.anthropicHint": "Anthropic requires the Tauri desktop app, or a proxy server (coming soon) for browser API calls.",
+ "settings.ai.anthropicCorsNote": "Claude isn't available on this deployment: GitHub Pages is static-only and can't run WorldScript's serverless proxy. Use the desktop app, or a Vercel/Cloudflare Pages deployment, instead.",
+ "settings.ai.anthropicHint": "Claude works in the desktop app (native), and in the browser on Vercel/Cloudflare Pages deployments (via WorldScript's own serverless proxy) — not on the static GitHub Pages mirror.",
"settings.ai.anthropicKey": "Claude API Key",
+ "settings.ai.anthropicProxyNote": "Claude requests from the browser are relayed through WorldScript's own serverless proxy on the way to Anthropic — the only cloud provider routed this way. Your key is never stored server-side.",
"settings.ai.connectionFailed": "Connection failed",
"settings.ai.connectionSuccess": "Connection successful",
- "settings.ai.corsRestriction": "CORS Restriction",
+ "settings.ai.corsRestriction": "Not Available Here",
"settings.ai.creativity": "AI Creativity Level",
"settings.ai.creativityDescription": "Higher values produce more unexpected results.",
"settings.ai.customModel": "Custom Model",
@@ -245,12 +246,12 @@
"settings.ai.temperature.creative": "2 – Creative",
"settings.ai.temperature.precise": "0 – Precise",
"settings.ai.testConnection": "Test connection",
- "settings.ai.testError.backendProxyRequired": "Claude requires a backend proxy (CORS restriction)",
"settings.ai.testError.desktopRequired": "Ollama and local OpenAI-compatible servers are only available in the desktop app. Browsers block direct connections from web pages to localhost (CORS and Private Network Access).",
"settings.ai.testError.httpError": "HTTP {{status}}",
"settings.ai.testError.noApiKey": "No {{provider}} API key set",
"settings.ai.testError.noWebgpu": "WebGPU unavailable in this browser — WebLLM needs WebGPU (try Chrome/Edge or enable flags).",
"settings.ai.testError.pluginUnavailable": "Local server networking is unavailable: the desktop HTTP plugin failed to load.",
+ "settings.ai.testError.proxyUnavailableStaticHost": "Claude is not available on this deployment: it has no serverless proxy (GitHub Pages is static-only). Use the desktop app, or a Vercel/Cloudflare Pages deployment instead.",
"settings.ai.testError.timeout": "Connection timed out ({{url}})",
"settings.ai.testError.unexpected": "An unexpected error occurred. Please try again.",
"settings.ai.testError.unknownProvider": "Unknown provider",
diff --git a/locales/es/settings.json b/locales/es/settings.json
index 0611d0772..2294e9242 100644
--- a/locales/es/settings.json
+++ b/locales/es/settings.json
@@ -104,12 +104,13 @@
"settings.ai.adaptive.vramTier": "Nivel de VRAM GPU",
"settings.ai.adaptive.warmedModels": "Modelos cargados",
"settings.ai.adaptive.warmedModelsHint": "Modelos en memoria listos para inferencia rápida",
- "settings.ai.anthropicCorsNote": "Anthropic Claude no puede llamarse directamente desde el navegador. Usa la aplicación de escritorio Tauri para llamadas de Anthropic, o espera el próximo soporte de servidor proxy.",
- "settings.ai.anthropicHint": "Anthropic requiere la aplicación de escritorio Tauri, o un servidor proxy (próximamente) para llamadas desde el navegador.",
+ "settings.ai.anthropicCorsNote": "Claude no está disponible en este despliegue: GitHub Pages es solo estático y no puede ejecutar el proxy serverless de WorldScript. Usa la app de escritorio, o un despliegue en Vercel/Cloudflare Pages, en su lugar.",
+ "settings.ai.anthropicHint": "Claude funciona en la app de escritorio (nativo), y en el navegador en despliegues de Vercel/Cloudflare Pages (a través del proxy serverless propio de WorldScript) — no en el mirror estático de GitHub Pages.",
"settings.ai.anthropicKey": "Clave API de Claude",
+ "settings.ai.anthropicProxyNote": "Las solicitudes de Claude desde el navegador se transmiten a través del propio proxy serverless de WorldScript camino a Anthropic — el único proveedor de nube enrutado así. Tu clave nunca se almacena en el servidor.",
"settings.ai.connectionFailed": "Conexión fallida",
"settings.ai.connectionSuccess": "Conexión exitosa",
- "settings.ai.corsRestriction": "Restricción CORS",
+ "settings.ai.corsRestriction": "No disponible aquí",
"settings.ai.creativity": "Nivel de creatividad de la IA",
"settings.ai.creativityDescription": "Los valores más altos producen resultados más inesperados.",
"settings.ai.customModel": "Modelo personalizado",
@@ -245,12 +246,12 @@
"settings.ai.temperature.creative": "2 – Creativo",
"settings.ai.temperature.precise": "0 – Preciso",
"settings.ai.testConnection": "Probar conexión",
- "settings.ai.testError.backendProxyRequired": "Claude requiere un proxy de backend (restricción CORS)",
"settings.ai.testError.desktopRequired": "Ollama y los servidores locales compatibles con OpenAI solo están disponibles en la aplicación de escritorio. Los navegadores bloquean las conexiones directas desde páginas web a localhost (CORS y Private Network Access).",
"settings.ai.testError.httpError": "HTTP {{status}}",
"settings.ai.testError.noApiKey": "No se ha configurado una clave de API de {{provider}}",
"settings.ai.testError.noWebgpu": "WebGPU no disponible en este navegador — WebLLM necesita WebGPU (prueba Chrome/Edge o activa las opciones).",
"settings.ai.testError.pluginUnavailable": "La red de servidores locales no está disponible: el plugin HTTP de escritorio no se pudo cargar.",
+ "settings.ai.testError.proxyUnavailableStaticHost": "Claude no está disponible en este despliegue: no hay proxy serverless (GitHub Pages es solo estático). Usa la app de escritorio, o un despliegue en Vercel/Cloudflare Pages.",
"settings.ai.testError.timeout": "Tiempo de espera agotado ({{url}})",
"settings.ai.testError.unexpected": "Se produjo un error inesperado. Inténtalo de nuevo.",
"settings.ai.testError.unknownProvider": "Proveedor desconocido",
diff --git a/locales/eu/settings.json b/locales/eu/settings.json
index 1c82d02d8..70ba1ddda 100644
--- a/locales/eu/settings.json
+++ b/locales/eu/settings.json
@@ -107,6 +107,7 @@
"settings.ai.anthropicCorsNote": "Anthropic Claude ezin da zuzenean arakatzailetik deitu. Konfiguratu proxy zerbitzari bat edo erabili Tauri mahaigaineko aplikazioa Anthropic deietarako.",
"settings.ai.anthropicHint": "Anthropic-ek proxy zerbitzari bat edo Tauri mahaigaineko aplikazioa behar du API deiak egiteko.",
"settings.ai.anthropicKey": "Claude API Key",
+ "settings.ai.anthropicProxyNote": "Claude requests from the browser are relayed through WorldScript's own serverless proxy on the way to Anthropic — the only cloud provider routed this way. Your key is never stored server-side.",
"settings.ai.connectionFailed": "Konexioak huts egin du",
"settings.ai.connectionSuccess": "Konexioa arrakastatsua da",
"settings.ai.corsRestriction": "CORS Murrizketa",
@@ -245,12 +246,12 @@
"settings.ai.temperature.creative": "2 – Sormena",
"settings.ai.temperature.precise": "0 – Zehatza",
"settings.ai.testConnection": "Probatu konexioa",
- "settings.ai.testError.backendProxyRequired": "Claude-k backend proxy bat behar du (CORS murrizketa)",
"settings.ai.testError.desktopRequired": "Ollama eta OpenAI-rekin bateragarriak diren zerbitzari lokalak mahaigaineko aplikazioan bakarrik daude erabilgarri. Nabigatzaileek web orrialdeetatik localhost-era zuzeneko konexioak blokeatzen dituzte (CORS eta Private Network Access).",
"settings.ai.testError.httpError": "HTTP {{status}}",
"settings.ai.testError.noApiKey": "Ez da {{provider}} API gakorik ezarri",
"settings.ai.testError.noWebgpu": "WebGPU ez dago erabilgarri nabigatzaile honetan — WebLLM-ek WebGPU behar du (probatu Chrome/Edge edo gaitu banderak).",
"settings.ai.testError.pluginUnavailable": "Zerbitzari lokalaren sarea ez dago erabilgarri: mahaigaineko HTTP pluginak huts egin du kargatzean.",
+ "settings.ai.testError.proxyUnavailableStaticHost": "Claude is not available on this deployment: it has no serverless proxy (GitHub Pages is static-only). Use the desktop app, or a Vercel/Cloudflare Pages deployment instead.",
"settings.ai.testError.timeout": "Konexioak denbora-muga gainditu du ({{url}})",
"settings.ai.testError.unexpected": "Ustekabeko errorea gertatu da. Saiatu berriro.",
"settings.ai.testError.unknownProvider": "Hornitzaile ezezaguna",
diff --git a/locales/fa/settings.json b/locales/fa/settings.json
index f37baa187..939eca267 100644
--- a/locales/fa/settings.json
+++ b/locales/fa/settings.json
@@ -107,6 +107,7 @@
"settings.ai.anthropicCorsNote": "Anthropic Claude را نمی توان مستقیماً از مرورگر فراخوانی کرد. یک سرور پراکسی راه اندازی کنید یا از برنامه دسکتاپ Tauri برای تماس های Anthropic استفاده کنید.",
"settings.ai.anthropicHint": "Anthropic به یک سرور پراکسی یا برنامه دسکتاپ Tauri برای تماسهای API نیاز دارد.",
"settings.ai.anthropicKey": "Claude API Key",
+ "settings.ai.anthropicProxyNote": "Claude requests from the browser are relayed through WorldScript's own serverless proxy on the way to Anthropic — the only cloud provider routed this way. Your key is never stored server-side.",
"settings.ai.connectionFailed": "اتصال ناموفق بود",
"settings.ai.connectionSuccess": "اتصال با موفقیت انجام شد",
"settings.ai.corsRestriction": "محدودیت CORS",
@@ -245,12 +246,12 @@
"settings.ai.temperature.creative": "2- خلاق",
"settings.ai.temperature.precise": "0 - دقیق",
"settings.ai.testConnection": "تست اتصال",
- "settings.ai.testError.backendProxyRequired": "Claude به یک پراکسی بکاند نیاز دارد (محدودیت CORS)",
"settings.ai.testError.desktopRequired": "Ollama و سرورهای محلی سازگار با OpenAI فقط در برنامه دسکتاپ در دسترس هستند. مرورگرها اتصالات مستقیم از صفحات وب به localhost را مسدود میکنند (CORS و Private Network Access).",
"settings.ai.testError.httpError": "HTTP {{status}}",
"settings.ai.testError.noApiKey": "کلید API {{provider}} تنظیم نشده است",
"settings.ai.testError.noWebgpu": "WebGPU در این مرورگر در دسترس نیست — WebLLM به WebGPU نیاز دارد (Chrome/Edge را امتحان کنید یا پرچمها را فعال کنید).",
"settings.ai.testError.pluginUnavailable": "شبکهسازی سرور محلی در دسترس نیست: بارگذاری افزونه HTTP دسکتاپ ناموفق بود.",
+ "settings.ai.testError.proxyUnavailableStaticHost": "Claude is not available on this deployment: it has no serverless proxy (GitHub Pages is static-only). Use the desktop app, or a Vercel/Cloudflare Pages deployment instead.",
"settings.ai.testError.timeout": "زمان اتصال به پایان رسید ({{url}})",
"settings.ai.testError.unexpected": "خطای غیرمنتظرهای رخ داد. لطفاً دوباره امتحان کنید.",
"settings.ai.testError.unknownProvider": "ارائهدهنده ناشناخته",
diff --git a/locales/fi/settings.json b/locales/fi/settings.json
index f645a1124..047ecfe1c 100644
--- a/locales/fi/settings.json
+++ b/locales/fi/settings.json
@@ -107,6 +107,7 @@
"settings.ai.anthropicCorsNote": "Anthropic Claudea ei voi kutsua suoraan selaimesta. Määritä välityspalvelin tai käytä Tauri-työpöytäsovellusta antrooppisiin puheluihin.",
"settings.ai.anthropicHint": "Anthropic vaatii välityspalvelimen tai Tauri-työpöytäsovelluksen API-kutsuja varten.",
"settings.ai.anthropicKey": "Claude API Key",
+ "settings.ai.anthropicProxyNote": "Claude requests from the browser are relayed through WorldScript's own serverless proxy on the way to Anthropic — the only cloud provider routed this way. Your key is never stored server-side.",
"settings.ai.connectionFailed": "Yhteys epäonnistui",
"settings.ai.connectionSuccess": "Yhteys onnistui",
"settings.ai.corsRestriction": "CORS-rajoitus",
@@ -245,12 +246,12 @@
"settings.ai.temperature.creative": "2 – Luova",
"settings.ai.temperature.precise": "0 – Tarkka",
"settings.ai.testConnection": "Testaa yhteys",
- "settings.ai.testError.backendProxyRequired": "Claude vaatii taustajärjestelmän välityspalvelimen (CORS-rajoitus)",
"settings.ai.testError.desktopRequired": "Ollama ja paikalliset OpenAI-yhteensopivat palvelimet ovat käytettävissä vain työpöytäsovelluksessa. Selaimet estävät suorat yhteydet verkkosivuilta osoitteeseen localhost (CORS ja Private Network Access).",
"settings.ai.testError.httpError": "HTTP {{status}}",
"settings.ai.testError.noApiKey": "{{provider}}-API-avainta ei ole asetettu",
"settings.ai.testError.noWebgpu": "WebGPU ei ole käytettävissä tässä selaimessa — WebLLM tarvitsee WebGPU:n (kokeile Chromea/Edgeä tai ota liput käyttöön).",
"settings.ai.testError.pluginUnavailable": "Paikallisen palvelimen verkko ei ole käytettävissä: työpöydän HTTP-lisäosan lataus epäonnistui.",
+ "settings.ai.testError.proxyUnavailableStaticHost": "Claude is not available on this deployment: it has no serverless proxy (GitHub Pages is static-only). Use the desktop app, or a Vercel/Cloudflare Pages deployment instead.",
"settings.ai.testError.timeout": "Yhteys aikakatkaistiin ({{url}})",
"settings.ai.testError.unexpected": "Tapahtui odottamaton virhe. Yritä uudelleen.",
"settings.ai.testError.unknownProvider": "Tuntematon palveluntarjoaja",
diff --git a/locales/fr/settings.json b/locales/fr/settings.json
index 5299b4bcc..73b1ffe77 100644
--- a/locales/fr/settings.json
+++ b/locales/fr/settings.json
@@ -104,12 +104,13 @@
"settings.ai.adaptive.vramTier": "Niveau VRAM GPU",
"settings.ai.adaptive.warmedModels": "Modèles chargés",
"settings.ai.adaptive.warmedModelsHint": "Modèles en mémoire prêts pour une inférence rapide",
- "settings.ai.anthropicCorsNote": "Anthropic Claude ne peut pas être appelé directement depuis le navigateur. Utilisez l'application de bureau Tauri pour les appels Anthropic, ou attendez le futur support de serveur proxy.",
- "settings.ai.anthropicHint": "Anthropic nécessite l'application de bureau Tauri, ou un serveur proxy (à venir) pour les appels depuis le navigateur.",
+ "settings.ai.anthropicCorsNote": "Claude n'est pas disponible sur ce déploiement : GitHub Pages est purement statique et ne peut pas exécuter le proxy serverless de WorldScript. Utilisez plutôt l'application de bureau, ou un déploiement Vercel/Cloudflare Pages.",
+ "settings.ai.anthropicHint": "Claude fonctionne dans l'application de bureau (natif), et dans le navigateur sur les déploiements Vercel/Cloudflare Pages (via le proxy serverless propre à WorldScript) — pas sur le miroir statique GitHub Pages.",
"settings.ai.anthropicKey": "Clé API Claude",
+ "settings.ai.anthropicProxyNote": "Les requêtes Claude depuis le navigateur transitent par le proxy serverless propre à WorldScript en route vers Anthropic — le seul fournisseur cloud acheminé ainsi. Votre clé n'est jamais stockée côté serveur.",
"settings.ai.connectionFailed": "Connexion échouée",
"settings.ai.connectionSuccess": "Connexion réussie",
- "settings.ai.corsRestriction": "Restriction CORS",
+ "settings.ai.corsRestriction": "Indisponible ici",
"settings.ai.creativity": "Niveau de créativité de l'IA",
"settings.ai.creativityDescription": "Des valeurs plus élevées produisent des résultats plus inattendus.",
"settings.ai.customModel": "Modèle personnalisé",
@@ -245,12 +246,12 @@
"settings.ai.temperature.creative": "2 – Créatif",
"settings.ai.temperature.precise": "0 – Précis",
"settings.ai.testConnection": "Tester la connexion",
- "settings.ai.testError.backendProxyRequired": "Claude nécessite un proxy backend (restriction CORS)",
"settings.ai.testError.desktopRequired": "Ollama et les serveurs locaux compatibles OpenAI ne sont disponibles que dans l'application de bureau. Les navigateurs bloquent les connexions directes des pages web vers localhost (CORS et Private Network Access).",
"settings.ai.testError.httpError": "HTTP {{status}}",
"settings.ai.testError.noApiKey": "Aucune clé API {{provider}} définie",
"settings.ai.testError.noWebgpu": "WebGPU indisponible dans ce navigateur — WebLLM nécessite WebGPU (essayez Chrome/Edge ou activez les indicateurs).",
"settings.ai.testError.pluginUnavailable": "Le réseau des serveurs locaux est indisponible : le plugin HTTP du bureau n'a pas pu se charger.",
+ "settings.ai.testError.proxyUnavailableStaticHost": "Claude n'est pas disponible sur ce déploiement : aucun proxy serverless n'existe (GitHub Pages est purement statique). Utilisez l'application de bureau, ou un déploiement Vercel/Cloudflare Pages.",
"settings.ai.testError.timeout": "Délai de connexion dépassé ({{url}})",
"settings.ai.testError.unexpected": "Une erreur inattendue s'est produite. Veuillez réessayer.",
"settings.ai.testError.unknownProvider": "Fournisseur inconnu",
diff --git a/locales/he/settings.json b/locales/he/settings.json
index 121addbba..03d3816b2 100644
--- a/locales/he/settings.json
+++ b/locales/he/settings.json
@@ -107,6 +107,7 @@
"settings.ai.anthropicCorsNote": "לא ניתן לקרוא ל‑Anthropic Claude ישירות מהדפדפן. הקימו שרת פרוקסי או השתמשו באפליקציית Tauri לשולחן העבודה לקריאות Anthropic.",
"settings.ai.anthropicHint": "Anthropic דורש שרת פרוקסי או את אפליקציית Tauri לשולחן העבודה לקריאות API.",
"settings.ai.anthropicKey": "Claude API Key",
+ "settings.ai.anthropicProxyNote": "Claude requests from the browser are relayed through WorldScript's own serverless proxy on the way to Anthropic — the only cloud provider routed this way. Your key is never stored server-side.",
"settings.ai.connectionFailed": "החיבור נכשל",
"settings.ai.connectionSuccess": "החיבור הצליח",
"settings.ai.corsRestriction": "הגבלת CORS",
@@ -245,12 +246,12 @@
"settings.ai.temperature.creative": "2 – יצירתי",
"settings.ai.temperature.precise": "0 – מדויק",
"settings.ai.testConnection": "בדיקת חיבור",
- "settings.ai.testError.backendProxyRequired": "Claude דורש שרת proxy (הגבלת CORS)",
"settings.ai.testError.desktopRequired": "Ollama ושרתים מקומיים תואמי OpenAI זמינים רק באפליקציית שולחן העבודה. דפדפנים חוסמים חיבורים ישירים מדפי אינטרנט אל localhost (CORS ו-Private Network Access).",
"settings.ai.testError.httpError": "HTTP {{status}}",
"settings.ai.testError.noApiKey": "לא הוגדר מפתח API עבור {{provider}}",
"settings.ai.testError.noWebgpu": "WebGPU אינו זמין בדפדפן זה — WebLLM דורש WebGPU (נסה Chrome/Edge או הפעל דגלים).",
"settings.ai.testError.pluginUnavailable": "רשת השרת המקומי אינה זמינה: תוסף ה-HTTP של שולחן העבודה נכשל בטעינה.",
+ "settings.ai.testError.proxyUnavailableStaticHost": "Claude is not available on this deployment: it has no serverless proxy (GitHub Pages is static-only). Use the desktop app, or a Vercel/Cloudflare Pages deployment instead.",
"settings.ai.testError.timeout": "פסק הזמן של החיבור פג ({{url}})",
"settings.ai.testError.unexpected": "אירעה שגיאה בלתי צפויה. נסה שוב.",
"settings.ai.testError.unknownProvider": "ספק לא ידוע",
diff --git a/locales/hu/settings.json b/locales/hu/settings.json
index 78d2feaea..b16c82f19 100644
--- a/locales/hu/settings.json
+++ b/locales/hu/settings.json
@@ -107,6 +107,7 @@
"settings.ai.anthropicCorsNote": "Az Anthropic Claude nem hívható közvetlenül a böngészőből. Hozzon létre egy proxyszervert, vagy használja a Tauri asztali alkalmazást antropikus hívásokhoz.",
"settings.ai.anthropicHint": "Az Anthropic proxykiszolgálót vagy a Tauri asztali alkalmazást igényel API-hívásokhoz.",
"settings.ai.anthropicKey": "Claude API Key",
+ "settings.ai.anthropicProxyNote": "Claude requests from the browser are relayed through WorldScript's own serverless proxy on the way to Anthropic — the only cloud provider routed this way. Your key is never stored server-side.",
"settings.ai.connectionFailed": "A csatlakozás sikertelen",
"settings.ai.connectionSuccess": "Sikeres csatlakozás",
"settings.ai.corsRestriction": "CORS korlátozás",
@@ -245,12 +246,12 @@
"settings.ai.temperature.creative": "2 – Kreatív",
"settings.ai.temperature.precise": "0 – Pontos",
"settings.ai.testConnection": "Kapcsolat tesztelése",
- "settings.ai.testError.backendProxyRequired": "A Claude háttérproxyt igényel (CORS-korlátozás)",
"settings.ai.testError.desktopRequired": "Az Ollama és a helyi OpenAI-kompatibilis kiszolgálók csak az asztali alkalmazásban érhetők el. A böngészők blokkolják a weboldalak közvetlen kapcsolatait a localhosttal (CORS és Private Network Access).",
"settings.ai.testError.httpError": "HTTP {{status}}",
"settings.ai.testError.noApiKey": "Nincs beállítva {{provider}} API-kulcs",
"settings.ai.testError.noWebgpu": "A WebGPU nem érhető el ebben a böngészőben — a WebLLM-nek WebGPU-ra van szüksége (próbáld a Chrome-ot/Edge-et, vagy engedélyezd a jelzőket).",
"settings.ai.testError.pluginUnavailable": "A helyi kiszolgálóhálózat nem érhető el: az asztali HTTP bővítmény betöltése sikertelen.",
+ "settings.ai.testError.proxyUnavailableStaticHost": "Claude is not available on this deployment: it has no serverless proxy (GitHub Pages is static-only). Use the desktop app, or a Vercel/Cloudflare Pages deployment instead.",
"settings.ai.testError.timeout": "A kapcsolat túllépte az időkorlátot ({{url}})",
"settings.ai.testError.unexpected": "Váratlan hiba történt. Kérjük, próbálja újra.",
"settings.ai.testError.unknownProvider": "Ismeretlen szolgáltató",
diff --git a/locales/is/settings.json b/locales/is/settings.json
index e3cd1f31a..6c09180d1 100644
--- a/locales/is/settings.json
+++ b/locales/is/settings.json
@@ -107,6 +107,7 @@
"settings.ai.anthropicCorsNote": "Ekki er hægt að hringja í Anthropic Claude beint úr vafranum. Settu upp proxy-miðlara eða notaðu Tauri skrifborðsforritið fyrir antropísk símtöl.",
"settings.ai.anthropicHint": "Anthropic krefst proxy-þjóns eða Tauri skrifborðsforritsins fyrir API símtöl.",
"settings.ai.anthropicKey": "Claude API Key",
+ "settings.ai.anthropicProxyNote": "Claude requests from the browser are relayed through WorldScript's own serverless proxy on the way to Anthropic — the only cloud provider routed this way. Your key is never stored server-side.",
"settings.ai.connectionFailed": "Tenging mistókst",
"settings.ai.connectionSuccess": "Tenging tókst",
"settings.ai.corsRestriction": "CORS takmörkun",
@@ -245,12 +246,12 @@
"settings.ai.temperature.creative": "2 - Skapandi",
"settings.ai.temperature.precise": "0 - Nákvæmt",
"settings.ai.testConnection": "Prófaðu tengingu",
- "settings.ai.testError.backendProxyRequired": "Claude þarfnast bakenda-milliþjóns (CORS-takmörkun)",
"settings.ai.testError.desktopRequired": "Ollama og staðbundnir OpenAI-samhæfir netþjónar eru aðeins tiltækir í skjáborðsforritinu. Vafrar loka fyrir bein tengsl frá vefsíðum við localhost (CORS og Private Network Access).",
"settings.ai.testError.httpError": "HTTP {{status}}",
"settings.ai.testError.noApiKey": "Enginn {{provider}} API lykill stilltur",
"settings.ai.testError.noWebgpu": "WebGPU er ekki tiltækt í þessum vafra — WebLLM þarfnast WebGPU (prófaðu Chrome/Edge eða virkjaðu flögg).",
"settings.ai.testError.pluginUnavailable": "Staðbundið netþjónasamband ekki tiltækt: ekki tókst að hlaða HTTP-viðbót skjáborðs.",
+ "settings.ai.testError.proxyUnavailableStaticHost": "Claude is not available on this deployment: it has no serverless proxy (GitHub Pages is static-only). Use the desktop app, or a Vercel/Cloudflare Pages deployment instead.",
"settings.ai.testError.timeout": "Tengingin féll á tíma ({{url}})",
"settings.ai.testError.unexpected": "Óvænt villa kom upp. Reyndu aftur.",
"settings.ai.testError.unknownProvider": "Óþekktur veitandi",
diff --git a/locales/it/settings.json b/locales/it/settings.json
index 2a9bea920..c89b1f85b 100644
--- a/locales/it/settings.json
+++ b/locales/it/settings.json
@@ -104,12 +104,13 @@
"settings.ai.adaptive.vramTier": "Livello VRAM GPU",
"settings.ai.adaptive.warmedModels": "Modelli caricati",
"settings.ai.adaptive.warmedModelsHint": "Modelli in memoria pronti per inferenza rapida",
- "settings.ai.anthropicCorsNote": "Anthropic Claude non può essere chiamato direttamente dal browser. Usa l'app desktop Tauri per le chiamate Anthropic, oppure attendi il supporto proxy in arrivo.",
- "settings.ai.anthropicHint": "Anthropic richiede l'app desktop Tauri, oppure un server proxy (in arrivo) per le chiamate dal browser.",
+ "settings.ai.anthropicCorsNote": "Claude non è disponibile su questo deployment: GitHub Pages è solo statico e non può eseguire il proxy serverless di WorldScript. Usa l'app desktop, oppure un deployment Vercel/Cloudflare Pages.",
+ "settings.ai.anthropicHint": "Claude funziona nell'app desktop (nativo) e nel browser sui deployment Vercel/Cloudflare Pages (tramite il proxy serverless proprio di WorldScript) — non sul mirror statico GitHub Pages.",
"settings.ai.anthropicKey": "Chiave API Claude",
+ "settings.ai.anthropicProxyNote": "Le richieste Claude dal browser vengono inoltrate tramite il proxy serverless proprio di WorldScript verso Anthropic — l'unico provider cloud instradato così. La tua chiave non viene mai memorizzata lato server.",
"settings.ai.connectionFailed": "Connessione fallita",
"settings.ai.connectionSuccess": "Connessione riuscita",
- "settings.ai.corsRestriction": "Restrizione CORS",
+ "settings.ai.corsRestriction": "Non disponibile qui",
"settings.ai.creativity": "Livello di creatività dell'IA",
"settings.ai.creativityDescription": "Valori più alti producono risultati più inaspettati.",
"settings.ai.customModel": "Modello personalizzato",
@@ -245,12 +246,12 @@
"settings.ai.temperature.creative": "2 – Creativo",
"settings.ai.temperature.precise": "0 – Preciso",
"settings.ai.testConnection": "Testa la connessione",
- "settings.ai.testError.backendProxyRequired": "Claude richiede un proxy backend (restrizione CORS)",
"settings.ai.testError.desktopRequired": "Ollama e i server locali compatibili con OpenAI sono disponibili solo nell'app desktop. I browser bloccano le connessioni dirette dalle pagine web a localhost (CORS e Private Network Access).",
"settings.ai.testError.httpError": "HTTP {{status}}",
"settings.ai.testError.noApiKey": "Nessuna chiave API {{provider}} impostata",
"settings.ai.testError.noWebgpu": "WebGPU non disponibile in questo browser — WebLLM richiede WebGPU (prova Chrome/Edge o abilita i flag).",
"settings.ai.testError.pluginUnavailable": "Rete dei server locali non disponibile: il plugin HTTP desktop non è stato caricato.",
+ "settings.ai.testError.proxyUnavailableStaticHost": "Claude non è disponibile su questo deployment: non esiste un proxy serverless (GitHub Pages è solo statico). Usa l'app desktop, oppure un deployment Vercel/Cloudflare Pages.",
"settings.ai.testError.timeout": "Connessione scaduta ({{url}})",
"settings.ai.testError.unexpected": "Si è verificato un errore imprevisto. Riprova.",
"settings.ai.testError.unknownProvider": "Provider sconosciuto",
diff --git a/locales/ja/settings.json b/locales/ja/settings.json
index d8f94cee3..70e9fe69d 100644
--- a/locales/ja/settings.json
+++ b/locales/ja/settings.json
@@ -107,6 +107,7 @@
"settings.ai.anthropicCorsNote": "Anthropic Claude をブラウザから直接呼び出すことはできません。プロキシ サーバーをセットアップするか、Anthropic 呼び出し用の Tauri デスクトップ アプリを使用します。",
"settings.ai.anthropicHint": "Anthropic では、API 呼び出しのためにプロキシ サーバーまたは Tauri デスクトップ アプリが必要です。",
"settings.ai.anthropicKey": "Claude API Key",
+ "settings.ai.anthropicProxyNote": "Claude requests from the browser are relayed through WorldScript's own serverless proxy on the way to Anthropic — the only cloud provider routed this way. Your key is never stored server-side.",
"settings.ai.connectionFailed": "接続に失敗しました",
"settings.ai.connectionSuccess": "接続成功",
"settings.ai.corsRestriction": "CORS の制限",
@@ -245,12 +246,12 @@
"settings.ai.temperature.creative": "2 – クリエイティブ",
"settings.ai.temperature.precise": "0 – 正確",
"settings.ai.testConnection": "テスト接続",
- "settings.ai.testError.backendProxyRequired": "Claude にはバックエンドプロキシが必要です(CORS 制限)",
"settings.ai.testError.desktopRequired": "Ollama およびローカルの OpenAI 互換サーバーはデスクトップアプリでのみ利用できます。ブラウザは Web ページから localhost への直接接続をブロックします(CORS および Private Network Access)。",
"settings.ai.testError.httpError": "HTTP {{status}}",
"settings.ai.testError.noApiKey": "{{provider}} の API キーが設定されていません",
"settings.ai.testError.noWebgpu": "このブラウザでは WebGPU が利用できません — WebLLM には WebGPU が必要です(Chrome/Edge を試すかフラグを有効にしてください)。",
"settings.ai.testError.pluginUnavailable": "ローカルサーバーへの接続が利用できません: デスクトップ HTTP プラグインの読み込みに失敗しました。",
+ "settings.ai.testError.proxyUnavailableStaticHost": "Claude is not available on this deployment: it has no serverless proxy (GitHub Pages is static-only). Use the desktop app, or a Vercel/Cloudflare Pages deployment instead.",
"settings.ai.testError.timeout": "接続がタイムアウトしました ({{url}})",
"settings.ai.testError.unexpected": "予期しないエラーが発生しました。もう一度お試しください。",
"settings.ai.testError.unknownProvider": "不明なプロバイダー",
diff --git a/locales/ko/settings.json b/locales/ko/settings.json
index d77159986..a4d18d87a 100644
--- a/locales/ko/settings.json
+++ b/locales/ko/settings.json
@@ -107,6 +107,7 @@
"settings.ai.anthropicCorsNote": "Anthropic Claude는 브라우저에서 직접 호출할 수 없습니다. Anthropic 통화를 위해 프록시 서버를 설정하거나 Tauri 데스크톱 앱을 사용하세요.",
"settings.ai.anthropicHint": "Anthropic에는 API 호출을 위해 프록시 서버 또는 Tauri 데스크톱 앱이 필요합니다.",
"settings.ai.anthropicKey": "Claude API Key",
+ "settings.ai.anthropicProxyNote": "Claude requests from the browser are relayed through WorldScript's own serverless proxy on the way to Anthropic — the only cloud provider routed this way. Your key is never stored server-side.",
"settings.ai.connectionFailed": "연결 실패",
"settings.ai.connectionSuccess": "연결 성공",
"settings.ai.corsRestriction": "CORS 제한",
@@ -245,12 +246,12 @@
"settings.ai.temperature.creative": "2 – 창의적",
"settings.ai.temperature.precise": "0 – 정확함",
"settings.ai.testConnection": "연결 테스트",
- "settings.ai.testError.backendProxyRequired": "Claude에는 백엔드 프록시가 필요합니다(CORS 제한)",
"settings.ai.testError.desktopRequired": "Ollama 및 로컬 OpenAI 호환 서버는 데스크톱 앱에서만 사용할 수 있습니다. 브라우저는 웹 페이지에서 localhost로의 직접 연결을 차단합니다(CORS 및 Private Network Access).",
"settings.ai.testError.httpError": "HTTP {{status}}",
"settings.ai.testError.noApiKey": "{{provider}} API 키가 설정되지 않았습니다",
"settings.ai.testError.noWebgpu": "이 브라우저에서는 WebGPU를 사용할 수 없습니다 — WebLLM에는 WebGPU가 필요합니다(Chrome/Edge를 사용하거나 플래그를 활성화하세요).",
"settings.ai.testError.pluginUnavailable": "로컬 서버 네트워킹을 사용할 수 없습니다: 데스크톱 HTTP 플러그인을 불러오지 못했습니다.",
+ "settings.ai.testError.proxyUnavailableStaticHost": "Claude is not available on this deployment: it has no serverless proxy (GitHub Pages is static-only). Use the desktop app, or a Vercel/Cloudflare Pages deployment instead.",
"settings.ai.testError.timeout": "연결 시간이 초과되었습니다 ({{url}})",
"settings.ai.testError.unexpected": "예기치 않은 오류가 발생했습니다. 다시 시도해 주세요.",
"settings.ai.testError.unknownProvider": "알 수 없는 제공자",
diff --git a/locales/pt/settings.json b/locales/pt/settings.json
index 8a3378801..e7b709471 100644
--- a/locales/pt/settings.json
+++ b/locales/pt/settings.json
@@ -107,6 +107,7 @@
"settings.ai.anthropicCorsNote": "O Antrópico Claude não pode ser chamado diretamente do navegador. Configure um servidor proxy ou use o aplicativo de desktop Tauri para chamadas Antrópicas.",
"settings.ai.anthropicHint": "A Anthropic requer um servidor proxy ou o aplicativo de desktop Tauri para chamadas de API.",
"settings.ai.anthropicKey": "Claude API Key",
+ "settings.ai.anthropicProxyNote": "Claude requests from the browser are relayed through WorldScript's own serverless proxy on the way to Anthropic — the only cloud provider routed this way. Your key is never stored server-side.",
"settings.ai.connectionFailed": "Falha na conexão",
"settings.ai.connectionSuccess": "Conexão bem-sucedida",
"settings.ai.corsRestriction": "Restrição CORS",
@@ -245,12 +246,12 @@
"settings.ai.temperature.creative": "2 – Criativo",
"settings.ai.temperature.precise": "0 – Preciso",
"settings.ai.testConnection": "Conexão de teste",
- "settings.ai.testError.backendProxyRequired": "O Claude requer um proxy de backend (restrição CORS)",
"settings.ai.testError.desktopRequired": "O Ollama e os servidores locais compatíveis com OpenAI só estão disponíveis na aplicação de ambiente de trabalho. Os navegadores bloqueiam ligações diretas de páginas web ao localhost (CORS e Private Network Access).",
"settings.ai.testError.httpError": "HTTP {{status}}",
"settings.ai.testError.noApiKey": "Nenhuma chave de API {{provider}} definida",
"settings.ai.testError.noWebgpu": "WebGPU indisponível neste navegador — o WebLLM necessita de WebGPU (experimente o Chrome/Edge ou ative as flags).",
"settings.ai.testError.pluginUnavailable": "A rede do servidor local não está disponível: o plugin HTTP de ambiente de trabalho falhou ao carregar.",
+ "settings.ai.testError.proxyUnavailableStaticHost": "Claude is not available on this deployment: it has no serverless proxy (GitHub Pages is static-only). Use the desktop app, or a Vercel/Cloudflare Pages deployment instead.",
"settings.ai.testError.timeout": "Tempo limite de conexão excedido ({{url}})",
"settings.ai.testError.unexpected": "Ocorreu um erro inesperado. Tente novamente.",
"settings.ai.testError.unknownProvider": "Fornecedor desconhecido",
diff --git a/locales/ru/settings.json b/locales/ru/settings.json
index def9ddf72..294701b0c 100644
--- a/locales/ru/settings.json
+++ b/locales/ru/settings.json
@@ -107,6 +107,7 @@
"settings.ai.anthropicCorsNote": "Anthropic Claude нельзя вызвать напрямую из браузера. Настройте прокси-сервер или используйте настольное приложение Tauri для вызовов Anthropic.",
"settings.ai.anthropicHint": "Для вызовов API Anthropic требуется прокси-сервер или настольное приложение Tauri.",
"settings.ai.anthropicKey": "Claude API Key",
+ "settings.ai.anthropicProxyNote": "Claude requests from the browser are relayed through WorldScript's own serverless proxy on the way to Anthropic — the only cloud provider routed this way. Your key is never stored server-side.",
"settings.ai.connectionFailed": "Соединение не удалось",
"settings.ai.connectionSuccess": "Соединение успешно",
"settings.ai.corsRestriction": "Ограничение CORS",
@@ -245,12 +246,12 @@
"settings.ai.temperature.creative": "2 – Креатив",
"settings.ai.temperature.precise": "0 – Точный",
"settings.ai.testConnection": "Тестовое соединение",
- "settings.ai.testError.backendProxyRequired": "Claude требует backend-прокси (ограничение CORS)",
"settings.ai.testError.desktopRequired": "Ollama и локальные серверы, совместимые с OpenAI, доступны только в настольном приложении. Браузеры блокируют прямые подключения с веб-страниц к localhost (CORS и Private Network Access).",
"settings.ai.testError.httpError": "HTTP {{status}}",
"settings.ai.testError.noApiKey": "Не задан ключ API {{provider}}",
"settings.ai.testError.noWebgpu": "WebGPU недоступен в этом браузере — WebLLM требует WebGPU (попробуйте Chrome/Edge или включите флаги).",
"settings.ai.testError.pluginUnavailable": "Сеть локального сервера недоступна: не удалось загрузить настольный HTTP-плагин.",
+ "settings.ai.testError.proxyUnavailableStaticHost": "Claude is not available on this deployment: it has no serverless proxy (GitHub Pages is static-only). Use the desktop app, or a Vercel/Cloudflare Pages deployment instead.",
"settings.ai.testError.timeout": "Тайм-аут соединения ({{url}})",
"settings.ai.testError.unexpected": "Произошла непредвиденная ошибка. Повторите попытку.",
"settings.ai.testError.unknownProvider": "Неизвестный провайдер",
diff --git a/locales/sv/settings.json b/locales/sv/settings.json
index 8daafddde..f31966340 100644
--- a/locales/sv/settings.json
+++ b/locales/sv/settings.json
@@ -107,6 +107,7 @@
"settings.ai.anthropicCorsNote": "Antropiska Claude kan inte ringas direkt från webbläsaren. Konfigurera en proxyserver eller använd Tauri-skrivbordsappen för antropiska samtal.",
"settings.ai.anthropicHint": "Anthropic kräver en proxyserver eller Tauri-skrivbordsappen för API-anrop.",
"settings.ai.anthropicKey": "Claude API Key",
+ "settings.ai.anthropicProxyNote": "Claude requests from the browser are relayed through WorldScript's own serverless proxy on the way to Anthropic — the only cloud provider routed this way. Your key is never stored server-side.",
"settings.ai.connectionFailed": "Anslutning misslyckades",
"settings.ai.connectionSuccess": "Anslutningen lyckades",
"settings.ai.corsRestriction": "CORS-begränsning",
@@ -245,12 +246,12 @@
"settings.ai.temperature.creative": "2 – Kreativt",
"settings.ai.temperature.precise": "0 – Exakt",
"settings.ai.testConnection": "Testa anslutningen",
- "settings.ai.testError.backendProxyRequired": "Claude kräver en backend-proxy (CORS-begränsning)",
"settings.ai.testError.desktopRequired": "Ollama och lokala OpenAI-kompatibla servrar är endast tillgängliga i skrivbordsappen. Webbläsare blockerar direkta anslutningar från webbsidor till localhost (CORS och Private Network Access).",
"settings.ai.testError.httpError": "HTTP {{status}}",
"settings.ai.testError.noApiKey": "Ingen {{provider}}-API-nyckel angiven",
"settings.ai.testError.noWebgpu": "WebGPU är inte tillgängligt i den här webbläsaren — WebLLM behöver WebGPU (prova Chrome/Edge eller aktivera flaggor).",
"settings.ai.testError.pluginUnavailable": "Lokal servernätverksanslutning ej tillgänglig: skrivbordets HTTP-plugin misslyckades med att läsas in.",
+ "settings.ai.testError.proxyUnavailableStaticHost": "Claude is not available on this deployment: it has no serverless proxy (GitHub Pages is static-only). Use the desktop app, or a Vercel/Cloudflare Pages deployment instead.",
"settings.ai.testError.timeout": "Anslutningen fick timeout ({{url}})",
"settings.ai.testError.unexpected": "Ett oväntat fel inträffade. Försök igen.",
"settings.ai.testError.unknownProvider": "Okänd leverantör",
diff --git a/locales/zh/settings.json b/locales/zh/settings.json
index e78001d3f..5e73bb661 100644
--- a/locales/zh/settings.json
+++ b/locales/zh/settings.json
@@ -107,6 +107,7 @@
"settings.ai.anthropicCorsNote": "Anthropic Claude 无法直接从浏览器调用。设置代理服务器或使用 Tauri 桌面应用程序进行 Anthropic 呼叫。",
"settings.ai.anthropicHint": "Anthropic 需要代理服务器或 Tauri 桌面应用程序来进行 API 调用。",
"settings.ai.anthropicKey": "Claude API Key",
+ "settings.ai.anthropicProxyNote": "Claude requests from the browser are relayed through WorldScript's own serverless proxy on the way to Anthropic — the only cloud provider routed this way. Your key is never stored server-side.",
"settings.ai.connectionFailed": "连接失败",
"settings.ai.connectionSuccess": "连接成功",
"settings.ai.corsRestriction": "CORS 限制",
@@ -245,12 +246,12 @@
"settings.ai.temperature.creative": "2 – 创意",
"settings.ai.temperature.precise": "0 – 精确",
"settings.ai.testConnection": "测试连接",
- "settings.ai.testError.backendProxyRequired": "Claude 需要后端代理(CORS 限制)",
"settings.ai.testError.desktopRequired": "Ollama 和本地 OpenAI 兼容服务器仅在桌面应用中可用。浏览器会阻止网页直接连接到 localhost(CORS 和 Private Network Access)。",
"settings.ai.testError.httpError": "HTTP {{status}}",
"settings.ai.testError.noApiKey": "未设置 {{provider}} API 密钥",
"settings.ai.testError.noWebgpu": "此浏览器不支持 WebGPU — WebLLM 需要 WebGPU(请尝试 Chrome/Edge 或启用相关标志)。",
"settings.ai.testError.pluginUnavailable": "本地服务器网络不可用:桌面 HTTP 插件加载失败。",
+ "settings.ai.testError.proxyUnavailableStaticHost": "Claude is not available on this deployment: it has no serverless proxy (GitHub Pages is static-only). Use the desktop app, or a Vercel/Cloudflare Pages deployment instead.",
"settings.ai.testError.timeout": "连接超时 ({{url}})",
"settings.ai.testError.unexpected": "发生意外错误。请重试。",
"settings.ai.testError.unknownProvider": "未知的提供商",
diff --git a/public/locales/ar/bundle.json b/public/locales/ar/bundle.json
index a1eb08c49..eca55f3b0 100644
--- a/public/locales/ar/bundle.json
+++ b/public/locales/ar/bundle.json
@@ -1725,6 +1725,7 @@
"settings.ai.anthropicCorsNote": "لا يمكن استدعاء Anthropic Claude مباشرة من المتصفّح. أعدّ خادم وكيل أو استخدم تطبيق Tauri لسطح المكتب لاستدعاءات Anthropic.",
"settings.ai.anthropicHint": "يتطلب Anthropic خادم وكيل أو تطبيق Tauri لسطح المكتب لاستدعاءات API.",
"settings.ai.anthropicKey": "Claude API Key",
+ "settings.ai.anthropicProxyNote": "Claude requests from the browser are relayed through WorldScript's own serverless proxy on the way to Anthropic — the only cloud provider routed this way. Your key is never stored server-side.",
"settings.ai.connectionFailed": "فشل الاتصال",
"settings.ai.connectionSuccess": "نجح الاتصال",
"settings.ai.corsRestriction": "قيد CORS",
@@ -1863,12 +1864,12 @@
"settings.ai.temperature.creative": "2 – إبداعي",
"settings.ai.temperature.precise": "0 – دقيق",
"settings.ai.testConnection": "اختبار الاتصال",
- "settings.ai.testError.backendProxyRequired": "يتطلب Claude وكيل خلفية (قيود CORS)",
"settings.ai.testError.desktopRequired": "تتوفر Ollama والخوادم المحلية المتوافقة مع OpenAI فقط في تطبيق سطح المكتب. تحظر المتصفحات الاتصالات المباشرة من صفحات الويب بـ localhost (CORS وPrivate Network Access).",
"settings.ai.testError.httpError": "HTTP {{status}}",
"settings.ai.testError.noApiKey": "لم يتم تعيين مفتاح API لـ {{provider}}",
"settings.ai.testError.noWebgpu": "WebGPU غير متوفر في هذا المتصفح — يحتاج WebLLM إلى WebGPU (جرّب Chrome/Edge أو فعّل العلامات).",
"settings.ai.testError.pluginUnavailable": "شبكة الخادم المحلي غير متوفرة: فشل تحميل مكوّن HTTP لسطح المكتب.",
+ "settings.ai.testError.proxyUnavailableStaticHost": "Claude is not available on this deployment: it has no serverless proxy (GitHub Pages is static-only). Use the desktop app, or a Vercel/Cloudflare Pages deployment instead.",
"settings.ai.testError.timeout": "انتهت مهلة الاتصال ({{url}})",
"settings.ai.testError.unexpected": "حدث خطأ غير متوقع. يرجى المحاولة مرة أخرى.",
"settings.ai.testError.unknownProvider": "مزوّد غير معروف",
diff --git a/public/locales/de/bundle.json b/public/locales/de/bundle.json
index 718ae2755..8ab5e0596 100644
--- a/public/locales/de/bundle.json
+++ b/public/locales/de/bundle.json
@@ -1722,12 +1722,13 @@
"settings.ai.adaptive.vramTier": "GPU-VRAM-Stufe",
"settings.ai.adaptive.warmedModels": "Geladene Modelle",
"settings.ai.adaptive.warmedModelsHint": "Modelle im Speicher für schnelle Inferenz bereit",
- "settings.ai.anthropicCorsNote": "Anthropic Claude kann nicht direkt aus dem Browser aufgerufen werden. Nutze die Tauri-Desktop-App für Anthropic-Aufrufe oder warte auf die geplante Proxy-Server-Unterstützung.",
- "settings.ai.anthropicHint": "Anthropic erfordert die Tauri-Desktop-App oder einen Proxy-Server (in Vorbereitung) für Aufrufe im Browser.",
+ "settings.ai.anthropicCorsNote": "Claude ist auf diesem Deployment nicht verfügbar: GitHub Pages ist rein statisch und kann den Serverless-Proxy von WorldScript nicht ausführen. Nutzen Sie stattdessen die Desktop-App oder ein Vercel-/Cloudflare-Pages-Deployment.",
+ "settings.ai.anthropicHint": "Claude funktioniert in der Desktop-App (nativ) sowie im Browser auf Vercel-/Cloudflare-Pages-Deployments (über den eigenen Serverless-Proxy von WorldScript) — nicht auf dem statischen GitHub-Pages-Mirror.",
"settings.ai.anthropicKey": "Claude API-Schlüssel",
+ "settings.ai.anthropicProxyNote": "Claude-Anfragen aus dem Browser werden über den eigenen Serverless-Proxy von WorldScript an Anthropic weitergeleitet — als einziger Cloud-Anbieter auf diesem Weg. Ihr Schlüssel wird niemals serverseitig gespeichert.",
"settings.ai.connectionFailed": "Verbindung fehlgeschlagen",
"settings.ai.connectionSuccess": "Verbindung erfolgreich",
- "settings.ai.corsRestriction": "CORS-Einschränkung",
+ "settings.ai.corsRestriction": "Hier nicht verfügbar",
"settings.ai.creativity": "KI-Kreativitätsstufe",
"settings.ai.creativityDescription": "Höhere Werte erzeugen unerwartetere Ergebnisse.",
"settings.ai.customModel": "Benutzerdefiniertes Modell",
@@ -1863,12 +1864,12 @@
"settings.ai.temperature.creative": "2 – Kreativ",
"settings.ai.temperature.precise": "0 – Präzise",
"settings.ai.testConnection": "Verbindung testen",
- "settings.ai.testError.backendProxyRequired": "Claude benötigt einen Backend-Proxy (CORS-Einschränkung)",
"settings.ai.testError.desktopRequired": "Ollama und lokale OpenAI-kompatible Server sind nur in der Desktop-App verfügbar. Browser blockieren direkte Verbindungen von Webseiten zu localhost (CORS und Private Network Access).",
"settings.ai.testError.httpError": "HTTP {{status}}",
"settings.ai.testError.noApiKey": "Kein {{provider}} API-Schlüssel gesetzt",
"settings.ai.testError.noWebgpu": "WebGPU in diesem Browser nicht verfügbar — WebLLM benötigt WebGPU (Chrome/Edge verwenden oder Flags aktivieren).",
"settings.ai.testError.pluginUnavailable": "Lokale Serververnetzung nicht verfügbar: Das Desktop-HTTP-Plugin konnte nicht geladen werden.",
+ "settings.ai.testError.proxyUnavailableStaticHost": "Claude ist auf diesem Deployment nicht verfügbar: Es gibt keinen Serverless-Proxy (GitHub Pages ist rein statisch). Nutzen Sie stattdessen die Desktop-App oder ein Vercel-/Cloudflare-Pages-Deployment.",
"settings.ai.testError.timeout": "Zeitüberschreitung bei der Verbindung ({{url}})",
"settings.ai.testError.unexpected": "Ein unerwarteter Fehler ist aufgetreten. Bitte versuchen Sie es erneut.",
"settings.ai.testError.unknownProvider": "Unbekannter Anbieter",
diff --git a/public/locales/el/bundle.json b/public/locales/el/bundle.json
index eb44ec1c3..6a5f2e782 100644
--- a/public/locales/el/bundle.json
+++ b/public/locales/el/bundle.json
@@ -1725,6 +1725,7 @@
"settings.ai.anthropicCorsNote": "Το Anthropic Claude δεν μπορεί να κληθεί απευθείας από το πρόγραμμα περιήγησης. Ρυθμίστε έναν διακομιστή μεσολάβησης ή χρησιμοποιήστε την εφαρμογή επιφάνειας εργασίας Tauri για κλήσεις Anthropic.",
"settings.ai.anthropicHint": "Το Anthropic απαιτεί διακομιστή μεσολάβησης ή την εφαρμογή επιφάνειας εργασίας Tauri για κλήσεις API.",
"settings.ai.anthropicKey": "Claude API Key",
+ "settings.ai.anthropicProxyNote": "Claude requests from the browser are relayed through WorldScript's own serverless proxy on the way to Anthropic — the only cloud provider routed this way. Your key is never stored server-side.",
"settings.ai.connectionFailed": "Η σύνδεση απέτυχε",
"settings.ai.connectionSuccess": "Επιτυχής σύνδεση",
"settings.ai.corsRestriction": "Περιορισμός CORS",
@@ -1863,12 +1864,12 @@
"settings.ai.temperature.creative": "2 – Δημιουργικό",
"settings.ai.temperature.precise": "0 – Ακριβές",
"settings.ai.testConnection": "Δοκιμαστική σύνδεση",
- "settings.ai.testError.backendProxyRequired": "Το Claude απαιτεί διακομιστή μεσολάβησης backend (περιορισμός CORS)",
"settings.ai.testError.desktopRequired": "Το Ollama και οι τοπικοί διακομιστές συμβατοί με OpenAI είναι διαθέσιμοι μόνο στην εφαρμογή επιφάνειας εργασίας. Οι φυλλομετρητές αποκλείουν άμεσες συνδέσεις από ιστοσελίδες προς το localhost (CORS και Private Network Access).",
"settings.ai.testError.httpError": "HTTP {{status}}",
"settings.ai.testError.noApiKey": "Δεν έχει οριστεί κλειδί API {{provider}}",
"settings.ai.testError.noWebgpu": "Το WebGPU δεν είναι διαθέσιμο σε αυτό το πρόγραμμα περιήγησης — το WebLLM απαιτεί WebGPU (δοκιμάστε Chrome/Edge ή ενεργοποιήστε τις σημαίες).",
"settings.ai.testError.pluginUnavailable": "Το δίκτυο τοπικού διακομιστή δεν είναι διαθέσιμο: η πρόσθετη λειτουργία HTTP επιφάνειας εργασίας απέτυχε να φορτωθεί.",
+ "settings.ai.testError.proxyUnavailableStaticHost": "Claude is not available on this deployment: it has no serverless proxy (GitHub Pages is static-only). Use the desktop app, or a Vercel/Cloudflare Pages deployment instead.",
"settings.ai.testError.timeout": "Λήξη χρονικού ορίου σύνδεσης ({{url}})",
"settings.ai.testError.unexpected": "Παρουσιάστηκε ένα μη αναμενόμενο σφάλμα. Δοκιμάστε ξανά.",
"settings.ai.testError.unknownProvider": "Άγνωστος πάροχος",
diff --git a/public/locales/en/bundle.json b/public/locales/en/bundle.json
index 7a5dc6fe7..ef7590cc3 100644
--- a/public/locales/en/bundle.json
+++ b/public/locales/en/bundle.json
@@ -1722,12 +1722,13 @@
"settings.ai.adaptive.vramTier": "GPU VRAM tier",
"settings.ai.adaptive.warmedModels": "Loaded models",
"settings.ai.adaptive.warmedModelsHint": "Models ready in memory for fast inference",
- "settings.ai.anthropicCorsNote": "Anthropic Claude cannot be called directly from the browser. Use the Tauri desktop app for Anthropic calls, or wait for the upcoming proxy server support.",
- "settings.ai.anthropicHint": "Anthropic requires the Tauri desktop app, or a proxy server (coming soon) for browser API calls.",
+ "settings.ai.anthropicCorsNote": "Claude isn't available on this deployment: GitHub Pages is static-only and can't run WorldScript's serverless proxy. Use the desktop app, or a Vercel/Cloudflare Pages deployment, instead.",
+ "settings.ai.anthropicHint": "Claude works in the desktop app (native), and in the browser on Vercel/Cloudflare Pages deployments (via WorldScript's own serverless proxy) — not on the static GitHub Pages mirror.",
"settings.ai.anthropicKey": "Claude API Key",
+ "settings.ai.anthropicProxyNote": "Claude requests from the browser are relayed through WorldScript's own serverless proxy on the way to Anthropic — the only cloud provider routed this way. Your key is never stored server-side.",
"settings.ai.connectionFailed": "Connection failed",
"settings.ai.connectionSuccess": "Connection successful",
- "settings.ai.corsRestriction": "CORS Restriction",
+ "settings.ai.corsRestriction": "Not Available Here",
"settings.ai.creativity": "AI Creativity Level",
"settings.ai.creativityDescription": "Higher values produce more unexpected results.",
"settings.ai.customModel": "Custom Model",
@@ -1863,12 +1864,12 @@
"settings.ai.temperature.creative": "2 – Creative",
"settings.ai.temperature.precise": "0 – Precise",
"settings.ai.testConnection": "Test connection",
- "settings.ai.testError.backendProxyRequired": "Claude requires a backend proxy (CORS restriction)",
"settings.ai.testError.desktopRequired": "Ollama and local OpenAI-compatible servers are only available in the desktop app. Browsers block direct connections from web pages to localhost (CORS and Private Network Access).",
"settings.ai.testError.httpError": "HTTP {{status}}",
"settings.ai.testError.noApiKey": "No {{provider}} API key set",
"settings.ai.testError.noWebgpu": "WebGPU unavailable in this browser — WebLLM needs WebGPU (try Chrome/Edge or enable flags).",
"settings.ai.testError.pluginUnavailable": "Local server networking is unavailable: the desktop HTTP plugin failed to load.",
+ "settings.ai.testError.proxyUnavailableStaticHost": "Claude is not available on this deployment: it has no serverless proxy (GitHub Pages is static-only). Use the desktop app, or a Vercel/Cloudflare Pages deployment instead.",
"settings.ai.testError.timeout": "Connection timed out ({{url}})",
"settings.ai.testError.unexpected": "An unexpected error occurred. Please try again.",
"settings.ai.testError.unknownProvider": "Unknown provider",
diff --git a/public/locales/es/bundle.json b/public/locales/es/bundle.json
index 736445000..c764b7311 100644
--- a/public/locales/es/bundle.json
+++ b/public/locales/es/bundle.json
@@ -1722,12 +1722,13 @@
"settings.ai.adaptive.vramTier": "Nivel de VRAM GPU",
"settings.ai.adaptive.warmedModels": "Modelos cargados",
"settings.ai.adaptive.warmedModelsHint": "Modelos en memoria listos para inferencia rápida",
- "settings.ai.anthropicCorsNote": "Anthropic Claude no puede llamarse directamente desde el navegador. Usa la aplicación de escritorio Tauri para llamadas de Anthropic, o espera el próximo soporte de servidor proxy.",
- "settings.ai.anthropicHint": "Anthropic requiere la aplicación de escritorio Tauri, o un servidor proxy (próximamente) para llamadas desde el navegador.",
+ "settings.ai.anthropicCorsNote": "Claude no está disponible en este despliegue: GitHub Pages es solo estático y no puede ejecutar el proxy serverless de WorldScript. Usa la app de escritorio, o un despliegue en Vercel/Cloudflare Pages, en su lugar.",
+ "settings.ai.anthropicHint": "Claude funciona en la app de escritorio (nativo), y en el navegador en despliegues de Vercel/Cloudflare Pages (a través del proxy serverless propio de WorldScript) — no en el mirror estático de GitHub Pages.",
"settings.ai.anthropicKey": "Clave API de Claude",
+ "settings.ai.anthropicProxyNote": "Las solicitudes de Claude desde el navegador se transmiten a través del propio proxy serverless de WorldScript camino a Anthropic — el único proveedor de nube enrutado así. Tu clave nunca se almacena en el servidor.",
"settings.ai.connectionFailed": "Conexión fallida",
"settings.ai.connectionSuccess": "Conexión exitosa",
- "settings.ai.corsRestriction": "Restricción CORS",
+ "settings.ai.corsRestriction": "No disponible aquí",
"settings.ai.creativity": "Nivel de creatividad de la IA",
"settings.ai.creativityDescription": "Los valores más altos producen resultados más inesperados.",
"settings.ai.customModel": "Modelo personalizado",
@@ -1863,12 +1864,12 @@
"settings.ai.temperature.creative": "2 – Creativo",
"settings.ai.temperature.precise": "0 – Preciso",
"settings.ai.testConnection": "Probar conexión",
- "settings.ai.testError.backendProxyRequired": "Claude requiere un proxy de backend (restricción CORS)",
"settings.ai.testError.desktopRequired": "Ollama y los servidores locales compatibles con OpenAI solo están disponibles en la aplicación de escritorio. Los navegadores bloquean las conexiones directas desde páginas web a localhost (CORS y Private Network Access).",
"settings.ai.testError.httpError": "HTTP {{status}}",
"settings.ai.testError.noApiKey": "No se ha configurado una clave de API de {{provider}}",
"settings.ai.testError.noWebgpu": "WebGPU no disponible en este navegador — WebLLM necesita WebGPU (prueba Chrome/Edge o activa las opciones).",
"settings.ai.testError.pluginUnavailable": "La red de servidores locales no está disponible: el plugin HTTP de escritorio no se pudo cargar.",
+ "settings.ai.testError.proxyUnavailableStaticHost": "Claude no está disponible en este despliegue: no hay proxy serverless (GitHub Pages es solo estático). Usa la app de escritorio, o un despliegue en Vercel/Cloudflare Pages.",
"settings.ai.testError.timeout": "Tiempo de espera agotado ({{url}})",
"settings.ai.testError.unexpected": "Se produjo un error inesperado. Inténtalo de nuevo.",
"settings.ai.testError.unknownProvider": "Proveedor desconocido",
diff --git a/public/locales/eu/bundle.json b/public/locales/eu/bundle.json
index 1be517aac..97f7f204d 100644
--- a/public/locales/eu/bundle.json
+++ b/public/locales/eu/bundle.json
@@ -1725,6 +1725,7 @@
"settings.ai.anthropicCorsNote": "Anthropic Claude ezin da zuzenean arakatzailetik deitu. Konfiguratu proxy zerbitzari bat edo erabili Tauri mahaigaineko aplikazioa Anthropic deietarako.",
"settings.ai.anthropicHint": "Anthropic-ek proxy zerbitzari bat edo Tauri mahaigaineko aplikazioa behar du API deiak egiteko.",
"settings.ai.anthropicKey": "Claude API Key",
+ "settings.ai.anthropicProxyNote": "Claude requests from the browser are relayed through WorldScript's own serverless proxy on the way to Anthropic — the only cloud provider routed this way. Your key is never stored server-side.",
"settings.ai.connectionFailed": "Konexioak huts egin du",
"settings.ai.connectionSuccess": "Konexioa arrakastatsua da",
"settings.ai.corsRestriction": "CORS Murrizketa",
@@ -1863,12 +1864,12 @@
"settings.ai.temperature.creative": "2 – Sormena",
"settings.ai.temperature.precise": "0 – Zehatza",
"settings.ai.testConnection": "Probatu konexioa",
- "settings.ai.testError.backendProxyRequired": "Claude-k backend proxy bat behar du (CORS murrizketa)",
"settings.ai.testError.desktopRequired": "Ollama eta OpenAI-rekin bateragarriak diren zerbitzari lokalak mahaigaineko aplikazioan bakarrik daude erabilgarri. Nabigatzaileek web orrialdeetatik localhost-era zuzeneko konexioak blokeatzen dituzte (CORS eta Private Network Access).",
"settings.ai.testError.httpError": "HTTP {{status}}",
"settings.ai.testError.noApiKey": "Ez da {{provider}} API gakorik ezarri",
"settings.ai.testError.noWebgpu": "WebGPU ez dago erabilgarri nabigatzaile honetan — WebLLM-ek WebGPU behar du (probatu Chrome/Edge edo gaitu banderak).",
"settings.ai.testError.pluginUnavailable": "Zerbitzari lokalaren sarea ez dago erabilgarri: mahaigaineko HTTP pluginak huts egin du kargatzean.",
+ "settings.ai.testError.proxyUnavailableStaticHost": "Claude is not available on this deployment: it has no serverless proxy (GitHub Pages is static-only). Use the desktop app, or a Vercel/Cloudflare Pages deployment instead.",
"settings.ai.testError.timeout": "Konexioak denbora-muga gainditu du ({{url}})",
"settings.ai.testError.unexpected": "Ustekabeko errorea gertatu da. Saiatu berriro.",
"settings.ai.testError.unknownProvider": "Hornitzaile ezezaguna",
diff --git a/public/locales/fa/bundle.json b/public/locales/fa/bundle.json
index 547aa5b4b..b41e76554 100644
--- a/public/locales/fa/bundle.json
+++ b/public/locales/fa/bundle.json
@@ -1725,6 +1725,7 @@
"settings.ai.anthropicCorsNote": "Anthropic Claude را نمی توان مستقیماً از مرورگر فراخوانی کرد. یک سرور پراکسی راه اندازی کنید یا از برنامه دسکتاپ Tauri برای تماس های Anthropic استفاده کنید.",
"settings.ai.anthropicHint": "Anthropic به یک سرور پراکسی یا برنامه دسکتاپ Tauri برای تماسهای API نیاز دارد.",
"settings.ai.anthropicKey": "Claude API Key",
+ "settings.ai.anthropicProxyNote": "Claude requests from the browser are relayed through WorldScript's own serverless proxy on the way to Anthropic — the only cloud provider routed this way. Your key is never stored server-side.",
"settings.ai.connectionFailed": "اتصال ناموفق بود",
"settings.ai.connectionSuccess": "اتصال با موفقیت انجام شد",
"settings.ai.corsRestriction": "محدودیت CORS",
@@ -1863,12 +1864,12 @@
"settings.ai.temperature.creative": "2- خلاق",
"settings.ai.temperature.precise": "0 - دقیق",
"settings.ai.testConnection": "تست اتصال",
- "settings.ai.testError.backendProxyRequired": "Claude به یک پراکسی بکاند نیاز دارد (محدودیت CORS)",
"settings.ai.testError.desktopRequired": "Ollama و سرورهای محلی سازگار با OpenAI فقط در برنامه دسکتاپ در دسترس هستند. مرورگرها اتصالات مستقیم از صفحات وب به localhost را مسدود میکنند (CORS و Private Network Access).",
"settings.ai.testError.httpError": "HTTP {{status}}",
"settings.ai.testError.noApiKey": "کلید API {{provider}} تنظیم نشده است",
"settings.ai.testError.noWebgpu": "WebGPU در این مرورگر در دسترس نیست — WebLLM به WebGPU نیاز دارد (Chrome/Edge را امتحان کنید یا پرچمها را فعال کنید).",
"settings.ai.testError.pluginUnavailable": "شبکهسازی سرور محلی در دسترس نیست: بارگذاری افزونه HTTP دسکتاپ ناموفق بود.",
+ "settings.ai.testError.proxyUnavailableStaticHost": "Claude is not available on this deployment: it has no serverless proxy (GitHub Pages is static-only). Use the desktop app, or a Vercel/Cloudflare Pages deployment instead.",
"settings.ai.testError.timeout": "زمان اتصال به پایان رسید ({{url}})",
"settings.ai.testError.unexpected": "خطای غیرمنتظرهای رخ داد. لطفاً دوباره امتحان کنید.",
"settings.ai.testError.unknownProvider": "ارائهدهنده ناشناخته",
diff --git a/public/locales/fi/bundle.json b/public/locales/fi/bundle.json
index 6a8469e35..eaf95b8f4 100644
--- a/public/locales/fi/bundle.json
+++ b/public/locales/fi/bundle.json
@@ -1725,6 +1725,7 @@
"settings.ai.anthropicCorsNote": "Anthropic Claudea ei voi kutsua suoraan selaimesta. Määritä välityspalvelin tai käytä Tauri-työpöytäsovellusta antrooppisiin puheluihin.",
"settings.ai.anthropicHint": "Anthropic vaatii välityspalvelimen tai Tauri-työpöytäsovelluksen API-kutsuja varten.",
"settings.ai.anthropicKey": "Claude API Key",
+ "settings.ai.anthropicProxyNote": "Claude requests from the browser are relayed through WorldScript's own serverless proxy on the way to Anthropic — the only cloud provider routed this way. Your key is never stored server-side.",
"settings.ai.connectionFailed": "Yhteys epäonnistui",
"settings.ai.connectionSuccess": "Yhteys onnistui",
"settings.ai.corsRestriction": "CORS-rajoitus",
@@ -1863,12 +1864,12 @@
"settings.ai.temperature.creative": "2 – Luova",
"settings.ai.temperature.precise": "0 – Tarkka",
"settings.ai.testConnection": "Testaa yhteys",
- "settings.ai.testError.backendProxyRequired": "Claude vaatii taustajärjestelmän välityspalvelimen (CORS-rajoitus)",
"settings.ai.testError.desktopRequired": "Ollama ja paikalliset OpenAI-yhteensopivat palvelimet ovat käytettävissä vain työpöytäsovelluksessa. Selaimet estävät suorat yhteydet verkkosivuilta osoitteeseen localhost (CORS ja Private Network Access).",
"settings.ai.testError.httpError": "HTTP {{status}}",
"settings.ai.testError.noApiKey": "{{provider}}-API-avainta ei ole asetettu",
"settings.ai.testError.noWebgpu": "WebGPU ei ole käytettävissä tässä selaimessa — WebLLM tarvitsee WebGPU:n (kokeile Chromea/Edgeä tai ota liput käyttöön).",
"settings.ai.testError.pluginUnavailable": "Paikallisen palvelimen verkko ei ole käytettävissä: työpöydän HTTP-lisäosan lataus epäonnistui.",
+ "settings.ai.testError.proxyUnavailableStaticHost": "Claude is not available on this deployment: it has no serverless proxy (GitHub Pages is static-only). Use the desktop app, or a Vercel/Cloudflare Pages deployment instead.",
"settings.ai.testError.timeout": "Yhteys aikakatkaistiin ({{url}})",
"settings.ai.testError.unexpected": "Tapahtui odottamaton virhe. Yritä uudelleen.",
"settings.ai.testError.unknownProvider": "Tuntematon palveluntarjoaja",
diff --git a/public/locales/fr/bundle.json b/public/locales/fr/bundle.json
index 4dd238b0a..5e276175e 100644
--- a/public/locales/fr/bundle.json
+++ b/public/locales/fr/bundle.json
@@ -1722,12 +1722,13 @@
"settings.ai.adaptive.vramTier": "Niveau VRAM GPU",
"settings.ai.adaptive.warmedModels": "Modèles chargés",
"settings.ai.adaptive.warmedModelsHint": "Modèles en mémoire prêts pour une inférence rapide",
- "settings.ai.anthropicCorsNote": "Anthropic Claude ne peut pas être appelé directement depuis le navigateur. Utilisez l'application de bureau Tauri pour les appels Anthropic, ou attendez le futur support de serveur proxy.",
- "settings.ai.anthropicHint": "Anthropic nécessite l'application de bureau Tauri, ou un serveur proxy (à venir) pour les appels depuis le navigateur.",
+ "settings.ai.anthropicCorsNote": "Claude n'est pas disponible sur ce déploiement : GitHub Pages est purement statique et ne peut pas exécuter le proxy serverless de WorldScript. Utilisez plutôt l'application de bureau, ou un déploiement Vercel/Cloudflare Pages.",
+ "settings.ai.anthropicHint": "Claude fonctionne dans l'application de bureau (natif), et dans le navigateur sur les déploiements Vercel/Cloudflare Pages (via le proxy serverless propre à WorldScript) — pas sur le miroir statique GitHub Pages.",
"settings.ai.anthropicKey": "Clé API Claude",
+ "settings.ai.anthropicProxyNote": "Les requêtes Claude depuis le navigateur transitent par le proxy serverless propre à WorldScript en route vers Anthropic — le seul fournisseur cloud acheminé ainsi. Votre clé n'est jamais stockée côté serveur.",
"settings.ai.connectionFailed": "Connexion échouée",
"settings.ai.connectionSuccess": "Connexion réussie",
- "settings.ai.corsRestriction": "Restriction CORS",
+ "settings.ai.corsRestriction": "Indisponible ici",
"settings.ai.creativity": "Niveau de créativité de l'IA",
"settings.ai.creativityDescription": "Des valeurs plus élevées produisent des résultats plus inattendus.",
"settings.ai.customModel": "Modèle personnalisé",
@@ -1863,12 +1864,12 @@
"settings.ai.temperature.creative": "2 – Créatif",
"settings.ai.temperature.precise": "0 – Précis",
"settings.ai.testConnection": "Tester la connexion",
- "settings.ai.testError.backendProxyRequired": "Claude nécessite un proxy backend (restriction CORS)",
"settings.ai.testError.desktopRequired": "Ollama et les serveurs locaux compatibles OpenAI ne sont disponibles que dans l'application de bureau. Les navigateurs bloquent les connexions directes des pages web vers localhost (CORS et Private Network Access).",
"settings.ai.testError.httpError": "HTTP {{status}}",
"settings.ai.testError.noApiKey": "Aucune clé API {{provider}} définie",
"settings.ai.testError.noWebgpu": "WebGPU indisponible dans ce navigateur — WebLLM nécessite WebGPU (essayez Chrome/Edge ou activez les indicateurs).",
"settings.ai.testError.pluginUnavailable": "Le réseau des serveurs locaux est indisponible : le plugin HTTP du bureau n'a pas pu se charger.",
+ "settings.ai.testError.proxyUnavailableStaticHost": "Claude n'est pas disponible sur ce déploiement : aucun proxy serverless n'existe (GitHub Pages est purement statique). Utilisez l'application de bureau, ou un déploiement Vercel/Cloudflare Pages.",
"settings.ai.testError.timeout": "Délai de connexion dépassé ({{url}})",
"settings.ai.testError.unexpected": "Une erreur inattendue s'est produite. Veuillez réessayer.",
"settings.ai.testError.unknownProvider": "Fournisseur inconnu",
diff --git a/public/locales/he/bundle.json b/public/locales/he/bundle.json
index 336c9a680..d3d7fe320 100644
--- a/public/locales/he/bundle.json
+++ b/public/locales/he/bundle.json
@@ -1725,6 +1725,7 @@
"settings.ai.anthropicCorsNote": "לא ניתן לקרוא ל‑Anthropic Claude ישירות מהדפדפן. הקימו שרת פרוקסי או השתמשו באפליקציית Tauri לשולחן העבודה לקריאות Anthropic.",
"settings.ai.anthropicHint": "Anthropic דורש שרת פרוקסי או את אפליקציית Tauri לשולחן העבודה לקריאות API.",
"settings.ai.anthropicKey": "Claude API Key",
+ "settings.ai.anthropicProxyNote": "Claude requests from the browser are relayed through WorldScript's own serverless proxy on the way to Anthropic — the only cloud provider routed this way. Your key is never stored server-side.",
"settings.ai.connectionFailed": "החיבור נכשל",
"settings.ai.connectionSuccess": "החיבור הצליח",
"settings.ai.corsRestriction": "הגבלת CORS",
@@ -1863,12 +1864,12 @@
"settings.ai.temperature.creative": "2 – יצירתי",
"settings.ai.temperature.precise": "0 – מדויק",
"settings.ai.testConnection": "בדיקת חיבור",
- "settings.ai.testError.backendProxyRequired": "Claude דורש שרת proxy (הגבלת CORS)",
"settings.ai.testError.desktopRequired": "Ollama ושרתים מקומיים תואמי OpenAI זמינים רק באפליקציית שולחן העבודה. דפדפנים חוסמים חיבורים ישירים מדפי אינטרנט אל localhost (CORS ו-Private Network Access).",
"settings.ai.testError.httpError": "HTTP {{status}}",
"settings.ai.testError.noApiKey": "לא הוגדר מפתח API עבור {{provider}}",
"settings.ai.testError.noWebgpu": "WebGPU אינו זמין בדפדפן זה — WebLLM דורש WebGPU (נסה Chrome/Edge או הפעל דגלים).",
"settings.ai.testError.pluginUnavailable": "רשת השרת המקומי אינה זמינה: תוסף ה-HTTP של שולחן העבודה נכשל בטעינה.",
+ "settings.ai.testError.proxyUnavailableStaticHost": "Claude is not available on this deployment: it has no serverless proxy (GitHub Pages is static-only). Use the desktop app, or a Vercel/Cloudflare Pages deployment instead.",
"settings.ai.testError.timeout": "פסק הזמן של החיבור פג ({{url}})",
"settings.ai.testError.unexpected": "אירעה שגיאה בלתי צפויה. נסה שוב.",
"settings.ai.testError.unknownProvider": "ספק לא ידוע",
diff --git a/public/locales/hu/bundle.json b/public/locales/hu/bundle.json
index e14393a34..06182a04c 100644
--- a/public/locales/hu/bundle.json
+++ b/public/locales/hu/bundle.json
@@ -1725,6 +1725,7 @@
"settings.ai.anthropicCorsNote": "Az Anthropic Claude nem hívható közvetlenül a böngészőből. Hozzon létre egy proxyszervert, vagy használja a Tauri asztali alkalmazást antropikus hívásokhoz.",
"settings.ai.anthropicHint": "Az Anthropic proxykiszolgálót vagy a Tauri asztali alkalmazást igényel API-hívásokhoz.",
"settings.ai.anthropicKey": "Claude API Key",
+ "settings.ai.anthropicProxyNote": "Claude requests from the browser are relayed through WorldScript's own serverless proxy on the way to Anthropic — the only cloud provider routed this way. Your key is never stored server-side.",
"settings.ai.connectionFailed": "A csatlakozás sikertelen",
"settings.ai.connectionSuccess": "Sikeres csatlakozás",
"settings.ai.corsRestriction": "CORS korlátozás",
@@ -1863,12 +1864,12 @@
"settings.ai.temperature.creative": "2 – Kreatív",
"settings.ai.temperature.precise": "0 – Pontos",
"settings.ai.testConnection": "Kapcsolat tesztelése",
- "settings.ai.testError.backendProxyRequired": "A Claude háttérproxyt igényel (CORS-korlátozás)",
"settings.ai.testError.desktopRequired": "Az Ollama és a helyi OpenAI-kompatibilis kiszolgálók csak az asztali alkalmazásban érhetők el. A böngészők blokkolják a weboldalak közvetlen kapcsolatait a localhosttal (CORS és Private Network Access).",
"settings.ai.testError.httpError": "HTTP {{status}}",
"settings.ai.testError.noApiKey": "Nincs beállítva {{provider}} API-kulcs",
"settings.ai.testError.noWebgpu": "A WebGPU nem érhető el ebben a böngészőben — a WebLLM-nek WebGPU-ra van szüksége (próbáld a Chrome-ot/Edge-et, vagy engedélyezd a jelzőket).",
"settings.ai.testError.pluginUnavailable": "A helyi kiszolgálóhálózat nem érhető el: az asztali HTTP bővítmény betöltése sikertelen.",
+ "settings.ai.testError.proxyUnavailableStaticHost": "Claude is not available on this deployment: it has no serverless proxy (GitHub Pages is static-only). Use the desktop app, or a Vercel/Cloudflare Pages deployment instead.",
"settings.ai.testError.timeout": "A kapcsolat túllépte az időkorlátot ({{url}})",
"settings.ai.testError.unexpected": "Váratlan hiba történt. Kérjük, próbálja újra.",
"settings.ai.testError.unknownProvider": "Ismeretlen szolgáltató",
diff --git a/public/locales/is/bundle.json b/public/locales/is/bundle.json
index f44a44cf4..8235b5321 100644
--- a/public/locales/is/bundle.json
+++ b/public/locales/is/bundle.json
@@ -1725,6 +1725,7 @@
"settings.ai.anthropicCorsNote": "Ekki er hægt að hringja í Anthropic Claude beint úr vafranum. Settu upp proxy-miðlara eða notaðu Tauri skrifborðsforritið fyrir antropísk símtöl.",
"settings.ai.anthropicHint": "Anthropic krefst proxy-þjóns eða Tauri skrifborðsforritsins fyrir API símtöl.",
"settings.ai.anthropicKey": "Claude API Key",
+ "settings.ai.anthropicProxyNote": "Claude requests from the browser are relayed through WorldScript's own serverless proxy on the way to Anthropic — the only cloud provider routed this way. Your key is never stored server-side.",
"settings.ai.connectionFailed": "Tenging mistókst",
"settings.ai.connectionSuccess": "Tenging tókst",
"settings.ai.corsRestriction": "CORS takmörkun",
@@ -1863,12 +1864,12 @@
"settings.ai.temperature.creative": "2 - Skapandi",
"settings.ai.temperature.precise": "0 - Nákvæmt",
"settings.ai.testConnection": "Prófaðu tengingu",
- "settings.ai.testError.backendProxyRequired": "Claude þarfnast bakenda-milliþjóns (CORS-takmörkun)",
"settings.ai.testError.desktopRequired": "Ollama og staðbundnir OpenAI-samhæfir netþjónar eru aðeins tiltækir í skjáborðsforritinu. Vafrar loka fyrir bein tengsl frá vefsíðum við localhost (CORS og Private Network Access).",
"settings.ai.testError.httpError": "HTTP {{status}}",
"settings.ai.testError.noApiKey": "Enginn {{provider}} API lykill stilltur",
"settings.ai.testError.noWebgpu": "WebGPU er ekki tiltækt í þessum vafra — WebLLM þarfnast WebGPU (prófaðu Chrome/Edge eða virkjaðu flögg).",
"settings.ai.testError.pluginUnavailable": "Staðbundið netþjónasamband ekki tiltækt: ekki tókst að hlaða HTTP-viðbót skjáborðs.",
+ "settings.ai.testError.proxyUnavailableStaticHost": "Claude is not available on this deployment: it has no serverless proxy (GitHub Pages is static-only). Use the desktop app, or a Vercel/Cloudflare Pages deployment instead.",
"settings.ai.testError.timeout": "Tengingin féll á tíma ({{url}})",
"settings.ai.testError.unexpected": "Óvænt villa kom upp. Reyndu aftur.",
"settings.ai.testError.unknownProvider": "Óþekktur veitandi",
diff --git a/public/locales/it/bundle.json b/public/locales/it/bundle.json
index 0f30552b1..aac9222bf 100644
--- a/public/locales/it/bundle.json
+++ b/public/locales/it/bundle.json
@@ -1722,12 +1722,13 @@
"settings.ai.adaptive.vramTier": "Livello VRAM GPU",
"settings.ai.adaptive.warmedModels": "Modelli caricati",
"settings.ai.adaptive.warmedModelsHint": "Modelli in memoria pronti per inferenza rapida",
- "settings.ai.anthropicCorsNote": "Anthropic Claude non può essere chiamato direttamente dal browser. Usa l'app desktop Tauri per le chiamate Anthropic, oppure attendi il supporto proxy in arrivo.",
- "settings.ai.anthropicHint": "Anthropic richiede l'app desktop Tauri, oppure un server proxy (in arrivo) per le chiamate dal browser.",
+ "settings.ai.anthropicCorsNote": "Claude non è disponibile su questo deployment: GitHub Pages è solo statico e non può eseguire il proxy serverless di WorldScript. Usa l'app desktop, oppure un deployment Vercel/Cloudflare Pages.",
+ "settings.ai.anthropicHint": "Claude funziona nell'app desktop (nativo) e nel browser sui deployment Vercel/Cloudflare Pages (tramite il proxy serverless proprio di WorldScript) — non sul mirror statico GitHub Pages.",
"settings.ai.anthropicKey": "Chiave API Claude",
+ "settings.ai.anthropicProxyNote": "Le richieste Claude dal browser vengono inoltrate tramite il proxy serverless proprio di WorldScript verso Anthropic — l'unico provider cloud instradato così. La tua chiave non viene mai memorizzata lato server.",
"settings.ai.connectionFailed": "Connessione fallita",
"settings.ai.connectionSuccess": "Connessione riuscita",
- "settings.ai.corsRestriction": "Restrizione CORS",
+ "settings.ai.corsRestriction": "Non disponibile qui",
"settings.ai.creativity": "Livello di creatività dell'IA",
"settings.ai.creativityDescription": "Valori più alti producono risultati più inaspettati.",
"settings.ai.customModel": "Modello personalizzato",
@@ -1863,12 +1864,12 @@
"settings.ai.temperature.creative": "2 – Creativo",
"settings.ai.temperature.precise": "0 – Preciso",
"settings.ai.testConnection": "Testa la connessione",
- "settings.ai.testError.backendProxyRequired": "Claude richiede un proxy backend (restrizione CORS)",
"settings.ai.testError.desktopRequired": "Ollama e i server locali compatibili con OpenAI sono disponibili solo nell'app desktop. I browser bloccano le connessioni dirette dalle pagine web a localhost (CORS e Private Network Access).",
"settings.ai.testError.httpError": "HTTP {{status}}",
"settings.ai.testError.noApiKey": "Nessuna chiave API {{provider}} impostata",
"settings.ai.testError.noWebgpu": "WebGPU non disponibile in questo browser — WebLLM richiede WebGPU (prova Chrome/Edge o abilita i flag).",
"settings.ai.testError.pluginUnavailable": "Rete dei server locali non disponibile: il plugin HTTP desktop non è stato caricato.",
+ "settings.ai.testError.proxyUnavailableStaticHost": "Claude non è disponibile su questo deployment: non esiste un proxy serverless (GitHub Pages è solo statico). Usa l'app desktop, oppure un deployment Vercel/Cloudflare Pages.",
"settings.ai.testError.timeout": "Connessione scaduta ({{url}})",
"settings.ai.testError.unexpected": "Si è verificato un errore imprevisto. Riprova.",
"settings.ai.testError.unknownProvider": "Provider sconosciuto",
diff --git a/public/locales/ja/bundle.json b/public/locales/ja/bundle.json
index 777ee8480..d8b184430 100644
--- a/public/locales/ja/bundle.json
+++ b/public/locales/ja/bundle.json
@@ -1725,6 +1725,7 @@
"settings.ai.anthropicCorsNote": "Anthropic Claude をブラウザから直接呼び出すことはできません。プロキシ サーバーをセットアップするか、Anthropic 呼び出し用の Tauri デスクトップ アプリを使用します。",
"settings.ai.anthropicHint": "Anthropic では、API 呼び出しのためにプロキシ サーバーまたは Tauri デスクトップ アプリが必要です。",
"settings.ai.anthropicKey": "Claude API Key",
+ "settings.ai.anthropicProxyNote": "Claude requests from the browser are relayed through WorldScript's own serverless proxy on the way to Anthropic — the only cloud provider routed this way. Your key is never stored server-side.",
"settings.ai.connectionFailed": "接続に失敗しました",
"settings.ai.connectionSuccess": "接続成功",
"settings.ai.corsRestriction": "CORS の制限",
@@ -1863,12 +1864,12 @@
"settings.ai.temperature.creative": "2 – クリエイティブ",
"settings.ai.temperature.precise": "0 – 正確",
"settings.ai.testConnection": "テスト接続",
- "settings.ai.testError.backendProxyRequired": "Claude にはバックエンドプロキシが必要です(CORS 制限)",
"settings.ai.testError.desktopRequired": "Ollama およびローカルの OpenAI 互換サーバーはデスクトップアプリでのみ利用できます。ブラウザは Web ページから localhost への直接接続をブロックします(CORS および Private Network Access)。",
"settings.ai.testError.httpError": "HTTP {{status}}",
"settings.ai.testError.noApiKey": "{{provider}} の API キーが設定されていません",
"settings.ai.testError.noWebgpu": "このブラウザでは WebGPU が利用できません — WebLLM には WebGPU が必要です(Chrome/Edge を試すかフラグを有効にしてください)。",
"settings.ai.testError.pluginUnavailable": "ローカルサーバーへの接続が利用できません: デスクトップ HTTP プラグインの読み込みに失敗しました。",
+ "settings.ai.testError.proxyUnavailableStaticHost": "Claude is not available on this deployment: it has no serverless proxy (GitHub Pages is static-only). Use the desktop app, or a Vercel/Cloudflare Pages deployment instead.",
"settings.ai.testError.timeout": "接続がタイムアウトしました ({{url}})",
"settings.ai.testError.unexpected": "予期しないエラーが発生しました。もう一度お試しください。",
"settings.ai.testError.unknownProvider": "不明なプロバイダー",
diff --git a/public/locales/ko/bundle.json b/public/locales/ko/bundle.json
index 44987fc6c..49b7951c3 100644
--- a/public/locales/ko/bundle.json
+++ b/public/locales/ko/bundle.json
@@ -1725,6 +1725,7 @@
"settings.ai.anthropicCorsNote": "Anthropic Claude는 브라우저에서 직접 호출할 수 없습니다. Anthropic 통화를 위해 프록시 서버를 설정하거나 Tauri 데스크톱 앱을 사용하세요.",
"settings.ai.anthropicHint": "Anthropic에는 API 호출을 위해 프록시 서버 또는 Tauri 데스크톱 앱이 필요합니다.",
"settings.ai.anthropicKey": "Claude API Key",
+ "settings.ai.anthropicProxyNote": "Claude requests from the browser are relayed through WorldScript's own serverless proxy on the way to Anthropic — the only cloud provider routed this way. Your key is never stored server-side.",
"settings.ai.connectionFailed": "연결 실패",
"settings.ai.connectionSuccess": "연결 성공",
"settings.ai.corsRestriction": "CORS 제한",
@@ -1863,12 +1864,12 @@
"settings.ai.temperature.creative": "2 – 창의적",
"settings.ai.temperature.precise": "0 – 정확함",
"settings.ai.testConnection": "연결 테스트",
- "settings.ai.testError.backendProxyRequired": "Claude에는 백엔드 프록시가 필요합니다(CORS 제한)",
"settings.ai.testError.desktopRequired": "Ollama 및 로컬 OpenAI 호환 서버는 데스크톱 앱에서만 사용할 수 있습니다. 브라우저는 웹 페이지에서 localhost로의 직접 연결을 차단합니다(CORS 및 Private Network Access).",
"settings.ai.testError.httpError": "HTTP {{status}}",
"settings.ai.testError.noApiKey": "{{provider}} API 키가 설정되지 않았습니다",
"settings.ai.testError.noWebgpu": "이 브라우저에서는 WebGPU를 사용할 수 없습니다 — WebLLM에는 WebGPU가 필요합니다(Chrome/Edge를 사용하거나 플래그를 활성화하세요).",
"settings.ai.testError.pluginUnavailable": "로컬 서버 네트워킹을 사용할 수 없습니다: 데스크톱 HTTP 플러그인을 불러오지 못했습니다.",
+ "settings.ai.testError.proxyUnavailableStaticHost": "Claude is not available on this deployment: it has no serverless proxy (GitHub Pages is static-only). Use the desktop app, or a Vercel/Cloudflare Pages deployment instead.",
"settings.ai.testError.timeout": "연결 시간이 초과되었습니다 ({{url}})",
"settings.ai.testError.unexpected": "예기치 않은 오류가 발생했습니다. 다시 시도해 주세요.",
"settings.ai.testError.unknownProvider": "알 수 없는 제공자",
diff --git a/public/locales/pt/bundle.json b/public/locales/pt/bundle.json
index a53336ee5..f94c9d2ee 100644
--- a/public/locales/pt/bundle.json
+++ b/public/locales/pt/bundle.json
@@ -1725,6 +1725,7 @@
"settings.ai.anthropicCorsNote": "O Antrópico Claude não pode ser chamado diretamente do navegador. Configure um servidor proxy ou use o aplicativo de desktop Tauri para chamadas Antrópicas.",
"settings.ai.anthropicHint": "A Anthropic requer um servidor proxy ou o aplicativo de desktop Tauri para chamadas de API.",
"settings.ai.anthropicKey": "Claude API Key",
+ "settings.ai.anthropicProxyNote": "Claude requests from the browser are relayed through WorldScript's own serverless proxy on the way to Anthropic — the only cloud provider routed this way. Your key is never stored server-side.",
"settings.ai.connectionFailed": "Falha na conexão",
"settings.ai.connectionSuccess": "Conexão bem-sucedida",
"settings.ai.corsRestriction": "Restrição CORS",
@@ -1863,12 +1864,12 @@
"settings.ai.temperature.creative": "2 – Criativo",
"settings.ai.temperature.precise": "0 – Preciso",
"settings.ai.testConnection": "Conexão de teste",
- "settings.ai.testError.backendProxyRequired": "O Claude requer um proxy de backend (restrição CORS)",
"settings.ai.testError.desktopRequired": "O Ollama e os servidores locais compatíveis com OpenAI só estão disponíveis na aplicação de ambiente de trabalho. Os navegadores bloqueiam ligações diretas de páginas web ao localhost (CORS e Private Network Access).",
"settings.ai.testError.httpError": "HTTP {{status}}",
"settings.ai.testError.noApiKey": "Nenhuma chave de API {{provider}} definida",
"settings.ai.testError.noWebgpu": "WebGPU indisponível neste navegador — o WebLLM necessita de WebGPU (experimente o Chrome/Edge ou ative as flags).",
"settings.ai.testError.pluginUnavailable": "A rede do servidor local não está disponível: o plugin HTTP de ambiente de trabalho falhou ao carregar.",
+ "settings.ai.testError.proxyUnavailableStaticHost": "Claude is not available on this deployment: it has no serverless proxy (GitHub Pages is static-only). Use the desktop app, or a Vercel/Cloudflare Pages deployment instead.",
"settings.ai.testError.timeout": "Tempo limite de conexão excedido ({{url}})",
"settings.ai.testError.unexpected": "Ocorreu um erro inesperado. Tente novamente.",
"settings.ai.testError.unknownProvider": "Fornecedor desconhecido",
diff --git a/public/locales/ru/bundle.json b/public/locales/ru/bundle.json
index 88afeb0f8..fa84463dc 100644
--- a/public/locales/ru/bundle.json
+++ b/public/locales/ru/bundle.json
@@ -1725,6 +1725,7 @@
"settings.ai.anthropicCorsNote": "Anthropic Claude нельзя вызвать напрямую из браузера. Настройте прокси-сервер или используйте настольное приложение Tauri для вызовов Anthropic.",
"settings.ai.anthropicHint": "Для вызовов API Anthropic требуется прокси-сервер или настольное приложение Tauri.",
"settings.ai.anthropicKey": "Claude API Key",
+ "settings.ai.anthropicProxyNote": "Claude requests from the browser are relayed through WorldScript's own serverless proxy on the way to Anthropic — the only cloud provider routed this way. Your key is never stored server-side.",
"settings.ai.connectionFailed": "Соединение не удалось",
"settings.ai.connectionSuccess": "Соединение успешно",
"settings.ai.corsRestriction": "Ограничение CORS",
@@ -1863,12 +1864,12 @@
"settings.ai.temperature.creative": "2 – Креатив",
"settings.ai.temperature.precise": "0 – Точный",
"settings.ai.testConnection": "Тестовое соединение",
- "settings.ai.testError.backendProxyRequired": "Claude требует backend-прокси (ограничение CORS)",
"settings.ai.testError.desktopRequired": "Ollama и локальные серверы, совместимые с OpenAI, доступны только в настольном приложении. Браузеры блокируют прямые подключения с веб-страниц к localhost (CORS и Private Network Access).",
"settings.ai.testError.httpError": "HTTP {{status}}",
"settings.ai.testError.noApiKey": "Не задан ключ API {{provider}}",
"settings.ai.testError.noWebgpu": "WebGPU недоступен в этом браузере — WebLLM требует WebGPU (попробуйте Chrome/Edge или включите флаги).",
"settings.ai.testError.pluginUnavailable": "Сеть локального сервера недоступна: не удалось загрузить настольный HTTP-плагин.",
+ "settings.ai.testError.proxyUnavailableStaticHost": "Claude is not available on this deployment: it has no serverless proxy (GitHub Pages is static-only). Use the desktop app, or a Vercel/Cloudflare Pages deployment instead.",
"settings.ai.testError.timeout": "Тайм-аут соединения ({{url}})",
"settings.ai.testError.unexpected": "Произошла непредвиденная ошибка. Повторите попытку.",
"settings.ai.testError.unknownProvider": "Неизвестный провайдер",
diff --git a/public/locales/sv/bundle.json b/public/locales/sv/bundle.json
index fb52f63e1..252c7dc73 100644
--- a/public/locales/sv/bundle.json
+++ b/public/locales/sv/bundle.json
@@ -1725,6 +1725,7 @@
"settings.ai.anthropicCorsNote": "Antropiska Claude kan inte ringas direkt från webbläsaren. Konfigurera en proxyserver eller använd Tauri-skrivbordsappen för antropiska samtal.",
"settings.ai.anthropicHint": "Anthropic kräver en proxyserver eller Tauri-skrivbordsappen för API-anrop.",
"settings.ai.anthropicKey": "Claude API Key",
+ "settings.ai.anthropicProxyNote": "Claude requests from the browser are relayed through WorldScript's own serverless proxy on the way to Anthropic — the only cloud provider routed this way. Your key is never stored server-side.",
"settings.ai.connectionFailed": "Anslutning misslyckades",
"settings.ai.connectionSuccess": "Anslutningen lyckades",
"settings.ai.corsRestriction": "CORS-begränsning",
@@ -1863,12 +1864,12 @@
"settings.ai.temperature.creative": "2 – Kreativt",
"settings.ai.temperature.precise": "0 – Exakt",
"settings.ai.testConnection": "Testa anslutningen",
- "settings.ai.testError.backendProxyRequired": "Claude kräver en backend-proxy (CORS-begränsning)",
"settings.ai.testError.desktopRequired": "Ollama och lokala OpenAI-kompatibla servrar är endast tillgängliga i skrivbordsappen. Webbläsare blockerar direkta anslutningar från webbsidor till localhost (CORS och Private Network Access).",
"settings.ai.testError.httpError": "HTTP {{status}}",
"settings.ai.testError.noApiKey": "Ingen {{provider}}-API-nyckel angiven",
"settings.ai.testError.noWebgpu": "WebGPU är inte tillgängligt i den här webbläsaren — WebLLM behöver WebGPU (prova Chrome/Edge eller aktivera flaggor).",
"settings.ai.testError.pluginUnavailable": "Lokal servernätverksanslutning ej tillgänglig: skrivbordets HTTP-plugin misslyckades med att läsas in.",
+ "settings.ai.testError.proxyUnavailableStaticHost": "Claude is not available on this deployment: it has no serverless proxy (GitHub Pages is static-only). Use the desktop app, or a Vercel/Cloudflare Pages deployment instead.",
"settings.ai.testError.timeout": "Anslutningen fick timeout ({{url}})",
"settings.ai.testError.unexpected": "Ett oväntat fel inträffade. Försök igen.",
"settings.ai.testError.unknownProvider": "Okänd leverantör",
diff --git a/public/locales/zh/bundle.json b/public/locales/zh/bundle.json
index 933af99eb..59a6d1ba0 100644
--- a/public/locales/zh/bundle.json
+++ b/public/locales/zh/bundle.json
@@ -1725,6 +1725,7 @@
"settings.ai.anthropicCorsNote": "Anthropic Claude 无法直接从浏览器调用。设置代理服务器或使用 Tauri 桌面应用程序进行 Anthropic 呼叫。",
"settings.ai.anthropicHint": "Anthropic 需要代理服务器或 Tauri 桌面应用程序来进行 API 调用。",
"settings.ai.anthropicKey": "Claude API Key",
+ "settings.ai.anthropicProxyNote": "Claude requests from the browser are relayed through WorldScript's own serverless proxy on the way to Anthropic — the only cloud provider routed this way. Your key is never stored server-side.",
"settings.ai.connectionFailed": "连接失败",
"settings.ai.connectionSuccess": "连接成功",
"settings.ai.corsRestriction": "CORS 限制",
@@ -1863,12 +1864,12 @@
"settings.ai.temperature.creative": "2 – 创意",
"settings.ai.temperature.precise": "0 – 精确",
"settings.ai.testConnection": "测试连接",
- "settings.ai.testError.backendProxyRequired": "Claude 需要后端代理(CORS 限制)",
"settings.ai.testError.desktopRequired": "Ollama 和本地 OpenAI 兼容服务器仅在桌面应用中可用。浏览器会阻止网页直接连接到 localhost(CORS 和 Private Network Access)。",
"settings.ai.testError.httpError": "HTTP {{status}}",
"settings.ai.testError.noApiKey": "未设置 {{provider}} API 密钥",
"settings.ai.testError.noWebgpu": "此浏览器不支持 WebGPU — WebLLM 需要 WebGPU(请尝试 Chrome/Edge 或启用相关标志)。",
"settings.ai.testError.pluginUnavailable": "本地服务器网络不可用:桌面 HTTP 插件加载失败。",
+ "settings.ai.testError.proxyUnavailableStaticHost": "Claude is not available on this deployment: it has no serverless proxy (GitHub Pages is static-only). Use the desktop app, or a Vercel/Cloudflare Pages deployment instead.",
"settings.ai.testError.timeout": "连接超时 ({{url}})",
"settings.ai.testError.unexpected": "发生意外错误。请重试。",
"settings.ai.testError.unknownProvider": "未知的提供商",
diff --git a/services/aiProviderService.ts b/services/aiProviderService.ts
index a5f8f08f1..67482c7db 100644
--- a/services/aiProviderService.ts
+++ b/services/aiProviderService.ts
@@ -29,6 +29,7 @@ import {
import { generateOpenRouterText, streamOpenRouter } from './ai/providers/openrouterProvider';
import { logRoutingDecision } from './ai/routingLogger';
import { attachCause, sanitizePromptValue, stripJsonFences } from './aiUtils';
+import { isServerlessProxyCapable } from './deployTarget';
import {
generateImage as generateImageGemini,
generateJson as generateJsonGemini,
@@ -241,47 +242,79 @@ async function streamOpenAI(
callbacks.onDone?.();
}
-// QNBS-v3 (ADR-0016 Track A): CORS is a *browser* restriction — Tauri's native HTTP plugin
-// (localServerFetch, ADR-0012) isn't subject to it, so desktop can call Anthropic directly.
-// Web/PWA still has no escape hatch and needs Track B's serverless proxy (not yet built).
+// QNBS-v3 (ADR-0016): both the Track A (desktop) and Track B (web-via-proxy) response bodies are
+// Anthropic's own Messages API JSON shape unmodified — the proxy relays it verbatim — so a single
+// parser serves both branches of streamAnthropic below.
+async function deliverAnthropicResponse(
+ res: Response,
+ callbacks: AIStreamCallbacks,
+): Promise {
+ if (!res.ok) throw new Error(`Claude API Error ${res.status}: ${res.statusText}`);
+ const json = (await res.json()) as { content?: Array<{ type?: string; text?: string }> };
+ // QNBS-v3 (CodeRabbit): Anthropic can return multiple content blocks (e.g. a `thinking` block
+ // plus several `text` blocks on extended-thinking models) — concatenate all text blocks instead
+ // of taking only the first, which silently truncated output.
+ const text = (json.content ?? [])
+ .filter((c) => c.type === 'text' && typeof c.text === 'string')
+ .map((c) => c.text)
+ .join('');
+ if (text) callbacks.onChunk(text);
+ callbacks.onDone?.();
+}
+
+// QNBS-v3 (ADR-0016): CORS is a *browser* restriction. Track A — Tauri's native HTTP plugin
+// (localServerFetch, ADR-0012) isn't subject to it, so desktop calls Anthropic directly. Track B —
+// web/PWA has no such escape hatch, so it relays through this app's own same-origin serverless
+// proxy (api/claude-proxy.ts / functions/api/claude-proxy.ts) instead, which itself isn't subject
+// to browser CORS on its outbound (server-to-server) leg. GitHub Pages hosts neither function, so
+// it stays genuinely unsupported — isServerlessProxyCapable() reports that structurally.
async function streamAnthropic(
prompt: string,
opts: AIRequestOptions,
callbacks: AIStreamCallbacks,
): Promise {
- if (!isTauriRuntime()) {
+ // QNBS-v3: platform-capability checks come before the API-key check — a GitHub Pages user with
+ // no key configured should learn the deployment can't support Claude at all, not that a key is
+ // missing (setting one wouldn't help).
+ if (!isTauriRuntime() && !isServerlessProxyCapable()) {
throw new Error(
- 'Claude/Anthropic: Direct browser requests are blocked by Anthropic (CORS). ' +
- 'Please use a backend proxy or switch to Gemini/OpenAI/Ollama.',
+ 'Claude/Anthropic is not available on this deployment (no serverless proxy on GitHub Pages). ' +
+ 'Please use the desktop app, a Vercel/Cloudflare Pages deployment, or switch providers.',
);
}
const apiKey = await storageService.getApiKey('anthropic');
if (!apiKey) throw new Error('NO_API_KEY: Claude API key missing. Please enter it in Settings.');
- const res = await localServerFetch('https://api.anthropic.com/v1/messages', {
+
+ if (isTauriRuntime()) {
+ const res = await localServerFetch('https://api.anthropic.com/v1/messages', {
+ method: 'POST',
+ headers: {
+ 'x-api-key': apiKey,
+ 'anthropic-version': '2023-06-01',
+ 'content-type': 'application/json',
+ },
+ body: JSON.stringify({
+ model: opts.model,
+ max_tokens: opts.maxTokens ?? 2048,
+ messages: [{ role: 'user', content: sanitizePromptValue(prompt) }],
+ }),
+ signal: opts.signal ?? null,
+ });
+ return deliverAnthropicResponse(res, callbacks);
+ }
+
+ const res = await fetch('/api/claude-proxy', {
method: 'POST',
- headers: {
- 'x-api-key': apiKey,
- 'anthropic-version': '2023-06-01',
- 'content-type': 'application/json',
- },
+ headers: { 'content-type': 'application/json' },
body: JSON.stringify({
+ apiKey,
model: opts.model,
- max_tokens: opts.maxTokens ?? 2048,
+ maxTokens: opts.maxTokens ?? 2048,
messages: [{ role: 'user', content: sanitizePromptValue(prompt) }],
}),
signal: opts.signal ?? null,
});
- if (!res.ok) throw new Error(`Claude API Error ${res.status}: ${res.statusText}`);
- const json = (await res.json()) as { content?: Array<{ type?: string; text?: string }> };
- // QNBS-v3 (CodeRabbit): Anthropic can return multiple content blocks (e.g. a `thinking` block
- // plus several `text` blocks on extended-thinking models) — concatenate all text blocks instead
- // of taking only the first, which silently truncated output.
- const text = (json.content ?? [])
- .filter((c) => c.type === 'text' && typeof c.text === 'string')
- .map((c) => c.text)
- .join('');
- if (text) callbacks.onChunk(text);
- callbacks.onDone?.();
+ return deliverAnthropicResponse(res, callbacks);
}
async function streamGrok(
@@ -406,8 +439,8 @@ async function generateTextSingleProvider(
return providerTextSchema.parse({ text: result }).text;
}
case 'anthropic': {
- // QNBS-v3 (ADR-0016 Track A): reuses the now-fixed streamAnthropic, which itself branches
- // on isTauriRuntime — desktop works, web still throws its CORS message.
+ // QNBS-v3 (ADR-0016): reuses streamAnthropic, which itself branches on isTauriRuntime
+ // (desktop, native) vs. isServerlessProxyCapable (web, via api/claude-proxy).
let result = '';
await streamAnthropic(prompt, o, {
onChunk: (text) => {
@@ -797,7 +830,7 @@ export type TestConnectionErrorKind =
| 'unreachable'
| 'pluginUnavailable'
| 'desktopRequired'
- | 'backendProxyRequired'
+ | 'proxyUnavailableStaticHost'
| 'noWebgpu'
| 'unknownProvider'
| 'unexpected';
@@ -858,13 +891,16 @@ export async function testAIConnection(
return testOllamaConnection(opts.ollamaBaseUrl);
}
case 'anthropic': {
- // QNBS-v3 (ADR-0016 Track A): desktop bypasses CORS via localServerFetch's native path;
- // web still has no escape hatch until Track B's proxy exists.
- if (!isTauriRuntime()) {
+ // QNBS-v3 (ADR-0016): desktop bypasses CORS via localServerFetch's native path (Track A);
+ // web relays through api/claude-proxy (Track B) — except GitHub Pages, which can host
+ // neither Vercel nor Cloudflare Pages Functions and stays structurally unsupported.
+ const isDesktop = isTauriRuntime();
+ if (!isDesktop && !isServerlessProxyCapable()) {
return {
ok: false,
- error: 'Claude requires the desktop app or a backend proxy (browser CORS restriction)',
- kind: 'backendProxyRequired',
+ error:
+ 'Claude is not available on this deployment (no serverless proxy on GitHub Pages)',
+ kind: 'proxyUnavailableStaticHost',
};
}
const apiKey = await storageService.getApiKey('anthropic');
@@ -880,21 +916,33 @@ export async function testAIConnection(
// request is the practical connectivity check, mirroring the pattern used for Grok.
// QNBS-v3 (CodeRabbit): bounded like every sibling connectivity check (testOllamaConnection
// uses timeoutMs: 5000; openai/grok/gemini use AbortSignal.timeout(8000)) — a stalled
- // native HTTP call must not hang the Settings test spinner indefinitely.
- const res = await localServerFetch('https://api.anthropic.com/v1/messages', {
- method: 'POST',
- headers: {
- 'x-api-key': apiKey,
- 'anthropic-version': '2023-06-01',
- 'content-type': 'application/json',
- },
- body: JSON.stringify({
- model: 'claude-haiku-4-5',
- max_tokens: 1,
- messages: [{ role: 'user', content: 'ping' }],
- }),
- timeoutMs: 8000,
- });
+ // native/proxy HTTP call must not hang the Settings test spinner indefinitely.
+ const res = isDesktop
+ ? await localServerFetch('https://api.anthropic.com/v1/messages', {
+ method: 'POST',
+ headers: {
+ 'x-api-key': apiKey,
+ 'anthropic-version': '2023-06-01',
+ 'content-type': 'application/json',
+ },
+ body: JSON.stringify({
+ model: 'claude-haiku-4-5',
+ max_tokens: 1,
+ messages: [{ role: 'user', content: 'ping' }],
+ }),
+ timeoutMs: 8000,
+ })
+ : await fetch('/api/claude-proxy', {
+ method: 'POST',
+ headers: { 'content-type': 'application/json' },
+ body: JSON.stringify({
+ apiKey,
+ model: 'claude-haiku-4-5',
+ maxTokens: 1,
+ messages: [{ role: 'user', content: 'ping' }],
+ }),
+ signal: AbortSignal.timeout(8000),
+ });
if (!res.ok) {
return {
ok: false,
diff --git a/services/deployTarget.ts b/services/deployTarget.ts
new file mode 100644
index 000000000..29a2512a3
--- /dev/null
+++ b/services/deployTarget.ts
@@ -0,0 +1,19 @@
+// QNBS-v3 (ADR-0016 Track B): the app's three web deploy targets differ in whether a serverless
+// function can exist at all — Vercel and Cloudflare Pages can host api/claude-proxy.ts /
+// functions/api/claude-proxy.ts, GitHub Pages is pure static hosting and never can (see
+// docs/adr/0016). Desktop (Tauri) doesn't need this at all — Track A's native-HTTP path bypasses
+// CORS directly; callers must check isTauriRuntime() first.
+import { GITHUB_PAGES_BASE } from '../config/resolveViteBase';
+
+/**
+ * True when the current web build is served from a host that can run a serverless function
+ * (Vercel, Cloudflare Pages, or local dev), false on the static-only GitHub Pages mirror.
+ *
+ * Signal: `import.meta.env.BASE_URL` is Vite's build-time-injected base path — already this
+ * codebase's canonical GitHub-Pages-vs-edge marker (see config/resolveViteBase.ts, which computes
+ * it from the same GITHUB_PAGES_BASE constant). GitHub Pages project pages are always served from
+ * the '/WorldScript-Studio/' subpath; Vercel, Cloudflare Pages, and custom domains serve from '/'.
+ */
+export function isServerlessProxyCapable(): boolean {
+ return import.meta.env.BASE_URL !== GITHUB_PAGES_BASE;
+}
diff --git a/tests/unit/aiProviderService.test.ts b/tests/unit/aiProviderService.test.ts
index 0b828987f..d09d0b17c 100644
--- a/tests/unit/aiProviderService.test.ts
+++ b/tests/unit/aiProviderService.test.ts
@@ -241,11 +241,12 @@ describe('generateJson', () => {
// ─── testAIConnection ─────────────────────────────────────────────────────────
describe('testAIConnection', () => {
- it('returns ok:false for anthropic (CORS restriction)', async () => {
+ it('returns ok:false (noApiKey) for anthropic on proxy-capable web with no key stored', async () => {
+ // QNBS-v3 (ADR-0016 Track B): default jsdom BASE_URL is proxy-capable ('/', not the GitHub
+ // Pages path) — see tests/unit/deployTarget.test.ts for the dedicated branch coverage.
const result = await testAIConnection('anthropic', {});
expect(result.ok).toBe(false);
- expect(result.error).toContain('CORS');
- expect(result.kind).toBe('backendProxyRequired');
+ expect(result.kind).toBe('noApiKey');
});
it('returns ok:false for ollama in browser (no Tauri)', async () => {
@@ -331,15 +332,19 @@ describe('testAIConnection — ollama desktop branch', () => {
});
});
-// QNBS-v3 (ADR-0016 Track A): CORS is a browser-only restriction — on desktop, Anthropic is
-// called directly via localServerFetch's native-HTTP escape hatch (plugin-http), same pattern
-// as Ollama. localServerFetch itself falls back to globalThis.fetch outside Tauri.
-describe('Anthropic — desktop branch (ADR-0016 Track A)', () => {
+// QNBS-v3 (ADR-0016): CORS is a browser-only restriction. Track A — on desktop, Anthropic is
+// called directly via localServerFetch's native-HTTP escape hatch (plugin-http), same pattern as
+// Ollama; localServerFetch itself falls back to globalThis.fetch outside Tauri. Track B — on
+// proxy-capable web (Vercel/Cloudflare, not GitHub Pages), the same call goes through this app's
+// own same-origin api/claude-proxy instead of straight to api.anthropic.com.
+describe('Anthropic — desktop (Track A) and web proxy (Track B) branches (ADR-0016)', () => {
const originalFetch = globalThis.fetch;
+ const originalBaseUrl = import.meta.env.BASE_URL;
afterEach(() => {
delete (window as { __TAURI_INTERNALS__?: unknown }).__TAURI_INTERNALS__;
globalThis.fetch = originalFetch;
+ import.meta.env.BASE_URL = originalBaseUrl;
});
it('testAIConnection: returns noApiKey when no Claude key is stored under Tauri', async () => {
@@ -378,10 +383,31 @@ describe('Anthropic — desktop branch (ADR-0016 Track A)', () => {
expect(result.params).toEqual({ status: 401 });
});
- it('testAIConnection: still returns backendProxyRequired outside Tauri (web unchanged)', async () => {
+ it('testAIConnection: returns proxyUnavailableStaticHost on GitHub Pages, without ever checking for a key', async () => {
+ import.meta.env.BASE_URL = '/WorldScript-Studio/';
const result = await testAIConnection('anthropic', {});
expect(result.ok).toBe(false);
- expect(result.kind).toBe('backendProxyRequired');
+ expect(result.kind).toBe('proxyUnavailableStaticHost');
+ expect(storageService.getApiKey).not.toHaveBeenCalled();
+ expect(mockPluginHttpFetch).not.toHaveBeenCalled();
+ });
+
+ it('testAIConnection: calls the proxy (not api.anthropic.com directly) on proxy-capable web', async () => {
+ vi.mocked(storageService.getApiKey).mockResolvedValueOnce('anthropic-key');
+ const fetchSpy = vi.fn().mockResolvedValueOnce(new Response('{}', { status: 200 }));
+ globalThis.fetch = fetchSpy as typeof fetch;
+ const result = await testAIConnection('anthropic', {});
+ expect(result.ok).toBe(true);
+ expect(fetchSpy).toHaveBeenCalledWith(
+ '/api/claude-proxy',
+ expect.objectContaining({ method: 'POST' }),
+ );
+ const [, init] = fetchSpy.mock.calls[0] as [string, RequestInit];
+ expect(JSON.parse(init.body as string)).toMatchObject({
+ apiKey: 'anthropic-key',
+ model: 'claude-haiku-4-5',
+ maxTokens: 1,
+ });
expect(mockPluginHttpFetch).not.toHaveBeenCalled();
});
@@ -403,6 +429,29 @@ describe('Anthropic — desktop branch (ADR-0016 Track A)', () => {
expect(onChunk).toHaveBeenCalledWith('Hello from Claude');
});
+ it('streamText: delivers a real Claude response via the proxy on proxy-capable web', async () => {
+ vi.mocked(storageService.getApiKey).mockResolvedValueOnce('anthropic-key');
+ const fetchSpy = vi.fn().mockResolvedValueOnce(
+ new Response(JSON.stringify({ content: [{ type: 'text', text: 'Hello via proxy' }] }), {
+ status: 200,
+ }),
+ );
+ globalThis.fetch = fetchSpy as typeof fetch;
+ const onChunk = vi.fn();
+ await streamText(
+ 'hello',
+ 'Balanced',
+ { provider: 'anthropic', model: 'claude-haiku-4-5' },
+ { onChunk },
+ );
+ expect(onChunk).toHaveBeenCalledWith('Hello via proxy');
+ expect(fetchSpy).toHaveBeenCalledWith(
+ '/api/claude-proxy',
+ expect.objectContaining({ method: 'POST' }),
+ );
+ expect(mockPluginHttpFetch).not.toHaveBeenCalled();
+ });
+
it('streamText: concatenates every text block instead of only the first (CodeRabbit)', async () => {
(window as { __TAURI_INTERNALS__?: unknown }).__TAURI_INTERNALS__ = {};
vi.mocked(storageService.getApiKey).mockResolvedValueOnce('anthropic-key');
@@ -428,17 +477,17 @@ describe('Anthropic — desktop branch (ADR-0016 Track A)', () => {
expect(onChunk).toHaveBeenCalledWith('Part one. Part two.');
});
- it('streamText: still throws the CORS message outside Tauri (web unchanged)', async () => {
+ it('streamText: throws the GitHub-Pages-unavailable message without ever checking for a key', async () => {
+ import.meta.env.BASE_URL = '/WorldScript-Studio/';
await expect(
streamText(
'hello',
'Balanced',
{ provider: 'anthropic', model: 'claude-haiku-4-5' },
- {
- onChunk: vi.fn(),
- },
+ { onChunk: vi.fn() },
),
- ).rejects.toThrow('CORS');
+ ).rejects.toThrow('not available on this deployment');
+ expect(storageService.getApiKey).not.toHaveBeenCalled();
expect(mockPluginHttpFetch).not.toHaveBeenCalled();
});
});
diff --git a/tests/unit/api/claudeProxyCore.test.ts b/tests/unit/api/claudeProxyCore.test.ts
new file mode 100644
index 000000000..9126f2353
--- /dev/null
+++ b/tests/unit/api/claudeProxyCore.test.ts
@@ -0,0 +1,252 @@
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
+import { handleClaudeProxyRequest } from '../../../api/_shared/claudeProxyCore';
+
+const PROXY_URL = 'https://worldscript-studio.vercel.app/api/claude-proxy';
+const VALID_ORIGIN = 'https://worldscript-studio.vercel.app';
+const VALID_API_KEY = 'sk-ant-api03-'.padEnd(30, 'x');
+
+function makeRequest(
+ body: unknown,
+ overrides: { method?: string; origin?: string | null; headers?: Record } = {},
+): Request {
+ const headers = new Headers(overrides.headers ?? {});
+ if (overrides.origin !== null) {
+ headers.set('origin', overrides.origin ?? VALID_ORIGIN);
+ }
+ const rawBody = typeof body === 'string' ? body : JSON.stringify(body);
+ if (!headers.has('content-length')) {
+ headers.set('content-length', String(new TextEncoder().encode(rawBody).length));
+ }
+ const method = overrides.method ?? 'POST';
+ // QNBS-v3: the Fetch spec forbids a body on GET/HEAD requests — the 405 test only needs the
+ // method + headers to reach the method check, never an actual body.
+ const bodyAllowed = method !== 'GET' && method !== 'HEAD';
+ return new Request(PROXY_URL, {
+ method,
+ headers,
+ ...(bodyAllowed ? { body: rawBody } : {}),
+ });
+}
+
+function validBody(overrides: Record = {}) {
+ return {
+ apiKey: VALID_API_KEY,
+ model: 'claude-haiku-4-5',
+ messages: [{ role: 'user', content: 'Hello Claude' }],
+ ...overrides,
+ };
+}
+
+// QNBS-v3: each test uses a distinct x-forwarded-for so the module-level rate-limit map
+// (keyed by client IP) never leaks state across unrelated test cases.
+let ipCounter = 0;
+function uniqueIp(): string {
+ ipCounter += 1;
+ return `203.0.113.${ipCounter % 255}`;
+}
+
+describe('handleClaudeProxyRequest', () => {
+ let consoleLogSpy: ReturnType;
+ let consoleErrorSpy: ReturnType;
+ let consoleWarnSpy: ReturnType;
+
+ beforeEach(() => {
+ consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
+ consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
+ consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
+ });
+
+ afterEach(() => {
+ // QNBS-v3: statelessness guarantee (ADR-0016) — the handler must never log the API key,
+ // messages, or any other request/response payload content, on any code path.
+ expect(consoleLogSpy).not.toHaveBeenCalled();
+ expect(consoleErrorSpy).not.toHaveBeenCalled();
+ expect(consoleWarnSpy).not.toHaveBeenCalled();
+ vi.restoreAllMocks();
+ });
+
+ it('rejects non-POST methods with 405', async () => {
+ const req = makeRequest(validBody(), {
+ method: 'GET',
+ headers: { 'x-forwarded-for': uniqueIp() },
+ });
+ const res = await handleClaudeProxyRequest(req);
+ expect(res.status).toBe(405);
+ });
+
+ it('rejects a request with no Origin header with 403', async () => {
+ const req = makeRequest(validBody(), {
+ origin: null,
+ headers: { 'x-forwarded-for': uniqueIp() },
+ });
+ const res = await handleClaudeProxyRequest(req);
+ expect(res.status).toBe(403);
+ });
+
+ it('rejects a cross-origin request with 403', async () => {
+ const req = makeRequest(validBody(), {
+ origin: 'https://evil.example.com',
+ headers: { 'x-forwarded-for': uniqueIp() },
+ });
+ const res = await handleClaudeProxyRequest(req);
+ expect(res.status).toBe(403);
+ });
+
+ it('rejects a body over the declared content-length limit with 413', async () => {
+ const oversizedBody = validBody({ messages: [{ role: 'user', content: 'x'.repeat(300_000) }] });
+ const req = makeRequest(oversizedBody, { headers: { 'x-forwarded-for': uniqueIp() } });
+ const res = await handleClaudeProxyRequest(req);
+ expect(res.status).toBe(413);
+ });
+
+ it('rejects a spoofed content-length by re-checking the actual body length', async () => {
+ const oversizedText = JSON.stringify(
+ validBody({ messages: [{ role: 'user', content: 'x'.repeat(300_000) }] }),
+ );
+ const req = makeRequest(oversizedText, {
+ headers: { 'x-forwarded-for': uniqueIp(), 'content-length': '10' },
+ });
+ const res = await handleClaudeProxyRequest(req);
+ expect(res.status).toBe(413);
+ });
+
+ it('rejects invalid JSON with 400', async () => {
+ const req = makeRequest('{not valid json', { headers: { 'x-forwarded-for': uniqueIp() } });
+ const res = await handleClaudeProxyRequest(req);
+ expect(res.status).toBe(400);
+ });
+
+ it('rejects a body that fails schema validation (missing apiKey) with 400', async () => {
+ const invalidBody: Record = validBody();
+ delete invalidBody['apiKey'];
+ const req = makeRequest(invalidBody, { headers: { 'x-forwarded-for': uniqueIp() } });
+ const res = await handleClaudeProxyRequest(req);
+ expect(res.status).toBe(400);
+ });
+
+ it('rejects an unlisted model with 400', async () => {
+ const req = makeRequest(validBody({ model: 'claude-does-not-exist' }), {
+ headers: { 'x-forwarded-for': uniqueIp() },
+ });
+ const res = await handleClaudeProxyRequest(req);
+ expect(res.status).toBe(400);
+ });
+
+ it('rate-limits a client after exceeding the per-window request count', async () => {
+ const ip = uniqueIp();
+ let lastRes: Response | undefined;
+ for (let i = 0; i < 21; i++) {
+ const req = makeRequest(validBody(), { headers: { 'x-forwarded-for': ip } });
+ lastRes = await handleClaudeProxyRequest(
+ req,
+ vi.fn().mockResolvedValue(
+ new Response(JSON.stringify({ content: [{ type: 'text', text: 'ok' }] }), {
+ status: 200,
+ }),
+ ),
+ );
+ }
+ expect(lastRes?.status).toBe(429);
+ });
+
+ it('relays a valid request to Anthropic and passes the response through unmodified', async () => {
+ const upstreamBody = { content: [{ type: 'text', text: 'Hello from Claude' }] };
+ const fetchImpl = vi
+ .fn()
+ .mockResolvedValue(new Response(JSON.stringify(upstreamBody), { status: 200 }));
+ const req = makeRequest(validBody(), { headers: { 'x-forwarded-for': uniqueIp() } });
+
+ const res = await handleClaudeProxyRequest(req, fetchImpl);
+
+ expect(res.status).toBe(200);
+ expect(await res.json()).toEqual(upstreamBody);
+ expect(res.headers.get('cache-control')).toBe('no-store');
+
+ expect(fetchImpl).toHaveBeenCalledTimes(1);
+ const [url, init] = fetchImpl.mock.calls[0] as [string, RequestInit];
+ expect(url).toBe('https://api.anthropic.com/v1/messages');
+ const sentHeaders = init.headers as Record;
+ expect(sentHeaders['x-api-key']).toBe(VALID_API_KEY);
+ expect(sentHeaders['anthropic-version']).toBe('2023-06-01');
+ const sentBody = JSON.parse(init.body as string);
+ expect(sentBody.model).toBe('claude-haiku-4-5');
+ expect(sentBody.max_tokens).toBe(2048);
+ expect(sentBody.messages).toEqual([{ role: 'user', content: 'Hello Claude' }]);
+ });
+
+ it('honors a caller-supplied maxTokens instead of the 2048 default', async () => {
+ const fetchImpl = vi
+ .fn()
+ .mockResolvedValue(new Response(JSON.stringify({ content: [] }), { status: 200 }));
+ const req = makeRequest(validBody({ maxTokens: 512 }), {
+ headers: { 'x-forwarded-for': uniqueIp() },
+ });
+ await handleClaudeProxyRequest(req, fetchImpl);
+ const [, init] = fetchImpl.mock.calls[0] as [string, RequestInit];
+ expect(JSON.parse(init.body as string).max_tokens).toBe(512);
+ });
+
+ it('propagates a non-2xx upstream status and body unchanged', async () => {
+ const errorBody = { type: 'error', error: { message: 'invalid x-api-key' } };
+ const fetchImpl = vi
+ .fn()
+ .mockResolvedValue(new Response(JSON.stringify(errorBody), { status: 401 }));
+ const req = makeRequest(validBody(), { headers: { 'x-forwarded-for': uniqueIp() } });
+ const res = await handleClaudeProxyRequest(req, fetchImpl);
+ expect(res.status).toBe(401);
+ expect(await res.json()).toEqual(errorBody);
+ });
+
+ it('returns 504 when the upstream call times out', async () => {
+ const timeoutError = new Error('The operation was aborted');
+ timeoutError.name = 'TimeoutError';
+ const fetchImpl = vi.fn().mockRejectedValue(timeoutError);
+ const req = makeRequest(validBody(), { headers: { 'x-forwarded-for': uniqueIp() } });
+ const res = await handleClaudeProxyRequest(req, fetchImpl);
+ expect(res.status).toBe(504);
+ });
+
+ it('returns 502 when the upstream call fails for a non-timeout reason', async () => {
+ const fetchImpl = vi.fn().mockRejectedValue(new Error('network down'));
+ const req = makeRequest(validBody(), { headers: { 'x-forwarded-for': uniqueIp() } });
+ const res = await handleClaudeProxyRequest(req, fetchImpl);
+ expect(res.status).toBe(502);
+ });
+
+ it(
+ "preserves the current client's own request history through a bounded eviction instead of " +
+ 'wiping the whole map (CodeRabbit, CWE-770, PR #301)',
+ async () => {
+ // QNBS-v3: a fresh Response per call — a Response body can only be read once, and this mock
+ // is invoked thousands of times, unlike the single-shot mockResolvedValue used elsewhere.
+ const fetchImpl = vi
+ .fn()
+ .mockImplementation(
+ async () => new Response(JSON.stringify({ content: [] }), { status: 200 }),
+ );
+
+ // QNBS-v3: exactly RATE_LIMIT_MAX_TRACKED_CLIENTS (5000) distinct spoofed clients fills the
+ // map to the eviction threshold (> 5000, not >=) without tripping it yet.
+ for (let i = 0; i < 5000; i++) {
+ const spoofedIp = `10.${Math.floor(i / 65025)}.${Math.floor(i / 255) % 255}.${i % 255}`;
+ const req = makeRequest(validBody(), { headers: { 'x-forwarded-for': spoofedIp } });
+ await handleClaudeProxyRequest(req, fetchImpl);
+ }
+
+ // QNBS-v3: the attacker's own 21 requests each push the map past the threshold (it never
+ // drains below 5000, since the attacker's own key already exists after the first call), so
+ // every one of these calls re-runs the eviction path with the attacker as the *current*
+ // client — exactly the condition the old `.clear()` bug mishandled, since it wiped the
+ // current caller's own just-recorded entry along with everyone else's.
+ const attackerIp = '198.51.100.1';
+ let lastRes: Response | undefined;
+ for (let i = 0; i < 21; i++) {
+ const req = makeRequest(validBody(), { headers: { 'x-forwarded-for': attackerIp } });
+ lastRes = await handleClaudeProxyRequest(req, fetchImpl);
+ }
+ // QNBS-v3: the 21st request must still trip the limit — proving the attacker's own history
+ // survived 20 rounds of eviction pressure instead of being reset by it.
+ expect(lastRes?.status).toBe(429);
+ },
+ );
+});
diff --git a/tests/unit/api/claudeProxyEntrypoints.test.ts b/tests/unit/api/claudeProxyEntrypoints.test.ts
new file mode 100644
index 000000000..ac26b10f6
--- /dev/null
+++ b/tests/unit/api/claudeProxyEntrypoints.test.ts
@@ -0,0 +1,32 @@
+import { describe, expect, it } from 'vitest';
+import handler, { config } from '../../../api/claude-proxy';
+import { onRequest } from '../../../functions/api/claude-proxy';
+
+// QNBS-v3 (ADR-0016 Track B): both platform entry points are thin wrappers around the shared
+// api/_shared/claudeProxyCore.ts relay (already covered exhaustively in claudeProxyCore.test.ts) —
+// these tests only confirm each wrapper actually delegates to it, using the cheapest observable
+// signal (a validation-failure request needs no network mocking to produce a real 400/403 response).
+
+describe('api/claude-proxy.ts (Vercel Edge Function entry point)', () => {
+ it('declares the edge runtime', () => {
+ expect(config).toEqual({ runtime: 'edge' });
+ });
+
+ it('delegates to the shared relay core', async () => {
+ const req = new Request('https://worldscript-studio.vercel.app/api/claude-proxy', {
+ method: 'GET',
+ });
+ const res = await handler(req);
+ expect(res.status).toBe(405);
+ });
+});
+
+describe('functions/api/claude-proxy.ts (Cloudflare Pages Function entry point)', () => {
+ it('delegates to the shared relay core', async () => {
+ const req = new Request('https://worldscript-studio.pages.dev/api/claude-proxy', {
+ method: 'GET',
+ });
+ const res = await onRequest({ request: req });
+ expect(res.status).toBe(405);
+ });
+});
diff --git a/tests/unit/deployTarget.test.ts b/tests/unit/deployTarget.test.ts
new file mode 100644
index 000000000..67b48ecf5
--- /dev/null
+++ b/tests/unit/deployTarget.test.ts
@@ -0,0 +1,29 @@
+import { afterEach, beforeEach, describe, expect, it } from 'vitest';
+import { isServerlessProxyCapable } from '../../services/deployTarget';
+
+describe('isServerlessProxyCapable', () => {
+ const originalBaseUrl = import.meta.env.BASE_URL;
+
+ beforeEach(() => {
+ import.meta.env.BASE_URL = originalBaseUrl;
+ });
+
+ afterEach(() => {
+ import.meta.env.BASE_URL = originalBaseUrl;
+ });
+
+ it('is true for the edge (Vercel/Cloudflare) root-domain base path', () => {
+ import.meta.env.BASE_URL = '/';
+ expect(isServerlessProxyCapable()).toBe(true);
+ });
+
+ it('is false for the GitHub Pages project-page base path', () => {
+ import.meta.env.BASE_URL = '/WorldScript-Studio/';
+ expect(isServerlessProxyCapable()).toBe(false);
+ });
+
+ it('is true for a custom VITE_BASE deployment (not the GitHub Pages path)', () => {
+ import.meta.env.BASE_URL = '/custom-base/';
+ expect(isServerlessProxyCapable()).toBe(true);
+ });
+});
diff --git a/tests/unit/settings/AiProviderCard.test.tsx b/tests/unit/settings/AiProviderCard.test.tsx
index a5739cd45..a7c0eca5a 100644
--- a/tests/unit/settings/AiProviderCard.test.tsx
+++ b/tests/unit/settings/AiProviderCard.test.tsx
@@ -384,21 +384,25 @@ describe('AiProviderCard — grok provider', () => {
});
});
-// QNBS-v3 (ADR-0016 Track A): desktop bypasses Anthropic's CORS restriction natively — the
-// warning-only block becomes a real key input there, while web/PWA keeps the warning.
-describe('AiProviderCard — anthropic provider (ADR-0016 Track A)', () => {
+// QNBS-v3 (ADR-0016): desktop (Track A, native) and proxy-capable web (Track B, api/claude-proxy —
+// Vercel/Cloudflare Pages) both render a real key input; only GitHub Pages (neither capability,
+// static-only) keeps the CORS/proxy-unavailable warning block.
+describe('AiProviderCard — anthropic provider (ADR-0016)', () => {
const anthropicAdvancedAi = {
...mockAdvancedAi,
provider: 'anthropic' as const,
model: 'claude-haiku-4-5' as const,
};
+ const originalBaseUrl = import.meta.env.BASE_URL;
afterEach(() => {
setDesktopRuntime(false);
+ import.meta.env.BASE_URL = originalBaseUrl;
});
- it('web: shows the CORS warning block, no key input', () => {
+ it('GitHub Pages: shows the unavailable warning block, no key input', () => {
setDesktopRuntime(false);
+ import.meta.env.BASE_URL = '/WorldScript-Studio/';
render(
{
expect(screen.queryByLabelText('settings.ai.anthropicKey')).toBeNull();
});
- it('desktop: shows the real key input and model selector, no warning block', () => {
+ it('proxy-capable web: shows the real key input, model selector, and proxy note', () => {
+ setDesktopRuntime(false);
+ import.meta.env.BASE_URL = '/';
+ render(
+ ,
+ );
+ expect(screen.getByLabelText('settings.ai.anthropicKey')).toBeTruthy();
+ expect(screen.getByText('Claude Haiku 4.5')).toBeTruthy();
+ expect(screen.getByText('settings.ai.anthropicProxyNote')).toBeTruthy();
+ expect(screen.queryByText('settings.ai.corsRestriction')).toBeNull();
+ });
+
+ it('proxy-capable web: saves the entered key via storageService.saveApiKey("anthropic", ...)', async () => {
+ setDesktopRuntime(false);
+ import.meta.env.BASE_URL = '/';
+ const user = userEvent.setup();
+ render(
+ ,
+ );
+ const input = screen.getByLabelText('settings.ai.anthropicKey');
+ await user.type(input, 'sk-ant-test-key');
+ const saveButton = screen.getByText('settings.ai.save');
+ await user.click(saveButton);
+ await waitFor(() => {
+ expect(storageService.saveApiKey).toHaveBeenCalledWith('anthropic', 'sk-ant-test-key');
+ });
+ });
+
+ it('desktop: shows the real key input and model selector, no warning block, no proxy note', () => {
setDesktopRuntime(true);
render(
{
expect(screen.getByLabelText('settings.ai.anthropicKey')).toBeTruthy();
expect(screen.getByText('Claude Haiku 4.5')).toBeTruthy();
expect(screen.queryByText('settings.ai.corsRestriction')).toBeNull();
+ expect(screen.queryByText('settings.ai.anthropicProxyNote')).toBeNull();
});
it('desktop: saves the entered key via storageService.saveApiKey("anthropic", ...)', async () => {